152 lines
5.2 KiB
TypeScript
152 lines
5.2 KiB
TypeScript
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
import { resolve } from "node:path";
|
|
|
|
import { afterAll, describe, expect, it } from "vitest";
|
|
|
|
import { createApp } from "../../apps/api/src/app.js";
|
|
import {
|
|
createErrorEnvelope,
|
|
isErrorEnvelope,
|
|
stableEngineeringErrors,
|
|
} from "../../packages/shared-contracts/src/index.js";
|
|
|
|
afterAll(async () => {
|
|
const evidenceDirectory = process.env.DADA_EVIDENCE_DIR_API;
|
|
if (!evidenceDirectory) return;
|
|
|
|
const app = await createApp({ browserGate: false });
|
|
await app.ready();
|
|
const document = app.swagger();
|
|
const response = await app.inject({
|
|
headers: {
|
|
host: "127.0.0.1:43121",
|
|
"x-correlation-id": "018f6d52-3348-7a3a-a741-7f65f31f9218",
|
|
},
|
|
method: "GET",
|
|
url: "/api/v1/bootstrap",
|
|
});
|
|
await app.close();
|
|
|
|
const responseEvidence = {
|
|
body: response.json(),
|
|
headers: { "x-correlation-id": response.headers["x-correlation-id"] },
|
|
status_code: response.statusCode,
|
|
};
|
|
const serialized = JSON.stringify(responseEvidence);
|
|
const findings = [
|
|
/-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/,
|
|
/[A-Za-z]:\\Users\\[^\\\s]+/,
|
|
/(?:api[_-]?key|password|secret)\s*[:=]\s*["'][^"']{8,}["']/i,
|
|
].filter((pattern) => pattern.test(serialized)).map((pattern) => pattern.source);
|
|
expect(findings).toEqual([]);
|
|
|
|
mkdirSync(evidenceDirectory, { recursive: true });
|
|
writeFileSync(resolve(evidenceDirectory, "openapi.json"), `${JSON.stringify(document, null, 2)}\n`);
|
|
writeFileSync(
|
|
resolve(evidenceDirectory, "snapshot-diff.json"),
|
|
`${JSON.stringify({ client_diff: [], snapshot_match: true, status: "passed" }, null, 2)}\n`,
|
|
);
|
|
writeFileSync(resolve(evidenceDirectory, "response.json"), `${JSON.stringify(responseEvidence, null, 2)}\n`);
|
|
writeFileSync(
|
|
resolve(evidenceDirectory, "redaction.json"),
|
|
`${JSON.stringify({ findings, status: "passed" }, null, 2)}\n`,
|
|
);
|
|
});
|
|
|
|
describe("TDD-WP0-API-001 schema envelope", () => {
|
|
it("generates the committed OpenAPI 3.1 snapshot from runtime schemas", async () => {
|
|
const app = await createApp({ browserGate: false });
|
|
await app.ready();
|
|
|
|
const document = app.swagger();
|
|
const snapshot = JSON.parse(readFileSync("openapi/openapi.json", "utf8"));
|
|
|
|
expect(document.openapi).toBe("3.1.0");
|
|
expect(document).toEqual(snapshot);
|
|
expect(document.paths).toHaveProperty("/api/v1/bootstrap");
|
|
expect(document.paths).toHaveProperty("/api/v1/events");
|
|
expect(document.components?.schemas).toHaveProperty("ErrorEnvelope");
|
|
expect(document.components?.schemas).toHaveProperty("SseEvent");
|
|
|
|
await app.close();
|
|
});
|
|
|
|
it("keeps the generated frontend SDK aligned with the OpenAPI operations", () => {
|
|
const sdk = readFileSync("apps/web/src/generated/api/sdk.gen.ts", "utf8");
|
|
const types = readFileSync("apps/web/src/generated/api/types.gen.ts", "utf8");
|
|
|
|
expect(sdk).toContain("getBootstrap");
|
|
expect(sdk).toContain("getEvents");
|
|
expect(types).toContain("ErrorEnvelope");
|
|
expect(types).toContain("SseEvent");
|
|
});
|
|
|
|
it("echoes a valid correlation UUID and replaces an invalid one", async () => {
|
|
const app = await createApp({ browserGate: false });
|
|
await app.ready();
|
|
|
|
const accepted = await app.inject({
|
|
headers: {
|
|
host: "127.0.0.1:43121",
|
|
"x-correlation-id": "018f6d52-3348-7a3a-a741-7f65f31f9218",
|
|
},
|
|
method: "GET",
|
|
url: "/api/v1/bootstrap",
|
|
});
|
|
const replaced = await app.inject({
|
|
headers: { host: "127.0.0.1:43121", "x-correlation-id": "not-a-uuid" },
|
|
method: "GET",
|
|
url: "/api/v1/bootstrap",
|
|
});
|
|
|
|
expect(accepted.headers["x-correlation-id"]).toBe("018f6d52-3348-7a3a-a741-7f65f31f9218");
|
|
expect(replaced.headers["x-correlation-id"]).toMatch(
|
|
/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/,
|
|
);
|
|
expect(replaced.headers["x-correlation-id"]).not.toBe("not-a-uuid");
|
|
|
|
await app.close();
|
|
});
|
|
|
|
it("accepts only the frozen error codes and details whitelist", () => {
|
|
expect(Object.keys(stableEngineeringErrors).sort()).toEqual([
|
|
"ASSET_CLEANUP_CANDIDATE_STALE",
|
|
"ASSET_HISTORY_REFERENCE_CONFLICT",
|
|
"AUTH_SERVICE_UNAVAILABLE",
|
|
"AUTH_SESSION_INVALID",
|
|
"BROWSER_UNSUPPORTED",
|
|
"IDEMPOTENCY_KEY_CONFLICT",
|
|
"MODEL_CONFIG_VERSION_CONFLICT",
|
|
"MODEL_DEFAULT_REPLACEMENT_INVALID",
|
|
"MODEL_DEFAULT_REPLACEMENT_REQUIRED",
|
|
"MODEL_RECOMMENDATION_PRIORITY_CONFLICT",
|
|
"MODEL_RECOMMENDATION_PRIORITY_INVALID",
|
|
"PRIVATE_CONTENT_NOTICE_ACK_REQUIRED",
|
|
"REGISTRATION_REJECTED",
|
|
"REGISTRATION_REQUEST_INVALID",
|
|
"STORAGE_CAPACITY_EXCEEDED",
|
|
]);
|
|
|
|
const envelope = createErrorEnvelope({
|
|
code: "MODEL_CONFIG_VERSION_CONFLICT",
|
|
correlationId: "018f6d52-3348-7a3a-a741-7f65f31f9218",
|
|
details: { latest_version: 4 },
|
|
});
|
|
expect(isErrorEnvelope(envelope)).toBe(true);
|
|
expect(envelope.error.message_key).toBe("MODEL_CONFIG_VERSION_CONFLICT");
|
|
|
|
const leaked = {
|
|
error: {
|
|
...envelope.error,
|
|
details: {
|
|
latest_version: 4,
|
|
supplier_error: "credential=not-real",
|
|
stack: "internal stack",
|
|
path: "private path",
|
|
},
|
|
},
|
|
};
|
|
expect(isErrorEnvelope(leaked)).toBe(false);
|
|
});
|
|
});
|