Files
clean-architecture-frontend…/tests/integration/http-diagnostics.test.ts
T

194 lines
5.3 KiB
TypeScript

import { describe, expect, it, vi } from "vitest";
import { createHttpClient } from "../../src/adapters/http/client.ts";
import type { DiagnosticRecordInput } from "../../src/contracts/diagnostics.ts";
import { TEST_HTTP_CONTRACT } from "../helpers/http-contract-fixture.ts";
function successResponse(data: unknown) {
return Response.json({
success: true,
data,
meta: { requestId: "request-1", traceId: "trace-1" },
});
}
function failureResponse(status: number) {
return Response.json(
{
success: false,
error: { code: "TEMPORARY" },
meta: { requestId: "request-1", traceId: "trace-1" },
},
{ status },
);
}
type EmittedEvent = Readonly<{
eventName: string;
attributes: Readonly<Record<string, unknown>>;
}>;
function harness(fetcher: typeof fetch, maxRetryAttempts = 1) {
const records: DiagnosticRecordInput[] = [];
const emitted: EmittedEvent[] = [];
let currentTime = 0;
const client = createHttpClient({
...TEST_HTTP_CONTRACT,
baseUrl: "https://api.test",
fetcher,
maxRetryAttempts,
clock: {
now: () => currentTime,
sleep: async () => {
currentTime += 150;
},
},
scheduler: {
setTimeout: () => 1,
clearTimeout: () => {},
},
correlationIdFactory: () => "correlation-fixed",
diagnostics: {
record(input) {
records.push(structuredClone(input));
},
},
telemetry: {
emit(eventName, attributes) {
emitted.push({
eventName,
attributes: structuredClone(attributes),
});
},
},
});
return { client, records, emitted };
}
describe("HTTP diagnostics and terminal telemetry", () => {
it("records one successful reference route outcome and no failure event", async () => {
const { client, records, emitted } = harness(
vi.fn(async () => successResponse([])),
);
await expect(
client.execute({
operationId: "LIST_ENTITIES",
routeId: "TEST_ROUTE",
}),
).resolves.toMatchObject({ ok: true });
expect(records).toEqual([
{
level: "info",
eventId: "http.request.completed",
context: {
route_id: "TEST_ROUTE",
operation_id: "LIST_ENTITIES",
correlation_id: "correlation-fixed",
outcome: "success",
error_kind: "NONE",
http_status_group: "2xx",
attempt_count_bucket: "1",
duration_bucket: "lt100ms",
},
},
]);
expect(emitted).toEqual([]);
});
it("summarizes retry recovery once without a terminal failure event", async () => {
const fetcher = vi
.fn<typeof fetch>()
.mockResolvedValueOnce(failureResponse(503))
.mockResolvedValueOnce(successResponse([]));
const { client, records, emitted } = harness(fetcher);
await expect(
client.execute({
operationId: "LIST_ENTITIES",
routeId: "TEST_ROUTE",
}),
).resolves.toMatchObject({ ok: true });
expect(fetcher).toHaveBeenCalledTimes(2);
expect(records).toHaveLength(1);
expect(records[0]).toMatchObject({
eventId: "http.request.completed",
context: {
outcome: "recovered",
correlation_id: "correlation-fixed",
duration_bucket: "100-499ms",
},
});
expect(emitted).toEqual([]);
});
it("emits one bounded terminal failure after all attempts", async () => {
const fetcher = vi.fn(async () => failureResponse(503));
const { client, records, emitted } = harness(fetcher);
await expect(
client.execute({
operationId: "CREATE_ENTITY",
routeId: "TEST_ROUTE",
body: { name: "private user input" },
idempotencyKey: "private-idempotency-key",
}),
).resolves.toMatchObject({
ok: false,
error: { kind: "SERVER_FAILURE" },
});
expect(fetcher).toHaveBeenCalledTimes(2);
expect(records).toHaveLength(1);
expect(emitted).toEqual([
{
eventName: "api.request.failed",
attributes: {
error_kind: "SERVER_FAILURE",
http_status_group: "5xx",
attempt_count_bucket: "2",
route_id: "TEST_ROUTE",
operation_id: "CREATE_ENTITY",
duration_bucket: "100-499ms",
},
},
]);
expect(JSON.stringify({ records, emitted })).not.toMatch(
/private user input|private-idempotency-key/,
);
});
it("records navigation abort without failure telemetry", async () => {
const caller = new AbortController();
caller.abort("navigation");
const fetcher = vi.fn(async (request: RequestInfo | URL) => {
const signal = (request as Request).signal;
if (signal.aborted) throw new DOMException("aborted", "AbortError");
return successResponse([]);
});
const { client, records, emitted } = harness(
fetcher as unknown as typeof fetch,
);
await expect(
client.execute({
operationId: "LIST_ENTITIES",
routeId: "TEST_ROUTE",
signal: caller.signal,
}),
).resolves.toMatchObject({
ok: false,
error: { kind: "REQUEST_ABORTED" },
});
expect(records).toHaveLength(1);
expect(records[0]).toMatchObject({
eventId: "http.request.completed",
context: { outcome: "aborted", error_kind: "REQUEST_ABORTED" },
});
expect(emitted).toEqual([]);
});
});