Files
SnapOtter/tests/unit/api/csp.test.ts
T
SnapOtter 8f9ba701be fix: QR code logo causes preview to vanish and become unrecoverable
The QR code generator's logo feature was broken in production (Docker)
due to three interacting issues:

1. The CSP connect-src directive did not include data:, so the
   qr-code-styling library's internal XHR to convert logo data URLs to
   blobs was silently blocked. The library has no onerror handler, so the
   render promise hung forever after the container was already cleared.

2. crossOrigin: "anonymous" was unnecessarily set on imageOptions for
   data URLs, which can cause canvas taint issues.

3. The logo options used a conditional spread that omitted the image key
   when no logo was set. The library's update() deep-merges options, so
   removing the logo preserved the stale data URL and the QR stayed
   broken even after logo removal.

Closes #121
2026-05-06 21:38:12 +08:00

76 lines
2.9 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { buildCsp } from "../../../apps/api/src/lib/csp.js";
function parseDirective(csp: string, directive: string): string[] {
const match = csp.match(new RegExp(`${directive}\\s+([^;]+)`));
return match ? match[1].trim().split(/\s+/) : [];
}
describe("buildCsp", () => {
describe("connect-src allows analytics domains", () => {
it.each([true, false])("includes PostHog ingest (isDocs=%s)", (isDocs) => {
const sources = parseDirective(buildCsp(isDocs), "connect-src");
expect(sources).toContain("https://us.i.posthog.com");
});
it.each([true, false])("includes PostHog assets (isDocs=%s)", (isDocs) => {
const sources = parseDirective(buildCsp(isDocs), "connect-src");
expect(sources).toContain("https://us-assets.i.posthog.com");
});
it.each([true, false])("includes Sentry ingest (isDocs=%s)", (isDocs) => {
const sources = parseDirective(buildCsp(isDocs), "connect-src");
expect(sources).toContain("https://*.ingest.us.sentry.io");
});
it.each([true, false])("keeps self (isDocs=%s)", (isDocs) => {
expect(parseDirective(buildCsp(isDocs), "connect-src")).toContain("'self'");
});
});
describe("script-src allows PostHog config loader", () => {
it.each([true, false])("includes PostHog assets origin (isDocs=%s)", (isDocs) => {
const sources = parseDirective(buildCsp(isDocs), "script-src");
expect(sources).toContain("https://us-assets.i.posthog.com");
});
it("docs pages allow unsafe-inline for Scalar", () => {
expect(parseDirective(buildCsp(true), "script-src")).toContain("'unsafe-inline'");
});
it("app pages do not allow unsafe-inline", () => {
expect(parseDirective(buildCsp(false), "script-src")).not.toContain("'unsafe-inline'");
});
});
describe("font-src allows Scalar docs fonts", () => {
it("docs pages include Scalar fonts origin", () => {
const sources = parseDirective(buildCsp(true), "font-src");
expect(sources).toContain("https://fonts.scalar.com");
});
it("app pages do not include Scalar fonts origin", () => {
const sources = parseDirective(buildCsp(false), "font-src");
expect(sources).not.toContain("https://fonts.scalar.com");
});
});
it("includes frame-ancestors none for app pages but not docs", () => {
expect(buildCsp(false)).toContain("frame-ancestors 'none'");
expect(buildCsp(true)).not.toContain("frame-ancestors");
});
it("allows OpenStreetMap tiles in img-src for app pages", () => {
const sources = parseDirective(buildCsp(false), "img-src");
expect(sources).toContain("https://tile.openstreetmap.org");
});
it.each([
true,
false,
])("connect-src allows data: URIs for client-side blob operations (isDocs=%s)", (isDocs) => {
const sources = parseDirective(buildCsp(isDocs), "connect-src");
expect(sources).toContain("data:");
});
});