1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-17 01:04:01 +00:00

Added adapter and unit tests

This commit is contained in:
gbubemismith
2025-04-02 11:44:08 -04:00
parent f1843764d5
commit 1def6398d2
19 changed files with 516 additions and 3 deletions

View File

@@ -64,6 +64,20 @@ export function makeSymmetricCryptoKey<T extends SymmetricCryptoKey>(
*/
export const mockFromJson = (stub: any) => (stub + "_fromJSON") as any;
/**
* Use to mock a return value of a static fromSdk method.
*/
export const mockFromSdk = (stub: any) => {
if (typeof stub === "object") {
return {
...stub,
__fromSdk: true,
};
}
return `${stub}_fromSdk`;
};
/**
* Tracks the emissions of the given observable.
*

View File

@@ -1,5 +1,7 @@
import { Jsonify } from "type-fest";
import { CipherPermissions as SdkCipherPermissions } from "@bitwarden/sdk-internal";
import { BaseResponse } from "../../../models/response/base.response";
export class CipherPermissionsApi extends BaseResponse {
@@ -18,4 +20,19 @@ export class CipherPermissionsApi extends BaseResponse {
static fromJSON(obj: Jsonify<CipherPermissionsApi>) {
return Object.assign(new CipherPermissionsApi(), obj);
}
/**
* Converts the SDK CipherPermissionsApi to a CipherPermissionsApi.
*/
static fromSdkCipherPermissions(obj: SdkCipherPermissions): CipherPermissionsApi | undefined {
if (!obj) {
return undefined;
}
const permissions = new CipherPermissionsApi();
permissions.delete = obj.delete;
permissions.restore = obj.restore;
return permissions;
}
}

View File

@@ -355,7 +355,7 @@ export class Cipher extends Domain implements Decryptable<CipherView> {
favorite: this.favorite,
organizationUseTotp: this.organizationUseTotp,
edit: this.edit,
permissions: undefined,
permissions: this.permissions,
viewPassword: this.viewPassword,
localData: this.localData
? { lastUsedDate: this.localData.lastUsedDate, lastLaunched: this.localData.lastLaunched }

View File

@@ -1,3 +1,5 @@
import { AttachmentView as SdkAttachmentView } from "@bitwarden/sdk-internal";
import { mockFromJson } from "../../../../spec";
import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
@@ -15,4 +17,41 @@ describe("AttachmentView", () => {
expect(actual.key).toEqual("encKeyB64_fromJSON");
});
describe("fromSdkAttachmentView", () => {
it("should return undefined when the input is null", () => {
const result = AttachmentView.fromSdkAttachmentView(null as unknown as any);
expect(result).toBeUndefined();
});
it("should return an AttachmentView from an SdkAttachmentView", () => {
const key = {
key: new Uint8Array([1, 2, 3]),
keyB64: "encKeyB64_fromString",
encKeyB64: "encKeyB64_fromString",
} as SymmetricCryptoKey;
jest.spyOn(SymmetricCryptoKey, "fromString").mockReturnValue(key);
const sdkAttachmentView = {
id: "id",
url: "url",
size: "size",
sizeName: "sizeName",
fileName: "fileName",
key: "encKeyB64_fromString",
} as SdkAttachmentView;
const result = AttachmentView.fromSdkAttachmentView(sdkAttachmentView);
expect(result).toMatchObject({
id: "id",
url: "url",
size: "size",
sizeName: "sizeName",
fileName: "fileName",
key: key,
});
});
});
});

View File

