mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 05:12:14 +00:00
feat(settings): show the status and reply of a failed channel test
The test-connection callout now appends the HTTP status or SMTP reply code the route reports, and shows what the relay answered on a line beneath it. The reply is rendered as a React text child, so markup in it is shown as characters. Adds the upstream_rejected message in all seven locales. Refs #947
This commit is contained in:
1 parent
f6a890df8a
commit
92606f6409
9 files changed
+147
-11
No files matched your search
@@ -6136,6 +6136,7 @@
|
||||
"rate_limited": "Vom Upstream rate-limitiert",
|
||||
"timeout": "Request hat das Zeitlimit überschritten",
|
||||
"upstream_error": "Upstream hat einen Fehler gemeldet",
|
||||
"upstream_rejected": "Die Gegenstelle hat die Nachricht abgelehnt",
|
||||
"connection_failed": "Verbindung fehlgeschlagen",
|
||||
"not_configured": "Nicht konfiguriert",
|
||||
"url_not_public": "URL ist kein öffentlicher Endpunkt",
|
||||
|
||||
@@ -6136,6 +6136,7 @@
|
||||
"rate_limited": "Rate-limited by the upstream",
|
||||
"timeout": "Request timed out",
|
||||
"upstream_error": "Upstream returned an error",
|
||||
"upstream_rejected": "The other end refused the message",
|
||||
"connection_failed": "Connection failed",
|
||||
"not_configured": "Not configured",
|
||||
"url_not_public": "URL is not a public endpoint",
|
||||
|
||||
@@ -6136,6 +6136,7 @@
|
||||
"rate_limited": "Limitado por el servidor remoto",
|
||||
"timeout": "La solicitud agotó el tiempo de espera",
|
||||
"upstream_error": "El servidor remoto devolvió un error",
|
||||
"upstream_rejected": "El servidor remoto rechazó el mensaje",
|
||||
"connection_failed": "Falló la conexión",
|
||||
"not_configured": "No configurado",
|
||||
"url_not_public": "La URL no es un endpoint público",
|
||||
|
||||
@@ -6136,6 +6136,7 @@
|
||||
"rate_limited": "Limité par le service en amont",
|
||||
"timeout": "Délai de la requête dépassé",
|
||||
"upstream_error": "Le service en amont a renvoyé une erreur",
|
||||
"upstream_rejected": "Le service distant a refusé le message",
|
||||
"connection_failed": "Échec de la connexion",
|
||||
"not_configured": "Non configuré",
|
||||
"url_not_public": "L’URL n’est pas un point de terminaison public",
|
||||
|
||||
@@ -6136,6 +6136,7 @@
|
||||
"rate_limited": "Limitato dal servizio upstream",
|
||||
"timeout": "Richiesta scaduta",
|
||||
"upstream_error": "Il servizio upstream ha restituito un errore",
|
||||
"upstream_rejected": "Il servizio remoto ha rifiutato il messaggio",
|
||||
"connection_failed": "Connessione non riuscita",
|
||||
"not_configured": "Non configurato",
|
||||
"url_not_public": "L’URL non è un endpoint pubblico",
|
||||
|
||||
@@ -6136,6 +6136,7 @@
|
||||
"rate_limited": "상대 서버에서 요청 수를 제한했어요",
|
||||
"timeout": "요청 시간이 초과됐어요",
|
||||
"upstream_error": "상대 서버가 오류를 반환했어요",
|
||||
"upstream_rejected": "상대 서버가 메시지를 거부했어요",
|
||||
"connection_failed": "연결에 실패했어요",
|
||||
"not_configured": "설정되지 않았어요",
|
||||
"url_not_public": "URL이 공개 엔드포인트가 아니에요",
|
||||
|
||||
@@ -6136,6 +6136,7 @@
|
||||
"rate_limited": "Ograniczone przez usługę nadrzędną",
|
||||
"timeout": "Przekroczono limit czasu żądania",
|
||||
"upstream_error": "Usługa nadrzędna zwróciła błąd",
|
||||
"upstream_rejected": "Usługa docelowa odrzuciła wiadomość",
|
||||
"connection_failed": "Połączenie nie powiodło się",
|
||||
"not_configured": "Nieskonfigurowano",
|
||||
"url_not_public": "Adres URL nie jest publicznym punktem końcowym",
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
/**
|
||||
* The test-connection failure callout (#947).
|
||||
*
|
||||
* SSR-only, like the rest of the component suite: the callout is hook-free so
|
||||
* it renders here without a provider. What is pinned is what a person reads:
|
||||
* the translated sentence, the status the other end answered, and the relay's
|
||||
* own words as text, never as markup.
|
||||
*/
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { TestConnectionFailure } from "../test-connection-button";
|
||||
|
||||
describe("TestConnectionFailure", () => {
|
||||
it("shows the sentence with the HTTP status and what the relay said", () => {
|
||||
const html = renderToStaticMarkup(
|
||||
<TestConnectionFailure
|
||||
message="The other end refused the message"
|
||||
upstreamStatus={400}
|
||||
upstreamBody='{"error":"Bad Request","errorCode":400}'
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(html).toContain('role="alert"');
|
||||
expect(html).toContain("The other end refused the message (HTTP 400)");
|
||||
expect(html).toContain(
|
||||
"{"error":"Bad Request","errorCode":400}",
|
||||
);
|
||||
});
|
||||
|
||||
it("renders a markup-shaped body as characters", () => {
|
||||
const html = renderToStaticMarkup(
|
||||
<TestConnectionFailure
|
||||
message="Upstream returned an error"
|
||||
upstreamStatus={502}
|
||||
upstreamBody='<img src=x onerror="alert(1)">'
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(html).not.toContain("<img");
|
||||
expect(html).toContain("<img src=x onerror="alert(1)">");
|
||||
});
|
||||
|
||||
it("shows the SMTP code when there is no HTTP status", () => {
|
||||
const html = renderToStaticMarkup(
|
||||
<TestConnectionFailure message="Credentials rejected" smtpCode={535} />,
|
||||
);
|
||||
|
||||
expect(html).toContain("Credentials rejected (SMTP 535)");
|
||||
});
|
||||
|
||||
it("shows the sentence alone when the server sent no detail", () => {
|
||||
const html = renderToStaticMarkup(
|
||||
<TestConnectionFailure message="Test failed" />,
|
||||
);
|
||||
|
||||
expect(html).toContain("Test failed");
|
||||
expect(html).not.toContain("HTTP");
|
||||
expect(html).not.toContain("test-connection-upstream-body");
|
||||
});
|
||||
});
|
||||
@@ -11,7 +11,10 @@ import { apiFetchRaw } from "@/lib/api/api-fetch";
|
||||
* Shared "Test connection" UX for the settings integrations + notifications
|
||||
* sections (A8-UI). POSTs to the given endpoint, expects a `{ data, error,
|
||||
* meta }` envelope, and surfaces success (latency) or a translated
|
||||
* `meta.errorCode` callout.
|
||||
* `meta.errorCode` callout. When the route reports what the other end
|
||||
* answered (`meta.upstreamStatus` / `meta.smtpCode` / `meta.upstreamBody`,
|
||||
* the notification test routes since #947), the callout shows the status and
|
||||
* the relay's own words beneath it, as plain text.
|
||||
*
|
||||
* Endpoints are user-scoped, so the button intentionally does NOT send an
|
||||
* Idempotency-Key — each click probes the upstream live.
|
||||
@@ -27,7 +30,56 @@ export interface TestConnectionButtonProps {
|
||||
interface TestResponse {
|
||||
data?: { latencyMs?: number; ok?: boolean; sent?: number };
|
||||
error?: string;
|
||||
meta?: { errorCode?: string };
|
||||
meta?: {
|
||||
errorCode?: string;
|
||||
upstreamStatus?: number;
|
||||
smtpCode?: number;
|
||||
upstreamBody?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface TestConnectionFailureProps {
|
||||
/** The translated error sentence. */
|
||||
message: string;
|
||||
upstreamStatus?: number;
|
||||
smtpCode?: number;
|
||||
/** What the other end answered, already bounded and cleaned by the server. */
|
||||
upstreamBody?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* The failure callout. Hook-free so it renders without a provider. The
|
||||
* relay's body is a React text child, never markup: whatever it contains is
|
||||
* shown as characters.
|
||||
*/
|
||||
export function TestConnectionFailure({
|
||||
message,
|
||||
upstreamStatus,
|
||||
smtpCode,
|
||||
upstreamBody,
|
||||
}: TestConnectionFailureProps) {
|
||||
const code =
|
||||
typeof upstreamStatus === "number"
|
||||
? ` (HTTP ${upstreamStatus})`
|
||||
: typeof smtpCode === "number"
|
||||
? ` (SMTP ${smtpCode})`
|
||||
: "";
|
||||
return (
|
||||
<div role="alert" className="space-y-1">
|
||||
<p className="text-destructive flex items-center gap-1.5 text-sm">
|
||||
<XCircle className="size-3.5 shrink-0" />
|
||||
{`${message}${code}`}
|
||||
</p>
|
||||
{upstreamBody ? (
|
||||
<p
|
||||
data-testid="test-connection-upstream-body"
|
||||
className="text-foreground font-mono text-xs break-all"
|
||||
>
|
||||
{upstreamBody}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function TestConnectionButton({
|
||||
@@ -39,7 +91,13 @@ export function TestConnectionButton({
|
||||
const [testing, setTesting] = useState(false);
|
||||
const [result, setResult] = useState<
|
||||
| { kind: "ok"; latency: number }
|
||||
| { kind: "error"; errorCode: string }
|
||||
| {
|
||||
kind: "error";
|
||||
errorCode: string;
|
||||
upstreamStatus?: number;
|
||||
smtpCode?: number;
|
||||
upstreamBody?: string;
|
||||
}
|
||||
| null
|
||||
>(null);
|
||||
|
||||
@@ -56,9 +114,20 @@ export function TestConnectionButton({
|
||||
latency: json.data?.latencyMs ?? 0,
|
||||
});
|
||||
} else {
|
||||
const meta = json.meta;
|
||||
setResult({
|
||||
kind: "error",
|
||||
errorCode: json.meta?.errorCode ?? "generic",
|
||||
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 {
|
||||
@@ -109,13 +178,12 @@ export function TestConnectionButton({
|
||||
)}
|
||||
|
||||
{result?.kind === "error" && (
|
||||
<p
|
||||
role="alert"
|
||||
className="text-destructive flex items-center gap-1.5 text-sm"
|
||||
>
|
||||
<XCircle className="h-3.5 w-3.5" />
|
||||
{describeError(result.errorCode)}
|
||||
</p>
|
||||
<TestConnectionFailure
|
||||
message={describeError(result.errorCode)}
|
||||
upstreamStatus={result.upstreamStatus}
|
||||
smtpCode={result.smtpCode}
|
||||
upstreamBody={result.upstreamBody}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user