package calendartest import ( "context" "net/http" "net/url" "strconv" "strings" "testing" calendar "amadan.net/oficina/calendar/internal/calendar" ) // BusyFor is "everything the host can see" (B2) reduced to UTC spans. // Driven through the real app: the host signs in, moves to Tokyo in // Settings, and creates events through the form, so what is asserted is // what the range query actually returns for the rows the form writes. func TestBusyForIsEverythingTheHostCanSee(t *testing.T) { app, session := signedIn(t, "host@example.com") h := app.Handler() ctx := context.Background() const tokyo = "Asia/Tokyo" rec := postAs(t, h, session, "/settings", url.Values{ "zone": {tokyo}, "week_start": {"1"}, "default_reminder_min": {"10"}, "default_all_day_min": {"360"}, }) if rec.Code != http.StatusSeeOther { t.Fatalf("saving settings: status %d\n%s", rec.Code, rec.Body.String()) } store := app.StoreForTest() host, ok, err := store.PersonByAddress(ctx, "host@example.com") if err != nil || !ok || host.Zone != tokyo { t.Fatalf("host after settings: %+v, %v, %v", host, ok, err) } // Every event is created in the host's zone, Tokyo, because that is // what the form does; the all-day ones carry no zone at all. mk := func(title, kind, status, start, end string, extra url.Values) int64 { t.Helper() form := url.Values{"title": {title}, "kind": {kind}, "status": {status}, "start": {start}, "end": {end}, "repeat": {"none"}} for k, v := range extra { form[k] = v } id, err := strconv.ParseInt(strings.TrimPrefix(createEvent(t, h, session, form), "/events/"), 10, 64) if err != nil { t.Fatalf("event id: %v", err) } return id } timed := mk("Standup", "meeting", "confirmed", testAt(1, "10:00"), testAt(1, "11:00"), nil) mk("Cancelled", "meeting", "cancelled", testAt(1, "12:00"), testAt(1, "13:00"), nil) mk("Bin day", "note", "confirmed", testAt(1, "14:00"), testAt(1, "15:00"), nil) held := mk("Hold", "meeting", "tentative", testAt(1, "16:00"), testAt(1, "17:00"), nil) mk("Away", "commitment", "confirmed", testDay(2), testDay(3), url.Values{"all_day": {"1"}}) mk("Retreat", "commitment", "confirmed", testDay(4), testDay(5), url.Values{ "all_day": {"1"}, "repeat": {"daily"}, "repeat_end": {testDay(6)}, }) from := utc(t, testDay(0), tokyo, true) to := utc(t, testDay(8), tokyo, true) got, err := store.BusyFor(ctx, host, from, to, nil) if err != nil { t.Fatalf("BusyFor: %v", err) } span := func(hhmm string) calendar.Busy { s := utc(t, testAt(1, hhmm), tokyo, false) return calendar.Busy{StartUTC: s, EndUTC: s + 3600} } day := func(n int) calendar.Busy { return calendar.Busy{StartUTC: utc(t, testDay(n), tokyo, true), EndUTC: utc(t, testDay(n+1), tokyo, true)} } if !has(got, span("10:00")) { t.Errorf("a confirmed timed meeting is not busy: %v", got) } if has(got, span("12:00")) { t.Error("a cancelled meeting is busy") } if has(got, span("14:00")) { t.Error("a note is busy — a birthday does not make the day fuller") } if !has(got, span("16:00")) { t.Error("a tentative event — a hold — is not busy") } // The empty-zone all-day event blocks the host's WHOLE Tokyo day, // not the UTC day its index columns name. if !has(got, day(2)) { t.Errorf("an all-day commitment does not block the host's Tokyo day: %v", got) } // The recurring one blocks each OCCURRENCE's date, not the master's. if !has(got, day(5)) || !has(got, day(6)) { t.Errorf("a daily all-day commitment does not block its later occurrences' days: %v", got) } // skip is the caller's exclusion: the event being rescheduled, and // holds that have lapsed but are not yet swept. got, err = store.BusyFor(ctx, host, from, to, map[int64]bool{timed: true, held: true}) if err != nil { t.Fatalf("BusyFor with skip: %v", err) } if has(got, span("10:00")) || has(got, span("16:00")) { t.Errorf("skipped events are still busy: %v", got) } if !has(got, day(2)) { t.Error("skipping two events removed an unrelated one") } } func utc(t *testing.T, local, zone string, allDay bool) int64 { t.Helper() u, err := calendar.ToUTC(local, zone, allDay) if err != nil { t.Fatalf("ToUTC(%q, %q): %v", local, zone, err) } return u } func has(busy []calendar.Busy, want calendar.Busy) bool { for _, b := range busy { if b == want { return true } } return false }