@@ -2,6 +2,8 @@
// @ts-strict-ignore
import { Jsonify } from "type-fest";
import { AttachmentView as SdkAttachmentView } from "@bitwarden/sdk-internal";
import { View } from "../../../models/view/view";
import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
import { Attachment } from "../domain/attachment";
@@ -40,4 +42,23 @@ export class AttachmentView implements View {
const key = obj.key == null ? null : SymmetricCryptoKey.fromJSON(obj.key);
return Object.assign(new AttachmentView(), obj, { key: key });
}
/**
* Converts the SDK AttachmentView to a AttachmentView.
*/
static fromSdkAttachmentView(obj: SdkAttachmentView): AttachmentView | undefined {
if (!obj) {
return undefined;
}
const view = new AttachmentView();
view.id = obj.id;
view.url = obj.url;
view.size = obj.size;
view.sizeName = obj.sizeName;
view.fileName = obj.fileName;
view.key = obj.key ? SymmetricCryptoKey.fromString(obj.key) : null;
return view;
}
}

View File

@@ -2,6 +2,8 @@
// @ts-strict-ignore
import { Jsonify } from "type-fest";
import { CardView as SdkCardView } from "@bitwarden/sdk-internal";
import { normalizeExpiryYearFormat } from "../../../autofill/utils";
import { CardLinkedId as LinkedId } from "../../enums";
import { linkedFieldOption } from "../../linked-field-option.decorator";
@@ -146,4 +148,15 @@ export class CardView extends ItemView {
return null;
}
/**
* Converts an SDK CardView to a CardView.
*/
static fromSdkCardView(obj: SdkCardView): CardView | undefined {
if (obj == null) {
return undefined;
}
return Object.assign(new CardView(), obj);
}
}

View File

@@ -1,4 +1,16 @@
import { mockFromJson } from "../../../../spec";
import {
CipherView as SdkCipherView,
CipherType as SdkCipherType,
CipherRepromptType as SdkCipherRepromptType,
AttachmentView as SdkAttachmentView,
LoginUriView as SdkLoginUriView,
LoginView as SdkLoginView,
FieldView as SdkFieldView,
FieldType as SdkFieldType,
} from "@bitwarden/sdk-internal";
import { mockFromJson, mockFromSdk } from "../../../../spec";
import { CipherRepromptType } from "../../enums";
import { CipherType } from "../../enums/cipher-type";
import { AttachmentView } from "./attachment.view";
@@ -9,6 +21,7 @@ import { IdentityView } from "./identity.view";
import { LoginView } from "./login.view";
import { PasswordHistoryView } from "./password-history.view";
import { SecureNoteView } from "./secure-note.view";
import { SshKeyView } from "./ssh-key.view";
jest.mock("../../models/view/login.view");
jest.mock("../../models/view/attachment.view");
@@ -73,4 +86,121 @@ describe("CipherView", () => {
expect(actual).toMatchObject(expected);
});
});
describe("fromSdkCipherView", () => {
let sdkCipherView: SdkCipherView;
beforeEach(() => {
jest.spyOn(CardView, "fromSdkCardView").mockImplementation(mockFromSdk);
jest.spyOn(IdentityView, "fromSdkIdentityView").mockImplementation(mockFromSdk);
jest.spyOn(LoginView, "fromSdkLoginView").mockImplementation(mockFromSdk);
jest.spyOn(SecureNoteView, "fromSdkSecureNoteView").mockImplementation(mockFromSdk);
jest.spyOn(SshKeyView, "fromSdkSshKeyView").mockImplementation(mockFromSdk);
jest.spyOn(AttachmentView, "fromSdkAttachmentView").mockImplementation(mockFromSdk);
jest.spyOn(FieldView, "fromSdkFieldView").mockImplementation(mockFromSdk);
sdkCipherView = {
id: "id",
organizationId: "orgId",
folderId: "folderId",
collectionIds: ["collectionId"],
key: undefined,
name: "name",
notes: undefined,
type: SdkCipherType.Login,
favorite: true,
edit: true,
reprompt: SdkCipherRepromptType.None,
organizationUseTotp: false,
viewPassword: true,
localData: undefined,
permissions: undefined,
attachments: [{ id: "attachmentId", url: "attachmentUrl" } as SdkAttachmentView],
login: {
username: "username",
password: "password",
uris: [{ uri: "bitwarden.com" } as SdkLoginUriView],
totp: "totp",
autofillOnPageLoad: true,
} as SdkLoginView,
identity: undefined,
card: undefined,
secureNote: undefined,
sshKey: undefined,
fields: [
{
name: "fieldName",
value: "fieldValue",
type: SdkFieldType.Linked,
linkedId: 100,
} as SdkFieldView,
],
passwordHistory: undefined,
creationDate: "2022-01-01T12:00:00.000Z",
revisionDate: "2022-01-02T12:00:00.000Z",
deletedDate: undefined,
};
});
it("returns undefined when input is null", () => {
expect(CipherView.fromSdkCipherView(null as unknown as SdkCipherView)).toBeUndefined();
});
it("maps properties correctly", () => {
const result = CipherView.fromSdkCipherView(sdkCipherView);
expect(result).toMatchObject({
id: "id",
organizationId: "orgId",
folderId: "folderId",
collectionIds: ["collectionId"],
name: "name",
notes: undefined,
type: CipherType.Login,
favorite: true,
edit: true,
reprompt: CipherRepromptType.None,
organizationUseTotp: false,
viewPassword: true,
localData: undefined,
permissions: undefined,
attachments: [
{
id: "attachmentId",
url: "attachmentUrl",
__fromSdk: true,
},
],
login: {
username: "username",
password: "password",
uris: [
{
uri: "bitwarden.com",
},
],
totp: "totp",
autofillOnPageLoad: true,
__fromSdk: true,
},
identity: undefined,
card: undefined,
secureNote: undefined,
sshKey: undefined,
fields: [
{
name: "fieldName",
value: "fieldValue",
type: SdkFieldType.Linked,
linkedId: 100,
__fromSdk: true,
},
],
passwordHistory: undefined,
creationDate: new Date("2022-01-01T12:00:00.000Z"),
revisionDate: new Date("2022-01-02T12:00:00.000Z"),
deletedDate: null,
});
});
});
});

