diff --git a/libs/common/spec/test-user-state.ts b/libs/common/spec/test-user-state.ts index 6b8c2985a23..5bafa83f74d 100644 --- a/libs/common/spec/test-user-state.ts +++ b/libs/common/spec/test-user-state.ts @@ -14,6 +14,7 @@ export class TestUserState implements UserState { getFromState = jest.fn(); update = jest.fn().mockImplementation(this.minimalUpdate); + updateFor = jest.fn(); createDerived = jest.fn(); next(next: T) { diff --git a/libs/common/src/platform/interfaces/user-state.ts b/libs/common/src/platform/interfaces/user-state.ts index 187ea332281..8963f78e7fa 100644 --- a/libs/common/src/platform/interfaces/user-state.ts +++ b/libs/common/src/platform/interfaces/user-state.ts @@ -1,5 +1,6 @@ import { Observable } from "rxjs"; +import { UserId } from "../../types/guid"; import { DerivedUserState } from "../services/default-user-state.provider"; import { DerivedStateDefinition } from "../types/derived-state-definition"; @@ -7,11 +8,24 @@ export interface UserState { readonly state$: Observable; readonly getFromState: () => Promise; /** - * + * Updates backing stores for the active user. * @param configureState function that takes the current state and returns the new state * @returns The new state */ readonly update: (configureState: (state: T) => T) => Promise; + /** + * Updates backing stores for the given userId, which may or may not be active. + * @param userId the UserId to target the update for + * @param configureState function that takes the current state for the targeted user and returns the new state + * @returns The new state + */ + readonly updateFor: (userId: UserId, configureState: (state: T) => T) => Promise; + + /** + * Creates a derives state from the current state. Derived states are always tied to the active user. + * @param derivedStateDefinition + * @returns + */ createDerived: ( derivedStateDefinition: DerivedStateDefinition ) => DerivedUserState; diff --git a/libs/common/src/platform/services/default-user-state.provider.ts b/libs/common/src/platform/services/default-user-state.provider.ts index 0c33147773b..3fa0fef3451 100644 --- a/libs/common/src/platform/services/default-user-state.provider.ts +++ b/libs/common/src/platform/services/default-user-state.provider.ts @@ -11,6 +11,7 @@ import { import { Jsonify } from "type-fest"; import { AccountService } from "../../auth/abstractions/account.service"; +import { UserId } from "../../types/guid"; import { EncryptService } from "../abstractions/encrypt.service"; import { AbstractMemoryStorageService, @@ -142,6 +143,20 @@ class DefaultUserState implements UserState { return newState; } + async updateFor(userId: UserId, configureState: (state: T) => T): Promise { + if (userId == null) { + throw new Error("Attempting to update user state, but no userId has been supplied."); + } + + const key = userKeyBuilder(userId, this.keyDefinition); + const currentStore = await this.chosenStorageLocation.get>(key); + const currentState = this.keyDefinition.deserializer(currentStore); + const newState = configureState(currentState); + await this.chosenStorageLocation.save(key, newState); + + return newState; + } + async getFromState(): Promise { const activeUser = await firstValueFrom(this.accountService.activeAccount$); if (activeUser == null || activeUser.id == null) { diff --git a/libs/common/src/vault/services/folder/folder.service.spec.ts b/libs/common/src/vault/services/folder/folder.service.spec.ts index 376ee52d7d6..1f743881671 100644 --- a/libs/common/src/vault/services/folder/folder.service.spec.ts +++ b/libs/common/src/vault/services/folder/folder.service.spec.ts @@ -3,7 +3,7 @@ import { Arg, Substitute, SubstituteOf } from "@fluffy-spoon/substitute"; import { mock } from "jest-mock-extended"; import { BehaviorSubject, firstValueFrom } from "rxjs"; -import { TestUserState } from "../../../../spec/test-active-user-state"; +import { TestUserState } from "../../../../spec/test-user-state"; import { CryptoService } from "../../../platform/abstractions/crypto.service"; import { EncryptService } from "../../../platform/abstractions/encrypt.service"; import { I18nService } from "../../../platform/abstractions/i18n.service"; @@ -123,17 +123,7 @@ describe("Folder Service", () => { test("replace", async () => { await folderService.replace({ "2": folderData("2", "test 2") }); - expect(await firstValueFrom(folderService.folders$)).toEqual([ - { - id: "2", - name: { - decryptedValue: [], - encryptedString: "test 2", - encryptionType: 0, - }, - revisionDate: null, - }, - ]); + expect(await firstValueFrom(folderService.folders$)).toEqual([folder("2", "test 2")]); }); test("delete", async () => { @@ -159,29 +149,24 @@ describe("Folder Service", () => { it("null userId", async () => { await folderService.clear(); - stateService.received(1).setEncryptedFolders(Arg.any(), Arg.any()); - - expect((await firstValueFrom(folderService.folders$)).length).toBe(0); - expect((await firstValueFrom(folderService.folderViews$)).length).toBe(0); + expect(userState.update).toHaveBeenCalled(); + expect(await firstValueFrom(folderService.folders$)).toEqual(expect.arrayContaining([])); }); - it("matching userId", async () => { - stateService.getUserId().resolves("1"); + it("active userId", async () => { await folderService.clear("1"); - stateService.received(1).setEncryptedFolders(Arg.any(), Arg.any()); - - expect((await firstValueFrom(folderService.folders$)).length).toBe(0); - expect((await firstValueFrom(folderService.folderViews$)).length).toBe(0); + expect(userState.updateFor).toHaveBeenCalled(); + const updateCallback = userState.updateFor.mock.calls[0][1]; + expect(updateCallback({ "2": folderData("2", "test") })).toEqual(expect.objectContaining({})); }); - it("missmatching userId", async () => { + it("inactive userId", async () => { await folderService.clear("12"); - stateService.received(1).setEncryptedFolders(Arg.any(), Arg.any()); - - expect((await firstValueFrom(folderService.folders$)).length).toBe(1); - expect((await firstValueFrom(folderService.folderViews$)).length).toBe(2); + expect(userState.updateFor).toHaveBeenCalled(); + const updateCallback = userState.updateFor.mock.calls[0][1]; + expect(updateCallback({ "2": folderData("2", "test") })).toEqual(expect.objectContaining({})); }); }); diff --git a/libs/common/src/vault/services/folder/folder.service.ts b/libs/common/src/vault/services/folder/folder.service.ts index 3930bd11b20..3da854faca2 100644 --- a/libs/common/src/vault/services/folder/folder.service.ts +++ b/libs/common/src/vault/services/folder/folder.service.ts @@ -8,6 +8,7 @@ import { UserState } from "../../../platform/interfaces/user-state"; import { Utils } from "../../../platform/misc/utils"; import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key"; import { DerivedUserState } from "../../../platform/services/default-user-state.provider"; +import { UserId } from "../../../types/guid"; import { CipherService } from "../../../vault/abstractions/cipher.service"; import { InternalFolderService as InternalFolderServiceAbstraction } from "../../../vault/abstractions/folder/folder.service.abstraction"; import { CipherData } from "../../../vault/models/data/cipher.data"; @@ -115,8 +116,11 @@ export class FolderService implements InternalFolderServiceAbstraction { } async clear(userId?: string): Promise { - // TODO: handle clear for non-active users - await this.folderState.update((_) => null); + if (userId == null) { + await this.folderState.update((_) => ({})); + } else { + await this.folderState.updateFor(userId as UserId, (_) => ({})); + } } async delete(id: string | string[]): Promise {