diff --git a/apps/desktop/src/autofill/services/desktop-fido2-user-interface.service.spec.ts b/apps/desktop/src/autofill/services/desktop-fido2-user-interface.service.spec.ts new file mode 100644 index 00000000000..14fefb6028c --- /dev/null +++ b/apps/desktop/src/autofill/services/desktop-fido2-user-interface.service.spec.ts @@ -0,0 +1,120 @@ +import { Router } from "@angular/router"; +import { mock } from "jest-mock-extended"; + +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; +import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; + +import { DesktopSettingsService } from "src/platform/services/desktop-settings.service"; + +import { + DesktopFido2UserInterfaceService, + DesktopFido2UserInterfaceSession, +} from "./desktop-fido2-user-interface.service"; +import type { NativeWindowObject } from "./desktop-fido2-user-interface.service"; + +describe("Desktop Fido2 User Interface Service", () => { + const accountService = mock(); + const authService = mock(); + const cipherService = mock(); + const desktopSettingsService = mock(); + const logService = mock(); + const messagingService = mock(); + const router = mock(); + let desktopFido2UserInterfaceService: DesktopFido2UserInterfaceService; + + beforeEach(() => { + desktopFido2UserInterfaceService = new DesktopFido2UserInterfaceService( + authService, + cipherService, + accountService, + logService, + messagingService, + router, + desktopSettingsService, + ); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + describe("newSession", () => { + it("logs a warning", async () => { + const fallbackSupported = false; + const nativeWindowObject = {}; + await desktopFido2UserInterfaceService.newSession(fallbackSupported, nativeWindowObject); + + expect(logService.warning).toHaveBeenCalled(); + }); + }); +}); + +describe("Desktop Fido2 User Interface Session", () => { + const accountService = mock(); + const authService = mock(); + const cipherService = mock(); + const desktopSettingsService = mock(); + const logService = mock(); + const router = mock(); + const windowObject = mock(); + let desktopFido2UserInterfaceSession: DesktopFido2UserInterfaceSession; + + beforeEach(() => { + desktopFido2UserInterfaceSession = new DesktopFido2UserInterfaceSession( + authService, + cipherService, + accountService, + logService, + router, + desktopSettingsService, + windowObject, + ); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + describe("availableCipherId$", () => { + it("returns available cipher ids", () => { + desktopFido2UserInterfaceSession.availableCipherIds$.subscribe({ + next: (ids) => { + expect(ids).toEqual(["id1", "id2"]); + }, + }); + + desktopFido2UserInterfaceSession["availableCipherIdsSubject"].next(["id1", "id2"]); + }); + }); + + describe("pickCredential", () => { + it("returns a cipherId when one exists", async () => { + const result = await desktopFido2UserInterfaceSession.pickCredential({ + cipherIds: ["id"], + userVerification: false, + assumeUserPresence: false, + masterPasswordRepromptRequired: false, + }); + + expect(result).toStrictEqual({ cipherId: "id", userVerified: false }); + }); + + it("returns a user chosen cipherId", async () => { + jest + .spyOn(desktopFido2UserInterfaceSession as any, "waitForUiChosenCipher") + .mockResolvedValue("id2"); + + const result = await desktopFido2UserInterfaceSession.pickCredential({ + cipherIds: ["id", "id2"], + userVerification: false, + assumeUserPresence: false, + masterPasswordRepromptRequired: false, + }); + + expect(result).toStrictEqual({ cipherId: "id2", userVerified: true }); + }); + }); +}); diff --git a/apps/desktop/src/modal/passkeys/create/fido2-create.component.spec.ts b/apps/desktop/src/modal/passkeys/create/fido2-create.component.spec.ts new file mode 100644 index 00000000000..cf273b1e05e --- /dev/null +++ b/apps/desktop/src/modal/passkeys/create/fido2-create.component.spec.ts @@ -0,0 +1,161 @@ +import { CommonModule } from "@angular/common"; +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { RouterModule, Router } from "@angular/router"; +import { MockProxy, mock } from "jest-mock-extended"; +import { of } from "rxjs"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { + BadgeModule, + ButtonModule, + DialogModule, + IconModule, + ItemModule, + SectionComponent, + TableModule, +} from "@bitwarden/components"; + +import { BitIconButtonComponent } from "../../../../../../libs/components/src/icon-button/icon-button.component"; +import { SectionHeaderComponent } from "../../../../../../libs/components/src/section/section-header.component"; +import { + DesktopFido2UserInterfaceService, + DesktopFido2UserInterfaceSession, +} from "../../../autofill/services/desktop-fido2-user-interface.service"; +import { DesktopSettingsService } from "../../../platform/services/desktop-settings.service"; + +import { Fido2CreateComponent } from "./fido2-create.component"; + +describe("Fido2CreateComponent", () => { + let rpid: string; + let component: Fido2CreateComponent; + let fixture: ComponentFixture; + let cipherService: MockProxy; + let desktopSettingsService: MockProxy; + let domainSettingService: MockProxy; + let interfaceService: MockProxy; + let session: MockProxy; + let router: MockProxy; + + beforeEach(async () => { + rpid = "example.com"; + router = mock(); + session = mock({ + getRpId: jest.fn().mockResolvedValue(rpid), + }); + cipherService = mock({ + getAllDecrypted: jest.fn().mockResolvedValue([ + { + login: { + hasUris: true, + fido2Credentials: [], + matchesUri: jest.fn().mockReturnValue(true), + }, + }, + { + login: { + hasUris: false, + fido2Credentials: [], + matchesUri: jest.fn().mockReturnValue(false), + }, + }, + ]), + }); + desktopSettingsService = mock(); + domainSettingService = mock({ + getUrlEquivalentDomains: jest + .fn() + .mockReturnValue(of(new Set(["example.com", "example.org"]))), + equivalentDomains$: of([ + ["example.com", "example.org"], + ["example.net", "example.info"], + ]), + }); + interfaceService = mock({ + getCurrentSession: jest.fn().mockReturnValue(session), + }); + + await TestBed.configureTestingModule({ + imports: [ + CommonModule, + RouterModule, + SectionHeaderComponent, + BitIconButtonComponent, + TableModule, + JslibModule, + IconModule, + ButtonModule, + DialogModule, + SectionComponent, + ItemModule, + BadgeModule, + ], + providers: [ + { provide: DesktopSettingsService, useValue: desktopSettingsService }, + { provide: DesktopFido2UserInterfaceService, useValue: interfaceService }, + { provide: DesktopFido2UserInterfaceSession, useValue: session }, + { provide: CipherService, useValue: cipherService }, + { provide: DomainSettingsService, useValue: domainSettingService }, + { provide: I18nService, useValue: mock() }, + { provide: Router, useValue: router }, + ], + }).compileComponents(); + + fixture = TestBed.createComponent(Fido2CreateComponent); + component = fixture.componentInstance; + }); + + it("creates the component", () => { + expect(component).toBeTruthy(); + }); + + it("ngOnInit", async () => { + component.session = session; + + await component.ngOnInit(); + + expect(session.getRpId).toHaveBeenCalledTimes(1); + expect(domainSettingService.getUrlEquivalentDomains).toHaveBeenCalledWith(rpid); + expect(cipherService.getAllDecrypted).toHaveBeenCalledTimes(1); + }); + + it("adds pass key to cipher", async () => { + component.session = session; + const cipher = new CipherView(); + + await component.addPasskeyToCipher(cipher); + + expect(session.notifyConfirmCredential).toHaveBeenCalledWith(true, cipher); + }); + + describe("confirming the pass key", () => { + // it("throws an error when no session exists", async () => { + // component.session = undefined; + + // await expect(component.confirmPasskey()).rejects.toThrow("No session found"); + // }); + it("confirms the pass key", async () => { + component.session = session; + + await component.confirmPasskey(); + + expect(router.navigate).toHaveBeenCalledWith(["/"]); + expect(session.notifyConfirmCredential).toHaveBeenCalledWith(true); + expect(desktopSettingsService.setModalMode).toHaveBeenCalledWith(false); + }); + }); + + it("closes the modal", async () => { + component.session = session; + + await component.closeModal(); + + expect(router.navigate).toHaveBeenCalledWith(["/"]); + expect(desktopSettingsService.setModalMode).toHaveBeenCalledWith(false); + expect(session.notifyConfirmCredential).toHaveBeenCalledWith(false); + expect(session.confirmChosenCipher).toHaveBeenCalledWith(null); + }); +}); diff --git a/libs/common/src/vault/services/cipher.service.spec.ts b/libs/common/src/vault/services/cipher.service.spec.ts index 6b225af0d84..4e66ef4455f 100644 --- a/libs/common/src/vault/services/cipher.service.spec.ts +++ b/libs/common/src/vault/services/cipher.service.spec.ts @@ -36,6 +36,7 @@ import { CipherCreateRequest } from "../models/request/cipher-create.request"; import { CipherPartialRequest } from "../models/request/cipher-partial.request"; import { CipherRequest } from "../models/request/cipher.request"; import { CipherView } from "../models/view/cipher.view"; +import { Fido2CredentialView } from "../models/view/fido2-credential.view"; import { LoginUriView } from "../models/view/login-uri.view"; import { CipherService } from "./cipher.service"; @@ -414,4 +415,41 @@ describe("Cipher Service", () => { ); }); }); + + describe("getAllDecryptedForIds", () => { + it("returns ciphers for the given ids", async () => { + const startingCiphers = [{ ...cipherObj }, { ...cipherObj, id: "id2" }]; + const expectedObj = [cipherObj]; + jest.spyOn(cipherService as any, "getAllDecrypted").mockResolvedValue(startingCiphers); + + const result = await cipherService.getAllDecryptedForIds(["id"]); + + expect(result).toEqual(expectedObj); + }); + }); + + describe("getPasskeyCiphers", () => { + it("returns the fido2 ciphers when they are available", async () => { + const passkeyDate = new Date(); + const cipherWithFido2 = [ + { + ...cipherObj, + login: { fido2Credentials: [{ creationDate: passkeyDate } as Fido2CredentialView] }, + }, + ]; + jest.spyOn(cipherService as any, "getAllDecrypted").mockResolvedValue(cipherWithFido2); + + const result = await cipherService.getAllDecrypted(); + + expect(result).toBe(cipherWithFido2); + }); + + it("returns null when there are no ciphers", async () => { + jest.spyOn(cipherService as any, "getAllDecrypted").mockResolvedValue(null); + + const result = await cipherService.getPasskeyCiphers(); + + expect(result).toBe(null); + }); + }); });