View File

@@ -1,5 +1,7 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { CipherView as SdkCipherView } from "@bitwarden/sdk-internal";
import { View } from "../../../models/view/view";
import { InitializerMetadata } from "../../../platform/interfaces/initializer-metadata.interface";
import { InitializerKey } from "../../../platform/services/cryptography/initializer-key";
@@ -222,4 +224,54 @@ export class CipherView implements View, InitializerMetadata {
return view;
}
/**
* Creates a CipherView from the SDK CipherView.
*/
static fromSdkCipherView(obj: SdkCipherView): CipherView | undefined {
if (obj == null) {
return undefined;
}
const cipherView = new CipherView();
const attachments = obj.attachments?.map((a) => AttachmentView.fromSdkAttachmentView(a));
const fields = obj.fields?.map((f) => FieldView.fromSdkFieldView(f));
const passwordHistory = obj.passwordHistory?.map((ph) =>
PasswordHistoryView.fromSdkPasswordHistoryView(ph),
);
const revisionDate = obj.revisionDate == null ? null : new Date(obj.revisionDate);
const creationDate = obj.creationDate == null ? null : new Date(obj.creationDate);
const deletedDate = obj.deletedDate == null ? null : new Date(obj.deletedDate);
Object.assign(cipherView, obj, {
revisionDate: revisionDate,
creationDate: creationDate,
deletedDate: deletedDate,
attachments: attachments,
fields: fields,
passwordHistory: passwordHistory,
});
switch (obj.type) {
case CipherType.Card:
cipherView.card = CardView.fromSdkCardView(obj.card);
break;
case CipherType.Identity:
cipherView.identity = IdentityView.fromSdkIdentityView(obj.identity);
break;
case CipherType.Login:
cipherView.login = LoginView.fromSdkLoginView(obj.login);
break;
case CipherType.SecureNote:
cipherView.secureNote = SecureNoteView.fromSdkSecureNoteView(obj.secureNote);
break;
case CipherType.SshKey:
cipherView.sshKey = SshKeyView.fromSdkSshKeyView(obj.sshKey);
break;
default:
break;
}
return cipherView;
}
}

