hardlink/internal/handlers/payment_handlers_test.go

163 lines
4.9 KiB
Go

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)
}
}