import assert from 'node:assert/strict' import { test } from 'node:test' import { buildIcs, parseIntervals } from '../dist/ics.js' const build = (raw, name = 'Belegt (Exchange)') => { const { intervals } = parseIntervals(raw) return buildIcs({ syncedAt: '2026-08-19T12:04:11.000Z', intervals }, name) } test('rechnet Ortszeit mit Offset korrekt nach UTC', () => { const { intervals } = parseIntervals('2026-08-19T14:00:00+02:00|2026-08-19T15:30:00+02:00') assert.equal(intervals.length, 1) assert.equal(intervals[0].start, '2026-08-19T12:00:00.000Z') assert.equal(intervals[0].end, '2026-08-19T13:30:00.000Z') }) test('normalisiert Offset ohne Doppelpunkt (+0200)', () => { const { intervals } = parseIntervals('2026-08-19T14:00:00+0200|2026-08-19T15:00:00+0200') assert.equal(intervals[0].start, '2026-08-19T12:00:00.000Z') }) test('behandelt Winterzeit und Sommerzeit unterschiedlich', () => { const winter = parseIntervals('2026-01-15T14:00:00+01:00|2026-01-15T15:00:00+01:00').intervals[0] const sommer = parseIntervals('2026-07-15T14:00:00+02:00|2026-07-15T15:00:00+02:00').intervals[0] assert.equal(winter.start, '2026-01-15T13:00:00.000Z') assert.equal(sommer.start, '2026-07-15T12:00:00.000Z') }) test('ganztaegiger Termin deckt den vollen lokalen Tag ab', () => { const { intervals } = parseIntervals('2026-08-19T00:00:00+02:00|2026-08-20T00:00:00+02:00') assert.equal(intervals[0].start, '2026-08-18T22:00:00.000Z') assert.equal(intervals[0].end, '2026-08-19T22:00:00.000Z') }) test('ueberspringt kaputte und sinnlose Zeilen, meldet sie zurueck', () => { const { intervals, skipped } = parseIntervals( [ '2026-08-19T14:00:00+02:00|2026-08-19T15:00:00+02:00', 'voelliger unsinn', '2026-08-19T14:00:00+02:00', '2026-08-19T15:00:00+02:00|2026-08-19T14:00:00+02:00', '', ].join('\n'), ) assert.equal(intervals.length, 1) assert.deepEqual(skipped, [ 'voelliger unsinn', '2026-08-19T14:00:00+02:00', '2026-08-19T15:00:00+02:00|2026-08-19T14:00:00+02:00', ]) }) test('erzeugt niemals TZID - das ist der Cal.com-Bug', () => { const ics = build('2026-08-19T14:00:00+02:00|2026-08-19T15:00:00+02:00') assert.ok(!ics.includes('TZID'), 'ICS darf kein TZID enthalten') assert.ok(!ics.includes('BEGIN:VTIMEZONE'), 'ICS darf keine VTIMEZONE enthalten') }) test('schreibt alle Zeitstempel als UTC mit Z', () => { const ics = build('2026-08-19T14:00:00+02:00|2026-08-19T15:00:00+02:00') const stamps = ics.match(/^(DTSTART|DTEND|DTSTAMP):.*$/gm) assert.equal(stamps.length, 3) for (const stamp of stamps) { assert.match(stamp, /:\d{8}T\d{6}Z$/, `nicht UTC: ${stamp}`) } assert.ok(ics.includes('DTSTART:20260819T120000Z')) assert.ok(ics.includes('DTEND:20260819T130000Z')) }) test('erzeugt kein RRULE', () => { const ics = build('2026-08-19T14:00:00+02:00|2026-08-19T15:00:00+02:00') assert.ok(!ics.includes('RRULE')) }) test('gibt ausschliesslich Belegt als Titel aus', () => { const ics = build('2026-08-19T14:00:00+02:00|2026-08-19T15:00:00+02:00') const summaries = ics.match(/^SUMMARY:.*$/gm) assert.deepEqual(summaries, ['SUMMARY:Belegt']) }) test('UID ist deterministisch ueber mehrere Laeufe', () => { const raw = '2026-08-19T14:00:00+02:00|2026-08-19T15:00:00+02:00' const first = build(raw).match(/^UID:.*$/m)[0] const second = build(raw).match(/^UID:.*$/m)[0] assert.equal(first, second) }) test('verwendet CRLF als Zeilenende', () => { const ics = build('2026-08-19T14:00:00+02:00|2026-08-19T15:00:00+02:00') assert.ok(ics.startsWith('BEGIN:VCALENDAR\r\n')) assert.ok(ics.endsWith('END:VCALENDAR\r\n')) assert.ok(!/[^\r]\n/.test(ics), 'jedes LF muss ein CR vorangehen') }) test('escaped Sonderzeichen im Kalendernamen', () => { const ics = build('', 'Belegt, extern; intern') const bs = String.fromCharCode(92) assert.ok(ics.includes(`X-WR-CALNAME:Belegt${bs}, extern${bs}; intern`)) }) test('faltet Zeilen laenger als 75 Oktette', () => { const ics = build('', 'A'.repeat(200)) for (const line of ics.split('\r\n')) { assert.ok(Buffer.byteLength(line, 'utf8') <= 75, `zu lang: ${line.length}`) } }) test('leerer Snapshot ergibt gueltigen, leeren Kalender', () => { const ics = buildIcs(null, 'Belegt (Exchange)') assert.ok(ics.includes('X-WR-CALDESC:Noch kein Sync empfangen')) assert.ok(!ics.includes('BEGIN:VEVENT')) assert.ok(ics.includes('END:VCALENDAR')) }) test('meldet den Sync-Zeitpunkt im CALDESC', () => { const ics = build('2026-08-19T14:00:00+02:00|2026-08-19T15:00:00+02:00') assert.ok(ics.includes('X-WR-CALDESC:Letzter Sync 2026-08-19T12:04:11.000Z')) })