pelagia-portal/App/tests/integration/company-branding.test.ts
Hardik 1071cb226f
All checks were successful
PR checks / checks (pull_request) Successful in 31s
feat(po): per-company logo, stamp & brand bar on exported POs
Companies can upload a logo and a stamp/seal (Admin → Companies → Edit →
Branding); both render on exported PDF and XLSX purchase orders. A fixed
brand-colour bar (#92D050, matching the sample PO) runs along the bottom of
every export.

- Company.logoKey / stampKey + migration
- buildCompanyAssetKey() deterministic storage keys (overwrite-in-place)
- uploadCompanyAsset / removeCompanyAsset server actions (≤4MB PNG/JPG/WebP,
  manage_vessels_accounts gated)
- CompanyBrandingUploader in the company edit dialog with live previews
- Export route embeds logo (top-left), stamp (signatory block) and brand bar
  in both ExcelJS and print-HTML paths
- Unit test (storage keys) + integration test (branding actions)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 01:17:23 +05:30

114 lines
4.6 KiB
TypeScript

/**
* Integration tests for company branding actions (logo + stamp uploads).
* Covers:
* - Manager can upload a logo / stamp; the key is stored on the company
* - Re-upload overwrites in place (deterministic key)
* - Invalid asset type, bad mime, and oversize files are rejected
* - removeCompanyAsset clears the key
* - Permission gating (TECHNICAL cannot manage branding)
*/
import { vi, describe, it, expect, beforeAll, afterAll } from "vitest";
vi.mock("@/auth", () => ({ auth: vi.fn() }));
vi.mock("next/cache", () => ({ revalidatePath: vi.fn() }));
vi.mock("@/lib/storage", async (importOriginal) => ({
...(await importOriginal<typeof import("@/lib/storage")>()),
uploadBuffer: vi.fn(), // don't touch the filesystem in tests
}));
import { auth } from "@/auth";
import { db } from "@/lib/db";
import { uploadBuffer } from "@/lib/storage";
import { uploadCompanyAsset, removeCompanyAsset } from "@/app/(portal)/admin/companies/actions";
import { makeSession } from "./helpers";
const mockedAuth = vi.mocked(auth);
const mockedUpload = vi.mocked(uploadBuffer);
let companyId: string;
function pngFile(name: string, bytes = 1024): File {
return new File([new Uint8Array(bytes)], name, { type: "image/png" });
}
function assetForm(id: string, type: string, file: File): FormData {
const form = new FormData();
form.set("companyId", id);
form.set("type", type);
form.set("file", file);
return form;
}
beforeAll(async () => {
const company = await db.company.create({
data: { name: "INTTEST_BRANDING_CO", code: "ZZBRAND" },
});
companyId = company.id;
});
afterAll(async () => {
await db.company.delete({ where: { id: companyId } }).catch(() => {});
});
describe("uploadCompanyAsset", () => {
it("stores a logo key on the company", async () => {
mockedAuth.mockResolvedValue(makeSession("u-mgr", "MANAGER") as never);
const res = await uploadCompanyAsset(assetForm(companyId, "logo", pngFile("logo.png")));
expect(res).toEqual({ ok: true });
const c = await db.company.findUniqueOrThrow({ where: { id: companyId } });
expect(c.logoKey).toBe(`company-assets/${companyId}/logo.png`);
expect(mockedUpload).toHaveBeenCalled();
});
it("stores a stamp key independently of the logo", async () => {
mockedAuth.mockResolvedValue(makeSession("u-mgr", "MANAGER") as never);
const res = await uploadCompanyAsset(assetForm(companyId, "stamp", pngFile("stamp.png")));
expect(res).toEqual({ ok: true });
const c = await db.company.findUniqueOrThrow({ where: { id: companyId } });
expect(c.stampKey).toBe(`company-assets/${companyId}/stamp.png`);
expect(c.logoKey).toBe(`company-assets/${companyId}/logo.png`);
});
it("rejects an unknown asset type", async () => {
mockedAuth.mockResolvedValue(makeSession("u-mgr", "MANAGER") as never);
const res = await uploadCompanyAsset(assetForm(companyId, "header", pngFile("x.png")));
expect(res).toEqual({ error: "Invalid asset type" });
});
it("rejects a non-image mime type", async () => {
mockedAuth.mockResolvedValue(makeSession("u-mgr", "MANAGER") as never);
const pdf = new File([new Uint8Array(10)], "x.pdf", { type: "application/pdf" });
const res = await uploadCompanyAsset(assetForm(companyId, "logo", pdf));
expect(res).toEqual({ error: "Image must be a PNG, JPG, or WebP" });
});
it("rejects a file over 4 MB", async () => {
mockedAuth.mockResolvedValue(makeSession("u-mgr", "MANAGER") as never);
const big = pngFile("big.png", 5 * 1024 * 1024);
const res = await uploadCompanyAsset(assetForm(companyId, "logo", big));
expect(res).toEqual({ error: "Image must be under 4 MB" });
});
it("refuses callers without manage_vessels_accounts", async () => {
mockedAuth.mockResolvedValue(makeSession("u-tech", "TECHNICAL") as never);
const res = await uploadCompanyAsset(assetForm(companyId, "logo", pngFile("logo.png")));
expect(res).toEqual({ error: "Unauthorized" });
});
});
describe("removeCompanyAsset", () => {
it("clears the stored key", async () => {
mockedAuth.mockResolvedValue(makeSession("u-mgr", "MANAGER") as never);
const res = await removeCompanyAsset(companyId, "logo");
expect(res).toEqual({ ok: true });
const c = await db.company.findUniqueOrThrow({ where: { id: companyId } });
expect(c.logoKey).toBeNull();
expect(c.stampKey).toBe(`company-assets/${companyId}/stamp.png`); // stamp untouched
});
it("refuses unauthorized callers", async () => {
mockedAuth.mockResolvedValue(makeSession("u-tech", "TECHNICAL") as never);
const res = await removeCompanyAsset(companyId, "stamp");
expect(res).toEqual({ error: "Unauthorized" });
});
});