refactored payment orchestration
This commit is contained in:
@@ -2,22 +2,23 @@ package gateway
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/tech/sendico/gateway/tgsettle/storage"
|
||||
storagemodel "github.com/tech/sendico/gateway/tgsettle/storage/model"
|
||||
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"
|
||||
notification "github.com/tech/sendico/pkg/model/notification"
|
||||
"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
|
||||
@@ -26,6 +27,9 @@ type fakePaymentsStore struct {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -67,286 +71,212 @@ func (f *fakeRepo) TelegramConfirmations() storage.TelegramConfirmationsStore {
|
||||
return f.tg
|
||||
}
|
||||
|
||||
type captureProducer struct {
|
||||
mu sync.Mutex
|
||||
confirmationRequests []*model.ConfirmationRequest
|
||||
executions []*model.PaymentGatewayExecution
|
||||
}
|
||||
//
|
||||
// FAKE BROKER (ОБЯЗАТЕЛЕН ДЛЯ СЕРВИСА)
|
||||
//
|
||||
|
||||
func (c *captureProducer) SendMessage(env envelope.Envelope) error {
|
||||
_, _ = env.Serialize()
|
||||
switch env.GetSignature().ToString() {
|
||||
case model.NewNotification(mservice.Notifications, notification.NAConfirmationRequest).ToString():
|
||||
var req model.ConfirmationRequest
|
||||
if err := json.Unmarshal(env.GetData(), &req); err == nil {
|
||||
c.mu.Lock()
|
||||
c.confirmationRequests = append(c.confirmationRequests, &req)
|
||||
c.mu.Unlock()
|
||||
}
|
||||
case model.NewNotification(mservice.PaymentGateway, notification.NAPaymentGatewayExecution).ToString():
|
||||
var exec model.PaymentGatewayExecution
|
||||
if err := json.Unmarshal(env.GetData(), &exec); err == nil {
|
||||
c.mu.Lock()
|
||||
c.executions = append(c.executions, &exec)
|
||||
c.mu.Unlock()
|
||||
}
|
||||
}
|
||||
type fakeBroker struct{}
|
||||
|
||||
func (f *fakeBroker) Publish(_ envelope.Envelope) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *captureProducer) Reset() {
|
||||
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()
|
||||
defer c.mu.Unlock()
|
||||
c.confirmationRequests = nil
|
||||
c.executions = nil
|
||||
c.reactions = append(c.reactions, env)
|
||||
c.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestOnIntentCreatesConfirmationRequest(t *testing.T) {
|
||||
//
|
||||
// TESTS
|
||||
//
|
||||
|
||||
func newTestService(_ *testing.T) (*Service, *fakeRepo, *captureProducer) {
|
||||
logger := mloggerfactory.NewLogger(false)
|
||||
repo := &fakeRepo{payments: &fakePaymentsStore{}, tg: &fakeTelegramStore{}}
|
||||
prod := &captureProducer{}
|
||||
t.Setenv("PGS_CHAT_ID", "-100")
|
||||
svc := NewService(logger, repo, prod, nil, Config{
|
||||
Rail: "card",
|
||||
TargetChatIDEnv: "PGS_CHAT_ID",
|
||||
TimeoutSeconds: 90,
|
||||
AcceptedUserIDs: []string{"42"},
|
||||
|
||||
repo := &fakeRepo{
|
||||
payments: &fakePaymentsStore{},
|
||||
tg: &fakeTelegramStore{},
|
||||
}
|
||||
|
||||
sigEnv := tnotifications.TelegramReaction(string(mservice.PaymentGateway), &model.TelegramReactionRequest{
|
||||
RequestID: "x",
|
||||
ChatID: "1",
|
||||
MessageID: "2",
|
||||
Emoji: "ok",
|
||||
})
|
||||
prod.Reset()
|
||||
|
||||
intent := &model.PaymentGatewayIntent{
|
||||
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",
|
||||
IdempotencyKey: "idem-1",
|
||||
OutgoingLeg: "card",
|
||||
QuoteRef: "quote-1",
|
||||
RequestedMoney: &paymenttypes.Money{Amount: "10.50", Currency: "USD"},
|
||||
TargetChatID: "",
|
||||
}
|
||||
if err := svc.onIntent(context.Background(), intent); err != nil {
|
||||
t.Fatalf("onIntent error: %v", err)
|
||||
}
|
||||
if len(prod.confirmationRequests) != 1 {
|
||||
t.Fatalf("expected 1 confirmation request, got %d", len(prod.confirmationRequests))
|
||||
}
|
||||
req := prod.confirmationRequests[0]
|
||||
if req.RequestID != "idem-1" || req.PaymentIntentID != "pi-1" || req.QuoteRef != "quote-1" {
|
||||
t.Fatalf("unexpected confirmation request fields: %#v", req)
|
||||
}
|
||||
if req.TargetChatID != "-100" {
|
||||
t.Fatalf("expected target chat id -100, got %q", req.TargetChatID)
|
||||
}
|
||||
if req.RequestedMoney == nil || req.RequestedMoney.Amount != "10.50" || req.RequestedMoney.Currency != "USD" {
|
||||
t.Fatalf("requested money mismatch: %#v", req.RequestedMoney)
|
||||
}
|
||||
if req.TimeoutSeconds != 90 {
|
||||
t.Fatalf("expected timeout 90, got %d", req.TimeoutSeconds)
|
||||
}
|
||||
if req.SourceService != string(mservice.PaymentGateway) || req.Rail != "card" {
|
||||
t.Fatalf("unexpected source/rail: %#v", req)
|
||||
}
|
||||
record := repo.payments.records["idem-1"]
|
||||
if record == nil {
|
||||
t.Fatalf("expected pending payment to be stored")
|
||||
}
|
||||
if record.Status != storagemodel.PaymentStatusPending {
|
||||
t.Fatalf("expected pending status, got %q", record.Status)
|
||||
}
|
||||
if record.RequestedMoney == nil || record.RequestedMoney.Amount != "10.50" {
|
||||
t.Fatalf("requested money not stored correctly: %#v", record.RequestedMoney)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIntentFromSubmitTransferUsesSourceMoney(t *testing.T) {
|
||||
req := &chainv1.SubmitTransferRequest{
|
||||
IdempotencyKey: "idem-1",
|
||||
ClientReference: "pi-1",
|
||||
Amount: &moneyv1.Money{Amount: "10.00", Currency: "EUR"},
|
||||
Metadata: map[string]string{
|
||||
metadataSourceAmount: "12.34",
|
||||
metadataSourceCurrency: "USD",
|
||||
},
|
||||
}
|
||||
intent, err := intentFromSubmitTransfer(req, "provider_settlement", "")
|
||||
if err != nil {
|
||||
t.Fatalf("intentFromSubmitTransfer error: %v", err)
|
||||
}
|
||||
if intent.RequestedMoney == nil || intent.RequestedMoney.Amount != "12.34" || intent.RequestedMoney.Currency != "USD" {
|
||||
t.Fatalf("expected source money override, got: %#v", intent.RequestedMoney)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfirmationResultPersistsExecutionAndReply(t *testing.T) {
|
||||
logger := mloggerfactory.NewLogger(false)
|
||||
repo := &fakeRepo{payments: &fakePaymentsStore{}, tg: &fakeTelegramStore{}}
|
||||
prod := &captureProducer{}
|
||||
svc := NewService(logger, repo, prod, nil, Config{Rail: "card"})
|
||||
intent := &model.PaymentGatewayIntent{
|
||||
PaymentIntentID: "pi-2",
|
||||
IdempotencyKey: "idem-2",
|
||||
QuoteRef: "quote-2",
|
||||
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",
|
||||
PaymentIntentID: intent.PaymentIntentID,
|
||||
QuoteRef: intent.QuoteRef,
|
||||
OutgoingLeg: intent.OutgoingLeg,
|
||||
RequestedMoney: intent.RequestedMoney,
|
||||
Status: storagemodel.PaymentStatusPending,
|
||||
IdempotencyKey: "idem-2",
|
||||
Status: storagemodel.PaymentStatusWaiting,
|
||||
})
|
||||
|
||||
result := &model.ConfirmationResult{
|
||||
RequestID: "idem-2",
|
||||
Money: &paymenttypes.Money{Amount: "5", Currency: "EUR"},
|
||||
Status: model.ConfirmationStatusConfirmed,
|
||||
RawReply: &model.TelegramMessage{ChatID: "1", MessageID: "2", Text: "5 EUR"},
|
||||
Status: model.ConfirmationStatusClarified,
|
||||
RawReply: &model.TelegramMessage{ChatID: "1", MessageID: "2"},
|
||||
}
|
||||
if err := svc.onConfirmationResult(context.Background(), result); err != nil {
|
||||
t.Fatalf("onConfirmationResult error: %v", err)
|
||||
|
||||
_ = svc.onConfirmationResult(context.Background(), result)
|
||||
|
||||
rec := repo.payments.records["idem-2"]
|
||||
|
||||
if rec.Status != storagemodel.PaymentStatusWaiting {
|
||||
t.Fatalf("clarified must not change status")
|
||||
}
|
||||
record := repo.payments.records["idem-2"]
|
||||
if record == nil {
|
||||
t.Fatalf("expected payment record to be stored")
|
||||
if repo.tg.records["idem-2"] == nil {
|
||||
t.Fatalf("telegram confirmation must be stored")
|
||||
}
|
||||
if record.Status != storagemodel.PaymentStatusExecuted {
|
||||
t.Fatalf("expected executed status, got %q", record.Status)
|
||||
}
|
||||
if record.ExecutedMoney == nil || record.ExecutedMoney.Amount != "5" {
|
||||
t.Fatalf("executed money not stored correctly: %#v", record.ExecutedMoney)
|
||||
}
|
||||
if repo.tg.records["idem-2"] == nil || repo.tg.records["idem-2"].RawReply.Text != "5 EUR" {
|
||||
t.Fatalf("telegram reply not stored correctly")
|
||||
if len(prod.reactions) != 0 {
|
||||
t.Fatalf("clarified must not publish reaction")
|
||||
}
|
||||
}
|
||||
|
||||
func TestClarifiedResultPersistsExecution(t *testing.T) {
|
||||
logger := mloggerfactory.NewLogger(false)
|
||||
repo := &fakeRepo{payments: &fakePaymentsStore{}, tg: &fakeTelegramStore{}}
|
||||
prod := &captureProducer{}
|
||||
svc := NewService(logger, repo, prod, nil, Config{Rail: "card"})
|
||||
intent := &model.PaymentGatewayIntent{
|
||||
PaymentIntentID: "pi-clarified",
|
||||
IdempotencyKey: "idem-clarified",
|
||||
QuoteRef: "quote-clarified",
|
||||
OutgoingLeg: "card",
|
||||
RequestedMoney: &paymenttypes.Money{Amount: "12", Currency: "USD"},
|
||||
}
|
||||
func TestRejected(t *testing.T) {
|
||||
svc, repo, prod := newTestService(t)
|
||||
|
||||
// ВАЖНО: чтобы текущий emitTransferStatusEvent не падал на nil,
|
||||
// даем минимально ожидаемые поля + non-nil ExecutedMoney.
|
||||
_ = repo.payments.Upsert(context.Background(), &storagemodel.PaymentRecord{
|
||||
IdempotencyKey: "idem-clarified",
|
||||
PaymentIntentID: intent.PaymentIntentID,
|
||||
QuoteRef: intent.QuoteRef,
|
||||
OutgoingLeg: intent.OutgoingLeg,
|
||||
RequestedMoney: intent.RequestedMoney,
|
||||
Status: storagemodel.PaymentStatusPending,
|
||||
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-clarified",
|
||||
Money: &paymenttypes.Money{Amount: "12", Currency: "USD"},
|
||||
Status: model.ConfirmationStatusClarified,
|
||||
RawReply: &model.TelegramMessage{ChatID: "1", MessageID: "3", Text: "12 USD"},
|
||||
RequestID: "idem-3",
|
||||
Status: model.ConfirmationStatusRejected,
|
||||
RawReply: &model.TelegramMessage{ChatID: "1", MessageID: "2"},
|
||||
}
|
||||
if err := svc.onConfirmationResult(context.Background(), result); err != nil {
|
||||
t.Fatalf("onConfirmationResult error: %v", err)
|
||||
|
||||
_ = svc.onConfirmationResult(context.Background(), result)
|
||||
|
||||
rec := repo.payments.records["idem-3"]
|
||||
|
||||
if rec.Status != storagemodel.PaymentStatusFailed {
|
||||
t.Fatalf("expected failed")
|
||||
}
|
||||
record := repo.payments.records["idem-clarified"]
|
||||
if record == nil || record.Status != storagemodel.PaymentStatusExecuted {
|
||||
t.Fatalf("expected payment executed status, got %#v", record)
|
||||
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 TestIdempotencyPreventsDuplicateWrites(t *testing.T) {
|
||||
logger := mloggerfactory.NewLogger(false)
|
||||
repo := &fakeRepo{payments: &fakePaymentsStore{records: map[string]*storagemodel.PaymentRecord{
|
||||
"idem-3": {IdempotencyKey: "idem-3", Status: storagemodel.PaymentStatusPending},
|
||||
}}, tg: &fakeTelegramStore{}}
|
||||
prod := &captureProducer{}
|
||||
svc := NewService(logger, repo, prod, nil, Config{Rail: "card"})
|
||||
intent := &model.PaymentGatewayIntent{
|
||||
PaymentIntentID: "pi-3",
|
||||
IdempotencyKey: "idem-3",
|
||||
OutgoingLeg: "card",
|
||||
QuoteRef: "quote-3",
|
||||
RequestedMoney: &paymenttypes.Money{Amount: "1", Currency: "USD"},
|
||||
TargetChatID: "chat",
|
||||
}
|
||||
if err := svc.onIntent(context.Background(), intent); err != nil {
|
||||
t.Fatalf("onIntent error: %v", err)
|
||||
}
|
||||
if len(prod.confirmationRequests) != 0 {
|
||||
t.Fatalf("expected no confirmation request for duplicate intent")
|
||||
}
|
||||
}
|
||||
func TestTimeout(t *testing.T) {
|
||||
svc, repo, prod := newTestService(t)
|
||||
|
||||
func TestTimeoutDoesNotPersistExecution(t *testing.T) {
|
||||
logger := mloggerfactory.NewLogger(false)
|
||||
repo := &fakeRepo{payments: &fakePaymentsStore{}, tg: &fakeTelegramStore{}}
|
||||
prod := &captureProducer{}
|
||||
svc := NewService(logger, repo, prod, nil, Config{Rail: "card"})
|
||||
intent := &model.PaymentGatewayIntent{
|
||||
PaymentIntentID: "pi-4",
|
||||
IdempotencyKey: "idem-4",
|
||||
QuoteRef: "quote-4",
|
||||
OutgoingLeg: "card",
|
||||
RequestedMoney: &paymenttypes.Money{Amount: "8", Currency: "USD"},
|
||||
}
|
||||
// ВАЖНО: чтобы текущий emitTransferStatusEvent не падал на nil,
|
||||
// даем минимально ожидаемые поля + non-nil ExecutedMoney.
|
||||
_ = repo.payments.Upsert(context.Background(), &storagemodel.PaymentRecord{
|
||||
IdempotencyKey: "idem-4",
|
||||
PaymentIntentID: intent.PaymentIntentID,
|
||||
QuoteRef: intent.QuoteRef,
|
||||
OutgoingLeg: intent.OutgoingLeg,
|
||||
RequestedMoney: intent.RequestedMoney,
|
||||
Status: storagemodel.PaymentStatusPending,
|
||||
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"},
|
||||
}
|
||||
if err := svc.onConfirmationResult(context.Background(), result); err != nil {
|
||||
t.Fatalf("onConfirmationResult error: %v", err)
|
||||
|
||||
_ = svc.onConfirmationResult(context.Background(), result)
|
||||
|
||||
rec := repo.payments.records["idem-4"]
|
||||
|
||||
if rec.Status != storagemodel.PaymentStatusFailed {
|
||||
t.Fatalf("timeout must be failed")
|
||||
}
|
||||
record := repo.payments.records["idem-4"]
|
||||
if record == nil || record.Status != storagemodel.PaymentStatusExpired {
|
||||
t.Fatalf("expected expired status for timeout, got %#v", record)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRejectedDoesNotPersistExecution(t *testing.T) {
|
||||
logger := mloggerfactory.NewLogger(false)
|
||||
repo := &fakeRepo{payments: &fakePaymentsStore{}, tg: &fakeTelegramStore{}}
|
||||
prod := &captureProducer{}
|
||||
svc := NewService(logger, repo, prod, nil, Config{Rail: "card"})
|
||||
intent := &model.PaymentGatewayIntent{
|
||||
PaymentIntentID: "pi-reject",
|
||||
IdempotencyKey: "idem-reject",
|
||||
QuoteRef: "quote-reject",
|
||||
OutgoingLeg: "card",
|
||||
RequestedMoney: &paymenttypes.Money{Amount: "3", Currency: "USD"},
|
||||
}
|
||||
_ = repo.payments.Upsert(context.Background(), &storagemodel.PaymentRecord{
|
||||
IdempotencyKey: "idem-reject",
|
||||
PaymentIntentID: intent.PaymentIntentID,
|
||||
QuoteRef: intent.QuoteRef,
|
||||
OutgoingLeg: intent.OutgoingLeg,
|
||||
RequestedMoney: intent.RequestedMoney,
|
||||
Status: storagemodel.PaymentStatusPending,
|
||||
})
|
||||
|
||||
result := &model.ConfirmationResult{
|
||||
RequestID: "idem-reject",
|
||||
Status: model.ConfirmationStatusRejected,
|
||||
RawReply: &model.TelegramMessage{ChatID: "1", MessageID: "4", Text: "no"},
|
||||
}
|
||||
if err := svc.onConfirmationResult(context.Background(), result); err != nil {
|
||||
t.Fatalf("onConfirmationResult error: %v", err)
|
||||
}
|
||||
record := repo.payments.records["idem-reject"]
|
||||
if record == nil || record.Status != storagemodel.PaymentStatusExpired {
|
||||
t.Fatalf("expected expired status for rejection, got %#v", record)
|
||||
}
|
||||
if repo.tg.records["idem-reject"] == nil {
|
||||
t.Fatalf("expected raw reply to be stored for rejection")
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user