import type { BodyRule, CheckFailure, HeaderExpect, HttpExpectConfig } from "../types"; import { checkBodyExpect } from "./body"; import { applyOperator } from "./body"; import { mismatchFailure, errorFailure } from "./failure"; export interface HttpObservation { statusCode: number; headers: Record; body: string | null; durationMs: number; } function checkStatus(obs: HttpObservation, allowed: number[]): { matched: boolean; failure: CheckFailure | null } { if (!allowed.includes(obs.statusCode)) { return { matched: false, failure: mismatchFailure( "status", "status", allowed, obs.statusCode, `status ${obs.statusCode} not in [${allowed}]`, ), }; } return { matched: true, failure: null }; } function checkDuration( obs: HttpObservation, maxDurationMs?: number, ): { matched: boolean; failure: CheckFailure | null } { if (maxDurationMs === undefined) return { matched: true, failure: null }; if (obs.durationMs > maxDurationMs) { return { matched: false, failure: mismatchFailure( "duration", "duration", `<=${maxDurationMs}ms`, obs.durationMs, `duration ${obs.durationMs}ms > ${maxDurationMs}ms`, ), }; } return { matched: true, failure: null }; } function checkHeaders( obs: HttpObservation, headerExpects?: Record, ): { matched: boolean; failure: CheckFailure | null } { if (!headerExpects) return { matched: true, failure: null }; for (const [key, expected] of Object.entries(headerExpects)) { const actualValue = obs.headers[key.toLowerCase()]; const path = `headers.${key}`; if (typeof expected === "string") { if (actualValue !== expected) { return { matched: false, failure: mismatchFailure("headers", path, expected, actualValue, `header ${key} mismatch`), }; } } else { if (actualValue === undefined) { if (expected.exists !== false) { return { matched: false, failure: mismatchFailure("headers", path, "defined", undefined, `header ${key} not found`), }; } continue; } if (!applyOperator(actualValue, expected)) { return { matched: false, failure: mismatchFailure("headers", path, expected, actualValue, `header ${key} mismatch`), }; } } } return { matched: true, failure: null }; } function checkBody(obs: HttpObservation, bodyRules?: BodyRule[]): { matched: boolean; failure: CheckFailure | null } { if (!bodyRules || bodyRules.length === 0) return { matched: true, failure: null }; if (obs.body === null) { return { matched: false, failure: errorFailure("body", "body", "body is null but body rules are configured"), }; } return checkBodyExpect(obs.body, bodyRules); } export function checkHttpExpect( obs: HttpObservation, expect?: HttpExpectConfig, ): { matched: boolean; failure: CheckFailure | null } { if (!expect) { return checkStatus(obs, [200]); } const statusResult = checkStatus(obs, expect.status ?? [200]); if (!statusResult.matched) return statusResult; const durationResult = checkDuration(obs, expect.maxDurationMs); if (!durationResult.matched) return durationResult; const headersResult = checkHeaders(obs, expect.headers); if (!headersResult.matched) return headersResult; const bodyResult = checkBody(obs, expect.body); if (!bodyResult.matched) return bodyResult; return { matched: true, failure: null }; }