import { beforeEach, describe, expect, it, vi } from "vitest"; import { api } from "../api/apiClient"; import { CryptoUtils } from "./crypto"; import { blobUrlToFile } from "./fileUtils"; import { decryptCanvasImages, decryptCanvasImagesWithSharingKey, encryptCanvasImages, } from "./letterLogic"; vi.mock("../api/apiClient", () => ({ api: { get: vi.fn(), }, apiServerUrl: "https://remote", })); vi.mock("./fileUtils", () => ({ blobUrlToFile: vi.fn(), })); describe("letterLogic image helpers", () => { let masterKey: CryptoKey; let crypto: CryptoUtils; beforeEach(async () => { const keyBundle = await CryptoUtils.deriveKeyBundle( "password123", "test@example.com", ); masterKey = keyBundle.masterKey; crypto = new CryptoUtils(); await crypto.initialize(); vi.clearAllMocks(); }); describe("encryptCanvasImages", () => { it("should not encrypt images whose src already ends with .bin", async () => { const canvasData = { objects: [ { type: "Image", src: "already-encrypted.png.bin", top: 0, left: 0, width: 100, height: 100, }, { type: "Textbox", text: "hello", top: 0, left: 0, width: 100, height: 100, }, ], }; const encryptImageSpy = vi.spyOn(CryptoUtils.prototype, "encryptImage"); const { encryptedImageFiles: uploads, encryptedCanvasData } = await encryptCanvasImages(canvasData, [], masterKey, crypto); expect(encryptImageSpy).not.toHaveBeenCalled(); expect(encryptedCanvasData.objects[0].src).toBe( "already-encrypted.png.bin", ); expect(uploads.size).toBe(0); }); it("should encrypt new blob-backed images and return encrypted uploads", async () => { const file = new File(["img"], "photo.png", { type: "image/png" }); const canvasData = { objects: [ { type: "Image", src: "blob:http://localhost/test-image", top: 0, left: 0, width: 100, height: 100, }, ], }; const canvasImages = [ { src: "blob:http://localhost/test-image", file, }, ]; vi.spyOn(CryptoUtils.prototype, "encryptImage").mockResolvedValue({ encryptedBlob: new Blob(["encrypted"], { type: "application/octet-stream", }), encrypted_dek: "wrapped-image-dek", filename: "photo.png.bin", }); const { encryptedImageFiles: uploads, encryptedCanvasData } = await encryptCanvasImages(canvasData, canvasImages, masterKey, crypto); expect(CryptoUtils.prototype.encryptImage).toHaveBeenCalledTimes(1); expect(encryptedCanvasData.objects[0].src).toBe("photo.png.bin"); expect(uploads.size).toBe(1); expect(uploads.has("photo.png.bin")).toBe(true); }); }); describe("decryptCanvasImages", () => { it("should decrypt images and replace src with blob URL", async () => { const canvasData = { objects: [ { type: "Image", src: "photo.png.bin", top: 0, left: 0, width: 100, height: 100, }, { type: "Textbox", text: "hello", top: 0, left: 0, width: 100, height: 100, }, ], }; const remoteImages = [ { file_name: "photo.png.bin", file: `https://remote/photo.png.bin` }, ]; vi.mocked(api.get).mockResolvedValue({ data: new Blob(["encrypted"]) }); vi.spyOn(CryptoUtils.prototype, "decryptImage").mockResolvedValue( "blob:http://localhost/decrypted", ); const { canvasDataWithDecryptedImages } = await decryptCanvasImages( canvasData, remoteImages, "wrapped-dek", masterKey, crypto, ); expect(api.get).toHaveBeenCalledWith( `https://remote/photo.png.bin`, expect.objectContaining({ responseType: "blob" }), ); expect(CryptoUtils.prototype.decryptImage).toHaveBeenCalledWith( expect.any(Blob), "wrapped-dek", masterKey, ); expect(canvasDataWithDecryptedImages.objects[0].src).toBe( "blob:http://localhost/decrypted", ); expect(canvasDataWithDecryptedImages.objects[1].text).toBe("hello"); }); it("should include raw file when includeRawFile is true", async () => { const canvasData = { objects: [ { type: "Image", src: "photo.png.bin", _customRawFile: null, top: 0, left: 0, width: 100, height: 100, }, ], }; const remoteImages = [ { file_name: "photo.png.bin", file: "https://remote/photo.png.bin" }, ]; vi.mocked(api.get).mockResolvedValue({ data: new Blob(["encrypted"]) }); vi.spyOn(CryptoUtils.prototype, "decryptImage").mockResolvedValue( "blob:http://localhost/decrypted", ); vi.mocked(blobUrlToFile).mockResolvedValue( new File(["raw"], "photo.png.bin"), ); const { canvasDataWithDecryptedImages } = await decryptCanvasImages( canvasData, remoteImages, "wrapped-dek", masterKey, crypto, true, ); expect(blobUrlToFile).toHaveBeenCalledWith( "blob:http://localhost/decrypted", "photo.png.bin", ); expect( canvasDataWithDecryptedImages.objects[0]._customRawFile, ).toBeInstanceOf(File); }); }); describe("decryptCanvasImagesWithSharingKey", () => { it("should decrypt images using sharing key", async () => { const canvasData = { objects: [ { type: "Image", src: "photo.png.bin", top: 0, left: 0, width: 100, height: 100, }, ], }; const remoteImages = [ { public_id: "1234", file_name: "photo.png.bin", file: "https://remote/photo.png.bin", }, ]; vi.mocked(api.get).mockResolvedValue({ data: new Blob(["encrypted"]) }); vi.spyOn( CryptoUtils.prototype, "decryptImageWithSharingKey", ).mockResolvedValue("blob:http://localhost/decrypted-shared"); const { canvasDataWithDecryptedImages } = await decryptCanvasImagesWithSharingKey( canvasData, remoteImages, "raw-sharing-key", crypto, ); expect(api.get).toHaveBeenCalledWith( "https://remote/photo.png.bin", expect.objectContaining({ responseType: "blob" }), ); expect( CryptoUtils.prototype.decryptImageWithSharingKey, ).toHaveBeenCalledWith(expect.any(Blob), "raw-sharing-key"); expect(canvasDataWithDecryptedImages.objects[0].src).toBe( "blob:http://localhost/decrypted-shared", ); }); }); });