Files

414 lines
11 KiB
Go

package workflow
import (
"encoding/json"
"io"
"log/slog"
"math/big"
"testing"
"time"
"github.com/hanzo-fi/go-libs/v5/pkg/observe/log"
"github.com/hanzo-fi/go-libs/v5/pkg/types/pointer"
"github.com/hanzo-fi/payments/internal/connectors"
"github.com/hanzo-fi/payments/internal/connectors/engine/activities"
"github.com/hanzo-fi/payments/ce/plugins/dummypay"
"github.com/hanzo-fi/payments/internal/connectors/plugins/registry"
"github.com/hanzo-fi/payments/pkg/domain/models"
"github.com/google/uuid"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/suite"
temporallog "go.temporal.io/sdk/log"
"go.temporal.io/sdk/testsuite"
temporalworkflow "go.temporal.io/sdk/workflow"
)
type UnitTestSuite struct {
suite.Suite
testsuite.WorkflowTestSuite
w *Workflow
a *activities.Activities
env *testsuite.TestWorkflowEnvironment
connectorID models.ConnectorID
accountID models.AccountID
paymentPayoutID models.PaymentID
paymentInitiationID models.PaymentInitiationID
paymentReversalID models.PaymentInitiationReversalID
connector models.Connector
configuredConnector models.Connector
bankAccount models.BankAccount
paymentServiceUser models.PaymentServiceUser
paymentPayout models.Payment
paymentWithAdjustmentAmount models.Payment
account models.Account
paymentInitiationPayout models.PaymentInitiation
paymentInitiationTransfer models.PaymentInitiation
paymentReversal models.PaymentInitiationReversal
pspAccount models.PSPAccount
pspPayment models.PSPPayment
pspPaymentReversed models.PSPPayment
pspBalance models.PSPBalance
pspOther models.PSPOther
}
func (s *UnitTestSuite) SetupTest() {
s.SetLogger(temporallog.NewStructuredLogger(slog.New(slog.NewTextHandler(io.Discard, nil))))
s.env = s.NewTestWorkflowEnvironment()
for _, def := range s.w.DefinitionSet() {
s.env.RegisterWorkflowWithOptions(def.Func, temporalworkflow.RegisterOptions{
Name: def.Name,
})
}
s.addData()
// EN-1093/H12: scheduleNextWorkflow now reads the polling period via a deterministic
// activity (the test env always takes the activity branch). Register it globally so any
// test that schedules is covered; .Maybe() leaves non-scheduling tests unaffected.
s.mockPollingPeriod(2 * time.Minute)
}
func (s *UnitTestSuite) AfterTest(suiteName, testName string) {
s.env.AssertExpectations(s.T())
}
// mockPollingPeriod registers the deterministic StorageConnectorsGetPollingPeriod activity
// (EN-1093/H12). Temporal's TestWorkflowEnvironment always reports the newest GetVersion, so
// scheduleNextWorkflow / create_payout / create_transfer always take the activity branch in
// tests. Registered with .Maybe() since not every test reaches the polling-period read.
func (s *UnitTestSuite) mockPollingPeriod(pollingPeriod time.Duration) {
s.env.OnActivity(activities.StorageConnectorsGetPollingPeriodActivity, mock.Anything, mock.Anything).Maybe().Return(
pollingPeriod,
nil,
)
}
func TestUnitTestSuite(t *testing.T) {
logger := logging.Testing()
w := Workflow{
connectors: connectors.NewManager(logger, true, time.Minute, time.Minute),
stackPublicURL: "http://localhost:8080",
stack: "test",
logger: logger,
}
a := activities.Activities{}
suite.Run(t, &UnitTestSuite{w: &w, a: &a})
}
func (s *UnitTestSuite) addData() {
now := s.env.Now().UTC()
s.connectorID = models.ConnectorID{
Reference: uuid.New(),
Provider: "test",
}
registry.RegisterPlugin("test", models.PluginTypePSP, func(_ models.ConnectorID, name string, logger logging.Logger, rm json.RawMessage) (models.Plugin, error) {
return dummypay.New(name, logger, rm)
}, []models.Capability{}, struct{}{}, 25)
dummyConf := json.RawMessage(`{"name":"somename","directory":"/tmp"}`)
connector := models.Connector{
ConnectorBase: models.ConnectorBase{
ID: s.connectorID,
Name: "test",
CreatedAt: now,
Provider: "test",
},
ScheduledForDeletion: false,
Config: dummyConf,
}
s.configuredConnector = connector
_, _, err := s.w.connectors.Load(s.configuredConnector, true, true)
s.NoError(err)
s.accountID = models.AccountID{
Reference: "test",
ConnectorID: s.connectorID,
}
s.paymentPayoutID = models.PaymentID{
PaymentReference: models.PaymentReference{
Reference: "test",
Type: models.PAYMENT_TYPE_PAYOUT,
},
ConnectorID: s.connectorID,
}
s.paymentInitiationID = models.PaymentInitiationID{
Reference: "test",
ConnectorID: s.connectorID,
}
s.paymentReversalID = models.PaymentInitiationReversalID{
Reference: "test",
ConnectorID: s.connectorID,
}
s.bankAccount = models.BankAccount{
ID: uuid.New(),
CreatedAt: now,
Name: "test",
Metadata: map[string]string{
"key": "value",
},
}
s.paymentServiceUser = models.PaymentServiceUser{
ID: uuid.New(),
Name: "test",
CreatedAt: now,
ContactDetails: &models.ContactDetails{
Email: pointer.For("test"),
PhoneNumber: pointer.For("test"),
},
Address: &models.Address{
StreetName: pointer.For("test"),
StreetNumber: pointer.For("test"),
City: pointer.For("test"),
Region: pointer.For("test"),
PostalCode: pointer.For("test"),
Country: pointer.For("test"),
},
BankAccountIDs: []uuid.UUID{
s.bankAccount.ID,
},
Metadata: map[string]string{
"foo": "bar",
},
}
s.paymentPayout = models.Payment{
ID: s.paymentPayoutID,
ConnectorID: s.connectorID,
Reference: "test",
CreatedAt: now,
Type: models.PAYMENT_TYPE_PAYOUT,
Amount: big.NewInt(100),
Asset: "USD/2",
Scheme: models.PAYMENT_SCHEME_A2A,
Status: models.PAYMENT_STATUS_SUCCEEDED,
SourceAccountID: &models.AccountID{
Reference: "test1",
ConnectorID: s.connectorID,
},
DestinationAccountID: &models.AccountID{
Reference: "test2",
ConnectorID: s.connectorID,
},
Metadata: map[string]string{
"key": "value",
},
Adjustments: []models.PaymentAdjustment{
{
ID: models.PaymentAdjustmentID{
PaymentID: s.paymentPayoutID,
Reference: "test",
CreatedAt: now,
Status: models.PAYMENT_STATUS_SUCCEEDED,
},
Reference: "test",
CreatedAt: now,
Status: models.PAYMENT_STATUS_SUCCEEDED,
Amount: big.NewInt(100),
Asset: pointer.For("USD/2"),
Metadata: map[string]string{
"key": "value",
},
Raw: []byte(`{}`),
},
},
}
s.paymentWithAdjustmentAmount = models.Payment{
ID: s.paymentPayoutID,
ConnectorID: s.connectorID,
Reference: "test",
CreatedAt: now,
Type: models.PAYMENT_TYPE_PAYOUT,
Amount: big.NewInt(100),
Asset: "USD/2",
Scheme: models.PAYMENT_SCHEME_A2A,
Status: models.PAYMENT_STATUS_AMOUNT_ADJUSTMENT,
SourceAccountID: &models.AccountID{
Reference: "test1",
ConnectorID: s.connectorID,
},
DestinationAccountID: &models.AccountID{
Reference: "test2",
ConnectorID: s.connectorID,
},
Metadata: map[string]string{
"key": "value",
},
Adjustments: []models.PaymentAdjustment{
{
ID: models.PaymentAdjustmentID{
PaymentID: s.paymentPayoutID,
Reference: "test",
CreatedAt: now,
Status: models.PAYMENT_STATUS_AMOUNT_ADJUSTMENT,
},
Reference: "test",
CreatedAt: now,
Status: models.PAYMENT_STATUS_AMOUNT_ADJUSTMENT,
Amount: big.NewInt(100),
Asset: pointer.For("USD/2"),
Metadata: map[string]string{
"key": "value",
},
Raw: []byte(`{}`),
},
},
}
s.paymentInitiationPayout = models.PaymentInitiation{
ID: s.paymentInitiationID,
ConnectorID: s.connectorID,
Reference: "test",
CreatedAt: s.env.Now().UTC(),
ScheduledAt: s.env.Now().UTC(),
Description: "test_payout",
Type: models.PAYMENT_INITIATION_TYPE_PAYOUT,
SourceAccountID: &models.AccountID{
Reference: "test1",
ConnectorID: s.connectorID,
},
DestinationAccountID: &models.AccountID{
Reference: "test2",
ConnectorID: s.connectorID,
},
Amount: big.NewInt(100),
Asset: "USD/2",
Metadata: map[string]string{
"key": "value",
},
}
s.paymentInitiationTransfer = models.PaymentInitiation{
ID: s.paymentInitiationID,
ConnectorID: s.connectorID,
Reference: "test",
CreatedAt: s.env.Now().UTC(),
ScheduledAt: s.env.Now().UTC(),
Description: "test_payout",
Type: models.PAYMENT_INITIATION_TYPE_TRANSFER,
SourceAccountID: &models.AccountID{
Reference: "test1",
ConnectorID: s.connectorID,
},
DestinationAccountID: &models.AccountID{
Reference: "test2",
ConnectorID: s.connectorID,
},
Amount: big.NewInt(100),
Asset: "USD/2",
Metadata: map[string]string{
"key": "value",
},
}
s.paymentReversal = models.PaymentInitiationReversal{
ID: s.paymentReversalID,
ConnectorID: s.connectorID,
PaymentInitiationID: s.paymentInitiationID,
Reference: "test",
CreatedAt: now,
Description: "test",
Amount: big.NewInt(50),
Asset: "USD/2",
Metadata: map[string]string{
"key": "value",
},
}
s.connector = models.Connector{
ConnectorBase: models.ConnectorBase{
ID: s.connectorID,
Name: "test",
CreatedAt: now,
Provider: "test",
},
ScheduledForDeletion: false,
Config: []byte(`{"name": "test", "pollingPeriod": "2m", "pageSize": 25}`),
}
s.account = models.Account{
ID: models.AccountID{
Reference: "test1",
ConnectorID: s.connectorID,
},
ConnectorID: s.connectorID,
Reference: "test1",
CreatedAt: s.env.Now().UTC(),
Type: models.ACCOUNT_TYPE_INTERNAL,
Name: pointer.For("test1"),
DefaultAsset: pointer.For("USD/2"),
Metadata: map[string]string{
"key": "value",
},
Raw: []byte(`{}`),
}
s.pspAccount = models.PSPAccount{
Reference: "test",
CreatedAt: s.env.Now().UTC(),
Name: pointer.For("test"),
DefaultAsset: pointer.For("USD/2"),
Metadata: map[string]string{
"key": "value",
},
Raw: []byte(`{}`),
}
s.pspPayment = models.PSPPayment{
Reference: "test",
CreatedAt: now,
Type: models.PAYMENT_TYPE_PAYOUT,
Amount: big.NewInt(100),
Asset: "USD/2",
Scheme: models.PAYMENT_SCHEME_A2A,
Status: models.PAYMENT_STATUS_SUCCEEDED,
SourceAccountReference: pointer.For("test1"),
DestinationAccountReference: pointer.For("test2"),
Metadata: map[string]string{
"key": "value",
},
Raw: []byte(`{}`),
}
s.pspPaymentReversed = models.PSPPayment{
Reference: "test",
CreatedAt: now,
Type: models.PAYMENT_TYPE_PAYOUT,
Amount: big.NewInt(100),
Asset: "USD/2",
Scheme: models.PAYMENT_SCHEME_A2A,
Status: models.PAYMENT_STATUS_REFUNDED,
SourceAccountReference: pointer.For("test1"),
DestinationAccountReference: pointer.For("test2"),
Metadata: map[string]string{
"key": "value",
},
Raw: []byte(`{}`),
}
s.pspBalance = models.PSPBalance{
AccountReference: "test",
CreatedAt: now,
Amount: big.NewInt(100),
Asset: "USD/2",
}
s.pspOther = models.PSPOther{
ID: "test",
Other: []byte(`{}`),
}
}