import { MockProxy, mock } from "jest-mock-extended"; import { of } from "rxjs"; import { UserVerificationDialogComponent } from "@bitwarden/auth/angular"; import { UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec"; import { UserId } from "@bitwarden/common/types/guid"; import { CipherRepromptType, CipherType } from "@bitwarden/common/vault/enums"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { DialogService } from "@bitwarden/components"; import { newGuid } from "@bitwarden/guid"; import { PasswordRepromptService } from "@bitwarden/vault"; // FIXME (PM-22628): Popup imports are forbidden in background // eslint-disable-next-line no-restricted-imports import { SetPinComponent } from "./../../auth/popup/components/set-pin.component"; import { Fido2UserVerificationService } from "./fido2-user-verification.service"; jest.mock("@bitwarden/auth/angular", () => ({ UserVerificationDialogComponent: { open: jest.fn().mockResolvedValue({ userAction: "confirm", verificationSuccess: true }), }, })); jest.mock("../../auth/popup/components/set-pin.component", () => { return { SetPinComponent: { open: jest.fn(), }, }; }); describe("Fido2UserVerificationService", () => { let fido2UserVerificationService: Fido2UserVerificationService; let passwordRepromptService: MockProxy; let userDecryptionOptionsService: MockProxy; let dialogService: MockProxy; let accountService: FakeAccountService; let cipher: CipherView; beforeEach(() => { passwordRepromptService = mock(); userDecryptionOptionsService = mock(); dialogService = mock(); accountService = mockAccountServiceWith(newGuid() as UserId); cipher = createCipherView(); fido2UserVerificationService = new Fido2UserVerificationService( passwordRepromptService, userDecryptionOptionsService, dialogService, accountService, ); (UserVerificationDialogComponent.open as jest.Mock).mockResolvedValue({ userAction: "confirm", verificationSuccess: true, }); }); describe("handleUserVerification", () => { describe("user verification requested is true", () => { it("should return true if user is redirected from lock screen and master password reprompt is not required", async () => { const result = await fido2UserVerificationService.handleUserVerification( true, cipher, true, ); expect(result).toBe(true); }); it("should call master password reprompt dialog if user is redirected from lock screen, has master password and master password reprompt is required", async () => { cipher.reprompt = CipherRepromptType.Password; userDecryptionOptionsService.hasMasterPasswordById$.mockReturnValue(of(true)); passwordRepromptService.showPasswordPrompt.mockResolvedValue(true); const result = await fido2UserVerificationService.handleUserVerification( true, cipher, true, ); expect(passwordRepromptService.showPasswordPrompt).toHaveBeenCalled(); expect(result).toBe(true); }); it("should call user verification dialog if user is redirected from lock screen, does not have a master password and master password reprompt is required", async () => { cipher.reprompt = CipherRepromptType.Password; userDecryptionOptionsService.hasMasterPasswordById$.mockReturnValue(of(false)); const result = await fido2UserVerificationService.handleUserVerification( true, cipher, true, ); expect(UserVerificationDialogComponent.open).toHaveBeenCalledWith(dialogService, { verificationType: "client", }); expect(result).toBe(true); }); it("should call user verification dialog if user is not redirected from lock screen, does not have a master password and master password reprompt is required", async () => { cipher.reprompt = CipherRepromptType.Password; userDecryptionOptionsService.hasMasterPasswordById$.mockReturnValue(of(false)); const result = await fido2UserVerificationService.handleUserVerification( true, cipher, false, ); expect(UserVerificationDialogComponent.open).toHaveBeenCalledWith(dialogService, { verificationType: "client", }); expect(result).toBe(true); }); it("should call master password reprompt dialog if user is not redirected from lock screen, has a master password and master password reprompt is required", async () => { cipher.reprompt = CipherRepromptType.Password; userDecryptionOptionsService.hasMasterPasswordById$.mockReturnValue(of(false)); passwordRepromptService.showPasswordPrompt.mockResolvedValue(true); const result = await fido2UserVerificationService.handleUserVerification( true, cipher, false, ); expect(UserVerificationDialogComponent.open).toHaveBeenCalledWith(dialogService, { verificationType: "client", }); expect(result).toBe(true); }); it("should call user verification dialog if user is not redirected from lock screen and no master password reprompt is required", async () => { const result = await fido2UserVerificationService.handleUserVerification( true, cipher, false, ); expect(UserVerificationDialogComponent.open).toHaveBeenCalledWith(dialogService, { verificationType: "client", }); expect(result).toBe(true); }); it("should prompt user to set pin if user has no verification method", async () => { (UserVerificationDialogComponent.open as jest.Mock).mockResolvedValue({ userAction: "confirm", verificationSuccess: false, noAvailableClientVerificationMethods: true, }); await fido2UserVerificationService.handleUserVerification(true, cipher, false); expect(SetPinComponent.open).toHaveBeenCalledWith(dialogService); }); }); describe("user verification requested is false", () => { it("should return false if user is redirected from lock screen and master password reprompt is not required", async () => { const result = await fido2UserVerificationService.handleUserVerification( false, cipher, true, ); expect(result).toBe(false); }); it("should return false if user is not redirected from lock screen and master password reprompt is not required", async () => { const result = await fido2UserVerificationService.handleUserVerification( false, cipher, false, ); expect(result).toBe(false); }); it("should call master password reprompt dialog if user is redirected from lock screen, has master password and master password reprompt is required", async () => { cipher.reprompt = CipherRepromptType.Password; userDecryptionOptionsService.hasMasterPasswordById$.mockReturnValue(of(true)); passwordRepromptService.showPasswordPrompt.mockResolvedValue(true); const result = await fido2UserVerificationService.handleUserVerification( false, cipher, true, ); expect(result).toBe(true); expect(passwordRepromptService.showPasswordPrompt).toHaveBeenCalled(); }); it("should call user verification dialog if user is redirected from lock screen, does not have a master password and master password reprompt is required", async () => { cipher.reprompt = CipherRepromptType.Password; userDecryptionOptionsService.hasMasterPasswordById$.mockReturnValue(of(false)); const result = await fido2UserVerificationService.handleUserVerification( false, cipher, true, ); expect(UserVerificationDialogComponent.open).toHaveBeenCalledWith(dialogService, { verificationType: "client", }); expect(result).toBe(true); }); it("should call user verification dialog if user is not redirected from lock screen, does not have a master password and master password reprompt is required", async () => { cipher.reprompt = CipherRepromptType.Password; userDecryptionOptionsService.hasMasterPasswordById$.mockReturnValue(of(false)); const result = await fido2UserVerificationService.handleUserVerification( false, cipher, false, ); expect(UserVerificationDialogComponent.open).toHaveBeenCalledWith(dialogService, { verificationType: "client", }); expect(result).toBe(true); }); it("should call master password reprompt dialog if user is not redirected from lock screen, has a master password and master password reprompt is required", async () => { cipher.reprompt = CipherRepromptType.Password; userDecryptionOptionsService.hasMasterPasswordById$.mockReturnValue(of(false)); passwordRepromptService.showPasswordPrompt.mockResolvedValue(true); const result = await fido2UserVerificationService.handleUserVerification( false, cipher, false, ); expect(UserVerificationDialogComponent.open).toHaveBeenCalledWith(dialogService, { verificationType: "client", }); expect(result).toBe(true); }); }); }); }); function createCipherView() { const cipher = new CipherView(); cipher.id = Utils.newGuid(); cipher.type = CipherType.Login; cipher.reprompt = CipherRepromptType.None; return cipher; }