diff --git a/chart/Chart.yaml b/chart/Chart.yaml index fa44949..f9f0677 100644 --- a/chart/Chart.yaml +++ b/chart/Chart.yaml @@ -4,5 +4,5 @@ description: A Helm chart for Kubernetes type: application -version: 0.2.5 -appVersion: "v0.2.5" +version: 0.2.6 +appVersion: "v0.2.6" diff --git a/internal/availability/availability.go b/internal/availability/availability.go index 0a57af4..aa99164 100644 --- a/internal/availability/availability.go +++ b/internal/availability/availability.go @@ -241,7 +241,7 @@ func loadLocation(name string) *time.Location { func blockIsFree(events []calendar.ParsedEvent, start, end time.Time, loc *time.Location) bool { for _, event := range events { - if event.Cancelled { + if event.Cancelled || !event.Busy { continue } diff --git a/internal/availability/availability_test.go b/internal/availability/availability_test.go index 5f42bd4..571af50 100644 --- a/internal/availability/availability_test.go +++ b/internal/availability/availability_test.go @@ -193,6 +193,43 @@ END:VCALENDAR` } } +func TestCompute_IgnoresTransparentEvents(t *testing.T) { + blocks := testBlocks(t) + body := `BEGIN:VCALENDAR +VERSION:2.0 +X-WR-TIMEZONE:Europe/London +BEGIN:VEVENT +UID:free-morning +DTSTART:20260406T090000 +DTEND:20260406T103000 +SUMMARY:Free morning +TRANSP:TRANSPARENT +END:VEVENT +END:VCALENDAR` + + opts := ComputeOptions{ + WorkingHours: testWorkingHours(t), + HolidayDates: []string{"2026-04-06"}, + ExcludeEnglandBankHolidays: true, + Now: londonTime(t, 2026, 4, 6, 8, 30), + } + + entries, err := Compute(body, "Europe/London", blocks, opts) + if err != nil { + t.Fatalf("Compute: %v", err) + } + if len(entries) == 0 { + t.Fatal("expected availability entries") + } + // With the transparent event, the Morning block should still be available. + if got, want := entries[0].Date, "2026-04-06"; got != want { + t.Fatalf("first available date: got %q, want %q", got, want) + } + if got, want := entries[0].Block, "Morning"; got != want { + t.Fatalf("first available block: got %q, want %q", got, want) + } +} + func TestHandler_AuthorizationAndResponse(t *testing.T) { st := newTestStore(t) blocks := testBlocks(t) diff --git a/internal/calendar/ical.go b/internal/calendar/ical.go index d6f63f1..a19ce84 100644 --- a/internal/calendar/ical.go +++ b/internal/calendar/ical.go @@ -19,6 +19,7 @@ type ParsedEvent struct { StartTime time.Time EndTime time.Time Cancelled bool + Busy bool } // ParsedCalendar is the parsed representation of an iCal feed. @@ -97,6 +98,7 @@ func ParseICalendar(data []byte, windowStart, windowEnd time.Time) (*ParsedCalen startTime time.Time endTime time.Time cancelled bool + busy bool rrule string } @@ -115,6 +117,11 @@ func ParseICalendar(data []byte, windowStart, windowEnd time.Time) (*ParsedCalen status := event.GetProperty(ics.ComponentPropertyStatus) cancelled := status != nil && status.Value == "CANCELLED" + busy := true + if transp := event.GetProperty(ics.ComponentPropertyTransp); transp != nil { + busy = strings.ToUpper(transp.Value) == "OPAQUE" + } + startAt, err := event.GetStartAt() if err != nil { continue @@ -133,6 +140,7 @@ func ParseICalendar(data []byte, windowStart, windowEnd time.Time) (*ParsedCalen startTime: startAt, endTime: endAt, cancelled: cancelled, + busy: busy, } if rruleProp := event.GetProperty(ics.ComponentPropertyRrule); rruleProp != nil { base.rrule = rruleProp.Value @@ -151,6 +159,7 @@ func ParseICalendar(data []byte, windowStart, windowEnd time.Time) (*ParsedCalen StartTime: base.startTime, EndTime: base.endTime, Cancelled: base.cancelled, + Busy: base.busy, }) } continue @@ -174,6 +183,7 @@ func ParseICalendar(data []byte, windowStart, windowEnd time.Time) (*ParsedCalen StartTime: inst, EndTime: endAt, Cancelled: base.cancelled, + Busy: base.busy, }) } continue @@ -187,6 +197,7 @@ func ParseICalendar(data []byte, windowStart, windowEnd time.Time) (*ParsedCalen StartTime: base.startTime, EndTime: base.endTime, Cancelled: base.cancelled, + Busy: base.busy, }) } } diff --git a/internal/calendar/ical_test.go b/internal/calendar/ical_test.go index 19b36ff..126205e 100644 --- a/internal/calendar/ical_test.go +++ b/internal/calendar/ical_test.go @@ -378,3 +378,64 @@ END:VCALENDAR` t.Fatalf("timezone: got %q, want %q", tz, "Europe/London") } } + +func TestParseICalendar_Transparency(t *testing.T) { + icalData := `BEGIN:VCALENDAR +VERSION:2.0 +BEGIN:VEVENT +UID:busy-event +DTSTART:20260406T100000Z +DTEND:20260406T110000Z +SUMMARY:Busy Event +TRANSP:OPAQUE +END:VEVENT +BEGIN:VEVENT +UID:free-event +DTSTART:20260406T120000Z +DTEND:20260406T130000Z +SUMMARY:Free Event +TRANSP:TRANSPARENT +END:VEVENT +BEGIN:VEVENT +UID:default-busy-event +DTSTART:20260406T140000Z +DTEND:20260406T150000Z +SUMMARY:Default Busy Event +END:VEVENT +END:VCALENDAR` + + now := time.Date(2026, 4, 6, 10, 0, 0, 0, time.UTC) + events, err := parseICalendar([]byte(icalData), now) + if err != nil { + t.Fatalf("parseICalendar: %v", err) + } + + if len(events) != 3 { + t.Fatalf("expected 3 events, got %d", len(events)) + } + + tests := []struct { + summary string + busy bool + }{ + {"Busy Event", true}, + {"Free Event", false}, + {"Default Busy Event", true}, + } + + for _, tt := range tests { + found := false + for _, ev := range events { + if ev.Summary == tt.summary { + found = true + if ev.Busy != tt.busy { + t.Errorf("%s: Busy got %v, want %v", tt.summary, ev.Busy, tt.busy) + } + break + } + } + if !found { + t.Errorf("could not find event: %s", tt.summary) + } + } +}