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