Files
KLINGO-CLINICAL-SYSLIFE-GCHAT/gti-scheduling-mcp/test/diagnose-client.test.ts
T
GustavoandClaude Sonnet 5 257afe98c0 Snapshot: gti-scheduling-mcp (Z-PRO + Klingo adapters) e workflow n8n de agendamento funcionando de ponta a ponta
- gti-scheduling-mcp: MCP server com ZProAdapter e KlingoAdapter (agendar() completo pros dois)
- Workflow n8n 'Agendamento - GChat - Z-PRO': webhook -> AI Agent -> MCP -> Z-PRO, testado com sucesso real
- Documentacao: specs, plano, runbook de problemas reais e export do workflow

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-10 17:23:10 -03:00

185 lines
5.4 KiB
TypeScript

/**
* 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<string, string> = {};
const fakeFetch = async (
url: RequestInfo | URL,
init?: RequestInit,
): Promise<Response> => {
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<string, string> = {};
const fakeFetch = async (
_url: RequestInfo | URL,
init?: RequestInit,
): Promise<Response> => {
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<Response> => {
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<Response> => {
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("<html><body>erro</body></html>");
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 = `<!DOCTYPE html>${"x".repeat(1000)}`;
const report = buildResponseReport(longHtml, []);
assert.equal(report.isJson, false);
assert.equal(report.structure, undefined);
assert.ok(report.sanitizedText.length <= 500);
});
});