1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-22 20:34:04 +00:00
Files
browser/libs/common/src/vault/utils/cipher-view-like-utils.spec.ts
Shane Melton 9f0a565241 [PM-25682] Migrate CipherView and subviews to be TS strict compliant (#16463)
* [PM-25682] Remove ts-strict-ignore from Vault view models and update types to be strict

* [PM-25682] Ignore ViewEncryptableKeys error for old decrypt methods

* [PM-25682] Add null/undefined as possible types for isNull* and other helpers that include null checks internally

* [PM-25682] Use patchValue instead of setValue which does not support undefined values

* [PM-25682] Add type assertions and other misc. null checks where necessary

* [PM-25682] Fix importers specs

* [PM-25682] Cleanup card view/details

* [PM-25682] Fix cipher view hasAttachment helper

* [PM-25682] Cleanup unecessary null assignments in notification.background.spec.ts

* [PM-25682] Ensure linkedId is undefined instead of null

* [PM-25682] Cleanup misc typing errors

* [PM-25682] Make the CipherId required

* [PM-25682] Undo CipherId assertions

* [PM-25682] Undo brand initial value change

* [PM-25682] Update SshKeyView

* [PM-25682] Add constructor to Fido2CredentialView

* [PM-25682] Prettier

* [PM-25682] Fix strict type warnings after merge with main

* [PM-25682] Cleanup cipher view spec

* [PM-25682] Cleanup new type warnings after merge

* [PM-25682] Undo removed eslint-disable-next-line comment

* [PM-25682] Fix flaky test

* [PM-25682] Use satisfies instead of as for Fido2CredentialAutofillView
2025-10-07 11:40:57 -04:00

655 lines
23 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("isArchived", () => {
it("returns true when the cipher is archived", () => {
const cipherListView = {
id: "1",
archivedDate: "2024-02-02",
type: "identity",
} as unknown as CipherListView;
const cipherView = createCipherView();
cipherView.archivedDate = new Date();
expect(CipherViewLikeUtils.isArchived(cipherListView)).toBe(true);
expect(CipherViewLikeUtils.isArchived(cipherView)).toBe(true);
});
it("returns false when the cipher is not archived", () => {
const cipherListView = {
id: "2",
type: "identity",
} as unknown as CipherListView;
const cipherView = createCipherView();
expect(CipherViewLikeUtils.isArchived(cipherListView)).toBe(false);
expect(CipherViewLikeUtils.isArchived(cipherView)).toBe(false);
});
});
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" as any;
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" as any;
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);
cipherView.attachments = [];
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);
});
});
});