Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesDiscussionsWikiPull RequestsProjectsCommitsActionsReleasesContributorsPulse● GatesSecuritySettingsDeploymentsPipelineInsightsAgents✨ Explain✨ Ask AI✨ Workspace✨ Spec✨ Tests▓ Debt Map✨ NL Search🏛 Archaeology
Blame · Line-by-line history

email-digest.test.ts

Each line is annotated with the commit that last touched it. Click any SHA to jump to that commit and see the surrounding change.

email-digest.test.tsBlame109 lines · 1 contributor
08420cdClaude1/**
2 * Block I7 — Weekly email digest tests.
3 *
4 * Pure helper coverage for textToHtml / escapeHtml / fmtRange, plus route auth
5 * smoke on /settings/digest/preview and the admin trigger endpoints. DB-backed
6 * calls (composeDigest / sendDigestForUser / sendDigestsToAll) are exercised
7 * against the live server.
8 */
9
10import { describe, it, expect } from "bun:test";
11import app from "../app";
12import { __internal } from "../lib/email-digest";
13
14const { textToHtml, escapeHtml, fmtRange } = __internal;
15
16describe("email-digest — escapeHtml", () => {
17 it("escapes <, >, & and quotes", () => {
18 expect(escapeHtml(`<a href="x">&</a>`)).toBe(
19 `&lt;a href=&quot;x&quot;&gt;&amp;&lt;/a&gt;`
20 );
21 });
22
23 it("leaves plain text alone", () => {
24 expect(escapeHtml("hello world")).toBe("hello world");
25 });
26});
27
28describe("email-digest — fmtRange", () => {
29 it("returns a single date when from === to (by day)", () => {
30 const d = new Date("2025-06-01T00:00:00Z");
31 expect(fmtRange(d, d)).toBe("2025-06-01");
32 });
33
34 it("joins distinct dates with arrow", () => {
35 const a = new Date("2025-06-01T00:00:00Z");
36 const b = new Date("2025-06-08T00:00:00Z");
37 const out = fmtRange(a, b);
38 expect(out).toContain("2025-06-01");
39 expect(out).toContain("2025-06-08");
40 expect(out).toContain("\u2192");
41 });
42});
43
44describe("email-digest — textToHtml", () => {
45 it("wraps H2 headings and list items", () => {
46 const html = textToHtml("## Section\n- item 1", "https://gluecron.com");
47 expect(html).toContain("<h3");
48 expect(html).toContain("Section");
49 expect(html).toContain("<li>item 1</li>");
50 });
51
52 it("renders <hr> for --- separator", () => {
53 const html = textToHtml("hello\n---\nfooter", "https://gluecron.com");
54 expect(html).toContain("<hr");
55 });
56
57 it("escapes user-controlled text in paragraphs", () => {
58 const html = textToHtml("<script>alert(1)</script>", "https://gluecron.com");
59 expect(html).not.toContain("<script>alert");
60 expect(html).toContain("&lt;script&gt;");
61 });
62
63 it("includes the base URL footer", () => {
64 const html = textToHtml("body", "https://gluecron.com");
65 expect(html).toContain("https://gluecron.com");
66 });
67});
68
69describe("email-digest — route auth", () => {
70 it("GET /settings/digest/preview without auth → 302 /login", async () => {
71 const res = await app.request("/settings/digest/preview");
72 expect(res.status).toBe(302);
73 expect(res.headers.get("location") || "").toContain("/login");
74 });
75
76 it("POST /admin/digests/run without auth → 302 /login", async () => {
77 const res = await app.request("/admin/digests/run", { method: "POST" });
78 expect(res.status).toBe(302);
79 expect(res.headers.get("location") || "").toContain("/login");
80 });
81
82 it("POST /admin/digests/preview without auth → 302 /login", async () => {
83 const res = await app.request("/admin/digests/preview", {
84 method: "POST",
85 body: new URLSearchParams({ username: "alice" }),
86 headers: { "content-type": "application/x-www-form-urlencoded" },
87 });
88 expect(res.status).toBe(302);
89 expect(res.headers.get("location") || "").toContain("/login");
90 });
91
92 it("GET /admin/digests without auth → 302 /login", async () => {
93 const res = await app.request("/admin/digests");
94 expect(res.status).toBe(302);
95 expect(res.headers.get("location") || "").toContain("/login");
96 });
97});
98
99describe("email-digest — settings form", () => {
100 it("POST /settings/notifications without auth → 302 /login", async () => {
101 const res = await app.request("/settings/notifications", {
102 method: "POST",
103 body: new URLSearchParams({ notify_email_digest_weekly: "1" }),
104 headers: { "content-type": "application/x-www-form-urlencoded" },
105 });
106 expect(res.status).toBe(302);
107 expect(res.headers.get("location") || "").toContain("/login");
108 });
109});