bitwarden-estensione-browser/libs/common/src/tools/send/services/send.service.spec.ts

488 lines
15 KiB
TypeScript

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<CryptoService>();
const i18nService = mock<I18nService>();
const keyGenerationService = mock<KeyGenerationService>();
const encryptService = mock<EncryptService>();
let sendStateProvider: SendStateProvider;
let sendService: SendService;
let stateProvider: FakeStateProvider;
let encryptedState: FakeActiveUserState<Record<string, SendData>>;
let decryptedState: FakeActiveUserState<SendView[]>;
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);
});
});
});