diff --git a/libs/common/src/vault/models/view/cipher.view.spec.ts b/libs/common/src/vault/models/view/cipher.view.spec.ts index 2965a9b1c7f..475fe9e23f3 100644 --- a/libs/common/src/vault/models/view/cipher.view.spec.ts +++ b/libs/common/src/vault/models/view/cipher.view.spec.ts @@ -109,6 +109,72 @@ describe("CipherView", () => { expect(actual.key).toBeInstanceOf(EncString); expect(actual.key?.toJSON()).toBe(cipherKeyObject.toJSON()); }); + + it("fromJSON should always restore top-level CipherView properties", () => { + jest.spyOn(LoginView, "fromJSON").mockImplementation(mockFromJson); + // Create a fully populated CipherView instance + const original = new CipherView(); + original.id = "test-id"; + original.organizationId = "org-id"; + original.folderId = "folder-id"; + original.name = "test-name"; + original.notes = "test-notes"; + original.type = CipherType.Login; + original.favorite = true; + original.organizationUseTotp = true; + original.permissions = new CipherPermissionsApi(); + original.edit = true; + original.viewPassword = false; + original.localData = { lastUsedDate: Date.now() }; + original.login = new LoginView(); + original.identity = new IdentityView(); + original.card = new CardView(); + original.secureNote = new SecureNoteView(); + original.sshKey = new SshKeyView(); + original.attachments = []; + original.fields = []; + original.passwordHistory = []; + original.collectionIds = ["collection-1"]; + original.revisionDate = new Date("2022-01-01"); + original.creationDate = new Date("2022-01-02"); + original.deletedDate = new Date("2022-01-03"); + original.archivedDate = new Date("2022-01-04"); + original.reprompt = CipherRepromptType.Password; + original.key = new EncString("test-key"); + original.decryptionFailure = true; + + // Serialize and deserialize + const json = original.toJSON(); + const restored = CipherView.fromJSON(json as any); + + // Get all enumerable properties from the original instance + const originalProps = Object.keys(original); + + // Check that all properties exist on the restored instance + for (const prop of originalProps) { + try { + expect(restored).toHaveProperty(prop); + } catch { + throw new Error(`Property '${prop}' is missing from restored instance`); + } + + // For non-function, non-getter properties, verify the value is defined + const descriptor = Object.getOwnPropertyDescriptor(CipherView.prototype, prop); + if (!descriptor?.get && typeof (original as any)[prop] !== "function") { + try { + expect((restored as any)[prop]).toBeDefined(); + } catch { + throw new Error(`Property '${prop}' is undefined in restored instance`); + } + } + } + + // Verify restored instance has the same properties as original + const restoredProps = Object.keys(restored!).sort(); + const sortedOriginalProps = originalProps.sort(); + + expect(restoredProps).toEqual(sortedOriginalProps); + }); }); describe("fromSdkCipherView", () => { diff --git a/libs/common/src/vault/models/view/cipher.view.ts b/libs/common/src/vault/models/view/cipher.view.ts index 3381f0a47ab..c586297d6a5 100644 --- a/libs/common/src/vault/models/view/cipher.view.ts +++ b/libs/common/src/vault/models/view/cipher.view.ts @@ -196,7 +196,19 @@ export class CipherView implements View, InitializerMetadata { const view = new CipherView(); view.type = obj.type ?? CipherType.Login; view.id = obj.id ?? ""; + view.organizationId = obj.organizationId ?? undefined; + view.folderId = obj.folderId ?? undefined; + view.collectionIds = obj.collectionIds ?? []; view.name = obj.name ?? ""; + view.notes = obj.notes; + view.edit = obj.edit ?? false; + view.viewPassword = obj.viewPassword ?? true; + view.favorite = obj.favorite ?? false; + view.organizationUseTotp = obj.organizationUseTotp ?? false; + view.localData = obj.localData ? obj.localData : undefined; + view.permissions = obj.permissions ? CipherPermissionsApi.fromJSON(obj.permissions) : undefined; + view.reprompt = obj.reprompt ?? CipherRepromptType.None; + view.decryptionFailure = obj.decryptionFailure ?? false; if (obj.creationDate) { view.creationDate = new Date(obj.creationDate); } @@ -209,7 +221,6 @@ export class CipherView implements View, InitializerMetadata { view.fields = obj.fields?.map((f: any) => FieldView.fromJSON(f)) ?? []; view.passwordHistory = obj.passwordHistory?.map((ph: any) => PasswordHistoryView.fromJSON(ph)) ?? []; - view.permissions = obj.permissions ? CipherPermissionsApi.fromJSON(obj.permissions) : undefined; if (obj.key != null) { let key: EncString | undefined;