mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 12:32:43 +00:00
test(settings): read a real failure response through the test button
The fetch and envelope reading move into runConnectionTest, which the button calls. A test stubs the fetch with the 502 envelopes the notification test routes send and asserts the rendered code, HTTP status, SMTP code and relay reply, so dropping the meta extraction fails it. Refs #947
This commit is contained in:
1 parent
6a663dfc27
commit
a11e5991cf
2 files changed
+166
-38
No files matched your search
@@ -0,0 +1,122 @@
|
||||
/**
|
||||
* How the test-connection button reads a real failure response (#947).
|
||||
*
|
||||
* The component suite is SSR-only, so the button's probe is exercised as the
|
||||
* function the button calls: `apiFetchRaw` is stubbed with the envelope a
|
||||
* notification test route sends, and the result is rendered through the same
|
||||
* callout the button renders. Removing the meta extraction turns this red.
|
||||
*/
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const apiFetchRawMock = vi.fn();
|
||||
vi.mock("@/lib/api/api-fetch", () => ({
|
||||
apiFetchRaw: (...args: unknown[]) => apiFetchRawMock(...args),
|
||||
}));
|
||||
|
||||
import {
|
||||
runConnectionTest,
|
||||
TestConnectionFailure,
|
||||
} from "../test-connection-button";
|
||||
|
||||
function envelope(status: number, body: unknown): Response {
|
||||
return new Response(JSON.stringify(body), {
|
||||
status,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
|
||||
function renderFailure(
|
||||
result: Awaited<ReturnType<typeof runConnectionTest>>,
|
||||
): string {
|
||||
if (result.kind !== "error") throw new Error("expected a failure");
|
||||
return renderToStaticMarkup(
|
||||
<TestConnectionFailure
|
||||
message={`code:${result.errorCode}`}
|
||||
upstreamStatus={result.upstreamStatus}
|
||||
smtpCode={result.smtpCode}
|
||||
upstreamBody={result.upstreamBody}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
apiFetchRawMock.mockReset();
|
||||
});
|
||||
|
||||
describe("runConnectionTest", () => {
|
||||
it("reads the code, the HTTP status and what the relay said from a 502", async () => {
|
||||
apiFetchRawMock.mockResolvedValue(
|
||||
envelope(502, {
|
||||
data: null,
|
||||
error: "The webhook answered HTTP 400.",
|
||||
meta: {
|
||||
errorCode: "upstream_rejected",
|
||||
upstreamStatus: 400,
|
||||
upstreamBody: "priority must be an integer",
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const result = await runConnectionTest("/api/settings/webhook/test");
|
||||
|
||||
expect(apiFetchRawMock).toHaveBeenCalledWith("/api/settings/webhook/test", {
|
||||
method: "POST",
|
||||
});
|
||||
const html = renderFailure(result);
|
||||
expect(html).toContain("code:upstream_rejected (HTTP 400)");
|
||||
expect(html).toContain("priority must be an integer");
|
||||
});
|
||||
|
||||
it("reads the SMTP reply code from an email failure", async () => {
|
||||
apiFetchRawMock.mockResolvedValue(
|
||||
envelope(502, {
|
||||
data: null,
|
||||
error: "The mail server rejected the credentials (SMTP 535).",
|
||||
meta: { errorCode: "credentials_rejected", smtpCode: 535 },
|
||||
}),
|
||||
);
|
||||
|
||||
const html = renderFailure(
|
||||
await runConnectionTest("/api/settings/email/test"),
|
||||
);
|
||||
|
||||
expect(html).toContain("code:credentials_rejected (SMTP 535)");
|
||||
expect(html).not.toContain("test-connection-upstream-body");
|
||||
});
|
||||
|
||||
it("falls back to the generic code when a proxy replaced the body", async () => {
|
||||
apiFetchRawMock.mockResolvedValue(
|
||||
new Response("<html>Bad Gateway</html>", { status: 502 }),
|
||||
);
|
||||
|
||||
expect(await runConnectionTest("/api/settings/ntfy/test")).toEqual({
|
||||
kind: "error",
|
||||
errorCode: "generic",
|
||||
upstreamStatus: undefined,
|
||||
smtpCode: undefined,
|
||||
upstreamBody: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it("reports success with the latency", async () => {
|
||||
apiFetchRawMock.mockResolvedValue(
|
||||
envelope(200, { data: { ok: true, latencyMs: 42 }, error: null }),
|
||||
);
|
||||
|
||||
expect(await runConnectionTest("/api/nightscout/test")).toEqual({
|
||||
kind: "ok",
|
||||
latency: 42,
|
||||
});
|
||||
});
|
||||
|
||||
it("is what the button calls", () => {
|
||||
const src = readFileSync(
|
||||
join(process.cwd(), "src/components/settings/test-connection-button.tsx"),
|
||||
"utf8",
|
||||
);
|
||||
expect(src).toMatch(/setResult\(await runConnectionTest\(endpoint\)\)/);
|
||||
});
|
||||
});
|
||||
@@ -82,6 +82,48 @@ export function TestConnectionFailure({
|
||||
);
|
||||
}
|
||||
|
||||
export type ConnectionTestResult =
|
||||
| { kind: "ok"; latency: number }
|
||||
| {
|
||||
kind: "error";
|
||||
errorCode: string;
|
||||
upstreamStatus?: number;
|
||||
smtpCode?: number;
|
||||
upstreamBody?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* The button's probe: POST the endpoint and read the envelope into what the
|
||||
* button shows. Exported so the reading of a real failure response can be
|
||||
* tested without a DOM.
|
||||
*/
|
||||
export async function runConnectionTest(
|
||||
endpoint: string,
|
||||
): Promise<ConnectionTestResult> {
|
||||
try {
|
||||
const res = await apiFetchRaw(endpoint, { method: "POST" });
|
||||
const json = (await res.json().catch(() => ({}))) as TestResponse;
|
||||
|
||||
if (res.ok && json.data?.ok !== false) {
|
||||
return { kind: "ok", latency: json.data?.latencyMs ?? 0 };
|
||||
}
|
||||
const meta = json.meta;
|
||||
return {
|
||||
kind: "error",
|
||||
errorCode: meta?.errorCode ?? "generic",
|
||||
upstreamStatus:
|
||||
typeof meta?.upstreamStatus === "number"
|
||||
? meta.upstreamStatus
|
||||
: undefined,
|
||||
smtpCode: typeof meta?.smtpCode === "number" ? meta.smtpCode : undefined,
|
||||
upstreamBody:
|
||||
typeof meta?.upstreamBody === "string" ? meta.upstreamBody : undefined,
|
||||
};
|
||||
} catch {
|
||||
return { kind: "error", errorCode: "connection_failed" };
|
||||
}
|
||||
}
|
||||
|
||||
export function TestConnectionButton({
|
||||
endpoint,
|
||||
disabled = false,
|
||||
@@ -89,49 +131,13 @@ export function TestConnectionButton({
|
||||
}: TestConnectionButtonProps) {
|
||||
const { t } = useTranslations();
|
||||
const [testing, setTesting] = useState(false);
|
||||
const [result, setResult] = useState<
|
||||
| { kind: "ok"; latency: number }
|
||||
| {
|
||||
kind: "error";
|
||||
errorCode: string;
|
||||
upstreamStatus?: number;
|
||||
smtpCode?: number;
|
||||
upstreamBody?: string;
|
||||
}
|
||||
| null
|
||||
>(null);
|
||||
const [result, setResult] = useState<ConnectionTestResult | null>(null);
|
||||
|
||||
async function handleClick() {
|
||||
setTesting(true);
|
||||
setResult(null);
|
||||
try {
|
||||
const res = await apiFetchRaw(endpoint, { method: "POST" });
|
||||
const json = (await res.json().catch(() => ({}))) as TestResponse;
|
||||
|
||||
if (res.ok && json.data?.ok !== false) {
|
||||
setResult({
|
||||
kind: "ok",
|
||||
latency: json.data?.latencyMs ?? 0,
|
||||
});
|
||||
} else {
|
||||
const meta = json.meta;
|
||||
setResult({
|
||||
kind: "error",
|
||||
errorCode: meta?.errorCode ?? "generic",
|
||||
upstreamStatus:
|
||||
typeof meta?.upstreamStatus === "number"
|
||||
? meta.upstreamStatus
|
||||
: undefined,
|
||||
smtpCode:
|
||||
typeof meta?.smtpCode === "number" ? meta.smtpCode : undefined,
|
||||
upstreamBody:
|
||||
typeof meta?.upstreamBody === "string"
|
||||
? meta.upstreamBody
|
||||
: undefined,
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
setResult({ kind: "error", errorCode: "connection_failed" });
|
||||
setResult(await runConnectionTest(endpoint));
|
||||
} finally {
|
||||
setTesting(false);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user