From a508e5d11d7826e4e2ca481865228ed1d5c11273 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Mon, 19 Jan 2026 12:47:16 +0100 Subject: [PATCH] Add tests --- libs/user-crypto-management/jest.config.js | 1 + .../src/user-key-rotation.service.spec.ts | 300 ++++++++++++++++++ libs/user-crypto-management/test.setup.ts | 1 + 3 files changed, 302 insertions(+) create mode 100644 libs/user-crypto-management/src/user-key-rotation.service.spec.ts create mode 100644 libs/user-crypto-management/test.setup.ts diff --git a/libs/user-crypto-management/jest.config.js b/libs/user-crypto-management/jest.config.js index 44122f8407e..886da6c0940 100644 --- a/libs/user-crypto-management/jest.config.js +++ b/libs/user-crypto-management/jest.config.js @@ -2,6 +2,7 @@ module.exports = { displayName: "user-crypto-management", preset: "../../jest.preset.js", testEnvironment: "node", + setupFilesAfterEnv: ["/test.setup.ts"], transform: { "^.+\\.[tj]s$": ["ts-jest", { tsconfig: "/tsconfig.spec.json" }], }, diff --git a/libs/user-crypto-management/src/user-key-rotation.service.spec.ts b/libs/user-crypto-management/src/user-key-rotation.service.spec.ts new file mode 100644 index 00000000000..af6d871d6d7 --- /dev/null +++ b/libs/user-crypto-management/src/user-key-rotation.service.spec.ts @@ -0,0 +1,300 @@ +import { mock, MockProxy } from "jest-mock-extended"; +import { BehaviorSubject, of } from "rxjs"; + +import { SdkService } from "@bitwarden/common/platform/abstractions/sdk/sdk.service"; +import { DialogService } from "@bitwarden/components"; +import { LogService } from "@bitwarden/logging"; +import { UserId } from "@bitwarden/user-core"; + +import { DefaultUserKeyRotationService } from "./user-key-rotation.service"; + +// Mock dialog open functions +const initialPromptedOpenTrue = jest.fn(); +initialPromptedOpenTrue.mockReturnValue({ closed: new BehaviorSubject(true) }); + +const initialPromptedOpenFalse = jest.fn(); +initialPromptedOpenFalse.mockReturnValue({ closed: new BehaviorSubject(false) }); + +const emergencyAccessTrustOpenTrusted = jest.fn(); +emergencyAccessTrustOpenTrusted.mockReturnValue({ + closed: new BehaviorSubject(true), +}); + +const emergencyAccessTrustOpenUntrusted = jest.fn(); +emergencyAccessTrustOpenUntrusted.mockReturnValue({ + closed: new BehaviorSubject(false), +}); + +const accountRecoveryTrustOpenTrusted = jest.fn(); +accountRecoveryTrustOpenTrusted.mockReturnValue({ + closed: new BehaviorSubject(true), +}); + +const accountRecoveryTrustOpenUntrusted = jest.fn(); +accountRecoveryTrustOpenUntrusted.mockReturnValue({ + closed: new BehaviorSubject(false), +}); + +// Mock the key-management-ui module before importing components +jest.mock("@bitwarden/key-management-ui", () => ({ + KeyRotationTrustInfoComponent: { + open: jest.fn(), + }, + EmergencyAccessTrustComponent: { + open: jest.fn(), + }, + AccountRecoveryTrustComponent: { + open: jest.fn(), + }, +})); + +import { + AccountRecoveryTrustComponent, + EmergencyAccessTrustComponent, + KeyRotationTrustInfoComponent, +} from "@bitwarden/key-management-ui"; + +describe("DefaultUserKeyRotationService", () => { + let service: DefaultUserKeyRotationService; + + let mockSdkService: MockProxy; + let mockLogService: MockProxy; + let mockDialogService: MockProxy; + + const mockUserId = "mockUserId" as UserId; + + let mockUserCryptoManagement: { + get_untrusted_emergency_access_public_keys: jest.Mock; + get_untrusted_organization_public_keys: jest.Mock; + }; + + beforeEach(() => { + jest.clearAllMocks(); + + mockSdkService = mock(); + mockLogService = mock(); + mockDialogService = mock(); + + mockUserCryptoManagement = { + get_untrusted_emergency_access_public_keys: jest.fn(), + get_untrusted_organization_public_keys: jest.fn(), + }; + + mockUserCryptoManagement.get_untrusted_emergency_access_public_keys.mockResolvedValue([]); + mockUserCryptoManagement.get_untrusted_organization_public_keys.mockResolvedValue([]); + + const mockSdkClient = { + take: jest.fn().mockReturnValue({ + value: { + user_crypto_management: () => mockUserCryptoManagement, + }, + [Symbol.dispose]: jest.fn(), + }), + }; + + mockSdkService.userClient$.mockReturnValue(of(mockSdkClient as any)); + + service = new DefaultUserKeyRotationService( + mockSdkService, + mockLogService, + mockDialogService, + ); + + KeyRotationTrustInfoComponent.open = initialPromptedOpenTrue; + EmergencyAccessTrustComponent.open = emergencyAccessTrustOpenTrusted; + AccountRecoveryTrustComponent.open = accountRecoveryTrustOpenTrusted; + }); + + describe("verifyTrust", () => { + const mockEmergencyAccessMembership = { + id: "mockId", + name: "mockName", + public_key: new Uint8Array([1, 2, 3]), + }; + + const mockOrganizationMembership = { + organization_id: "mockOrgId", + name: "mockOrgName", + public_key: new Uint8Array([4, 5, 6]), + }; + + it("returns empty arrays if initial dialog is closed", async () => { + KeyRotationTrustInfoComponent.open = initialPromptedOpenFalse; + mockUserCryptoManagement.get_untrusted_emergency_access_public_keys.mockResolvedValue([ + mockEmergencyAccessMembership, + ]); + mockUserCryptoManagement.get_untrusted_organization_public_keys.mockResolvedValue([ + mockOrganizationMembership, + ]); + + const { + wasTrustDenied, + trustedOrganizationPublicKeys: trustedOrgs, + trustedEmergencyAccessUserPublicKeys: trustedEmergencyAccessUsers, + } = await service.verifyTrust(mockUserId); + + expect(trustedEmergencyAccessUsers).toEqual([]); + expect(trustedOrgs).toEqual([]); + expect(wasTrustDenied).toBe(true); + }); + + it("returns empty arrays if account recovery dialog is closed", async () => { + KeyRotationTrustInfoComponent.open = initialPromptedOpenTrue; + AccountRecoveryTrustComponent.open = accountRecoveryTrustOpenUntrusted; + mockUserCryptoManagement.get_untrusted_emergency_access_public_keys.mockResolvedValue([ + mockEmergencyAccessMembership, + ]); + mockUserCryptoManagement.get_untrusted_organization_public_keys.mockResolvedValue([ + mockOrganizationMembership, + ]); + + const { + wasTrustDenied, + trustedOrganizationPublicKeys: trustedOrgs, + trustedEmergencyAccessUserPublicKeys: trustedEmergencyAccessUsers, + } = await service.verifyTrust(mockUserId); + + expect(trustedEmergencyAccessUsers).toEqual([]); + expect(trustedOrgs).toEqual([]); + expect(wasTrustDenied).toBe(true); + }); + + it("returns empty arrays if emergency access dialog is closed", async () => { + KeyRotationTrustInfoComponent.open = initialPromptedOpenTrue; + AccountRecoveryTrustComponent.open = accountRecoveryTrustOpenTrusted; + EmergencyAccessTrustComponent.open = emergencyAccessTrustOpenUntrusted; + mockUserCryptoManagement.get_untrusted_emergency_access_public_keys.mockResolvedValue([ + mockEmergencyAccessMembership, + ]); + mockUserCryptoManagement.get_untrusted_organization_public_keys.mockResolvedValue([]); + + const { + wasTrustDenied, + trustedOrganizationPublicKeys: trustedOrgs, + trustedEmergencyAccessUserPublicKeys: trustedEmergencyAccessUsers, + } = await service.verifyTrust(mockUserId); + + expect(trustedEmergencyAccessUsers).toEqual([]); + expect(trustedOrgs).toEqual([]); + expect(wasTrustDenied).toBe(true); + }); + + it("returns trusted keys when all dialogs are confirmed with only emergency access users", async () => { + KeyRotationTrustInfoComponent.open = initialPromptedOpenTrue; + EmergencyAccessTrustComponent.open = emergencyAccessTrustOpenTrusted; + AccountRecoveryTrustComponent.open = accountRecoveryTrustOpenTrusted; + mockUserCryptoManagement.get_untrusted_emergency_access_public_keys.mockResolvedValue([ + mockEmergencyAccessMembership, + ]); + mockUserCryptoManagement.get_untrusted_organization_public_keys.mockResolvedValue([]); + + const { + wasTrustDenied, + trustedOrganizationPublicKeys: trustedOrgs, + trustedEmergencyAccessUserPublicKeys: trustedEmergencyAccessUsers, + } = await service.verifyTrust(mockUserId); + + expect(wasTrustDenied).toBe(false); + expect(trustedEmergencyAccessUsers).toEqual([mockEmergencyAccessMembership.public_key]); + expect(trustedOrgs).toEqual([]); + }); + + it("returns trusted keys when all dialogs are confirmed with only organizations", async () => { + KeyRotationTrustInfoComponent.open = initialPromptedOpenTrue; + EmergencyAccessTrustComponent.open = emergencyAccessTrustOpenTrusted; + AccountRecoveryTrustComponent.open = accountRecoveryTrustOpenTrusted; + mockUserCryptoManagement.get_untrusted_emergency_access_public_keys.mockResolvedValue([]); + mockUserCryptoManagement.get_untrusted_organization_public_keys.mockResolvedValue([ + mockOrganizationMembership, + ]); + + const { + wasTrustDenied, + trustedOrganizationPublicKeys: trustedOrgs, + trustedEmergencyAccessUserPublicKeys: trustedEmergencyAccessUsers, + } = await service.verifyTrust(mockUserId); + + expect(wasTrustDenied).toBe(false); + expect(trustedEmergencyAccessUsers).toEqual([]); + expect(trustedOrgs).toEqual([mockOrganizationMembership.public_key]); + }); + + it("returns empty arrays when no organizations or emergency access users exist", async () => { + mockUserCryptoManagement.get_untrusted_emergency_access_public_keys.mockResolvedValue([]); + mockUserCryptoManagement.get_untrusted_organization_public_keys.mockResolvedValue([]); + + const { + wasTrustDenied, + trustedOrganizationPublicKeys: trustedOrgs, + trustedEmergencyAccessUserPublicKeys: trustedEmergencyAccessUsers, + } = await service.verifyTrust(mockUserId); + + expect(wasTrustDenied).toBe(false); + expect(trustedEmergencyAccessUsers).toEqual([]); + expect(trustedOrgs).toEqual([]); + }); + + it("returns trusted keys when all dialogs are confirmed with both organizations and emergency access users", async () => { + KeyRotationTrustInfoComponent.open = initialPromptedOpenTrue; + EmergencyAccessTrustComponent.open = emergencyAccessTrustOpenTrusted; + AccountRecoveryTrustComponent.open = accountRecoveryTrustOpenTrusted; + mockUserCryptoManagement.get_untrusted_emergency_access_public_keys.mockResolvedValue([ + mockEmergencyAccessMembership, + ]); + mockUserCryptoManagement.get_untrusted_organization_public_keys.mockResolvedValue([ + mockOrganizationMembership, + ]); + + const { + wasTrustDenied, + trustedOrganizationPublicKeys: trustedOrgs, + trustedEmergencyAccessUserPublicKeys: trustedEmergencyAccessUsers, + } = await service.verifyTrust(mockUserId); + + expect(wasTrustDenied).toBe(false); + expect(trustedEmergencyAccessUsers).toEqual([mockEmergencyAccessMembership.public_key]); + expect(trustedOrgs).toEqual([mockOrganizationMembership.public_key]); + }); + + it("does not show initial dialog when no organizations or emergency access users exist", async () => { + mockUserCryptoManagement.get_untrusted_emergency_access_public_keys.mockResolvedValue([]); + mockUserCryptoManagement.get_untrusted_organization_public_keys.mockResolvedValue([]); + + await service.verifyTrust(mockUserId); + + expect(KeyRotationTrustInfoComponent.open).not.toHaveBeenCalled(); + }); + + it("shows initial dialog when organizations exist", async () => { + KeyRotationTrustInfoComponent.open = initialPromptedOpenTrue; + AccountRecoveryTrustComponent.open = accountRecoveryTrustOpenTrusted; + mockUserCryptoManagement.get_untrusted_emergency_access_public_keys.mockResolvedValue([]); + mockUserCryptoManagement.get_untrusted_organization_public_keys.mockResolvedValue([ + mockOrganizationMembership, + ]); + + await service.verifyTrust(mockUserId); + + expect(KeyRotationTrustInfoComponent.open).toHaveBeenCalledWith(mockDialogService, { + numberOfEmergencyAccessUsers: 0, + orgName: mockOrganizationMembership.name, + }); + }); + + it("shows initial dialog when emergency access users exist", async () => { + KeyRotationTrustInfoComponent.open = initialPromptedOpenTrue; + EmergencyAccessTrustComponent.open = emergencyAccessTrustOpenTrusted; + mockUserCryptoManagement.get_untrusted_emergency_access_public_keys.mockResolvedValue([ + mockEmergencyAccessMembership, + ]); + mockUserCryptoManagement.get_untrusted_organization_public_keys.mockResolvedValue([]); + + await service.verifyTrust(mockUserId); + + expect(KeyRotationTrustInfoComponent.open).toHaveBeenCalledWith(mockDialogService, { + numberOfEmergencyAccessUsers: 1, + orgName: undefined, + }); + }); + }); +}); diff --git a/libs/user-crypto-management/test.setup.ts b/libs/user-crypto-management/test.setup.ts new file mode 100644 index 00000000000..f0cf585fdd8 --- /dev/null +++ b/libs/user-crypto-management/test.setup.ts @@ -0,0 +1 @@ +import "core-js/proposals/explicit-resource-management";