413 lines
11 KiB
Go
413 lines
11 KiB
Go
package gateway
|
|
|
|
import (
|
|
"context"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/tech/sendico/gateway/tgsettle/storage"
|
|
storagemodel "github.com/tech/sendico/gateway/tgsettle/storage/model"
|
|
"github.com/tech/sendico/pkg/discovery"
|
|
envelope "github.com/tech/sendico/pkg/messaging/envelope"
|
|
tnotifications "github.com/tech/sendico/pkg/messaging/notifications/telegram"
|
|
mloggerfactory "github.com/tech/sendico/pkg/mlogger/factory"
|
|
"github.com/tech/sendico/pkg/model"
|
|
"github.com/tech/sendico/pkg/mservice"
|
|
paymenttypes "github.com/tech/sendico/pkg/payments/types"
|
|
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
|
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
|
|
)
|
|
|
|
//
|
|
// FAKE STORES
|
|
//
|
|
|
|
type fakePaymentsStore struct {
|
|
mu sync.Mutex
|
|
records map[string]*storagemodel.PaymentRecord
|
|
}
|
|
|
|
func (f *fakePaymentsStore) FindByIdempotencyKey(_ context.Context, key string) (*storagemodel.PaymentRecord, error) {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
if f.records == nil {
|
|
return nil, nil
|
|
}
|
|
return f.records[key], nil
|
|
}
|
|
|
|
func (f *fakePaymentsStore) FindByOperationRef(_ context.Context, key string) (*storagemodel.PaymentRecord, error) {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
if f.records == nil {
|
|
return nil, nil
|
|
}
|
|
for _, record := range f.records {
|
|
if record != nil && record.OperationRef == key {
|
|
return record, nil
|
|
}
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
func (f *fakePaymentsStore) Upsert(_ context.Context, record *storagemodel.PaymentRecord) error {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
if f.records == nil {
|
|
f.records = map[string]*storagemodel.PaymentRecord{}
|
|
}
|
|
f.records[record.IdempotencyKey] = record
|
|
return nil
|
|
}
|
|
|
|
type fakeTelegramStore struct {
|
|
mu sync.Mutex
|
|
records map[string]*storagemodel.TelegramConfirmation
|
|
}
|
|
|
|
func (f *fakeTelegramStore) Upsert(_ context.Context, record *storagemodel.TelegramConfirmation) error {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
if f.records == nil {
|
|
f.records = map[string]*storagemodel.TelegramConfirmation{}
|
|
}
|
|
f.records[record.RequestID] = record
|
|
return nil
|
|
}
|
|
|
|
type fakeRepo struct {
|
|
payments *fakePaymentsStore
|
|
tg *fakeTelegramStore
|
|
pending *fakePendingStore
|
|
treasury storage.TreasuryRequestsStore
|
|
}
|
|
|
|
func (f *fakeRepo) Payments() storage.PaymentsStore {
|
|
return f.payments
|
|
}
|
|
|
|
func (f *fakeRepo) TelegramConfirmations() storage.TelegramConfirmationsStore {
|
|
return f.tg
|
|
}
|
|
|
|
func (f *fakeRepo) PendingConfirmations() storage.PendingConfirmationsStore {
|
|
return f.pending
|
|
}
|
|
|
|
func (f *fakeRepo) TreasuryRequests() storage.TreasuryRequestsStore {
|
|
return f.treasury
|
|
}
|
|
|
|
type fakePendingStore struct {
|
|
mu sync.Mutex
|
|
records map[string]*storagemodel.PendingConfirmation
|
|
}
|
|
|
|
func (f *fakePendingStore) Upsert(_ context.Context, record *storagemodel.PendingConfirmation) error {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
if f.records == nil {
|
|
f.records = map[string]*storagemodel.PendingConfirmation{}
|
|
}
|
|
cp := *record
|
|
f.records[record.RequestID] = &cp
|
|
return nil
|
|
}
|
|
|
|
func (f *fakePendingStore) FindByRequestID(_ context.Context, requestID string) (*storagemodel.PendingConfirmation, error) {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
if f.records == nil {
|
|
return nil, nil
|
|
}
|
|
return f.records[requestID], nil
|
|
}
|
|
|
|
func (f *fakePendingStore) FindByMessageID(_ context.Context, messageID string) (*storagemodel.PendingConfirmation, error) {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
for _, record := range f.records {
|
|
if record != nil && record.MessageID == messageID {
|
|
return record, nil
|
|
}
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
func (f *fakePendingStore) MarkClarified(_ context.Context, requestID string) error {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
if record := f.records[requestID]; record != nil {
|
|
record.Clarified = true
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (f *fakePendingStore) AttachMessage(_ context.Context, requestID string, messageID string) error {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
if record := f.records[requestID]; record != nil {
|
|
if record.MessageID == "" {
|
|
record.MessageID = messageID
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (f *fakePendingStore) DeleteByRequestID(_ context.Context, requestID string) error {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
delete(f.records, requestID)
|
|
return nil
|
|
}
|
|
|
|
func (f *fakePendingStore) ListExpired(_ context.Context, now time.Time, limit int64) ([]storagemodel.PendingConfirmation, error) {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
if limit <= 0 {
|
|
limit = 100
|
|
}
|
|
result := make([]storagemodel.PendingConfirmation, 0)
|
|
for _, record := range f.records {
|
|
if record == nil || record.ExpiresAt.IsZero() || record.ExpiresAt.After(now) {
|
|
continue
|
|
}
|
|
result = append(result, *record)
|
|
if int64(len(result)) >= limit {
|
|
break
|
|
}
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
//
|
|
// FAKE BROKER (ОБЯЗАТЕЛЕН ДЛЯ СЕРВИСА)
|
|
//
|
|
|
|
type fakeBroker struct{}
|
|
|
|
func (f *fakeBroker) Publish(_ envelope.Envelope) error {
|
|
return nil
|
|
}
|
|
|
|
func (f *fakeBroker) Subscribe(event model.NotificationEvent) (<-chan envelope.Envelope, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
func (f *fakeBroker) Unsubscribe(event model.NotificationEvent, subChan <-chan envelope.Envelope) error {
|
|
return nil
|
|
}
|
|
|
|
//
|
|
// CAPTURE ONLY TELEGRAM REACTIONS
|
|
//
|
|
|
|
type captureProducer struct {
|
|
mu sync.Mutex
|
|
reactions []envelope.Envelope
|
|
sig string
|
|
}
|
|
|
|
func (c *captureProducer) SendMessage(env envelope.Envelope) error {
|
|
if env.GetSignature().ToString() != c.sig {
|
|
return nil
|
|
}
|
|
c.mu.Lock()
|
|
c.reactions = append(c.reactions, env)
|
|
c.mu.Unlock()
|
|
return nil
|
|
}
|
|
|
|
//
|
|
// TESTS
|
|
//
|
|
|
|
func newTestService(_ *testing.T) (*Service, *fakeRepo, *captureProducer) {
|
|
logger := mloggerfactory.NewLogger(false)
|
|
|
|
repo := &fakeRepo{
|
|
payments: &fakePaymentsStore{},
|
|
tg: &fakeTelegramStore{},
|
|
pending: &fakePendingStore{},
|
|
}
|
|
|
|
sigEnv := tnotifications.TelegramReaction(string(mservice.PaymentGateway), &model.TelegramReactionRequest{
|
|
RequestID: "x",
|
|
ChatID: "1",
|
|
MessageID: "2",
|
|
Emoji: "ok",
|
|
})
|
|
|
|
prod := &captureProducer{
|
|
sig: sigEnv.GetSignature().ToString(),
|
|
}
|
|
|
|
svc := NewService(logger, repo, prod, &fakeBroker{}, Config{
|
|
Rail: "card",
|
|
SuccessReaction: "👍",
|
|
})
|
|
|
|
return svc, repo, prod
|
|
}
|
|
|
|
func TestConfirmed(t *testing.T) {
|
|
svc, repo, prod := newTestService(t)
|
|
|
|
_ = repo.payments.Upsert(context.Background(), &storagemodel.PaymentRecord{
|
|
IdempotencyKey: "idem-1",
|
|
PaymentIntentID: "pi-1",
|
|
QuoteRef: "quote-1",
|
|
OutgoingLeg: "card",
|
|
RequestedMoney: &paymenttypes.Money{Amount: "5", Currency: "EUR"},
|
|
Status: storagemodel.PaymentStatusWaiting,
|
|
})
|
|
|
|
result := &model.ConfirmationResult{
|
|
RequestID: "idem-1",
|
|
Money: &paymenttypes.Money{Amount: "5", Currency: "EUR"},
|
|
Status: model.ConfirmationStatusConfirmed,
|
|
RawReply: &model.TelegramMessage{ChatID: "1", MessageID: "2"},
|
|
}
|
|
|
|
_ = svc.onConfirmationResult(context.Background(), result)
|
|
|
|
rec := repo.payments.records["idem-1"]
|
|
|
|
if rec.Status != storagemodel.PaymentStatusSuccess {
|
|
t.Fatalf("expected success, got %s", rec.Status)
|
|
}
|
|
if rec.RequestedMoney == nil {
|
|
t.Fatalf("requested money not set")
|
|
}
|
|
if rec.ExecutedAt.IsZero() {
|
|
t.Fatalf("executedAt not set")
|
|
}
|
|
if repo.tg.records["idem-1"] == nil {
|
|
t.Fatalf("telegram confirmation not stored")
|
|
}
|
|
if len(prod.reactions) != 1 {
|
|
t.Fatalf("reaction must be published")
|
|
}
|
|
}
|
|
|
|
func TestClarified(t *testing.T) {
|
|
svc, repo, prod := newTestService(t)
|
|
|
|
_ = repo.payments.Upsert(context.Background(), &storagemodel.PaymentRecord{
|
|
IdempotencyKey: "idem-2",
|
|
Status: storagemodel.PaymentStatusWaiting,
|
|
})
|
|
|
|
result := &model.ConfirmationResult{
|
|
RequestID: "idem-2",
|
|
Status: model.ConfirmationStatusClarified,
|
|
RawReply: &model.TelegramMessage{ChatID: "1", MessageID: "2"},
|
|
}
|
|
|
|
_ = svc.onConfirmationResult(context.Background(), result)
|
|
|
|
rec := repo.payments.records["idem-2"]
|
|
|
|
if rec.Status != storagemodel.PaymentStatusWaiting {
|
|
t.Fatalf("clarified must not change status")
|
|
}
|
|
if repo.tg.records["idem-2"] == nil {
|
|
t.Fatalf("telegram confirmation must be stored")
|
|
}
|
|
if len(prod.reactions) != 0 {
|
|
t.Fatalf("clarified must not publish reaction")
|
|
}
|
|
}
|
|
|
|
func TestRejected(t *testing.T) {
|
|
svc, repo, prod := newTestService(t)
|
|
|
|
// ВАЖНО: чтобы текущий emitTransferStatusEvent не падал на nil,
|
|
// даем минимально ожидаемые поля + non-nil ExecutedMoney.
|
|
_ = repo.payments.Upsert(context.Background(), &storagemodel.PaymentRecord{
|
|
IdempotencyKey: "idem-3",
|
|
PaymentIntentID: "pi-3",
|
|
QuoteRef: "quote-3",
|
|
OutgoingLeg: "card",
|
|
RequestedMoney: &paymenttypes.Money{Amount: "5", Currency: "EUR"},
|
|
ExecutedMoney: &paymenttypes.Money{Amount: "0", Currency: "EUR"},
|
|
Status: storagemodel.PaymentStatusWaiting,
|
|
})
|
|
|
|
result := &model.ConfirmationResult{
|
|
RequestID: "idem-3",
|
|
Status: model.ConfirmationStatusRejected,
|
|
RawReply: &model.TelegramMessage{ChatID: "1", MessageID: "2"},
|
|
}
|
|
|
|
_ = svc.onConfirmationResult(context.Background(), result)
|
|
|
|
rec := repo.payments.records["idem-3"]
|
|
|
|
if rec.Status != storagemodel.PaymentStatusFailed {
|
|
t.Fatalf("expected failed")
|
|
}
|
|
if repo.tg.records["idem-3"] == nil {
|
|
t.Fatalf("telegram confirmation must be stored")
|
|
}
|
|
if len(prod.reactions) != 0 {
|
|
t.Fatalf("rejected must not publish reaction")
|
|
}
|
|
}
|
|
|
|
func TestTimeout(t *testing.T) {
|
|
svc, repo, prod := newTestService(t)
|
|
|
|
// ВАЖНО: чтобы текущий emitTransferStatusEvent не падал на nil,
|
|
// даем минимально ожидаемые поля + non-nil ExecutedMoney.
|
|
_ = repo.payments.Upsert(context.Background(), &storagemodel.PaymentRecord{
|
|
IdempotencyKey: "idem-4",
|
|
PaymentIntentID: "pi-4",
|
|
QuoteRef: "quote-4",
|
|
OutgoingLeg: "card",
|
|
RequestedMoney: &paymenttypes.Money{Amount: "5", Currency: "EUR"},
|
|
ExecutedMoney: &paymenttypes.Money{Amount: "0", Currency: "EUR"},
|
|
Status: storagemodel.PaymentStatusWaiting,
|
|
})
|
|
|
|
result := &model.ConfirmationResult{
|
|
RequestID: "idem-4",
|
|
Status: model.ConfirmationStatusTimeout,
|
|
RawReply: &model.TelegramMessage{ChatID: "1", MessageID: "2"},
|
|
}
|
|
|
|
_ = svc.onConfirmationResult(context.Background(), result)
|
|
|
|
rec := repo.payments.records["idem-4"]
|
|
|
|
if rec.Status != storagemodel.PaymentStatusFailed {
|
|
t.Fatalf("timeout must be failed")
|
|
}
|
|
if repo.tg.records["idem-4"] == nil {
|
|
t.Fatalf("telegram confirmation must be stored")
|
|
}
|
|
if len(prod.reactions) != 0 {
|
|
t.Fatalf("timeout must not publish reaction")
|
|
}
|
|
}
|
|
|
|
func TestIntentFromSubmitTransfer_NormalizesOutgoingLeg(t *testing.T) {
|
|
intent, err := intentFromSubmitTransfer(&chainv1.SubmitTransferRequest{
|
|
IdempotencyKey: "idem-5",
|
|
IntentRef: "pi-5",
|
|
OperationRef: "op-5",
|
|
PaymentRef: "pay-5",
|
|
Amount: &moneyv1.Money{Amount: "10", Currency: "USD"},
|
|
Metadata: map[string]string{
|
|
metadataOutgoingLeg: "card",
|
|
},
|
|
}, "provider_settlement", "")
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if got, want := intent.OutgoingLeg, discovery.RailCardPayout; got != want {
|
|
t.Fatalf("unexpected outgoing leg: got=%q want=%q", got, want)
|
|
}
|
|
}
|