package handlers import ( "context" "encoding/json" "errors" "io" "net/http" "net/http/httptest" "net/url" "strings" "testing" "gitea.futuresens.co.uk/futuresens/hardlink/config" "gitea.futuresens.co.uk/futuresens/hardlink/internal/paymentsvc" "gitea.futuresens.co.uk/futuresens/hardlink/internal/types" "gitea.futuresens.co.uk/futuresens/hardlink/paymentstatus" ) type paymentProviderFunc func( context.Context, paymentsvc.SaleRequest, paymentsvc.StatusHandler, ) (*paymentsvc.Result, error) func (f paymentProviderFunc) Sale( ctx context.Context, request paymentsvc.SaleRequest, onStatus paymentsvc.StatusHandler, ) (*paymentsvc.Result, error) { return f(ctx, request, onStatus) } func TestSalePaymentStreamsTerminalUnavailableAsAuthoritativeUnsuccessfulResult(t *testing.T) { provider := paymentProviderFunc(func( _ context.Context, request paymentsvc.SaleRequest, onStatus paymentsvc.StatusHandler, ) (*paymentsvc.Result, error) { onStatus(paymentsvc.StatusUpdate{Code: paymentstatus.Starting}) onStatus(paymentsvc.StatusUpdate{Code: paymentstatus.TerminalUnavailable}) return &paymentsvc.Result{ RequestID: request.RequestID, Operation: "SALE", Status: "TERMINAL_UNAVAILABLE", ErrorMessage: "Payment terminal is unavailable", Amount: request.Amount, Currency: request.Currency, DeviceUsed: "terminal-1", DeviceType: "Dojo Terminal", }, nil }) recorder := performSalePaymentRequest(t, provider) if recorder.Code != http.StatusOK { t.Fatalf("outer status = %d, want %d", recorder.Code, http.StatusOK) } messages := decodePaymentStream(t, recorder.Body) if len(messages) != 3 { t.Fatalf("stream messages = %#v, want two statuses and one result", messages) } if messages[0].Type != "status" || messages[0].Code != paymentstatus.Starting || messages[1].Type != "status" || messages[1].Code != paymentstatus.TerminalUnavailable { t.Fatalf("status frames = %#v", messages[:2]) } final := messages[2] if final.Type != "result" || final.Response == nil { t.Fatalf("final frame = %#v", final) } if final.Response.Status.Code != http.StatusOK { t.Fatalf("nested status = %d, want %d", final.Response.Status.Code, http.StatusOK) } resultURL, err := url.Parse(final.Response.Data) if err != nil { t.Fatal(err) } if resultURL.Path != types.CheckinUnsuccessfulEndpoint { t.Fatalf("result path = %q, want %q", resultURL.Path, types.CheckinUnsuccessfulEndpoint) } if got := resultURL.Query().Get("Description"); got != "Payment terminal is unavailable" { t.Fatalf("Description = %q", got) } body := recorder.Body.String() for _, forbidden := range []string{ "Dojo returned HTTP 409", "offline or currently in use", "traceId", "docs.dojo.tech", } { if strings.Contains(body, forbidden) { t.Fatalf("stream exposed %q: %s", forbidden, body) } } } func TestSalePaymentRetainsNestedBadGatewayForProviderErrorsAfterStreamingStarts(t *testing.T) { provider := paymentProviderFunc(func( _ context.Context, _ paymentsvc.SaleRequest, onStatus paymentsvc.StatusHandler, ) (*paymentsvc.Result, error) { onStatus(paymentsvc.StatusUpdate{Code: paymentstatus.Starting}) return nil, errors.New("provider failed") }) recorder := performSalePaymentRequest(t, provider) if recorder.Code != http.StatusOK { t.Fatalf("outer status = %d, want streaming status %d", recorder.Code, http.StatusOK) } messages := decodePaymentStream(t, recorder.Body) if len(messages) != 2 || messages[1].Type != "result" || messages[1].Response == nil { t.Fatalf("stream messages = %#v", messages) } if messages[1].Response.Status.Code != http.StatusBadGateway { t.Fatalf("nested status = %d, want %d", messages[1].Response.Status.Code, http.StatusBadGateway) } resultURL, err := url.Parse(messages[1].Response.Data) if err != nil { t.Fatal(err) } if resultURL.Path != types.CheckinUnsuccessfulEndpoint { t.Fatalf("result path = %q, want %q", resultURL.Path, types.CheckinUnsuccessfulEndpoint) } } func performSalePaymentRequest(t *testing.T, provider paymentsvc.Provider) *httptest.ResponseRecorder { t.Helper() app := &App{ paymentService: paymentsvc.NewService(provider), cfg: &config.ConfigRec{ Hotel: "HOTEL", Kiosk: 7, TimeoutSeconds: 5, }, } request := httptest.NewRequest( http.MethodPost, "/api/payment/sale", strings.NewReader(`{"reference":"BOOKING-123","amount":10852,"currency":"GBP"}`), ) request.Header.Set("Content-Type", "application/json") recorder := httptest.NewRecorder() app.salePayment(recorder, request) return recorder } func decodePaymentStream(t *testing.T, body io.Reader) []paymentStreamMessage { t.Helper() decoder := json.NewDecoder(body) var messages []paymentStreamMessage for { var message paymentStreamMessage if err := decoder.Decode(&message); err != nil { if errors.Is(err, io.EOF) { return messages } t.Fatal(err) } messages = append(messages, message) } }