View File

@@ -2,6 +2,8 @@
// @ts-strict-ignore
import { Jsonify } from "type-fest";
import { Fido2CredentialView as SdkFido2CredentialView } from "@bitwarden/sdk-internal";
import { ItemView } from "./item.view";
export class Fido2CredentialView extends ItemView {
@@ -29,4 +31,30 @@ export class Fido2CredentialView extends ItemView {
creationDate,
});
}
/**
* Converts the SDK Fido2CredentialView to a Fido2CredentialView.
*/
static fromSdkFido2CredentialView(obj: SdkFido2CredentialView): Fido2CredentialView | undefined {
if (!obj) {
return undefined;
}
const view = new Fido2CredentialView();
view.credentialId = obj.credentialId;
view.keyType = obj.keyType as "public-key";
view.keyAlgorithm = obj.keyAlgorithm as "ECDSA";
view.keyCurve = obj.keyCurve as "P-256";
view.keyValue = obj.keyValue;
view.rpId = obj.rpId;
view.userHandle = obj.userHandle;
view.userName = obj.userName;
view.counter = Number(obj.counter);
view.rpName = obj.rpName;
view.userDisplayName = obj.userDisplayName;
view.discoverable = obj.discoverable?.toLowerCase() === "true" ? true : false;
view.creationDate = obj.creationDate ? new Date(obj.creationDate) : null;
return view;
}
}

View File

@@ -2,6 +2,8 @@
// @ts-strict-ignore
import { Jsonify } from "type-fest";
import { FieldView as SdkFieldView } from "@bitwarden/sdk-internal";
import { View } from "../../../models/view/view";
import { FieldType, LinkedIdType } from "../../enums";
import { Field } from "../domain/field";
@@ -31,4 +33,21 @@ export class FieldView implements View {
static fromJSON(obj: Partial<Jsonify<FieldView>>): FieldView {
return Object.assign(new FieldView(), obj);
}
/**
* Converts the SDK FieldView to a FieldView.
*/
static fromSdkFieldView(obj: SdkFieldView): FieldView | undefined {
if (!obj) {
return undefined;
}
const view = new FieldView();
view.name = obj.name;
view.value = obj.value;
view.type = obj.type;
view.linkedId = obj.linkedId as unknown as LinkedIdType;
return view;
}
}

View File

@@ -2,6 +2,8 @@
// @ts-strict-ignore
import { Jsonify } from "type-fest";
import { IdentityView as SdkIdentityView } from "@bitwarden/sdk-internal";
import { Utils } from "../../../platform/misc/utils";
import { IdentityLinkedId as LinkedId } from "../../enums";
import { linkedFieldOption } from "../../linked-field-option.decorator";
@@ -158,4 +160,15 @@ export class IdentityView extends ItemView {
static fromJSON(obj: Partial<Jsonify<IdentityView>>): IdentityView {
return Object.assign(new IdentityView(), obj);
}
/**
* Converts the SDK IdentityView to an IdentityView.
*/
static fromSdkIdentityView(obj: SdkIdentityView): IdentityView | undefined {
if (obj == null) {
return undefined;
}
return Object.assign(new IdentityView(), obj);
}
}

View File

