1
0
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:
Matt Gibson 2024-02-06 11:35:22 -05:00 committed by GitHub
parent cc88826be4
commit 166269520c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 189 additions and 7 deletions

View File

@ -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 {

View File

@ -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() {

View File

@ -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]);
});
});

View File

@ -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);

View File

@ -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,

View File

@ -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);

View File

@ -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} */

View File

@ -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.