wannpassts/backend/internal/calendar/ics_test.go
Tronax cb30223fd4
feat: add WannPassts booking app with Go backend and Vue frontend
Initial project setup for a privacy-focused, self-hosted scheduling app:

- Go backend with JWT auth, SQLite storage, and chi router
- Calendar integrations via Google OAuth (FreeBusy), CalDAV, and ICS links
- Public booking pages with configurable slots and booking requests
- AES-256-GCM encryption for stored provider tokens
- Vue 3 + TypeScript frontend with Vite, Pinia, and Vue Router
- Docs, .gitignore, and .env.example for local development
2026-08-25 18:50:28 +02:00

145 lines
3.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package calendar
import (
"testing"
"time"
)
const sampleICS = `BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Test//EN
BEGIN:VTIMEZONE
TZID:Europe/Berlin
BEGIN:STANDARD
DTSTART:19701025T030000
TZOFFSETFROM:+0200
TZOFFSETTO:+0100
END:STANDARD
END:VTIMEZONE
BEGIN:VEVENT
UID:1
DTSTART;TZID=Europe/Berlin:20260825T090000
DTEND;TZID=Europe/Berlin:20260825T100000
SUMMARY:Zahnarzt
END:VEVENT
BEGIN:VEVENT
UID:2
DTSTART:20260826T120000Z
DURATION:PT45M
SUMMARY:Call
END:VEVENT
BEGIN:VEVENT
UID:3
DTSTART;VALUE=DATE:20260827
SUMMARY:Ganztägig
END:VEVENT
BEGIN:VEVENT
UID:4
DTSTART:20260828T080000Z
DTEND:20260828T090000Z
STATUS:CANCELLED
SUMMARY:Abgesagt
END:VEVENT
BEGIN:VEVENT
UID:5
DTSTART:20260828T100000Z
DTEND:20260828T110000Z
TRANSP:TRANSPARENT
SUMMARY:Verfügbar
END:VEVENT
BEGIN:VEVENT
UID:6
DTSTART:20260829T080000Z
DTEND:202609
01T080000Z
SUMMARY:Gefaltet
END:VEVENT
END:VCALENDAR
`
func TestParseICS(t *testing.T) {
berlin, err := time.LoadLocation("Europe/Berlin")
if err != nil {
t.Fatal(err)
}
events := parseICS(sampleICS, berlin)
if len(events) != 6 {
t.Fatalf("6 Events erwartet, got %d", len(events))
}
// TZID-Event
if !events[0].start.Equal(time.Date(2026, 8, 25, 9, 0, 0, 0, berlin)) {
t.Errorf("TZID-Start falsch: %v", events[0].start)
}
if !events[0].end.Equal(time.Date(2026, 8, 25, 10, 0, 0, 0, berlin)) {
t.Errorf("TZID-Ende falsch: %v", events[0].end)
}
// UTC + DURATION
if !events[1].end.Equal(events[1].start.Add(45*time.Minute)) {
t.Errorf("DURATION nicht angewandt: %v", events[1].end)
}
// Ganztägig
if !events[2].allDay {
t.Error("VALUE=DATE nicht erkannt")
}
// Cancelled / Transparent markiert
if !events[3].cancelled {
t.Error("CANCELLED nicht erkannt")
}
if !events[4].transparent {
t.Error("TRANSPARENT nicht erkannt")
}
// Gefaltete Zeile (DTEND über zwei Zeilen)
if !events[5].end.Equal(time.Date(2026, 9, 1, 8, 0, 0, 0, time.UTC)) {
t.Errorf("Folding nicht aufgelöst, Ende: %v", events[5].end)
}
}
func TestParseISODuration(t *testing.T) {
cases := map[string]time.Duration{
"PT15M": 15 * time.Minute,
"PT1H30M": 90 * time.Minute,
"P1D": 24 * time.Hour,
"P2W": 14 * 24 * time.Hour,
"PT45S": 45 * time.Second,
"": 0,
"X": 0,
"P-1D": 0,
}
for in, want := range cases {
got, ok := parseISODuration(in)
if in == "" || in == "X" || in == "P-1D" {
if ok {
t.Errorf("%q sollte ungültig sein", in)
}
continue
}
if !ok || got != want {
t.Errorf("parseISODuration(%q) = %v (ok=%v), want %v", in, got, ok, want)
}
}
}
func TestClampIntervals(t *testing.T) {
from := time.Date(2026, 8, 25, 0, 0, 0, 0, time.UTC)
to := from.Add(48 * time.Hour)
in := []Interval{
{Start: from.Add(-2 * time.Hour), End: from.Add(time.Hour)},
{Start: from.Add(3 * time.Hour), End: from.Add(2 * time.Hour)}, // ungültig
{Start: from.Add(10 * time.Hour), End: to.Add(time.Hour)},
}
out := clampIntervals(in, from, to)
if len(out) != 2 {
t.Fatalf("2 Intervalle erwartet, got %d", len(out))
}
if !out[0].Start.Equal(from) || !out[0].End.Equal(from.Add(time.Hour)) {
t.Errorf("Intervall 1 nicht geklemmt: %v%v", out[0].Start, out[0].End)
}
if !out[1].End.Equal(to) {
t.Errorf("Intervall 2 nicht geklemmt: %v", out[1].End)
}
}