// @vitest-environment node import { readFileSync } from "node:fs"; import Ajv2020, { type ValidateFunction } from "ajv/dist/2020.js"; import { describe, expect, it } from "vitest"; import { parseToolboxApp, parseToolboxCatalog } from "../src/index.js"; const appSchema = JSON.parse( readFileSync( new URL("../../../schemas/toolbox-app.v1.schema.json", import.meta.url), "utf8", ), ) as object; const catalogSchema = JSON.parse( readFileSync( new URL("../../../schemas/toolbox-catalog.v1.schema.json", import.meta.url), "utf8", ), ) as object; const ajv = new Ajv2020({ allErrors: true, strict: true, // `not: { required: [...] }` intentionally names the opposite union arm. strictRequired: false, }); const validateApp = ajv.compile(appSchema); const validateCatalog = ajv.compile(catalogSchema); type Parser = (value: unknown) => unknown; function runtimeAccepts(parser: Parser, value: unknown): boolean { try { parser(value); return true; } catch { return false; } } function expectParity( validate: ValidateFunction, parser: Parser, value: unknown, expected: boolean, ): void { const schemaAccepted = validate(value); expect( schemaAccepted, ajv.errorsText(validate.errors, { separator: "\n" }), ).toBe(expected); expect(runtimeAccepts(parser, value)).toBe(expected); } const validApp = (): Record => ({ schemaVersion: 1, id: "de.add-ideas.example", name: "Example", version: "1.2.3", description: "Example application", entry: "./", icon: "./icon.svg", categories: ["documents"], tags: ["example"], integration: { contextVersion: 1, launchModes: ["navigate"], embedding: "unsupported", }, requirements: { secureContext: false, workers: false, indexedDb: false, crossOriginIsolated: false, }, privacy: { processing: "local", fileUploads: false, telemetry: false, }, source: { repository: "https://example.test/source", license: "MIT", }, }); const validCatalog = (): Record => ({ schemaVersion: 1, id: "de.add-ideas.toolbox", name: "Example Toolbox", home: "./", theme: { mode: "system", brand: "Example" }, apps: [{ manifest: "./apps/example/toolbox-app.json", enabled: true }], }); describe("canonical schema and runtime parser parity", () => { const acceptedApps: Array<[string, () => Record]> = [ ["the baseline app", validApp], [ "relative references containing spaces and path @ signs", () => ({ ...validApp(), entry: " ./nested app/?mode=1#result ", actions: [{ id: "docs", label: "Docs", url: "./people/@me" }], }), ], [ "case-insensitive and compact absolute HTTP URLs", () => ({ ...validApp(), source: { repository: "HTTPS:example.test", license: "MIT" }, }), ], ]; it.each(acceptedApps)("accepts %s", (_label, fixture) => { expectParity(validateApp, parseToolboxApp, fixture(), true); }); const rejectedApps: Array<[string, () => Record]> = [ ["a whitespace-only name", () => ({ ...validApp(), name: " \t " })], [ "a whitespace-only list item", () => ({ ...validApp(), categories: ["documents", " \n "] }), ], [ "a non-web entry scheme", () => ({ ...validApp(), entry: "javascript:alert(1)" }), ], ["a backslash path", () => ({ ...validApp(), icon: ".\\icon.svg" })], ["an empty HTTP authority", () => ({ ...validApp(), entry: "https://" })], [ "a credential-bearing privacy URL", () => ({ ...validApp(), privacy: { processing: "local", fileUploads: false, telemetry: false, url: "https://user:secret@example.test/privacy", }, }), ], [ "a relative source repository", () => ({ ...validApp(), source: { repository: "../source", license: "MIT" }, }), ], [ "a compact credential-bearing source repository", () => ({ ...validApp(), source: { repository: "https:user@example.test", license: "MIT" }, }), ], [ "a whitespace-only action label", () => ({ ...validApp(), actions: [{ id: "docs", label: " ", url: "./docs" }], }), ], ]; it.each(rejectedApps)("rejects %s", (_label, fixture) => { expectParity(validateApp, parseToolboxApp, fixture(), false); }); const acceptedCatalogs: Array<[string, () => Record]> = [ ["the baseline catalog", validCatalog], [ "an uppercase HTTPS manifest URL", () => ({ ...validCatalog(), apps: [ { manifest: "HTTPS://example.test/apps/example/toolbox-app.json", enabled: true, }, ], }), ], ]; it.each(acceptedCatalogs)("accepts %s", (_label, fixture) => { expectParity(validateCatalog, parseToolboxCatalog, fixture(), true); }); const rejectedCatalogs: Array<[string, () => Record]> = [ [ "a whitespace-only catalog name", () => ({ ...validCatalog(), name: " " }), ], [ "a whitespace-only brand", () => ({ ...validCatalog(), theme: { mode: "system", brand: "\t" }, }), ], [ "a non-web home URL", () => ({ ...validCatalog(), home: "file:///tmp/toolbox" }), ], [ "a backslash manifest path", () => ({ ...validCatalog(), apps: [{ manifest: ".\\apps\\toolbox-app.json", enabled: true }], }), ], [ "a credential-bearing manifest URL", () => ({ ...validCatalog(), apps: [ { manifest: "https://user:secret@example.test/toolbox-app.json", enabled: true, }, ], }), ], [ "a non-web external entry", () => ({ ...validCatalog(), apps: [ { name: "External", entry: "data:text/html,hello", launch: "new-tab", enabled: true, }, ], }), ], [ "a whitespace-only external name", () => ({ ...validCatalog(), apps: [ { name: " ", entry: "https://example.test/", launch: "new-tab", enabled: true, }, ], }), ], ]; it.each(rejectedCatalogs)("rejects %s", (_label, fixture) => { expectParity(validateCatalog, parseToolboxCatalog, fixture(), false); }); });