import { describe, test, expect } from 'bun:test'; import { readFileSync } from 'fs'; import { join, dirname } from 'path'; import { fileURLToPath } from 'url'; const here = dirname(fileURLToPath(import.meta.url)); const specPath = join(here, '..', 'openapi.yaml'); const spec = Bun.YAML.parse(readFileSync(specPath, 'utf-8')) as Record; describe('openapi.yaml lint', () => { test('declares OpenAPI 3.1', () => { expect(typeof spec.openapi).toBe('string'); expect(String(spec.openapi).startsWith('3.1')).toBe(true); }); test('has the required top-level fields', () => { expect(spec).toHaveProperty('info'); expect(spec).toHaveProperty('paths'); expect(spec).toHaveProperty('components'); const info = spec.info as Record; expect(typeof info.title).toBe('string'); expect(typeof info.version).toBe('string'); }); test('every operation has summary + responses', () => { const paths = (spec.paths ?? {}) as Record>; const httpMethods = new Set([ 'get', 'post', 'put', 'patch', 'delete', 'options', 'head', ]); for (const [routePath, route] of Object.entries(paths)) { for (const [verb, op] of Object.entries(route)) { if (!httpMethods.has(verb)) continue; const operation = op as Record; expect(operation.summary, `${verb.toUpperCase()} ${routePath} missing summary`).toBeDefined(); const responses = operation.responses as Record | undefined; expect(responses, `${verb.toUpperCase()} ${routePath} missing responses`).toBeDefined(); expect( Object.keys(responses ?? {}).length, `${verb.toUpperCase()} ${routePath} has empty responses`, ).toBeGreaterThan(0); } } }); test('every $ref resolves to a defined component', () => { const refs = collectRefs(spec); expect(refs.length).toBeGreaterThan(0); for (const ref of refs) { expect( ref.startsWith('#/'), `non-internal $ref not allowed: ${ref}`, ).toBe(true); const segments = ref.slice(2).split('/'); let cursor: unknown = spec; for (const segment of segments) { expect( isObject(cursor), `dangling $ref: ${ref} (failed at "${segment}")`, ).toBe(true); cursor = (cursor as Record)[segment]; } expect(cursor, `unresolved $ref: ${ref}`).toBeDefined(); } }); test('every security requirement references a defined scheme', () => { const schemes = ( ((spec.components ?? {}) as Record>) .securitySchemes ?? {} ) as Record; const definedSchemes = new Set(Object.keys(schemes)); const requirements = collectSecurityRequirements(spec); for (const name of requirements) { expect( definedSchemes.has(name), `security requirement "${name}" not defined under components.securitySchemes`, ).toBe(true); } }); test('declares the V3.1 transfer surface', () => { const paths = (spec.paths ?? {}) as Record; expect(paths['/v1/transfer/health']).toBeDefined(); expect(paths['/v1/transfer/{streamId}/chunk']).toBeDefined(); expect(paths['/v1/transfer/{streamId}/state']).toBeDefined(); expect(paths['/v1/transfer/control']).toBeDefined(); const schemes = ( ((spec.components ?? {}) as Record>) .securitySchemes ?? {} ) as Record; expect(schemes.ShadeTransferAuthenticator).toBeDefined(); }); }); function isObject(value: unknown): value is Record { return typeof value === 'object' && value !== null && !Array.isArray(value); } function collectRefs(node: unknown, out: string[] = []): string[] { if (Array.isArray(node)) { for (const item of node) collectRefs(item, out); return out; } if (!isObject(node)) return out; for (const [key, value] of Object.entries(node)) { if (key === '$ref' && typeof value === 'string') { out.push(value); continue; } collectRefs(value, out); } return out; } function collectSecurityRequirements(spec: Record): Set { const names = new Set(); const collectFromList = (list: unknown): void => { if (!Array.isArray(list)) return; for (const entry of list) { if (!isObject(entry)) continue; for (const name of Object.keys(entry)) names.add(name); } }; collectFromList(spec.security); const paths = (spec.paths ?? {}) as Record>; for (const route of Object.values(paths)) { for (const value of Object.values(route)) { if (!isObject(value)) continue; collectFromList(value.security); } } return names; }