import { Readable } from "stream"; import { StampSettingsService } from "./stamp-settings.service"; /** * The one global company stamp feeds three document paths (invoices, warehouse * papers, contract signature blocks). All three treat the returned value as a * `data:` URL — so the data-URL-or-null contract of getStampImageUrl is what * these specs pin down, especially its behaviour when MinIO cannot be reached. */ describe("StampSettingsService.getStampImageUrl", () => { const PNG = Buffer.from("fake-png-bytes"); const OBJECT_URL = "https://minio.local:9000/edr-freight/stamp/company.png"; const build = ( overrides: { stampUrl?: string | null; getFileStream?: jest.Mock; findSingleton?: jest.Mock; } = {}, ) => { const service = Object.create( StampSettingsService.prototype, ) as StampSettingsService; const warn = jest.fn(); Object.assign(service, { logger: { warn, log: jest.fn() }, repository: { findSingleton: overrides.findSingleton ?? jest.fn().mockResolvedValue({ id: "s-1", stampFileId: overrides.stampUrl ? "f-1" : null, stampFile: overrides.stampUrl ? { url: overrides.stampUrl } : null, updatedById: null, updatedAt: null, }), create: jest.fn(), update: jest.fn(), }, minioService: { getObjectNameFromUrl: jest.fn().mockReturnValue("stamp/company.png"), getFileStream: overrides.getFileStream ?? jest.fn().mockResolvedValue(Readable.from(PNG)), }, filesService: { upload: jest.fn() }, }); return { service, warn }; }; it("inlines the stored stamp as a data URL", async () => { const { service } = build({ stampUrl: OBJECT_URL }); await expect(service.getStampImageUrl()).resolves.toBe( `data:image/png;base64,${PNG.toString("base64")}`, ); }); it("returns null when no stamp is configured", async () => { const { service } = build({ stampUrl: null }); await expect(service.getStampImageUrl()).resolves.toBeNull(); }); it("passes an already-inlined data URL straight through", async () => { const dataUrl = "data:image/png;base64,QUJD"; const { service } = build({ stampUrl: dataUrl }); await expect(service.getStampImageUrl()).resolves.toBe(dataUrl); }); /** * Regression: inlineImageUrl falls back to the raw object URL when MinIO is * unreachable, which is right for getView (a browser can fetch it) but wrong * here. ContractTransitionService base64-decodes this value to snapshot the * seal — and a URL decodes to garbage bytes WITHOUT throwing, so a transient * MinIO failure used to seal an executed contract with a corrupt image file. * Degrade to null instead so callers draw their text/vector seal. */ it("returns null rather than a raw object URL when MinIO inlining fails", async () => { const { service, warn } = build({ stampUrl: OBJECT_URL, getFileStream: jest.fn().mockRejectedValue(new Error("connect ECONNREFUSED")), }); await expect(service.getStampImageUrl()).resolves.toBeNull(); expect(warn).toHaveBeenCalledWith(expect.stringMatching(/plain seal/i)); }); it("never throws when the settings lookup itself fails", async () => { const { service, warn } = build({ findSingleton: jest.fn().mockRejectedValue(new Error("db is down")), }); await expect(service.getStampImageUrl()).resolves.toBeNull(); expect(warn).toHaveBeenCalledWith(expect.stringContaining("db is down")); }); it("still exposes the raw URL through getView, which a browser can load", async () => { const { service } = build({ stampUrl: OBJECT_URL, getFileStream: jest.fn().mockRejectedValue(new Error("connect ECONNREFUSED")), }); await expect(service.getView()).resolves.toEqual( expect.objectContaining({ stampImageUrl: OBJECT_URL }), ); }); });