From c049384d8380f886a2149b20610d8f5acb2b3294 Mon Sep 17 00:00:00 2001 From: Thomas Avery Date: Tue, 5 Nov 2024 18:08:40 -0600 Subject: [PATCH] Add unit tests to service --- ...symmetric-key-regeneration.service.spec.ts | 309 ++++++++++++++++++ 1 file changed, 309 insertions(+) create mode 100644 libs/key-management/src/user-asymmetric-key-regeneration/services/default-user-asymmetric-key-regeneration.service.spec.ts diff --git a/libs/key-management/src/user-asymmetric-key-regeneration/services/default-user-asymmetric-key-regeneration.service.spec.ts b/libs/key-management/src/user-asymmetric-key-regeneration/services/default-user-asymmetric-key-regeneration.service.spec.ts new file mode 100644 index 0000000000..337a7eb9f1 --- /dev/null +++ b/libs/key-management/src/user-asymmetric-key-regeneration/services/default-user-asymmetric-key-regeneration.service.spec.ts @@ -0,0 +1,309 @@ +import { MockProxy, mock } from "jest-mock-extended"; +import { of, throwError } from "rxjs"; + +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; +import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { SdkService } from "@bitwarden/common/platform/abstractions/sdk/sdk.service"; +import { EncryptedString } 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 { makeStaticByteArray, mockEnc } from "@bitwarden/common/spec"; +import { CsprngArray } from "@bitwarden/common/types/csprng"; +import { UserId } from "@bitwarden/common/types/guid"; +import { UserKey } from "@bitwarden/common/types/key"; +import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { Cipher } from "@bitwarden/common/vault/models/domain/cipher"; +import { BitwardenClient, VerifyAsymmetricKeysResponse } from "@bitwarden/sdk-internal"; + +import { KeyService } from "../../abstractions/key.service"; +import { UserAsymmetricKeysRegenerationApiService } from "../abstractions/user-asymmetric-key-regeneration-api.service"; + +import { DefaultUserAsymmetricKeysRegenerationService } from "./default-user-asymmetric-key-regeneration.service"; + +function setupVerificationResponse( + mockVerificationResponse: VerifyAsymmetricKeysResponse, + sdkService: MockProxy, +) { + const mockKeyPairResponse = { + userPublicKey: "userPublicKey", + userKeyEncryptedPrivateKey: "userKeyEncryptedPrivateKey", + }; + + sdkService.userClient$.mockReturnValue( + of({ + crypto: () => ({ + verify_asymmetric_keys: jest.fn().mockReturnValue(mockVerificationResponse), + make_key_pair: jest.fn().mockReturnValue(mockKeyPairResponse), + }), + free: jest.fn(), + echo: jest.fn(), + version: jest.fn(), + throw: jest.fn(), + catch: jest.fn(), + } as unknown as BitwardenClient), + ); +} + +function setupUserKeyValidation( + cipherService: MockProxy, + keyService: MockProxy, + encryptService: MockProxy, +) { + const cipher = new Cipher(); + cipher.id = "id"; + cipher.edit = true; + cipher.viewPassword = true; + cipher.favorite = false; + cipher.name = mockEnc("EncryptedString"); + cipher.notes = mockEnc("EncryptedString"); + cipher.deletedDate = null; + cipher.key = mockEnc("EncKey"); + cipherService.getAll.mockResolvedValue([cipher]); + encryptService.decryptToBytes.mockResolvedValue(makeStaticByteArray(64)); + (window as any).bitwardenContainerService = new ContainerService(keyService, encryptService); +} + +describe("handleUserAsymmetricKeysRegeneration", () => { + let sut: DefaultUserAsymmetricKeysRegenerationService; + const userId = "userId" as UserId; + + let keyService: MockProxy; + let cipherService: MockProxy; + let userAsymmetricKeysRegenerationApiService: MockProxy; + let logService: MockProxy; + let sdkService: MockProxy; + let apiService: MockProxy; + let configService: MockProxy; + let encryptService: MockProxy; + + beforeEach(() => { + keyService = mock(); + cipherService = mock(); + userAsymmetricKeysRegenerationApiService = mock(); + logService = mock(); + sdkService = mock(); + apiService = mock(); + configService = mock(); + encryptService = mock(); + + sut = new DefaultUserAsymmetricKeysRegenerationService( + keyService, + cipherService, + userAsymmetricKeysRegenerationApiService, + logService, + sdkService, + apiService, + configService, + ); + + configService.getFeatureFlag.mockResolvedValue(true); + + const mockRandomBytes = new Uint8Array(64) as CsprngArray; + const mockEncryptedString = new SymmetricCryptoKey( + mockRandomBytes, + ).toString() as EncryptedString; + const mockUserKey = new SymmetricCryptoKey(mockRandomBytes) as UserKey; + keyService.userKey$.mockReturnValue(of(mockUserKey)); + keyService.userEncryptedPrivateKey$.mockReturnValue(of(mockEncryptedString)); + apiService.getUserPublicKey.mockResolvedValue({ + userId: "userId", + publicKey: "publicKey", + } as any); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + it("should not call regeneration code when feature flag is off", async () => { + configService.getFeatureFlag.mockResolvedValue(false); + + await sut.handleUserAsymmetricKeysRegeneration(userId); + + expect(keyService.userKey$).not.toHaveBeenCalled(); + }); + + it("should not regenerate when top level error is thrown", async () => { + const mockVerificationResponse: VerifyAsymmetricKeysResponse = { + privateKeyDecryptable: true, + validPrivateKey: false, + }; + setupVerificationResponse(mockVerificationResponse, sdkService); + keyService.userKey$.mockReturnValue(throwError(() => new Error("error"))); + + await sut.handleUserAsymmetricKeysRegeneration(userId); + + expect( + userAsymmetricKeysRegenerationApiService.regenerateUserAsymmetricKeys, + ).not.toHaveBeenCalled(); + expect(keyService.setPrivateKey).not.toHaveBeenCalled(); + }); + + it("should not regenerate when private key is decryptable and valid", async () => { + const mockVerificationResponse: VerifyAsymmetricKeysResponse = { + privateKeyDecryptable: true, + validPrivateKey: true, + }; + setupVerificationResponse(mockVerificationResponse, sdkService); + + await sut.handleUserAsymmetricKeysRegeneration(userId); + + expect( + userAsymmetricKeysRegenerationApiService.regenerateUserAsymmetricKeys, + ).not.toHaveBeenCalled(); + expect(keyService.setPrivateKey).not.toHaveBeenCalled(); + }); + + it("should regenerate when private key is decryptable and invalid", async () => { + const mockVerificationResponse: VerifyAsymmetricKeysResponse = { + privateKeyDecryptable: true, + validPrivateKey: false, + }; + setupVerificationResponse(mockVerificationResponse, sdkService); + + await sut.handleUserAsymmetricKeysRegeneration(userId); + + expect( + userAsymmetricKeysRegenerationApiService.regenerateUserAsymmetricKeys, + ).toHaveBeenCalled(); + expect(keyService.setPrivateKey).toHaveBeenCalled(); + }); + + it("should not set private key on known API error", async () => { + const mockVerificationResponse: VerifyAsymmetricKeysResponse = { + privateKeyDecryptable: true, + validPrivateKey: false, + }; + setupVerificationResponse(mockVerificationResponse, sdkService); + + userAsymmetricKeysRegenerationApiService.regenerateUserAsymmetricKeys.mockRejectedValue( + new Error("Key regeneration not supported for this user."), + ); + + await sut.handleUserAsymmetricKeysRegeneration(userId); + + expect( + userAsymmetricKeysRegenerationApiService.regenerateUserAsymmetricKeys, + ).toHaveBeenCalled(); + expect(keyService.setPrivateKey).not.toHaveBeenCalled(); + }); + + it("should not set private key on unknown API error", async () => { + const mockVerificationResponse: VerifyAsymmetricKeysResponse = { + privateKeyDecryptable: true, + validPrivateKey: false, + }; + setupVerificationResponse(mockVerificationResponse, sdkService); + + userAsymmetricKeysRegenerationApiService.regenerateUserAsymmetricKeys.mockRejectedValue( + new Error("error"), + ); + + await sut.handleUserAsymmetricKeysRegeneration(userId); + + expect( + userAsymmetricKeysRegenerationApiService.regenerateUserAsymmetricKeys, + ).toHaveBeenCalled(); + expect(keyService.setPrivateKey).not.toHaveBeenCalled(); + }); + + it("should regenerate when private key is not decryptable and user key is valid", async () => { + const mockVerificationResponse: VerifyAsymmetricKeysResponse = { + privateKeyDecryptable: false, + validPrivateKey: true, + }; + setupVerificationResponse(mockVerificationResponse, sdkService); + setupUserKeyValidation(cipherService, keyService, encryptService); + + await sut.handleUserAsymmetricKeysRegeneration(userId); + + expect( + userAsymmetricKeysRegenerationApiService.regenerateUserAsymmetricKeys, + ).toHaveBeenCalled(); + expect(keyService.setPrivateKey).toHaveBeenCalled(); + }); + + it("should not regenerate when private key is not decryptable and user key is invalid", async () => { + const mockVerificationResponse: VerifyAsymmetricKeysResponse = { + privateKeyDecryptable: false, + validPrivateKey: true, + }; + setupVerificationResponse(mockVerificationResponse, sdkService); + setupUserKeyValidation(cipherService, keyService, encryptService); + encryptService.decryptToBytes.mockRejectedValue(new Error("error")); + + await sut.handleUserAsymmetricKeysRegeneration(userId); + + expect( + userAsymmetricKeysRegenerationApiService.regenerateUserAsymmetricKeys, + ).not.toHaveBeenCalled(); + expect(keyService.setPrivateKey).not.toHaveBeenCalled(); + }); + + it("should not regenerate when private key is not decryptable and no ciphers to check", async () => { + const mockVerificationResponse: VerifyAsymmetricKeysResponse = { + privateKeyDecryptable: false, + validPrivateKey: true, + }; + setupVerificationResponse(mockVerificationResponse, sdkService); + cipherService.getAll.mockResolvedValue([]); + + await sut.handleUserAsymmetricKeysRegeneration(userId); + + expect( + userAsymmetricKeysRegenerationApiService.regenerateUserAsymmetricKeys, + ).not.toHaveBeenCalled(); + expect(keyService.setPrivateKey).not.toHaveBeenCalled(); + }); + + it("should regenerate when private key is not decryptable and invalid and user key is valid", async () => { + const mockVerificationResponse: VerifyAsymmetricKeysResponse = { + privateKeyDecryptable: false, + validPrivateKey: false, + }; + setupVerificationResponse(mockVerificationResponse, sdkService); + setupUserKeyValidation(cipherService, keyService, encryptService); + + await sut.handleUserAsymmetricKeysRegeneration(userId); + + expect( + userAsymmetricKeysRegenerationApiService.regenerateUserAsymmetricKeys, + ).toHaveBeenCalled(); + expect(keyService.setPrivateKey).toHaveBeenCalled(); + }); + + it("should not regenerate when private key is not decryptable and invalid and user key is invalid", async () => { + const mockVerificationResponse: VerifyAsymmetricKeysResponse = { + privateKeyDecryptable: false, + validPrivateKey: false, + }; + setupVerificationResponse(mockVerificationResponse, sdkService); + setupUserKeyValidation(cipherService, keyService, encryptService); + encryptService.decryptToBytes.mockRejectedValue(new Error("error")); + + await sut.handleUserAsymmetricKeysRegeneration(userId); + + expect( + userAsymmetricKeysRegenerationApiService.regenerateUserAsymmetricKeys, + ).not.toHaveBeenCalled(); + expect(keyService.setPrivateKey).not.toHaveBeenCalled(); + }); + + it("should not regenerate when private key is not decryptable and invalid and no ciphers to check", async () => { + const mockVerificationResponse: VerifyAsymmetricKeysResponse = { + privateKeyDecryptable: false, + validPrivateKey: false, + }; + setupVerificationResponse(mockVerificationResponse, sdkService); + cipherService.getAll.mockResolvedValue([]); + + await sut.handleUserAsymmetricKeysRegeneration(userId); + + expect( + userAsymmetricKeysRegenerationApiService.regenerateUserAsymmetricKeys, + ).not.toHaveBeenCalled(); + expect(keyService.setPrivateKey).not.toHaveBeenCalled(); + }); +});