mirror of
https://github.com/bitwarden/browser.git
synced 2024-11-22 11:45:59 +01: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:
parent
cc88826be4
commit
166269520c
@ -1,5 +1,5 @@
|
||||
import { mock } from "jest-mock-extended";
|
||||
import { Observable } from "rxjs";
|
||||
import { Observable, map } from "rxjs";
|
||||
|
||||
import {
|
||||
GlobalState,
|
||||
@ -99,11 +99,14 @@ export class FakeSingleUserStateProvider implements SingleUserStateProvider {
|
||||
}
|
||||
|
||||
export class FakeActiveUserStateProvider implements ActiveUserStateProvider {
|
||||
activeUserId$: Observable<UserId>;
|
||||
establishedMocks: 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> {
|
||||
let result = this.states.get(keyDefinition.fullName);
|
||||
@ -137,6 +140,21 @@ export class FakeActiveUserStateProvider implements ActiveUserStateProvider {
|
||||
}
|
||||
|
||||
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> {
|
||||
return this.activeUser.get(keyDefinition);
|
||||
}
|
||||
@ -163,6 +181,7 @@ export class FakeStateProvider implements StateProvider {
|
||||
singleUser: FakeSingleUserStateProvider = new FakeSingleUserStateProvider();
|
||||
activeUser: FakeActiveUserStateProvider = new FakeActiveUserStateProvider(this.accountService);
|
||||
derived: FakeDerivedStateProvider = new FakeDerivedStateProvider();
|
||||
activeUserId$: Observable<UserId> = this.activeUser.activeUserId$;
|
||||
}
|
||||
|
||||
export class FakeDerivedStateProvider implements DerivedStateProvider {
|
||||
|
@ -182,13 +182,13 @@ export class FakeActiveUserState<T> implements ActiveUserState<T> {
|
||||
}
|
||||
const newState = configureState(current, combinedDependencies);
|
||||
this.stateSubject.next([this.userId, newState]);
|
||||
this.nextMock(this.userId, newState);
|
||||
this.nextMock([this.userId, newState]);
|
||||
return newState;
|
||||
}
|
||||
|
||||
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;
|
||||
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 { UserId } from "../../../types/guid";
|
||||
import {
|
||||
AbstractMemoryStorageService,
|
||||
AbstractStorageService,
|
||||
@ -14,11 +17,15 @@ import { DefaultActiveUserState } from "./default-active-user-state";
|
||||
export class DefaultActiveUserStateProvider implements ActiveUserStateProvider {
|
||||
private cache: Record<string, ActiveUserState<unknown>> = {};
|
||||
|
||||
activeUserId$: Observable<UserId | undefined>;
|
||||
|
||||
constructor(
|
||||
protected readonly accountService: AccountService,
|
||||
protected readonly memoryStorage: AbstractMemoryStorageService & ObservableStorageService,
|
||||
protected readonly diskStorage: AbstractStorageService & ObservableStorageService,
|
||||
) {}
|
||||
) {
|
||||
this.activeUserId$ = this.accountService.activeAccount$.pipe(map((account) => account?.id));
|
||||
}
|
||||
|
||||
get<T>(keyDefinition: KeyDefinition<T>): ActiveUserState<T> {
|
||||
const cacheKey = this.buildCacheKey(keyDefinition);
|
||||
|
@ -21,9 +21,10 @@ describe("DefaultStateProvider", () => {
|
||||
let globalStateProvider: FakeGlobalStateProvider;
|
||||
let derivedStateProvider: FakeDerivedStateProvider;
|
||||
let accountService: FakeAccountService;
|
||||
const userId = "fakeUserId" as UserId;
|
||||
|
||||
beforeEach(() => {
|
||||
accountService = mockAccountServiceWith("fakeUserId" as UserId);
|
||||
accountService = mockAccountServiceWith(userId);
|
||||
activeUserStateProvider = new FakeActiveUserStateProvider(accountService);
|
||||
singleUserStateProvider = new FakeSingleUserStateProvider();
|
||||
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", () => {
|
||||
const keyDefinition = new KeyDefinition(new StateDefinition("test", "disk"), "test", {
|
||||
deserializer: () => null,
|
||||
|
@ -1,20 +1,41 @@
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
import { UserId } from "../../../types/guid";
|
||||
import { DerivedStateDependencies } from "../../../types/state";
|
||||
import { DeriveDefinition } from "../derive-definition";
|
||||
import { DerivedState } from "../derived-state";
|
||||
import { DerivedStateProvider } from "../derived-state.provider";
|
||||
import { GlobalStateProvider } from "../global-state.provider";
|
||||
import { KeyDefinition } from "../key-definition";
|
||||
import { StateProvider } from "../state.provider";
|
||||
import { ActiveUserStateProvider, SingleUserStateProvider } from "../user-state.provider";
|
||||
|
||||
export class DefaultStateProvider implements StateProvider {
|
||||
activeUserId$: Observable<UserId>;
|
||||
constructor(
|
||||
private readonly activeUserStateProvider: ActiveUserStateProvider,
|
||||
private readonly singleUserStateProvider: SingleUserStateProvider,
|
||||
private readonly globalStateProvider: GlobalStateProvider,
|
||||
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"] =
|
||||
this.activeUserStateProvider.get.bind(this.activeUserStateProvider);
|
||||
|
@ -17,6 +17,23 @@ import { ActiveUserStateProvider, SingleUserStateProvider } from "./user-state.p
|
||||
* and {@link GlobalStateProvider}.
|
||||
*/
|
||||
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} */
|
||||
getActive: <T>(keyDefinition: KeyDefinition<T>) => ActiveUserState<T>;
|
||||
/** @see{@link SingleUserStateProvider.get} */
|
||||
|
@ -1,3 +1,5 @@
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
import { UserId } from "../../types/guid";
|
||||
|
||||
import { KeyDefinition } from "./key-definition";
|
||||
@ -18,6 +20,10 @@ export abstract class SingleUserStateProvider {
|
||||
* to the currently active user
|
||||
*/
|
||||
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
|
||||
* that the emitted values always represents the state for the currently active user.
|
||||
|
Loading…
Reference in New Issue
Block a user