// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0 // The tenant brand end to end (plan item AT2.6): who may change it, what is refused, what is stored, // and what the organization layout will read back. Every call goes through the kernel's caller, so // the permission check, the validation and the data wall are the real ones. import { type BrandView, tenantStyle } from "@app/modules/branding" import { checkLogo } from "@conusai-kit/brand/logo/check" import { AppError } from "@conusai-kit/core" import { organizations } from "@conusai-kit/tenancy" import { afterEach, describe, expect, it } from "vitest" import { closeApps, type DocumentsApp, makeDocumentsApp, type SeededProject } from "./support/documents-app.js" import { chunk, dimensions, MARK_WEBP, markPng, noisePng } from "./support/images.js" afterEach(closeApps) const document = { engine: { name: "unused", version: "0" }, page_count: 0, pages: [] } async function fresh(): Promise<{ app: DocumentsApp; project: SeededProject }> { const app = await makeDocumentsApp({ document }) return { app, project: await app.seedProject("acme", "owner@acme.test") } } // Real images: the kit's checks read a PNG's chunks and decode its pixels. const PNG = markPng(1000, 250) const png = (width: number) => markPng(width, 60) const svgOf = (inner: string) => Buffer.from(`${inner}`) const refusal = async (promise: Promise): Promise => { try { await promise } catch (error) { if (error instanceof AppError) return error throw error } throw new Error("expected a refusal") } const stored = (app: DocumentsApp, project: SeededProject) => app.data .global(organizations) .get(project.orgId) .then((row) => row?.branding ?? null) describe("reading the brand", () => { it("answers nothing set for an organization that has no brand", async () => { const { project } = await fresh() expect(await project.call.callByName("branding.brand.get", { org: project.orgSlug })).toEqual({ displayName: null, primary: null, accent: null, logoLight: null, logoDark: null, }) }) it("lets any member read it", async () => { const { app, project } = await fresh() const viewer = await app.addMember(project, "viewer@acme.test", "viewer") expect(await viewer.call.callByName("branding.brand.get", { org: project.orgSlug })).toMatchObject({ primary: null, }) }) }) describe("changing the brand", () => { it("stores the name and the colours on the organization and answers with them", async () => { const { app, project } = await fresh() const view = await project.call.callByName("branding.brand.set", { org: project.orgSlug, displayName: " Acme Review ", primary: "#1a1a8c", accent: "#f4c430", }) expect(view).toMatchObject({ displayName: "Acme Review", primary: "#1a1a8c", accent: "#f4c430" }) expect(await stored(app, project)).toEqual({ displayName: "Acme Review", primary: "#1a1a8c", accent: "#f4c430" }) }) it("leaves what is not sent as it was, and clears what is sent as null", async () => { const { app, project } = await fresh() await project.call.callByName("branding.brand.set", { org: project.orgSlug, primary: "#1a1a8c", accent: "#f4c430" }) await project.call.callByName("branding.brand.set", { org: project.orgSlug, accent: null }) expect(await stored(app, project)).toEqual({ primary: "#1a1a8c" }) await project.call.callByName("branding.brand.set", { org: project.orgSlug, primary: null }) // an empty brand is no brand: one representation of "none" expect(await stored(app, project)).toBeNull() }) it("refuses a colour that is not hex, with the field named and the value not repeated", async () => { const { app, project } = await fresh() for (const bad of ["red", "#12", "#gggggg", "rgb(1,2,3)", "#fff;}body{display:none", "url(//evil)"]) { const error = await refusal(project.call.callByName("branding.brand.set", { org: project.orgSlug, primary: bad })) expect(error.status, bad).toBe(422) // the refusal names the field and does not repeat what was typed in it expect(JSON.stringify(error.problem), bad).toContain("primary") expect(JSON.stringify(error.problem), bad).not.toContain(bad) } expect(await stored(app, project)).toBeNull() }) it("refuses an empty display name, which would be a name nobody sees", async () => { const { project } = await fresh() const error = await refusal( project.call.callByName("branding.brand.set", { org: project.orgSlug, displayName: " " }), ) expect(error.status).toBe(422) }) it("is an administrator's act: a member and a viewer are answered as strangers, and nothing changes", async () => { const { app, project } = await fresh() const admin = await app.addMember(project, "admin@acme.test", "admin") const member = await app.addMember(project, "member@acme.test", "member") const viewer = await app.addMember(project, "viewer@acme.test", "viewer") for (const who of [member, viewer]) { // 404, not 403: the kernel answers a member who lacks the permission as it answers a stranger (spec ยง3.3) expect( (await refusal(who.call.callByName("branding.brand.set", { org: project.orgSlug, primary: "#000000" }))).status, ).toBe(404) expect( ( await refusal( who.call.callByName("branding.logo.set", { org: project.orgSlug, variant: "light", contentType: "image/png", body: PNG.toString("base64"), }), ) ).status, ).toBe(404) } expect(await stored(app, project)).toBeNull() await admin.call.callByName("branding.brand.set", { org: project.orgSlug, primary: "#000000" }) expect(await stored(app, project)).toEqual({ primary: "#000000" }) }) it("is declared to write one audit event per change", async () => { const app = await makeDocumentsApp({ document }) expect(app.registry.procedures.get("branding.brand.set")?.def.audit).toBe("branding.brand_changed") expect(app.registry.procedures.get("branding.logo.set")?.def.audit).toBe("branding.brand_changed") expect(app.registry.audit["branding.brand_changed"]).toBeDefined() }) }) const logoKeys = (app: DocumentsApp, project: SeededProject) => app.store .keysOf({ level: "org", actor: project.owner, orgId: project.orgId }) .filter((key) => key.startsWith("branding/")) .sort() const setLogo = (project: SeededProject, variant: "light" | "dark", contentType: string, bytes: Uint8Array) => project.call.callByName("branding.logo.set", { org: project.orgSlug, variant, contentType, body: Buffer.from(bytes).toString("base64"), }) as Promise const getLogo = async (project: SeededProject, variant: "light" | "dark", size?: "1x" | "2x") => { const back = (await project.call.callByName("branding.logo.get", { org: project.orgSlug, variant, ...(size === undefined ? {} : { size }), })) as { contentType: string; body: string } return { contentType: back.contentType, bytes: Buffer.from(back.body, "base64") } } describe("the logos", () => { it("stores a raster logo as its 1x and 2x renditions, and serves the 2x unless the 1x is asked for", async () => { const { app, project } = await fresh() const view = await setLogo(project, "light", "image/png", PNG) expect(view.logoLight).toMatch(/^\/acme\/brand\/logo\/light\?v=[0-9a-f]{16}$/) expect(view.logoDark).toBeNull() const two = await getLogo(project, "light") expect(two.contentType).toBe("image/png") expect(dimensions(two.bytes)).toEqual({ width: 840, height: 210 }) expect(dimensions((await getLogo(project, "light", "1x")).bytes)).toEqual({ width: 420, height: 105 }) expect(two.bytes.equals(PNG)).toBe(false) // the object lives under the organization's own namespace of the store: two files, never the upload expect(logoKeys(app, project)).toHaveLength(2) }) it("resizes an image larger than a stored logo may be instead of refusing it, and every stored file passes the kit check", async () => { const { project } = await fresh() const big = noisePng(700, 300) expect(big.length).toBeGreaterThan(256 * 1024) await setLogo(project, "dark", "image/png", big) for (const size of ["1x", "2x"] as const) { const { bytes, contentType } = await getLogo(project, "dark", size) expect(bytes.length).toBeLessThanOrEqual(256 * 1024) expect(checkLogo(contentType, bytes)).toMatchObject({ ok: true }) } }) it("takes a WebP and stores it as PNG", async () => { const { project } = await fresh() await setLogo(project, "dark", "image/webp", MARK_WEBP) const { bytes, contentType } = await getLogo(project, "dark") expect(contentType).toBe("image/png") expect(dimensions(bytes)).toEqual({ width: 400, height: 100 }) }) it("replaces a logo and removes the files it replaced", async () => { const { app, project } = await fresh() await setLogo(project, "dark", "image/png", png(100)) const second = await setLogo(project, "dark", "image/png", png(200)) const keys = logoKeys(app, project) expect(keys).toHaveLength(2) expect(keys.every((key) => second.logoDark?.includes(key.split("-")[2] ?? "?"))).toBe(true) // the same image again is the same logo: nothing is lost const third = await setLogo(project, "dark", "image/png", png(200)) expect(third.logoDark).toBe(second.logoDark) expect(logoKeys(app, project)).toEqual(keys) }) it("keeps an SVG that is shapes alone exactly as uploaded, one file for both sizes", async () => { const { app, project } = await fresh() const logo = svgOf('') const view = await setLogo(project, "light", "image/svg+xml", logo) expect(view.logoLight).not.toBeNull() expect(logoKeys(app, project)).toHaveLength(1) for (const size of [undefined, "1x", "2x"] as const) { const back = await getLogo(project, "light", size) expect(back.contentType).toBe("image/svg+xml") expect(back.bytes.equals(logo)).toBe(true) } }) it("refuses an SVG that carries a script, stores nothing and leaves the brand as it was", async () => { const { app, project } = await fresh() await project.call.callByName("branding.brand.set", { org: project.orgSlug, primary: "#1a1a8c" }) const before = await stored(app, project) for (const inner of [ "", '', '', ]) { const error = await refusal(setLogo(project, "light", "image/svg+xml", svgOf(inner))) expect(error.status, inner).toBe(422) } expect(await stored(app, project)).toEqual(before) expect(logoKeys(app, project)).toEqual([]) }) it("refuses a file the kit's checks refuse, naming the rule and not the file", async () => { const { app, project } = await fresh() const damaged = Buffer.from(PNG) damaged[40] = (damaged[40] ?? 0) ^ 0xff const lie = Buffer.concat([ PNG.subarray(0, 8), PNG.subarray(8, 33), chunk("IDAT", Buffer.from("not zlib data at all")), chunk("IEND", new Uint8Array()), ]) for (const [name, type, bytes, rule] of [ ["a PNG that is really markup", "image/png", Buffer.from(""), /not a PNG/], ["a WebP that is a PNG", "image/webp", PNG, /not a WebP/], ["a PNG with a damaged chunk", "image/png", damaged, /damaged/], ["a PNG cut short", "image/png", PNG.subarray(0, 200), /damaged|PNG/], ["a PNG whose data does not decode", "image/png", lie, /decoded|visible/], ["a type that is not an image", "text/html", Buffer.from(""), /PNG, WebP or SVG/], [ "more than the upload limit", "image/png", Buffer.concat([PNG, Buffer.alloc(2 * 1024 * 1024 + 1 - PNG.length)]), /larger than/, ], ] as const) { const error = await refusal(setLogo(project, "light", type, bytes)) expect(error.status, name).toBe(422) expect(JSON.stringify(error.problem), name).toMatch(rule) } expect(await stored(app, project)).toBeNull() expect(logoKeys(app, project)).toEqual([]) }) it("refuses a body that is not base64", async () => { const { project } = await fresh() const text = await refusal( project.call.callByName("branding.logo.set", { org: project.orgSlug, variant: "light", contentType: "image/png", body: "not base64!", }), ) expect(text.status).toBe(422) }) it("is removed from the brand and from the store when it is cleared, and then there is no logo to read", async () => { const { app, project } = await fresh() await setLogo(project, "light", "image/png", PNG) await setLogo(project, "dark", "image/png", png(100)) expect(logoKeys(app, project)).toHaveLength(4) await project.call.callByName("branding.logo.clear", { org: project.orgSlug, variant: "light" }) expect(logoKeys(app, project)).toHaveLength(2) expect( (await refusal(project.call.callByName("branding.logo.get", { org: project.orgSlug, variant: "light" }))).status, ).toBe(404) expect(dimensions((await getLogo(project, "dark")).bytes)).toEqual({ width: 100, height: 60 }) }) it("is not readable by another organization's member", async () => { const { app, project } = await fresh() await setLogo(project, "light", "image/png", PNG) const other = await app.seedProject("other", "owner@other.test") expect( (await refusal(other.call.callByName("branding.logo.get", { org: project.orgSlug, variant: "light" }))).status, ).toBe(404) }) }) describe("what the organization layout writes from a stored brand", () => { it("is the brand's own colours, legible in both themes", () => { const { style, report } = tenantStyle({ primary: "#1a1a8c", accent: "#f4c430" }) expect(style["--brand-accent"]).toBe("#1a1a8c") expect(style["--brand-accent-foreground"]).toBe("#ffffff") expect(report.dark?.inkContrast).toBeGreaterThanOrEqual(4.5) expect(report.light?.inkContrast).toBeGreaterThanOrEqual(4.5) }) it("is nothing for an organization with no colour of its own, so the app's brand shows", () => { expect(tenantStyle({ primary: null, accent: null }).style).toEqual({}) }) })