import { TestBed } from "@angular/core/testing"; import { Router } from "@angular/router"; import { mock, MockProxy } from "jest-mock-extended"; import { BehaviorSubject, of } from "rxjs"; import { AccountService, Account } from "@bitwarden/common/auth/abstractions/account.service"; import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { mockAccountInfoWith } from "@bitwarden/common/spec"; import { UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherRepromptType, CipherType } from "@bitwarden/common/vault/enums"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { DialogService } from "@bitwarden/components"; import { PasswordRepromptService } from "@bitwarden/vault"; import { DesktopAutofillService } from "../../../autofill/services/desktop-autofill.service"; import { DesktopSettingsService } from "../../../platform/services/desktop-settings.service"; import { DesktopFido2UserInterfaceService, DesktopFido2UserInterfaceSession, } from "../../services/desktop-fido2-user-interface.service"; import { Fido2CreateComponent } from "./fido2-create.component"; describe("Fido2CreateComponent", () => { let component: Fido2CreateComponent; let mockDesktopSettingsService: MockProxy; let mockFido2UserInterfaceService: MockProxy; let mockAccountService: MockProxy; let mockCipherService: MockProxy; let mockDesktopAutofillService: MockProxy; let mockDialogService: MockProxy; let mockDomainSettingsService: MockProxy; let mockLogService: MockProxy; let mockPasswordRepromptService: MockProxy; let mockRouter: MockProxy; let mockSession: MockProxy; let mockI18nService: MockProxy; const activeAccountSubject = new BehaviorSubject({ id: "test-user-id" as UserId, ...mockAccountInfoWith({ email: "test@example.com", name: "Test User", }), }); beforeEach(async () => { mockDesktopSettingsService = mock(); mockFido2UserInterfaceService = mock(); mockAccountService = mock(); mockCipherService = mock(); mockDesktopAutofillService = mock(); mockDialogService = mock(); mockDomainSettingsService = mock(); mockLogService = mock(); mockPasswordRepromptService = mock(); mockRouter = mock(); mockSession = mock(); mockI18nService = mock(); mockFido2UserInterfaceService.getCurrentSession.mockReturnValue(mockSession); mockAccountService.activeAccount$ = activeAccountSubject; await TestBed.configureTestingModule({ providers: [ Fido2CreateComponent, { provide: DesktopSettingsService, useValue: mockDesktopSettingsService }, { provide: DesktopFido2UserInterfaceService, useValue: mockFido2UserInterfaceService }, { provide: AccountService, useValue: mockAccountService }, { provide: CipherService, useValue: mockCipherService }, { provide: DesktopAutofillService, useValue: mockDesktopAutofillService }, { provide: DialogService, useValue: mockDialogService }, { provide: DomainSettingsService, useValue: mockDomainSettingsService }, { provide: LogService, useValue: mockLogService }, { provide: PasswordRepromptService, useValue: mockPasswordRepromptService }, { provide: Router, useValue: mockRouter }, { provide: I18nService, useValue: mockI18nService }, ], }).compileComponents(); component = TestBed.inject(Fido2CreateComponent); }); afterEach(() => { jest.restoreAllMocks(); }); function createMockCiphers(): CipherView[] { const cipher1 = new CipherView(); cipher1.id = "cipher-1"; cipher1.name = "Test Cipher 1"; cipher1.type = CipherType.Login; cipher1.login = { username: "test1@example.com", uris: [{ uri: "https://example.com", match: null }], matchesUri: jest.fn().mockReturnValue(true), get hasFido2Credentials() { return false; }, } as any; cipher1.reprompt = CipherRepromptType.None; cipher1.deletedDate = null; return [cipher1]; } describe("ngOnInit", () => { beforeEach(() => { mockSession.getRpId.mockResolvedValue("example.com"); Object.defineProperty(mockDesktopAutofillService, "lastRegistrationRequest", { get: jest.fn().mockReturnValue({ userHandle: new Uint8Array([1, 2, 3]), }), configurable: true, }); mockDomainSettingsService.getUrlEquivalentDomains.mockReturnValue(of(new Set())); }); it("should initialize session and set show header to false", async () => { const mockCiphers = createMockCiphers(); mockCipherService.getAllDecrypted.mockResolvedValue(mockCiphers); await component.ngOnInit(); expect(mockFido2UserInterfaceService.getCurrentSession).toHaveBeenCalled(); expect(component.session).toBe(mockSession); }); it("should show error dialog when no active session found", async () => { mockFido2UserInterfaceService.getCurrentSession.mockReturnValue(null); mockDialogService.openSimpleDialog.mockResolvedValue(false); await component.ngOnInit(); expect(mockDialogService.openSimpleDialog).toHaveBeenCalledWith({ title: { key: "unableToSavePasskey" }, content: { key: "closeThisBitwardenWindow" }, type: "danger", acceptButtonText: { key: "closeThisWindow" }, acceptAction: expect.any(Function), cancelButtonText: null, }); }); }); describe("addCredentialToCipher", () => { beforeEach(() => { component.session = mockSession; }); it("should add passkey to cipher", async () => { const cipher = createMockCiphers()[0]; await component.addCredentialToCipher(cipher); expect(mockSession.notifyConfirmCreateCredential).toHaveBeenCalledWith(true, cipher); }); it("should not add passkey when password reprompt is cancelled", async () => { const cipher = createMockCiphers()[0]; cipher.reprompt = CipherRepromptType.Password; mockPasswordRepromptService.showPasswordPrompt.mockResolvedValue(false); await component.addCredentialToCipher(cipher); expect(mockSession.notifyConfirmCreateCredential).toHaveBeenCalledWith(false, cipher); }); it("should call openSimpleDialog when cipher already has a fido2 credential", async () => { const cipher = createMockCiphers()[0]; Object.defineProperty(cipher.login, "hasFido2Credentials", { get: jest.fn().mockReturnValue(true), }); mockDialogService.openSimpleDialog.mockResolvedValue(true); await component.addCredentialToCipher(cipher); expect(mockDialogService.openSimpleDialog).toHaveBeenCalledWith({ title: { key: "overwritePasskey" }, content: { key: "alreadyContainsPasskey" }, type: "warning", }); expect(mockSession.notifyConfirmCreateCredential).toHaveBeenCalledWith(true, cipher); }); it("should not add passkey when user cancels overwrite dialog", async () => { const cipher = createMockCiphers()[0]; Object.defineProperty(cipher.login, "hasFido2Credentials", { get: jest.fn().mockReturnValue(true), }); mockDialogService.openSimpleDialog.mockResolvedValue(false); await component.addCredentialToCipher(cipher); expect(mockSession.notifyConfirmCreateCredential).toHaveBeenCalledWith(false, cipher); }); }); describe("confirmPasskey", () => { beforeEach(() => { component.session = mockSession; }); it("should confirm passkey creation successfully", async () => { await component.confirmPasskey(); expect(mockSession.notifyConfirmCreateCredential).toHaveBeenCalledWith(true); }); it("should call openSimpleDialog when session is null", async () => { component.session = null; mockDialogService.openSimpleDialog.mockResolvedValue(false); await component.confirmPasskey(); expect(mockDialogService.openSimpleDialog).toHaveBeenCalledWith({ title: { key: "unableToSavePasskey" }, content: { key: "closeThisBitwardenWindow" }, type: "danger", acceptButtonText: { key: "closeThisWindow" }, acceptAction: expect.any(Function), cancelButtonText: null, }); }); }); describe("closeModal", () => { it("should close modal and notify session", async () => { component.session = mockSession; await component.closeModal(); expect(mockSession.notifyConfirmCreateCredential).toHaveBeenCalledWith(false); expect(mockSession.confirmChosenCipher).toHaveBeenCalledWith(null); }); }); });