diff --git a/libs/common/spec/test-active-user-state.ts b/libs/common/spec/test-active-user-state.ts new file mode 100644 index 00000000000..9c646e8f1c0 --- /dev/null +++ b/libs/common/spec/test-active-user-state.ts @@ -0,0 +1,33 @@ +import { BehaviorSubject, firstValueFrom } from "rxjs"; + +import { ActiveUserState } from "../src/platform/interfaces/active-user-state"; + +export class TestUserState implements ActiveUserState { + private _state$: BehaviorSubject; + get state$() { + return this._state$.asObservable(); + } + + constructor(initialValue: T) { + this._state$ = new BehaviorSubject(initialValue); + } + + getFromState = jest.fn(); + update = jest.fn().mockImplementation(this.minimalUpdate); + createDerived = jest.fn(); + + next(next: T) { + this._state$.next(next); + } + + complete() { + this._state$.complete(); + } + + private async minimalUpdate(configureState: (state: T) => T) { + const currentState = await firstValueFrom(this.state$); + const newState = configureState(currentState); + this._state$.next(newState); + return newState; + } +} diff --git a/libs/common/src/auth/services/account.service.ts b/libs/common/src/auth/services/account.service.ts index 557c38c2f75..d8407ab1151 100644 --- a/libs/common/src/auth/services/account.service.ts +++ b/libs/common/src/auth/services/account.service.ts @@ -2,10 +2,9 @@ import { BehaviorSubject, Subject, combineLatestWith, - map, distinctUntilChanged, + map, share, - tap, } from "rxjs"; import { AccountInfo, InternalAccountService } from "../../auth/abstractions/account.service"; @@ -24,7 +23,6 @@ export class AccountServiceImplementation implements InternalAccountService { activeAccount$ = this.activeAccountId.pipe( combineLatestWith(this.accounts$), map(([id, accounts]) => (id ? { id, ...accounts[id] } : undefined)), - tap((stuff) => console.log("stuff", stuff)), distinctUntilChanged(), share() ); diff --git a/libs/common/src/platform/services/default-active-user-state.provider.spec.ts b/libs/common/src/platform/services/default-active-user-state.provider.spec.ts index ed0e12cb946..5a8b6378c83 100644 --- a/libs/common/src/platform/services/default-active-user-state.provider.spec.ts +++ b/libs/common/src/platform/services/default-active-user-state.provider.spec.ts @@ -83,7 +83,10 @@ describe("DefaultStateProvider", () => { await new Promise((resolve) => setTimeout(resolve, 10)); // Service does an update - await fakeDomainState.update((state) => state.array.push("value3")); + await fakeDomainState.update((state) => { + state.array.push("value3"); + return state; + }); await new Promise((resolve) => setTimeout(resolve, 10)); subscription.unsubscribe(); 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 8dc158ed442..c82d6f8b48d 100644 --- a/libs/common/src/vault/services/folder/folder.service.spec.ts +++ b/libs/common/src/vault/services/folder/folder.service.spec.ts @@ -1,15 +1,20 @@ // eslint-disable-next-line no-restricted-imports import { Arg, Substitute, SubstituteOf } from "@fluffy-spoon/substitute"; +import { mock } from "jest-mock-extended"; import { BehaviorSubject, firstValueFrom } from "rxjs"; +import { TestUserState as TestActiveUserState } from "../../../../spec/test-active-user-state"; +import { ActiveUserStateProvider } from "../../../platform/abstractions/active-user-state.provider"; import { CryptoService } from "../../../platform/abstractions/crypto.service"; import { EncryptService } from "../../../platform/abstractions/encrypt.service"; import { I18nService } from "../../../platform/abstractions/i18n.service"; import { EncString } from "../../../platform/models/domain/enc-string"; import { ContainerService } from "../../../platform/services/container.service"; +import { DerivedActiveUserState } from "../../../platform/services/default-active-user-state.provider"; import { StateService } from "../../../platform/services/state.service"; import { CipherService } from "../../abstractions/cipher.service"; import { FolderData } from "../../models/data/folder.data"; +import { Folder } from "../../models/domain/folder"; import { FolderView } from "../../models/view/folder.view"; import { FolderService } from "../../services/folder/folder.service"; @@ -23,6 +28,11 @@ describe("Folder Service", () => { let stateService: SubstituteOf; let activeAccount: BehaviorSubject; let activeAccountUnlocked: BehaviorSubject; + const activeUserStateProvider = mock(); + let activeUserState: TestActiveUserState>; + const derivedActiveUserState = + mock, FolderView[]>>(); + let folderViews$: BehaviorSubject; beforeEach(() => { cryptoService = Substitute.for(); @@ -33,17 +43,38 @@ describe("Folder Service", () => { activeAccount = new BehaviorSubject("123"); activeAccountUnlocked = new BehaviorSubject(true); - stateService.getEncryptedFolders().resolves({ + const initialState = { "1": folderData("1", "test"), - }); + }; + stateService.getEncryptedFolders().resolves(initialState); stateService.activeAccount$.returns(activeAccount); stateService.activeAccountUnlocked$.returns(activeAccountUnlocked); (window as any).bitwardenContainerService = new ContainerService(cryptoService, encryptService); - folderService = new FolderService(cryptoService, i18nService, cipherService, stateService); + activeUserState = new TestActiveUserState({}); + activeUserState.next(initialState); + activeUserStateProvider.create.mockReturnValue(activeUserState); + activeUserState.createDerived.mockReturnValue(derivedActiveUserState); + + folderViews$ = new BehaviorSubject([]); + derivedActiveUserState.state$ = folderViews$; + + folderService = new FolderService( + cryptoService, + i18nService, + cipherService, + activeUserStateProvider, + stateService + ); }); - it("encrypt", async () => { + afterEach(() => { + jest.resetAllMocks(); + folderViews$.complete(); + activeUserState.complete(); + }); + + test("encrypt", async () => { const model = new FolderView(); model.id = "2"; model.name = "Test Folder"; @@ -63,59 +94,34 @@ describe("Folder Service", () => { }); describe("get", () => { - it("exists", async () => { + it("returns the current state", async () => { const result = await folderService.get("1"); - expect(result).toEqual({ - id: "1", - name: { - decryptedValue: [], - encryptedString: "test", - encryptionType: 0, - }, - revisionDate: null, - }); + expect(result).toEqual(folder("1", "test")); }); - it("not exists", async () => { + it("returns a Folder object", async () => { + const result = await folderService.get("1"); + + expect(result).toBeInstanceOf(Folder); + }); + + it("returns null if not found", async () => { const result = await folderService.get("2"); - expect(result).toBe(undefined); + expect(result).toBe(null); }); }); - it("upsert", async () => { + test("upsert emits new folder$ array", async () => { await folderService.upsert(folderData("2", "test 2")); expect(await firstValueFrom(folderService.folders$)).toEqual([ - { - id: "1", - name: { - decryptedValue: [], - encryptedString: "test", - encryptionType: 0, - }, - revisionDate: null, - }, - { - id: "2", - name: { - decryptedValue: [], - encryptedString: "test 2", - encryptionType: 0, - }, - revisionDate: null, - }, - ]); - - expect(await firstValueFrom(folderService.folderViews$)).toEqual([ - { id: "1", name: [], revisionDate: null }, - { id: "2", name: [], revisionDate: null }, - { id: null, name: [], revisionDate: null }, + folder("1", "test"), + folder("2", "test 2"), ]); }); - - it("replace", async () => { + test("replace", async () => { await folderService.replace({ "2": folderData("2", "test 2") }); expect(await firstValueFrom(folderService.folders$)).toEqual([ @@ -129,37 +135,25 @@ describe("Folder Service", () => { revisionDate: null, }, ]); - - expect(await firstValueFrom(folderService.folderViews$)).toEqual([ - { id: "2", name: [], revisionDate: null }, - { id: null, name: [], revisionDate: null }, - ]); }); - it("delete", async () => { + test("delete", async () => { await folderService.delete("1"); expect((await firstValueFrom(folderService.folders$)).length).toBe(0); - - expect(await firstValueFrom(folderService.folderViews$)).toEqual([ - { id: null, name: [], revisionDate: null }, - ]); }); - it("clearCache", async () => { + test("clearCache", async () => { await folderService.clearCache(); expect((await firstValueFrom(folderService.folders$)).length).toBe(1); expect((await firstValueFrom(folderService.folderViews$)).length).toBe(0); }); - it("locking should clear", async () => { - activeAccountUnlocked.next(false); - // Sleep for 100ms to avoid timing issues - await new Promise((r) => setTimeout(r, 100)); + test("clear nulls folders", async () => { + await folderService.clear(); - expect((await firstValueFrom(folderService.folders$)).length).toBe(0); - expect((await firstValueFrom(folderService.folderViews$)).length).toBe(0); + expect(await firstValueFrom(folderService.folders$)).toEqual(expect.arrayContaining([])); }); describe("clear", () => { @@ -193,10 +187,18 @@ describe("Folder Service", () => { }); function folderData(id: string, name: string) { - const data = new FolderData({} as any); - data.id = id; - data.name = name; + return Object.assign(new FolderData({} as any), { + id, + name, + revisionDate: null, + }); + } - return data; + function folder(id: string, name: string) { + return Object.assign(new Folder({} as any), { + id, + name: new EncString(name), + revisionDate: null, + }); } }); diff --git a/libs/common/src/vault/services/folder/folder.service.ts b/libs/common/src/vault/services/folder/folder.service.ts index fade328163c..2a74af53969 100644 --- a/libs/common/src/vault/services/folder/folder.service.ts +++ b/libs/common/src/vault/services/folder/folder.service.ts @@ -68,7 +68,7 @@ export class FolderService implements InternalFolderServiceAbstraction { async get(id: string): Promise { const folders = await firstValueFrom(this.folderState.state$); - return new Folder(folders[id]); + return folders[id] == null ? null : new Folder(folders[id]); } async getAllFromState(): Promise { @@ -115,6 +115,7 @@ export class FolderService implements InternalFolderServiceAbstraction { } async clear(userId?: string): Promise { + // TODO: handle clear for non-active users await this.folderState.update((_) => null); }