// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0 // The brand as a share image and an email show it (spec §10.2, §10.3): the app brand's name and logo, or an // organization's, drawn through the kit's logo checks. Every logo here goes the way it goes in production: an // organization's through the caller (so a stored, normalised file), the app's from a file of a public folder. import { mkdtempSync, rmSync, writeFileSync } from "node:fs" import { tmpdir } from "node:os" import { join } from "node:path" import { type BrandView, type ComposedEmail, composeEmail, type MailTransport, renderShareImage, type ShareBrand, sendEmail, shareBrand, } from "@app/modules/branding" import { afterEach, describe, expect, it } from "vitest" import { appLogoSource, readersFor } from "../apps/web/src/server/brand-assets" import { closeApps, makeDocumentsApp, type SeededProject } from "./support/documents-app.js" import { dimensions, markPng, pixelAt } from "./support/images.js" const directories: string[] = [] afterEach(async () => { await closeApps() for (const dir of directories.splice(0)) rmSync(dir, { recursive: true, force: true }) }) const TEAL = [0x80, 0xcd, 0xc6, 255] const svg = (fill: string) => `` /** A public folder holding the app brand's two logos: a blue rectangle for light, a red one for dark. */ function publicFolder(): string { const dir = mkdtempSync(join(tmpdir(), "share-public-")) directories.push(dir) writeFileSync(join(dir, "app-light.svg"), svg("#3366aa")) writeFileSync(join(dir, "app-dark.svg"), svg("#aa3333")) return dir } const appIdentity = { name: "Example App", logo: { light: "/app-light.svg", dark: "/app-dark.svg" } } const empty: BrandView = { displayName: null, primary: null, accent: null, logoLight: null, logoDark: null } async function acme(): Promise<{ project: SeededProject }> { const app = await makeDocumentsApp({ document: { engine: { name: "unused", version: "0" }, page_count: 0, pages: [] }, }) return { project: await app.seedProject("acme", "owner@acme.test") } } const upload = (project: SeededProject, variant: "light" | "dark", bytes: Uint8Array) => project.call.callByName("branding.logo.set", { org: project.orgSlug, variant, contentType: "image/png", body: Buffer.from(bytes).toString("base64"), }) // The logo box of the share image: 300 x 80 at (80, 80); a wide logo's middle is at (230, 120). const logoCentre = (png: Uint8Array) => pixelAt(png, 230, 120) describe("the app brand's logo from the public folder", () => { it("is read from a file of the folder and from nowhere else", async () => { const dir = publicFolder() expect(await appLogoSource("/app-dark.svg", dir)).toMatchObject({ contentType: "image/svg+xml" }) await expect(appLogoSource("/../secret.svg", dir)).rejects.toThrow(/public folder/) await expect(appLogoSource("/app.exe", dir)).rejects.toThrow(/PNG, WebP or SVG/) await expect(appLogoSource("/missing.svg", dir)).rejects.toThrow() }) }) describe("the share image", () => { it("draws the app brand's logo for the dark page when there is no organization", async () => { const dir = publicFolder() const share = await shareBrand({ app: appIdentity, tenant: null, readers: { appLogo: (path) => appLogoSource(path, dir), tenantLogo: () => Promise.reject(new Error("none")) }, }) expect(share.name).toBe("Example App") const png = await renderShareImage(share, { title: share.name }) expect(dimensions(png)).toEqual({ width: 1200, height: 630 }) expect(logoCentre(png)).toEqual([0xaa, 0x33, 0x33, 255]) }) it("draws an organization's own logo and display name, and the logo is the stored one", async () => { const { project } = await acme() await project.call.callByName("branding.brand.set", { org: project.orgSlug, displayName: "Acme Review" }) await upload(project, "dark", markPng(2000, 500)) const brand = (await project.call.callByName("branding.brand.get", { org: project.orgSlug })) as BrandView const share = await shareBrand({ app: appIdentity, tenant: { name: "Acme Ltd", brand }, readers: readersFor(project.call, project.orgSlug, publicFolder()), }) expect(share.name).toBe("Acme Review") expect(share.logo.contentType).toBe("image/png") expect(logoCentre(await renderShareImage(share, { title: share.name }))).toEqual(TEAL) }) it("draws the only logo the organization has when it is the light one, because the page is dark", async () => { const { project } = await acme() await upload(project, "light", markPng(2000, 500)) const brand = (await project.call.callByName("branding.brand.get", { org: project.orgSlug })) as BrandView expect(brand.logoDark).toBeNull() const share = await shareBrand({ app: appIdentity, tenant: { name: "Acme Ltd", brand }, readers: readersFor(project.call, project.orgSlug, publicFolder()), }) expect(share.name).toBe("Acme Ltd") expect(logoCentre(await renderShareImage(share, { title: share.name }))).toEqual(TEAL) }) it("is not stopped by a missing app logo file when the organization has a good logo of its own", async () => { const { project } = await acme() await upload(project, "dark", markPng(2000, 500)) const brand = (await project.call.callByName("branding.brand.get", { org: project.orgSlug })) as BrandView const share = await shareBrand({ app: appIdentity, tenant: { name: "Acme Ltd", brand }, readers: readersFor(project.call, project.orgSlug, mkdtempSync(join(tmpdir(), "share-nofile-"))), }) expect(logoCentre(await renderShareImage(share, { title: share.name }))).toEqual(TEAL) }) it("falls back to the app brand's logo for an organization with none", async () => { const { project } = await acme() const share = await shareBrand({ app: appIdentity, tenant: { name: "Acme Ltd", brand: empty }, readers: readersFor(project.call, project.orgSlug, publicFolder()), }) expect(logoCentre(await renderShareImage(share, { title: share.name }))).toEqual([0xaa, 0x33, 0x33, 255]) }) it("draws the app brand's logo instead when the chosen one cannot be drawn, and does not fail", async () => { const dir = publicFolder() const broken: ShareBrand = { name: "Acme Ltd", logo: { contentType: "image/png", bytes: new TextEncoder().encode("") }, fallback: await appLogoSource("/app-dark.svg", dir), } expect(logoCentre(await renderShareImage(broken, { title: broken.name }))).toEqual([0xaa, 0x33, 0x33, 255]) const refused: ShareBrand = { ...broken, fallback: broken.logo } await expect(renderShareImage(refused, { title: "x" })).rejects.toThrow(/logo cannot be used/) }) }) describe("the email", () => { const recorded = () => { const sent: Array = [] const transport: MailTransport = { async send(message) { sent.push(message) }, } return { sent, transport } } async function shareOf(logo: Uint8Array | null, dir = publicFolder()): Promise { const { project } = await acme() if (logo) await upload(project, "dark", logo) const brand = (await project.call.callByName("branding.brand.get", { org: project.orgSlug })) as BrandView return shareBrand({ app: appIdentity, tenant: { name: "Acme Ltd", brand: { ...brand, displayName: "Acme Review" } }, readers: readersFor(project.call, project.orgSlug, dir), }) } it("carries the organization's name and its logo as an inline attachment, and names no host", async () => { const mail = await composeEmail(await shareOf(markPng(2000, 500)), { subject: "You are invited", paragraphs: ["Hello,", "Open the app to accept."], }) expect(mail.subject).toBe("You are invited - Acme Review") expect(mail.html).toContain('alt="Acme Review"') expect(mail.html).toContain('src="cid:brand-logo"') expect(mail.html).not.toMatch(/https?:/) expect(mail.html.toLowerCase()).not.toContain("conusai") expect(mail.text).toBe("Hello,\n\nOpen the app to accept.\n\nAcme Review\n") const [logo] = mail.attachments expect(logo).toMatchObject({ cid: "brand-logo", contentType: "image/png" }) expect(dimensions(logo?.content ?? new Uint8Array())).toEqual({ width: 420, height: 105 }) expect(pixelAt(logo?.content ?? new Uint8Array(), 210, 52)).toEqual(TEAL) }) it("uses the app brand's logo for an organization with none", async () => { const mail = await composeEmail(await shareOf(null), { subject: "Hi", paragraphs: ["x"] }) const content = mail.attachments[0]?.content ?? new Uint8Array() expect(dimensions(content).width).toBe(420) expect(pixelAt(content, 210, 52)).toEqual([0xaa, 0x33, 0x33, 255]) }) it("escapes what it is given as text, and refuses a subject that is more than one line", async () => { const share = await shareOf(null) const mail = await composeEmail(share, { subject: "x", paragraphs: [' & more'] }) expect(mail.html).toContain("<script>alert("1")</script> & more") expect(mail.html).not.toContain("