From 978c0854d75e7b317e85d2019bfebaa393335135 Mon Sep 17 00:00:00 2001 From: Crimson Thompson Date: Tue, 8 Oct 2024 11:18:21 +0200 Subject: [PATCH 01/13] feat(payments): add mutex to wise client to protect recipient cache --- internal/connectors/plugins/public/wise/client/client.go | 3 +++ .../plugins/public/wise/client/recipient_accounts.go | 2 ++ 2 files changed, 5 insertions(+) diff --git a/internal/connectors/plugins/public/wise/client/client.go b/internal/connectors/plugins/public/wise/client/client.go index c43b1ad5..d08d394c 100644 --- a/internal/connectors/plugins/public/wise/client/client.go +++ b/internal/connectors/plugins/public/wise/client/client.go @@ -3,6 +3,7 @@ package client import ( "fmt" "net/http" + "sync" "github.com/formancehq/payments/internal/connectors/httpwrapper" lru "github.com/hashicorp/golang-lru/v2" @@ -25,6 +26,7 @@ func (t *apiTransport) RoundTrip(req *http.Request) (*http.Response, error) { type Client struct { httpClient httpwrapper.Client + mux *sync.Mutex recipientAccountsCache *lru.Cache[uint64, *RecipientAccount] } @@ -44,6 +46,7 @@ func New(apiKey string) (*Client, error) { httpClient, err := httpwrapper.NewClient(config) return &Client{ httpClient: httpClient, + mux: &sync.Mutex{}, recipientAccountsCache: recipientsCache, }, err } diff --git a/internal/connectors/plugins/public/wise/client/recipient_accounts.go b/internal/connectors/plugins/public/wise/client/recipient_accounts.go index 6c753402..b609d034 100644 --- a/internal/connectors/plugins/public/wise/client/recipient_accounts.go +++ b/internal/connectors/plugins/public/wise/client/recipient_accounts.go @@ -58,6 +58,8 @@ func (c *Client) GetRecipientAccount(ctx context.Context, accountID uint64) (*Re // now := time.Now() // defer f(ctx, now) + c.mux.Lock() + defer c.mux.Unlock() if rc, ok := c.recipientAccountsCache.Get(accountID); ok { return rc, nil } From ef1ff130cbe3e58f5a84ff1c5a44052b8648b7db Mon Sep 17 00:00:00 2001 From: Crimson Thompson Date: Tue, 8 Oct 2024 11:22:51 +0200 Subject: [PATCH 02/13] feat(payments): use pointer receiver for wise plugin for consistency --- .../plugins/public/wise/accounts.go | 2 +- .../plugins/public/wise/balances.go | 2 +- .../plugins/public/wise/external_accounts.go | 2 +- .../plugins/public/wise/payments.go | 2 +- .../connectors/plugins/public/wise/plugin.go | 22 +++++++++---------- .../plugins/public/wise/profiles.go | 2 +- .../plugins/public/wise/uninstall.go | 2 +- .../plugins/public/wise/webhooks.go | 8 +++---- 8 files changed, 21 insertions(+), 21 deletions(-) diff --git a/internal/connectors/plugins/public/wise/accounts.go b/internal/connectors/plugins/public/wise/accounts.go index 9c459c75..a63c859a 100644 --- a/internal/connectors/plugins/public/wise/accounts.go +++ b/internal/connectors/plugins/public/wise/accounts.go @@ -16,7 +16,7 @@ type accountsState struct { LastAccountID uint64 `json:"lastAccountID"` } -func (p Plugin) fetchNextAccounts(ctx context.Context, req models.FetchNextAccountsRequest) (models.FetchNextAccountsResponse, error) { +func (p *Plugin) fetchNextAccounts(ctx context.Context, req models.FetchNextAccountsRequest) (models.FetchNextAccountsResponse, error) { var oldState accountsState if req.State != nil { if err := json.Unmarshal(req.State, &oldState); err != nil { diff --git a/internal/connectors/plugins/public/wise/balances.go b/internal/connectors/plugins/public/wise/balances.go index 69f15067..a706ee2d 100644 --- a/internal/connectors/plugins/public/wise/balances.go +++ b/internal/connectors/plugins/public/wise/balances.go @@ -10,7 +10,7 @@ import ( "github.com/formancehq/payments/internal/models" ) -func (p Plugin) fetchNextBalances(ctx context.Context, req models.FetchNextBalancesRequest) (models.FetchNextBalancesResponse, error) { +func (p *Plugin) fetchNextBalances(ctx context.Context, req models.FetchNextBalancesRequest) (models.FetchNextBalancesResponse, error) { var from models.PSPAccount if req.FromPayload == nil { return models.FetchNextBalancesResponse{}, models.ErrMissingFromPayloadInRequest diff --git a/internal/connectors/plugins/public/wise/external_accounts.go b/internal/connectors/plugins/public/wise/external_accounts.go index b143a94f..f1a8bf71 100644 --- a/internal/connectors/plugins/public/wise/external_accounts.go +++ b/internal/connectors/plugins/public/wise/external_accounts.go @@ -16,7 +16,7 @@ type externalAccountsState struct { LastSeekPosition uint64 `json:"lastSeekPosition"` } -func (p Plugin) fetchExternalAccounts(ctx context.Context, req models.FetchNextExternalAccountsRequest) (models.FetchNextExternalAccountsResponse, error) { +func (p *Plugin) fetchExternalAccounts(ctx context.Context, req models.FetchNextExternalAccountsRequest) (models.FetchNextExternalAccountsResponse, error) { var oldState externalAccountsState if req.State != nil { if err := json.Unmarshal(req.State, &oldState); err != nil { diff --git a/internal/connectors/plugins/public/wise/payments.go b/internal/connectors/plugins/public/wise/payments.go index 43951f5a..83a6873c 100644 --- a/internal/connectors/plugins/public/wise/payments.go +++ b/internal/connectors/plugins/public/wise/payments.go @@ -15,7 +15,7 @@ type paymentsState struct { Offset int } -func (p Plugin) fetchNextPayments(ctx context.Context, req models.FetchNextPaymentsRequest) (models.FetchNextPaymentsResponse, error) { +func (p *Plugin) fetchNextPayments(ctx context.Context, req models.FetchNextPaymentsRequest) (models.FetchNextPaymentsResponse, error) { var oldState paymentsState if req.State != nil { if err := json.Unmarshal(req.State, &oldState); err != nil { diff --git a/internal/connectors/plugins/public/wise/plugin.go b/internal/connectors/plugins/public/wise/plugin.go index ddcd26c7..5a25e953 100644 --- a/internal/connectors/plugins/public/wise/plugin.go +++ b/internal/connectors/plugins/public/wise/plugin.go @@ -62,39 +62,39 @@ func (p *Plugin) Install(ctx context.Context, req models.InstallRequest) (models }, nil } -func (p Plugin) Uninstall(ctx context.Context, req models.UninstallRequest) (models.UninstallResponse, error) { +func (p *Plugin) Uninstall(ctx context.Context, req models.UninstallRequest) (models.UninstallResponse, error) { return p.uninstall(ctx, req) } -func (p Plugin) FetchNextAccounts(ctx context.Context, req models.FetchNextAccountsRequest) (models.FetchNextAccountsResponse, error) { +func (p *Plugin) FetchNextAccounts(ctx context.Context, req models.FetchNextAccountsRequest) (models.FetchNextAccountsResponse, error) { if p.client == nil { return models.FetchNextAccountsResponse{}, plugins.ErrNotYetInstalled } return p.fetchNextAccounts(ctx, req) } -func (p Plugin) FetchNextBalances(ctx context.Context, req models.FetchNextBalancesRequest) (models.FetchNextBalancesResponse, error) { +func (p *Plugin) FetchNextBalances(ctx context.Context, req models.FetchNextBalancesRequest) (models.FetchNextBalancesResponse, error) { if p.client == nil { return models.FetchNextBalancesResponse{}, plugins.ErrNotYetInstalled } return p.fetchNextBalances(ctx, req) } -func (p Plugin) FetchNextExternalAccounts(ctx context.Context, req models.FetchNextExternalAccountsRequest) (models.FetchNextExternalAccountsResponse, error) { +func (p *Plugin) FetchNextExternalAccounts(ctx context.Context, req models.FetchNextExternalAccountsRequest) (models.FetchNextExternalAccountsResponse, error) { if p.client == nil { return models.FetchNextExternalAccountsResponse{}, plugins.ErrNotYetInstalled } return p.fetchExternalAccounts(ctx, req) } -func (p Plugin) FetchNextPayments(ctx context.Context, req models.FetchNextPaymentsRequest) (models.FetchNextPaymentsResponse, error) { +func (p *Plugin) FetchNextPayments(ctx context.Context, req models.FetchNextPaymentsRequest) (models.FetchNextPaymentsResponse, error) { if p.client == nil { return models.FetchNextPaymentsResponse{}, plugins.ErrNotYetInstalled } return p.fetchNextPayments(ctx, req) } -func (p Plugin) FetchNextOthers(ctx context.Context, req models.FetchNextOthersRequest) (models.FetchNextOthersResponse, error) { +func (p *Plugin) FetchNextOthers(ctx context.Context, req models.FetchNextOthersRequest) (models.FetchNextOthersResponse, error) { if p.client == nil { return models.FetchNextOthersResponse{}, plugins.ErrNotYetInstalled } @@ -107,26 +107,26 @@ func (p Plugin) FetchNextOthers(ctx context.Context, req models.FetchNextOthersR } } -func (p Plugin) CreateBankAccount(ctx context.Context, req models.CreateBankAccountRequest) (models.CreateBankAccountResponse, error) { +func (p *Plugin) CreateBankAccount(ctx context.Context, req models.CreateBankAccountRequest) (models.CreateBankAccountResponse, error) { return models.CreateBankAccountResponse{}, plugins.ErrNotImplemented } -func (p Plugin) CreateTransfer(ctx context.Context, req models.CreateTransferRequest) (models.CreateTransferResponse, error) { +func (p *Plugin) CreateTransfer(ctx context.Context, req models.CreateTransferRequest) (models.CreateTransferResponse, error) { return models.CreateTransferResponse{}, plugins.ErrNotImplemented } -func (p Plugin) CreatePayout(ctx context.Context, req models.CreatePayoutRequest) (models.CreatePayoutResponse, error) { +func (p *Plugin) CreatePayout(ctx context.Context, req models.CreatePayoutRequest) (models.CreatePayoutResponse, error) { return models.CreatePayoutResponse{}, plugins.ErrNotImplemented } -func (p Plugin) CreateWebhooks(ctx context.Context, req models.CreateWebhooksRequest) (models.CreateWebhooksResponse, error) { +func (p *Plugin) CreateWebhooks(ctx context.Context, req models.CreateWebhooksRequest) (models.CreateWebhooksResponse, error) { if p.client == nil { return models.CreateWebhooksResponse{}, plugins.ErrNotYetInstalled } return p.createWebhooks(ctx, req) } -func (p Plugin) TranslateWebhook(ctx context.Context, req models.TranslateWebhookRequest) (models.TranslateWebhookResponse, error) { +func (p *Plugin) TranslateWebhook(ctx context.Context, req models.TranslateWebhookRequest) (models.TranslateWebhookResponse, error) { if p.client == nil { return models.TranslateWebhookResponse{}, plugins.ErrNotYetInstalled } diff --git a/internal/connectors/plugins/public/wise/profiles.go b/internal/connectors/plugins/public/wise/profiles.go index 1b3ae801..10febfad 100644 --- a/internal/connectors/plugins/public/wise/profiles.go +++ b/internal/connectors/plugins/public/wise/profiles.go @@ -13,7 +13,7 @@ type profilesState struct { LastProfileID uint64 `json:"lastProfileID"` } -func (p Plugin) fetchNextProfiles(ctx context.Context, req models.FetchNextOthersRequest) (models.FetchNextOthersResponse, error) { +func (p *Plugin) fetchNextProfiles(ctx context.Context, req models.FetchNextOthersRequest) (models.FetchNextOthersResponse, error) { var oldState profilesState if req.State != nil { if err := json.Unmarshal(req.State, &oldState); err != nil { diff --git a/internal/connectors/plugins/public/wise/uninstall.go b/internal/connectors/plugins/public/wise/uninstall.go index b44ac2ed..11e9c7dc 100644 --- a/internal/connectors/plugins/public/wise/uninstall.go +++ b/internal/connectors/plugins/public/wise/uninstall.go @@ -7,7 +7,7 @@ import ( "github.com/formancehq/payments/internal/models" ) -func (p Plugin) uninstall(ctx context.Context, req models.UninstallRequest) (models.UninstallResponse, error) { +func (p *Plugin) uninstall(ctx context.Context, req models.UninstallRequest) (models.UninstallResponse, error) { profiles, err := p.client.GetProfiles(ctx) if err != nil { return models.UninstallResponse{}, err diff --git a/internal/connectors/plugins/public/wise/webhooks.go b/internal/connectors/plugins/public/wise/webhooks.go index 7f5c5fbc..8db61a25 100644 --- a/internal/connectors/plugins/public/wise/webhooks.go +++ b/internal/connectors/plugins/public/wise/webhooks.go @@ -26,7 +26,7 @@ type webhookConfig struct { var webhookConfigs map[string]webhookConfig -func (p Plugin) createWebhooks(ctx context.Context, req models.CreateWebhooksRequest) (models.CreateWebhooksResponse, error) { +func (p *Plugin) createWebhooks(ctx context.Context, req models.CreateWebhooksRequest) (models.CreateWebhooksResponse, error) { var from client.Profile if req.FromPayload == nil { return models.CreateWebhooksResponse{}, models.ErrMissingFromPayloadInRequest @@ -67,7 +67,7 @@ func (p Plugin) createWebhooks(ctx context.Context, req models.CreateWebhooksReq }, nil } -func (p Plugin) translateTransferStateChangedWebhook(ctx context.Context, req models.TranslateWebhookRequest) (models.WebhookResponse, error) { +func (p *Plugin) translateTransferStateChangedWebhook(ctx context.Context, req models.TranslateWebhookRequest) (models.WebhookResponse, error) { transfer, err := p.client.TranslateTransferStateChangedWebhook(ctx, req.Webhook.Body) if err != nil { return models.WebhookResponse{}, err @@ -83,7 +83,7 @@ func (p Plugin) translateTransferStateChangedWebhook(ctx context.Context, req mo }, nil } -func (p Plugin) translateBalanceUpdateWebhook(ctx context.Context, req models.TranslateWebhookRequest) (models.WebhookResponse, error) { +func (p *Plugin) translateBalanceUpdateWebhook(ctx context.Context, req models.TranslateWebhookRequest) (models.WebhookResponse, error) { update, err := p.client.TranslateBalanceUpdateWebhook(ctx, req.Webhook.Body) if err != nil { return models.WebhookResponse{}, err @@ -139,7 +139,7 @@ func (p Plugin) translateBalanceUpdateWebhook(ctx context.Context, req models.Tr }, nil } -func (p Plugin) verifySignature(body []byte, signature string) error { +func (p *Plugin) verifySignature(body []byte, signature string) error { msgHash := sha256.New() _, err := msgHash.Write(body) if err != nil { From f550ae834d5121f896dce84ef4cb4753cac856a7 Mon Sep 17 00:00:00 2001 From: Crimson Thompson Date: Tue, 8 Oct 2024 11:38:24 +0200 Subject: [PATCH 03/13] feat(payments): make wise client an interface and generate mock --- .../plugins/public/wise/client/balances.go | 4 +- .../plugins/public/wise/client/client.go | 30 +- .../public/wise/client/client_generated.go | 280 ++++++++++++++++++ .../plugins/public/wise/client/payouts.go | 4 +- .../plugins/public/wise/client/profiles.go | 2 +- .../plugins/public/wise/client/quotes.go | 2 +- .../public/wise/client/recipient_accounts.go | 4 +- .../plugins/public/wise/client/transfers.go | 6 +- .../plugins/public/wise/client/webhooks.go | 10 +- .../connectors/plugins/public/wise/plugin.go | 2 +- 10 files changed, 323 insertions(+), 21 deletions(-) create mode 100644 internal/connectors/plugins/public/wise/client/client_generated.go diff --git a/internal/connectors/plugins/public/wise/client/balances.go b/internal/connectors/plugins/public/wise/client/balances.go index 51f4c9c6..6da16bfe 100644 --- a/internal/connectors/plugins/public/wise/client/balances.go +++ b/internal/connectors/plugins/public/wise/client/balances.go @@ -34,7 +34,7 @@ type Balance struct { Visible bool `json:"visible"` } -func (c *Client) GetBalances(ctx context.Context, profileID uint64) ([]Balance, error) { +func (c *client) GetBalances(ctx context.Context, profileID uint64) ([]Balance, error) { // TODO(polo): metrics // f := connectors.ClientMetrics(ctx, "wise", "list_balances") // now := time.Now() @@ -55,7 +55,7 @@ func (c *Client) GetBalances(ctx context.Context, profileID uint64) ([]Balance, return balances, nil } -func (c *Client) GetBalance(ctx context.Context, profileID uint64, balanceID uint64) (*Balance, error) { +func (c *client) GetBalance(ctx context.Context, profileID uint64, balanceID uint64) (*Balance, error) { // TODO(polo): metrics // f := connectors.ClientMetrics(ctx, "wise", "list_balances") // now := time.Now() diff --git a/internal/connectors/plugins/public/wise/client/client.go b/internal/connectors/plugins/public/wise/client/client.go index d08d394c..67693f92 100644 --- a/internal/connectors/plugins/public/wise/client/client.go +++ b/internal/connectors/plugins/public/wise/client/client.go @@ -1,6 +1,8 @@ package client import ( + "context" + "encoding/json" "fmt" "net/http" "sync" @@ -23,18 +25,38 @@ func (t *apiTransport) RoundTrip(req *http.Request) (*http.Response, error) { return t.underlying.RoundTrip(req) } -type Client struct { +//go:generate mockgen -source client.go -destination client_generated.go -package client . Client +type Client interface { + GetBalance(ctx context.Context, profileID uint64, balanceID uint64) (*Balance, error) + GetBalances(ctx context.Context, profileID uint64) ([]Balance, error) + GetPayout(ctx context.Context, payoutID string) (*Payout, error) + CreatePayout(ctx context.Context, quote Quote, targetAccount uint64, transactionID string) (*Payout, error) + GetProfiles(ctx context.Context) ([]Profile, error) + CreateQuote(ctx context.Context, profileID, currency string, amount json.Number) (Quote, error) + GetRecipientAccounts(ctx context.Context, profileID uint64, pageSize int, seekPositionForNext uint64) (*RecipientAccountsResponse, error) + GetRecipientAccount(ctx context.Context, accountID uint64) (*RecipientAccount, error) + GetTransfers(ctx context.Context, profileID uint64, offset int, limit int) ([]Transfer, error) + GetTransfer(ctx context.Context, transferID string) (*Transfer, error) + CreateTransfer(ctx context.Context, quote Quote, targetAccount uint64, transactionID string) (*Transfer, error) + CreateWebhook(ctx context.Context, profileID uint64, name, triggerOn, url, version string) (*webhookSubscriptionResponse, error) + ListWebhooksSubscription(ctx context.Context, profileID uint64) ([]webhookSubscriptionResponse, error) + DeleteWebhooks(ctx context.Context, profileID uint64, subscriptionID string) error + TranslateTransferStateChangedWebhook(ctx context.Context, payload []byte) (Transfer, error) + TranslateBalanceUpdateWebhook(ctx context.Context, payload []byte) (balanceUpdateWebhookPayload, error) +} + +type client struct { httpClient httpwrapper.Client mux *sync.Mutex recipientAccountsCache *lru.Cache[uint64, *RecipientAccount] } -func (w *Client) endpoint(path string) string { +func (c *client) endpoint(path string) string { return fmt.Sprintf("%s/%s", apiEndpoint, path) } -func New(apiKey string) (*Client, error) { +func New(apiKey string) (Client, error) { recipientsCache, _ := lru.New[uint64, *RecipientAccount](2048) config := &httpwrapper.Config{ Transport: &apiTransport{ @@ -44,7 +66,7 @@ func New(apiKey string) (*Client, error) { } httpClient, err := httpwrapper.NewClient(config) - return &Client{ + return &client{ httpClient: httpClient, mux: &sync.Mutex{}, recipientAccountsCache: recipientsCache, diff --git a/internal/connectors/plugins/public/wise/client/client_generated.go b/internal/connectors/plugins/public/wise/client/client_generated.go new file mode 100644 index 00000000..46020c97 --- /dev/null +++ b/internal/connectors/plugins/public/wise/client/client_generated.go @@ -0,0 +1,280 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: client.go +// +// Generated by this command: +// +// mockgen -source client.go -destination client_generated.go -package client . Client +// + +// Package client is a generated GoMock package. +package client + +import ( + context "context" + json "encoding/json" + reflect "reflect" + + gomock "go.uber.org/mock/gomock" +) + +// MockClient is a mock of Client interface. +type MockClient struct { + ctrl *gomock.Controller + recorder *MockClientMockRecorder +} + +// MockClientMockRecorder is the mock recorder for MockClient. +type MockClientMockRecorder struct { + mock *MockClient +} + +// NewMockClient creates a new mock instance. +func NewMockClient(ctrl *gomock.Controller) *MockClient { + mock := &MockClient{ctrl: ctrl} + mock.recorder = &MockClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockClient) EXPECT() *MockClientMockRecorder { + return m.recorder +} + +// CreatePayout mocks base method. +func (m *MockClient) CreatePayout(ctx context.Context, quote Quote, targetAccount uint64, transactionID string) (*Payout, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreatePayout", ctx, quote, targetAccount, transactionID) + ret0, _ := ret[0].(*Payout) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CreatePayout indicates an expected call of CreatePayout. +func (mr *MockClientMockRecorder) CreatePayout(ctx, quote, targetAccount, transactionID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreatePayout", reflect.TypeOf((*MockClient)(nil).CreatePayout), ctx, quote, targetAccount, transactionID) +} + +// CreateQuote mocks base method. +func (m *MockClient) CreateQuote(ctx context.Context, profileID, currency string, amount json.Number) (Quote, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateQuote", ctx, profileID, currency, amount) + ret0, _ := ret[0].(Quote) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CreateQuote indicates an expected call of CreateQuote. +func (mr *MockClientMockRecorder) CreateQuote(ctx, profileID, currency, amount any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateQuote", reflect.TypeOf((*MockClient)(nil).CreateQuote), ctx, profileID, currency, amount) +} + +// CreateTransfer mocks base method. +func (m *MockClient) CreateTransfer(ctx context.Context, quote Quote, targetAccount uint64, transactionID string) (*Transfer, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateTransfer", ctx, quote, targetAccount, transactionID) + ret0, _ := ret[0].(*Transfer) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CreateTransfer indicates an expected call of CreateTransfer. +func (mr *MockClientMockRecorder) CreateTransfer(ctx, quote, targetAccount, transactionID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateTransfer", reflect.TypeOf((*MockClient)(nil).CreateTransfer), ctx, quote, targetAccount, transactionID) +} + +// CreateWebhook mocks base method. +func (m *MockClient) CreateWebhook(ctx context.Context, profileID uint64, name, triggerOn, url, version string) (*webhookSubscriptionResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateWebhook", ctx, profileID, name, triggerOn, url, version) + ret0, _ := ret[0].(*webhookSubscriptionResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CreateWebhook indicates an expected call of CreateWebhook. +func (mr *MockClientMockRecorder) CreateWebhook(ctx, profileID, name, triggerOn, url, version any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateWebhook", reflect.TypeOf((*MockClient)(nil).CreateWebhook), ctx, profileID, name, triggerOn, url, version) +} + +// DeleteWebhooks mocks base method. +func (m *MockClient) DeleteWebhooks(ctx context.Context, profileID uint64, subscriptionID string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteWebhooks", ctx, profileID, subscriptionID) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteWebhooks indicates an expected call of DeleteWebhooks. +func (mr *MockClientMockRecorder) DeleteWebhooks(ctx, profileID, subscriptionID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteWebhooks", reflect.TypeOf((*MockClient)(nil).DeleteWebhooks), ctx, profileID, subscriptionID) +} + +// GetBalance mocks base method. +func (m *MockClient) GetBalance(ctx context.Context, profileID, balanceID uint64) (*Balance, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetBalance", ctx, profileID, balanceID) + ret0, _ := ret[0].(*Balance) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetBalance indicates an expected call of GetBalance. +func (mr *MockClientMockRecorder) GetBalance(ctx, profileID, balanceID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBalance", reflect.TypeOf((*MockClient)(nil).GetBalance), ctx, profileID, balanceID) +} + +// GetBalances mocks base method. +func (m *MockClient) GetBalances(ctx context.Context, profileID uint64) ([]Balance, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetBalances", ctx, profileID) + ret0, _ := ret[0].([]Balance) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetBalances indicates an expected call of GetBalances. +func (mr *MockClientMockRecorder) GetBalances(ctx, profileID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBalances", reflect.TypeOf((*MockClient)(nil).GetBalances), ctx, profileID) +} + +// GetPayout mocks base method. +func (m *MockClient) GetPayout(ctx context.Context, payoutID string) (*Payout, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetPayout", ctx, payoutID) + ret0, _ := ret[0].(*Payout) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetPayout indicates an expected call of GetPayout. +func (mr *MockClientMockRecorder) GetPayout(ctx, payoutID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPayout", reflect.TypeOf((*MockClient)(nil).GetPayout), ctx, payoutID) +} + +// GetProfiles mocks base method. +func (m *MockClient) GetProfiles(ctx context.Context) ([]Profile, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetProfiles", ctx) + ret0, _ := ret[0].([]Profile) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetProfiles indicates an expected call of GetProfiles. +func (mr *MockClientMockRecorder) GetProfiles(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetProfiles", reflect.TypeOf((*MockClient)(nil).GetProfiles), ctx) +} + +// GetRecipientAccount mocks base method. +func (m *MockClient) GetRecipientAccount(ctx context.Context, accountID uint64) (*RecipientAccount, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetRecipientAccount", ctx, accountID) + ret0, _ := ret[0].(*RecipientAccount) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetRecipientAccount indicates an expected call of GetRecipientAccount. +func (mr *MockClientMockRecorder) GetRecipientAccount(ctx, accountID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRecipientAccount", reflect.TypeOf((*MockClient)(nil).GetRecipientAccount), ctx, accountID) +} + +// GetRecipientAccounts mocks base method. +func (m *MockClient) GetRecipientAccounts(ctx context.Context, profileID uint64, pageSize int, seekPositionForNext uint64) (*RecipientAccountsResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetRecipientAccounts", ctx, profileID, pageSize, seekPositionForNext) + ret0, _ := ret[0].(*RecipientAccountsResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetRecipientAccounts indicates an expected call of GetRecipientAccounts. +func (mr *MockClientMockRecorder) GetRecipientAccounts(ctx, profileID, pageSize, seekPositionForNext any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRecipientAccounts", reflect.TypeOf((*MockClient)(nil).GetRecipientAccounts), ctx, profileID, pageSize, seekPositionForNext) +} + +// GetTransfer mocks base method. +func (m *MockClient) GetTransfer(ctx context.Context, transferID string) (*Transfer, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetTransfer", ctx, transferID) + ret0, _ := ret[0].(*Transfer) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetTransfer indicates an expected call of GetTransfer. +func (mr *MockClientMockRecorder) GetTransfer(ctx, transferID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTransfer", reflect.TypeOf((*MockClient)(nil).GetTransfer), ctx, transferID) +} + +// GetTransfers mocks base method. +func (m *MockClient) GetTransfers(ctx context.Context, profileID uint64, offset, limit int) ([]Transfer, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetTransfers", ctx, profileID, offset, limit) + ret0, _ := ret[0].([]Transfer) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetTransfers indicates an expected call of GetTransfers. +func (mr *MockClientMockRecorder) GetTransfers(ctx, profileID, offset, limit any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTransfers", reflect.TypeOf((*MockClient)(nil).GetTransfers), ctx, profileID, offset, limit) +} + +// ListWebhooksSubscription mocks base method. +func (m *MockClient) ListWebhooksSubscription(ctx context.Context, profileID uint64) ([]webhookSubscriptionResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListWebhooksSubscription", ctx, profileID) + ret0, _ := ret[0].([]webhookSubscriptionResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListWebhooksSubscription indicates an expected call of ListWebhooksSubscription. +func (mr *MockClientMockRecorder) ListWebhooksSubscription(ctx, profileID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListWebhooksSubscription", reflect.TypeOf((*MockClient)(nil).ListWebhooksSubscription), ctx, profileID) +} + +// TranslateBalanceUpdateWebhook mocks base method. +func (m *MockClient) TranslateBalanceUpdateWebhook(ctx context.Context, payload []byte) (balanceUpdateWebhookPayload, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "TranslateBalanceUpdateWebhook", ctx, payload) + ret0, _ := ret[0].(balanceUpdateWebhookPayload) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// TranslateBalanceUpdateWebhook indicates an expected call of TranslateBalanceUpdateWebhook. +func (mr *MockClientMockRecorder) TranslateBalanceUpdateWebhook(ctx, payload any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TranslateBalanceUpdateWebhook", reflect.TypeOf((*MockClient)(nil).TranslateBalanceUpdateWebhook), ctx, payload) +} + +// TranslateTransferStateChangedWebhook mocks base method. +func (m *MockClient) TranslateTransferStateChangedWebhook(ctx context.Context, payload []byte) (Transfer, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "TranslateTransferStateChangedWebhook", ctx, payload) + ret0, _ := ret[0].(Transfer) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// TranslateTransferStateChangedWebhook indicates an expected call of TranslateTransferStateChangedWebhook. +func (mr *MockClientMockRecorder) TranslateTransferStateChangedWebhook(ctx, payload any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TranslateTransferStateChangedWebhook", reflect.TypeOf((*MockClient)(nil).TranslateTransferStateChangedWebhook), ctx, payload) +} diff --git a/internal/connectors/plugins/public/wise/client/payouts.go b/internal/connectors/plugins/public/wise/client/payouts.go index 985cd060..1272e6b1 100644 --- a/internal/connectors/plugins/public/wise/client/payouts.go +++ b/internal/connectors/plugins/public/wise/client/payouts.go @@ -59,7 +59,7 @@ func (t *Payout) UnmarshalJSON(data []byte) error { return nil } -func (c *Client) GetPayout(ctx context.Context, payoutID string) (*Payout, error) { +func (c *client) GetPayout(ctx context.Context, payoutID string) (*Payout, error) { // TODO(polo): metrics // f := connectors.ClientMetrics(ctx, "wise", "get_payout") // now := time.Now() @@ -80,7 +80,7 @@ func (c *Client) GetPayout(ctx context.Context, payoutID string) (*Payout, error return &payout, nil } -func (c *Client) CreatePayout(ctx context.Context, quote Quote, targetAccount uint64, transactionID string) (*Payout, error) { +func (c *client) CreatePayout(ctx context.Context, quote Quote, targetAccount uint64, transactionID string) (*Payout, error) { // TODO(polo): metrics // f := connectors.ClientMetrics(ctx, "wise", "initiate_payout") // now := time.Now() diff --git a/internal/connectors/plugins/public/wise/client/profiles.go b/internal/connectors/plugins/public/wise/client/profiles.go index 41aae06e..122277b2 100644 --- a/internal/connectors/plugins/public/wise/client/profiles.go +++ b/internal/connectors/plugins/public/wise/client/profiles.go @@ -11,7 +11,7 @@ type Profile struct { Type string `json:"type"` } -func (c *Client) GetProfiles(ctx context.Context) ([]Profile, error) { +func (c *client) GetProfiles(ctx context.Context) ([]Profile, error) { // TODO(polo): metrics // f := connectors.ClientMetrics(ctx, "wise", "list_profiles") // now := time.Now() diff --git a/internal/connectors/plugins/public/wise/client/quotes.go b/internal/connectors/plugins/public/wise/client/quotes.go index 19799ddd..e9e77091 100644 --- a/internal/connectors/plugins/public/wise/client/quotes.go +++ b/internal/connectors/plugins/public/wise/client/quotes.go @@ -14,7 +14,7 @@ type Quote struct { ID uuid.UUID `json:"id"` } -func (c *Client) CreateQuote(ctx context.Context, profileID, currency string, amount json.Number) (Quote, error) { +func (c *client) CreateQuote(ctx context.Context, profileID, currency string, amount json.Number) (Quote, error) { // TODO(polo): metrics // f := connectors.ClientMetrics(ctx, "wise", "create_quote") // now := time.Now() diff --git a/internal/connectors/plugins/public/wise/client/recipient_accounts.go b/internal/connectors/plugins/public/wise/client/recipient_accounts.go index b609d034..241514ae 100644 --- a/internal/connectors/plugins/public/wise/client/recipient_accounts.go +++ b/internal/connectors/plugins/public/wise/client/recipient_accounts.go @@ -22,7 +22,7 @@ type RecipientAccount struct { } `json:"name"` } -func (c *Client) GetRecipientAccounts(ctx context.Context, profileID uint64, pageSize int, seekPositionForNext uint64) (*RecipientAccountsResponse, error) { +func (c *client) GetRecipientAccounts(ctx context.Context, profileID uint64, pageSize int, seekPositionForNext uint64) (*RecipientAccountsResponse, error) { // TODO(polo): metrics // f := connectors.ClientMetrics(ctx, "wise", "list_recipient_accounts") // now := time.Now() @@ -52,7 +52,7 @@ func (c *Client) GetRecipientAccounts(ctx context.Context, profileID uint64, pag return &accounts, nil } -func (c *Client) GetRecipientAccount(ctx context.Context, accountID uint64) (*RecipientAccount, error) { +func (c *client) GetRecipientAccount(ctx context.Context, accountID uint64) (*RecipientAccount, error) { // TODO(polo): metrics // f := connectors.ClientMetrics(ctx, "wise", "get_recipient_account") // now := time.Now() diff --git a/internal/connectors/plugins/public/wise/client/transfers.go b/internal/connectors/plugins/public/wise/client/transfers.go index 9a546be1..6dd46d45 100644 --- a/internal/connectors/plugins/public/wise/client/transfers.go +++ b/internal/connectors/plugins/public/wise/client/transfers.go @@ -59,7 +59,7 @@ func (t *Transfer) UnmarshalJSON(data []byte) error { return nil } -func (c *Client) GetTransfers(ctx context.Context, profileID uint64, offset int, limit int) ([]Transfer, error) { +func (c *client) GetTransfers(ctx context.Context, profileID uint64, offset int, limit int) ([]Transfer, error) { // TODO(polo): metrics // f := connectors.ClientMetrics(ctx, "wise", "list_transfers") // now := time.Now() @@ -156,7 +156,7 @@ func (c *Client) GetTransfers(ctx context.Context, profileID uint64, offset int, return transfers, nil } -func (c *Client) GetTransfer(ctx context.Context, transferID string) (*Transfer, error) { +func (c *client) GetTransfer(ctx context.Context, transferID string) (*Transfer, error) { // TODO(polo): metrics // f := connectors.ClientMetrics(ctx, "wise", "get_transfer") // now := time.Now() @@ -177,7 +177,7 @@ func (c *Client) GetTransfer(ctx context.Context, transferID string) (*Transfer, return &transfer, nil } -func (c *Client) CreateTransfer(ctx context.Context, quote Quote, targetAccount uint64, transactionID string) (*Transfer, error) { +func (c *client) CreateTransfer(ctx context.Context, quote Quote, targetAccount uint64, transactionID string) (*Transfer, error) { // TODO(polo): metrics // metrics.GetMetricsRegistry().ConnectorPSPCalls().Add(ctx, 1, metric.WithAttributes([]attribute.KeyValue{ // attribute.String("connector", "wise"), diff --git a/internal/connectors/plugins/public/wise/client/webhooks.go b/internal/connectors/plugins/public/wise/client/webhooks.go index 3b15c536..75633ff6 100644 --- a/internal/connectors/plugins/public/wise/client/webhooks.go +++ b/internal/connectors/plugins/public/wise/client/webhooks.go @@ -36,7 +36,7 @@ type webhookSubscriptionResponse struct { CreatedAt string `json:"created_at"` } -func (c *Client) CreateWebhook(ctx context.Context, profileID uint64, name, triggerOn, url, version string) (*webhookSubscriptionResponse, error) { +func (c *client) CreateWebhook(ctx context.Context, profileID uint64, name, triggerOn, url, version string) (*webhookSubscriptionResponse, error) { reqBody, err := json.Marshal(webhookSubscription{ Name: name, TriggerOn: triggerOn, @@ -70,7 +70,7 @@ func (c *Client) CreateWebhook(ctx context.Context, profileID uint64, name, trig return &res, nil } -func (c *Client) ListWebhooksSubscription(ctx context.Context, profileID uint64) ([]webhookSubscriptionResponse, error) { +func (c *client) ListWebhooksSubscription(ctx context.Context, profileID uint64) ([]webhookSubscriptionResponse, error) { req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.endpoint(fmt.Sprintf("/v3/profiles/%d/subscriptions", profileID)), http.NoBody) if err != nil { @@ -86,7 +86,7 @@ func (c *Client) ListWebhooksSubscription(ctx context.Context, profileID uint64) return res, nil } -func (c *Client) DeleteWebhooks(ctx context.Context, profileID uint64, subscriptionID string) error { +func (c *client) DeleteWebhooks(ctx context.Context, profileID uint64, subscriptionID string) error { req, err := http.NewRequestWithContext(ctx, http.MethodDelete, c.endpoint(fmt.Sprintf("/v3/profiles/%d/subscriptions/%s", profileID, subscriptionID)), http.NoBody) if err != nil { @@ -119,7 +119,7 @@ type transferStateChangedWebhookPayload struct { SentAt string `json:"sent_at"` } -func (c *Client) TranslateTransferStateChangedWebhook(ctx context.Context, payload []byte) (Transfer, error) { +func (c *client) TranslateTransferStateChangedWebhook(ctx context.Context, payload []byte) (Transfer, error) { var transferStatedChangedEvent transferStateChangedWebhookPayload err := json.Unmarshal(payload, &transferStatedChangedEvent) if err != nil { @@ -161,7 +161,7 @@ type balanceUpdateWebhookPayload struct { SentAt string `json:"sent_at"` } -func (c *Client) TranslateBalanceUpdateWebhook(ctx context.Context, payload []byte) (balanceUpdateWebhookPayload, error) { +func (c *client) TranslateBalanceUpdateWebhook(ctx context.Context, payload []byte) (balanceUpdateWebhookPayload, error) { var balanceUpdateEvent balanceUpdateWebhookPayload err := json.Unmarshal(payload, &balanceUpdateEvent) if err != nil { diff --git a/internal/connectors/plugins/public/wise/plugin.go b/internal/connectors/plugins/public/wise/plugin.go index 5a25e953..5801ba24 100644 --- a/internal/connectors/plugins/public/wise/plugin.go +++ b/internal/connectors/plugins/public/wise/plugin.go @@ -16,7 +16,7 @@ var ( type Plugin struct { config Config - client *client.Client + client client.Client } func (p *Plugin) Install(ctx context.Context, req models.InstallRequest) (models.InstallResponse, error) { From 35c2dc03a3d9ddd5d53a9d6e5ccca88a84a03471 Mon Sep 17 00:00:00 2001 From: Crimson Thompson Date: Tue, 8 Oct 2024 12:02:56 +0200 Subject: [PATCH 04/13] feat(payments): add unit test for wise plugin --- .../connectors/plugins/public/wise/plugin.go | 3 + .../plugins/public/wise/plugin_test.go | 105 ++++++++++++++++++ 2 files changed, 108 insertions(+) create mode 100644 internal/connectors/plugins/public/wise/plugin_test.go diff --git a/internal/connectors/plugins/public/wise/plugin.go b/internal/connectors/plugins/public/wise/plugin.go index 5801ba24..fc53f7c0 100644 --- a/internal/connectors/plugins/public/wise/plugin.go +++ b/internal/connectors/plugins/public/wise/plugin.go @@ -63,6 +63,9 @@ func (p *Plugin) Install(ctx context.Context, req models.InstallRequest) (models } func (p *Plugin) Uninstall(ctx context.Context, req models.UninstallRequest) (models.UninstallResponse, error) { + if p.client == nil { + return models.UninstallResponse{}, plugins.ErrNotYetInstalled + } return p.uninstall(ctx, req) } diff --git a/internal/connectors/plugins/public/wise/plugin_test.go b/internal/connectors/plugins/public/wise/plugin_test.go new file mode 100644 index 00000000..55a9fe17 --- /dev/null +++ b/internal/connectors/plugins/public/wise/plugin_test.go @@ -0,0 +1,105 @@ +package wise + +import ( + "bytes" + "context" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/json" + "encoding/pem" + "testing" + + "github.com/formancehq/payments/internal/connectors/plugins" + "github.com/formancehq/payments/internal/models" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestPlugin(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Wise Plugin Suite") +} + +var _ = Describe("Wise Plugin", func() { + var ( + plg *Plugin + block *pem.Block + pemKey *bytes.Buffer + ) + + BeforeEach(func() { + plg = &Plugin{} + + privatekey, err := rsa.GenerateKey(rand.Reader, 2048) + Expect(err).To(BeNil()) + publickey := &privatekey.PublicKey + publicKeyBytes, err := x509.MarshalPKIXPublicKey(publickey) + Expect(err).To(BeNil()) + block = &pem.Block{ + Type: "PUBLIC KEY", + Bytes: publicKeyBytes, + } + pemKey = bytes.NewBufferString("") + + err = pem.Encode(pemKey, block) + Expect(err).To(BeNil()) + }) + + Context("install", func() { + It("reports validation errors in the config", func(ctx SpecContext) { + req := models.InstallRequest{Config: json.RawMessage(`{}`)} + _, err := plg.Install(context.Background(), req) + Expect(err).To(MatchError(ContainSubstring("config"))) + }) + It("rejects malformed pem keys", func(ctx SpecContext) { + config := json.RawMessage(`{"apiKey":"dummy","webhookPublicKey":"badKey"}`) + req := models.InstallRequest{Config: config} + _, err := plg.Install(context.Background(), req) + Expect(err).To(MatchError(ContainSubstring("public key"))) + }) + It("returns valid install response", func(ctx SpecContext) { + config := &Config{ + APIKey: "key", + WebhookPublicKey: pemKey.String(), + } + configJson, err := json.Marshal(config) + req := models.InstallRequest{Config: configJson} + res, err := plg.Install(context.Background(), req) + Expect(err).To(BeNil()) + Expect(len(res.Capabilities) > 0).To(BeTrue()) + Expect(len(res.Workflow) > 0).To(BeTrue()) + Expect(res.Workflow[0].Name).To(Equal("fetch_profiles")) + }) + }) + + Context("calling functions on uninstalled plugins", func() { + It("returns valid uninstall response", func(ctx SpecContext) { + req := models.UninstallRequest{ConnectorID: "dummyID"} + _, err := plg.Uninstall(context.Background(), req) + Expect(err).To(MatchError(plugins.ErrNotYetInstalled)) + }) + + It("fails when fetch next accounts is called before install", func(ctx SpecContext) { + req := models.FetchNextAccountsRequest{ + State: json.RawMessage(`{}`), + } + _, err := plg.FetchNextAccounts(context.Background(), req) + Expect(err).To(MatchError(plugins.ErrNotYetInstalled)) + }) + It("fails when fetch next balances is called before install", func(ctx SpecContext) { + req := models.FetchNextBalancesRequest{ + State: json.RawMessage(`{}`), + } + _, err := plg.FetchNextBalances(context.Background(), req) + Expect(err).To(MatchError(plugins.ErrNotYetInstalled)) + }) + It("fails when fetch next external accounts is called before install", func(ctx SpecContext) { + req := models.FetchNextExternalAccountsRequest{ + State: json.RawMessage(`{}`), + } + _, err := plg.FetchNextExternalAccounts(context.Background(), req) + Expect(err).To(MatchError(plugins.ErrNotYetInstalled)) + }) + }) +}) From 4b19e361765e59045999913f7208372d1001feea Mon Sep 17 00:00:00 2001 From: Crimson Thompson Date: Tue, 8 Oct 2024 16:34:19 +0200 Subject: [PATCH 05/13] feat(payments): add wise uninstall tests --- .../plugins/public/wise/client/client.go | 4 +- .../public/wise/client/client_generated.go | 8 +-- .../plugins/public/wise/client/webhooks.go | 35 +++++---- .../connectors/plugins/public/wise/plugin.go | 4 ++ .../plugins/public/wise/uninstall_test.go | 71 +++++++++++++++++++ 5 files changed, 98 insertions(+), 24 deletions(-) create mode 100644 internal/connectors/plugins/public/wise/uninstall_test.go diff --git a/internal/connectors/plugins/public/wise/client/client.go b/internal/connectors/plugins/public/wise/client/client.go index 67693f92..59b5d518 100644 --- a/internal/connectors/plugins/public/wise/client/client.go +++ b/internal/connectors/plugins/public/wise/client/client.go @@ -38,8 +38,8 @@ type Client interface { GetTransfers(ctx context.Context, profileID uint64, offset int, limit int) ([]Transfer, error) GetTransfer(ctx context.Context, transferID string) (*Transfer, error) CreateTransfer(ctx context.Context, quote Quote, targetAccount uint64, transactionID string) (*Transfer, error) - CreateWebhook(ctx context.Context, profileID uint64, name, triggerOn, url, version string) (*webhookSubscriptionResponse, error) - ListWebhooksSubscription(ctx context.Context, profileID uint64) ([]webhookSubscriptionResponse, error) + CreateWebhook(ctx context.Context, profileID uint64, name, triggerOn, url, version string) (*WebhookSubscriptionResponse, error) + ListWebhooksSubscription(ctx context.Context, profileID uint64) ([]WebhookSubscriptionResponse, error) DeleteWebhooks(ctx context.Context, profileID uint64, subscriptionID string) error TranslateTransferStateChangedWebhook(ctx context.Context, payload []byte) (Transfer, error) TranslateBalanceUpdateWebhook(ctx context.Context, payload []byte) (balanceUpdateWebhookPayload, error) diff --git a/internal/connectors/plugins/public/wise/client/client_generated.go b/internal/connectors/plugins/public/wise/client/client_generated.go index 46020c97..5ade0073 100644 --- a/internal/connectors/plugins/public/wise/client/client_generated.go +++ b/internal/connectors/plugins/public/wise/client/client_generated.go @@ -86,10 +86,10 @@ func (mr *MockClientMockRecorder) CreateTransfer(ctx, quote, targetAccount, tran } // CreateWebhook mocks base method. -func (m *MockClient) CreateWebhook(ctx context.Context, profileID uint64, name, triggerOn, url, version string) (*webhookSubscriptionResponse, error) { +func (m *MockClient) CreateWebhook(ctx context.Context, profileID uint64, name, triggerOn, url, version string) (*WebhookSubscriptionResponse, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "CreateWebhook", ctx, profileID, name, triggerOn, url, version) - ret0, _ := ret[0].(*webhookSubscriptionResponse) + ret0, _ := ret[0].(*WebhookSubscriptionResponse) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -235,10 +235,10 @@ func (mr *MockClientMockRecorder) GetTransfers(ctx, profileID, offset, limit any } // ListWebhooksSubscription mocks base method. -func (m *MockClient) ListWebhooksSubscription(ctx context.Context, profileID uint64) ([]webhookSubscriptionResponse, error) { +func (m *MockClient) ListWebhooksSubscription(ctx context.Context, profileID uint64) ([]WebhookSubscriptionResponse, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ListWebhooksSubscription", ctx, profileID) - ret0, _ := ret[0].([]webhookSubscriptionResponse) + ret0, _ := ret[0].([]WebhookSubscriptionResponse) ret1, _ := ret[1].(error) return ret0, ret1 } diff --git a/internal/connectors/plugins/public/wise/client/webhooks.go b/internal/connectors/plugins/public/wise/client/webhooks.go index 75633ff6..b5e45630 100644 --- a/internal/connectors/plugins/public/wise/client/webhooks.go +++ b/internal/connectors/plugins/public/wise/client/webhooks.go @@ -9,23 +9,22 @@ import ( "time" ) +type WebhookDelivery struct { + Version string `json:"version"` + URL string `json:"url"` +} + type webhookSubscription struct { - Name string `json:"name"` - TriggerOn string `json:"trigger_on"` - Delivery struct { - Version string `json:"version"` - URL string `json:"url"` - } `json:"delivery"` + Name string `json:"name"` + TriggerOn string `json:"trigger_on"` + Delivery WebhookDelivery `json:"delivery"` } -type webhookSubscriptionResponse struct { - ID string `json:"id"` - Name string `json:"name"` - Delivery struct { - Version string `json:"version"` - URL string `json:"url"` - } `json:"delivery"` - TriggerOn string `json:"trigger_on"` +type WebhookSubscriptionResponse struct { + ID string `json:"id"` + Name string `json:"name"` + Delivery WebhookDelivery `json:"delivery"` + TriggerOn string `json:"trigger_on"` Scope struct { Domain string `json:"domain"` } `json:"scope"` @@ -36,7 +35,7 @@ type webhookSubscriptionResponse struct { CreatedAt string `json:"created_at"` } -func (c *client) CreateWebhook(ctx context.Context, profileID uint64, name, triggerOn, url, version string) (*webhookSubscriptionResponse, error) { +func (c *client) CreateWebhook(ctx context.Context, profileID uint64, name, triggerOn, url, version string) (*WebhookSubscriptionResponse, error) { reqBody, err := json.Marshal(webhookSubscription{ Name: name, TriggerOn: triggerOn, @@ -61,7 +60,7 @@ func (c *client) CreateWebhook(ctx context.Context, profileID uint64, name, trig } req.Header.Set("Content-Type", "application/json") - var res webhookSubscriptionResponse + var res WebhookSubscriptionResponse var errRes wiseErrors statusCode, err := c.httpClient.Do(req, &res, &errRes) if err != nil { @@ -70,14 +69,14 @@ func (c *client) CreateWebhook(ctx context.Context, profileID uint64, name, trig return &res, nil } -func (c *client) ListWebhooksSubscription(ctx context.Context, profileID uint64) ([]webhookSubscriptionResponse, error) { +func (c *client) ListWebhooksSubscription(ctx context.Context, profileID uint64) ([]WebhookSubscriptionResponse, error) { req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.endpoint(fmt.Sprintf("/v3/profiles/%d/subscriptions", profileID)), http.NoBody) if err != nil { return nil, err } - var res []webhookSubscriptionResponse + var res []WebhookSubscriptionResponse var errRes wiseErrors statusCode, err := c.httpClient.Do(req, &res, &errRes) if err != nil { diff --git a/internal/connectors/plugins/public/wise/plugin.go b/internal/connectors/plugins/public/wise/plugin.go index fc53f7c0..aae57a78 100644 --- a/internal/connectors/plugins/public/wise/plugin.go +++ b/internal/connectors/plugins/public/wise/plugin.go @@ -173,4 +173,8 @@ func (p *Plugin) TranslateWebhook(ctx context.Context, req models.TranslateWebho }, nil } +func (p *Plugin) SetClient(cl client.Client) { + p.client = cl +} + var _ models.Plugin = &Plugin{} diff --git a/internal/connectors/plugins/public/wise/uninstall_test.go b/internal/connectors/plugins/public/wise/uninstall_test.go new file mode 100644 index 00000000..edfa22bf --- /dev/null +++ b/internal/connectors/plugins/public/wise/uninstall_test.go @@ -0,0 +1,71 @@ +package wise + +import ( + "fmt" + + "github.com/formancehq/payments/internal/connectors/plugins/public/wise/client" + "github.com/formancehq/payments/internal/models" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "go.uber.org/mock/gomock" +) + +var _ = Describe("Wise Plugin Uninstall", func() { + var ( + plg *Plugin + m *client.MockClient + ) + + BeforeEach(func() { + plg = &Plugin{} + + ctrl := gomock.NewController(GinkgoT()) + m = client.NewMockClient(ctrl) + plg.SetClient(m) + }) + + Context("uninstall", func() { + var ( + profiles []client.Profile + expectedWebhookID = "webhook1" + expectedWebhookID2 = "webhook2" + ) + + BeforeEach(func() { + profiles = []client.Profile{ + {ID: 1, Type: "type1"}, + {ID: 2, Type: "type2"}, + } + }) + + It("deletes webhooks related to accounts", func(ctx SpecContext) { + req := models.UninstallRequest{ConnectorID: "dummyID"} + m.EXPECT().GetProfiles(ctx).Return( + profiles, + nil, + ) + m.EXPECT().ListWebhooksSubscription(ctx, profiles[0].ID).Return( + []client.WebhookSubscriptionResponse{ + {ID: expectedWebhookID, Delivery: client.WebhookDelivery{ + URL: fmt.Sprintf("http://somesite.fr/%s", req.ConnectorID), + }}, + {ID: "skipped", Delivery: client.WebhookDelivery{URL: "http://somesite.fr"}}, + }, + nil, + ) + m.EXPECT().ListWebhooksSubscription(ctx, profiles[1].ID).Return( + []client.WebhookSubscriptionResponse{ + {ID: expectedWebhookID2, Delivery: client.WebhookDelivery{ + URL: fmt.Sprintf("http://%s.somesite.com", req.ConnectorID), + }}, + }, + nil, + ) + m.EXPECT().DeleteWebhooks(ctx, profiles[0].ID, expectedWebhookID).Return(nil) + m.EXPECT().DeleteWebhooks(ctx, profiles[1].ID, expectedWebhookID2).Return(nil) + + _, err := plg.Uninstall(ctx, req) + Expect(err).To(BeNil()) + }) + }) +}) From 404723a5b0c75b97143f4947182189ec8d12bc7f Mon Sep 17 00:00:00 2001 From: Crimson Thompson Date: Tue, 8 Oct 2024 17:34:44 +0200 Subject: [PATCH 06/13] feat(payments): add unit test for wise accounts and balances methods --- .../plugins/public/wise/accounts.go | 2 +- .../plugins/public/wise/accounts_test.go | 68 +++++++++++++++++ .../plugins/public/wise/balances.go | 2 +- .../plugins/public/wise/balances_test.go | 73 +++++++++++++++++++ .../plugins/public/wise/client/balances.go | 18 +++-- .../plugins/public/wise/metadata.go | 2 +- 6 files changed, 154 insertions(+), 11 deletions(-) create mode 100644 internal/connectors/plugins/public/wise/accounts_test.go create mode 100644 internal/connectors/plugins/public/wise/balances_test.go diff --git a/internal/connectors/plugins/public/wise/accounts.go b/internal/connectors/plugins/public/wise/accounts.go index a63c859a..86ba1fea 100644 --- a/internal/connectors/plugins/public/wise/accounts.go +++ b/internal/connectors/plugins/public/wise/accounts.go @@ -60,7 +60,7 @@ func (p *Plugin) fetchNextAccounts(ctx context.Context, req models.FetchNextAcco Name: &balance.Name, DefaultAsset: pointer.For(currency.FormatAsset(supportedCurrenciesWithDecimal, balance.Amount.Currency)), Metadata: map[string]string{ - metadataProfileIDKey: strconv.FormatUint(from.ID, 10), + MetadataProfileIDKey: strconv.FormatUint(from.ID, 10), }, Raw: raw, }) diff --git a/internal/connectors/plugins/public/wise/accounts_test.go b/internal/connectors/plugins/public/wise/accounts_test.go new file mode 100644 index 00000000..f81cee41 --- /dev/null +++ b/internal/connectors/plugins/public/wise/accounts_test.go @@ -0,0 +1,68 @@ +package wise + +import ( + "encoding/json" + "fmt" + + "github.com/formancehq/payments/internal/connectors/plugins/public/wise/client" + "github.com/formancehq/payments/internal/models" + "go.uber.org/mock/gomock" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("Wise Plugin Accounts", func() { + var ( + plg *Plugin + m *client.MockClient + ) + + BeforeEach(func() { + plg = &Plugin{} + + ctrl := gomock.NewController(GinkgoT()) + m = client.NewMockClient(ctrl) + plg.SetClient(m) + }) + + Context("fetch next accounts", func() { + var ( + balances []client.Balance + expectedProfileID uint64 + ) + + BeforeEach(func() { + expectedProfileID = 123454 + balances = []client.Balance{ + {ID: 14556, Type: "type1"}, + {ID: 3334, Type: "type2"}, + } + }) + + It("fetches accounts from wise", func(ctx SpecContext) { + req := models.FetchNextAccountsRequest{ + State: json.RawMessage(`{}`), + FromPayload: json.RawMessage(fmt.Sprintf(`{"ID":%d}`, expectedProfileID)), + PageSize: len(balances), + } + m.EXPECT().GetBalances(ctx, expectedProfileID).Return( + balances, + nil, + ) + + res, err := plg.FetchNextAccounts(ctx, req) + Expect(err).To(BeNil()) + Expect(res.HasMore).To(BeTrue()) + Expect(res.Accounts).To(HaveLen(req.PageSize)) + Expect(res.Accounts[0].Reference).To(Equal(fmt.Sprint(balances[0].ID))) + Expect(res.Accounts[1].Reference).To(Equal(fmt.Sprint(balances[1].ID))) + + var state accountsState + + err = json.Unmarshal(res.NewState, &state) + Expect(err).To(BeNil()) + Expect(fmt.Sprint(state.LastAccountID)).To(Equal(res.Accounts[len(res.Accounts)-1].Reference)) + }) + }) +}) diff --git a/internal/connectors/plugins/public/wise/balances.go b/internal/connectors/plugins/public/wise/balances.go index a706ee2d..7d32f5be 100644 --- a/internal/connectors/plugins/public/wise/balances.go +++ b/internal/connectors/plugins/public/wise/balances.go @@ -24,7 +24,7 @@ func (p *Plugin) fetchNextBalances(ctx context.Context, req models.FetchNextBala return models.FetchNextBalancesResponse{}, err } - pID, ok := from.Metadata[metadataProfileIDKey] + pID, ok := from.Metadata[MetadataProfileIDKey] if !ok { return models.FetchNextBalancesResponse{}, errors.New("missing profile ID in from payload when fetching balances") } diff --git a/internal/connectors/plugins/public/wise/balances_test.go b/internal/connectors/plugins/public/wise/balances_test.go new file mode 100644 index 00000000..e3114dfa --- /dev/null +++ b/internal/connectors/plugins/public/wise/balances_test.go @@ -0,0 +1,73 @@ +package wise + +import ( + "encoding/json" + "fmt" + "math/big" + + "github.com/formancehq/payments/internal/connectors/plugins/public/wise/client" + "github.com/formancehq/payments/internal/models" + "go.uber.org/mock/gomock" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("Wise Plugin Balances", func() { + var ( + plg *Plugin + m *client.MockClient + ) + + BeforeEach(func() { + plg = &Plugin{} + + ctrl := gomock.NewController(GinkgoT()) + m = client.NewMockClient(ctrl) + plg.SetClient(m) + }) + + Context("fetch next balances", func() { + var ( + balance client.Balance + expectedProfileID uint64 + profileVal uint64 + ) + + BeforeEach(func() { + expectedProfileID = 123454 + profileVal = 999999 + balance = client.Balance{ + ID: 14556, + Type: "type1", + Amount: client.BalanceAmount{Value: json.Number("44.99"), Currency: "USD"}, + } + }) + + It("fetches balances from wise client", func(ctx SpecContext) { + req := models.FetchNextBalancesRequest{ + State: json.RawMessage(`{}`), + FromPayload: json.RawMessage(fmt.Sprintf( + `{"Reference":"%d","Metadata":{"%s":"%d"}}`, + expectedProfileID, + MetadataProfileIDKey, + profileVal, + )), + PageSize: 10, + } + m.EXPECT().GetBalance(ctx, profileVal, expectedProfileID).Return( + &balance, + nil, + ) + + res, err := plg.FetchNextBalances(ctx, req) + Expect(err).To(BeNil()) + Expect(res.HasMore).To(BeFalse()) + Expect(res.Balances).To(HaveLen(1)) // always returns 1 + Expect(res.Balances[0].AccountReference).To(Equal(fmt.Sprint(expectedProfileID))) + expectedBalance, err := balance.Amount.Value.Float64() + Expect(err).To(BeNil()) + Expect(res.Balances[0].Amount).To(BeEquivalentTo(big.NewInt(int64(expectedBalance * 100)))) + }) + }) +}) diff --git a/internal/connectors/plugins/public/wise/client/balances.go b/internal/connectors/plugins/public/wise/client/balances.go index 6da16bfe..6c12bb8a 100644 --- a/internal/connectors/plugins/public/wise/client/balances.go +++ b/internal/connectors/plugins/public/wise/client/balances.go @@ -8,15 +8,17 @@ import ( "time" ) +type BalanceAmount struct { + Value json.Number `json:"value"` + Currency string `json:"currency"` +} + type Balance struct { - ID uint64 `json:"id"` - Currency string `json:"currency"` - Type string `json:"type"` - Name string `json:"name"` - Amount struct { - Value json.Number `json:"value"` - Currency string `json:"currency"` - } `json:"amount"` + ID uint64 `json:"id"` + Currency string `json:"currency"` + Type string `json:"type"` + Name string `json:"name"` + Amount BalanceAmount `json:"amount"` ReservedAmount struct { Value json.Number `json:"value"` Currency string `json:"currency"` diff --git a/internal/connectors/plugins/public/wise/metadata.go b/internal/connectors/plugins/public/wise/metadata.go index 5261fc4d..36c9530a 100644 --- a/internal/connectors/plugins/public/wise/metadata.go +++ b/internal/connectors/plugins/public/wise/metadata.go @@ -1,5 +1,5 @@ package wise const ( - metadataProfileIDKey = "profile_id" + MetadataProfileIDKey = "profile_id" ) From 39795f28a4976af2e3083ec057da116cb2c6e51a Mon Sep 17 00:00:00 2001 From: Crimson Thompson Date: Wed, 9 Oct 2024 11:44:05 +0200 Subject: [PATCH 07/13] feat(payments): ensure that last seek position is persisted in wise external accounts state --- internal/connectors/plugins/public/wise/external_accounts.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/internal/connectors/plugins/public/wise/external_accounts.go b/internal/connectors/plugins/public/wise/external_accounts.go index f1a8bf71..f9be48a2 100644 --- a/internal/connectors/plugins/public/wise/external_accounts.go +++ b/internal/connectors/plugins/public/wise/external_accounts.go @@ -71,6 +71,7 @@ func (p *Plugin) fetchExternalAccounts(ctx context.Context, req models.FetchNext } } + newState.LastSeekPosition = pagedExternalAccounts.SeekPositionForNext if len(accounts) >= req.PageSize { hasMore = true break @@ -80,8 +81,6 @@ func (p *Plugin) fetchExternalAccounts(ctx context.Context, req models.FetchNext // No more data to fetch break } - - newState.LastSeekPosition = pagedExternalAccounts.SeekPositionForNext } payload, err := json.Marshal(newState) From 70c1d05531a352c6bbe5408ab2d01f91bc347ca5 Mon Sep 17 00:00:00 2001 From: Crimson Thompson Date: Wed, 9 Oct 2024 11:44:32 +0200 Subject: [PATCH 08/13] feat(payments): add unit test for wise external accounts --- .../public/wise/external_accounts_test.go | 74 +++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 internal/connectors/plugins/public/wise/external_accounts_test.go diff --git a/internal/connectors/plugins/public/wise/external_accounts_test.go b/internal/connectors/plugins/public/wise/external_accounts_test.go new file mode 100644 index 00000000..771f4fe3 --- /dev/null +++ b/internal/connectors/plugins/public/wise/external_accounts_test.go @@ -0,0 +1,74 @@ +package wise + +import ( + "encoding/json" + "fmt" + + "github.com/formancehq/payments/internal/connectors/plugins/public/wise/client" + "github.com/formancehq/payments/internal/models" + "go.uber.org/mock/gomock" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("Wise Plugin External Accounts", func() { + var ( + plg *Plugin + m *client.MockClient + ) + + BeforeEach(func() { + plg = &Plugin{} + + ctrl := gomock.NewController(GinkgoT()) + m = client.NewMockClient(ctrl) + plg.SetClient(m) + }) + + Context("fetch next external accounts", func() { + var ( + accounts []*client.RecipientAccount + expectedProfileID uint64 + lastSeekPosition uint64 + ) + + BeforeEach(func() { + expectedProfileID = 154 + lastSeekPosition = 83 + accounts = []*client.RecipientAccount{ + {ID: lastSeekPosition + 1, Profile: expectedProfileID}, + {ID: lastSeekPosition + 2, Profile: expectedProfileID}, + } + }) + + It("fetches recpient accounts from wise", func(ctx SpecContext) { + req := models.FetchNextExternalAccountsRequest{ + State: json.RawMessage(fmt.Sprintf(`{"lastSeekPosition":%d}`, lastSeekPosition)), + FromPayload: json.RawMessage(fmt.Sprintf( + `{"id":%d,"type":"sometype"}`, + expectedProfileID, + )), + PageSize: len(accounts), + } + recipientRes := &client.RecipientAccountsResponse{ + Content: accounts, + SeekPositionForCurrent: lastSeekPosition + 1, + SeekPositionForNext: accounts[len(accounts)-1].ID + 1, + } + m.EXPECT().GetRecipientAccounts(ctx, expectedProfileID, req.PageSize, lastSeekPosition).Return(recipientRes, nil) + + res, err := plg.FetchNextExternalAccounts(ctx, req) + Expect(err).To(BeNil()) + Expect(res.HasMore).To(BeTrue()) + Expect(res.ExternalAccounts).To(HaveLen(req.PageSize)) + Expect(res.ExternalAccounts[0].Reference).To(Equal(fmt.Sprint(accounts[0].ID))) + + var state externalAccountsState + + err = json.Unmarshal(res.NewState, &state) + Expect(err).To(BeNil()) + Expect(state.LastSeekPosition).To(Equal(recipientRes.SeekPositionForNext)) + }) + }) +}) From 2827e4d7b393cb16c242ec828ca0a6e9b65249f8 Mon Sep 17 00:00:00 2001 From: Crimson Thompson Date: Wed, 9 Oct 2024 12:19:54 +0200 Subject: [PATCH 09/13] feat(payments): increment offset in wise payments to account for skipped transactions --- internal/connectors/plugins/public/wise/payments.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/internal/connectors/plugins/public/wise/payments.go b/internal/connectors/plugins/public/wise/payments.go index 83a6873c..dc5325bd 100644 --- a/internal/connectors/plugins/public/wise/payments.go +++ b/internal/connectors/plugins/public/wise/payments.go @@ -9,6 +9,7 @@ import ( "github.com/formancehq/payments/internal/connectors/plugins/currency" "github.com/formancehq/payments/internal/connectors/plugins/public/wise/client" "github.com/formancehq/payments/internal/models" + "github.com/hashicorp/go-hclog" ) type paymentsState struct { @@ -52,9 +53,13 @@ func (p *Plugin) fetchNextPayments(ctx context.Context, req models.FetchNextPaym if err != nil { return models.FetchNextPaymentsResponse{}, err } + newState.Offset++ + if payment == nil { + hclog.Default().Info(fmt.Sprintf("skipping unsupported wise payment: %d", transfer.ID)) + continue + } payments = append(payments, *payment) - newState.Offset++ if len(payments) >= req.PageSize { break From 41b908497e30014bbafdd48689fd2491e343d9a8 Mon Sep 17 00:00:00 2001 From: Crimson Thompson Date: Wed, 9 Oct 2024 12:20:16 +0200 Subject: [PATCH 10/13] feat(payments): add tests for wise payments --- .../plugins/public/wise/payments_test.go | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 internal/connectors/plugins/public/wise/payments_test.go diff --git a/internal/connectors/plugins/public/wise/payments_test.go b/internal/connectors/plugins/public/wise/payments_test.go new file mode 100644 index 00000000..3bc71138 --- /dev/null +++ b/internal/connectors/plugins/public/wise/payments_test.go @@ -0,0 +1,79 @@ +package wise + +import ( + "encoding/json" + "fmt" + "math/big" + + "github.com/formancehq/payments/internal/connectors/plugins/public/wise/client" + "github.com/formancehq/payments/internal/models" + "go.uber.org/mock/gomock" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("Wise Plugin Payments", func() { + var ( + plg *Plugin + m *client.MockClient + ) + + BeforeEach(func() { + plg = &Plugin{} + + ctrl := gomock.NewController(GinkgoT()) + m = client.NewMockClient(ctrl) + plg.SetClient(m) + }) + + Context("fetch next payments", func() { + var ( + transfers []client.Transfer + expectedProfileID uint64 + ) + + BeforeEach(func() { + expectedProfileID = 111 + transfers = []client.Transfer{ + {ID: 1, Reference: "ref1", TargetValue: json.Number("25"), TargetCurrency: "EUR"}, + {ID: 2, Reference: "ref2", TargetValue: json.Number("44"), TargetCurrency: "DKK"}, + {ID: 3, Reference: "ref2", TargetValue: json.Number("61"), TargetCurrency: "EEK"}, // skipped due to unsupported currency + {ID: 4, Reference: "ref2", TargetValue: json.Number("95"), TargetCurrency: "CAD"}, + } + }) + + It("fetches payments from wise", func(ctx SpecContext) { + req := models.FetchNextPaymentsRequest{ + State: json.RawMessage(`{}`), + FromPayload: json.RawMessage(fmt.Sprintf(`{"ID":%d}`, expectedProfileID)), + PageSize: len(transfers), + } + m.EXPECT().GetTransfers(ctx, expectedProfileID, 0, req.PageSize).Return( + transfers, + nil, + ) + m.EXPECT().GetTransfers(ctx, expectedProfileID, 4, req.PageSize).Return( + []client.Transfer{}, + nil, + ) + + res, err := plg.FetchNextPayments(ctx, req) + Expect(err).To(BeNil()) + Expect(res.HasMore).To(BeFalse()) + Expect(res.Payments).To(HaveLen(req.PageSize - 1)) + Expect(res.Payments[0].Reference).To(Equal(fmt.Sprint(transfers[0].ID))) + expectedAmount, err := transfers[0].TargetValue.Int64() + Expect(err).To(BeNil()) + Expect(res.Payments[0].Amount).To(Equal(big.NewInt(expectedAmount * 100))) // after conversion to minors + Expect(res.Payments[1].Reference).To(Equal(fmt.Sprint(transfers[1].ID))) + Expect(res.Payments[2].Reference).To(Equal(fmt.Sprint(transfers[3].ID))) + + var state paymentsState + + err = json.Unmarshal(res.NewState, &state) + Expect(err).To(BeNil()) + Expect(fmt.Sprint(state.Offset)).To(Equal(res.Payments[len(res.Payments)-1].Reference)) + }) + }) +}) From 2dd9b8142d18d085c96e929717d2a471e6f54ad4 Mon Sep 17 00:00:00 2001 From: Crimson Thompson Date: Wed, 9 Oct 2024 14:15:12 +0200 Subject: [PATCH 11/13] feat(payments): add test for wise profiles and webhooks --- .../plugins/public/wise/plugin_test.go | 12 +++ .../plugins/public/wise/profiles_test.go | 76 ++++++++++++++++ .../plugins/public/wise/webhooks_test.go | 87 +++++++++++++++++++ 3 files changed, 175 insertions(+) create mode 100644 internal/connectors/plugins/public/wise/profiles_test.go create mode 100644 internal/connectors/plugins/public/wise/webhooks_test.go diff --git a/internal/connectors/plugins/public/wise/plugin_test.go b/internal/connectors/plugins/public/wise/plugin_test.go index 55a9fe17..9c7aaca8 100644 --- a/internal/connectors/plugins/public/wise/plugin_test.go +++ b/internal/connectors/plugins/public/wise/plugin_test.go @@ -94,6 +94,13 @@ var _ = Describe("Wise Plugin", func() { _, err := plg.FetchNextBalances(context.Background(), req) Expect(err).To(MatchError(plugins.ErrNotYetInstalled)) }) + It("fails when fetch next others is called before install", func(ctx SpecContext) { + req := models.FetchNextOthersRequest{ + State: json.RawMessage(`{}`), + } + _, err := plg.FetchNextOthers(context.Background(), req) + Expect(err).To(MatchError(plugins.ErrNotYetInstalled)) + }) It("fails when fetch next external accounts is called before install", func(ctx SpecContext) { req := models.FetchNextExternalAccountsRequest{ State: json.RawMessage(`{}`), @@ -101,5 +108,10 @@ var _ = Describe("Wise Plugin", func() { _, err := plg.FetchNextExternalAccounts(context.Background(), req) Expect(err).To(MatchError(plugins.ErrNotYetInstalled)) }) + It("fails when create webhook is called before install", func(ctx SpecContext) { + req := models.CreateWebhooksRequest{} + _, err := plg.CreateWebhooks(context.Background(), req) + Expect(err).To(MatchError(plugins.ErrNotYetInstalled)) + }) }) }) diff --git a/internal/connectors/plugins/public/wise/profiles_test.go b/internal/connectors/plugins/public/wise/profiles_test.go new file mode 100644 index 00000000..fd43d418 --- /dev/null +++ b/internal/connectors/plugins/public/wise/profiles_test.go @@ -0,0 +1,76 @@ +package wise + +import ( + "encoding/json" + "fmt" + + "github.com/formancehq/payments/internal/connectors/plugins" + "github.com/formancehq/payments/internal/connectors/plugins/public/wise/client" + "github.com/formancehq/payments/internal/models" + "go.uber.org/mock/gomock" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("Wise Plugin Profiles", func() { + var ( + plg *Plugin + m *client.MockClient + ) + + BeforeEach(func() { + plg = &Plugin{} + + ctrl := gomock.NewController(GinkgoT()) + m = client.NewMockClient(ctrl) + plg.SetClient(m) + }) + + Context("fetch next profiles", func() { + var ( + profiles []client.Profile + ) + + BeforeEach(func() { + profiles = []client.Profile{ + {ID: 14556, Type: "type1"}, + {ID: 3334, Type: "type2"}, + } + }) + + It("replies with unimplemented when unknown other type in request", func(ctx SpecContext) { + req := models.FetchNextOthersRequest{ + State: json.RawMessage(`{}`), + PageSize: len(profiles), + } + _, err := plg.FetchNextOthers(ctx, req) + Expect(err).To(MatchError(plugins.ErrNotImplemented)) + }) + + It("fetches profiles from wise", func(ctx SpecContext) { + req := models.FetchNextOthersRequest{ + State: json.RawMessage(`{}`), + Name: "fetch_profiles", + PageSize: len(profiles), + } + m.EXPECT().GetProfiles(ctx).Return( + profiles, + nil, + ) + + res, err := plg.FetchNextOthers(ctx, req) + Expect(err).To(BeNil()) + Expect(res.HasMore).To(BeTrue()) + Expect(res.Others).To(HaveLen(req.PageSize)) + Expect(res.Others[0].ID).To(Equal(fmt.Sprint(profiles[0].ID))) + Expect(res.Others[1].ID).To(Equal(fmt.Sprint(profiles[1].ID))) + + var state profilesState + + err = json.Unmarshal(res.NewState, &state) + Expect(err).To(BeNil()) + Expect(fmt.Sprint(state.LastProfileID)).To(Equal(res.Others[len(res.Others)-1].ID)) + }) + }) +}) diff --git a/internal/connectors/plugins/public/wise/webhooks_test.go b/internal/connectors/plugins/public/wise/webhooks_test.go new file mode 100644 index 00000000..3c8ff376 --- /dev/null +++ b/internal/connectors/plugins/public/wise/webhooks_test.go @@ -0,0 +1,87 @@ +package wise + +import ( + "encoding/json" + "fmt" + "net/url" + + "github.com/formancehq/payments/internal/connectors/plugins/public/wise/client" + "github.com/formancehq/payments/internal/models" + "go.uber.org/mock/gomock" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("Wise Plugin Webhooks", func() { + var ( + plg *Plugin + m *client.MockClient + ) + + BeforeEach(func() { + plg = &Plugin{} + + ctrl := gomock.NewController(GinkgoT()) + m = client.NewMockClient(ctrl) + plg.SetClient(m) + }) + + Context("create webhooks", func() { + var ( + expectedProfileID uint64 + expectedWebhookPath string + expectedWebhookResponseID string + webhookBaseUrl string + err error + ) + + BeforeEach(func() { + expectedProfileID = 44 + webhookConfigs = map[string]webhookConfig{ + "test": { + triggerOn: "transfers#state-change", + urlPath: "/transferstatechanged", + fn: plg.translateTransferStateChangedWebhook, + version: "1.0.0", + }, + } + expectedWebhookResponseID = "sampleResID" + webhookBaseUrl = "http://example.com" + expectedWebhookPath, err = url.JoinPath(webhookBaseUrl, webhookConfigs["test"].urlPath) + Expect(err).To(BeNil()) + }) + + It("skips making calls when webhook url missing", func(ctx SpecContext) { + req := models.CreateWebhooksRequest{ + FromPayload: json.RawMessage(fmt.Sprintf(`{"id":%d}`, expectedProfileID)), + } + + _, err := plg.CreateWebhooks(ctx, req) + Expect(err).To(MatchError(ErrStackPublicUrlMissing)) + }) + + It("creates webhooks with configured urls", func(ctx SpecContext) { + req := models.CreateWebhooksRequest{ + FromPayload: json.RawMessage(fmt.Sprintf(`{"id":%d}`, expectedProfileID)), + WebhookBaseUrl: webhookBaseUrl, + } + m.EXPECT().CreateWebhook( + ctx, + expectedProfileID, + "test", + webhookConfigs["test"].triggerOn, + expectedWebhookPath, + webhookConfigs["test"].version, + ).Return( + &client.WebhookSubscriptionResponse{ID: expectedWebhookResponseID}, + nil, + ) + + res, err := plg.CreateWebhooks(ctx, req) + Expect(err).To(BeNil()) + Expect(res.Others).To(HaveLen(len(webhookConfigs))) + Expect(res.Others[0].ID).To(Equal(expectedWebhookResponseID)) + }) + }) +}) From 87d49738d7047184e47c82504ebcf2129050ae4f Mon Sep 17 00:00:00 2001 From: Crimson Thompson Date: Wed, 9 Oct 2024 15:57:08 +0200 Subject: [PATCH 12/13] feat(payments): add wise webhook tests --- .../plugins/public/wise/client/client.go | 2 +- .../public/wise/client/client_generated.go | 4 +- .../plugins/public/wise/client/webhooks.go | 48 ++++--- .../connectors/plugins/public/wise/plugin.go | 21 ++- .../plugins/public/wise/plugin_test.go | 133 +++++++++++++++++- .../plugins/public/wise/webhooks.go | 2 +- 6 files changed, 173 insertions(+), 37 deletions(-) diff --git a/internal/connectors/plugins/public/wise/client/client.go b/internal/connectors/plugins/public/wise/client/client.go index 59b5d518..90906621 100644 --- a/internal/connectors/plugins/public/wise/client/client.go +++ b/internal/connectors/plugins/public/wise/client/client.go @@ -42,7 +42,7 @@ type Client interface { ListWebhooksSubscription(ctx context.Context, profileID uint64) ([]WebhookSubscriptionResponse, error) DeleteWebhooks(ctx context.Context, profileID uint64, subscriptionID string) error TranslateTransferStateChangedWebhook(ctx context.Context, payload []byte) (Transfer, error) - TranslateBalanceUpdateWebhook(ctx context.Context, payload []byte) (balanceUpdateWebhookPayload, error) + TranslateBalanceUpdateWebhook(ctx context.Context, payload []byte) (BalanceUpdateWebhookPayload, error) } type client struct { diff --git a/internal/connectors/plugins/public/wise/client/client_generated.go b/internal/connectors/plugins/public/wise/client/client_generated.go index 5ade0073..1a00738f 100644 --- a/internal/connectors/plugins/public/wise/client/client_generated.go +++ b/internal/connectors/plugins/public/wise/client/client_generated.go @@ -250,10 +250,10 @@ func (mr *MockClientMockRecorder) ListWebhooksSubscription(ctx, profileID any) * } // TranslateBalanceUpdateWebhook mocks base method. -func (m *MockClient) TranslateBalanceUpdateWebhook(ctx context.Context, payload []byte) (balanceUpdateWebhookPayload, error) { +func (m *MockClient) TranslateBalanceUpdateWebhook(ctx context.Context, payload []byte) (BalanceUpdateWebhookPayload, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "TranslateBalanceUpdateWebhook", ctx, payload) - ret0, _ := ret[0].(balanceUpdateWebhookPayload) + ret0, _ := ret[0].(BalanceUpdateWebhookPayload) ret1, _ := ret[1].(error) return ret0, ret1 } diff --git a/internal/connectors/plugins/public/wise/client/webhooks.go b/internal/connectors/plugins/public/wise/client/webhooks.go index b5e45630..5e6049be 100644 --- a/internal/connectors/plugins/public/wise/client/webhooks.go +++ b/internal/connectors/plugins/public/wise/client/webhooks.go @@ -139,32 +139,36 @@ func (c *client) TranslateTransferStateChangedWebhook(ctx context.Context, paylo return *transfer, nil } -type balanceUpdateWebhookPayload struct { - Data struct { - Resource struct { - ID uint64 `json:"id"` - ProfileID uint64 `json:"profile_id"` - Type string `json:"type"` - } `json:"resource"` - Amount json.Number `json:"amount"` - BalanceID uint64 `json:"balance_id"` - Currency string `json:"currency"` - TransactionType string `json:"transaction_type"` - OccurredAt string `json:"occurred_at"` - TransferReference string `json:"transfer_reference"` - ChannelName string `json:"channel_name"` - } `json:"data"` - SubscriptionID string `json:"subscription_id"` - EventType string `json:"event_type"` - SchemaVersion string `json:"schema_version"` - SentAt string `json:"sent_at"` +type BalanceUpdateWebhookPayload struct { + Data BalanceUpdateWebhookData `json:"data"` + SubscriptionID string `json:"subscription_id"` + EventType string `json:"event_type"` + SchemaVersion string `json:"schema_version"` + SentAt string `json:"sent_at"` +} + +type BalanceUpdateWebhookData struct { + Resource BalanceUpdateWebhookResource `json:"resource"` + Amount json.Number `json:"amount"` + BalanceID uint64 `json:"balance_id"` + Currency string `json:"currency"` + TransactionType string `json:"transaction_type"` + OccurredAt string `json:"occurred_at"` + TransferReference string `json:"transfer_reference"` + ChannelName string `json:"channel_name"` +} + +type BalanceUpdateWebhookResource struct { + ID uint64 `json:"id"` + ProfileID uint64 `json:"profile_id"` + Type string `json:"type"` } -func (c *client) TranslateBalanceUpdateWebhook(ctx context.Context, payload []byte) (balanceUpdateWebhookPayload, error) { - var balanceUpdateEvent balanceUpdateWebhookPayload +func (c *client) TranslateBalanceUpdateWebhook(ctx context.Context, payload []byte) (BalanceUpdateWebhookPayload, error) { + var balanceUpdateEvent BalanceUpdateWebhookPayload err := json.Unmarshal(payload, &balanceUpdateEvent) if err != nil { - return balanceUpdateWebhookPayload{}, err + return BalanceUpdateWebhookPayload{}, err } return balanceUpdateEvent, nil diff --git a/internal/connectors/plugins/public/wise/plugin.go b/internal/connectors/plugins/public/wise/plugin.go index aae57a78..a8c33da2 100644 --- a/internal/connectors/plugins/public/wise/plugin.go +++ b/internal/connectors/plugins/public/wise/plugin.go @@ -11,7 +11,14 @@ import ( ) var ( - ErrStackPublicUrlMissing = errors.New("STACK_PUBLIC_URL is not set") + HeadersTestNotification = "X-Test-Notification" + HeadersDeliveryID = "X-Delivery-Id" + HeadersSignature = "X-Signature-Sha256" + + ErrStackPublicUrlMissing = errors.New("STACK_PUBLIC_URL is not set") + ErrWebhookHeaderXDeliveryIDMissing = errors.New("missing X-Delivery-Id header") + ErrWebhookHeaderXSignatureMissing = errors.New("missing X-Signature-Sha256 header") + ErrWebhookNameUnknown = errors.New("unknown webhook name") ) type Plugin struct { @@ -134,21 +141,21 @@ func (p *Plugin) TranslateWebhook(ctx context.Context, req models.TranslateWebho return models.TranslateWebhookResponse{}, plugins.ErrNotYetInstalled } - testNotif, ok := req.Webhook.Headers["X-Test-Notification"] + testNotif, ok := req.Webhook.Headers[HeadersTestNotification] if ok && len(testNotif) > 0 { if testNotif[0] == "true" { return models.TranslateWebhookResponse{}, nil } } - v, ok := req.Webhook.Headers["X-Delivery-Id"] + v, ok := req.Webhook.Headers[HeadersDeliveryID] if !ok || len(v) == 0 { - return models.TranslateWebhookResponse{}, errors.New("missing X-Delivery-Id header") + return models.TranslateWebhookResponse{}, ErrWebhookHeaderXDeliveryIDMissing } - signatures, ok := req.Webhook.Headers["X-Signature-Sha256"] + signatures, ok := req.Webhook.Headers[HeadersSignature] if !ok || len(signatures) == 0 { - return models.TranslateWebhookResponse{}, errors.New("missing X-Signature-Sha256 header") + return models.TranslateWebhookResponse{}, ErrWebhookHeaderXSignatureMissing } err := p.verifySignature(req.Webhook.Body, signatures[0]) @@ -158,7 +165,7 @@ func (p *Plugin) TranslateWebhook(ctx context.Context, req models.TranslateWebho config, ok := webhookConfigs[req.Name] if !ok { - return models.TranslateWebhookResponse{}, errors.New("unknown webhook name") + return models.TranslateWebhookResponse{}, ErrWebhookNameUnknown } res, err := config.fn(ctx, req) diff --git a/internal/connectors/plugins/public/wise/plugin_test.go b/internal/connectors/plugins/public/wise/plugin_test.go index 9c7aaca8..e57e1392 100644 --- a/internal/connectors/plugins/public/wise/plugin_test.go +++ b/internal/connectors/plugins/public/wise/plugin_test.go @@ -3,17 +3,24 @@ package wise import ( "bytes" "context" + "crypto" "crypto/rand" "crypto/rsa" + "crypto/sha256" "crypto/x509" + "encoding/base64" "encoding/json" "encoding/pem" + "fmt" "testing" + "time" "github.com/formancehq/payments/internal/connectors/plugins" + "github.com/formancehq/payments/internal/connectors/plugins/public/wise/client" "github.com/formancehq/payments/internal/models" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + "go.uber.org/mock/gomock" ) func TestPlugin(t *testing.T) { @@ -23,15 +30,17 @@ func TestPlugin(t *testing.T) { var _ = Describe("Wise Plugin", func() { var ( - plg *Plugin - block *pem.Block - pemKey *bytes.Buffer + plg *Plugin + block *pem.Block + pemKey *bytes.Buffer + privatekey *rsa.PrivateKey ) BeforeEach(func() { plg = &Plugin{} - privatekey, err := rsa.GenerateKey(rand.Reader, 2048) + var err error + privatekey, err = rsa.GenerateKey(rand.Reader, 2048) Expect(err).To(BeNil()) publickey := &privatekey.PublicKey publicKeyBytes, err := x509.MarshalPKIXPublicKey(publickey) @@ -73,6 +82,122 @@ var _ = Describe("Wise Plugin", func() { }) }) + Context("translate webhook", func() { + var ( + body []byte + signature []byte + m *client.MockClient + ) + + BeforeEach(func() { + config := &Config{ + APIKey: "key", + WebhookPublicKey: pemKey.String(), + } + configJson, err := json.Marshal(config) + req := models.InstallRequest{Config: configJson} + _, err = plg.Install(context.Background(), req) + Expect(err).To(BeNil()) + + ctrl := gomock.NewController(GinkgoT()) + m = client.NewMockClient(ctrl) + plg.SetClient(m) + + body = bytes.NewBufferString("body content").Bytes() + hash := sha256.New() + hash.Write(body) + digest := hash.Sum(nil) + + signature, err = rsa.SignPKCS1v15(rand.Reader, privatekey, crypto.SHA256, digest) + Expect(err).To(BeNil()) + }) + + It("it fails when X-Delivery-ID header missing", func(ctx SpecContext) { + req := models.TranslateWebhookRequest{} + _, err := plg.TranslateWebhook(context.Background(), req) + Expect(err).To(MatchError(ErrWebhookHeaderXDeliveryIDMissing)) + }) + + It("it fails when X-Signature-Sha256 header missing", func(ctx SpecContext) { + req := models.TranslateWebhookRequest{ + Webhook: models.PSPWebhook{ + Headers: map[string][]string{ + HeadersDeliveryID: {"delivery-id"}, + }, + }, + } + _, err := plg.TranslateWebhook(context.Background(), req) + Expect(err).To(MatchError(ErrWebhookHeaderXSignatureMissing)) + }) + + It("it fails when unknown webhook name in request", func(ctx SpecContext) { + req := models.TranslateWebhookRequest{ + Name: "unknown", + Webhook: models.PSPWebhook{ + Body: body, + Headers: map[string][]string{ + HeadersDeliveryID: {"delivery-id"}, + HeadersSignature: {base64.StdEncoding.EncodeToString(signature)}, + }, + }, + } + + _, err := plg.TranslateWebhook(context.Background(), req) + Expect(err).To(MatchError(ErrWebhookNameUnknown)) + }) + + It("it can create the transfer_state_changed webhook", func(ctx SpecContext) { + req := models.TranslateWebhookRequest{ + Name: "transfer_state_changed", + Webhook: models.PSPWebhook{ + Body: body, + Headers: map[string][]string{ + HeadersDeliveryID: {"delivery-id"}, + HeadersSignature: {base64.StdEncoding.EncodeToString(signature)}, + }, + }, + } + transfer := client.Transfer{ID: 1, Reference: "ref1", TargetValue: json.Number("25"), TargetCurrency: "EUR"} + m.EXPECT().TranslateTransferStateChangedWebhook(ctx, body).Return(transfer, nil) + + res, err := plg.TranslateWebhook(ctx, req) + Expect(err).To(BeNil()) + Expect(res.Responses).To(HaveLen(1)) + Expect(res.Responses[0].IdempotencyKey).To(Equal(req.Webhook.Headers[HeadersDeliveryID][0])) + Expect(res.Responses[0].Payment).NotTo(BeNil()) + Expect(res.Responses[0].Payment.Reference).To(Equal(fmt.Sprint(transfer.ID))) + }) + + It("it can create the balance_update webhook", func(ctx SpecContext) { + req := models.TranslateWebhookRequest{ + Name: "balance_update", + Webhook: models.PSPWebhook{ + Body: body, + Headers: map[string][]string{ + HeadersDeliveryID: {"delivery-id"}, + HeadersSignature: {base64.StdEncoding.EncodeToString(signature)}, + }, + }, + } + balance := client.BalanceUpdateWebhookPayload{ + Data: client.BalanceUpdateWebhookData{ + TransferReference: "trx", + OccurredAt: time.Now().Format(time.RFC3339), + Currency: "USD", + Amount: json.Number("43"), + }, + } + m.EXPECT().TranslateBalanceUpdateWebhook(ctx, body).Return(balance, nil) + + res, err := plg.TranslateWebhook(ctx, req) + Expect(err).To(BeNil()) + Expect(res.Responses).To(HaveLen(1)) + Expect(res.Responses[0].IdempotencyKey).To(Equal(req.Webhook.Headers[HeadersDeliveryID][0])) + Expect(res.Responses[0].Payment).NotTo(BeNil()) + Expect(res.Responses[0].Payment.Reference).To(Equal(balance.Data.TransferReference)) + }) + }) + Context("calling functions on uninstalled plugins", func() { It("returns valid uninstall response", func(ctx SpecContext) { req := models.UninstallRequest{ConnectorID: "dummyID"} diff --git a/internal/connectors/plugins/public/wise/webhooks.go b/internal/connectors/plugins/public/wise/webhooks.go index 8db61a25..0ffe065d 100644 --- a/internal/connectors/plugins/public/wise/webhooks.go +++ b/internal/connectors/plugins/public/wise/webhooks.go @@ -149,7 +149,7 @@ func (p *Plugin) verifySignature(body []byte, signature string) error { data, err := base64.StdEncoding.DecodeString(signature) if err != nil { - return err + return fmt.Errorf("failed to decode signature for wise webhook: %w", err) } err = rsa.VerifyPKCS1v15(p.config.webhookPublicKey, crypto.SHA256, msgHashSum, data) From d89142e554c20f622d68b343ea0af732e0a2ba80 Mon Sep 17 00:00:00 2001 From: Crimson Thompson Date: Thu, 10 Oct 2024 18:08:09 +0200 Subject: [PATCH 13/13] feat(payments): keep metadataProfileIDKey private in wise module --- internal/connectors/plugins/public/wise/accounts.go | 2 +- internal/connectors/plugins/public/wise/balances.go | 2 +- internal/connectors/plugins/public/wise/balances_test.go | 2 +- internal/connectors/plugins/public/wise/metadata.go | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/internal/connectors/plugins/public/wise/accounts.go b/internal/connectors/plugins/public/wise/accounts.go index 86ba1fea..a63c859a 100644 --- a/internal/connectors/plugins/public/wise/accounts.go +++ b/internal/connectors/plugins/public/wise/accounts.go @@ -60,7 +60,7 @@ func (p *Plugin) fetchNextAccounts(ctx context.Context, req models.FetchNextAcco Name: &balance.Name, DefaultAsset: pointer.For(currency.FormatAsset(supportedCurrenciesWithDecimal, balance.Amount.Currency)), Metadata: map[string]string{ - MetadataProfileIDKey: strconv.FormatUint(from.ID, 10), + metadataProfileIDKey: strconv.FormatUint(from.ID, 10), }, Raw: raw, }) diff --git a/internal/connectors/plugins/public/wise/balances.go b/internal/connectors/plugins/public/wise/balances.go index 7d32f5be..a706ee2d 100644 --- a/internal/connectors/plugins/public/wise/balances.go +++ b/internal/connectors/plugins/public/wise/balances.go @@ -24,7 +24,7 @@ func (p *Plugin) fetchNextBalances(ctx context.Context, req models.FetchNextBala return models.FetchNextBalancesResponse{}, err } - pID, ok := from.Metadata[MetadataProfileIDKey] + pID, ok := from.Metadata[metadataProfileIDKey] if !ok { return models.FetchNextBalancesResponse{}, errors.New("missing profile ID in from payload when fetching balances") } diff --git a/internal/connectors/plugins/public/wise/balances_test.go b/internal/connectors/plugins/public/wise/balances_test.go index e3114dfa..2806798c 100644 --- a/internal/connectors/plugins/public/wise/balances_test.go +++ b/internal/connectors/plugins/public/wise/balances_test.go @@ -50,7 +50,7 @@ var _ = Describe("Wise Plugin Balances", func() { FromPayload: json.RawMessage(fmt.Sprintf( `{"Reference":"%d","Metadata":{"%s":"%d"}}`, expectedProfileID, - MetadataProfileIDKey, + metadataProfileIDKey, profileVal, )), PageSize: 10, diff --git a/internal/connectors/plugins/public/wise/metadata.go b/internal/connectors/plugins/public/wise/metadata.go index 36c9530a..5261fc4d 100644 --- a/internal/connectors/plugins/public/wise/metadata.go +++ b/internal/connectors/plugins/public/wise/metadata.go @@ -1,5 +1,5 @@ package wise const ( - MetadataProfileIDKey = "profile_id" + metadataProfileIDKey = "profile_id" )