Series: Go
go
170 lines
· Updated 2026-04-18
payment_test.go
Go/payment-gateway-challenge/internal/handlers/payment_test.go
package handlers
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"sync"
"testing"
"time"
"payment-gateway-challenge/internal/bank"
"payment-gateway-challenge/internal/config"
"payment-gateway-challenge/internal/models"
"github.com/google/uuid"
"github.com/labstack/echo/v4"
"gorm.io/gorm"
)
// memRepo is an in-memory PaymentWriterReader for fast handler tests.
type memRepo struct {
mu sync.Mutex
rows map[uuid.UUID]*models.Payment
}
func newMemRepo() *memRepo {
return &memRepo{rows: make(map[uuid.UUID]*models.Payment)}
}
func (m *memRepo) Create(_ context.Context, p *models.Payment) error {
m.mu.Lock()
defer m.mu.Unlock()
cp := *p
m.rows[p.ID] = &cp
return nil
}
func (m *memRepo) GetByID(_ context.Context, id uuid.UUID) (*models.Payment, error) {
m.mu.Lock()
defer m.mu.Unlock()
p, ok := m.rows[id]
if !ok {
return nil, gorm.ErrRecordNotFound
}
return p, nil
}
func TestProcessPayment_Authorized(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("/payments", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(bank.Response{
Authorized: true,
AuthorizationCode: "test-auth-code",
})
})
srv := httptest.NewServer(mux)
t.Cleanup(srv.Close)
cfg := &config.Config{
BankSimulatorURL: srv.URL + "/payments",
AllowedCurrencies: []string{"GBP"},
HTTPClientTimeout: 5 * time.Second,
}
bc := bank.NewClient(cfg.BankSimulatorURL, srv.Client())
repo := newMemRepo()
api := NewPaymentAPI(cfg, repo, bc)
api.clockFn = func() time.Time { return time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC) }
body := `{
"card_number":"2222405343248877",
"expiry_month":4,
"expiry_year":2028,
"currency":"GBP",
"amount":100,
"cvv":"123"
}`
e := echo.New()
req := httptest.NewRequest(http.MethodPost, "/api/payments", strings.NewReader(body))
req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
if err := api.ProcessPayment(c); err != nil {
t.Fatal(err)
}
if rec.Code != http.StatusCreated {
t.Fatalf("status = %d, body=%s", rec.Code, rec.Body.String())
}
var got ProcessPaymentResponse
if err := json.Unmarshal(rec.Body.Bytes(), &got); err != nil {
t.Fatal(err)
}
if got.Status != "Authorized" || got.LastFourCardDigits != "8877" {
t.Fatalf("unexpected response: %+v", got)
}
if _, err := uuid.Parse(got.ID); err != nil {
t.Fatalf("id not uuid: %v", err)
}
}
func TestProcessPayment_RejectedValidation(t *testing.T) {
// Bank should never be called — use invalid URL that would fail if hit.
cfg := &config.Config{
BankSimulatorURL: "http://127.0.0.1:1/payments",
AllowedCurrencies: []string{"USD"},
HTTPClientTimeout: time.Millisecond,
}
bc := bank.NewClient(cfg.BankSimulatorURL, http.DefaultClient)
repo := newMemRepo()
api := NewPaymentAPI(cfg, repo, bc)
api.clockFn = func() time.Time { return time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC) }
body := `{"card_number":"1","expiry_month":13,"expiry_year":2020,"currency":"XXX","amount":0,"cvv":"ab"}`
e := echo.New()
req := httptest.NewRequest(http.MethodPost, "/api/payments", strings.NewReader(body))
req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
if err := api.ProcessPayment(c); err != nil {
t.Fatal(err)
}
if rec.Code != http.StatusBadRequest {
t.Fatalf("expected 400, got %d: %s", rec.Code, rec.Body.String())
}
}
func TestGetPayment_OK(t *testing.T) {
id := uuid.New()
repo := newMemRepo()
_ = repo.Create(context.Background(), &models.Payment{
ID: id,
Status: "Authorized",
CardLast4: "4242",
CardNumberLength: 16,
ExpiryMonth: 12,
ExpiryYear: 2030,
Currency: "USD",
Amount: 50,
})
cfg := &config.Config{AllowedCurrencies: []string{"USD"}}
api := NewPaymentAPI(cfg, repo, bank.NewClient("http://unused", http.DefaultClient))
e := echo.New()
req := httptest.NewRequest(http.MethodGet, "/api/payments/"+id.String(), nil)
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
c.SetParamNames("id")
c.SetParamValues(id.String())
if err := api.GetPayment(c); err != nil {
t.Fatal(err)
}
if rec.Code != http.StatusOK {
t.Fatalf("status %d", rec.Code)
}
var got PaymentDetailResponse
if err := json.Unmarshal(rec.Body.Bytes(), &got); err != nil {
t.Fatal(err)
}
if got.MaskedCardNumber != "************4242" {
t.Fatalf("mask = %q", got.MaskedCardNumber)
}
}
Related articles
Go
go
Updated 2026-04-18
main.go
main.go — go source code from the Go learning materials (Go/Baasid/cmd/hashpw/main.go).
Read article →
Go
go
Updated 2026-04-18
main.go
main.go — go source code from the Go learning materials (Go/Baasid/cmd/server/main.go).
Read article →
Go
go
Updated 2026-04-18
docs.go
docs.go — go source code from the Go learning materials (Go/Baasid/docs/docs.go).
Read article →
Go
go
Updated 2026-04-18
jwt.go
jwt.go — go source code from the Go learning materials (Go/Baasid/internal/auth/jwt.go).
Read article →
Go
go
Updated 2026-04-18
password.go
password.go — go source code from the Go learning materials (Go/Baasid/internal/auth/password.go).
Read article →
Go
go
Updated 2026-04-18
password_test.go
password_test.go — go source code from the Go learning materials (Go/Baasid/internal/auth/password_test.go).
Read article →