/** * test/diagnose-client.test.ts * * Testes unitários do cliente HTTP de diagnóstico e do tratamento de * respostas. Nenhum teste chama a API real — todos usam um fetch fake. */ import { describe, it } from "node:test"; import assert from "node:assert/strict"; import { RequestTimeoutError, NetworkRequestError, buildAppointmentListUrl, fetchAppointmentList, } from "../src/diagnostics/client.js"; import { tryParseJson, buildResponseReport, } from "../src/diagnostics/response.js"; import type { ZProConfig } from "../src/config/config.types.js"; const config: ZProConfig = { provider: "zpro", baseUrl: "https://zpro.example.com", apiToken: "token-super-secreto", apiId: "api-1", }; describe("buildAppointmentListUrl", () => { it("monta a URL com ApiID e parâmetros conservadores", () => { const url = buildAppointmentListUrl({ baseUrl: "https://zpro.example.com/", apiId: "api-1", }); assert.equal( url, "https://zpro.example.com/v2/api/external/api-1/appointment/list?page=1&limit=5", ); }); it("não inclui o token na URL", () => { const url = buildAppointmentListUrl(config); assert.ok(!url.includes(config.apiToken)); }); }); describe("fetchAppointmentList", () => { it("envia GET com Bearer token e Accept, e retorna os dados da resposta", async () => { let capturedUrl = ""; let capturedHeaders: Record = {}; const fakeFetch = async ( url: RequestInfo | URL, init?: RequestInit, ): Promise => { capturedUrl = String(url); capturedHeaders = Object.fromEntries( new Headers(init?.headers).entries(), ); return new Response(JSON.stringify({ ok: true }), { status: 200, headers: { "content-type": "application/json" }, }); }; const response = await fetchAppointmentList(config, { fetchImpl: fakeFetch, }); assert.equal(capturedUrl, buildAppointmentListUrl(config)); assert.equal(capturedHeaders.authorization, "Bearer token-super-secreto"); assert.equal(capturedHeaders.accept, "application/json"); assert.equal(response.status, 200); assert.equal(response.contentType, "application/json"); assert.deepEqual(JSON.parse(response.bodyText), { ok: true }); assert.equal(typeof response.durationMs, "number"); }); it("não envia o token em outro header", async () => { let capturedHeaders: Record = {}; const fakeFetch = async ( _url: RequestInfo | URL, init?: RequestInit, ): Promise => { capturedHeaders = Object.fromEntries( new Headers(init?.headers).entries(), ); return new Response("", { status: 200 }); }; await fetchAppointmentList(config, { fetchImpl: fakeFetch }); assert.deepEqual(capturedHeaders, { authorization: `Bearer ${config.apiToken}`, accept: "application/json", }); }); it("lança RequestTimeoutError quando a requisição excede o timeout", async () => { const fakeFetch = async ( _url: RequestInfo | URL, init?: RequestInit, ): Promise => { return new Promise((_resolve, reject) => { init?.signal?.addEventListener("abort", () => { reject(new DOMException("The operation was aborted", "AbortError")); }); }); }; await assert.rejects( fetchAppointmentList(config, { timeoutMs: 50, fetchImpl: fakeFetch, }), (error: unknown) => error instanceof RequestTimeoutError, ); }); it("lança NetworkRequestError em erro de rede, sem expor o token", async () => { const fakeFetch = async (): Promise => { throw new TypeError("fetch failed"); }; await assert.rejects( fetchAppointmentList(config, { fetchImpl: fakeFetch }), (error: unknown) => { assert.ok(error instanceof NetworkRequestError); assert.ok(!error.message.includes(config.apiToken)); return true; }, ); }); }); describe("tratamento de resposta", () => { it("identifica uma resposta JSON", () => { const parsed = tryParseJson('{"data": [1, 2]}'); assert.deepEqual(parsed, { data: [1, 2] }); }); it("identifica uma resposta não JSON", () => { const parsed = tryParseJson("erro"); assert.equal(parsed, null); }); it("monta relatório para resposta JSON com conteúdo sanitizado e estrutura", () => { const report = buildResponseReport( JSON.stringify({ data: [ { id: "app-1", patient: { name: "Maria dos Santos", phone: "75999999999" }, status: "confirmed", }, ], page: 1, }), ["token-super-secreto"], ); assert.equal(report.isJson, true); assert.ok(report.structure); assert.equal(report.structure.rootType, "object"); assert.equal(report.structure.arrayItemCount, undefined); assert.ok(!report.sanitizedText.includes("Maria dos Santos")); assert.ok(!report.sanitizedText.includes("75999999999")); }); it("monta relatório para resposta não JSON limitado a 500 caracteres", () => { const longHtml = `${"x".repeat(1000)}`; const report = buildResponseReport(longHtml, []); assert.equal(report.isJson, false); assert.equal(report.structure, undefined); assert.ok(report.sanitizedText.length <= 500); }); });