From e59ef4533d5b9d91dfd3c93d7dbeba9472048f50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Thu, 12 Nov 2026 14:07:19 +0100 Subject: [PATCH] fix(push): say why a test push failed, under its own title The Web Push test answered a refused push with `ok: false` and the sender's `reason`, but the Test button reads `meta.errorCode`, so it only said "Test failed". The answer now carries `upstream_rejected`, or `connection_failed` when no push service answered. The test push used the administrator's test title, which put the brand first in several languages; it has its own title now. --- messages/de.json | 3 ++- messages/en.json | 3 ++- messages/es.json | 3 ++- messages/fr.json | 3 ++- messages/it.json | 3 ++- messages/ko.json | 3 ++- messages/nl.json | 3 ++- messages/pl.json | 3 ++- messages/pt-BR.json | 3 ++- messages/ru.json | 3 ++- .../web-push/test/__tests__/route.test.ts | 17 ++++++++++++++++- .../api/notifications/web-push/test/route.ts | 16 ++++++++++++---- .../test-connection-button-response.test.tsx | 18 ++++++++++++++++++ .../openapi/routes/notifications-transport.ts | 2 +- 14 files changed, 67 insertions(+), 16 deletions(-) diff --git a/messages/de.json b/messages/de.json index c1eb8749a..41b64180e 100644 --- a/messages/de.json +++ b/messages/de.json @@ -9289,7 +9289,8 @@ }, "user": { "telegramTestBody": "HealthLog: Verbindung hergestellt. Telegram-Benachrichtigungen sind aktiv.", - "webPushTestBody": "Wenn du das siehst, kommen Push-Benachrichtigungen auf diesem Gerät an." + "webPushTestBody": "Wenn du das siehst, kommen Push-Benachrichtigungen auf diesem Gerät an.", + "webPushTestTitle": "Test-Benachrichtigung von HealthLog" }, "moodReminder": { "title": "Tägliche Stimmungs-Erinnerung", diff --git a/messages/en.json b/messages/en.json index 38641c38d..dea9402a2 100644 --- a/messages/en.json +++ b/messages/en.json @@ -9289,7 +9289,8 @@ }, "user": { "telegramTestBody": "HealthLog: connection successful. Telegram notifications are active.", - "webPushTestBody": "If you can see this, push notifications reach this device." + "webPushTestBody": "If you can see this, push notifications reach this device.", + "webPushTestTitle": "HealthLog test notification" }, "moodReminder": { "title": "Daily mood reminder", diff --git a/messages/es.json b/messages/es.json index f0d50e141..e4284f1e3 100644 --- a/messages/es.json +++ b/messages/es.json @@ -9289,7 +9289,8 @@ }, "user": { "telegramTestBody": "HealthLog: conexión correcta. Las notificaciones de Telegram están activas.", - "webPushTestBody": "Si ves esto, las notificaciones push llegan a este dispositivo." + "webPushTestBody": "Si ves esto, las notificaciones push llegan a este dispositivo.", + "webPushTestTitle": "Notificación de prueba de HealthLog" }, "moodReminder": { "title": "Recordatorio diario de ánimo", diff --git a/messages/fr.json b/messages/fr.json index ecd78a0cd..0d5cddda6 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -9289,7 +9289,8 @@ }, "user": { "telegramTestBody": "HealthLog: connexion réussie. Les notifications Telegram sont actives.", - "webPushTestBody": "Si vous voyez ceci, les notifications push arrivent sur cet appareil." + "webPushTestBody": "Si vous voyez ceci, les notifications push arrivent sur cet appareil.", + "webPushTestTitle": "Notification de test HealthLog" }, "moodReminder": { "title": "Rappel quotidien d’humeur", diff --git a/messages/it.json b/messages/it.json index 3ff814e06..092346598 100644 --- a/messages/it.json +++ b/messages/it.json @@ -9289,7 +9289,8 @@ }, "user": { "telegramTestBody": "HealthLog: connessione riuscita. Le notifiche Telegram sono attive.", - "webPushTestBody": "Se vedi questo messaggio, le notifiche push arrivano su questo dispositivo." + "webPushTestBody": "Se vedi questo messaggio, le notifiche push arrivano su questo dispositivo.", + "webPushTestTitle": "Notifica di prova di HealthLog" }, "moodReminder": { "title": "Promemoria quotidiano dell’umore", diff --git a/messages/ko.json b/messages/ko.json index 5cfbd4f5a..dc6f68aee 100644 --- a/messages/ko.json +++ b/messages/ko.json @@ -9289,7 +9289,8 @@ }, "user": { "telegramTestBody": "HealthLog: 연결에 성공했어요. Telegram 알림이 켜져 있어요.", - "webPushTestBody": "이 알림이 보이면 이 기기에서 푸시 알림을 받을 수 있어요." + "webPushTestBody": "이 알림이 보이면 이 기기에서 푸시 알림을 받을 수 있어요.", + "webPushTestTitle": "HealthLog 테스트 알림" }, "moodReminder": { "title": "매일 기분 알림", diff --git a/messages/nl.json b/messages/nl.json index 53e39c1b8..51d74116f 100644 --- a/messages/nl.json +++ b/messages/nl.json @@ -9289,7 +9289,8 @@ }, "user": { "telegramTestBody": "HealthLog: verbinding geslaagd. Telegram-meldingen zijn actief.", - "webPushTestBody": "Zie je dit, dan komen pushmeldingen aan op dit apparaat." + "webPushTestBody": "Zie je dit, dan komen pushmeldingen aan op dit apparaat.", + "webPushTestTitle": "Testmelding van HealthLog" }, "moodReminder": { "title": "Dagelijkse stemmingsherinnering", diff --git a/messages/pl.json b/messages/pl.json index 736876de3..ba7664f2a 100644 --- a/messages/pl.json +++ b/messages/pl.json @@ -9289,7 +9289,8 @@ }, "user": { "telegramTestBody": "HealthLog: połączenie udane. Powiadomienia Telegram są aktywne.", - "webPushTestBody": "Jeśli to widzisz, powiadomienia push docierają na to urządzenie." + "webPushTestBody": "Jeśli to widzisz, powiadomienia push docierają na to urządzenie.", + "webPushTestTitle": "Powiadomienie testowe HealthLog" }, "moodReminder": { "title": "Codzienne przypomnienie o nastroju", diff --git a/messages/pt-BR.json b/messages/pt-BR.json index e0eb99ea0..9c4c36a1c 100644 --- a/messages/pt-BR.json +++ b/messages/pt-BR.json @@ -9289,7 +9289,8 @@ }, "user": { "telegramTestBody": "HealthLog: conexão bem-sucedida. As notificações do Telegram estão ativas.", - "webPushTestBody": "Se você está vendo isto, as notificações push chegam a este dispositivo." + "webPushTestBody": "Se você está vendo isto, as notificações push chegam a este dispositivo.", + "webPushTestTitle": "Notificação de teste do HealthLog" }, "moodReminder": { "title": "Lembrete diário de humor", diff --git a/messages/ru.json b/messages/ru.json index f05a230ed..6f368e37c 100644 --- a/messages/ru.json +++ b/messages/ru.json @@ -9289,7 +9289,8 @@ }, "user": { "telegramTestBody": "HealthLog: подключение успешно. Уведомления в Telegram активны.", - "webPushTestBody": "Если вы видите это сообщение, push-уведомления приходят на это устройство." + "webPushTestBody": "Если вы видите это сообщение, push-уведомления приходят на это устройство.", + "webPushTestTitle": "Тестовое уведомление HealthLog" }, "moodReminder": { "title": "Ежедневное напоминание о настроении", diff --git a/src/app/api/notifications/web-push/test/__tests__/route.test.ts b/src/app/api/notifications/web-push/test/__tests__/route.test.ts index 980830013..f53d370af 100644 --- a/src/app/api/notifications/web-push/test/__tests__/route.test.ts +++ b/src/app/api/notifications/web-push/test/__tests__/route.test.ts @@ -134,7 +134,7 @@ describe("POST /api/notifications/web-push/test", () => { } expect(payloadsSent()).toEqual([ expect.objectContaining({ - title: "HealthLog Test-Benachrichtigung", + title: "Test-Benachrichtigung von HealthLog", body: "Wenn du das siehst, kommen Push-Benachrichtigungen auf diesem Gerät an.", tag: "self-test", }), @@ -181,6 +181,21 @@ describe("POST /api/notifications/web-push/test", () => { expect(res.status).toBe(200); expect(JSON.parse(text).data.ok).toBe(false); + // What the Test button reads to say why. + expect(JSON.parse(text).meta.errorCode).toBe("upstream_rejected"); expect(text).not.toMatch(/phone-secret|laptop-secret/); }); + + it("tells a push service that never answered apart from one that refused", async () => { + mocks.safeFetch.mockRejectedValue(new TypeError("fetch failed")); + + const res = await post(); + const body = (await res.json()) as { + data: { ok: boolean }; + meta?: { errorCode?: string }; + }; + + expect(body.data.ok).toBe(false); + expect(body.meta?.errorCode).toBe("connection_failed"); + }); }); diff --git a/src/app/api/notifications/web-push/test/route.ts b/src/app/api/notifications/web-push/test/route.ts index 54e45d09f..0ae423903 100644 --- a/src/app/api/notifications/web-push/test/route.ts +++ b/src/app/api/notifications/web-push/test/route.ts @@ -1,6 +1,6 @@ import type { NextRequest } from "next/server"; import { apiHandler, requireAuth } from "@/lib/api-handler"; -import { apiSuccess, apiError } from "@/lib/api-response"; +import { apiSuccess, apiSuccessWithMeta, apiError } from "@/lib/api-response"; import { checkRateLimit } from "@/lib/rate-limit"; import { annotate } from "@/lib/logging/context"; import { coerceLocale } from "@/lib/i18n/config"; @@ -34,7 +34,7 @@ export const POST = apiHandler(async (request: NextRequest) => { const result = await sendViaWebPush(user.id, { eventType: "SYSTEM_ALERT", userId: user.id, - title: t("notifications.admin.testNotificationTitle"), + title: t("notifications.user.webPushTestTitle"), message: t("notifications.user.webPushTestBody"), metadata: { webPushTag: "self-test" }, }); @@ -54,7 +54,15 @@ export const POST = apiHandler(async (request: NextRequest) => { annotate({ meta: { web_push_test_ok: result.ok, web_push_test_reason: result.reason }, }); - return apiSuccess( - result.ok ? { ok: true, latencyMs } : { ok: false, reason: result.reason }, + if (result.ok) return apiSuccess({ ok: true, latencyMs }); + // The Test button says what went wrong from `meta.errorCode`, like every + // other channel's test; `reason` is the sender's own code. + return apiSuccessWithMeta( + { ok: false, reason: result.reason }, + { + errorCode: result.reason?.endsWith("_network_error") + ? "connection_failed" + : "upstream_rejected", + }, ); }); diff --git a/src/components/settings/__tests__/test-connection-button-response.test.tsx b/src/components/settings/__tests__/test-connection-button-response.test.tsx index 97ab62800..5c97ee3b2 100644 --- a/src/components/settings/__tests__/test-connection-button-response.test.tsx +++ b/src/components/settings/__tests__/test-connection-button-response.test.tsx @@ -101,6 +101,24 @@ describe("runConnectionTest", () => { }); }); + it("reads why a push test that ran was refused", async () => { + // The Web Push test answers 200 with `ok: false`; the code rides `meta`. + apiFetchRawMock.mockResolvedValue( + envelope(200, { + data: { ok: false, reason: "web_push_403" }, + error: null, + meta: { errorCode: "upstream_rejected" }, + }), + ); + + const result = await runConnectionTest("/api/notifications/web-push/test"); + + expect(result).toMatchObject({ + kind: "error", + errorCode: "upstream_rejected", + }); + }); + it("reports success with the latency", async () => { apiFetchRawMock.mockResolvedValue( envelope(200, { data: { ok: true, latencyMs: 42 }, error: null }), diff --git a/src/lib/openapi/routes/notifications-transport.ts b/src/lib/openapi/routes/notifications-transport.ts index 1839477d9..6d3321680 100644 --- a/src/lib/openapi/routes/notifications-transport.ts +++ b/src/lib/openapi/routes/notifications-transport.ts @@ -191,7 +191,7 @@ export const notificationTransportPaths: NonNullable< summary: "Send a test push to every Web Push subscription", description: "Fires one notification to each of the caller's subscriptions, through the same sender real reminders use, with the text in the account's language. The device the test was pressed on gets it whichever subscription is newest. Endpoints the push service reports gone (404/410) are removed, as on a real send. Takes no body. Rate-limited 5 per minute per user.\n\n" + - "Read `ok`, not the status. A push that every service rejects still answers 200, with `ok: false` and a `reason`. No endpoint is returned: the full endpoint is the routing secret for that subscription.", + "Read `ok`, not the status. A push that every service rejects still answers 200, with `ok: false`, a `reason`, and `meta.errorCode`: `upstream_rejected`, or `connection_failed` when no push service answered. No endpoint is returned: the full endpoint is the routing secret for that subscription.", responses: { "200": { description: