import { ToolboxValidationError, contextualizeToolboxLink, discoverToolboxCatalog, loadToolboxContext, parseToolboxApp, parseToolboxCatalog, type ToolboxAppManifest, } from "../src/index.js"; import { describe, expect, it, vi } from "vitest"; const app = ( overrides: Record = {}, ): Record => ({ schemaVersion: 1, id: "de.add-ideas.pdf-tools", name: "PDF Workbench", version: "1.2.3", description: "Local PDF tools", entry: "./index.html", icon: "./icon.svg", categories: ["documents", "pdf"], tags: ["merge", "split"], integration: { contextVersion: 1, launchModes: ["navigate", "new-tab"], embedding: "unsupported", }, requirements: { secureContext: true, workers: true, indexedDb: true, crossOriginIsolated: false, }, privacy: { processing: "local", fileUploads: false, telemetry: false, }, source: { repository: "https://git.example.test/pdf-tools", license: "AGPL-3.0-only", }, ...overrides, }); const catalog = ( overrides: Record = {}, ): Record => ({ schemaVersion: 1, id: "de.add-ideas.toolbox", name: "Toolbox", home: "./", theme: { mode: "system", brand: "add·ideas" }, apps: [{ manifest: "./pdf/toolbox-app.json", enabled: true }], ...overrides, }); const json = (value: unknown, init: ResponseInit = {}): Response => new Response(JSON.stringify(value), { status: 200, headers: { "content-type": "application/json" }, ...init, }); describe("v1 runtime parsing", () => { it("parses the full app contract and drops unknown optional fields", () => { const parsed = parseToolboxApp( app({ futureField: "ignored", privacy: { processing: "local", fileUploads: false, telemetry: false, futurePrivacyField: true, }, }), ); expect(parsed).toMatchObject({ schemaVersion: 1, id: "de.add-ideas.pdf-tools", privacy: { processing: "local", fileUploads: false, telemetry: false }, }); expect(parsed).not.toHaveProperty("futureField"); expect(parsed.privacy).not.toHaveProperty("futurePrivacyField"); }); it("rejects unsupported versions and malformed known optional fields", () => { expect(() => parseToolboxApp(app({ schemaVersion: 2 }))).toThrow( ToolboxValidationError, ); expect(() => parseToolboxApp( app({ requirements: { secureContext: true, workers: true, indexedDb: true, crossOriginIsolated: false, topLevelContext: "sometimes", }, }), ), ).toThrow(/topLevelContext must be a boolean/u); }); it("accepts omitted source metadata but validates it when supplied", () => { const withoutSource = app(); delete withoutSource.source; expect(parseToolboxApp(withoutSource)).not.toHaveProperty("source"); expect(() => parseToolboxApp( app({ source: { repository: "../repository", license: "MIT" } }), ), ).toThrow(/absolute HTTP\(S\) URL/u); }); it("parses manifest references and external inline catalog entries", () => { const parsed = parseToolboxCatalog( catalog({ futureField: 1, apps: [ { manifest: "./pdf/toolbox-app.json", enabled: true, future: true }, { name: "External docs", entry: "https://docs.example.test/", launch: "new-tab", enabled: true, }, ], }), ); expect(parsed.apps).toHaveLength(2); expect(parsed.apps[1]).toEqual({ name: "External docs", entry: "https://docs.example.test/", launch: "new-tab", enabled: true, }); expect(parsed).not.toHaveProperty("futureField"); }); }); describe("same-origin discovery", () => { it("prefers a same-origin query context over meta context", () => { const result = discoverToolboxCatalog({ location: "https://tools.example.test/pdf/?toolbox=%2Fcatalog.json", document: { querySelector: () => ({ getAttribute: () => "/meta-catalog.json", }), }, }); expect(result.status).toBe("found"); if (result.status === "found") { expect(result.source).toBe("query"); expect(result.catalogUrl.href).toBe( "https://tools.example.test/catalog.json", ); } }); it("rejects a cross-origin query without falling through to meta", () => { const result = discoverToolboxCatalog({ location: "https://tools.example.test/pdf/?toolbox=https%3A%2F%2Fevil.test%2Fcatalog.json", document: { querySelector: () => ({ getAttribute: () => "/safe.json" }), }, }); expect(result.status).toBe("error"); if (result.status === "error") expect(result.error.code).toBe("cross-origin"); }); }); describe("loading and links", () => { it("loads a catalog, resolves manifests and URLs, and supports external apps", async () => { const fetch = vi.fn( async (input: string | URL | Request, init?: RequestInit) => { void init; const url = new URL(input instanceof Request ? input.url : input); if (url.pathname === "/catalog.json") { return json( catalog({ apps: [ { manifest: "./pdf/toolbox-app.json", enabled: true }, { manifest: "./disabled/toolbox-app.json", enabled: false }, { name: "Documentation", entry: "https://docs.example.test/", launch: "new-tab", enabled: true, }, ], }), ); } if (url.pathname === "/pdf/toolbox-app.json") return json(app()); if (url.pathname === "/disabled/toolbox-app.json") { throw new Error("Disabled manifests must not be fetched"); } return new Response("Not found", { status: 404 }); }, ); const result = await loadToolboxContext({ location: "https://tools.example.test/pdf/?toolbox=/catalog.json", fetch, }); for (const [, init] of fetch.mock.calls) { expect(init).toMatchObject({ redirect: "error" }); } expect(result.status).toBe("ready"); if (result.status === "ready") { expect(result.context.catalog.homeUrl.href).toBe( "https://tools.example.test/", ); expect(result.context.catalog.apps[0]?.kind).toBe("manifest"); const first = result.context.catalog.apps[0]; if (first?.kind === "manifest") { expect(first.app.entryUrl.href).toBe( "https://tools.example.test/pdf/index.html", ); } expect(result.context.catalog.apps[1]).toMatchObject({ kind: "external", launch: "new-tab", }); } }); it("returns a graceful error result for invalid JSON and fetch failure", async () => { const invalid = await loadToolboxContext({ location: "https://tools.example.test/?toolbox=/catalog.json", fetch: async () => json(catalog({ schemaVersion: 2 })), }); expect(invalid.status).toBe("error"); const unavailable = await loadToolboxContext({ location: "https://tools.example.test/?toolbox=/catalog.json", fetch: async () => new Response("No", { status: 503 }), }); expect(unavailable.status).toBe("error"); if (unavailable.status === "error") { expect(unavailable.error.code).toBe("fetch-failed"); } }); it("rejects a fetch redirected to a cross-origin response URL", async () => { const response = json(catalog()); Object.defineProperty(response, "url", { value: "https://evil.example/toolbox.catalog.json", }); const result = await loadToolboxContext({ location: "https://tools.example.test/?toolbox=/catalog.json", fetch: async () => response, }); expect(result.status).toBe("error"); if (result.status === "error") expect(result.error.code).toBe("cross-origin"); }); it("adds context to same-origin links but leaves external links alone", () => { expect( new URL( contextualizeToolboxLink("./other/", "/catalog.json", { location: "https://tools.example.test/pdf/", }), ).searchParams.get("toolbox"), ).toBe("https://tools.example.test/catalog.json"); expect( contextualizeToolboxLink("https://docs.example.test/", "/catalog.json", { location: "https://tools.example.test/pdf/", }), ).toBe("https://docs.example.test/"); }); }); export const validAppFixture = app() as unknown as ToolboxAppManifest;