busyfeed: Initial commit mit Tag-Deployment
Bestehende App plus Gitea-Actions-Pipeline: Tag v* loest Build, Tests und Container-Neustart auf dem VPS aus. - .gitea/workflows/deploy.yml: npm ci/build/test als Gate, danach docker compose up -d --build. Die .env wird aus dem Repo-Secret DOTENV erzeugt, damit auf dem Host keine Secret-Datei gepflegt werden muss. - docker-compose.yml: fester Projektname (der CI-Job hat ein anderes Arbeitsverzeichnis als der Host), Image mit Versionstag fuer Rollback, fester Volume-Name. - README: Deployment- und Rollback-Abschnitt, Token-Rotation angepasst. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
118
test/ics.test.js
Normal file
118
test/ics.test.js
Normal file
@@ -0,0 +1,118 @@
|
||||
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'))
|
||||
})
|
||||
Reference in New Issue
Block a user