1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-28 10:33:31 +00:00
Files
browser/libs/common/src/vault/utils/cipher-view-like-utils.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

625 lines
22 KiB
TypeScript

import { CipherListView } from "@bitwarden/sdk-internal";
import { CipherType } from "../enums";
import { Attachment } from "../models/domain/attachment";
import { AttachmentView } from "../models/view/attachment.view";
import { CipherView } from "../models/view/cipher.view";
import { Fido2CredentialView } from "../models/view/fido2-credential.view";
import { IdentityView } from "../models/view/identity.view";
import { LoginUriView } from "../models/view/login-uri.view";
import { LoginView } from "../models/view/login.view";
import { CipherViewLikeUtils } from "./cipher-view-like-utils";
describe("CipherViewLikeUtils", () => {
const createCipherView = (type: CipherType = CipherType.Login): CipherView => {
const cipherView = new CipherView();
// Always set a type to avoid issues within `CipherViewLikeUtils`
cipherView.type = type;
return cipherView;
};
describe("isCipherListView", () => {
it("returns true when the cipher is a CipherListView", () => {
const cipherListViewLogin = {
type: {
login: {},
},
} as CipherListView;
const cipherListViewSshKey = {
type: "sshKey",
} as CipherListView;
expect(CipherViewLikeUtils.isCipherListView(cipherListViewLogin)).toBe(true);
expect(CipherViewLikeUtils.isCipherListView(cipherListViewSshKey)).toBe(true);
});
it("returns false when the cipher is not a CipherListView", () => {
const cipherView = createCipherView();
cipherView.type = CipherType.SecureNote;
expect(CipherViewLikeUtils.isCipherListView(cipherView)).toBe(false);
});
});
describe("getLogin", () => {
it("returns null when the cipher is not a login", () => {
const cipherView = createCipherView(CipherType.SecureNote);
expect(CipherViewLikeUtils.getLogin(cipherView)).toBeNull();
expect(CipherViewLikeUtils.getLogin({ type: "identity" } as CipherListView)).toBeNull();
});
describe("CipherView", () => {
it("returns the login object", () => {
const cipherView = createCipherView(CipherType.Login);
expect(CipherViewLikeUtils.getLogin(cipherView)).toEqual(cipherView.login);
});
});
describe("CipherListView", () => {
it("returns the login object", () => {
const cipherListView = {
type: {
login: {
username: "testuser",
hasFido2: false,
},
},
} as CipherListView;
expect(CipherViewLikeUtils.getLogin(cipherListView)).toEqual(
(cipherListView.type as any).login,
);
});
});
});
describe("getCard", () => {
it("returns null when the cipher is not a card", () => {
const cipherView = createCipherView(CipherType.SecureNote);
expect(CipherViewLikeUtils.getCard(cipherView)).toBeNull();
expect(CipherViewLikeUtils.getCard({ type: "identity" } as CipherListView)).toBeNull();
});
describe("CipherView", () => {
it("returns the card object", () => {
const cipherView = createCipherView(CipherType.Card);
expect(CipherViewLikeUtils.getCard(cipherView)).toEqual(cipherView.card);
});
});
describe("CipherListView", () => {
it("returns the card object", () => {
const cipherListView = {
type: {
card: {
brand: "Visa",
},
},
} as CipherListView;
expect(CipherViewLikeUtils.getCard(cipherListView)).toEqual(
(cipherListView.type as any).card,
);
});
});
});
describe("isDeleted", () => {
it("returns true when the cipher is deleted", () => {
const cipherListView = { deletedDate: "2024-02-02", type: "identity" } as CipherListView;
const cipherView = createCipherView();
cipherView.deletedDate = new Date();
expect(CipherViewLikeUtils.isDeleted(cipherListView)).toBe(true);
expect(CipherViewLikeUtils.isDeleted(cipherView)).toBe(true);
});
it("returns false when the cipher is not deleted", () => {
const cipherListView = { deletedDate: undefined, type: "identity" } as CipherListView;
const cipherView = createCipherView();
expect(CipherViewLikeUtils.isDeleted(cipherListView)).toBe(false);
expect(CipherViewLikeUtils.isDeleted(cipherView)).toBe(false);
});
});
describe("canAssignToCollections", () => {
describe("CipherView", () => {
let cipherView: CipherView;
beforeEach(() => {
cipherView = createCipherView();
});
it("returns true when the cipher is not assigned to an organization", () => {
expect(CipherViewLikeUtils.canAssignToCollections(cipherView)).toBe(true);
});
it("returns false when the cipher is assigned to an organization and cannot be edited", () => {
cipherView.organizationId = "org-id";
cipherView.edit = false;
cipherView.viewPassword = false;
expect(CipherViewLikeUtils.canAssignToCollections(cipherView)).toBe(false);
});
it("returns true when the cipher is assigned to an organization and can be edited", () => {
cipherView.organizationId = "org-id";
cipherView.edit = true;
cipherView.viewPassword = true;
expect(CipherViewLikeUtils.canAssignToCollections(cipherView)).toBe(true);
});
});
describe("CipherListView", () => {
let cipherListView: CipherListView;
beforeEach(() => {
cipherListView = {
organizationId: undefined,
edit: false,
viewPassword: false,
type: { login: {} },
} as CipherListView;
});
it("returns true when the cipher is not assigned to an organization", () => {
expect(CipherViewLikeUtils.canAssignToCollections(cipherListView)).toBe(true);
});
it("returns false when the cipher is assigned to an organization and cannot be edited", () => {
cipherListView.organizationId = "org-id";
expect(CipherViewLikeUtils.canAssignToCollections(cipherListView)).toBe(false);
});
it("returns true when the cipher is assigned to an organization and can be edited", () => {
cipherListView.organizationId = "org-id";
cipherListView.edit = true;
cipherListView.viewPassword = true;
expect(CipherViewLikeUtils.canAssignToCollections(cipherListView)).toBe(true);
});
});
});
describe("getType", () => {
describe("CipherView", () => {
it("returns the type of the cipher", () => {
const cipherView = createCipherView();
cipherView.type = CipherType.Login;
expect(CipherViewLikeUtils.getType(cipherView)).toBe(CipherType.Login);
cipherView.type = CipherType.SecureNote;
expect(CipherViewLikeUtils.getType(cipherView)).toBe(CipherType.SecureNote);
cipherView.type = CipherType.SshKey;
expect(CipherViewLikeUtils.getType(cipherView)).toBe(CipherType.SshKey);
cipherView.type = CipherType.Identity;
expect(CipherViewLikeUtils.getType(cipherView)).toBe(CipherType.Identity);
cipherView.type = CipherType.Card;
expect(CipherViewLikeUtils.getType(cipherView)).toBe(CipherType.Card);
});
});
describe("CipherListView", () => {
it("converts the `CipherViewListType` to `CipherType`", () => {
const cipherListView = {
type: { login: {} },
} as CipherListView;
expect(CipherViewLikeUtils.getType(cipherListView)).toBe(CipherType.Login);
cipherListView.type = { card: { brand: "Visa" } };
expect(CipherViewLikeUtils.getType(cipherListView)).toBe(CipherType.Card);
cipherListView.type = "sshKey";
expect(CipherViewLikeUtils.getType(cipherListView)).toBe(CipherType.SshKey);
cipherListView.type = "identity";
expect(CipherViewLikeUtils.getType(cipherListView)).toBe(CipherType.Identity);
cipherListView.type = "secureNote";
expect(CipherViewLikeUtils.getType(cipherListView)).toBe(CipherType.SecureNote);
});
});
});
describe("subtitle", () => {
describe("CipherView", () => {
it("returns the subtitle of the cipher", () => {
const cipherView = createCipherView();
cipherView.login = new LoginView();
cipherView.login.username = "Test Username";
expect(CipherViewLikeUtils.subtitle(cipherView)).toBe("Test Username");
});
});
describe("CipherListView", () => {
it("returns the subtitle of the cipher", () => {
const cipherListView = {
subtitle: "Test Subtitle",
type: "identity",
} as CipherListView;
expect(CipherViewLikeUtils.subtitle(cipherListView)).toBe("Test Subtitle");
});
});
});
describe("hasAttachments", () => {
describe("CipherView", () => {
it("returns true when the cipher has attachments", () => {
const cipherView = createCipherView();
cipherView.attachments = [new AttachmentView({ id: "1" } as Attachment)];
expect(CipherViewLikeUtils.hasAttachments(cipherView)).toBe(true);
});
it("returns false when the cipher has no attachments", () => {
const cipherView = new CipherView();
(cipherView.attachments as any) = null;
expect(CipherViewLikeUtils.hasAttachments(cipherView)).toBe(false);
});
});
describe("CipherListView", () => {
it("returns true when there are attachments", () => {
const cipherListView = { attachments: 1, type: "secureNote" } as CipherListView;
expect(CipherViewLikeUtils.hasAttachments(cipherListView)).toBe(true);
});
it("returns false when there are no attachments", () => {
const cipherListView = { attachments: 0, type: "secureNote" } as CipherListView;
expect(CipherViewLikeUtils.hasAttachments(cipherListView)).toBe(false);
});
});
});
describe("canLaunch", () => {
it("returns false when the cipher is not a login", () => {
const cipherView = createCipherView(CipherType.SecureNote);
expect(CipherViewLikeUtils.canLaunch(cipherView)).toBe(false);
expect(CipherViewLikeUtils.canLaunch({ type: "identity" } as CipherListView)).toBe(false);
});
describe("CipherView", () => {
it("returns true when the login has URIs that can be launched", () => {
const cipherView = createCipherView(CipherType.Login);
cipherView.login = new LoginView();
cipherView.login.uris = [{ uri: "https://example.com" } as LoginUriView];
expect(CipherViewLikeUtils.canLaunch(cipherView)).toBe(true);
});
it("returns true when the uri does not have a protocol", () => {
const cipherView = createCipherView(CipherType.Login);
cipherView.login = new LoginView();
const uriView = new LoginUriView();
uriView.uri = "bitwarden.com";
cipherView.login.uris = [uriView];
expect(CipherViewLikeUtils.canLaunch(cipherView)).toBe(true);
});
it("returns false when the login has no URIs", () => {
const cipherView = createCipherView(CipherType.Login);
cipherView.login = new LoginView();
expect(CipherViewLikeUtils.canLaunch(cipherView)).toBe(false);
});
});
describe("CipherListView", () => {
it("returns true when the login has URIs that can be launched", () => {
const cipherListView = {
type: { login: { uris: [{ uri: "https://example.com" }] } },
} as CipherListView;
expect(CipherViewLikeUtils.canLaunch(cipherListView)).toBe(true);
});
it("returns true when the uri does not have a protocol", () => {
const cipherListView = {
type: { login: { uris: [{ uri: "bitwarden.com" }] } },
} as CipherListView;
expect(CipherViewLikeUtils.canLaunch(cipherListView)).toBe(true);
});
it("returns false when the login has no URIs", () => {
const cipherListView = { type: { login: {} } } as CipherListView;
expect(CipherViewLikeUtils.canLaunch(cipherListView)).toBe(false);
});
});
});
describe("getLaunchUri", () => {
it("returns undefined when the cipher is not a login", () => {
const cipherView = createCipherView(CipherType.SecureNote);
expect(CipherViewLikeUtils.getLaunchUri(cipherView)).toBeUndefined();
expect(
CipherViewLikeUtils.getLaunchUri({ type: "identity" } as CipherListView),
).toBeUndefined();
});
describe("CipherView", () => {
it("returns the first launch-able URI", () => {
const cipherView = createCipherView(CipherType.Login);
cipherView.login = new LoginView();
cipherView.login.uris = [
{ uri: "" } as LoginUriView,
{ uri: "https://example.com" } as LoginUriView,
{ uri: "https://another.com" } as LoginUriView,
];
expect(CipherViewLikeUtils.getLaunchUri(cipherView)).toBe("https://example.com");
});
it("returns undefined when there are no URIs", () => {
const cipherView = createCipherView(CipherType.Login);
cipherView.login = new LoginView();
expect(CipherViewLikeUtils.getLaunchUri(cipherView)).toBeUndefined();
});
it("appends protocol when there are none", () => {
const cipherView = createCipherView(CipherType.Login);
cipherView.login = new LoginView();
const uriView = new LoginUriView();
uriView.uri = "bitwarden.com";
cipherView.login.uris = [uriView];
expect(CipherViewLikeUtils.getLaunchUri(cipherView)).toBe("http://bitwarden.com");
});
});
describe("CipherListView", () => {
it("returns the first launch-able URI", () => {
const cipherListView = {
type: { login: { uris: [{ uri: "" }, { uri: "https://example.com" }] } },
} as CipherListView;
expect(CipherViewLikeUtils.getLaunchUri(cipherListView)).toBe("https://example.com");
});
it("returns undefined when there are no URIs", () => {
const cipherListView = { type: { login: {} } } as CipherListView;
expect(CipherViewLikeUtils.getLaunchUri(cipherListView)).toBeUndefined();
});
});
});
describe("matchesUri", () => {
const emptySet = new Set<string>();
it("returns false when the cipher is not a login", () => {
const cipherView = createCipherView(CipherType.SecureNote);
expect(CipherViewLikeUtils.matchesUri(cipherView, "https://example.com", emptySet)).toBe(
false,
);
});
describe("CipherView", () => {
it("returns true when the URI matches", () => {
const cipherView = createCipherView(CipherType.Login);
cipherView.login = new LoginView();
const uri = new LoginUriView();
uri.uri = "https://example.com";
cipherView.login.uris = [uri];
expect(CipherViewLikeUtils.matchesUri(cipherView, "https://example.com", emptySet)).toBe(
true,
);
});
it("returns false when the URI does not match", () => {
const cipherView = createCipherView(CipherType.Login);
cipherView.login = new LoginView();
const uri = new LoginUriView();
uri.uri = "https://www.bitwarden.com";
cipherView.login.uris = [uri];
expect(
CipherViewLikeUtils.matchesUri(cipherView, "https://www.another.com", emptySet),
).toBe(false);
});
});
describe("CipherListView", () => {
it("returns true when the URI matches", () => {
const cipherListView = {
type: { login: { uris: [{ uri: "https://example.com" }] } },
} as CipherListView;
expect(
CipherViewLikeUtils.matchesUri(cipherListView, "https://example.com", emptySet),
).toBe(true);
});
it("returns false when the URI does not match", () => {
const cipherListView = {
type: { login: { uris: [{ uri: "https://bitwarden.com" }] } },
} as CipherListView;
expect(
CipherViewLikeUtils.matchesUri(cipherListView, "https://another.com", emptySet),
).toBe(false);
});
});
});
describe("hasCopyableValue", () => {
describe("CipherView", () => {
it("returns true for login fields", () => {
const cipherView = createCipherView(CipherType.Login);
cipherView.login = new LoginView();
cipherView.login.username = "testuser";
cipherView.login.password = "testpass";
expect(CipherViewLikeUtils.hasCopyableValue(cipherView, "username")).toBe(true);
expect(CipherViewLikeUtils.hasCopyableValue(cipherView, "password")).toBe(true);
});
it("returns true for card fields", () => {
const cipherView = createCipherView(CipherType.Card);
cipherView.card = { number: "1234-5678-9012-3456", code: "123" } as any;
expect(CipherViewLikeUtils.hasCopyableValue(cipherView, "cardNumber")).toBe(true);
expect(CipherViewLikeUtils.hasCopyableValue(cipherView, "securityCode")).toBe(true);
});
it("returns true for identity fields", () => {
const cipherView = createCipherView(CipherType.Identity);
cipherView.identity = new IdentityView();
cipherView.identity.email = "example@bitwarden.com";
cipherView.identity.phone = "123-456-7890";
expect(CipherViewLikeUtils.hasCopyableValue(cipherView, "email")).toBe(true);
expect(CipherViewLikeUtils.hasCopyableValue(cipherView, "phone")).toBe(true);
});
it("returns false when values are not populated", () => {
const cipherView = createCipherView(CipherType.Login);
expect(CipherViewLikeUtils.hasCopyableValue(cipherView, "email")).toBe(false);
expect(CipherViewLikeUtils.hasCopyableValue(cipherView, "password")).toBe(false);
expect(CipherViewLikeUtils.hasCopyableValue(cipherView, "securityCode")).toBe(false);
expect(CipherViewLikeUtils.hasCopyableValue(cipherView, "username")).toBe(false);
});
});
describe("CipherListView", () => {
it("returns true for copyable fields in a login cipher", () => {
const cipherListView = {
type: { login: { username: "testuser" } },
copyableFields: ["LoginUsername", "LoginPassword"],
} as CipherListView;
expect(CipherViewLikeUtils.hasCopyableValue(cipherListView, "username")).toBe(true);
expect(CipherViewLikeUtils.hasCopyableValue(cipherListView, "password")).toBe(true);
});
it("returns true for copyable fields in a card cipher", () => {
const cipherListView = {
type: { card: { brand: "MasterCard" } },
copyableFields: ["CardNumber", "CardSecurityCode"],
} as CipherListView;
expect(CipherViewLikeUtils.hasCopyableValue(cipherListView, "cardNumber")).toBe(true);
expect(CipherViewLikeUtils.hasCopyableValue(cipherListView, "securityCode")).toBe(true);
});
it("returns true for copyable fields in an sshKey ciphers", () => {
const cipherListView = {
type: "sshKey",
copyableFields: ["SshKey"],
} as CipherListView;
expect(CipherViewLikeUtils.hasCopyableValue(cipherListView, "privateKey")).toBe(true);
expect(CipherViewLikeUtils.hasCopyableValue(cipherListView, "publicKey")).toBe(true);
expect(CipherViewLikeUtils.hasCopyableValue(cipherListView, "keyFingerprint")).toBe(true);
});
it("returns true for copyable fields in an identity cipher", () => {
const cipherListView = {
type: "identity",
copyableFields: ["IdentityUsername", "IdentityEmail", "IdentityPhone"],
} as CipherListView;
expect(CipherViewLikeUtils.hasCopyableValue(cipherListView, "username")).toBe(true);
expect(CipherViewLikeUtils.hasCopyableValue(cipherListView, "email")).toBe(true);
expect(CipherViewLikeUtils.hasCopyableValue(cipherListView, "phone")).toBe(true);
});
it("returns false for when missing a field", () => {
const cipherListView = {
type: { login: {} },
copyableFields: ["LoginUsername"],
} as CipherListView;
expect(CipherViewLikeUtils.hasCopyableValue(cipherListView, "password")).toBe(false);
expect(CipherViewLikeUtils.hasCopyableValue(cipherListView, "phone")).toBe(false);
expect(CipherViewLikeUtils.hasCopyableValue(cipherListView, "address")).toBe(false);
expect(CipherViewLikeUtils.hasCopyableValue(cipherListView, "publicKey")).toBe(false);
});
});
});
describe("hasFido2Credentials", () => {
describe("CipherView", () => {
it("returns true when the login has FIDO2 credentials", () => {
const cipherView = createCipherView(CipherType.Login);
cipherView.login = new LoginView();
cipherView.login.fido2Credentials = [new Fido2CredentialView()];
expect(CipherViewLikeUtils.hasFido2Credentials(cipherView)).toBe(true);
});
it("returns false when the login has no FIDO2 credentials", () => {
const cipherView = createCipherView(CipherType.Login);
cipherView.login = new LoginView();
expect(CipherViewLikeUtils.hasFido2Credentials(cipherView)).toBe(false);
});
});
describe("CipherListView", () => {
it("returns true when the login has FIDO2 credentials", () => {
const cipherListView = {
type: { login: { fido2Credentials: [{ credentialId: "fido2-1" }] } },
} as CipherListView;
expect(CipherViewLikeUtils.hasFido2Credentials(cipherListView)).toBe(true);
});
it("returns false when the login has no FIDO2 credentials", () => {
const cipherListView = { type: { login: {} } } as CipherListView;
expect(CipherViewLikeUtils.hasFido2Credentials(cipherListView)).toBe(false);
});
});
});
describe("decryptionFailure", () => {
it("returns true when the cipher has a decryption failure", () => {
const cipherView = createCipherView();
cipherView.decryptionFailure = true;
expect(CipherViewLikeUtils.decryptionFailure(cipherView)).toBe(true);
});
it("returns false when the cipher does not have a decryption failure", () => {
const cipherView = createCipherView();
cipherView.decryptionFailure = false;
expect(CipherViewLikeUtils.decryptionFailure(cipherView)).toBe(false);
});
it("returns false when the cipher is a CipherListView without decryptionFailure", () => {
const cipherListView = { type: "secureNote" } as CipherListView;
expect(CipherViewLikeUtils.decryptionFailure(cipherListView)).toBe(false);
});
});
});