oficina / calendar Public

Keep split recurrence exceptions with their original series occurrence

Paul Campbell pushed by paul@keymail.dev b9010e109e8900c55b7c5547898013a69dcf1a4e parent 7b0d4b7
8 files changed, +323 −35
  • internal/calendar/split_series_test.go +228 −0
  • internal/calendar/store_override.go +11 −10
  • internal/calendar/store_split_overrides.go +52 −0
  • internal/calendartest/booking_abuse_test.go +3 −3
  • internal/calendartest/booking_http_test.go +19 −12
  • internal/calendartest/booking_review2_test.go +3 −3
  • internal/calendartest/booking_review_test.go +4 −4
  • internal/calendartest/booking_sweep_test.go +3 −3

CI failed — run details

diff --git a/internal/calendar/split_series_test.go b/internal/calendar/split_series_test.go
new file mode 100644
index 0000000..fc11d2a
--- /dev/null
+++ b/internal/calendar/split_series_test.go
@@ -0,0 +1,228 @@
+package calendar
+
+import (
+ "bytes"
+ "testing"
+ "time"
+)
+
+func TestSplitSeriesKeepsOverridesWithOriginalOccurrence(t *testing.T) {
+ a := NewForTest(t, t.TempDir())
+ ctx := t.Context()
+ p, err := a.store.EnsurePersonSetup(ctx, "split@example.test")
+ if err != nil {
+ t.Fatal(err)
+ }
+ cal, err := a.store.DefaultCalendarID(ctx, p.ID)
+ if err != nil {
+ t.Fatal(err)
+ }
+ input := EventInput{CalendarID: cal, Title: "Daily", StartLocal: "2026-09-01T09:00", EndLocal: "2026-09-01T10:00", TZ: "Europe/Dublin", RRule: "FREQ=DAILY;COUNT=30", Kind: KindMeeting, Status: StatusConfirmed}
+ master, err := a.store.CreateEvent(ctx, p.ID, input)
+ if err != nil {
+ t.Fatal(err)
+ }
+ cases := []struct {
+ original, start, end string
+ tail, cancel bool
+ }{
+ {"2026-09-03T09:00", "2026-09-20T11:00", "2026-09-20T12:00", false, false},
+ {"2026-09-10T09:00", "2026-09-10T11:00", "2026-09-10T12:00", true, false},
+ {"2026-09-12T09:00", "2026-09-02T11:00", "2026-09-02T12:00", true, false},
+ {"2026-09-14T09:00", "2026-09-04T11:00", "2026-09-04T12:00", true, true},
+ }
+ originals := map[string]Event{}
+ for _, c := range cases {
+ moved := input
+ moved.RRule = ""
+ moved.StartLocal = c.start
+ moved.EndLocal = c.end
+ if err := a.store.UpdateOccurrence(ctx, p.ID, master.ID, c.original, moved); err != nil {
+ t.Fatal(err)
+ }
+ ov, ok, err := a.store.findOverride(ctx, master.ID, c.original)
+ if err != nil || !ok {
+ t.Fatal("missing override", err)
+ }
+ if c.cancel {
+ if err := a.store.DeleteEvent(ctx, p.ID, ov.ID); err != nil {
+ t.Fatal(err)
+ }
+ if err := a.db.G.First(&ov, ov.ID).Error; err != nil {
+ t.Fatal(err)
+ }
+ }
+ originals[c.original] = ov
+ }
+ tailInput := input
+ tailInput.Title = "Following"
+ tailInput.StartLocal = "2026-09-10T09:00"
+ tailInput.EndLocal = "2026-09-10T10:00"
+ if err := a.store.SplitSeries(ctx, p.ID+100, master.ID, "2026-09-10T09:00", tailInput); err == nil {
+ t.Fatal("foreign split accepted")
+ }
+ if err := a.store.SplitSeries(ctx, p.ID, master.ID, "2026-09-10T09:00", tailInput); err != nil {
+ t.Fatal(err)
+ }
+ var tail Event
+ if err := a.db.G.Where("calendar_id = ? AND id <> ? AND series_id IS NULL", cal, master.ID).First(&tail).Error; err != nil {
+ t.Fatal(err)
+ }
+ if tail.UIDHash == master.UIDHash || bytes.Equal(tail.UIDSealed, master.UIDSealed) {
+ t.Fatal("tail reused master identity")
+ }
+ for _, c := range cases {
+ var got Event
+ old := originals[c.original]
+ if err := a.db.G.First(&got, old.ID).Error; err != nil {
+ t.Fatal(err)
+ }
+ want := master
+ if c.tail {
+ want = tail
+ }
+ if got.SeriesID == nil || *got.SeriesID != want.ID || got.UIDHash != want.UIDHash || !bytes.Equal(got.UIDSealed, want.UIDSealed) {
+ t.Errorf("%s: exception belongs to wrong series or UID", c.original)
+ }
+ if got.RecurrenceIDLocal != c.original || got.StartLocal != c.start || got.EndLocal != c.end || (got.DeletedAt != nil) != c.cancel {
+ t.Errorf("%s: changed occurrence or cancellation", c.original)
+ }
+ if c.tail && (got.Sequence <= old.Sequence || got.UpdatedAt.Before(old.UpdatedAt)) {
+ t.Errorf("%s: export revision did not advance", c.original)
+ }
+ if !c.tail && (got.Sequence != old.Sequence || !got.UpdatedAt.Equal(old.UpdatedAt)) {
+ t.Errorf("%s: head exception changed", c.original)
+ }
+ }
+ from := time.Date(2026, 9, 1, 0, 0, 0, 0, time.UTC).Unix()
+ rows, err := a.store.EventsForFeed(ctx, p.ID, cal, from, from+40*86400)
+ if err != nil {
+ t.Fatal(err)
+ }
+ uids := map[int64]string{}
+ for _, row := range rows {
+ if row.SeriesID == nil {
+ uids[row.ID] = row.UIDSealedOpen
+ }
+ }
+ if len(uids) != 2 {
+ t.Fatalf("feed masters = %d", len(uids))
+ }
+ overrides := 0
+ for _, row := range rows {
+ if row.SeriesID != nil {
+ overrides++
+ if row.UIDSealedOpen == "" || row.UIDSealedOpen != uids[*row.SeriesID] {
+ t.Errorf("exported override %d has wrong UID", row.ID)
+ }
+ if row.ID == originals["2026-09-14T09:00"].ID && row.Status != StatusCancelled {
+ t.Fatal("cancelled exception resurrected in feed")
+ }
+ }
+ }
+ if overrides != len(cases) {
+ t.Fatalf("exported exceptions = %d", overrides)
+ }
+}
+
+func TestSplitSeriesOriginalBoundaryForAllDayAndDST(t *testing.T) {
+ for _, tc := range []struct {
+ name, zone, start, end, original, moved, movedEnd, cut, cutEnd string
+ allDay bool
+ }{
+ {"all-day", "", "2026-10-01", "2026-10-02", "2026-10-26", "2026-10-02", "2026-10-03", "2026-10-25", "2026-10-26", true},
+ {"dst-fold", "Europe/Dublin", "2026-10-01T01:30", "2026-10-01T02:30", "2026-10-25T01:30", "2026-10-02T11:00", "2026-10-02T12:00", "2026-10-25T01:30", "2026-10-25T02:30", false},
+ } {
+ t.Run(tc.name, func(t *testing.T) {
+ a := NewForTest(t, t.TempDir())
+ ctx := t.Context()
+ p, err := a.store.EnsurePersonSetup(ctx, "boundary@example.test")
+ if err != nil {
+ t.Fatal(err)
+ }
+ cal, err := a.store.DefaultCalendarID(ctx, p.ID)
+ if err != nil {
+ t.Fatal(err)
+ }
+ input := EventInput{CalendarID: cal, Title: "Recurring", StartLocal: tc.start, EndLocal: tc.end, TZ: tc.zone, AllDay: tc.allDay, RRule: "FREQ=DAILY;COUNT=40", Kind: KindMeeting, Status: StatusConfirmed}
+ master, err := a.store.CreateEvent(ctx, p.ID, input)
+ if err != nil {
+ t.Fatal(err)
+ }
+ moved := input
+ moved.RRule = ""
+ moved.StartLocal = tc.moved
+ moved.EndLocal = tc.movedEnd
+ if err := a.store.UpdateOccurrence(ctx, p.ID, master.ID, tc.original, moved); err != nil {
+ t.Fatal(err)
+ }
+ old, ok, err := a.store.findOverride(ctx, master.ID, tc.original)
+ if err != nil || !ok {
+ t.Fatal("missing override", err)
+ }
+ input.StartLocal = tc.cut
+ input.EndLocal = tc.cutEnd
+ if err := a.store.SplitSeries(ctx, p.ID, master.ID, tc.cut, input); err != nil {
+ t.Fatal(err)
+ }
+ var got Event
+ if err := a.db.G.First(&got, old.ID).Error; err != nil {
+ t.Fatal(err)
+ }
+ if got.SeriesID == nil || *got.SeriesID == master.ID || got.UIDHash == master.UIDHash || got.RecurrenceIDLocal != tc.original || got.StartUTC != old.StartUTC {
+ t.Fatal("original occurrence lost its tail or moved instant")
+ }
+ })
+ }
+}
+
+func TestSplitSeriesRollsBackIfOverrideCannotBeClassified(t *testing.T) {
+ a := NewForTest(t, t.TempDir())
+ ctx := t.Context()
+ p, err := a.store.EnsurePersonSetup(ctx, "rollback@example.test")
+ if err != nil {
+ t.Fatal(err)
+ }
+ cal, err := a.store.DefaultCalendarID(ctx, p.ID)
+ if err != nil {
+ t.Fatal(err)
+ }
+ in := EventInput{CalendarID: cal, Title: "Recurring", StartLocal: "2026-09-01T09:00", EndLocal: "2026-09-01T10:00", TZ: "UTC", RRule: "FREQ=DAILY;COUNT=30", Kind: KindMeeting, Status: StatusConfirmed}
+ master, err := a.store.CreateEvent(ctx, p.ID, in)
+ if err != nil {
+ t.Fatal(err)
+ }
+ moved := in
+ moved.RRule = ""
+ moved.StartLocal = "2026-09-12T11:00"
+ moved.EndLocal = "2026-09-12T12:00"
+ if err := a.store.UpdateOccurrence(ctx, p.ID, master.ID, "2026-09-12T09:00", moved); err != nil {
+ t.Fatal(err)
+ }
+ ov, ok, err := a.store.findOverride(ctx, master.ID, "2026-09-12T09:00")
+ if err != nil || !ok {
+ t.Fatal("missing override", err)
+ }
+ if err := a.db.G.Model(&Event{}).Where("id = ?", ov.ID).Update("recurrence_id_local", "invalid-legacy-value").Error; err != nil {
+ t.Fatal(err)
+ }
+ in.StartLocal = "2026-09-10T09:00"
+ in.EndLocal = "2026-09-10T10:00"
+ if err := a.store.SplitSeries(ctx, p.ID, master.ID, "2026-09-10T09:00", in); err == nil {
+ t.Fatal("unclassifiable exception silently reassigned")
+ }
+ current, err := a.store.Event(ctx, p.ID, master.ID)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if current.RRule != master.RRule || current.Sequence != master.Sequence {
+ t.Fatal("head changed despite rollback")
+ }
+ var count int64
+ if err := a.db.G.Model(&Event{}).Where("calendar_id = ?", cal).Count(&count).Error; err != nil {
+ t.Fatal(err)
+ }
+ if count != 2 {
+ t.Fatal("split left a partial tail")
+ }
+}
diff --git a/internal/calendar/store_override.go b/internal/calendar/store_override.go
index 5f84de2..f7133f0 100644
--- a/internal/calendar/store_override.go
+++ b/internal/calendar/store_override.go
@@ -160,14 +160,20 @@ func (s *Store) SplitSeries(ctx context.Context, personID, seriesID int64, origi
return s.g.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
headUntil, _ := RuleUntilUTC(headRule, master.StartLocal, master.TZ, master.AllDay)
- if err := tx.Model(&Event{}).Where("id = ?", seriesID).
+ head := splitWritableEvents(tx, personID, master.CalendarID).
+ Where("id = ? AND deleted_at IS NULL AND sequence = ?", seriesID, master.Sequence).
+ Select("rrule", "until_utc", "sequence", "updated_at").
Updates(map[string]any{
"rrule": headRule,
"until_utc": headUntil,
- "sequence": master.Sequence + 1,
+ "sequence": gorm.Expr("sequence + 1"),
"updated_at": time.Now().UTC(),
- }).Error; err != nil {
- return fmt.Errorf("calendar: end the head series: %w", err)
+ })
+ if head.Error != nil {
+ return fmt.Errorf("calendar: end the head series: %w", head.Error)
+ }
+ if head.RowsAffected != 1 {
+ return ErrNotFound
}
// The tail is a new series with the master's rule, starting at
@@ -186,12 +192,7 @@ func (s *Store) SplitSeries(ctx context.Context, personID, seriesID int64, origi
return fmt.Errorf("calendar: create the tail series: %w", err)
}
- // Overrides after the cut belong to the tail, not the head.
- // Left on the head they would be orphans: rows nothing renders
- // and nothing can reach.
- if err := tx.Model(&Event{}).
- Where("series_id = ? AND start_utc >= ?", seriesID, cut.Unix()).
- Update("series_id", ev.ID).Error; err != nil {
+ if err := moveSplitOverrides(tx, personID, master.Event, ev, cut); err != nil {
return err
}
return s.rescheduleAllForTx(tx, seriesID)
diff --git a/internal/calendar/store_split_overrides.go b/internal/calendar/store_split_overrides.go
new file mode 100644
index 0000000..d13658c
--- /dev/null
+++ b/internal/calendar/store_split_overrides.go
@@ -0,0 +1,52 @@
+package calendar
+
+import (
+ "fmt"
+ "time"
+
+ "amadan.net/rastrillo/rastrillo/scope"
+ "gorm.io/gorm"
+)
+
+// An exception belongs to the series containing its original occurrence,
+// even when someone moved it across the split date. Cancelled exceptions
+// move too, so splitting cannot restore a deleted occurrence.
+func moveSplitOverrides(tx *gorm.DB, personID int64, master, tail Event, cut time.Time) error {
+ writable := func() *gorm.DB {
+ return splitWritableEvents(tx, personID, master.CalendarID).Where("series_id = ?", master.ID)
+ }
+ var overrides []Event
+ if err := writable().Find(&overrides).Error; err != nil {
+ return err
+ }
+ loc, err := Zone(master.TZ)
+ if err != nil {
+ return err
+ }
+ now := time.Now().UTC()
+ for _, override := range overrides {
+ original, _, err := ParseLocal(override.RecurrenceIDLocal, loc)
+ if err != nil {
+ return fmt.Errorf("calendar: read split occurrence: %w", err)
+ }
+ if original.Before(cut) {
+ continue
+ }
+ updates := map[string]any{"series_id": tail.ID, "uid_hash": tail.UIDHash, "uid_sealed": tail.UIDSealed, "sequence": gorm.Expr("sequence + 1"), "updated_at": now}
+ result := writable().Where("id = ?", override.ID).
+ Select("series_id", "uid_hash", "uid_sealed", "sequence", "updated_at").Updates(updates)
+ if result.Error != nil {
+ return result.Error
+ }
+ if result.RowsAffected != 1 {
+ return ErrNotFound
+ }
+ }
+ return nil
+}
+
+func splitWritableEvents(tx *gorm.DB, personID, calendarID int64) *gorm.DB {
+ calendars := scope.OwnedBy(tx.Model(&Grant{}), "person_id", personID).
+ Select("calendar_id").Where("role IN ?", []string{RoleOwner, RoleEditor})
+ return tx.Model(&Event{}).Where("calendar_id = ? AND calendar_id IN (?)", calendarID, calendars)
+}
diff --git a/internal/calendartest/booking_abuse_test.go b/internal/calendartest/booking_abuse_test.go
index 3a58026..1b7a34f 100644
--- a/internal/calendartest/booking_abuse_test.go
+++ b/internal/calendartest/booking_abuse_test.go
@@ -29,7 +29,7 @@ func fromIP(t *testing.T, h http.Handler, ip, target string, form url.Values) *h
func TestTheSixthOpenHoldFromOneIPIs429(t *testing.T) {
app, _, path, _ := publishedPage(t)
h := app.Handler()
- _, starts := firstDaySlots(t, h, path)
+ _, starts := futureDaySlots(t, h, path)
for i := 0; i < 5; i++ {
rec := fromIP(t, h, "203.0.113.9", path, holdForm(starts[i], "S", "s"+string(rune('a'+i))+"@example.com"))
if rec.Code != 200 {
@@ -49,7 +49,7 @@ func TestTheSixthOpenHoldFromOneIPIs429(t *testing.T) {
func TestTheThirdOpenHoldForOneAddressIs429(t *testing.T) {
app, _, path, _ := publishedPage(t)
h := app.Handler()
- _, starts := firstDaySlots(t, h, path)
+ _, starts := futureDaySlots(t, h, path)
for i := 0; i < 2; i++ {
fromIP(t, h, "203.0.113."+string(rune('1'+i)), path, holdForm(starts[i], "S", "same@example.com"))
}
@@ -93,7 +93,7 @@ func TestADayOutsideTheHorizonIs404AndAStrangeZoneFallsBack(t *testing.T) {
func TestAnOversizedFormIs422(t *testing.T) {
app, _, path, _ := publishedPage(t)
h := app.Handler()
- _, starts := firstDaySlots(t, h, path)
+ _, starts := futureDaySlots(t, h, path)
form := holdForm(starts[0], "A", "a@example.com")
form.Set("note", strings.Repeat("x", 20<<10))
if rec := strangerPost(t, h, path, form); rec.Code != http.StatusUnprocessableEntity {
diff --git a/internal/calendartest/booking_http_test.go b/internal/calendartest/booking_http_test.go
index 6d37680..9328a6a 100644
--- a/internal/calendartest/booking_http_test.go
+++ b/internal/calendartest/booking_http_test.go
@@ -67,19 +67,26 @@ var (
reDay = regexp.MustCompile(`\?day=(\d{4}-\d{2}-\d{2})`)
)
-// firstDaySlots opens the page, follows the first day link, and returns
-// that day's path and its slot starts.
-func firstDaySlots(t *testing.T, h http.Handler, path string) (string, []string) {
+// futureDaySlots uses a whole future day so a late test run still has
+// enough slots for the hold-limit and rescheduling tests.
+func futureDaySlots(t *testing.T, h http.Handler, path string) (string, []string) {
t.Helper()
page := strangerGet(t, h, path)
if page.Code != 200 {
t.Fatalf("page: %d\n%s", page.Code, page.Body.String())
}
- m := reDay.FindStringSubmatch(page.Body.String())
- if m == nil {
- t.Fatalf("no day links:\n%s", page.Body.String())
+ today := time.Now().UTC().Format("2006-01-02")
+ selected := ""
+ for _, match := range reDay.FindAllStringSubmatch(page.Body.String(), -1) {
+ if match[1] > today {
+ selected = match[1]
+ break
+ }
+ }
+ if selected == "" {
+ t.Fatalf("no future day links:\n%s", page.Body.String())
}
- dayPath := path + "?day=" + m[1] + "&zone=UTC"
+ dayPath := path + "?day=" + selected + "&zone=UTC"
day := strangerGet(t, h, dayPath)
var starts []string
for _, s := range reStart.FindAllStringSubmatch(day.Body.String(), -1) {
@@ -98,7 +105,7 @@ func holdForm(start, name, address string) url.Values {
func TestAStrangerHoldsAndOnlyTheButtonConfirms(t *testing.T) {
app, mailer, path, _ := publishedPage(t)
h := app.Handler()
- _, starts := firstDaySlots(t, h, path)
+ _, starts := futureDaySlots(t, h, path)
// Step one: the slot pick, no name yet.
pick := strangerPost(t, h, path, url.Values{"start": {starts[0]}, "zone": {"UTC"}})
@@ -147,7 +154,7 @@ func TestTheICSIsARequestWithNoManagementLinkAndNoHostPrivateFields(t *testing.T
h := app.Handler()
st := app.StoreForTest()
ctx := context.Background()
- _, starts := firstDaySlots(t, h, path)
+ _, starts := futureDaySlots(t, h, path)
strangerPost(t, h, path, holdForm(starts[0], "Doe, Jane \"JJ\": Support", "doc@tito.io"))
link := reManage.FindString(mailer.body(0))
strangerPost(t, h, link+"/confirm", nil)
@@ -198,7 +205,7 @@ func TestTheICSIsARequestWithNoManagementLinkAndNoHostPrivateFields(t *testing.T
func TestTheSameSlotCannotBeHeldTwiceAndAWrongTokenIsTheRouters404(t *testing.T) {
app, _, path, _ := publishedPage(t)
h := app.Handler()
- _, starts := firstDaySlots(t, h, path)
+ _, starts := futureDaySlots(t, h, path)
strangerPost(t, h, path, holdForm(starts[0], "A", "a@example.com"))
rec := strangerPost(t, h, path, holdForm(starts[0], "B", "b@example.com"))
if rec.Code != http.StatusConflict || !strings.Contains(rec.Body.String(), "just gone") {
@@ -217,7 +224,7 @@ func TestTheSameSlotCannotBeHeldTwiceAndAWrongTokenIsTheRouters404(t *testing.T)
func TestRescheduleNeedsTheRevisionAndCancelTellsBoth(t *testing.T) {
app, mailer, path, _ := publishedPage(t)
h := app.Handler()
- _, starts := firstDaySlots(t, h, path)
+ _, starts := futureDaySlots(t, h, path)
strangerPost(t, h, path, holdForm(starts[0], "Doc", "doc@tito.io"))
link := reManage.FindString(mailer.body(0))
strangerPost(t, h, link+"/confirm", nil)
@@ -267,7 +274,7 @@ func TestAHoldWhoseMailIsRefusedStillStands(t *testing.T) {
app, mailer, path, _ := publishedPage(t)
mailer.refuse = "bounce@example.com"
h := app.Handler()
- _, starts := firstDaySlots(t, h, path)
+ _, starts := futureDaySlots(t, h, path)
rec := strangerPost(t, h, path, holdForm(starts[0], "B", "bounce@example.com"))
if rec.Code != 200 || !strings.Contains(rec.Body.String(), "If nothing arrives") {
t.Errorf("refused mail: %d\n%s", rec.Code, rec.Body.String())
diff --git a/internal/calendartest/booking_review2_test.go b/internal/calendartest/booking_review2_test.go
index 44babe8..19d3fd9 100644
--- a/internal/calendartest/booking_review2_test.go
+++ b/internal/calendartest/booking_review2_test.go
@@ -23,7 +23,7 @@ import (
func TestAMultilineNoteFromABrowserIsAccepted(t *testing.T) {
app, _, path, _ := publishedPage(t)
h := app.Handler()
- _, starts := firstDaySlots(t, h, path)
+ _, starts := futureDaySlots(t, h, path)
form := holdForm(starts[0], "Doc", "doc@tito.io")
form.Set("note", "First paragraph.\r\n\r\nSecond paragraph.")
if rec := strangerPost(t, h, path, form); rec.Code != http.StatusOK {
@@ -178,7 +178,7 @@ func TestADeferredHoldMailIsNotReportedSent(t *testing.T) {
"availability": {"every day 0-24"}, "duration_min": {"30"}, "buffer_min": {"0"}, "notice_min": {"0"}, "horizon_days": {"30"}})
postAs(t, h, session, "/settings/booking/1/publish", url.Values{"publish": {"1"}})
path := strings.TrimSpace(rePublicURL.FindString(getAs(t, h, session, "/settings/booking").Body.String()))
- _, starts := firstDaySlots(t, h, path)
+ _, starts := futureDaySlots(t, h, path)
if len(starts) < 5 {
t.Fatalf("only %d slots on the first day", len(starts))
}
@@ -251,7 +251,7 @@ func TestTheReschedulePickerOnAClosedPageSaysSo(t *testing.T) {
h := app.Handler()
session := app.SignInForTest(t, "host@example.com")
mailer.reset() // the sign-in link is not the mail under test
- _, starts := firstDaySlots(t, h, path)
+ _, starts := futureDaySlots(t, h, path)
strangerPost(t, h, path, holdForm(starts[0], "Doc", "doc@tito.io"))
link := reManage.FindString(mailer.body(0))
strangerPost(t, h, link+"/confirm", nil)
diff --git a/internal/calendartest/booking_review_test.go b/internal/calendartest/booking_review_test.go
index 5b04513..fd261e1 100644
--- a/internal/calendartest/booking_review_test.go
+++ b/internal/calendartest/booking_review_test.go
@@ -19,7 +19,7 @@ import (
func TestANameWithALineBreakIsRefused(t *testing.T) {
app, _, path, _ := publishedPage(t)
h := app.Handler()
- _, starts := firstDaySlots(t, h, path)
+ _, starts := futureDaySlots(t, h, path)
rec := strangerPost(t, h, path, holdForm(starts[0], "Jane\nPrivate", "j@example.com"))
if rec.Code != http.StatusUnprocessableEntity || !strings.Contains(rec.Body.String(), "one line") {
t.Errorf("a name with a newline: %d", rec.Code)
@@ -31,7 +31,7 @@ func TestANameWithALineBreakIsRefused(t *testing.T) {
func TestTheHostsConfirmationCarriesTheNote(t *testing.T) {
app, mailer, path, _ := publishedPage(t)
h := app.Handler()
- _, starts := firstDaySlots(t, h, path)
+ _, starts := futureDaySlots(t, h, path)
form := holdForm(starts[0], "Doc", "doc@tito.io")
form.Set("note", "Bring the folder")
strangerPost(t, h, path, form)
@@ -47,7 +47,7 @@ func TestTheHostsConfirmationCarriesTheNote(t *testing.T) {
func TestTheReschedulePickerShowsTheSnapshot(t *testing.T) {
app, mailer, path, host := publishedPage(t)
h := app.Handler()
- _, starts := firstDaySlots(t, h, path)
+ _, starts := futureDaySlots(t, h, path)
strangerPost(t, h, path, holdForm(starts[0], "Doc", "doc@tito.io"))
link := reManage.FindString(mailer.body(0))
strangerPost(t, h, link+"/confirm", nil)
@@ -71,7 +71,7 @@ func TestAnOwedHoldMailIsRetiredOnceConfirmed(t *testing.T) {
t.Setenv("CARLOS_ADMIN_TOKEN", "test-token")
app, mailer, path, _ := publishedPage(t)
h := app.Handler()
- _, starts := firstDaySlots(t, h, path)
+ _, starts := futureDaySlots(t, h, path)
mailer.refuse = "doc@tito.io"
strangerPost(t, h, path, holdForm(starts[0], "Doc", "doc@tito.io"))
mailer.refuse = ""
diff --git a/internal/calendartest/booking_sweep_test.go b/internal/calendartest/booking_sweep_test.go
index b638e97..e2dae2f 100644
--- a/internal/calendartest/booking_sweep_test.go
+++ b/internal/calendartest/booking_sweep_test.go
@@ -23,7 +23,7 @@ func TestTheTickExpiresHoldsAndWakesForThem(t *testing.T) {
t.Setenv("CARLOS_ADMIN_TOKEN", "test-token")
app, _, path, _ := publishedPage(t)
h := app.Handler()
- _, starts := firstDaySlots(t, h, path)
+ _, starts := futureDaySlots(t, h, path)
strangerPost(t, h, path, holdForm(starts[0], "A", "a@example.com"))
if _, err := app.DBForTest().Exec(`UPDATE bookings SET hold_expires_at = ?`, time.Now().Add(-time.Minute).Unix()); err != nil {
t.Fatal(err)
@@ -43,7 +43,7 @@ func TestTheTickRetriesOwedMailAndDrainOnRequestDoesOneThing(t *testing.T) {
t.Setenv("CARLOS_ADMIN_TOKEN", "test-token")
app, mailer, path, _ := publishedPage(t)
h := app.Handler()
- _, starts := firstDaySlots(t, h, path)
+ _, starts := futureDaySlots(t, h, path)
strangerPost(t, h, path, holdForm(starts[0], "Doc", "doc@tito.io"))
link := reManage.FindString(mailer.body(0))
// The host's mailbox bounces for now.
@@ -104,7 +104,7 @@ func TestNothingSealedReachesTheLog(t *testing.T) {
"duration_min": {"30"}, "buffer_min": {"0"}, "notice_min": {"0"}, "horizon_days": {"30"}})
postAs(t, h, session, "/settings/booking/1/publish", url.Values{"publish": {"1"}})
path := regexp.MustCompile(`/book/paul/[a-z0-9]{8}`).FindString(getAs(t, h, session, "/settings/booking").Body.String())
- _, starts := firstDaySlots(t, h, path)
+ _, starts := futureDaySlots(t, h, path)
strangerPost(t, h, path, holdForm(starts[0], "Dermot Lacey", "dermot@elsewhere.example"))
log := buf.String()