oficina / calendar Public

Clone
git clone https://amadan.net/oficina/calendar

Plain git — no account needed to clone.

Download

Download this file

1package calendartest
2
3import (
4 "context"
5 "net/http"
6 "net/url"
7 "strconv"
8 "strings"
9 "testing"
10
11 calendar "amadan.net/oficina/calendar/internal/calendar"
12)
13
14// BusyFor is "everything the host can see" (B2) reduced to UTC spans.
15// Driven through the real app: the host signs in, moves to Tokyo in
16// Settings, and creates events through the form, so what is asserted is
17// what the range query actually returns for the rows the form writes.
18func TestBusyForIsEverythingTheHostCanSee(t *testing.T) {
19 app, session := signedIn(t, "host@example.com")
20 h := app.Handler()
21 ctx := context.Background()
22 const tokyo = "Asia/Tokyo"
23
24 rec := postAs(t, h, session, "/settings", url.Values{
25 "zone": {tokyo}, "week_start": {"1"}, "default_reminder_min": {"10"}, "default_all_day_min": {"360"},
26 })
27 if rec.Code != http.StatusSeeOther {
28 t.Fatalf("saving settings: status %d\n%s", rec.Code, rec.Body.String())
29 }
30 store := app.StoreForTest()
31 host, ok, err := store.PersonByAddress(ctx, "host@example.com")
32 if err != nil || !ok || host.Zone != tokyo {
33 t.Fatalf("host after settings: %+v, %v, %v", host, ok, err)
34 }
35
36 // Every event is created in the host's zone, Tokyo, because that is
37 // what the form does; the all-day ones carry no zone at all.
38 mk := func(title, kind, status, start, end string, extra url.Values) int64 {
39 t.Helper()
40 form := url.Values{"title": {title}, "kind": {kind}, "status": {status}, "start": {start}, "end": {end}, "repeat": {"none"}}
41 for k, v := range extra {
42 form[k] = v
43 }
44 id, err := strconv.ParseInt(strings.TrimPrefix(createEvent(t, h, session, form), "/events/"), 10, 64)
45 if err != nil {
46 t.Fatalf("event id: %v", err)
47 }
48 return id
49 }
50 timed := mk("Standup", "meeting", "confirmed", testAt(1, "10:00"), testAt(1, "11:00"), nil)
51 mk("Cancelled", "meeting", "cancelled", testAt(1, "12:00"), testAt(1, "13:00"), nil)
52 mk("Bin day", "note", "confirmed", testAt(1, "14:00"), testAt(1, "15:00"), nil)
53 held := mk("Hold", "meeting", "tentative", testAt(1, "16:00"), testAt(1, "17:00"), nil)
54 mk("Away", "commitment", "confirmed", testDay(2), testDay(3), url.Values{"all_day": {"1"}})
55 mk("Retreat", "commitment", "confirmed", testDay(4), testDay(5), url.Values{
56 "all_day": {"1"}, "repeat": {"daily"}, "repeat_end": {testDay(6)},
57 })
58
59 from := utc(t, testDay(0), tokyo, true)
60 to := utc(t, testDay(8), tokyo, true)
61 got, err := store.BusyFor(ctx, host, from, to, nil)
62 if err != nil {
63 t.Fatalf("BusyFor: %v", err)
64 }
65 span := func(hhmm string) calendar.Busy {
66 s := utc(t, testAt(1, hhmm), tokyo, false)
67 return calendar.Busy{StartUTC: s, EndUTC: s + 3600}
68 }
69 day := func(n int) calendar.Busy {
70 return calendar.Busy{StartUTC: utc(t, testDay(n), tokyo, true), EndUTC: utc(t, testDay(n+1), tokyo, true)}
71 }
72
73 if !has(got, span("10:00")) {
74 t.Errorf("a confirmed timed meeting is not busy: %v", got)
75 }
76 if has(got, span("12:00")) {
77 t.Error("a cancelled meeting is busy")
78 }
79 if has(got, span("14:00")) {
80 t.Error("a note is busy — a birthday does not make the day fuller")
81 }
82 if !has(got, span("16:00")) {
83 t.Error("a tentative event — a hold — is not busy")
84 }
85 // The empty-zone all-day event blocks the host's WHOLE Tokyo day,
86 // not the UTC day its index columns name.
87 if !has(got, day(2)) {
88 t.Errorf("an all-day commitment does not block the host's Tokyo day: %v", got)
89 }
90 // The recurring one blocks each OCCURRENCE's date, not the master's.
91 if !has(got, day(5)) || !has(got, day(6)) {
92 t.Errorf("a daily all-day commitment does not block its later occurrences' days: %v", got)
93 }
94
95 // skip is the caller's exclusion: the event being rescheduled, and
96 // holds that have lapsed but are not yet swept.
97 got, err = store.BusyFor(ctx, host, from, to, map[int64]bool{timed: true, held: true})
98 if err != nil {
99 t.Fatalf("BusyFor with skip: %v", err)
100 }
101 if has(got, span("10:00")) || has(got, span("16:00")) {
102 t.Errorf("skipped events are still busy: %v", got)
103 }
104 if !has(got, day(2)) {
105 t.Error("skipping two events removed an unrelated one")
106 }
107}
108
109func utc(t *testing.T, local, zone string, allDay bool) int64 {
110 t.Helper()
111 u, err := calendar.ToUTC(local, zone, allDay)
112 if err != nil {
113 t.Fatalf("ToUTC(%q, %q): %v", local, zone, err)
114 }
115 return u
116}
117
118func has(busy []calendar.Busy, want calendar.Busy) bool {
119 for _, b := range busy {
120 if b == want {
121 return true
122 }
123 }
124 return false
125}
126