package calendartest import ( "net/http" "net/url" "strings" "testing" "time" ) // testDay and testAt anchor these tests to dates that are always in the // FUTURE, and that is the whole point of them. // // The dates here used to be written out literally. That made the suite // rot on a clock: a chip's own label is tense-dependent — chipFor prints // the start time for an event still to come and "ended " for one // already over (viewdata.go) — so an assertion that a day still shows // its 09:30 occurrence passed all morning and failed every afternoon, // and would have failed outright from the day after the literals were // written. It was found exactly that way, at 18:07 on the day the // literals named. // // The person these tests sign in as is in UTC, so UTC is the zone the // submitted wall time is read in. func testDay(n int) string { return time.Now().UTC().AddDate(0, 0, n).Format("2006-01-02") } func testAt(n int, hhmm string) string { return testDay(n) + "T" + hhmm } // Editing one occurrence of a series is where calendars go wrong most // visibly: the moved occurrence appears at its new time AND the // original slot still shows the unmoved one, or the moved one vanishes // entirely. // // The rule that prevents both is that an override is keyed by the // occurrence's ORIGINAL local time and rendered at its new one. func TestMovingOneOccurrenceLeavesTheRestAlone(t *testing.T) { app, session := signedIn(t, "someone@example.com") h := app.Handler() loc := createEvent(t, h, session, url.Values{ "title": {"Daily standup"}, "start": {testAt(1, "09:30")}, "end": {testAt(1, "09:45")}, "kind": {"meeting"}, "status": {"confirmed"}, "repeat": {"daily"}, }) id := strings.TrimPrefix(loc, "/events/") // Move ONLY the second occurrence, from 09:30 to 14:00. rec := postAs(t, h, session, "/events/"+id, url.Values{ "scope": {"this"}, "occurrence": {testAt(2, "09:30")}, "title": {"Daily standup"}, "calendar_id": {calendarIDOf(t, h, session)}, "start": {testAt(2, "14:00")}, "end": {testAt(2, "14:15")}, "kind": {"meeting"}, "status": {"confirmed"}, "repeat": {"none"}, }) if rec.Code != http.StatusSeeOther { t.Fatalf("moving one occurrence: status %d\n%s", rec.Code, rec.Body.String()) } // The moved day shows it once, at the new time, and not at the old. // // Assert against the CHIP's own time, not against the page: every // day carries 48 slot links whose accessible names contain every // half hour, so a bare Contains("09:30") matches on every day // whether or not anything is scheduled then. moved := getAs(t, h, session, "/day/"+testDay(2)).Body.String() if !hasChipAt(moved, "14:00") { t.Errorf("the moved occurrence is not at its new time:\n%s", moved) } if hasChipAt(moved, "09:30") { t.Error("the moved occurrence still appears at its ORIGINAL time — " + "the override is not claiming the slot the expander generates") } if n := strings.Count(moved, "Daily standup"); n != 1 { t.Errorf("the moved day shows the event %d times, want exactly 1", n) } // Every other day is untouched. for _, day := range []string{testDay(1), testDay(3)} { body := getAs(t, h, session, "/day/"+day).Body.String() if !hasChipAt(body, "09:30") { t.Errorf("%s lost its 09:30 occurrence when a different day was moved", day) } if n := strings.Count(body, "Daily standup"); n != 1 { t.Errorf("%s shows the event %d times, want exactly 1", day, n) } } } // hasChipAt reports whether a rendered page carries an event chip // starting at hhmm — as opposed to merely containing that time // somewhere, which every day does in its slot links. func hasChipAt(body, hhmm string) bool { return strings.Contains(body, ""+hhmm+"") } // Deleting one occurrence writes an EXDATE, which removes exactly that // day and nothing else. func TestDeletingOneOccurrenceRemovesOnlyThatDay(t *testing.T) { app, session := signedIn(t, "someone@example.com") h := app.Handler() loc := createEvent(t, h, session, url.Values{ "title": {"Daily standup"}, "start": {testAt(1, "09:30")}, "end": {testAt(1, "09:45")}, "kind": {"meeting"}, "status": {"confirmed"}, "repeat": {"daily"}, }) id := strings.TrimPrefix(loc, "/events/") rec := postAs(t, h, session, "/events/"+id+"/delete", url.Values{ "scope": {"this"}, "occurrence": {testAt(2, "09:30")}, }) if rec.Code != http.StatusSeeOther { t.Fatalf("deleting one occurrence: status %d", rec.Code) } if body := getAs(t, h, session, "/day/"+testDay(2)).Body.String(); strings.Contains(body, "Daily standup") { t.Error("the deleted occurrence is still on its day") } for _, day := range []string{testDay(1), testDay(3)} { if body := getAs(t, h, session, "/day/"+day).Body.String(); !strings.Contains(body, "Daily standup") { t.Errorf("%s lost its occurrence when a different day was deleted", day) } } } // "This one and everything after it" splits the series: the head keeps // its earlier occurrences unchanged, the tail starts from the edit. func TestSplittingASeriesLeavesTheHeadAlone(t *testing.T) { app, session := signedIn(t, "someone@example.com") h := app.Handler() loc := createEvent(t, h, session, url.Values{ "title": {"Standup"}, "start": {testAt(1, "09:30")}, "end": {testAt(1, "09:45")}, "kind": {"meeting"}, "status": {"confirmed"}, "repeat": {"daily"}, }) id := strings.TrimPrefix(loc, "/events/") rec := postAs(t, h, session, "/events/"+id, url.Values{ "scope": {"following"}, "occurrence": {testAt(9, "09:30")}, "title": {"Standup"}, "calendar_id": {calendarIDOf(t, h, session)}, "start": {testAt(9, "11:00")}, "end": {testAt(9, "11:15")}, "kind": {"meeting"}, "status": {"confirmed"}, "repeat": {"daily"}, }) if rec.Code != http.StatusSeeOther { t.Fatalf("splitting the series: status %d\n%s", rec.Code, rec.Body.String()) } // Before the split: still 09:30. before := getAs(t, h, session, "/day/"+testDay(4)).Body.String() if !hasChipAt(before, "09:30") { t.Errorf("an occurrence before the split lost its original time:\n%s", before) } // After: 11:00, and only once. after := getAs(t, h, session, "/day/"+testDay(10)).Body.String() if !hasChipAt(after, "11:00") { t.Errorf("an occurrence after the split is not at the new time:\n%s", after) } if n := strings.Count(after, "Standup"); n != 1 { t.Errorf("a day after the split shows the event %d times, want 1 — "+ "the head series was not ended where the tail begins", n) } } // calendarIDOf reads the calendar a new event would default to. func calendarIDOf(t *testing.T, h http.Handler, session *http.Cookie) string { t.Helper() page := getAs(t, h, session, "/events/new").Body.String() i := strings.Index(page, `