package main import ( "html" "io" "net/http" "net/http/httptest" "net/url" "regexp" "testing" "time" ) // formHxGetRe extracts the hx-get URL of a slot button pointing at /form. var formHxGetRe = regexp.MustCompile(`hx-get="([^"]*/form[^"]*)"`) // TestHandleGetSlots_OffsetSlotStartSurvivesURLParsing pins the bidbuddy.com.au // (Australia/Perth) production failure: Cal.com returns slot starts in the // event's own timezone with a numeric `+08:00` offset. That value is carried in // the slot button's hx-get query string; a raw `+` in a query string decodes to // a SPACE server-side, corrupting the timestamp ("2026-06-11T10:00:00 08:00"). // The corrupted value flows form → book → Cal.com, which rejects it with // "start must be a valid ISO 8601 date string". The slot's machine value must // therefore round-trip through URL query parsing as a valid RFC3339 timestamp. func TestHandleGetSlots_OffsetSlotStartSurvivesURLParsing(t *testing.T) { // 2026-06-11T10:00:00+08:00 (Perth) == 2026-06-11T02:00:00Z. fakeCalcom(t, "Australia/Perth", func(w http.ResponseWriter, _ *http.Request) { _, _ = io.WriteString(w, `{ "status":"success", "data":{"2026-06-11":[{"start":"2026-06-11T10:00:00+08:00"}]} }`) }) h, _ := newKeyedHandler(t) req := httptest.NewRequest(http.MethodGet, "/slots?username=alice&eventType=30min&date=2026-06-11&blockId=b1", nil) rec := httptest.NewRecorder() h.HandleGetSlots(rec, req) if rec.Code != http.StatusOK { t.Fatalf("expected 200, got %d (body: %s)", rec.Code, rec.Body.String()) } m := formHxGetRe.FindStringSubmatch(rec.Body.String()) if m == nil { t.Fatalf("no slot button hx-get to /form found in body: %q", rec.Body.String()) } // The browser reads the attribute via the DOM, which unescapes & → &. u, err := url.Parse(html.UnescapeString(m[1])) if err != nil { t.Fatalf("parse slot form URL %q: %v", m[1], err) } // u.Query() decodes exactly as net/http would on the wire — this is where a // raw `+` becomes a space. got := u.Query().Get("start") parsed, err := time.Parse(time.RFC3339, got) if err != nil { t.Fatalf("slot start %q is not a valid ISO 8601 string after URL parsing (Cal.com would reject it): %v", got, err) } if want := time.Date(2026, 6, 11, 2, 0, 0, 0, time.UTC); !parsed.Equal(want) { t.Errorf("slot start instant: got %s, want %s", parsed.UTC(), want) } }