65 lines
2.3 KiB
Go
65 lines
2.3 KiB
Go
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)
|
|
}
|
|
}
|