201 lines
6.3 KiB
Go
201 lines
6.3 KiB
Go
package sendico
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
"go.mongodb.org/mongo-driver/v2/bson"
|
|
)
|
|
|
|
type accountServer struct {
|
|
mu sync.Mutex
|
|
rows []accountDTO
|
|
posts []string
|
|
capable bool
|
|
missingCurrency bool
|
|
failCurrency string
|
|
loseResponse bool
|
|
wrongResponse bool
|
|
}
|
|
|
|
func customerLedger(code string) accountDTO {
|
|
return accountDTO{ID: bson.NewObjectID().Hex(), ScopeRef: org, Currency: code, Type: "ACCOUNT_TYPE_ASSET", LedgerRole: "ACCOUNT_ROLE_OPERATING", Status: "ACCOUNT_STATUS_ACTIVE", Metadata: map[string]string{"sxchnge_subject_ref": subject, "external_subject_ref": subject}}
|
|
}
|
|
|
|
func (s *accountServer) serve(t *testing.T) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
require.NotEmpty(t, r.Header.Get("Authorization"))
|
|
var data any
|
|
switch {
|
|
case strings.Contains(r.URL.Path, "/currencies/"):
|
|
rows := []map[string]any{{"code": "RUB", "accountingScale": 2}, {"code": "USDT", "accountingScale": 6}, {"code": "USDC", "accountingScale": 6}}
|
|
if s.missingCurrency {
|
|
rows = rows[:2]
|
|
}
|
|
data = map[string]any{"currencies": rows}
|
|
case r.Method == http.MethodPost:
|
|
var request createAccountDTO
|
|
require.NoError(t, json.NewDecoder(r.Body).Decode(&request))
|
|
require.Equal(t, "asset", request.Type)
|
|
require.Equal(t, "operating", request.LedgerRole)
|
|
require.False(t, request.AllowNegative)
|
|
require.Equal(t, subject, request.Metadata["external_subject_ref"])
|
|
require.Equal(t, subject, request.Metadata["sxchnge_subject_ref"])
|
|
s.posts = append(s.posts, request.Currency)
|
|
if s.failCurrency == request.Currency {
|
|
s.failCurrency = ""
|
|
w.WriteHeader(503)
|
|
return
|
|
}
|
|
var row accountDTO
|
|
for _, a := range s.rows {
|
|
if a.Currency == request.Currency && a.Metadata["external_subject_ref"] == subject {
|
|
row = a
|
|
}
|
|
}
|
|
if row.ID == "" {
|
|
row = customerLedger(request.Currency)
|
|
s.rows = append(s.rows, row)
|
|
}
|
|
if s.loseResponse {
|
|
s.loseResponse = false
|
|
w.WriteHeader(503)
|
|
return
|
|
}
|
|
if s.wrongResponse {
|
|
row.Currency = "EUR"
|
|
}
|
|
w.WriteHeader(http.StatusCreated)
|
|
data = map[string]any{"account": row}
|
|
case strings.HasSuffix(r.URL.Path, "/balance"):
|
|
parts := strings.Split(r.URL.Path, "/")
|
|
data = map[string]any{"balance": balanceDTO{ID: parts[len(parts)-2], Money: moneyDTO{Amount: "45.00", Currency: "RUB"}}}
|
|
default:
|
|
data = map[string]any{"accounts": s.rows, "externalSubjectAccounts": s.capable}
|
|
}
|
|
require.NoError(t, json.NewEncoder(w).Encode(map[string]any{"status": "success", "data": data}))
|
|
}
|
|
}
|
|
|
|
func TestEnsureCurrencyAccountsPreservesExistingAndRecoversPartialFailure(t *testing.T) {
|
|
for _, lost := range []bool{false, true} {
|
|
t.Run(map[bool]string{false: "partial failure", true: "lost response"}[lost], func(t *testing.T) {
|
|
s := &accountServer{capable: true, failCurrency: "USDT", loseResponse: lost}
|
|
if lost {
|
|
s.failCurrency = ""
|
|
}
|
|
server := httptest.NewServer(s.serve(t))
|
|
defer server.Close()
|
|
c, err := Open(config(t, server.URL))
|
|
require.NoError(t, err)
|
|
require.Error(t, c.EnsureAccounts(t.Context(), subject))
|
|
require.Len(t, s.rows, 1)
|
|
firstID := s.rows[0].ID
|
|
require.NoError(t, c.EnsureAccounts(t.Context(), subject))
|
|
require.NoError(t, c.EnsureAccounts(t.Context(), subject))
|
|
require.Len(t, s.rows, 3)
|
|
require.Equal(t, firstID, s.rows[0].ID)
|
|
// Bound creates are replayed to finish interrupted wallet/route setup;
|
|
// the provider must return the original ledger each time.
|
|
require.Equal(t, 3, countCurrency(s.posts, "RUB"))
|
|
})
|
|
}
|
|
}
|
|
|
|
func countCurrency(rows []string, code string) int {
|
|
n := 0
|
|
for _, r := range rows {
|
|
if r == code {
|
|
n++
|
|
}
|
|
}
|
|
return n
|
|
}
|
|
|
|
func TestEnsureAccountsConcurrentRequestsUseStableSubjectBinding(t *testing.T) {
|
|
s := &accountServer{capable: true}
|
|
server := httptest.NewServer(s.serve(t))
|
|
defer server.Close()
|
|
c, err := Open(config(t, server.URL))
|
|
require.NoError(t, err)
|
|
errors := make(chan error, 12)
|
|
for range 12 {
|
|
go func() { errors <- c.EnsureAccounts(t.Context(), subject) }()
|
|
}
|
|
for range 12 {
|
|
require.NoError(t, <-errors)
|
|
}
|
|
require.Len(t, s.rows, 3)
|
|
}
|
|
|
|
func TestEnsureAccountsRejectsUnsafeProviderStateBeforeCreating(t *testing.T) {
|
|
for _, kind := range []string{"old provider", "missing currency", "frozen", "owned", "negative", "duplicate", "wrong subject"} {
|
|
t.Run(kind, func(t *testing.T) {
|
|
s := &accountServer{capable: true}
|
|
a := customerLedger("RUB")
|
|
switch kind {
|
|
case "old provider":
|
|
s.capable = false
|
|
case "missing currency":
|
|
s.missingCurrency = true
|
|
case "frozen":
|
|
a.Status = "ACCOUNT_STATUS_FROZEN"
|
|
s.rows = []accountDTO{a}
|
|
case "owned":
|
|
a.Owner = source
|
|
s.rows = []accountDTO{a}
|
|
case "negative":
|
|
a.AllowNegative = true
|
|
s.rows = []accountDTO{a}
|
|
case "duplicate":
|
|
s.rows = []accountDTO{a, customerLedger("RUB")}
|
|
case "wrong subject":
|
|
a.Metadata["sxchnge_subject_ref"] = source
|
|
s.rows = []accountDTO{a}
|
|
}
|
|
server := httptest.NewServer(s.serve(t))
|
|
defer server.Close()
|
|
c, err := Open(config(t, server.URL))
|
|
require.NoError(t, err)
|
|
require.Error(t, c.EnsureAccounts(t.Context(), subject))
|
|
require.Empty(t, s.posts)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestEnsureAccountsValidatesCreateReceipt(t *testing.T) {
|
|
s := &accountServer{capable: true, wrongResponse: true}
|
|
server := httptest.NewServer(s.serve(t))
|
|
defer server.Close()
|
|
c, err := Open(config(t, server.URL))
|
|
require.NoError(t, err)
|
|
require.Error(t, c.EnsureAccounts(t.Context(), subject))
|
|
require.Len(t, s.posts, 1)
|
|
}
|
|
|
|
func TestEnsureAccountsKeepsExistingBindingsAndBalances(t *testing.T) {
|
|
a := customerLedger("RUB")
|
|
delete(a.Metadata, "external_subject_ref")
|
|
s := &accountServer{capable: true, rows: []accountDTO{a}}
|
|
server := httptest.NewServer(s.serve(t))
|
|
defer server.Close()
|
|
c, err := Open(config(t, server.URL))
|
|
require.NoError(t, err)
|
|
require.NoError(t, c.EnsureAccounts(t.Context(), subject))
|
|
require.Equal(t, []string{"USDT", "USDC"}, s.posts)
|
|
// A subsequent read uses the original RUB ledger and its authoritative funds.
|
|
s.rows = s.rows[:1]
|
|
accounts, err := c.Accounts(t.Context(), subject)
|
|
require.NoError(t, err)
|
|
require.Len(t, accounts, 1)
|
|
require.Equal(t, a.ID, accounts[0].ID)
|
|
require.EqualValues(t, 4500, accounts[0].Available)
|
|
}
|