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(`&'`)).toBe( '<a href="x">&'</a>' ); }); test('cdata splits embedded CDATA terminators', () => { expect(cdata('a]]>b')).toBe('b]]>'); }); test('formatVerseText capitalizes and replaces trailing punctuation', () => { expect(formatVerseText('for God so loved the world,')).toBe('For God so loved the world...'); }); });