163 lines
4.9 KiB
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)
|
|
}
|
|
}
|