feat: validate HTTP envelopes and payload schemas
This commit is contained in:
@@ -68,4 +68,35 @@ describe("shared HTTP client", () => {
|
||||
});
|
||||
expect(JSON.stringify(result)).not.toContain("raw body");
|
||||
});
|
||||
|
||||
it("classifies malformed JSON and invalid payloads at the boundary", async () => {
|
||||
server.use(
|
||||
http.get(
|
||||
"https://api.test/api/sample/resources",
|
||||
() =>
|
||||
new HttpResponse("{", {
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}),
|
||||
),
|
||||
);
|
||||
const client = createHttpClient({ baseUrl: "https://api.test", clock });
|
||||
await expect(client.execute("LIST_SAMPLE_RESOURCES")).resolves.toMatchObject({
|
||||
ok: false,
|
||||
error: { kind: "MALFORMED_JSON" },
|
||||
});
|
||||
|
||||
server.use(
|
||||
http.get("https://api.test/api/sample/resources", () =>
|
||||
HttpResponse.json({
|
||||
success: true,
|
||||
data: [{ id: "resource-1", name: 42 }],
|
||||
meta: { requestId: "request-1", traceId: "trace-1" },
|
||||
}),
|
||||
),
|
||||
);
|
||||
await expect(client.execute("LIST_SAMPLE_RESOURCES")).resolves.toMatchObject({
|
||||
ok: false,
|
||||
error: { kind: "SCHEMA_MISMATCH" },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
validateEnvelope,
|
||||
validateOperationPayload,
|
||||
validateOperationRequest,
|
||||
} from "../../src/adapters/http/schema-registry.js";
|
||||
|
||||
describe("HTTP runtime schema boundary", () => {
|
||||
it("rejects an invalid top-level envelope", () => {
|
||||
expect(validateEnvelope({ success: true }).success).toBe(false);
|
||||
});
|
||||
|
||||
it("rejects an invalid operation payload with safe issue metadata", () => {
|
||||
const result = validateOperationPayload("SampleResourceListPayload", [
|
||||
{ id: "resource-1", name: 42 },
|
||||
]);
|
||||
|
||||
expect(result).toMatchObject({
|
||||
success: false,
|
||||
issues: [{ path: "0.name" }],
|
||||
});
|
||||
expect(JSON.stringify(result)).not.toContain("resource-1");
|
||||
});
|
||||
|
||||
it("returns a deep-cloned additive-tolerant payload", () => {
|
||||
const source = [{ id: "resource-1", name: "Example", additive: "accepted" }];
|
||||
const result = validateOperationPayload("SampleResourceListPayload", source);
|
||||
|
||||
expect(result).toMatchObject({
|
||||
success: true,
|
||||
data: [{ id: "resource-1", additive: "accepted" }],
|
||||
});
|
||||
expect(result.data).not.toBe(source);
|
||||
});
|
||||
|
||||
it("validates outbound commands before transport", () => {
|
||||
expect(
|
||||
validateOperationRequest("CreateSampleResourceCommand", { name: "" }).success,
|
||||
).toBe(false);
|
||||
expect(
|
||||
validateOperationRequest("CreateSampleResourceCommand", { name: "Example" })
|
||||
.success,
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("fails closed for an unregistered schema", () => {
|
||||
expect(validateOperationPayload("UnknownPayload", {})).toMatchObject({
|
||||
success: false,
|
||||
issues: [{ code: "SCHEMA_NOT_REGISTERED" }],
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user