@@ -1,5 +1,6 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { compareApis } from "../../src/core/compare";
|
||||
import { compareApiDescriptions, compareApis } from "../../src/core/compare";
|
||||
import { validateHarAgainstOpenApi } from "../../src/core/contract";
|
||||
import { inspectHar, inspectRawExchange } from "../../src/core/har";
|
||||
import {
|
||||
collectOperations,
|
||||
@@ -38,6 +39,46 @@ describe("operation derivation, comparisons and saved exchanges", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
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'}}}}}}}}",
|
||||
@@ -66,6 +107,90 @@ describe("operation derivation, comparisons and saved exchanges", () => {
|
||||
);
|
||||
});
|
||||
|
||||
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"}}}]}}',
|
||||
@@ -84,4 +209,49 @@ describe("operation derivation, comparisons and saved exchanges", () => {
|
||||
)[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");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user