@@ -1,3 +1,5 @@
import { LoginUriView as SdkLoginUriView, UriMatchType } from "@bitwarden/sdk-internal";
import { UriMatchStrategy, UriMatchStrategySetting } from "../../../models/domain/domain-service";
import { Utils } from "../../../platform/misc/utils";
@@ -184,6 +186,26 @@ describe("LoginUriView", () => {
});
});
});
describe("fromSdkLoginUriView", () => {
it("should return undefined when the input is null", () => {
const result = LoginUriView.fromSdkLoginUriView(null as unknown as SdkLoginUriView);
expect(result).toBeUndefined();
});
it("should create a LoginUriView from a SdkLoginUriView", () => {
const sdkLoginUriView = {
uri: "https://example.com",
match: UriMatchType.Host,
} as SdkLoginUriView;
const loginUriView = LoginUriView.fromSdkLoginUriView(sdkLoginUriView);
expect(loginUriView).toBeInstanceOf(LoginUriView);
expect(loginUriView!.uri).toBe(sdkLoginUriView.uri);
expect(loginUriView!.match).toBe(sdkLoginUriView.match);
});
});
});
function uriFactory(match: UriMatchStrategySetting, uri: string) {

View File

@@ -2,6 +2,8 @@
// @ts-strict-ignore
import { Jsonify } from "type-fest";
import { LoginUriView as SdkLoginUriView } from "@bitwarden/sdk-internal";
import { UriMatchStrategy, UriMatchStrategySetting } from "../../../models/domain/domain-service";
import { View } from "../../../models/view/view";
import { SafeUrls } from "../../../platform/misc/safe-urls";
@@ -112,6 +114,21 @@ export class LoginUriView implements View {
return Object.assign(new LoginUriView(), obj);
}
/**
* Converts a LoginUriView object from the SDK to a LoginUriView object.
*/
static fromSdkLoginUriView(obj: SdkLoginUriView): LoginUriView | undefined {
if (obj == null) {
return undefined;
}
const view = new LoginUriView();
view.uri = obj.uri;
view.match = obj.match;
return view;
}
matchesUri(
targetUri: string,
equivalentDomains: Set<string>,

View File

@@ -1,4 +1,6 @@
import { mockFromJson } from "../../../../spec";
import { LoginView as SdkLoginView } from "@bitwarden/sdk-internal";
import { mockFromJson, mockFromSdk } from "../../../../spec";
import { LoginUriView } from "./login-uri.view";
import { LoginView } from "./login.view";
@@ -25,4 +27,35 @@ describe("LoginView", () => {
uris: ["uri1_fromJSON", "uri2_fromJSON", "uri3_fromJSON"],
});
});
describe("fromSdkLoginView", () => {
it("should return undefined when the input is null", () => {
const result = LoginView.fromSdkLoginView(null as unknown as SdkLoginView);
expect(result).toBeUndefined();
});
it("should return a LoginView from an SdkLoginView", () => {
jest.spyOn(LoginUriView, "fromSdkLoginUriView").mockImplementation(mockFromSdk);
const sdkLoginView = {
username: "username",
password: "password",
passwordRevisionDate: "2025-01-01T01:06:40.441Z",
uris: [{ uri: "bitwarden.com" } as any],
totp: "totp",
autofillOnPageLoad: true,
} as SdkLoginView;
const result = LoginView.fromSdkLoginView(sdkLoginView);
expect(result).toMatchObject({
username: "username",
password: "password",
passwordRevisionDate: new Date("2025-01-01T01:06:40.441Z"),
uris: [expect.objectContaining({ uri: "bitwarden.com", __fromSdk: true })],
totp: "totp",
autofillOnPageLoad: true,
});
});
});
});

View File

@@ -1,5 +1,7 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { LoginView as SdkLoginView } from "@bitwarden/sdk-internal";
import { UriMatchStrategySetting } from "../../../models/domain/domain-service";
import { Utils } from "../../../platform/misc/utils";
import { DeepJsonify } from "../../../types/deep-jsonify";
@@ -100,4 +102,27 @@ export class LoginView extends ItemView {
fido2Credentials,
});
}
/**
* Converts the SDK LoginView to a LoginView.
*
* Note: FIDO2 credentials remain encrypted at this stage.
* Unlike other fields that are decrypted as part of the LoginView, the SDK maintains
* the FIDO2 credentials in encrypted form. We can decrypt them later using a separate
* call to client.vault().ciphers().decrypt_fido2_credentials().
*/
static fromSdkLoginView(obj: SdkLoginView): LoginView | undefined {
if (obj == null) {
return undefined;
}
const passwordRevisionDate =
obj.passwordRevisionDate == null ? null : new Date(obj.passwordRevisionDate);
const uris = obj.uris?.map((uri) => LoginUriView.fromSdkLoginUriView(uri));
return Object.assign(new LoginView(), obj, {
passwordRevisionDate,
uris,
});
}
}

