1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-19 09:43:23 +00:00
Files
browser/libs/vault/src/services/copy-cipher-field.service.spec.ts
Nick Krantz b4120e0e3f [PM-22134] Migrate list views to CipherListView from the SDK (#15174)
* add `CipherViewLike` and utilities to handle `CipherView` and `CipherViewLike`

* migrate libs needed for web vault to support `CipherViewLike`

* migrate web vault components to support

* add  for CipherView.  will have to be later

* fetch full CipherView for copying a password

* have only the cipher service utilize SDK migration flag

- This keeps feature flag logic away from the component
- Also cuts down on what is needed for other platforms

* strongly type CipherView for AC vault

- Probably temporary before migration of the AC vault to `CipherListView` SDK

* fix build icon tests by being more gracious with the uri structure

* migrate desktop components to CipherListViews$

* consume card from sdk

* add browser implementation for `CipherListView`

* update copy message for single copiable items

* refactor `getCipherViewLikeLogin` to `getLogin`

* refactor `getCipherViewLikeCard` to `getCard`

* add `hasFido2Credentials` helper

* add decryption failure to cipher like utils

* add todo with ticket

* fix decryption failure typing

* fix copy card messages

* fix addition of organizations and collections for `PopupCipherViewLike`

- accessors were being lost

* refactor to getters to fix re-rendering bug

* fix decryption failure helper

* fix sorting functions for `CipherViewLike`

* formatting

* add `CipherViewLikeUtils` tests

* refactor "copiable" to "copyable" to match SDK

* use `hasOldAttachments` from cipherlistview

* fix typing

* update SDK version

* add feature flag for cipher list view work

* use `CipherViewLikeUtils` for copyable values rather than referring to the cipher directly

* update restricted item type to support CipherViewLike

* add cipher support to `CipherViewLikeUtils`

* update `isCipherListView` check

* refactor CipherLike to a separate type

* refactor `getFullCipherView` into the cipher service

* add optional chaining for `uriChecksum`

* set empty array for decrypted CipherListView

* migrate nudge service to use `cipherListViews`

* update web vault to not depend on `cipherViews$`

* update popup list filters to use `CipherListView`

* fix storybook

* fix tests

* accept undefined as a MY VAULT filter value for cipher list views

* use `LoginUriView` for uri logic (#15530)

* filter out null ciphers from the `_allDecryptedCiphers$` (#15539)

* use `launchUri` to avoid any unexpected behavior in URIs - this appends `http://` when missing
2025-07-17 14:55:32 -05:00

192 lines
8.3 KiB
TypeScript

import { mock, MockProxy } from "jest-mock-extended";
import { of } from "rxjs";
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
import { AccountService, Account } from "@bitwarden/common/auth/abstractions/account.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { EventType } from "@bitwarden/common/enums";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service";
import { CipherRepromptType, CipherType } from "@bitwarden/common/vault/enums";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { LoginView } from "@bitwarden/common/vault/models/view/login.view";
import { ToastService } from "@bitwarden/components";
import { CopyAction, CopyCipherFieldService, PasswordRepromptService } from "@bitwarden/vault";
describe("CopyCipherFieldService", () => {
let service: CopyCipherFieldService;
let platformUtilsService: MockProxy<PlatformUtilsService>;
let toastService: MockProxy<ToastService>;
let eventCollectionService: MockProxy<EventCollectionService>;
let passwordRepromptService: MockProxy<PasswordRepromptService>;
let totpService: MockProxy<TotpService>;
let i18nService: MockProxy<I18nService>;
let billingAccountProfileStateService: MockProxy<BillingAccountProfileStateService>;
let accountService: MockProxy<AccountService>;
const userId = "userId";
beforeEach(() => {
platformUtilsService = mock<PlatformUtilsService>();
toastService = mock<ToastService>();
eventCollectionService = mock<EventCollectionService>();
passwordRepromptService = mock<PasswordRepromptService>();
totpService = mock<TotpService>();
i18nService = mock<I18nService>();
billingAccountProfileStateService = mock<BillingAccountProfileStateService>();
accountService = mock<AccountService>();
accountService.activeAccount$ = of({ id: userId } as Account);
service = new CopyCipherFieldService(
platformUtilsService,
toastService,
eventCollectionService,
passwordRepromptService,
totpService,
i18nService,
billingAccountProfileStateService,
accountService,
);
});
describe("copy", () => {
let cipher: CipherView;
let valueToCopy: string;
let actionType: CopyAction;
let skipReprompt: boolean;
beforeEach(() => {
cipher = mock<CipherView>();
valueToCopy = "test";
actionType = "username";
skipReprompt = false;
});
it("should return early when valueToCopy is null", async () => {
valueToCopy = null;
const result = await service.copy(valueToCopy, actionType, cipher, skipReprompt);
expect(result).toBeFalsy();
expect(platformUtilsService.copyToClipboard).not.toHaveBeenCalled();
});
it("should copy value to clipboard", async () => {
const result = await service.copy(valueToCopy, actionType, cipher, skipReprompt);
expect(result).toBeTruthy();
expect(platformUtilsService.copyToClipboard).toHaveBeenCalledWith(valueToCopy);
});
it("should show a success toast on copy", async () => {
i18nService.t.mockReturnValueOnce("Username").mockReturnValueOnce("Username copied");
const result = await service.copy(valueToCopy, actionType, cipher, skipReprompt);
expect(result).toBeTruthy();
expect(toastService.showToast).toHaveBeenCalledWith({
variant: "success",
message: "Username copied",
title: "",
});
expect(i18nService.t).toHaveBeenCalledWith("username");
expect(i18nService.t).toHaveBeenCalledWith("valueCopied", "Username");
});
describe("password reprompt", () => {
beforeEach(() => {
actionType = "password";
cipher.reprompt = CipherRepromptType.Password;
});
it("should show password prompt when actionType requires it", async () => {
passwordRepromptService.showPasswordPrompt.mockResolvedValue(true);
const result = await service.copy(valueToCopy, actionType, cipher, skipReprompt);
expect(result).toBeTruthy();
expect(passwordRepromptService.showPasswordPrompt).toHaveBeenCalled();
});
it("should skip password prompt when cipher.reprompt is 'None'", async () => {
cipher.reprompt = CipherRepromptType.None;
const result = await service.copy(valueToCopy, actionType, cipher, skipReprompt);
expect(result).toBeTruthy();
expect(passwordRepromptService.showPasswordPrompt).not.toHaveBeenCalled();
expect(platformUtilsService.copyToClipboard).toHaveBeenCalled();
});
it("should skip password prompt when skipReprompt is true", async () => {
skipReprompt = true;
const result = await service.copy(valueToCopy, actionType, cipher, skipReprompt);
expect(result).toBeTruthy();
expect(passwordRepromptService.showPasswordPrompt).not.toHaveBeenCalled();
});
it("should return early when password prompt is not confirmed", async () => {
passwordRepromptService.showPasswordPrompt.mockResolvedValue(false);
const result = await service.copy(valueToCopy, actionType, cipher, skipReprompt);
expect(result).toBeFalsy();
expect(platformUtilsService.copyToClipboard).not.toHaveBeenCalled();
});
});
describe("totp", () => {
beforeEach(() => {
actionType = "totp";
cipher.type = CipherType.Login;
cipher.login = new LoginView();
cipher.login.totp = "secret-totp";
cipher.reprompt = CipherRepromptType.None;
cipher.organizationUseTotp = false;
});
it("should get TOTP code when allowed from premium", async () => {
billingAccountProfileStateService.hasPremiumFromAnySource$.mockReturnValue(of(true));
totpService.getCode$.mockReturnValue(of({ code: "123456", period: 30 }));
const result = await service.copy(valueToCopy, actionType, cipher, skipReprompt);
expect(result).toBeTruthy();
expect(totpService.getCode$).toHaveBeenCalledWith(valueToCopy);
expect(platformUtilsService.copyToClipboard).toHaveBeenCalledWith("123456");
expect(billingAccountProfileStateService.hasPremiumFromAnySource$).toHaveBeenCalledWith(
userId,
);
});
it("should get TOTP code when allowed from organization", async () => {
cipher.organizationUseTotp = true;
totpService.getCode$.mockReturnValue(of({ code: "123456", period: 30 }));
const result = await service.copy(valueToCopy, actionType, cipher, skipReprompt);
expect(result).toBeTruthy();
expect(totpService.getCode$).toHaveBeenCalledWith(valueToCopy);
expect(platformUtilsService.copyToClipboard).toHaveBeenCalledWith("123456");
});
it("should return early when the user is not allowed to use TOTP", async () => {
billingAccountProfileStateService.hasPremiumFromAnySource$.mockReturnValue(of(false));
const result = await service.copy(valueToCopy, actionType, cipher, skipReprompt);
expect(result).toBeFalsy();
expect(totpService.getCode$).not.toHaveBeenCalled();
expect(platformUtilsService.copyToClipboard).not.toHaveBeenCalled();
expect(billingAccountProfileStateService.hasPremiumFromAnySource$).toHaveBeenCalledWith(
userId,
);
});
it("should return early when TOTP is not set", async () => {
cipher.login.totp = null;
const result = await service.copy(valueToCopy, actionType, cipher, skipReprompt);
expect(result).toBeFalsy();
expect(totpService.getCode$).not.toHaveBeenCalled();
expect(platformUtilsService.copyToClipboard).not.toHaveBeenCalled();
});
});
it("should collect an event when actionType has one", async () => {
actionType = "password";
skipReprompt = true;
await service.copy(valueToCopy, actionType, cipher, skipReprompt);
expect(eventCollectionService.collect).toHaveBeenCalledWith(
EventType.Cipher_ClientCopiedPassword,
cipher.id,
false,
cipher.organizationId,
);
});
});
});