1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-10 05:13:29 +00:00
Files
browser/libs/common/src/vault/services/cipher.service.spec.ts
Daniel Riera 2549afc45d PM-23890 Update overlays when a cipher is deleted (#15800)
* PM-23890 WIP

* add messaging service references

* Revert "PM-19574 Browser Autofill overlay displays outdated data (#15624)"

This reverts commit 9eceaa3d4c.

* add test

* fix merge spacing

* Revert "Revert "PM-19574 Browser Autofill overlay displays outdated data (#15624)""

This reverts commit 0fc6ec4d0b.
2025-08-05 12:17:38 -04:00

742 lines
29 KiB
TypeScript

import { mock } from "jest-mock-extended";
import { BehaviorSubject, filter, firstValueFrom, map, of } from "rxjs";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { CipherResponse } from "@bitwarden/common/vault/models/response/cipher.response";
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
// eslint-disable-next-line no-restricted-imports
import { CipherDecryptionKeys, KeyService } from "@bitwarden/key-management";
import { MessageSender } from "@bitwarden/messaging";
import { FakeAccountService, mockAccountServiceWith } from "../../../spec/fake-account-service";
import { FakeStateProvider } from "../../../spec/fake-state-provider";
import { makeStaticByteArray, makeSymmetricCryptoKey } from "../../../spec/utils";
import { ApiService } from "../../abstractions/api.service";
import { AutofillSettingsService } from "../../autofill/services/autofill-settings.service";
import { DomainSettingsService } from "../../autofill/services/domain-settings.service";
import { EncryptService } from "../../key-management/crypto/abstractions/encrypt.service";
import { EncString } from "../../key-management/crypto/models/enc-string";
import { UriMatchStrategy } from "../../models/domain/domain-service";
import { ConfigService } from "../../platform/abstractions/config/config.service";
import { I18nService } from "../../platform/abstractions/i18n.service";
import { StateService } from "../../platform/abstractions/state.service";
import { Utils } from "../../platform/misc/utils";
import { EncArrayBuffer } from "../../platform/models/domain/enc-array-buffer";
import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key";
import { ContainerService } from "../../platform/services/container.service";
import { CipherId, UserId, OrganizationId, CollectionId } from "../../types/guid";
import { CipherKey, OrgKey, UserKey } from "../../types/key";
import { CipherEncryptionService } from "../abstractions/cipher-encryption.service";
import { EncryptionContext } from "../abstractions/cipher.service";
import { CipherFileUploadService } from "../abstractions/file-upload/cipher-file-upload.service";
import { SearchService } from "../abstractions/search.service";
import { FieldType } from "../enums";
import { CipherRepromptType } from "../enums/cipher-reprompt-type";
import { CipherType } from "../enums/cipher-type";
import { CipherPermissionsApi } from "../models/api/cipher-permissions.api";
import { CipherData } from "../models/data/cipher.data";
import { Cipher } from "../models/domain/cipher";
import { CipherCreateRequest } from "../models/request/cipher-create.request";
import { CipherPartialRequest } from "../models/request/cipher-partial.request";
import { CipherRequest } from "../models/request/cipher.request";
import { AttachmentView } from "../models/view/attachment.view";
import { CipherView } from "../models/view/cipher.view";
import { LoginUriView } from "../models/view/login-uri.view";
import { CipherService } from "./cipher.service";
const ENCRYPTED_TEXT = "This data has been encrypted";
function encryptText(clearText: string | Uint8Array) {
return Promise.resolve(new EncString(`${clearText} has been encrypted`));
}
const ENCRYPTED_BYTES = mock<EncArrayBuffer>();
const cipherData: CipherData = {
id: "id",
organizationId: "orgId",
folderId: "folderId",
edit: true,
viewPassword: true,
organizationUseTotp: true,
favorite: false,
revisionDate: "2022-01-31T12:00:00.000Z",
type: CipherType.Login,
name: "EncryptedString",
notes: "EncryptedString",
creationDate: "2022-01-01T12:00:00.000Z",
deletedDate: null,
permissions: new CipherPermissionsApi(),
key: "EncKey",
reprompt: CipherRepromptType.None,
login: {
uris: [
{ uri: "EncryptedString", uriChecksum: "EncryptedString", match: UriMatchStrategy.Domain },
],
username: "EncryptedString",
password: "EncryptedString",
passwordRevisionDate: "2022-01-31T12:00:00.000Z",
totp: "EncryptedString",
autofillOnPageLoad: false,
},
passwordHistory: [{ password: "EncryptedString", lastUsedDate: "2022-01-31T12:00:00.000Z" }],
attachments: [
{ id: "a1", url: "url", size: "1100", sizeName: "1.1 KB", fileName: "file", key: "EncKey" },
{ id: "a2", url: "url", size: "1100", sizeName: "1.1 KB", fileName: "file", key: "EncKey" },
],
fields: [
{ name: "EncryptedString", value: "EncryptedString", type: FieldType.Text, linkedId: null },
{ name: "EncryptedString", value: "EncryptedString", type: FieldType.Hidden, linkedId: null },
],
};
const mockUserId = Utils.newGuid() as UserId;
let accountService: FakeAccountService;
describe("Cipher Service", () => {
const keyService = mock<KeyService>();
const stateService = mock<StateService>();
const autofillSettingsService = mock<AutofillSettingsService>();
const domainSettingsService = mock<DomainSettingsService>();
const apiService = mock<ApiService>();
const cipherFileUploadService = mock<CipherFileUploadService>();
const i18nService = mock<I18nService>();
const searchService = mock<SearchService>();
const encryptService = mock<EncryptService>();
const configService = mock<ConfigService>();
accountService = mockAccountServiceWith(mockUserId);
const logService = mock<LogService>();
const stateProvider = new FakeStateProvider(accountService);
const cipherEncryptionService = mock<CipherEncryptionService>();
const messageSender = mock<MessageSender>();
const userId = "TestUserId" as UserId;
const orgId = "4ff8c0b2-1d3e-4f8c-9b2d-1d3e4f8c0b2" as OrganizationId;
let cipherService: CipherService;
let encryptionContext: EncryptionContext;
beforeEach(() => {
encryptService.encryptFileData.mockReturnValue(Promise.resolve(ENCRYPTED_BYTES));
encryptService.encryptString.mockReturnValue(Promise.resolve(new EncString(ENCRYPTED_TEXT)));
(window as any).bitwardenContainerService = new ContainerService(keyService, encryptService);
cipherService = new CipherService(
keyService,
domainSettingsService,
apiService,
i18nService,
searchService,
stateService,
autofillSettingsService,
encryptService,
cipherFileUploadService,
configService,
stateProvider,
accountService,
logService,
cipherEncryptionService,
messageSender,
);
encryptionContext = { cipher: new Cipher(cipherData), encryptedFor: userId };
});
afterEach(() => {
jest.resetAllMocks();
});
describe("saveAttachmentRawWithServer()", () => {
it("should upload encrypted file contents with save attachments", async () => {
const fileName = "filename";
const fileData = new Uint8Array(10);
keyService.getOrgKey.mockReturnValue(
Promise.resolve<any>(new SymmetricCryptoKey(new Uint8Array(32)) as OrgKey),
);
keyService.makeDataEncKey.mockReturnValue(
Promise.resolve<any>(new SymmetricCryptoKey(new Uint8Array(32))),
);
configService.checkServerMeetsVersionRequirement$.mockReturnValue(of(false));
configService.getFeatureFlag
.calledWith(FeatureFlag.CipherKeyEncryption)
.mockResolvedValue(false);
const spy = jest.spyOn(cipherFileUploadService, "upload");
await cipherService.saveAttachmentRawWithServer(new Cipher(), fileName, fileData, userId);
expect(spy).toHaveBeenCalled();
});
});
describe("createWithServer()", () => {
it("should call apiService.postCipherAdmin when orgAdmin param is true and the cipher orgId != null", async () => {
const spy = jest
.spyOn(apiService, "postCipherAdmin")
.mockImplementation(() => Promise.resolve<any>(encryptionContext.cipher.toCipherData()));
await cipherService.createWithServer(encryptionContext, true);
const expectedObj = new CipherCreateRequest(encryptionContext);
expect(spy).toHaveBeenCalled();
expect(spy).toHaveBeenCalledWith(expectedObj);
});
it("should call apiService.postCipher when orgAdmin param is true and the cipher orgId is null", async () => {
encryptionContext.cipher.organizationId = null!;
const spy = jest
.spyOn(apiService, "postCipher")
.mockImplementation(() => Promise.resolve<any>(encryptionContext.cipher.toCipherData()));
await cipherService.createWithServer(encryptionContext, true);
const expectedObj = new CipherRequest(encryptionContext);
expect(spy).toHaveBeenCalled();
expect(spy).toHaveBeenCalledWith(expectedObj);
});
it("should call apiService.postCipherCreate if collectionsIds != null", async () => {
encryptionContext.cipher.collectionIds = ["123"];
const spy = jest
.spyOn(apiService, "postCipherCreate")
.mockImplementation(() => Promise.resolve<any>(encryptionContext.cipher.toCipherData()));
await cipherService.createWithServer(encryptionContext);
const expectedObj = new CipherCreateRequest(encryptionContext);
expect(spy).toHaveBeenCalled();
expect(spy).toHaveBeenCalledWith(expectedObj);
});
it("should call apiService.postCipher when orgAdmin and collectionIds logic is false", async () => {
const spy = jest
.spyOn(apiService, "postCipher")
.mockImplementation(() => Promise.resolve<any>(encryptionContext.cipher.toCipherData()));
await cipherService.createWithServer(encryptionContext);
const expectedObj = new CipherRequest(encryptionContext);
expect(spy).toHaveBeenCalled();
expect(spy).toHaveBeenCalledWith(expectedObj);
});
});
describe("updateWithServer()", () => {
it("should call apiService.putCipherAdmin when orgAdmin param is true", async () => {
const spy = jest
.spyOn(apiService, "putCipherAdmin")
.mockImplementation(() => Promise.resolve<any>(encryptionContext.cipher.toCipherData()));
await cipherService.updateWithServer(encryptionContext, true);
const expectedObj = new CipherRequest(encryptionContext);
expect(spy).toHaveBeenCalled();
expect(spy).toHaveBeenCalledWith(encryptionContext.cipher.id, expectedObj);
});
it("should call apiService.putCipher if cipher.edit is true", async () => {
encryptionContext.cipher.edit = true;
const spy = jest
.spyOn(apiService, "putCipher")
.mockImplementation(() => Promise.resolve<any>(encryptionContext.cipher.toCipherData()));
await cipherService.updateWithServer(encryptionContext);
const expectedObj = new CipherRequest(encryptionContext);
expect(spy).toHaveBeenCalled();
expect(spy).toHaveBeenCalledWith(encryptionContext.cipher.id, expectedObj);
});
it("should call apiService.putPartialCipher when orgAdmin, and edit are false", async () => {
encryptionContext.cipher.edit = false;
const spy = jest
.spyOn(apiService, "putPartialCipher")
.mockImplementation(() => Promise.resolve<any>(encryptionContext.cipher.toCipherData()));
await cipherService.updateWithServer(encryptionContext);
const expectedObj = new CipherPartialRequest(encryptionContext.cipher);
expect(spy).toHaveBeenCalled();
expect(spy).toHaveBeenCalledWith(encryptionContext.cipher.id, expectedObj);
});
});
describe("encrypt", () => {
let cipherView: CipherView;
beforeEach(() => {
cipherView = new CipherView();
cipherView.type = CipherType.Login;
encryptService.unwrapSymmetricKey.mockResolvedValue(
new SymmetricCryptoKey(makeStaticByteArray(64)),
);
configService.checkServerMeetsVersionRequirement$.mockReturnValue(of(true));
keyService.makeCipherKey.mockReturnValue(
Promise.resolve(new SymmetricCryptoKey(makeStaticByteArray(64)) as CipherKey),
);
encryptService.encryptString.mockImplementation(encryptText);
encryptService.wrapSymmetricKey.mockResolvedValue(new EncString("Re-encrypted Cipher Key"));
jest.spyOn(cipherService as any, "getAutofillOnPageLoadDefault").mockResolvedValue(true);
});
it("should call encrypt method of CipherEncryptionService when feature flag is true", async () => {
configService.getFeatureFlag
.calledWith(FeatureFlag.PM22136_SdkCipherEncryption)
.mockResolvedValue(true);
cipherEncryptionService.encrypt.mockResolvedValue(encryptionContext);
const result = await cipherService.encrypt(cipherView, userId);
expect(result).toEqual(encryptionContext);
expect(cipherEncryptionService.encrypt).toHaveBeenCalledWith(cipherView, userId);
});
it("should call legacy encrypt when feature flag is false", async () => {
configService.getFeatureFlag
.calledWith(FeatureFlag.PM22136_SdkCipherEncryption)
.mockResolvedValue(false);
jest.spyOn(cipherService as any, "encryptCipher").mockResolvedValue(encryptionContext.cipher);
const result = await cipherService.encrypt(cipherView, userId);
expect(result).toEqual(encryptionContext);
expect(cipherEncryptionService.encrypt).not.toHaveBeenCalled();
});
it("should call legacy encrypt when keys are provided", async () => {
configService.getFeatureFlag
.calledWith(FeatureFlag.PM22136_SdkCipherEncryption)
.mockResolvedValue(true);
jest.spyOn(cipherService as any, "encryptCipher").mockResolvedValue(encryptionContext.cipher);
const encryptKey = new SymmetricCryptoKey(new Uint8Array(32));
const decryptKey = new SymmetricCryptoKey(new Uint8Array(32));
let result = await cipherService.encrypt(cipherView, userId, encryptKey);
expect(result).toEqual(encryptionContext);
expect(cipherEncryptionService.encrypt).not.toHaveBeenCalled();
result = await cipherService.encrypt(cipherView, userId, undefined, decryptKey);
expect(result).toEqual(encryptionContext);
expect(cipherEncryptionService.encrypt).not.toHaveBeenCalled();
result = await cipherService.encrypt(cipherView, userId, encryptKey, decryptKey);
expect(result).toEqual(encryptionContext);
expect(cipherEncryptionService.encrypt).not.toHaveBeenCalled();
});
it("should return the encrypting user id", async () => {
keyService.getOrgKey.mockReturnValue(
Promise.resolve<any>(new SymmetricCryptoKey(new Uint8Array(32)) as OrgKey),
);
const { encryptedFor } = await cipherService.encrypt(cipherView, userId);
expect(encryptedFor).toEqual(userId);
});
describe("login encryption", () => {
it("should add a uri hash to login uris", async () => {
encryptService.hash.mockImplementation((value) => Promise.resolve(`${value} hash`));
cipherView.login.uris = [
{ uri: "uri", match: UriMatchStrategy.RegularExpression } as LoginUriView,
];
keyService.getOrgKey.mockReturnValue(
Promise.resolve<any>(new SymmetricCryptoKey(new Uint8Array(32)) as OrgKey),
);
const { cipher } = await cipherService.encrypt(cipherView, userId);
expect(cipher.login.uris).toEqual([
{
uri: new EncString("uri has been encrypted"),
uriChecksum: new EncString("uri hash has been encrypted"),
match: UriMatchStrategy.RegularExpression,
},
]);
});
});
describe("cipher.key", () => {
beforeEach(() => {
keyService.getOrgKey.mockReturnValue(
Promise.resolve<any>(new SymmetricCryptoKey(new Uint8Array(32)) as OrgKey),
);
});
it("is null when feature flag is false", async () => {
configService.getFeatureFlag
.calledWith(FeatureFlag.CipherKeyEncryption)
.mockResolvedValue(false);
const { cipher } = await cipherService.encrypt(cipherView, userId);
expect(cipher.key).toBeNull();
});
describe("when feature flag is true", () => {
beforeEach(() => {
configService.getFeatureFlag
.calledWith(FeatureFlag.CipherKeyEncryption)
.mockResolvedValue(true);
});
it("is null when the cipher is not viewPassword", async () => {
cipherView.viewPassword = false;
const { cipher } = await cipherService.encrypt(cipherView, userId);
expect(cipher.key).toBeNull();
});
it("is defined when the cipher is viewPassword", async () => {
cipherView.viewPassword = true;
const { cipher } = await cipherService.encrypt(cipherView, userId);
expect(cipher.key).toBeDefined();
});
});
});
describe("encryptCipherForRotation", () => {
beforeEach(() => {
jest.spyOn<any, string>(cipherService, "encryptCipherWithCipherKey");
keyService.getOrgKey.mockReturnValue(
Promise.resolve<any>(new SymmetricCryptoKey(new Uint8Array(32)) as OrgKey),
);
});
it("is not called when feature flag is false", async () => {
configService.getFeatureFlag
.calledWith(FeatureFlag.CipherKeyEncryption)
.mockResolvedValue(false);
await cipherService.encrypt(cipherView, userId);
expect(cipherService["encryptCipherWithCipherKey"]).not.toHaveBeenCalled();
});
describe("when feature flag is true", () => {
beforeEach(() => {
configService.getFeatureFlag
.calledWith(FeatureFlag.CipherKeyEncryption)
.mockResolvedValue(true);
});
it("is called when cipher viewPassword is true", async () => {
cipherView.viewPassword = true;
await cipherService.encrypt(cipherView, userId);
expect(cipherService["encryptCipherWithCipherKey"]).toHaveBeenCalled();
});
it("is not called when cipher viewPassword is false and original cipher has no key", async () => {
cipherView.viewPassword = false;
await cipherService.encrypt(cipherView, userId, undefined, undefined, new Cipher());
expect(cipherService["encryptCipherWithCipherKey"]).not.toHaveBeenCalled();
});
it("is called when cipher viewPassword is false and original cipher has a key", async () => {
cipherView.viewPassword = false;
await cipherService.encrypt(
cipherView,
userId,
undefined,
undefined,
encryptionContext.cipher,
);
expect(cipherService["encryptCipherWithCipherKey"]).toHaveBeenCalled();
});
});
});
});
describe("getRotatedData", () => {
const originalUserKey = new SymmetricCryptoKey(new Uint8Array(32)) as UserKey;
const newUserKey = new SymmetricCryptoKey(new Uint8Array(32)) as UserKey;
let decryptedCiphers: BehaviorSubject<Record<CipherId, CipherView>>;
let failedCiphers: BehaviorSubject<CipherView[]>;
let encryptedKey: EncString;
beforeEach(() => {
configService.getFeatureFlag
.calledWith(FeatureFlag.CipherKeyEncryption)
.mockResolvedValue(true);
configService.checkServerMeetsVersionRequirement$.mockReturnValue(of(true));
searchService.indexedEntityId$.mockReturnValue(of(null));
stateService.getUserId.mockResolvedValue(mockUserId);
const keys = { userKey: originalUserKey } as CipherDecryptionKeys;
keyService.cipherDecryptionKeys$.mockReturnValue(of(keys));
const cipher1 = new CipherView(encryptionContext.cipher);
cipher1.id = "Cipher 1" as CipherId;
cipher1.organizationId = null;
const cipher2 = new CipherView(encryptionContext.cipher);
cipher2.id = "Cipher 2" as CipherId;
cipher2.organizationId = null;
decryptedCiphers = new BehaviorSubject({ [cipher1.id]: cipher1, [cipher2.id]: cipher2 });
jest
.spyOn(cipherService, "cipherViews$")
.mockImplementation((userId: UserId) =>
decryptedCiphers.pipe(map((ciphers) => Object.values(ciphers))),
);
failedCiphers = new BehaviorSubject<CipherView[]>([]);
jest
.spyOn(cipherService, "failedToDecryptCiphers$")
.mockImplementation((userId: UserId) => failedCiphers);
encryptService.unwrapSymmetricKey.mockResolvedValue(
new SymmetricCryptoKey(new Uint8Array(32)),
);
encryptedKey = new EncString("Re-encrypted Cipher Key");
encryptService.wrapSymmetricKey.mockResolvedValue(encryptedKey);
keyService.makeCipherKey.mockResolvedValue(
new SymmetricCryptoKey(new Uint8Array(32)) as CipherKey,
);
});
it("returns re-encrypted user ciphers", async () => {
const result = await cipherService.getRotatedData(originalUserKey, newUserKey, mockUserId);
expect(result[0]).toMatchObject({ id: "Cipher 1", key: "Re-encrypted Cipher Key" });
expect(result[1]).toMatchObject({ id: "Cipher 2", key: "Re-encrypted Cipher Key" });
});
it("throws if the original user key is null", async () => {
await expect(cipherService.getRotatedData(null!, newUserKey, mockUserId)).rejects.toThrow(
"Original user key is required to rotate ciphers",
);
});
it("throws if the new user key is null", async () => {
await expect(
cipherService.getRotatedData(originalUserKey, null!, mockUserId),
).rejects.toThrow("New user key is required to rotate ciphers");
});
it("throws if the user has any failed to decrypt ciphers", async () => {
const badCipher = new CipherView(encryptionContext.cipher);
badCipher.id = "Cipher 3";
badCipher.organizationId = null;
badCipher.decryptionFailure = true;
failedCiphers.next([badCipher]);
await expect(
cipherService.getRotatedData(originalUserKey, newUserKey, mockUserId),
).rejects.toThrow("Cannot rotate ciphers when decryption failures are present");
});
it("uses the sdk to re-encrypt ciphers when feature flag is enabled", async () => {
configService.getFeatureFlag
.calledWith(FeatureFlag.PM22136_SdkCipherEncryption)
.mockResolvedValue(true);
cipherEncryptionService.encryptCipherForRotation.mockResolvedValue({
cipher: encryptionContext.cipher,
encryptedFor: mockUserId,
});
const result = await cipherService.getRotatedData(originalUserKey, newUserKey, mockUserId);
expect(result).toHaveLength(2);
expect(cipherEncryptionService.encryptCipherForRotation).toHaveBeenCalledWith(
expect.any(CipherView),
mockUserId,
newUserKey,
);
});
it("sends overlay update when cipherViews$ emits", async () => {
(cipherService.cipherViews$ as jest.Mock)?.mockRestore();
const decryptedView = new CipherView(encryptionContext.cipher);
jest.spyOn(cipherService, "getAllDecrypted").mockResolvedValue([decryptedView]);
const sendSpy = jest.spyOn(messageSender, "send");
await firstValueFrom(
cipherService
.cipherViews$(mockUserId)
.pipe(filter((cipherViews): cipherViews is CipherView[] => cipherViews != null)),
);
expect(sendSpy).toHaveBeenCalledWith("updateOverlayCiphers");
expect(sendSpy).toHaveBeenCalledTimes(1);
});
});
describe("decrypt", () => {
it("should call decrypt method of CipherEncryptionService when feature flag is true", async () => {
configService.getFeatureFlag
.calledWith(FeatureFlag.PM19941MigrateCipherDomainToSdk)
.mockResolvedValue(true);
cipherEncryptionService.decrypt.mockResolvedValue(new CipherView(encryptionContext.cipher));
const result = await cipherService.decrypt(encryptionContext.cipher, userId);
expect(result).toEqual(new CipherView(encryptionContext.cipher));
expect(cipherEncryptionService.decrypt).toHaveBeenCalledWith(
encryptionContext.cipher,
userId,
);
});
it("should call legacy decrypt when feature flag is false", async () => {
const mockUserKey = new SymmetricCryptoKey(new Uint8Array(32)) as UserKey;
configService.getFeatureFlag
.calledWith(FeatureFlag.PM19941MigrateCipherDomainToSdk)
.mockResolvedValue(false);
cipherService.getKeyForCipherKeyDecryption = jest.fn().mockResolvedValue(mockUserKey);
jest
.spyOn(encryptionContext.cipher, "decrypt")
.mockResolvedValue(new CipherView(encryptionContext.cipher));
const result = await cipherService.decrypt(encryptionContext.cipher, userId);
expect(result).toEqual(new CipherView(encryptionContext.cipher));
expect(encryptionContext.cipher.decrypt).toHaveBeenCalledWith(mockUserKey);
});
});
describe("getDecryptedAttachmentBuffer", () => {
const mockEncryptedContent = new Uint8Array([1, 2, 3]);
const mockDecryptedContent = new Uint8Array([4, 5, 6]);
it("should use SDK when feature flag is enabled", async () => {
const cipher = new Cipher(cipherData);
const attachment = new AttachmentView(cipher.attachments![0]);
configService.getFeatureFlag
.calledWith(FeatureFlag.PM19941MigrateCipherDomainToSdk)
.mockResolvedValue(true);
jest.spyOn(cipherService, "ciphers$").mockReturnValue(of({ [cipher.id]: cipherData }));
cipherEncryptionService.decryptAttachmentContent.mockResolvedValue(mockDecryptedContent);
const mockResponse = {
arrayBuffer: jest.fn().mockResolvedValue(mockEncryptedContent.buffer),
} as unknown as Response;
const result = await cipherService.getDecryptedAttachmentBuffer(
cipher.id as CipherId,
attachment,
mockResponse,
userId,
);
expect(result).toEqual(mockDecryptedContent);
expect(cipherEncryptionService.decryptAttachmentContent).toHaveBeenCalledWith(
cipher,
attachment,
mockEncryptedContent,
userId,
);
});
it("should use legacy decryption when feature flag is enabled", async () => {
configService.getFeatureFlag
.calledWith(FeatureFlag.PM19941MigrateCipherDomainToSdk)
.mockResolvedValue(false);
const cipher = new Cipher(cipherData);
const attachment = new AttachmentView(cipher.attachments![0]);
attachment.key = makeSymmetricCryptoKey(64);
const mockResponse = {
arrayBuffer: jest.fn().mockResolvedValue(mockEncryptedContent.buffer),
} as unknown as Response;
const mockEncBuf = {} as EncArrayBuffer;
EncArrayBuffer.fromResponse = jest.fn().mockResolvedValue(mockEncBuf);
encryptService.decryptFileData.mockResolvedValue(mockDecryptedContent);
const result = await cipherService.getDecryptedAttachmentBuffer(
cipher.id as CipherId,
attachment,
mockResponse,
userId,
);
expect(result).toEqual(mockDecryptedContent);
expect(encryptService.decryptFileData).toHaveBeenCalledWith(mockEncBuf, attachment.key);
});
});
describe("shareWithServer()", () => {
it("should use cipherEncryptionService to move the cipher when feature flag enabled", async () => {
configService.getFeatureFlag
.calledWith(FeatureFlag.PM22136_SdkCipherEncryption)
.mockResolvedValue(true);
apiService.putShareCipher.mockResolvedValue(new CipherResponse(cipherData));
const expectedCipher = new Cipher(cipherData);
expectedCipher.organizationId = orgId;
const cipherView = new CipherView(expectedCipher);
const collectionIds = ["collection1", "collection2"] as CollectionId[];
cipherView.organizationId = undefined; // Ensure organizationId is undefined for this test
cipherEncryptionService.moveToOrganization.mockResolvedValue({
cipher: expectedCipher,
encryptedFor: userId,
});
await cipherService.shareWithServer(cipherView, orgId, collectionIds, userId);
// Expect SDK usage
expect(cipherEncryptionService.moveToOrganization).toHaveBeenCalledWith(
cipherView,
orgId,
userId,
);
// Expect collectionIds to be assigned
expect(apiService.putShareCipher).toHaveBeenCalledWith(
cipherView.id,
expect.objectContaining({
cipher: expect.objectContaining({ organizationId: orgId }),
collectionIds: collectionIds,
}),
);
});
it("should use legacy encryption when feature flag disabled", async () => {
configService.getFeatureFlag
.calledWith(FeatureFlag.PM22136_SdkCipherEncryption)
.mockResolvedValue(false);
apiService.putShareCipher.mockResolvedValue(new CipherResponse(cipherData));
const expectedCipher = new Cipher(cipherData);
expectedCipher.organizationId = orgId;
const cipherView = new CipherView(expectedCipher);
const collectionIds = ["collection1", "collection2"] as CollectionId[];
cipherView.organizationId = undefined; // Ensure organizationId is undefined for this test
const oldEncryptSharedSpy = jest
.spyOn(cipherService as any, "encryptSharedCipher")
.mockResolvedValue({
cipher: expectedCipher,
encryptedFor: userId,
});
await cipherService.shareWithServer(cipherView, orgId, collectionIds, userId);
// Expect no SDK usage
expect(cipherEncryptionService.moveToOrganization).not.toHaveBeenCalled();
expect(oldEncryptSharedSpy).toHaveBeenCalledWith(
expect.objectContaining({
organizationId: orgId,
collectionIds: collectionIds,
} as unknown as CipherView),
userId,
);
});
});
});