clients/apps/browser/src/vault/services/fido2-user-verification.service.spec.ts
Dave cf6569bfea
feat(user-decryption-options) [PM-26413]: Remove ActiveUserState from UserDecryptionOptionsService (#16894)
* feat(user-decryption-options) [PM-26413]: Update UserDecryptionOptionsService and tests to use UserId-only APIs.

* feat(user-decryption-options) [PM-26413]: Update InternalUserDecryptionOptionsService call sites to use UserId-only API.

* feat(user-decryption-options) [PM-26413] Update userDecryptionOptions$ call sites to use the UserId-only API.

* feat(user-decryption-options) [PM-26413]: Update additional call sites.

* feat(user-decryption-options) [PM-26413]: Update dependencies and an additional call site.

* feat(user-verification-service) [PM-26413]: Replace where allowed by unrestricted imports invocation of UserVerificationService.hasMasterPassword (deprecated) with UserDecryptionOptions.hasMasterPasswordById$. Additional work to complete as tech debt tracked in PM-27009.

* feat(user-decryption-options) [PM-26413]: Update for non-null strict adherence.

* feat(user-decryption-options) [PM-26413]: Update type safety and defensive returns.

* chore(user-decryption-options) [PM-26413]: Comment cleanup.

* feat(user-decryption-options) [PM-26413]: Update tests.

* feat(user-decryption-options) [PM-26413]: Standardize null-checking on active account id for new API consumption.

* feat(vault-timeout-settings-service) [PM-26413]: Add test cases to illustrate null active account from AccountService.

* fix(fido2-user-verification-service-spec) [PM-26413]: Update test harness to use FakeAccountService.

* fix(downstream-components) [PM-26413]: Prefer use of the getUserId operator in all authenticated contexts for user id provided to UserDecryptionOptionsService.

---------

Co-authored-by: bnagawiecki <107435978+bnagawiecki@users.noreply.github.com>
2025-11-25 11:23:22 -05:00

258 lines
9.9 KiB
TypeScript

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<PasswordRepromptService>;
let userDecryptionOptionsService: MockProxy<UserDecryptionOptionsServiceAbstraction>;
let dialogService: MockProxy<DialogService>;
let accountService: FakeAccountService;
let cipher: CipherView;
beforeEach(() => {
passwordRepromptService = mock<PasswordRepromptService>();
userDecryptionOptionsService = mock<UserDecryptionOptionsServiceAbstraction>();
dialogService = mock<DialogService>();
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;
}