Implements CalDAV sync using github.com/emersion/go-webdav: - CalDAVService with background polling (configurable per-tenant interval) - Push: deadlines -> VTODO, appointments -> VEVENT on create/update/delete - Pull: periodic fetch from CalDAV, reconcile with local DB - Conflict resolution: KanzlAI wins dates/status, CalDAV wins notes/description - Conflicts logged as case_events with caldav_conflict type - UID pattern: kanzlai-{deadline|appointment}-{uuid}@kanzlai.msbls.de - CalDAV config per tenant in tenants.settings JSONB Endpoints: - POST /api/caldav/sync — trigger full sync for current tenant - GET /api/caldav/status — last sync time, item counts, errors 8 unit tests for UID generation, parsing, path construction, config parsing.
125 lines
3.8 KiB
Go
125 lines
3.8 KiB
Go
package services
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
func TestDeadlineUID(t *testing.T) {
|
|
id := uuid.MustParse("550e8400-e29b-41d4-a716-446655440000")
|
|
uid := deadlineUID(id)
|
|
want := "kanzlai-deadline-550e8400-e29b-41d4-a716-446655440000@kanzlai.msbls.de"
|
|
if uid != want {
|
|
t.Errorf("deadlineUID = %q, want %q", uid, want)
|
|
}
|
|
}
|
|
|
|
func TestAppointmentUID(t *testing.T) {
|
|
id := uuid.MustParse("550e8400-e29b-41d4-a716-446655440000")
|
|
uid := appointmentUID(id)
|
|
want := "kanzlai-appointment-550e8400-e29b-41d4-a716-446655440000@kanzlai.msbls.de"
|
|
if uid != want {
|
|
t.Errorf("appointmentUID = %q, want %q", uid, want)
|
|
}
|
|
}
|
|
|
|
func TestIsKanzlAIUID(t *testing.T) {
|
|
tests := []struct {
|
|
uid string
|
|
objectType string
|
|
want bool
|
|
}{
|
|
{"kanzlai-deadline-550e8400-e29b-41d4-a716-446655440000@kanzlai.msbls.de", "deadline", true},
|
|
{"kanzlai-appointment-550e8400-e29b-41d4-a716-446655440000@kanzlai.msbls.de", "appointment", true},
|
|
{"kanzlai-deadline-550e8400-e29b-41d4-a716-446655440000@kanzlai.msbls.de", "appointment", false},
|
|
{"random-uid@other.com", "deadline", false},
|
|
{"", "deadline", false},
|
|
}
|
|
for _, tt := range tests {
|
|
got := isKanzlAIUID(tt.uid, tt.objectType)
|
|
if got != tt.want {
|
|
t.Errorf("isKanzlAIUID(%q, %q) = %v, want %v", tt.uid, tt.objectType, got, tt.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestExtractIDFromUID(t *testing.T) {
|
|
id := uuid.MustParse("550e8400-e29b-41d4-a716-446655440000")
|
|
|
|
tests := []struct {
|
|
uid string
|
|
objectType string
|
|
want uuid.UUID
|
|
}{
|
|
{"kanzlai-deadline-550e8400-e29b-41d4-a716-446655440000@kanzlai.msbls.de", "deadline", id},
|
|
{"kanzlai-appointment-550e8400-e29b-41d4-a716-446655440000@kanzlai.msbls.de", "appointment", id},
|
|
{"invalid-uid", "deadline", uuid.Nil},
|
|
{"kanzlai-deadline-not-a-uuid@kanzlai.msbls.de", "deadline", uuid.Nil},
|
|
}
|
|
for _, tt := range tests {
|
|
got := extractIDFromUID(tt.uid, tt.objectType)
|
|
if got != tt.want {
|
|
t.Errorf("extractIDFromUID(%q, %q) = %v, want %v", tt.uid, tt.objectType, got, tt.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCalendarObjectPath(t *testing.T) {
|
|
tests := []struct {
|
|
calendarPath string
|
|
uid string
|
|
want string
|
|
}{
|
|
{"/dav/calendars/user/cal", "kanzlai-deadline-abc@kanzlai.msbls.de", "/dav/calendars/user/cal/kanzlai-deadline-abc@kanzlai.msbls.de.ics"},
|
|
{"/dav/calendars/user/cal/", "kanzlai-deadline-abc@kanzlai.msbls.de", "/dav/calendars/user/cal/kanzlai-deadline-abc@kanzlai.msbls.de.ics"},
|
|
}
|
|
for _, tt := range tests {
|
|
got := calendarObjectPath(tt.calendarPath, tt.uid)
|
|
if got != tt.want {
|
|
t.Errorf("calendarObjectPath(%q, %q) = %q, want %q", tt.calendarPath, tt.uid, got, tt.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestParseCalDAVConfig(t *testing.T) {
|
|
settings := []byte(`{"caldav": {"url": "https://dav.example.com", "username": "user", "password": "pass", "calendar_path": "/cal", "sync_enabled": true, "sync_interval_minutes": 30}}`)
|
|
cfg, err := parseCalDAVConfig(settings)
|
|
if err != nil {
|
|
t.Fatalf("parseCalDAVConfig: %v", err)
|
|
}
|
|
if cfg.URL != "https://dav.example.com" {
|
|
t.Errorf("URL = %q, want %q", cfg.URL, "https://dav.example.com")
|
|
}
|
|
if cfg.Username != "user" {
|
|
t.Errorf("Username = %q, want %q", cfg.Username, "user")
|
|
}
|
|
if cfg.SyncIntervalMinutes != 30 {
|
|
t.Errorf("SyncIntervalMinutes = %d, want 30", cfg.SyncIntervalMinutes)
|
|
}
|
|
if !cfg.SyncEnabled {
|
|
t.Error("SyncEnabled = false, want true")
|
|
}
|
|
}
|
|
|
|
func TestParseCalDAVConfig_Empty(t *testing.T) {
|
|
cfg, err := parseCalDAVConfig(nil)
|
|
if err != nil {
|
|
t.Fatalf("parseCalDAVConfig(nil): %v", err)
|
|
}
|
|
if cfg.URL != "" {
|
|
t.Errorf("expected empty config, got URL=%q", cfg.URL)
|
|
}
|
|
}
|
|
|
|
func TestParseCalDAVConfig_NoCalDAV(t *testing.T) {
|
|
settings := []byte(`{"other_setting": true}`)
|
|
cfg, err := parseCalDAVConfig(settings)
|
|
if err != nil {
|
|
t.Fatalf("parseCalDAVConfig: %v", err)
|
|
}
|
|
if cfg.URL != "" {
|
|
t.Errorf("expected empty caldav config, got URL=%q", cfg.URL)
|
|
}
|
|
}
|