View File

@@ -1,3 +1,5 @@
import { PasswordHistoryView as SdkPasswordHistoryView } from "@bitwarden/sdk-internal";
import { PasswordHistoryView } from "./password-history.view";
describe("PasswordHistoryView", () => {
@@ -10,4 +12,25 @@ describe("PasswordHistoryView", () => {
expect(actual.lastUsedDate).toEqual(lastUsedDate);
});
describe("fromSdkPasswordHistoryView", () => {
it("should return undefined when the input is null", () => {
const result = PasswordHistoryView.fromSdkPasswordHistoryView(null as unknown as any);
expect(result).toBeUndefined();
});
it("should return a PasswordHistoryView from an SdkPasswordHistoryView", () => {
const sdkPasswordHistoryView = {
password: "password",
lastUsedDate: "2023-10-01T00:00:00Z",
} as SdkPasswordHistoryView;
const result = PasswordHistoryView.fromSdkPasswordHistoryView(sdkPasswordHistoryView);
expect(result).toMatchObject({
password: "password",
lastUsedDate: new Date("2023-10-01T00:00:00Z"),
});
});
});
});

View File

@@ -2,6 +2,8 @@
// @ts-strict-ignore
import { Jsonify } from "type-fest";
import { PasswordHistoryView as SdkPasswordHistoryView } from "@bitwarden/sdk-internal";
import { View } from "../../../models/view/view";
import { Password } from "../domain/password";
@@ -24,4 +26,19 @@ export class PasswordHistoryView implements View {
lastUsedDate: lastUsedDate,
});
}
/**
* Converts the SDK PasswordHistoryView to a PasswordHistoryView.
*/
static fromSdkPasswordHistoryView(obj: SdkPasswordHistoryView): PasswordHistoryView | undefined {
if (!obj) {
return undefined;
}
const view = new PasswordHistoryView();
view.password = obj.password;
view.lastUsedDate = obj.lastUsedDate == null ? null : new Date(obj.lastUsedDate);
return view;
}
}

View File

@@ -2,6 +2,8 @@
// @ts-strict-ignore
import { Jsonify } from "type-fest";
import { SecureNoteView as SdkSecureNoteView } from "@bitwarden/sdk-internal";
import { SecureNoteType } from "../../enums";
import { SecureNote } from "../domain/secure-note";
@@ -26,4 +28,15 @@ export class SecureNoteView extends ItemView {
static fromJSON(obj: Partial<Jsonify<SecureNoteView>>): SecureNoteView {
return Object.assign(new SecureNoteView(), obj);
}
/**
* Converts the SDK SecureNoteView to a SecureNoteView.
*/
static fromSdkSecureNoteView(obj: SdkSecureNoteView): SecureNoteView | undefined {
if (!obj) {
return undefined;
}
return Object.assign(new SecureNoteView(), obj);
}
}

View File

@@ -2,6 +2,8 @@
// @ts-strict-ignore
import { Jsonify } from "type-fest";
import { SshKeyView as SdkSshKeyView } from "@bitwarden/sdk-internal";
import { SshKey } from "../domain/ssh-key";
import { ItemView } from "./item.view";
@@ -44,4 +46,19 @@ export class SshKeyView extends ItemView {
static fromJSON(obj: Partial<Jsonify<SshKeyView>>): SshKeyView {
return Object.assign(new SshKeyView(), obj);
}
/**
* Converts the SDK SshKeyView to a SshKeyView.
*/
static fromSdkSshKeyView(obj: SdkSshKeyView): SshKeyView | undefined {
if (!obj) {
return undefined;
}
const keyFingerprint = obj.fingerprint;
return Object.assign(new SshKeyView(), obj, {
keyFingerprint,
});
}
}