package main import ( "errors" "fmt" "testing" ) // TestClassifyBookingError_GroundedTaxonomy locks in the mapping against the // REAL Cal.com error bodies captured by probing the live /book endpoint // (2026-06-21). Each case feeds a *CalcomAPIError built from an actual response // and asserts the curated visitor copy + retry affordance. The raw Cal.com // message must never be the rendered string. func TestClassifyBookingError_GroundedTaxonomy(t *testing.T) { const adminMsg = "Call us on 1234." cases := []struct { name string err error wantMsg string wantRtry bool }{ { // Live: status 400, BadRequestException: "Attempting to book a meeting in the past." name: "past instant", err: &CalcomAPIError{Endpoint: "booking", Status: 400, Code: "BadRequestException", Message: "Attempting to book a meeting in the past."}, wantMsg: msgSlotUnavailable, wantRtry: true, }, { // Live: status 400, BadRequestException: "User either already has booking at this time or is not available" name: "slot taken / host unavailable", err: &CalcomAPIError{Endpoint: "booking", Status: 400, Code: "BadRequestException", Message: "User either already has booking at this time or is not available"}, wantMsg: msgSlotUnavailable, wantRtry: true, }, { // Live: status 400, BAD_REQUEST: "responses - {guests}email_validation_error, " name: "invalid guest email", err: &CalcomAPIError{Endpoint: "booking", Status: 400, Code: "BAD_REQUEST", Message: "responses - {guests}email_validation_error, "}, wantMsg: msgInvalidGuestEmail, wantRtry: false, }, { // Cal.com phone rejection: "{attendeePhoneNumber} invalid_number". name: "invalid phone number", err: &CalcomAPIError{Endpoint: "booking", Status: 400, Code: "BAD_REQUEST", Message: "responses - {attendeePhoneNumber}invalid_number"}, wantMsg: msgInvalidPhone, wantRtry: false, }, { // A rejected custom-field answer (a required field / generic responses error // that names no guest/phone signal). name: "rejected custom field answer", err: &CalcomAPIError{Endpoint: "booking", Status: 400, Code: "BAD_REQUEST", Message: "responses - {favourite-colour} is required"}, wantMsg: msgRejectedAnswer, wantRtry: false, }, { name: "event fully booked / booking limit", err: &CalcomAPIError{Endpoint: "booking", Status: 400, Code: "BadRequestException", Message: "booking_limit reached for this event type"}, wantMsg: msgEventFullyBooked, wantRtry: true, }, { // Live: status 404, NotFoundException — unmapped → Tier-2 admin message. name: "not found (unmapped 4xx) → admin message", err: &CalcomAPIError{Endpoint: "booking", Status: 404, Code: "NotFoundException", Message: "Event type with slug nope not found"}, wantMsg: adminMsg, wantRtry: false, }, { name: "auth failure (401) → admin message", err: &CalcomAPIError{Endpoint: "booking", Status: 401, Code: "UnauthorizedException", Message: "Invalid API Key"}, wantMsg: adminMsg, wantRtry: false, }, { name: "cal.com 5xx → admin message", err: &CalcomAPIError{Endpoint: "booking", Status: 503, Code: "", Message: "service unavailable"}, wantMsg: adminMsg, wantRtry: false, }, { name: "network/transport error (not a CalcomAPIError) → admin message", err: fmt.Errorf("post bookings: %w", errors.New("dial tcp: i/o timeout")), wantMsg: adminMsg, wantRtry: false, }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { got := classifyBookingError(tc.err, adminMsg) if got.message != tc.wantMsg { t.Errorf("message = %q, want %q", got.message, tc.wantMsg) } if got.canRetry != tc.wantRtry { t.Errorf("canRetry = %v, want %v", got.canRetry, tc.wantRtry) } // The raw Cal.com message must never be the rendered copy. var apiErr *CalcomAPIError if errors.As(tc.err, &apiErr) && apiErr.Message != "" && got.message == apiErr.Message { t.Errorf("rendered the raw Cal.com message verbatim: %q", got.message) } }) } } // TestClassifyBookingError_BlankAdminMessageIsCallerDefaulted documents that the // classifier renders whatever adminMessage it is given for Tier-2 — the caller // (HandleCreateBooking) substitutes DefaultUnavailableMessage when the form // field is blank, so the two stay in sync via the single const. func TestClassifyBookingError_BlankAdminMessageIsCallerDefaulted(t *testing.T) { err := &CalcomAPIError{Endpoint: "booking", Status: 404, Message: "nope"} got := classifyBookingError(err, DefaultUnavailableMessage) if got.message != DefaultUnavailableMessage { t.Errorf("message = %q, want DefaultUnavailableMessage", got.message) } }