import { expect } from "@std/expect"; import { describe, it } from "@std/testing/bdd"; import type { JSONSchema } from "@commonfabric/api"; import type { SchemaGenerationDiagnostic } from "../../src/interface.ts"; import { SchemaGenerator } from "../../src/schema-generator.ts"; import { asObjectSchema, getTypeFromCode } from "../utils.ts"; const ALIASES = ` type Cfc = T & { readonly __ct_cfc__?: { readonly meta?: Meta; readonly of?: T } }; type Confidential = Cfc; type WriteAuthorizedBy = Cfc; type Identity = X; type Fn = () => void; declare const f: Fn; declare const g: Fn; `; /** * Generates the schema and diagnostics of the fixture's root interface, as a * schema that defines a document where `definesDocument` is set. */ async function generate(code: string, definesDocument = false) { const { type, checker } = await getTypeFromCode(ALIASES + code, "Holder"); const diagnostics: SchemaGenerationDiagnostic[] = []; const schema = asObjectSchema( new SchemaGenerator().generateSchema(type, checker, undefined, { definesDocument, onDiagnostic: (diagnostic) => diagnostics.push(diagnostic), writerIdentityForSourceFile: (file) => ({ file }), }), ); return { schema, diagnostics }; } /** Each recursive value, through references, requiring the chain to settle. */ function* recursiveValues( schema: ReturnType, root: JSONSchema, ) { const resolve = (value: JSONSchema) => { const node = asObjectSchema(value); return typeof node.$ref === "string" ? asObjectSchema(schema.$defs![node.$ref.split("/").pop()!]!) : node; }; let node = resolve(root); const visited = new Set(); for (;;) { yield resolve(node.properties!.value!); const optionalNext = asObjectSchema(node.properties!.next!); const next = optionalNext.anyOf ? asObjectSchema( optionalNext.anyOf.find((arm) => typeof arm === "object" && typeof arm.$ref === "string" )!, ) : optionalNext; if (typeof next.$ref === "string") { if (visited.has(next.$ref)) break; visited.add(next.$ref); } node = resolve(next); } expect(visited.size).toBeGreaterThan(0); } describe("recursive binding identity", () => { it("keeps a writer named by a nongeneric tuple's declared element", async () => { const { schema, diagnostics } = await generate(` interface Holder { pair: [WriteAuthorizedBy] } `); const pair = asObjectSchema(schema.properties!.pair!); expect(pair.items).toEqual({ type: "string", ifc: { writeAuthorizedBy: { __ctWriterIdentityOf: { file: "test.ts", path: ["f"] }, }, }, }); expect(diagnostics).toEqual([]); }); it("keeps a writer named by the declared element of a nongeneric tuple alias", async () => { const { schema, diagnostics } = await generate(` type Pair = [WriteAuthorizedBy]; type Named = Pair; interface Holder { pair: Pair; named: Named } `); for (const field of ["pair", "named"]) { expect(asObjectSchema(schema.properties![field]!).items).toEqual({ type: "string", ifc: { writeAuthorizedBy: { __ctWriterIdentityOf: { file: "test.ts", path: ["f"] }, }, }, }); } expect(diagnostics).toEqual([]); }); it("keeps inherited, forwarded, and defaulted writers distinct through plain generics", async () => { const { schema, diagnostics } = await generate(` interface Base { left: WriteAuthorizedBy; right: WriteAuthorizedBy; } interface Derived extends Base {} type Forward = Derived; interface Holder { original: Forward; reversed: Forward; defaulted: Forward; } `); for ( const [field, left, right] of [ ["original", "f", "g"], ["reversed", "g", "f"], ["defaulted", "f", "f"], ] as const ) { const members = asObjectSchema(schema.properties![field]!).properties!; for ( const [member, writer] of [["left", left], ["right", right]] as const ) { expect(asObjectSchema(members[member]!).ifc?.writeAuthorizedBy) .toEqual({ __ctWriterIdentityOf: { file: "test.ts", path: [writer] }, }); } } expect(diagnostics).toEqual([]); }); it("keeps each named policy through Record and recursive interface payloads", async () => { const { schema, diagnostics } = await generate(` type F = WriteAuthorizedBy; type G = WriteAuthorizedBy; interface Node { value: W; next?: Sec> } type Sec = Confidential, readonly ["a"]>; interface Holder { f: Sec; g: Sec; records: Record; } `); for (const writer of ["f", "g"]) { for ( const value of recursiveValues(schema, schema.properties![writer]!) ) { expect(value.ifc?.writeAuthorizedBy).toEqual({ __ctWriterIdentityOf: { file: "test.ts", path: [writer] }, }); } } const record = asObjectSchema(schema.properties!.records!); const value = asObjectSchema(record.additionalProperties!); const resolved = typeof value.$ref === "string" ? asObjectSchema(schema.$defs![value.$ref.split("/").pop()!]!) : value; expect(resolved.ifc?.writeAuthorizedBy).toEqual({ __ctWriterIdentityOf: { file: "test.ts", path: ["f"] }, }); expect(diagnostics).toEqual([]); }); for (const member of ["[W][0]", "W extends unknown ? W : never"]) { it(`reports an unread writer when a generic member uses ${member} in a schema that defines a document, and nothing in a view`, async () => { const code = ` interface Box { value: ${member} } interface Holder { a: Box> } `; const { diagnostics } = await generate(code, true); const view = await generate(code); expect(diagnostics).toHaveLength(1); expect(diagnostics[0]).toMatchObject({ type: "cfc-write-authorized-by:unread", severity: "error", }); expect(diagnostics[0]!.message).toContain("operator syntax"); expect(diagnostics[0]!.message).toContain("pass the policy unchanged"); expect(view.diagnostics).toEqual([]); }); } for (const order of [["x", "y"], ["y", "x"]]) { it(`rejects indirect writer bindings in recursive policies, reading ${order.join(" then ")}`, async () => { const { diagnostics } = await generate(` type Indirect = typeof f; type Pair = Confidential<{ left: WriteAuthorizedBy; right: WriteAuthorizedBy; }, readonly ["pair"]>; type Sec = Confidential<{ value: W; next?: Sec>; }, readonly ["a"]>; interface Holder { ${ order.map((field) => `${field}: Sec>;` ).join("\n") } } `); expect(diagnostics.length).toBeGreaterThan(0); for (const diagnostic of diagnostics) { expect(diagnostic).toMatchObject({ type: "cfc-write-authorized-by:unread", severity: "error", }); } }); } for (const order of [["f", "g"], ["g", "f"]]) { for ( const [spelling, declarations, argument] of [ [ "a written policy", "", (writer: string) => `WriteAuthorizedBy`, ], [ "an alias with a writer argument", "type Owned = WriteAuthorizedBy;", (writer: string) => `Owned`, ], [ "an alias whose writer defaults to its preceding argument", "type Owned = WriteAuthorizedBy;", (writer: string) => `Owned`, ], [ "an alias with a fixed writer", `type F = WriteAuthorizedBy; type G = WriteAuthorizedBy;`, (writer: string) => `${writer.toUpperCase()}`, ], ] as const ) { it(`keeps each recursive value's writer through ${spelling}, reading ${order.join(" then ")}`, async () => { const { schema, diagnostics } = await generate(` ${declarations} type Sec = Confidential<{ value: W; next?: Sec>; }, readonly ["a"]>; interface Holder { ${ order.map((writer) => `${writer}: Sec<${argument(writer)}>;`).join( "\n", ) } } `); for (const writer of order) { for ( const value of recursiveValues(schema, schema.properties![writer]!) ) { expect(value.ifc?.writeAuthorizedBy).toEqual({ __ctWriterIdentityOf: { file: "test.ts", path: [writer] }, }); } } expect(diagnostics).toEqual([]); }); } } for (const operator of ["|", "&"]) { it(`keeps a recursive policy finite when ${operator} repeats the same writer`, async () => { const { schema, diagnostics } = await generate(` type Sec = Confidential<{ value: W; next?: Sec>; }, readonly ["a"]>; interface Holder { f: Sec> } `); expect(diagnostics).toEqual([]); expect(Object.keys(schema.$defs ?? {}).length).toBeGreaterThan(0); for (const definition of Object.values(schema.$defs!)) { const next = asObjectSchema( asObjectSchema(definition).properties!.next!, ); expect(next.$ref).toMatch(/^#\/\$defs\//); } }); } for ( const argument of [ "If>", "First<[W, WriteAuthorizedBy]>", "Unbox>", "NonNullable", "W[]", ] ) { it(`rejects a policy chain that reaches the nesting limit through ${argument}`, async () => { const { diagnostics } = await generate(` type If = C extends true ? A : B; type First = T[0]; type Box = { boxed: T }; type Unbox = T extends Box ? U : T; type Sec = Confidential<{ value: W; next?: Sec<${argument}>; }, readonly ["a"]>; interface Holder { f: Sec> } `); expect(diagnostics).toHaveLength(1); expect(diagnostics[0]).toMatchObject({ severity: "error", type: "cfc-schema:recursion-limit", }); }); } it("shares a recursive definition when two queries name the same writer", async () => { const { schema, diagnostics } = await generate(` type Sec = Confidential<{ value: W; next?: Sec>; }, readonly ["a"]>; interface Holder { a: Sec>; b: Sec>; } `); expect(diagnostics).toEqual([]); expect(Object.keys(schema.$defs ?? {})).toHaveLength(1); expect(schema.properties!.a).toEqual(schema.properties!.b); }); for (const operator of ["|", "&"]) { for (const useDefault of [false, true]) { it(`emits recursive references when an alias repeats a writer with \`${operator}\`${useDefault ? " through a default argument" : ""}`, async () => { const { schema, diagnostics } = await generate(` type Repeat = W ${operator} typeof f; type WithDefault = Repeat; type Sec = Confidential<{ value: WriteAuthorizedBy; next?: Sec<${useDefault ? "WithDefault" : "Repeat"}>; }, readonly ["a"]>; interface Holder { f: Sec } `); expect(diagnostics).toEqual([]); expect(Object.keys(schema.$defs ?? {}).length).toBeGreaterThan(0); for (const definition of Object.values(schema.$defs!)) { const properties = asObjectSchema(definition).properties!; expect(asObjectSchema(properties.value!).ifc?.writeAuthorizedBy) .toEqual({ __ctWriterIdentityOf: { file: "test.ts", path: ["f"] }, }); expect(asObjectSchema(properties.next!).$ref).toMatch(/^#\/\$defs\//); } }); } } it("keeps writer positions distinct when two recursive payloads use the same bindings in opposite order", async () => { const { schema, diagnostics } = await generate(` type Pair = Confidential<{ left: WriteAuthorizedBy; right: WriteAuthorizedBy; }, readonly ["pair"]>; type Sec = Confidential<{ value: W; next?: Sec>; }, readonly ["a"]>; type Both = Confidential<{ forward: Sec>; reverse: Sec>; }, readonly ["b"]>; interface Holder { pair: Both } `); const pair = asObjectSchema(schema.properties!.pair!); for ( const [direction, writers] of [ ["forward", ["f", "g"]], ["reverse", ["g", "f"]], ] as const ) { for ( const value of recursiveValues(schema, pair.properties![direction]!) ) { for (const [index, field] of ["left", "right"].entries()) { expect( asObjectSchema(value.properties![field]!).ifc?.writeAuthorizedBy, ) .toEqual({ __ctWriterIdentityOf: { file: "test.ts", path: [writers[index]] }, }); } } } expect(diagnostics).toEqual([]); }); });