Skip to content

Commit

Permalink
feat(payments): add wise webhook tests
Browse files Browse the repository at this point in the history
  • Loading branch information
laouji committed Oct 9, 2024
1 parent 5b78eb7 commit bbe5399
Show file tree
Hide file tree
Showing 6 changed files with 173 additions and 37 deletions.
2 changes: 1 addition & 1 deletion internal/connectors/plugins/public/wise/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

48 changes: 26 additions & 22 deletions internal/connectors/plugins/public/wise/client/webhooks.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
21 changes: 14 additions & 7 deletions internal/connectors/plugins/public/wise/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -126,21 +133,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])
Expand All @@ -150,7 +157,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)
Expand Down
133 changes: 129 additions & 4 deletions internal/connectors/plugins/public/wise/plugin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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)
Expand Down Expand Up @@ -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"}
Expand Down
2 changes: 1 addition & 1 deletion internal/connectors/plugins/public/wise/webhooks.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down

0 comments on commit bbe5399

Please sign in to comment.