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

atom-feed.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.

atom-feed.test.tsBlame231 lines · 1 contributor
26484ebClaude1/**
2 * Block J19 — Atom feed renderer. Pure XML + route smokes.
3 */
4
5import { describe, it, expect } from "bun:test";
6import app from "../app";
7import {
8 escapeXml,
9 toIsoUtc,
10 renderAtomFeed,
11 ATOM_CONTENT_TYPE,
12 __internal,
13 type AtomEntry,
14} from "../lib/atom-feed";
15
16describe("atom-feed — escapeXml", () => {
17 it("escapes the five XML metacharacters", () => {
18 expect(escapeXml("a < b & c > d")).toBe("a &lt; b &amp; c &gt; d");
19 expect(escapeXml(`"single'`)).toBe("&quot;single&apos;");
20 });
21
22 it("returns '' for empty / falsy input", () => {
23 expect(escapeXml("")).toBe("");
24 });
25
26 it("idempotent escape of already-escaped text", () => {
27 // Double-escaping is expected — this is a serialiser, not a re-encoder.
28 // We just need it not to throw or drop content.
29 const once = escapeXml("&");
30 const twice = escapeXml(once);
31 expect(twice).toBe("&amp;amp;");
32 });
33});
34
35describe("atom-feed — toIsoUtc", () => {
36 it("converts valid ISO strings to ISO-UTC", () => {
37 const out = toIsoUtc("2026-04-15T12:00:00Z");
38 expect(out).toBe("2026-04-15T12:00:00.000Z");
39 });
40
41 it("accepts Date instances", () => {
42 const out = toIsoUtc(new Date("2026-04-15T12:00:00Z"));
43 expect(out).toBe("2026-04-15T12:00:00.000Z");
44 });
45
46 it("falls back to epoch on garbage", () => {
47 expect(toIsoUtc("not-a-date")).toBe("1970-01-01T00:00:00.000Z");
48 expect(toIsoUtc(null)).toBe("1970-01-01T00:00:00.000Z");
49 expect(toIsoUtc(undefined)).toBe("1970-01-01T00:00:00.000Z");
50 expect(toIsoUtc("")).toBe("1970-01-01T00:00:00.000Z");
51 });
52});
53
54describe("atom-feed — renderAtomFeed", () => {
55 const entry: AtomEntry = {
56 id: "tag:gluecron,2026:alice/repo/commit/abc",
57 title: "Fix bug",
58 href: "https://gluecron.com/alice/repo/commit/abc",
59 updatedAt: "2026-04-15T12:00:00Z",
60 summary: "Fix a bug",
61 author: { name: "Alice", email: "a@x" },
62 };
63
64 it("prefixes with the XML declaration + feed root", () => {
65 const xml = renderAtomFeed({
66 id: "tag:feed",
67 title: "Test",
68 selfHref: "https://gluecron.com/feed",
69 entries: [],
70 });
71 expect(xml.startsWith('<?xml version="1.0" encoding="utf-8"?>\n')).toBe(
72 true
73 );
74 expect(xml).toContain('<feed xmlns="http://www.w3.org/2005/Atom">');
75 expect(xml.trim().endsWith("</feed>")).toBe(true);
76 });
77
78 it("emits required feed-level elements", () => {
79 const xml = renderAtomFeed({
80 id: "tag:feed",
81 title: "Test Feed",
82 subtitle: "Sub",
83 selfHref: "https://g.c/feed",
84 alternateHref: "https://g.c/",
85 entries: [],
86 });
87 expect(xml).toContain("<id>tag:feed</id>");
88 expect(xml).toContain("<title>Test Feed</title>");
89 expect(xml).toContain("<subtitle>Sub</subtitle>");
90 expect(xml).toContain('<link rel="self" href="https://g.c/feed"/>');
91 expect(xml).toContain('<link rel="alternate" href="https://g.c/"/>');
92 expect(xml).toMatch(/<updated>.+<\/updated>/);
93 });
94
95 it("renders entry title, id, link, updated, published, author, summary", () => {
96 const xml = renderAtomFeed({
97 id: "tag:feed",
98 title: "Test",
99 selfHref: "https://g.c/feed",
100 entries: [entry],
101 });
102 expect(xml).toContain("<entry>");
103 expect(xml).toContain("</entry>");
104 expect(xml).toContain(`<id>${entry.id}</id>`);
105 expect(xml).toContain(`<title>${entry.title}</title>`);
106 expect(xml).toContain(`<link rel="alternate" href="${entry.href}"/>`);
107 expect(xml).toContain("<updated>2026-04-15T12:00:00.000Z</updated>");
108 expect(xml).toContain("<published>2026-04-15T12:00:00.000Z</published>");
109 expect(xml).toContain("<name>Alice</name>");
110 expect(xml).toContain("<email>a@x</email>");
111 expect(xml).toContain('<summary type="text">Fix a bug</summary>');
112 });
113
114 it("escapes entry text fields", () => {
115 const xml = renderAtomFeed({
116 id: "tag:feed",
117 title: "Test & Ampersand",
118 selfHref: "https://g.c/feed?x=1&y=2",
119 entries: [
120 {
121 id: "tag:e",
122 title: "<script>bad</script>",
123 href: "https://g.c/x?a=1&b=2",
124 updatedAt: "2026-04-15T12:00:00Z",
125 summary: 'She said "hi"',
126 },
127 ],
128 });
129 expect(xml).toContain("Test &amp; Ampersand");
130 expect(xml).toContain("x=1&amp;y=2");
131 expect(xml).toContain("&lt;script&gt;bad&lt;/script&gt;");
132 expect(xml).toContain("&quot;hi&quot;");
133 // Must not contain the raw unescaped forms.
134 expect(xml).not.toContain("<script>bad</script>");
135 });
136
137 it("picks feed updated from the newest entry when not set explicitly", () => {
138 const xml = renderAtomFeed({
139 id: "tag:feed",
140 title: "Test",
141 selfHref: "https://g.c/feed",
142 entries: [
143 { id: "a", title: "A", href: "h", updatedAt: "2026-04-10T00:00:00Z" },
144 { id: "b", title: "B", href: "h", updatedAt: "2026-04-14T00:00:00Z" },
145 { id: "c", title: "C", href: "h", updatedAt: "2026-04-12T00:00:00Z" },
146 ],
147 });
148 // The feed `<updated>` should pick the newest entry (Apr 14)
149 expect(xml).toContain("<updated>2026-04-14T00:00:00.000Z</updated>");
150 });
151
152 it("respects explicit feed updatedAt", () => {
153 const xml = renderAtomFeed({
154 id: "tag:feed",
155 title: "Test",
156 selfHref: "https://g.c/feed",
157 updatedAt: "2026-01-01T00:00:00Z",
158 entries: [
159 { id: "a", title: "A", href: "h", updatedAt: "2026-04-14T00:00:00Z" },
160 ],
161 });
162 expect(xml).toContain("<updated>2026-01-01T00:00:00.000Z</updated>");
163 });
164
165 it("produces a well-formed document even with zero entries", () => {
166 const xml = renderAtomFeed({
167 id: "tag:empty",
168 title: "Empty",
169 selfHref: "https://g.c/empty.atom",
170 entries: [],
171 });
172 expect(xml).toContain("<id>tag:empty</id>");
173 expect(xml).not.toContain("<entry>");
174 });
175
176 it("falls back to (untitled) when a title is empty", () => {
177 const xml = renderAtomFeed({
178 id: "tag:feed",
179 title: "Test",
180 selfHref: "https://g.c/feed",
181 entries: [
182 { id: "a", title: "", href: "h", updatedAt: "2026-04-14T00:00:00Z" },
183 ],
184 });
185 expect(xml).toContain("<title>(untitled)</title>");
186 });
187});
188
189describe("atom-feed — ATOM_CONTENT_TYPE", () => {
190 it("is the canonical Atom mime with charset", () => {
191 expect(ATOM_CONTENT_TYPE).toBe("application/atom+xml; charset=utf-8");
192 });
193});
194
195describe("atom-feed — __internal", () => {
196 it("re-exports the helpers for parity", () => {
197 expect(__internal.escapeXml).toBe(escapeXml);
198 expect(__internal.toIsoUtc).toBe(toIsoUtc);
199 expect(__internal.renderAtomFeed).toBe(renderAtomFeed);
200 expect(__internal.ATOM_CONTENT_TYPE).toBe(ATOM_CONTENT_TYPE);
201 });
202});
203
204describe("atom-feed — routes", () => {
205 it("GET /:o/:r/commits.atom returns 200 with Atom content-type", async () => {
206 const res = await app.request("/alice/nope/commits.atom");
207 expect(res.status).toBe(200);
208 expect(res.headers.get("content-type")).toBe(ATOM_CONTENT_TYPE);
209 const body = await res.text();
210 expect(body).toContain('<feed xmlns="http://www.w3.org/2005/Atom">');
211 });
212
213 it("GET /:o/:r/releases.atom returns 200 Atom", async () => {
214 const res = await app.request("/alice/nope/releases.atom");
215 expect(res.status).toBe(200);
216 expect(res.headers.get("content-type")).toBe(ATOM_CONTENT_TYPE);
217 });
218
219 it("GET /:o/:r/issues.atom returns 200 Atom", async () => {
220 const res = await app.request("/alice/nope/issues.atom");
221 expect(res.status).toBe(200);
222 expect(res.headers.get("content-type")).toBe(ATOM_CONTENT_TYPE);
223 });
224
225 it("cache headers set for feed reader friendliness", async () => {
226 const res = await app.request("/alice/nope/commits.atom");
227 const cc = res.headers.get("cache-control") || "";
228 expect(cc).toContain("max-age");
229 expect(cc).toContain("stale-while-revalidate");
230 });
231});