import { mock } from "jest-mock-extended"; import { firstValueFrom } from "rxjs"; import { FakeAccountService, FakeActiveUserState, FakeStateProvider, awaitAsync, mockAccountServiceWith, } from "../../../../spec"; import { CryptoService } from "../../../platform/abstractions/crypto.service"; import { EncryptService } from "../../../platform/abstractions/encrypt.service"; import { I18nService } from "../../../platform/abstractions/i18n.service"; import { KeyGenerationService } from "../../../platform/abstractions/key-generation.service"; import { Utils } from "../../../platform/misc/utils"; import { EncString } from "../../../platform/models/domain/enc-string"; import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key"; import { ContainerService } from "../../../platform/services/container.service"; import { UserId } from "../../../types/guid"; import { UserKey } from "../../../types/key"; import { SendType } from "../enums/send-type"; import { SendFileApi } from "../models/api/send-file.api"; import { SendTextApi } from "../models/api/send-text.api"; import { SendFileData } from "../models/data/send-file.data"; import { SendTextData } from "../models/data/send-text.data"; import { SendData } from "../models/data/send.data"; import { SendView } from "../models/view/send.view"; import { SEND_USER_DECRYPTED, SEND_USER_ENCRYPTED } from "./key-definitions"; import { SendStateProvider } from "./send-state.provider"; import { SendService } from "./send.service"; import { createSendData, testSend, testSendData, testSendViewData, } from "./test-data/send-tests.data"; describe("SendService", () => { const cryptoService = mock(); const i18nService = mock(); const keyGenerationService = mock(); const encryptService = mock(); let sendStateProvider: SendStateProvider; let sendService: SendService; let stateProvider: FakeStateProvider; let encryptedState: FakeActiveUserState>; let decryptedState: FakeActiveUserState; const mockUserId = Utils.newGuid() as UserId; let accountService: FakeAccountService; beforeEach(() => { accountService = mockAccountServiceWith(mockUserId); stateProvider = new FakeStateProvider(accountService); sendStateProvider = new SendStateProvider(stateProvider); (window as any).bitwardenContainerService = new ContainerService(cryptoService, encryptService); accountService.activeAccountSubject.next({ id: mockUserId, email: "email", name: "name", }); // Initial encrypted state encryptedState = stateProvider.activeUser.getFake(SEND_USER_ENCRYPTED); encryptedState.nextState({ "1": testSendData("1", "Test Send"), }); // Initial decrypted state decryptedState = stateProvider.activeUser.getFake(SEND_USER_DECRYPTED); decryptedState.nextState([testSendViewData("1", "Test Send")]); sendService = new SendService( cryptoService, i18nService, keyGenerationService, sendStateProvider, encryptService, ); }); describe("get$", () => { it("exists", async () => { const result = await firstValueFrom(sendService.get$("1")); expect(result).toEqual(testSend("1", "Test Send")); }); it("does not exist", async () => { const result = await firstValueFrom(sendService.get$("2")); expect(result).toBe(undefined); }); it("updated observable", async () => { const singleSendObservable = sendService.get$("1"); const result = await firstValueFrom(singleSendObservable); expect(result).toEqual(testSend("1", "Test Send")); await sendService.replace({ "1": testSendData("1", "Test Send Updated"), }); const result2 = await firstValueFrom(singleSendObservable); expect(result2).toEqual(testSend("1", "Test Send Updated")); }); it("reports a change when name changes on a new send", async () => { let changed = false; sendService.get$("1").subscribe(() => { changed = true; }); const sendDataObject = testSendData("1", "Test Send 2"); //it is immediately called when subscribed, we need to reset the value changed = false; await sendService.replace({ "1": sendDataObject, "2": testSendData("2", "Test Send 2"), }); expect(changed).toEqual(true); }); it("reports a change when notes changes on a new send", async () => { const sendDataObject = createSendData() as SendData; await sendService.replace({ "1": sendDataObject, "2": testSendData("2", "Test Send 2"), }); let changed = false; sendService.get$("1").subscribe(() => { changed = true; }); sendDataObject.notes = "New notes"; //it is immediately called when subscribed, we need to reset the value changed = false; await sendService.replace({ "1": sendDataObject, "2": testSendData("2", "Test Send 2"), }); expect(changed).toEqual(true); }); it("reports a change when Text changes on a new send", async () => { const sendDataObject = createSendData() as SendData; await sendService.replace({ "1": sendDataObject, "2": testSendData("2", "Test Send 2"), }); let changed = false; sendService.get$("1").subscribe(() => { changed = true; }); //it is immediately called when subscribed, we need to reset the value changed = false; sendDataObject.text.text = "new text"; await sendService.replace({ "1": sendDataObject, "2": testSendData("2", "Test Send 2"), }); expect(changed).toEqual(true); }); it("reports a change when Text is set as null on a new send", async () => { const sendDataObject = createSendData() as SendData; await sendService.replace({ "1": sendDataObject, "2": testSendData("2", "Test Send 2"), }); let changed = false; sendService.get$("1").subscribe(() => { changed = true; }); //it is immediately called when subscribed, we need to reset the value changed = false; sendDataObject.text = null; await sendService.replace({ "1": sendDataObject, "2": testSendData("2", "Test Send 2"), }); expect(changed).toEqual(true); }); it("Doesn't reports a change when File changes on a new send", async () => { const sendDataObject = createSendData({ type: SendType.File, file: new SendFileData(new SendFileApi({ FileName: "name of file" })), }) as SendData; await sendService.replace({ "1": sendDataObject, "2": testSendData("2", "Test Send 2"), }); sendDataObject.file = new SendFileData(new SendFileApi({ FileName: "updated name of file" })); let changed = false; sendService.get$("1").subscribe(() => { changed = true; }); //it is immediately called when subscribed, we need to reset the value changed = false; await sendService.replace({ "1": sendDataObject, "2": testSendData("2", "Test Send 2"), }); expect(changed).toEqual(false); }); it("reports a change when key changes on a new send", async () => { const sendDataObject = createSendData() as SendData; await sendService.replace({ "1": sendDataObject, "2": testSendData("2", "Test Send 2"), }); let changed = false; sendService.get$("1").subscribe(() => { changed = true; }); //it is immediately called when subscribed, we need to reset the value changed = false; sendDataObject.key = "newKey"; await sendService.replace({ "1": sendDataObject, "2": testSendData("2", "Test Send 2"), }); expect(changed).toEqual(true); }); it("reports a change when revisionDate changes on a new send", async () => { const sendDataObject = createSendData() as SendData; await sendService.replace({ "1": sendDataObject, "2": testSendData("2", "Test Send 2"), }); let changed = false; sendService.get$("1").subscribe(() => { changed = true; }); //it is immediately called when subscribed, we need to reset the value changed = false; sendDataObject.revisionDate = "2025-04-05"; await sendService.replace({ "1": sendDataObject, "2": testSendData("2", "Test Send 2"), }); expect(changed).toEqual(true); }); it("reports a change when a property is set as null on a new send", async () => { const sendDataObject = createSendData() as SendData; await sendService.replace({ "1": sendDataObject, "2": testSendData("2", "Test Send 2"), }); let changed = false; sendService.get$("1").subscribe(() => { changed = true; }); //it is immediately called when subscribed, we need to reset the value changed = false; sendDataObject.name = null; await sendService.replace({ "1": sendDataObject, "2": testSendData("2", "Test Send 2"), }); expect(changed).toEqual(true); }); it("does not reports a change when text's text is set as null on a new send and old send and reports a change then new send sets a text", async () => { const sendDataObject = createSendData({ text: new SendTextData(new SendTextApi({ Text: null })), }) as SendData; await sendService.replace({ "1": sendDataObject, "2": testSendData("2", "Test Send 2"), }); let changed = false; sendService.get$("1").subscribe(() => { changed = true; }); //it is immediately called when subscribed, we need to reset the value changed = false; await sendService.replace({ "1": sendDataObject, "2": testSendData("2", "Test Send 2"), }); expect(changed).toEqual(false); sendDataObject.text.text = "Asdf"; await sendService.replace({ "1": sendDataObject, "2": testSendData("2", "Test Send 2"), }); expect(changed).toEqual(true); }); it("do not reports a change when nothing changes on the observed send", async () => { let changed = false; sendService.get$("1").subscribe(() => { changed = true; }); const sendDataObject = testSendData("1", "Test Send"); //it is immediately called when subscribed, we need to reset the value changed = false; await sendService.replace({ "1": sendDataObject, "2": testSendData("3", "Test Send 3"), }); expect(changed).toEqual(false); }); it("reports a change when the observed send is deleted", async () => { let changed = false; sendService.get$("1").subscribe(() => { changed = true; }); //it is immediately called when subscribed, we need to reset the value changed = false; await sendService.replace({ "2": testSendData("2", "Test Send 2"), }); expect(changed).toEqual(true); }); }); it("getAll", async () => { const sends = await sendService.getAll(); const send1 = sends[0]; expect(sends).toHaveLength(1); expect(send1).toEqual(testSend("1", "Test Send")); }); describe("getFromState", () => { it("exists", async () => { const result = await sendService.getFromState("1"); expect(result).toEqual(testSend("1", "Test Send")); }); it("does not exist", async () => { const result = await sendService.getFromState("2"); expect(result).toBe(null); }); }); it("getAllDecryptedFromState", async () => { const sends = await sendService.getAllDecryptedFromState(); expect(sends[0]).toMatchObject(testSendViewData("1", "Test Send")); }); describe("getRotatedKeys", () => { let encryptedKey: EncString; beforeEach(() => { encryptService.decryptToBytes.mockResolvedValue(new Uint8Array(32)); encryptedKey = new EncString("Re-encrypted Send Key"); encryptService.encrypt.mockResolvedValue(encryptedKey); }); it("returns re-encrypted user sends", async () => { const newUserKey = new SymmetricCryptoKey(new Uint8Array(32)) as UserKey; const result = await sendService.getRotatedKeys(newUserKey); expect(result).toMatchObject([{ id: "1", key: "Re-encrypted Send Key" }]); }); it("returns null if there are no sends", async () => { // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // eslint-disable-next-line @typescript-eslint/no-floating-promises sendService.replace(null); await awaitAsync(); const newUserKey = new SymmetricCryptoKey(new Uint8Array(32)) as UserKey; const result = await sendService.getRotatedKeys(newUserKey); expect(result).toEqual([]); }); it("throws if the new user key is null", async () => { await expect(sendService.getRotatedKeys(null)).rejects.toThrowError( "New user key is required for rotation.", ); }); }); // InternalSendService it("upsert", async () => { await sendService.upsert(testSendData("2", "Test 2")); expect(await firstValueFrom(sendService.sends$)).toEqual([ testSend("1", "Test Send"), testSend("2", "Test 2"), ]); }); it("replace", async () => { await sendService.replace({ "2": testSendData("2", "test 2") }); expect(await firstValueFrom(sendService.sends$)).toEqual([testSend("2", "test 2")]); }); it("clear", async () => { await sendService.clear(); await awaitAsync(); expect(await firstValueFrom(sendService.sends$)).toEqual([]); }); describe("Delete", () => { it("Sends count should decrease after delete", async () => { const sendsBeforeDelete = await firstValueFrom(sendService.sends$); await sendService.delete(sendsBeforeDelete[0].id); const sendsAfterDelete = await firstValueFrom(sendService.sends$); expect(sendsAfterDelete.length).toBeLessThan(sendsBeforeDelete.length); }); it("Intended send should be delete", async () => { const sendsBeforeDelete = await firstValueFrom(sendService.sends$); await sendService.delete(sendsBeforeDelete[0].id); const sendsAfterDelete = await firstValueFrom(sendService.sends$); expect(sendsAfterDelete[0]).not.toBe(sendsBeforeDelete[0]); }); it("Deleting on an empty sends array should not throw", async () => { sendStateProvider.getEncryptedSends = jest.fn().mockResolvedValue(null); await expect(sendService.delete("2")).resolves.not.toThrow(); }); it("Delete multiple sends", async () => { await sendService.upsert(testSendData("2", "send data 2")); await sendService.delete(["1", "2"]); const sendsAfterDelete = await firstValueFrom(sendService.sends$); expect(sendsAfterDelete.length).toBe(0); }); }); });