mirror of
https://github.com/bitwarden/browser
synced 2025-12-19 09:43:23 +00:00
* 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
192 lines
8.3 KiB
TypeScript
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,
|
|
);
|
|
});
|
|
});
|
|
});
|