mirror of
https://github.com/bitwarden/browser
synced 2025-12-11 13:53:34 +00:00
Allow common get and set operations from state providers (#7824)
* Allow common get and set operations from state providers * Use finnish endings for observables
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
import { mock } from "jest-mock-extended";
|
import { mock } from "jest-mock-extended";
|
||||||
import { Observable } from "rxjs";
|
import { Observable, map } from "rxjs";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
GlobalState,
|
GlobalState,
|
||||||
@@ -99,11 +99,14 @@ export class FakeSingleUserStateProvider implements SingleUserStateProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class FakeActiveUserStateProvider implements ActiveUserStateProvider {
|
export class FakeActiveUserStateProvider implements ActiveUserStateProvider {
|
||||||
|
activeUserId$: Observable<UserId>;
|
||||||
establishedMocks: Map<string, FakeActiveUserState<unknown>> = new Map();
|
establishedMocks: Map<string, FakeActiveUserState<unknown>> = new Map();
|
||||||
|
|
||||||
states: Map<string, FakeActiveUserState<unknown>> = new Map();
|
states: Map<string, FakeActiveUserState<unknown>> = new Map();
|
||||||
|
|
||||||
constructor(public accountService: FakeAccountService) {}
|
constructor(public accountService: FakeAccountService) {
|
||||||
|
this.activeUserId$ = accountService.activeAccountSubject.asObservable().pipe(map((a) => a.id));
|
||||||
|
}
|
||||||
|
|
||||||
get<T>(keyDefinition: KeyDefinition<T>): ActiveUserState<T> {
|
get<T>(keyDefinition: KeyDefinition<T>): ActiveUserState<T> {
|
||||||
let result = this.states.get(keyDefinition.fullName);
|
let result = this.states.get(keyDefinition.fullName);
|
||||||
@@ -137,6 +140,21 @@ export class FakeActiveUserStateProvider implements ActiveUserStateProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class FakeStateProvider implements StateProvider {
|
export class FakeStateProvider implements StateProvider {
|
||||||
|
getUserState$<T>(keyDefinition: KeyDefinition<T>, userId?: UserId): Observable<T> {
|
||||||
|
if (userId) {
|
||||||
|
return this.getUser<T>(userId, keyDefinition).state$;
|
||||||
|
}
|
||||||
|
return this.getActive<T>(keyDefinition).state$;
|
||||||
|
}
|
||||||
|
|
||||||
|
async setUserState<T>(keyDefinition: KeyDefinition<T>, value: T, userId?: UserId): Promise<void> {
|
||||||
|
if (userId) {
|
||||||
|
await this.getUser(userId, keyDefinition).update(() => value);
|
||||||
|
} else {
|
||||||
|
await this.getActive(keyDefinition).update(() => value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
getActive<T>(keyDefinition: KeyDefinition<T>): ActiveUserState<T> {
|
getActive<T>(keyDefinition: KeyDefinition<T>): ActiveUserState<T> {
|
||||||
return this.activeUser.get(keyDefinition);
|
return this.activeUser.get(keyDefinition);
|
||||||
}
|
}
|
||||||
@@ -163,6 +181,7 @@ export class FakeStateProvider implements StateProvider {
|
|||||||
singleUser: FakeSingleUserStateProvider = new FakeSingleUserStateProvider();
|
singleUser: FakeSingleUserStateProvider = new FakeSingleUserStateProvider();
|
||||||
activeUser: FakeActiveUserStateProvider = new FakeActiveUserStateProvider(this.accountService);
|
activeUser: FakeActiveUserStateProvider = new FakeActiveUserStateProvider(this.accountService);
|
||||||
derived: FakeDerivedStateProvider = new FakeDerivedStateProvider();
|
derived: FakeDerivedStateProvider = new FakeDerivedStateProvider();
|
||||||
|
activeUserId$: Observable<UserId> = this.activeUser.activeUserId$;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class FakeDerivedStateProvider implements DerivedStateProvider {
|
export class FakeDerivedStateProvider implements DerivedStateProvider {
|
||||||
|
|||||||
@@ -182,13 +182,13 @@ export class FakeActiveUserState<T> implements ActiveUserState<T> {
|
|||||||
}
|
}
|
||||||
const newState = configureState(current, combinedDependencies);
|
const newState = configureState(current, combinedDependencies);
|
||||||
this.stateSubject.next([this.userId, newState]);
|
this.stateSubject.next([this.userId, newState]);
|
||||||
this.nextMock(this.userId, newState);
|
this.nextMock([this.userId, newState]);
|
||||||
return newState;
|
return newState;
|
||||||
}
|
}
|
||||||
|
|
||||||
updateMock = this.update as jest.MockedFunction<typeof this.update>;
|
updateMock = this.update as jest.MockedFunction<typeof this.update>;
|
||||||
|
|
||||||
nextMock = jest.fn<void, [UserId, T]>();
|
nextMock = jest.fn<void, [[UserId, T]]>();
|
||||||
|
|
||||||
private _keyDefinition: KeyDefinition<T> | null = null;
|
private _keyDefinition: KeyDefinition<T> | null = null;
|
||||||
get keyDefinition() {
|
get keyDefinition() {
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import { mock } from "jest-mock-extended";
|
||||||
|
|
||||||
|
import { mockAccountServiceWith, trackEmissions } from "../../../../spec";
|
||||||
|
import { AuthenticationStatus } from "../../../auth/enums/authentication-status";
|
||||||
|
import { UserId } from "../../../types/guid";
|
||||||
|
import {
|
||||||
|
AbstractMemoryStorageService,
|
||||||
|
AbstractStorageService,
|
||||||
|
ObservableStorageService,
|
||||||
|
} from "../../abstractions/storage.service";
|
||||||
|
|
||||||
|
import { DefaultActiveUserStateProvider } from "./default-active-user-state.provider";
|
||||||
|
|
||||||
|
describe("DefaultActiveUserStateProvider", () => {
|
||||||
|
const memoryStorage = mock<AbstractMemoryStorageService & ObservableStorageService>();
|
||||||
|
const diskStorage = mock<AbstractStorageService & ObservableStorageService>();
|
||||||
|
const userId = "userId" as UserId;
|
||||||
|
const accountInfo = {
|
||||||
|
id: userId,
|
||||||
|
name: "name",
|
||||||
|
email: "email",
|
||||||
|
status: AuthenticationStatus.Locked,
|
||||||
|
};
|
||||||
|
const accountService = mockAccountServiceWith(userId, accountInfo);
|
||||||
|
let sut: DefaultActiveUserStateProvider;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
sut = new DefaultActiveUserStateProvider(accountService, memoryStorage, diskStorage);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.resetAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should track the active User id from account service", () => {
|
||||||
|
const emissions = trackEmissions(sut.activeUserId$);
|
||||||
|
|
||||||
|
accountService.activeAccountSubject.next(undefined);
|
||||||
|
accountService.activeAccountSubject.next(accountInfo);
|
||||||
|
|
||||||
|
expect(emissions).toEqual([userId, undefined, userId]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,4 +1,7 @@
|
|||||||
|
import { Observable, map } from "rxjs";
|
||||||
|
|
||||||
import { AccountService } from "../../../auth/abstractions/account.service";
|
import { AccountService } from "../../../auth/abstractions/account.service";
|
||||||
|
import { UserId } from "../../../types/guid";
|
||||||
import {
|
import {
|
||||||
AbstractMemoryStorageService,
|
AbstractMemoryStorageService,
|
||||||
AbstractStorageService,
|
AbstractStorageService,
|
||||||
@@ -14,11 +17,15 @@ import { DefaultActiveUserState } from "./default-active-user-state";
|
|||||||
export class DefaultActiveUserStateProvider implements ActiveUserStateProvider {
|
export class DefaultActiveUserStateProvider implements ActiveUserStateProvider {
|
||||||
private cache: Record<string, ActiveUserState<unknown>> = {};
|
private cache: Record<string, ActiveUserState<unknown>> = {};
|
||||||
|
|
||||||
|
activeUserId$: Observable<UserId | undefined>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected readonly accountService: AccountService,
|
protected readonly accountService: AccountService,
|
||||||
protected readonly memoryStorage: AbstractMemoryStorageService & ObservableStorageService,
|
protected readonly memoryStorage: AbstractMemoryStorageService & ObservableStorageService,
|
||||||
protected readonly diskStorage: AbstractStorageService & ObservableStorageService,
|
protected readonly diskStorage: AbstractStorageService & ObservableStorageService,
|
||||||
) {}
|
) {
|
||||||
|
this.activeUserId$ = this.accountService.activeAccount$.pipe(map((account) => account?.id));
|
||||||
|
}
|
||||||
|
|
||||||
get<T>(keyDefinition: KeyDefinition<T>): ActiveUserState<T> {
|
get<T>(keyDefinition: KeyDefinition<T>): ActiveUserState<T> {
|
||||||
const cacheKey = this.buildCacheKey(keyDefinition);
|
const cacheKey = this.buildCacheKey(keyDefinition);
|
||||||
|
|||||||
@@ -21,9 +21,10 @@ describe("DefaultStateProvider", () => {
|
|||||||
let globalStateProvider: FakeGlobalStateProvider;
|
let globalStateProvider: FakeGlobalStateProvider;
|
||||||
let derivedStateProvider: FakeDerivedStateProvider;
|
let derivedStateProvider: FakeDerivedStateProvider;
|
||||||
let accountService: FakeAccountService;
|
let accountService: FakeAccountService;
|
||||||
|
const userId = "fakeUserId" as UserId;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
accountService = mockAccountServiceWith("fakeUserId" as UserId);
|
accountService = mockAccountServiceWith(userId);
|
||||||
activeUserStateProvider = new FakeActiveUserStateProvider(accountService);
|
activeUserStateProvider = new FakeActiveUserStateProvider(accountService);
|
||||||
singleUserStateProvider = new FakeSingleUserStateProvider();
|
singleUserStateProvider = new FakeSingleUserStateProvider();
|
||||||
globalStateProvider = new FakeGlobalStateProvider();
|
globalStateProvider = new FakeGlobalStateProvider();
|
||||||
@@ -36,6 +37,74 @@ describe("DefaultStateProvider", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("activeUserId$", () => {
|
||||||
|
it("should track the active User id from active user state provider", () => {
|
||||||
|
expect(sut.activeUserId$).toBe(activeUserStateProvider.activeUserId$);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getUserState$", () => {
|
||||||
|
const keyDefinition = new KeyDefinition<string>(new StateDefinition("test", "disk"), "test", {
|
||||||
|
deserializer: (s) => s,
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should get the state for the active user if no userId is provided", () => {
|
||||||
|
const state = sut.getUserState$(keyDefinition);
|
||||||
|
expect(state).toBe(activeUserStateProvider.get(keyDefinition).state$);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not return state for a single user if no userId is provided", () => {
|
||||||
|
const state = sut.getUserState$(keyDefinition);
|
||||||
|
expect(state).not.toBe(singleUserStateProvider.get(userId, keyDefinition).state$);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should get the state for the provided userId", () => {
|
||||||
|
const userId = "user" as UserId;
|
||||||
|
const state = sut.getUserState$(keyDefinition, userId);
|
||||||
|
expect(state).toBe(singleUserStateProvider.get(userId, keyDefinition).state$);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not get the active user state if userId is provided", () => {
|
||||||
|
const userId = "user" as UserId;
|
||||||
|
const state = sut.getUserState$(keyDefinition, userId);
|
||||||
|
expect(state).not.toBe(activeUserStateProvider.get(keyDefinition).state$);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("setUserState", () => {
|
||||||
|
const keyDefinition = new KeyDefinition<string>(new StateDefinition("test", "disk"), "test", {
|
||||||
|
deserializer: (s) => s,
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should set the state for the active user if no userId is provided", async () => {
|
||||||
|
const value = "value";
|
||||||
|
await sut.setUserState(keyDefinition, value);
|
||||||
|
const state = activeUserStateProvider.getFake(keyDefinition);
|
||||||
|
expect(state.nextMock).toHaveBeenCalledWith([expect.any(String), value]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not set state for a single user if no userId is provided", async () => {
|
||||||
|
const value = "value";
|
||||||
|
await sut.setUserState(keyDefinition, value);
|
||||||
|
const state = singleUserStateProvider.getFake(userId, keyDefinition);
|
||||||
|
expect(state.nextMock).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should set the state for the provided userId", async () => {
|
||||||
|
const value = "value";
|
||||||
|
await sut.setUserState(keyDefinition, value, userId);
|
||||||
|
const state = singleUserStateProvider.getFake(userId, keyDefinition);
|
||||||
|
expect(state.nextMock).toHaveBeenCalledWith(value);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not set the active user state if userId is provided", async () => {
|
||||||
|
const value = "value";
|
||||||
|
await sut.setUserState(keyDefinition, value, userId);
|
||||||
|
const state = activeUserStateProvider.getFake(keyDefinition);
|
||||||
|
expect(state.nextMock).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("should bind the activeUserStateProvider", () => {
|
it("should bind the activeUserStateProvider", () => {
|
||||||
const keyDefinition = new KeyDefinition(new StateDefinition("test", "disk"), "test", {
|
const keyDefinition = new KeyDefinition(new StateDefinition("test", "disk"), "test", {
|
||||||
deserializer: () => null,
|
deserializer: () => null,
|
||||||
|
|||||||
@@ -1,20 +1,41 @@
|
|||||||
import { Observable } from "rxjs";
|
import { Observable } from "rxjs";
|
||||||
|
|
||||||
|
import { UserId } from "../../../types/guid";
|
||||||
import { DerivedStateDependencies } from "../../../types/state";
|
import { DerivedStateDependencies } from "../../../types/state";
|
||||||
import { DeriveDefinition } from "../derive-definition";
|
import { DeriveDefinition } from "../derive-definition";
|
||||||
import { DerivedState } from "../derived-state";
|
import { DerivedState } from "../derived-state";
|
||||||
import { DerivedStateProvider } from "../derived-state.provider";
|
import { DerivedStateProvider } from "../derived-state.provider";
|
||||||
import { GlobalStateProvider } from "../global-state.provider";
|
import { GlobalStateProvider } from "../global-state.provider";
|
||||||
|
import { KeyDefinition } from "../key-definition";
|
||||||
import { StateProvider } from "../state.provider";
|
import { StateProvider } from "../state.provider";
|
||||||
import { ActiveUserStateProvider, SingleUserStateProvider } from "../user-state.provider";
|
import { ActiveUserStateProvider, SingleUserStateProvider } from "../user-state.provider";
|
||||||
|
|
||||||
export class DefaultStateProvider implements StateProvider {
|
export class DefaultStateProvider implements StateProvider {
|
||||||
|
activeUserId$: Observable<UserId>;
|
||||||
constructor(
|
constructor(
|
||||||
private readonly activeUserStateProvider: ActiveUserStateProvider,
|
private readonly activeUserStateProvider: ActiveUserStateProvider,
|
||||||
private readonly singleUserStateProvider: SingleUserStateProvider,
|
private readonly singleUserStateProvider: SingleUserStateProvider,
|
||||||
private readonly globalStateProvider: GlobalStateProvider,
|
private readonly globalStateProvider: GlobalStateProvider,
|
||||||
private readonly derivedStateProvider: DerivedStateProvider,
|
private readonly derivedStateProvider: DerivedStateProvider,
|
||||||
) {}
|
) {
|
||||||
|
this.activeUserId$ = this.activeUserStateProvider.activeUserId$;
|
||||||
|
}
|
||||||
|
|
||||||
|
getUserState$<T>(keyDefinition: KeyDefinition<T>, userId?: UserId): Observable<T> {
|
||||||
|
if (userId) {
|
||||||
|
return this.getUser<T>(userId, keyDefinition).state$;
|
||||||
|
} else {
|
||||||
|
return this.getActive<T>(keyDefinition).state$;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async setUserState<T>(keyDefinition: KeyDefinition<T>, value: T, userId?: UserId): Promise<void> {
|
||||||
|
if (userId) {
|
||||||
|
await this.getUser<T>(userId, keyDefinition).update(() => value);
|
||||||
|
} else {
|
||||||
|
await this.getActive<T>(keyDefinition).update(() => value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
getActive: InstanceType<typeof ActiveUserStateProvider>["get"] =
|
getActive: InstanceType<typeof ActiveUserStateProvider>["get"] =
|
||||||
this.activeUserStateProvider.get.bind(this.activeUserStateProvider);
|
this.activeUserStateProvider.get.bind(this.activeUserStateProvider);
|
||||||
|
|||||||
@@ -17,6 +17,23 @@ import { ActiveUserStateProvider, SingleUserStateProvider } from "./user-state.p
|
|||||||
* and {@link GlobalStateProvider}.
|
* and {@link GlobalStateProvider}.
|
||||||
*/
|
*/
|
||||||
export abstract class StateProvider {
|
export abstract class StateProvider {
|
||||||
|
/** @see{@link ActiveUserState.activeUserId$} */
|
||||||
|
activeUserId$: Observable<UserId | undefined>;
|
||||||
|
/**
|
||||||
|
* Gets a state observable for a given key and userId.
|
||||||
|
*
|
||||||
|
* @param keyDefinition - The key definition for the state you want to get.
|
||||||
|
* @param userId - The userId for which you want the state for. If not provided, the state for the currently active user will be returned.
|
||||||
|
*/
|
||||||
|
getUserState$: <T>(keyDefinition: KeyDefinition<T>, userId?: UserId) => Observable<T>;
|
||||||
|
/**
|
||||||
|
* Sets the state for a given key and userId.
|
||||||
|
*
|
||||||
|
* @param keyDefinition - The key definition for the state you want to set.
|
||||||
|
* @param value - The value to set the state to.
|
||||||
|
* @param userId - The userId for which you want to set the state for. If not provided, the state for the currently active user will be set.
|
||||||
|
*/
|
||||||
|
setUserState: <T>(keyDefinition: KeyDefinition<T>, value: T, userId?: UserId) => Promise<void>;
|
||||||
/** @see{@link ActiveUserStateProvider.get} */
|
/** @see{@link ActiveUserStateProvider.get} */
|
||||||
getActive: <T>(keyDefinition: KeyDefinition<T>) => ActiveUserState<T>;
|
getActive: <T>(keyDefinition: KeyDefinition<T>) => ActiveUserState<T>;
|
||||||
/** @see{@link SingleUserStateProvider.get} */
|
/** @see{@link SingleUserStateProvider.get} */
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { Observable } from "rxjs";
|
||||||
|
|
||||||
import { UserId } from "../../types/guid";
|
import { UserId } from "../../types/guid";
|
||||||
|
|
||||||
import { KeyDefinition } from "./key-definition";
|
import { KeyDefinition } from "./key-definition";
|
||||||
@@ -18,6 +20,10 @@ export abstract class SingleUserStateProvider {
|
|||||||
* to the currently active user
|
* to the currently active user
|
||||||
*/
|
*/
|
||||||
export abstract class ActiveUserStateProvider {
|
export abstract class ActiveUserStateProvider {
|
||||||
|
/**
|
||||||
|
* Convenience re-emission of active user ID from {@link AccountService.activeAccount$}
|
||||||
|
*/
|
||||||
|
activeUserId$: Observable<UserId | undefined>;
|
||||||
/**
|
/**
|
||||||
* Gets a {@link ActiveUserState} scoped to the given {@link KeyDefinition}, but updates when active user changes such
|
* Gets a {@link ActiveUserState} scoped to the given {@link KeyDefinition}, but updates when active user changes such
|
||||||
* that the emitted values always represents the state for the currently active user.
|
* that the emitted values always represents the state for the currently active user.
|
||||||
|
|||||||
Reference in New Issue
Block a user