mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
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
76 lines
2.9 KiB
TypeScript
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:");
|
|
});
|
|
});
|