Files
edr-platform/apps/edr-freight-api/src/modules/stamp-settings/stamp-settings.service.spec.ts

111 lines
3.9 KiB
TypeScript

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 }),
);
});
});