200 lines
6.7 KiB
Go
200 lines
6.7 KiB
Go
package dojo
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"reflect"
|
|
"strings"
|
|
"sync/atomic"
|
|
"testing"
|
|
|
|
"gitea.futuresens.co.uk/futuresens/hardlink/internal/paymentsvc"
|
|
"gitea.futuresens.co.uk/futuresens/hardlink/paymentstatus"
|
|
)
|
|
|
|
const terminalUnavailableBody = `{"detail":"the terminal is either offline or currently in use","errors":{},"status":409,"title":"terminal unavailable","traceId":"trace-secret","type":"https://docs.dojo.tech/problems/terminal-unavailable"}`
|
|
|
|
func TestDoJSONReturnsStructuredHTTPResponseError(t *testing.T) {
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
w.WriteHeader(http.StatusConflict)
|
|
_, _ = w.Write([]byte(" " + terminalUnavailableBody + " "))
|
|
}))
|
|
defer server.Close()
|
|
|
|
client := newDojoTestClient(t, server)
|
|
err := client.doJSON(context.Background(), http.MethodPost, "/payment-intents", nil, false, nil)
|
|
|
|
var responseErr *httpResponseError
|
|
if !errors.As(err, &responseErr) {
|
|
t.Fatalf("error = %T %v, want *httpResponseError", err, err)
|
|
}
|
|
if responseErr.StatusCode != http.StatusConflict {
|
|
t.Fatalf("StatusCode = %d, want %d", responseErr.StatusCode, http.StatusConflict)
|
|
}
|
|
if responseErr.Body != terminalUnavailableBody {
|
|
t.Fatalf("Body = %q, want retained trimmed response", responseErr.Body)
|
|
}
|
|
}
|
|
|
|
func TestSaleMapsCreateTerminalSessionConflictToTerminalUnavailable(t *testing.T) {
|
|
var paymentIntentRequests atomic.Int32
|
|
var terminalSessionRequests atomic.Int32
|
|
var unexpectedRequests atomic.Int32
|
|
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch {
|
|
case r.Method == http.MethodPost && r.URL.Path == "/payment-intents":
|
|
paymentIntentRequests.Add(1)
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, _ = fmt.Fprintln(w, `{"id":"intent-123"}`)
|
|
case r.Method == http.MethodPost && r.URL.Path == "/terminal-sessions":
|
|
terminalSessionRequests.Add(1)
|
|
w.Header().Set("Content-Type", "application/problem+json")
|
|
w.WriteHeader(http.StatusConflict)
|
|
_, _ = fmt.Fprintln(w, terminalUnavailableBody)
|
|
default:
|
|
unexpectedRequests.Add(1)
|
|
http.Error(w, "unexpected request", http.StatusInternalServerError)
|
|
}
|
|
}))
|
|
defer server.Close()
|
|
|
|
client := newDojoTestClient(t, server)
|
|
request := paymentsvc.SaleRequest{
|
|
RequestID: "REQ-123",
|
|
Reference: "BOOKING-123",
|
|
Amount: 10852,
|
|
Currency: "GBP",
|
|
}
|
|
var statuses []string
|
|
result, err := client.Sale(context.Background(), request, func(update paymentsvc.StatusUpdate) {
|
|
statuses = append(statuses, update.Code)
|
|
})
|
|
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if result == nil {
|
|
t.Fatal("Sale returned nil result")
|
|
}
|
|
if result.Success || result.Status == "APPROVED" {
|
|
t.Fatalf("result was accidentally successful: %+v", result)
|
|
}
|
|
if result.Status != dojoTerminalUnavailableStatus || result.ErrorMessage != dojoTerminalUnavailableMessage {
|
|
t.Fatalf("terminal-unavailable result = %+v", result)
|
|
}
|
|
if result.RequestID != request.RequestID ||
|
|
result.Operation != "SALE" ||
|
|
result.Amount != request.Amount ||
|
|
result.Currency != request.Currency ||
|
|
result.DeviceUsed != "terminal-1" ||
|
|
result.DeviceType != "Dojo Terminal" {
|
|
t.Fatalf("base result fields were not retained: %+v", result)
|
|
}
|
|
if result.TransactionID != "" || result.CustomerReceipt != "" || result.MerchantReceipt != "" {
|
|
t.Fatalf("terminal-unavailable result invented transaction data: %+v", result)
|
|
}
|
|
if !reflect.DeepEqual(statuses, []string{paymentstatus.Starting, paymentstatus.TerminalUnavailable}) {
|
|
t.Fatalf("statuses = %#v", statuses)
|
|
}
|
|
if paymentIntentRequests.Load() != 1 || terminalSessionRequests.Load() != 1 || unexpectedRequests.Load() != 0 {
|
|
t.Fatalf(
|
|
"requests: intent=%d terminal=%d unexpected=%d",
|
|
paymentIntentRequests.Load(),
|
|
terminalSessionRequests.Load(),
|
|
unexpectedRequests.Load(),
|
|
)
|
|
}
|
|
|
|
resultText := fmt.Sprintf("%+v", result)
|
|
for _, forbidden := range []string{"offline or currently in use", "trace-secret", "docs.dojo.tech", terminalUnavailableBody} {
|
|
if strings.Contains(resultText, forbidden) {
|
|
t.Fatalf("result exposed %q: %s", forbidden, resultText)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestSaleDoesNotClassifyPaymentIntentConflictAsTerminalUnavailable(t *testing.T) {
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path != "/payment-intents" {
|
|
t.Errorf("unexpected request %s %s", r.Method, r.URL.Path)
|
|
}
|
|
w.WriteHeader(http.StatusConflict)
|
|
_, _ = fmt.Fprintln(w, terminalUnavailableBody)
|
|
}))
|
|
defer server.Close()
|
|
|
|
client := newDojoTestClient(t, server)
|
|
var statuses []string
|
|
result, err := client.Sale(context.Background(), paymentsvc.SaleRequest{
|
|
RequestID: "REQ-123",
|
|
Amount: 10852,
|
|
Currency: "GBP",
|
|
}, func(update paymentsvc.StatusUpdate) {
|
|
statuses = append(statuses, update.Code)
|
|
})
|
|
|
|
if result != nil || err == nil {
|
|
t.Fatalf("Sale result/error = %+v/%v, want nil generic error", result, err)
|
|
}
|
|
var responseErr *httpResponseError
|
|
if !errors.As(err, &responseErr) || responseErr.StatusCode != http.StatusConflict {
|
|
t.Fatalf("error = %T %v, want wrapped HTTP 409", err, err)
|
|
}
|
|
if !reflect.DeepEqual(statuses, []string{paymentstatus.Starting}) {
|
|
t.Fatalf("statuses = %#v, want only PAYMENT_STARTING", statuses)
|
|
}
|
|
}
|
|
|
|
func TestSaleRetainsGenericCreateTerminalSessionErrors(t *testing.T) {
|
|
for _, statusCode := range []int{http.StatusBadRequest, http.StatusBadGateway} {
|
|
t.Run(http.StatusText(statusCode), func(t *testing.T) {
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.Path {
|
|
case "/payment-intents":
|
|
_, _ = fmt.Fprintln(w, `{"id":"intent-123"}`)
|
|
case "/terminal-sessions":
|
|
w.WriteHeader(statusCode)
|
|
_, _ = fmt.Fprintln(w, `{"detail":"provider failure"}`)
|
|
default:
|
|
t.Errorf("unexpected request %s %s", r.Method, r.URL.Path)
|
|
}
|
|
}))
|
|
defer server.Close()
|
|
|
|
client := newDojoTestClient(t, server)
|
|
result, err := client.Sale(context.Background(), paymentsvc.SaleRequest{
|
|
RequestID: "REQ-123",
|
|
Amount: 10852,
|
|
Currency: "GBP",
|
|
}, nil)
|
|
|
|
if result != nil || err == nil {
|
|
t.Fatalf("Sale result/error = %+v/%v, want nil generic error", result, err)
|
|
}
|
|
var responseErr *httpResponseError
|
|
if !errors.As(err, &responseErr) || responseErr.StatusCode != statusCode {
|
|
t.Fatalf("error = %T %v, want wrapped HTTP %d", err, err, statusCode)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func newDojoTestClient(t *testing.T, server *httptest.Server) *Client {
|
|
t.Helper()
|
|
client, err := NewClient(Config{
|
|
BaseURL: server.URL,
|
|
APIKey: "test-api-key",
|
|
SoftwareHouseID: "software-house-1",
|
|
TerminalID: "terminal-1",
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
client.httpClient = server.Client()
|
|
return client
|
|
}
|