Add bilingual support for notifications and automatic HTTPS

Implement bilingual text for push notifications, allowing users to receive them in their preferred language. Automatically configure HTTPS with Let's Encrypt for custom domains to ensure persistent subscriptions.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 94f06aee-a5cf-45df-979e-f940cce77214
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/yEFtMKN
Replit-Helium-Checkpoint-Created: true
This commit is contained in:
riyadhafraa
2026-05-16 16:28:42 +00:00
parent d78818b88a
commit 55d19298e7
7 changed files with 317 additions and 23 deletions
@@ -26,7 +26,7 @@ if (!DATABASE_URL) {
}
// Track sendNotification calls and let each test rig the next response.
const sendCalls: Array<{ endpoint: string }> = [];
const sendCalls: Array<{ endpoint: string; payload: string }> = [];
let nextThrow: { statusCode: number } | null = null;
mock.module("web-push", {
@@ -36,8 +36,8 @@ mock.module("web-push", {
publicKey: "B".padEnd(87, "A"),
privateKey: "C".padEnd(43, "A"),
}),
sendNotification: async (sub: { endpoint: string }) => {
sendCalls.push({ endpoint: sub.endpoint });
sendNotification: async (sub: { endpoint: string }, payload: string) => {
sendCalls.push({ endpoint: sub.endpoint, payload });
if (nextThrow) {
const err: Error & { statusCode?: number } = new Error("stubbed");
err.statusCode = nextThrow.statusCode;
@@ -182,3 +182,146 @@ test("non-410/404 errors leave the subscription intact", async () => {
"transient 5xx must NOT delete the subscription",
);
});
test("notificationsMuted=true silences every channel", async () => {
const userId = await createUser("pmute");
const endpoint = `https://push.example.test/mute-${Date.now()}`;
await insertSub(userId, endpoint);
await pool.query(
`UPDATE users SET notifications_muted = true WHERE id = $1`,
[userId],
);
const before = sendCalls.length;
await sendPushToUser(userId, {
title: "t",
body: "b",
type: "note",
});
assert.equal(
sendCalls.length,
before,
"muted user must not trigger sendNotification at all",
);
assert.equal(
await rowExists(endpoint),
true,
"muting must not delete the subscription",
);
});
test("per-channel flag off skips that channel only", async () => {
const userId = await createUser("pchan");
const endpoint = `https://push.example.test/chan-${Date.now()}`;
await insertSub(userId, endpoint);
await pool.query(
`UPDATE users SET notify_notes_enabled = false WHERE id = $1`,
[userId],
);
const before = sendCalls.length;
await sendPushToUser(userId, {
title: "t",
body: "b",
type: "note",
});
assert.equal(
sendCalls.length,
before,
"notes-disabled user must not receive a note push",
);
// Same user, different channel — order push must still fire.
await sendPushToUser(userId, {
title: "t",
body: "b",
type: "order",
});
assert.equal(
sendCalls.length,
before + 1,
"order channel must still fire when only notes is off",
);
});
test("oversized body is truncated so the wire payload stays under 4096 bytes", async () => {
const userId = await createUser("ptrunc");
const endpoint = `https://push.example.test/trunc-${Date.now()}`;
await insertSub(userId, endpoint);
// 8000 bytes of ASCII — comfortably above the 3500-byte budget and
// way past the 4096-byte web-push cap, so truncation MUST kick in.
const huge = "x".repeat(8000);
await sendPushToUser(userId, {
title: "t",
body: huge,
type: "note",
});
const sent = sendCalls.at(-1);
assert.ok(sent, "send stub should have been called");
const size = Buffer.byteLength(sent.payload, "utf8");
assert.ok(
size < 4096,
`truncated payload must fit under 4096 bytes, got ${size}`,
);
const parsed = JSON.parse(sent.payload) as { body: string };
assert.ok(
parsed.body.length < huge.length,
"body must have been shortened",
);
});
test("preferredLanguage=en picks the English title/body", async () => {
const userId = await createUser("plangen");
await pool.query(
`UPDATE users SET preferred_language = 'en' WHERE id = $1`,
[userId],
);
const endpoint = `https://push.example.test/langen-${Date.now()}`;
await insertSub(userId, endpoint);
await sendPushToUser(userId, {
title: "ar-fallback",
body: "ar-fallback",
titleAr: "ملاحظة جديدة",
titleEn: "New note",
bodyAr: "أرسل لك ملاحظة",
bodyEn: "sent you a note",
type: "note",
});
const sent = sendCalls.at(-1);
assert.ok(sent, "send stub should have been called");
const parsed = JSON.parse(sent.payload) as { title: string; body: string };
assert.equal(parsed.title, "New note");
assert.equal(parsed.body, "sent you a note");
});
test("preferredLanguage=ar picks the Arabic title/body", async () => {
const userId = await createUser("plangar");
// Default is already 'ar' but be explicit so the test documents intent.
await pool.query(
`UPDATE users SET preferred_language = 'ar' WHERE id = $1`,
[userId],
);
const endpoint = `https://push.example.test/langar-${Date.now()}`;
await insertSub(userId, endpoint);
await sendPushToUser(userId, {
title: "ignored",
body: "ignored",
titleAr: "ملاحظة جديدة",
titleEn: "New note",
bodyAr: "أرسل لك ملاحظة",
bodyEn: "sent you a note",
type: "note",
});
const sent = sendCalls.at(-1);
assert.ok(sent, "send stub should have been called");
const parsed = JSON.parse(sent.payload) as { title: string; body: string };
assert.equal(parsed.title, "ملاحظة جديدة");
assert.equal(parsed.body, "أرسل لك ملاحظة");
});