mirror of
https://github.com/pupperpowell/bibdle.git
synced 2026-10-22 05:54:10 -04:00
fix(rss): halt future-verse leaks, correct timezone dates, fix discovery
- Cap /feed.xml at the current America/New_York calendar day so pre-scheduled future verses (and stray 2124 rows) can no longer surface at the top of the feed - Emit DST-aware RFC 822 pubDates (correct -04:00/-05:00 offset) instead of a hardcoded EST offset, and parse calendar days as UTC so titles no longer shift by one day on UTC hosts - Add content-addressed ETag + Last-Modified with If-None-Match / If-Modified-Since conditional handling - Add atom:link rel=self, content:encoded, channel pubDate/generator/image - Move RSS auto-discovery <link> into app.html: the root route sets ssr=false, so the layout <svelte:head> link never reached the raw HTML and feed readers could not discover /feed.xml - Extract feed helpers into $lib/server/rss and add unit tests
This commit is contained in:
1 parent
4a53e09ab3
commit
97bff35323
5 files changed
+329
-108
No files matched your search
@@ -0,0 +1,94 @@
|
||||
import { describe, test, expect } from 'bun:test';
|
||||
import {
|
||||
cdata,
|
||||
escapeXml,
|
||||
feedFingerprint,
|
||||
formatRFC822,
|
||||
formatReadableDate,
|
||||
formatVerseText,
|
||||
latestModification,
|
||||
todayInTimeZone
|
||||
} from '$lib/server/rss';
|
||||
|
||||
describe('RSS date formatting', () => {
|
||||
test('formatReadableDate uses the calendar day, independent of server TZ', () => {
|
||||
expect(formatReadableDate('2026-09-18')).toBe('September 18, 2026');
|
||||
expect(formatReadableDate('2026-01-01')).toBe('January 1, 2026');
|
||||
});
|
||||
|
||||
test('formatRFC822 emits a valid GMT timestamp', () => {
|
||||
expect(formatRFC822('2026-01-15')).toBe('Thu, 15 Jan 2026 05:00:00 GMT');
|
||||
expect(formatRFC822('2026-09-18')).toBe('Fri, 18 Sep 2026 04:00:00 GMT');
|
||||
});
|
||||
|
||||
test('formatRFC822 handles daylight saving (EST -05:00 vs EDT -04:00)', () => {
|
||||
// Winter: midnight New York is 05:00 UTC.
|
||||
expect(formatRFC822('2026-01-15')).toContain('05:00:00 GMT');
|
||||
// Summer: midnight New York is 04:00 UTC.
|
||||
expect(formatRFC822('2026-07-15')).toContain('04:00:00 GMT');
|
||||
// Never a bare, ambiguous abbreviation like EST/EDT.
|
||||
expect(formatRFC822('2026-07-15')).not.toMatch(/\bE[SD]T\b/);
|
||||
});
|
||||
|
||||
test('todayInTimeZone respects the reference timezone', () => {
|
||||
// 02:00 UTC on the 23rd is still the evening of the 22nd in New York.
|
||||
expect(
|
||||
todayInTimeZone('America/New_York', new Date('2026-09-23T02:00:00Z'))
|
||||
).toBe('2026-09-22');
|
||||
// 04:00 UTC on the 23rd is midnight in New York.
|
||||
expect(
|
||||
todayInTimeZone('America/New_York', new Date('2026-09-23T04:00:00Z'))
|
||||
).toBe('2026-09-23');
|
||||
});
|
||||
});
|
||||
|
||||
describe('RSS content fingerprint', () => {
|
||||
const row = { date: '2026-09-18', reference: 'Leviticus 19:9-11', verseText: 'a' };
|
||||
|
||||
test('is stable for identical content', () => {
|
||||
expect(feedFingerprint([row])).toBe(feedFingerprint([{ ...row }]));
|
||||
});
|
||||
|
||||
test('changes when verse text changes', () => {
|
||||
expect(feedFingerprint([row])).not.toBe(feedFingerprint([{ ...row, verseText: 'b' }]));
|
||||
});
|
||||
|
||||
test('changes when the set of days changes', () => {
|
||||
expect(feedFingerprint([row])).not.toBe(
|
||||
feedFingerprint([row, { ...row, date: '2026-09-17' }])
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('latestModification', () => {
|
||||
const fallback = new Date('2026-09-18T00:00:00Z');
|
||||
|
||||
test('returns the newest createdAt', () => {
|
||||
const older = new Date('2026-09-16T10:00:00Z');
|
||||
const newer = new Date('2026-09-17T10:00:00Z');
|
||||
expect(
|
||||
latestModification([{ createdAt: older }, { createdAt: newer }], fallback)
|
||||
).toEqual(newer);
|
||||
});
|
||||
|
||||
test('falls back when rows have no timestamp', () => {
|
||||
expect(latestModification([{ createdAt: null }], fallback)).toEqual(fallback);
|
||||
expect(latestModification([], fallback)).toEqual(fallback);
|
||||
});
|
||||
});
|
||||
|
||||
describe('XML serialization helpers', () => {
|
||||
test('escapeXml escapes all predefined entities', () => {
|
||||
expect(escapeXml(`<a href="x">&'</a>`)).toBe(
|
||||
'<a href="x">&'</a>'
|
||||
);
|
||||
});
|
||||
|
||||
test('cdata splits embedded CDATA terminators', () => {
|
||||
expect(cdata('a]]>b')).toBe('<![CDATA[a]]]]><![CDATA[>b]]>');
|
||||
});
|
||||
|
||||
test('formatVerseText capitalizes and replaces trailing punctuation', () => {
|
||||
expect(formatVerseText('for God so loved the world,')).toBe('For God so loved the world...');
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user