import { describe, it, expect } from 'vitest' import { readFileSync } from 'node:fs' import { fileURLToPath } from 'node:url' import { dirname, join } from 'node:path' import { parseICS } from '../src/lib/ical-io.js' import { serializeICS } from '../src/lib/ical-io.js' const __dirname = dirname(fileURLToPath(import.meta.url)) const GOLDEN = readFileSync(join(__dirname, 'golden-org.ics'), 'utf-8') describe('parseICS', () => { const { meta, events } = parseICS(GOLDEN) it('parses calendar meta', () => { expect(meta.prodid).toBe('-//Allocate//iCal4j 1.0//EN') expect(meta.version).toBe('2.0') expect(meta.calscale).toBe('GREGORIAN') expect(meta.vtimezones.length).toBe(1) }) it('parses all 61 events', () => { expect(events).toHaveLength(61) }) it('parses first event fields correctly', () => { const e = events[0] expect(e.summary).toBe('ACC INFO SYS, Tutorial') expect(e.location).toBe('CA_B_B471') expect(e.dtstartDate).toBe('2026-07-27') expect(e.dtstartTime).toBe('18:00') expect(e.dtendTime).toBe('20:00') expect(e.tzid).toBe('Australia/Melbourne') expect(e.rrule).toBeNull() expect(e.exdates).toEqual([]) }) it('preserves description with escaped commas', () => { const e = events[0] expect(e.description).toContain('ACF2400_CA_S2_ON-CAMPUS') expect(e.description).toContain('ACC INFO SYS') }) it('captures dtstamp in _raw', () => { expect(events[0]._raw.dtstamp).toBe('20260711T085008Z') }) it('events have UIDs', () => { expect(events[0].uid).toBe('uid0') expect(events[60].uid).toBe('uid60') }) }) describe('serializeICS round-trip', () => { it('round-trips: parse(serialize(parse(x))) equals parse(x)', () => { const original = parseICS(GOLDEN) const text = serializeICS(original) const reparsed = parseICS(text) expect(reparsed.events).toHaveLength(original.events.length) // Spot-check first event const e0 = reparsed.events[0] expect(e0.summary).toBe(original.events[0].summary) expect(e0.dtstartDate).toBe(original.events[0].dtstartDate) expect(e0.dtstartTime).toBe(original.events[0].dtstartTime) expect(e0.tzid).toBe(original.events[0].tzid) expect(e0._raw.dtstamp).toBe(original.events[0]._raw.dtstamp) }) it('preserves VTIMEZONE in output', () => { const { meta } = parseICS(serializeICS(parseICS(GOLDEN))) expect(meta.vtimezones).toHaveLength(1) }) it('serializes a manually-built recurring event', () => { const ev = { uid: 'test-1', summary: 'Test Event', location: 'Room A', description: 'Desc', dtstartDate: '2026-07-27', dtstartTime: '09:00', dtendTime: '10:30', tzid: 'UTC', rrule: { freq: 'WEEKLY', interval: 1, byday: 'MO', untilDate: '2026-10-19' }, exdates: ['2026-09-21'], _raw: { dtstamp: '20260711T085008Z' }, } const text = serializeICS({ meta: { prodid: '-//Test//EN', version: '2.0', calscale: 'GREGORIAN', method: null, xWrCalname: null, xWrTimezone: null, vtimezones: [], }, events: [ev], }) expect(text).toContain('BEGIN:VEVENT') expect(text).toContain('SUMMARY:Test Event') expect(text).toContain('RRULE:FREQ=WEEKLY') expect(text).toContain('BYDAY=MO') expect(text).toContain('EXDATE') expect(text).toContain('DTSTART:20260727T090000Z') // Re-parse and verify const reparsed = parseICS(text) expect(reparsed.events[0].rrule.freq).toBe('WEEKLY') expect(reparsed.events[0].rrule.byday).toBe('MO') expect(reparsed.events[0].exdates).toContain('2026-09-21') }) })