diff --git a/libs/admin-console/src/common/collections/services/default-collection-vNext.service.spec.ts b/libs/admin-console/src/common/collections/services/default-collection-vNext.service.spec.ts index 11774e838e..5cb0eee6a2 100644 --- a/libs/admin-console/src/common/collections/services/default-collection-vNext.service.spec.ts +++ b/libs/admin-console/src/common/collections/services/default-collection-vNext.service.spec.ts @@ -1,4 +1,4 @@ -import { mock } from "jest-mock-extended"; +import { mock, MockProxy } from "jest-mock-extended"; import { firstValueFrom, of } from "rxjs"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; @@ -6,6 +6,7 @@ import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt. import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; +import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { ContainerService } from "@bitwarden/common/platform/services/container.service"; import { FakeStateProvider, @@ -16,7 +17,7 @@ import { import { CollectionId, OrganizationId, UserId } from "@bitwarden/common/types/guid"; import { OrgKey } from "@bitwarden/common/types/key"; -import { CollectionData } from "../models"; +import { CollectionData, CollectionView } from "../models"; import { DefaultCollectionvNextService, @@ -46,31 +47,40 @@ describe("DefaultCollectionService", () => { }); // Arrange cryptoService - orgKeys and mock decryption - const cryptoService = mockCryptoService(); + const [cryptoService, encryptService] = mockCryptoService(); + const orgKey1 = makeSymmetricCryptoKey(64, 1); + const orgKey2 = makeSymmetricCryptoKey(64, 2); cryptoService.orgKeys$.mockReturnValue( of({ - [org1]: makeSymmetricCryptoKey(), - [org2]: makeSymmetricCryptoKey(), + [org1]: orgKey1, + [org2]: orgKey2, }), ); const collectionService = new DefaultCollectionvNextService( cryptoService, - mock(), + encryptService, mockI18nService(), fakeStateProvider, ); + // Assert emitted values const result = await firstValueFrom(collectionService.decryptedCollections$(of(userId))); - expect(result.length).toBe(2); - expect(result[0]).toMatchObject({ - id: collection1.id, - name: "DECRYPTED_STRING", - }); - expect(result[1]).toMatchObject({ - id: collection2.id, - name: "DECRYPTED_STRING", - }); + // expect(result.length).toBe(2); + expect(result).toContainEqual(collectionViewFactory(collection1)); + expect(result).toContainEqual(collectionViewFactory(collection2)); + + // Assert that the correct org keys were used for each encrypted string + const collection1NameData = new EncString(collection1.name).data; + const collection2NameData = new EncString(collection2.name).data; + expect(encryptService.decryptToUtf8).toHaveBeenCalledWith( + expect.objectContaining({ data: collection1NameData }), + orgKey1, + ); + expect(encryptService.decryptToUtf8).toHaveBeenCalledWith( + expect.objectContaining({ data: collection2NameData }), + orgKey2, + ); }); it("handles null collection state", async () => { @@ -81,10 +91,10 @@ describe("DefaultCollectionService", () => { // Arrange state provider const userId = Utils.newGuid() as UserId; const fakeStateProvider = new FakeStateProvider(mockAccountServiceWith(userId)); - await fakeStateProvider.setUserState(ENCRYPTED_COLLECTION_DATA_KEY, null); + await fakeStateProvider.setUserState(ENCRYPTED_COLLECTION_DATA_KEY, null, userId); // Arrange cryptoService - orgKeys and mock decryption - const cryptoService = mockCryptoService(); + const [cryptoService, encryptService] = mockCryptoService(); cryptoService.orgKeys$.mockReturnValue( of({ [org1]: makeSymmetricCryptoKey(), @@ -94,7 +104,75 @@ describe("DefaultCollectionService", () => { const collectionService = new DefaultCollectionvNextService( cryptoService, - mock(), + encryptService, + mockI18nService(), + fakeStateProvider, + ); + + const encryptedCollections = await firstValueFrom( + collectionService.encryptedCollections$(of(userId)), + ); + expect(encryptedCollections.length).toBe(0); + }); + }); + + describe("encryptedCollections$", () => { + it("emits encrypted collections from state", async () => { + // Arrange test collections + const org1 = Utils.newGuid() as OrganizationId; + const org2 = Utils.newGuid() as OrganizationId; + + const collection1 = collectionDataFactory(org1); + const collection2 = collectionDataFactory(org2); + + // Arrange state provider + const userId = Utils.newGuid() as UserId; + const fakeStateProvider = new FakeStateProvider(mockAccountServiceWith(userId)); + await fakeStateProvider.setUserState( + ENCRYPTED_COLLECTION_DATA_KEY, + { + [collection1.id]: collection1, + [collection2.id]: collection2, + }, + userId, + ); + + // Arrange cryptoService - just so we don't get errors + const [cryptoService, encryptService] = mockCryptoService(); + cryptoService.orgKeys$.mockReturnValue(of({})); + + const collectionService = new DefaultCollectionvNextService( + cryptoService, + encryptService, + mockI18nService(), + fakeStateProvider, + ); + + const result = await firstValueFrom(collectionService.encryptedCollections$(of(userId))); + expect(result.length).toBe(2); + expect(result[0]).toMatchObject({ + id: collection1.id, + name: makeEncString("ENC_NAME_" + collection1.id), + }); + expect(result[1]).toMatchObject({ + id: collection2.id, + name: makeEncString("ENC_NAME_" + collection2.id), + }); + }); + + it("handles null collection state", async () => { + // Arrange state provider + const userId = Utils.newGuid() as UserId; + const fakeStateProvider = new FakeStateProvider(mockAccountServiceWith(userId)); + await fakeStateProvider.setUserState(ENCRYPTED_COLLECTION_DATA_KEY, null); + + // Arrange cryptoService - orgKeys and mock decryption + const [cryptoService, encryptService] = mockCryptoService(); + cryptoService.orgKeys$.mockReturnValue(of({})); + + const collectionService = new DefaultCollectionvNextService( + cryptoService, + encryptService, mockI18nService(), fakeStateProvider, ); @@ -103,11 +181,6 @@ describe("DefaultCollectionService", () => { collectionService.decryptedCollections$(of(userId)), ); expect(decryptedCollections.length).toBe(0); - - const encryptedCollections = await firstValueFrom( - collectionService.encryptedCollections$(of(userId)), - ); - expect(encryptedCollections.length).toBe(0); }); }); }); @@ -118,23 +191,34 @@ const mockI18nService = () => { return i18nService; }; -const mockCryptoService = () => { +const mockCryptoService: () => [MockProxy, MockProxy] = () => { const cryptoService = mock(); const encryptService = mock(); encryptService.decryptToUtf8 - .calledWith(expect.any(EncString), expect.anything()) - .mockResolvedValue("DECRYPTED_STRING"); + .calledWith(expect.any(EncString), expect.any(SymmetricCryptoKey)) + .mockImplementation((encString, key) => + Promise.resolve(encString.data.replace("ENC_", "DEC_")), + ); (window as any).bitwardenContainerService = new ContainerService(cryptoService, encryptService); - return cryptoService; + return [cryptoService, encryptService]; }; const collectionDataFactory = (orgId: OrganizationId) => { const collection = new CollectionData({} as any); collection.id = Utils.newGuid() as CollectionId; collection.organizationId = orgId; - collection.name = makeEncString("ENC_STRING").encryptedString; + collection.name = makeEncString("ENC_NAME_" + collection.id).encryptedString; return collection; }; + +const collectionViewFactory = (data: CollectionData) => + Object.assign(new CollectionView(), { + id: data.id, + name: "DEC_NAME_" + data.id, + assigned: true, + externalId: null, + organizationId: data.organizationId, + }); diff --git a/libs/common/spec/utils.ts b/libs/common/spec/utils.ts index d372232937..1cead2aa62 100644 --- a/libs/common/spec/utils.ts +++ b/libs/common/spec/utils.ts @@ -46,8 +46,15 @@ export function makeStaticByteArray(length: number, start = 0) { return arr; } -export function makeSymmetricCryptoKey(length: 32 | 64 = 64) { - return new SymmetricCryptoKey(makeStaticByteArray(length)) as T; +/** + * Creates a symmetric crypto key for use in tests. This is deterministic, i.e. it will produce identical keys + * for identical argument values. Provide a unique value to the `seed` parameter to create different keys. + */ +export function makeSymmetricCryptoKey( + length: 32 | 64 = 64, + seed = 0, +) { + return new SymmetricCryptoKey(makeStaticByteArray(length, seed)) as T; } /**