Express-App mit JSON-Speicher unter VCH_DATA_DIR, Monatskalender mit Fotos, Personen mit Rollen und Teilzeit-Freitagen, Abwesenheiten, Feiertage je Bundesland, faire Zufallsauslosung mit Fixieren und Neu-Auslosen. Nextcloud: CalDAV-Discovery, Zielkalender wählbar, Termine je Werktag schreiben und aktualisieren, Abwesenheiten aus Kalender importieren, Personen samt Fotos aus dem Systemadressbuch übernehmen. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
52 lines
2.8 KiB
JavaScript
52 lines
2.8 KiB
JavaScript
import { test } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { buildEventLines, buildCalendar, escapeText, foldLine, eventUid } from '../server/ics.js';
|
|
|
|
test('escapeText maskiert Sonderzeichen', () => {
|
|
assert.equal(escapeText('a,b;c\\d\ne'), 'a\\,b\\;c\\\\d\\ne');
|
|
});
|
|
|
|
test('foldLine faltet lange Zeilen nach 75 Oktetten', () => {
|
|
const long = 'X'.repeat(200);
|
|
const folded = foldLine(long);
|
|
const lines = folded.split('\r\n');
|
|
assert.ok(lines.length > 1);
|
|
assert.ok(lines.every((l) => Buffer.byteLength(l) <= 75));
|
|
assert.ok(lines.slice(1).every((l) => l.startsWith(' ')));
|
|
assert.equal(lines.map((l, i) => (i ? l.slice(1) : l)).join(''), long);
|
|
// Umlaute werden nicht zerschnitten
|
|
const umlaut = foldLine('Ä'.repeat(100));
|
|
assert.equal(umlaut.split('\r\n').map((l, i) => (i ? l.slice(1) : l)).join(''), 'Ä'.repeat(100));
|
|
});
|
|
|
|
test('Termin mit Uhrzeit und Zeitzone', () => {
|
|
const lines = buildEventLines({ uid: 'u1', date: '2026-10-05', summary: 'Morning Meeting: A / B', time: '09:00', durationMinutes: 15, sequence: 2, now: new Date('2026-09-15T10:00:00Z') });
|
|
assert.ok(lines.includes('DTSTART;TZID=Europe/Berlin:20261005T090000'));
|
|
assert.ok(lines.includes('DTEND;TZID=Europe/Berlin:20261005T091500'));
|
|
assert.ok(lines.includes('SEQUENCE:2'));
|
|
assert.ok(lines.includes('DTSTAMP:20260915T100000Z'));
|
|
});
|
|
|
|
test('ganztägiger Termin ohne Uhrzeit', () => {
|
|
const lines = buildEventLines({ uid: 'u1', date: '2026-10-30', summary: 'x', time: '' });
|
|
assert.ok(lines.includes('DTSTART;VALUE=DATE:20261030'));
|
|
assert.ok(lines.includes('DTEND;VALUE=DATE:20261031'));
|
|
});
|
|
|
|
test('Kalender ist wohlgeformt, ohne METHOD für CalDAV, mit ORGANIZER vor Teilnehmern', () => {
|
|
const ev = buildEventLines({ uid: eventUid('2026-10-05'), date: '2026-10-05', summary: 'S', time: '09:00', attendees: [{ name: 'Max Mustermann', email: 'max@example.de' }], organizer: { name: 'Rotatory', email: 'team@example.de' } });
|
|
const ics = buildCalendar([ev]);
|
|
assert.ok(!ics.includes('METHOD:'), 'CalDAV-Objekte dürfen keine METHOD tragen');
|
|
assert.ok(buildCalendar([ev], { method: 'PUBLISH' }).includes('METHOD:PUBLISH'));
|
|
assert.ok(ics.includes('ORGANIZER;CN="Rotatory":mailto:team@example.de'));
|
|
assert.ok(ics.indexOf('ORGANIZER') < ics.indexOf('ATTENDEE'));
|
|
const noOrganizer = buildCalendar([buildEventLines({ uid: 'x', date: '2026-10-05', summary: 'S', attendees: [{ name: 'A', email: 'a@example.de' }] })]);
|
|
assert.ok(!noOrganizer.includes('ATTENDEE'), 'ohne ORGANIZER keine Teilnehmer');
|
|
assert.ok(ics.startsWith('BEGIN:VCALENDAR\r\n'));
|
|
assert.ok(ics.endsWith('END:VCALENDAR\r\n'));
|
|
assert.ok(ics.includes('BEGIN:VTIMEZONE'));
|
|
assert.ok(ics.includes('UID:rotatory-2026-10-05@rotatory.local'));
|
|
assert.ok(ics.includes('ATTENDEE;CN="Max Mustermann"'));
|
|
assert.ok(ics.split('\r\n').every((l) => Buffer.byteLength(l) <= 75));
|
|
});
|