// 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(``)
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({})
})
})