255746d9f5
Squash merge PR #591: feat: restore ntfy interactive settings test delivery support
187 lines
5.5 KiB
TypeScript
187 lines
5.5 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import {
|
|
getNotificationActionLabels,
|
|
isNtfyNotificationUrl,
|
|
type PushNotificationAction,
|
|
renderNotificationActionPayload,
|
|
} from "../services/notifications/action-renderer.js";
|
|
|
|
function decodeRfc2047Base64(value: string): string {
|
|
const match = /^=\?UTF-8\?B\?(.+)\?=$/.exec(value);
|
|
if (!match) {
|
|
return value;
|
|
}
|
|
|
|
return Buffer.from(match[1], "base64").toString("utf-8");
|
|
}
|
|
|
|
const actions: PushNotificationAction[] = [
|
|
{
|
|
kind: "taken",
|
|
label: "Take",
|
|
url: "https://app.example.com/api/notification-actions/taken-token",
|
|
method: "POST",
|
|
},
|
|
{
|
|
kind: "skip",
|
|
label: "Skip",
|
|
url: "https://app.example.com/api/notification-actions/skip-token",
|
|
method: "POST",
|
|
},
|
|
{ kind: "view", label: "View", url: "https://app.example.com/?date=2026-01-05", method: "GET" },
|
|
];
|
|
|
|
describe("notification action renderer", () => {
|
|
it("builds ntfy native actions without duplicate click headers", () => {
|
|
const result = renderNotificationActionPayload("ntfy://ntfy.sh/medassist", "Body", {
|
|
actions,
|
|
clickUrl: "https://app.example.com/api/notification-actions/respond-token",
|
|
respondUrl: "https://app.example.com/api/notification-actions/respond-token",
|
|
viewUrl: "https://app.example.com/?date=2026-01-05",
|
|
tags: ["pill"],
|
|
priority: 4,
|
|
sequenceId: "medassist-sequence",
|
|
});
|
|
|
|
expect(result.message).toBe("Body");
|
|
expect(result.headers).toMatchObject({
|
|
Tags: "pill",
|
|
Priority: "4",
|
|
"X-Sequence-ID": "medassist-sequence",
|
|
});
|
|
expect(result.headers.Click).toBeUndefined();
|
|
|
|
const parsedActions = JSON.parse(result.headers.Actions ?? "[]");
|
|
expect(parsedActions).toEqual([
|
|
{
|
|
action: "http",
|
|
label: "Take",
|
|
url: "https://app.example.com/api/notification-actions/taken-token",
|
|
method: "POST",
|
|
clear: true,
|
|
},
|
|
{
|
|
action: "http",
|
|
label: "Skip",
|
|
url: "https://app.example.com/api/notification-actions/skip-token",
|
|
method: "POST",
|
|
clear: true,
|
|
},
|
|
{
|
|
action: "view",
|
|
label: "View",
|
|
url: "https://app.example.com/?date=2026-01-05",
|
|
clear: false,
|
|
},
|
|
]);
|
|
});
|
|
|
|
it("keeps the ntfy click header when there are no native actions", () => {
|
|
const result = renderNotificationActionPayload("ntfy://ntfy.sh/medassist", "Body", {
|
|
clickUrl: "https://app.example.com/api/notification-actions/respond-token",
|
|
});
|
|
|
|
expect(result.headers.Click).toBe("https://app.example.com/api/notification-actions/respond-token");
|
|
});
|
|
|
|
it("treats direct https ntfy URLs as ntfy targets with native actions", () => {
|
|
const result = renderNotificationActionPayload("https://ntfy.danielvolz.org/medis_test", "Body", {
|
|
actions,
|
|
clickUrl: "https://app.example.com/api/notification-actions/respond-token",
|
|
respondUrl: "https://app.example.com/api/notification-actions/respond-token",
|
|
viewUrl: "https://app.example.com/?date=2026-01-05",
|
|
});
|
|
|
|
expect(isNtfyNotificationUrl("https://ntfy.danielvolz.org/medis_test")).toBe(true);
|
|
expect(result.message).toBe("Body");
|
|
expect(result.headers.Actions).toBeTruthy();
|
|
expect(result.message).not.toContain("Respond:");
|
|
});
|
|
|
|
it("keeps insecure http mutation targets as direct ntfy http actions without the dev fallback", () => {
|
|
const result = renderNotificationActionPayload("https://ntfy.danielvolz.org/medis_test", "Body", {
|
|
actions: [
|
|
{
|
|
kind: "taken",
|
|
label: "Take",
|
|
url: "http://192.168.0.113:5173/api/notification-actions/taken-token",
|
|
method: "POST",
|
|
},
|
|
],
|
|
});
|
|
|
|
expect(JSON.parse(result.headers.Actions ?? "[]")).toEqual([
|
|
{
|
|
action: "http",
|
|
label: "Take",
|
|
url: "http://192.168.0.113:5173/api/notification-actions/taken-token",
|
|
method: "POST",
|
|
clear: true,
|
|
},
|
|
]);
|
|
});
|
|
|
|
it("encodes non-ascii ntfy action labels as RFC 2047 headers", () => {
|
|
const result = renderNotificationActionPayload("https://ntfy.danielvolz.org/medis_test", "Body", {
|
|
actions: [
|
|
{
|
|
kind: "skip",
|
|
label: "Überspringen",
|
|
url: "https://app.example.com/api/notification-actions/skip-token",
|
|
method: "POST",
|
|
},
|
|
{
|
|
kind: "view",
|
|
label: "Öffnen",
|
|
url: "https://app.example.com/?date=2026-01-05",
|
|
method: "GET",
|
|
},
|
|
],
|
|
});
|
|
|
|
expect(result.headers.Actions).toMatch(/^=\?UTF-8\?B\?/);
|
|
expect(JSON.parse(decodeRfc2047Base64(result.headers.Actions ?? "[]"))).toEqual([
|
|
{
|
|
action: "http",
|
|
label: "Überspringen",
|
|
url: "https://app.example.com/api/notification-actions/skip-token",
|
|
method: "POST",
|
|
clear: true,
|
|
},
|
|
{
|
|
action: "view",
|
|
label: "Öffnen",
|
|
url: "https://app.example.com/?date=2026-01-05",
|
|
clear: false,
|
|
},
|
|
]);
|
|
});
|
|
|
|
it("uses consistent action-form labels for English and German", () => {
|
|
expect(getNotificationActionLabels("en")).toEqual({
|
|
taken: "Take",
|
|
skip: "Skip",
|
|
respond: "Respond",
|
|
view: "View",
|
|
});
|
|
expect(getNotificationActionLabels("de")).toEqual({
|
|
taken: "Einnehmen",
|
|
skip: "Überspringen",
|
|
respond: "Antworten",
|
|
view: "Öffnen",
|
|
});
|
|
});
|
|
|
|
it("appends respond and view fallback links for non-ntfy providers", () => {
|
|
const result = renderNotificationActionPayload("https://hooks.slack.com/services/a/b/c", "Body", {
|
|
respondUrl: "https://app.example.com/api/notification-actions/respond-token",
|
|
viewUrl: "https://app.example.com/?date=2026-01-05",
|
|
});
|
|
|
|
expect(result.headers).toEqual({});
|
|
expect(result.message).toBe(
|
|
"Body\n\nRespond:\nhttps://app.example.com/api/notification-actions/respond-token\n\nView:\nhttps://app.example.com/?date=2026-01-05"
|
|
);
|
|
});
|
|
});
|