import { describe, expect, it } from "vitest"; import { compareApiDescriptions, compareApis } from "../../src/core/compare"; import { validateHarAgainstOpenApi } from "../../src/core/contract"; import { inspectHar, inspectRawExchange } from "../../src/core/har"; import { collectOperations, operationExample, securityInventory, } from "../../src/core/operations"; import { parseApiDocument } from "../../src/core/parse"; import { createWorkspace } from "../../src/core/refs"; const SPEC = `openapi: 3.1.0 info: { title: T, version: '1' } servers: [{ url: "https://api.example.test" }] paths: /items/{id}: get: operationId: readItem parameters: - { name: id, in: path, required: true, schema: { type: string, example: "a'b" } } responses: "200": { description: ok, content: { application/json: { schema: { type: object, properties: { id: {type: string} } } } } } components: securitySchemes: { key: { type: apiKey, in: header, name: X-Key } } `; describe("operation derivation, comparisons and saved exchanges", () => { it("collects operations, schemes and safely quoted inert commands", () => { const document = parseApiDocument(SPEC, "openapi.yaml"); const operations = collectOperations(document.value); expect(operations.map((item) => item.key)).toEqual(["GET /items/{id}"]); const example = operationExample(createWorkspace(document), operations[0]!); expect(example.url).toContain("a'b"); expect(example.commands.curl).toContain("curl --request GET"); expect(example.commands.fetch).toContain("fetch("); expect(securityInventory(document.value)).toEqual([ { name: "key", type: "apiKey", detail: "header ยท X-Key" }, ]); }); it("inventories and compares OpenAPI 3.1 webhook receiver operations", () => { const document = parseApiDocument(`openapi: 3.1.0 info: { title: Hooks, version: '1' } paths: {} webhooks: orderChanged: post: operationId: receiveOrderChange requestBody: content: application/json: schema: { type: object, properties: { id: { type: string } } } responses: { '204': { description: accepted } } `); const operations = collectOperations(document.value); expect(operations).toEqual([ expect.objectContaining({ key: "WEBHOOK orderChanged POST", kind: "webhook", method: "POST", path: "orderChanged", }), ]); const example = operationExample(createWorkspace(document), operations[0]!); expect(example.url).toBe( "https://receiver.example.test/webhooks/orderChanged", ); expect(example.notices.join(" ")).toMatch(/placeholder/iu); const removed = parseApiDocument( "openapi: 3.1.0\ninfo: {title: Hooks, version: '2'}\npaths: {}\n", ); expect(compareApis(document, removed)).toContainEqual( expect.objectContaining({ level: "breaking", path: "WEBHOOK orderChanged POST", }), ); }); it("reports a missing local reference without aborting generation", () => { const document = parseApiDocument( "openapi: 3.1.0\ninfo: {title: Missing, version: '1'}\npaths: {/x: {get: {responses: {'200': {description: ok, content: {application/json: {schema: {$ref: 'missing.yaml#/Result'}}}}}}}}", "openapi.yaml", ); const operation = collectOperations(document.value)[0]!; const example = operationExample(createWorkspace(document), operation); expect(example.response).toBeUndefined(); expect(example.notices.join(" ")).toMatch(/missing\.yaml/iu); }); it("classifies removed operations and additions", () => { const before = parseApiDocument(SPEC, "before.yaml"); const after = parseApiDocument( SPEC.replace(" get:", " post:"), "after.yaml", ); expect(compareApis(before, after)).toEqual( expect.arrayContaining([ expect.objectContaining({ level: "breaking", path: "GET /items/{id}" }), expect.objectContaining({ level: "non-breaking", path: "POST /items/{id}", }), ]), ); }); it("reports directional request and response schema regressions", () => { const before = parseApiDocument(`openapi: 3.1.0 info: { title: Contract, version: '1' } paths: /items: post: requestBody: content: application/json: schema: { type: object, properties: { kind: { type: string, enum: [a, b] } } } responses: '200': description: ok content: application/json: schema: { type: object, required: [id], properties: { id: { type: string } } } `); const after = parseApiDocument(`openapi: 3.1.0 info: { title: Contract, version: '2' } paths: /items: post: requestBody: content: application/json: schema: { type: object, required: [kind], properties: { kind: { type: string, enum: [a] } } } responses: '200': description: ok content: application/json: schema: { type: object, properties: {} } `); const changes = compareApis(before, after); expect(changes).toEqual( expect.arrayContaining([ expect.objectContaining({ level: "breaking", message: expect.stringContaining("Request enum removed"), }), expect.objectContaining({ level: "breaking", message: expect.stringContaining("became required"), }), expect.objectContaining({ level: "breaking", message: expect.stringContaining("Required response property"), }), ]), ); }); it("collects OpenAPI 3.2 QUERY and custom methods", () => { const document = parseApiDocument( `openapi: 3.2.0 info: { title: Extended, version: '1' } paths: /items: query: { responses: { '200': { description: ok } } } additionalOperations: COPY: { responses: { '204': { description: copied } } } `, ); expect(collectOperations(document.value).map((item) => item.key)).toEqual([ "QUERY /items", "COPY /items", ]); }); it("compares AsyncAPI operations without treating them as HTTP paths", () => { const before = parseApiDocument( "asyncapi: 3.1.0\ninfo: {title: E, version: '1'}\nchannels: { c: {address: events, messages: {}} }\noperations: { send: {action: send, channel: {$ref: '#/channels/c'}} }", ); const after = parseApiDocument( "asyncapi: 3.1.0\ninfo: {title: E, version: '2'}\nchannels: { c: {address: events, messages: {}} }\noperations: {}", ); expect(compareApiDescriptions(before, after)).toContainEqual( expect.objectContaining({ level: "breaking", message: "Operation was removed.", }), ); }); it("summarizes HAR and raw exchanges without exposing headers", () => { const har = inspectHar( '{"log":{"entries":[{"time":7,"request":{"method":"POST","url":"https://example.test/x","headers":[{"name":"Authorization","value":"secret"}],"postData":{"text":"abc"}},"response":{"status":201,"headers":[],"content":{"size":9,"mimeType":"application/json"}}}]}}', ); expect(har[0]).toMatchObject({ method: "POST", status: 201, requestHeaders: 1, requestBytes: 3, responseBytes: 9, }); expect(JSON.stringify(har)).not.toContain("secret"); expect( inspectRawExchange( "GET /x HTTP/1.1\r\nHost: example.test\r\n\r\n--- response ---\r\nHTTP/1.1 204 No Content\r\nX-Test: yes\r\n", )[0], ).toMatchObject({ method: "GET", url: "/x", status: 204 }); }); it("matches HAR exchanges to operations and validates captured response JSON", () => { const document = parseApiDocument(SPEC, "openapi.yaml"); const report = validateHarAgainstOpenApi( JSON.stringify({ log: { entries: [ { request: { method: "GET", url: "https://api.example.test/items/item-1", headers: [ { name: "Authorization", value: "Bearer do-not-report" }, ], }, response: { status: 200, headers: [], content: { mimeType: "application/json", text: JSON.stringify({ id: 42 }), }, }, }, { request: { method: "POST", url: "https://api.example.test/not-declared", headers: [], }, response: { status: 404, headers: [], content: {} }, }, ], }, }), createWorkspace(document), ); expect(report.coveredOperations).toBe(1); expect(report.unmatchedExchanges).toBe(1); expect(report.exchanges[0]?.operation).toBe("GET /items/{id}"); expect(report.diagnostics.map((item) => item.message).join("\n")).toMatch( /expected string|does not match a declared operation/iu, ); expect(JSON.stringify(report)).not.toContain("do-not-report"); }); });