mirror of
https://github.com/hanzo-fi/payments.git
synced 2026-10-30 20:31:20 +00:00
328 lines
9.8 KiB
Go
328 lines
9.8 KiB
Go
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/hanzo-fi/go-libs/v5/pkg/observe/log"
|
|
"github.com/hanzo-fi/payments/pkg/domain/plugins"
|
|
"github.com/hanzo-fi/payments/ce/plugins/wise/client"
|
|
"github.com/hanzo-fi/payments/pkg/domain/models"
|
|
. "github.com/onsi/ginkgo/v2"
|
|
. "github.com/onsi/gomega"
|
|
"go.uber.org/mock/gomock"
|
|
)
|
|
|
|
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
|
|
privatekey *rsa.PrivateKey
|
|
logger = logging.NewDefaultLogger(GinkgoWriter, true, false, false)
|
|
)
|
|
|
|
BeforeEach(func() {
|
|
plg = &Plugin{
|
|
Plugin: plugins.NewBasePlugin(),
|
|
}
|
|
|
|
plg.supportedWebhooks = map[string]supportedWebhook{
|
|
"transfer_state_changed": {
|
|
triggerOn: "transfers#state-change",
|
|
urlPath: "/transferstatechanged",
|
|
fn: plg.translateTransferStateChangedWebhook,
|
|
version: "2.0.0",
|
|
},
|
|
"balance_update": {
|
|
triggerOn: "balances#update",
|
|
urlPath: "/balanceupdate",
|
|
fn: plg.translateBalanceUpdateWebhook,
|
|
version: "2.2.0",
|
|
},
|
|
}
|
|
|
|
var err error
|
|
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) {
|
|
_, err := New("wise", logger, json.RawMessage(`{}`))
|
|
Expect(err).To(MatchError(ContainSubstring("validation")))
|
|
})
|
|
It("rejects malformed pem keys", func(ctx SpecContext) {
|
|
config := json.RawMessage(`{"apiKey":"dummy","webhookPublicKey":"badKey"}`)
|
|
_, err := New("wise", logger, config)
|
|
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)
|
|
Expect(err).To(BeNil())
|
|
_, err = New("wise", logger, configJson)
|
|
Expect(err).To(BeNil())
|
|
req := models.InstallRequest{}
|
|
res, err := plg.Install(context.Background(), req)
|
|
Expect(err).To(BeNil())
|
|
Expect(len(res.Workflow) > 0).To(BeTrue())
|
|
Expect(res.Workflow[0].Name).To(Equal("fetch_profiles"))
|
|
})
|
|
})
|
|
|
|
Context("verifying webhook", func() {
|
|
var (
|
|
body []byte
|
|
signature []byte
|
|
ctrl *gomock.Controller
|
|
m *client.MockClient
|
|
)
|
|
|
|
BeforeEach(func() {
|
|
config := &Config{
|
|
APIKey: "key",
|
|
WebhookPublicKey: pemKey.String(),
|
|
}
|
|
configJson, err := json.Marshal(config)
|
|
Expect(err).To(BeNil())
|
|
plg, err = New("wise", logger, configJson)
|
|
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())
|
|
})
|
|
|
|
AfterEach(func() {
|
|
ctrl.Finish()
|
|
})
|
|
|
|
It("it fails when X-Delivery-ID header missing", func(ctx SpecContext) {
|
|
req := models.VerifyWebhookRequest{}
|
|
_, err := plg.VerifyWebhook(context.Background(), req)
|
|
Expect(err).To(MatchError(ErrWebhookHeaderXDeliveryIDMissing))
|
|
})
|
|
|
|
It("it fails when X-Signature-Sha256 header missing", func(ctx SpecContext) {
|
|
req := models.VerifyWebhookRequest{
|
|
Webhook: models.PSPWebhook{
|
|
Headers: map[string][]string{
|
|
HeadersDeliveryID: {"delivery-id"},
|
|
},
|
|
},
|
|
}
|
|
_, err := plg.VerifyWebhook(context.Background(), req)
|
|
Expect(err).To(MatchError(ErrWebhookHeaderXSignatureMissing))
|
|
})
|
|
|
|
It("it can create the balance_update webhook", func(ctx SpecContext) {
|
|
req := models.VerifyWebhookRequest{
|
|
Config: &models.WebhookConfig{Name: "balance_update"},
|
|
Webhook: models.PSPWebhook{
|
|
Body: body,
|
|
Headers: map[string][]string{
|
|
HeadersDeliveryID: {"delivery-id"},
|
|
HeadersSignature: {base64.StdEncoding.EncodeToString(signature)},
|
|
},
|
|
},
|
|
}
|
|
|
|
res, err := plg.VerifyWebhook(ctx, req)
|
|
Expect(err).To(BeNil())
|
|
Expect(res.WebhookIdempotencyKey).To(Equal(&req.Webhook.Headers[HeadersDeliveryID][0]))
|
|
})
|
|
})
|
|
|
|
Context("translate webhook", func() {
|
|
var (
|
|
body []byte
|
|
signature []byte
|
|
ctrl *gomock.Controller
|
|
m *client.MockClient
|
|
)
|
|
|
|
BeforeEach(func() {
|
|
config := &Config{
|
|
APIKey: "key",
|
|
WebhookPublicKey: pemKey.String(),
|
|
}
|
|
configJson, err := json.Marshal(config)
|
|
Expect(err).To(BeNil())
|
|
plg, err = New("wise", logger, configJson)
|
|
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())
|
|
})
|
|
|
|
AfterEach(func() {
|
|
ctrl.Finish()
|
|
})
|
|
|
|
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(gomock.Any(), body).Return(transfer, nil)
|
|
|
|
res, err := plg.TranslateWebhook(ctx, req)
|
|
Expect(err).To(BeNil())
|
|
Expect(res.Responses).To(HaveLen(1))
|
|
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(gomock.Any(), body).Return(balance, nil)
|
|
|
|
res, err := plg.TranslateWebhook(ctx, req)
|
|
Expect(err).To(BeNil())
|
|
Expect(res.Responses).To(HaveLen(1))
|
|
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"}
|
|
_, 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 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(`{}`),
|
|
}
|
|
_, 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))
|
|
})
|
|
It("fails when create transfer is called before install", func(ctx SpecContext) {
|
|
req := models.CreateTransferRequest{}
|
|
_, err := plg.CreateTransfer(context.Background(), req)
|
|
Expect(err).To(MatchError(plugins.ErrNotYetInstalled))
|
|
})
|
|
It("fails when create transfer is called before install", func(ctx SpecContext) {
|
|
req := models.CreatePayoutRequest{}
|
|
_, err := plg.CreatePayout(context.Background(), req)
|
|
Expect(err).To(MatchError(plugins.ErrNotYetInstalled))
|
|
})
|
|
})
|
|
})
|