Files

559 lines
15 KiB
Go

package adyen
import (
"context"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"math/big"
"time"
"github.com/adyen/adyen-go-api-library/v7/src/webhook"
"github.com/hanzo-fi/go-libs/v5/pkg/types/pointer"
"github.com/hanzo-fi/payments/ce/plugins/adyen/client"
"github.com/hanzo-fi/payments/pkg/domain/models"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
gomock "go.uber.org/mock/gomock"
)
var _ = Describe("Adyen Plugin Accounts", func() {
var (
plg models.Plugin
ctrl *gomock.Controller
m *client.MockClient
now time.Time
)
BeforeEach(func() {
ctrl = gomock.NewController(GinkgoT())
m = client.NewMockClient(ctrl)
p := &Plugin{client: m}
p.initWebhookConfig()
plg = p
now = time.Now().UTC()
})
AfterEach(func() {
ctrl.Finish()
})
Context("creating webhooks", func() {
It("should fail - stack public url not set", func(ctx SpecContext) {
req := models.CreateWebhooksRequest{
ConnectorID: "test",
}
_, err := plg.CreateWebhooks(ctx, req)
Expect(err).To(MatchError("STACK_PUBLIC_URL is not set"))
})
It("should fail - wrong url format", func(ctx SpecContext) {
req := models.CreateWebhooksRequest{
ConnectorID: "test",
WebhookBaseUrl: "&grjete%",
}
_, err := plg.CreateWebhooks(ctx, req)
Expect(err).ToNot(BeNil())
})
It("should work perfectly", func(ctx SpecContext) {
req := models.CreateWebhooksRequest{
ConnectorID: "test",
WebhookBaseUrl: "http://localhost:8080/test",
}
expectedURL := "http://localhost:8080/test/standard"
m.EXPECT().CreateWebhook(gomock.Any(), expectedURL, req.ConnectorID).Return(client.CreateWebhookResponse{HMACKey: "test"}, nil)
configs, err := plg.CreateWebhooks(ctx, req)
Expect(err).To(BeNil())
Expect(configs.Configs).To(HaveLen(1))
Expect(configs.Configs[0].Metadata[webhookHMACMetadataKey]).To(Equal("test"))
})
})
Context("verifying webhooks", func() {
var (
w webhook.Webhook
)
BeforeEach(func() {
w = webhook.Webhook{
Live: "false",
NotificationItems: &[]webhook.NotificationItem{
{
NotificationRequestItem: webhook.NotificationRequestItem{
PspReference: "test",
Amount: webhook.Amount{
Currency: "EUR",
Value: 100,
},
EventCode: webhook.EventCodeAuthorisation,
EventDate: &now,
MerchantAccountCode: "test",
Operations: []string{},
PaymentMethod: "visa",
Success: "true",
},
},
},
}
})
It("should fail - wrong basic auth", func(ctx SpecContext) {
req := models.VerifyWebhookRequest{
Config: &models.WebhookConfig{
Name: "standard",
Metadata: map[string]string{
webhookHMACMetadataKey: "test",
},
},
Webhook: models.PSPWebhook{
BasicAuth: &models.BasicAuth{
Username: "test",
Password: "test",
},
QueryValues: map[string][]string{},
Headers: map[string][]string{},
Body: []byte{},
},
}
m.EXPECT().VerifyWebhookBasicAuth(req.Webhook.BasicAuth).Return(
false,
)
_, err := plg.VerifyWebhook(ctx, req)
Expect(err).ToNot(BeNil())
Expect(err).To(MatchError("invalid basic auth: webhook verification error"))
})
It("should not process - invalid hmac", func(ctx SpecContext) {
b, _ := json.Marshal(&w)
req := models.VerifyWebhookRequest{
Config: &models.WebhookConfig{
Name: "standard",
Metadata: map[string]string{
webhookHMACMetadataKey: "test",
},
},
Webhook: models.PSPWebhook{
QueryValues: map[string][]string{},
Headers: map[string][]string{},
Body: b,
},
}
m.EXPECT().VerifyWebhookBasicAuth(req.Webhook.BasicAuth).Return(true)
m.EXPECT().TranslateWebhook(string(req.Webhook.Body)).Return(&w, nil)
m.EXPECT().VerifyWebhookHMAC(gomock.Any(), "test").Return(false)
resp, err := plg.VerifyWebhook(ctx, req)
Expect(err).ToNot(BeNil())
Expect(resp.WebhookIdempotencyKey).To(BeNil())
})
It("should be ok", func(ctx SpecContext) {
b, _ := json.Marshal(&w)
sha := sha256.Sum256(b)
ik := base64.StdEncoding.EncodeToString(sha[:])
req := models.VerifyWebhookRequest{
Config: &models.WebhookConfig{
Name: "standard",
Metadata: map[string]string{
webhookHMACMetadataKey: "test",
},
},
Webhook: models.PSPWebhook{
QueryValues: map[string][]string{},
Headers: map[string][]string{},
Body: b,
},
}
m.EXPECT().VerifyWebhookBasicAuth(req.Webhook.BasicAuth).Return(true)
m.EXPECT().TranslateWebhook(string(req.Webhook.Body)).Return(&w, nil)
m.EXPECT().VerifyWebhookHMAC(gomock.Any(), "test").Return(true)
resp, err := plg.VerifyWebhook(ctx, req)
Expect(err).To(BeNil())
Expect(resp.WebhookIdempotencyKey).To(Equal(pointer.For(ik)))
})
})
Context("translating webhooks", func() {
It("should handle authorization", func(ctx SpecContext) {
expectedPSPPayment := models.PSPPayment{
Reference: "test",
CreatedAt: now,
Type: models.PAYMENT_TYPE_PAYIN,
Amount: big.NewInt(100),
Asset: "EUR/2",
Scheme: models.PAYMENT_SCHEME_CARD_VISA,
Status: models.PAYMENT_STATUS_AUTHORISATION,
DestinationAccountReference: pointer.For("test"),
}
doTranslateCall(
ctx,
plg,
m,
webhook.EventCodeAuthorisation,
100,
now,
expectedPSPPayment,
)
})
})
It("should handle authorisation adjustments", func(ctx SpecContext) {
expectedPSPPayment := models.PSPPayment{
ParentReference: "test1",
Reference: "test",
CreatedAt: now,
Type: models.PAYMENT_TYPE_PAYIN,
Amount: big.NewInt(150),
Asset: "EUR/2",
Scheme: models.PAYMENT_SCHEME_OTHER,
Status: models.PAYMENT_STATUS_AMOUNT_ADJUSTMENT,
DestinationAccountReference: pointer.For("test"),
}
doTranslateCall(
ctx,
plg,
m,
webhook.EventCodeAuthorisationAdjustment,
150,
now,
expectedPSPPayment,
)
})
It("should handle cancellation", func(ctx SpecContext) {
expectedPSPPayment := models.PSPPayment{
ParentReference: "test1",
Reference: "test",
CreatedAt: now,
Type: models.PAYMENT_TYPE_PAYIN,
Amount: big.NewInt(100),
Asset: "EUR/2",
Scheme: models.PAYMENT_SCHEME_CARD_VISA,
Status: models.PAYMENT_STATUS_CANCELLED,
DestinationAccountReference: pointer.For("test"),
}
doTranslateCall(
ctx,
plg,
m,
webhook.EventCodeCancellation,
100,
now,
expectedPSPPayment,
)
})
It("should handle capture", func(ctx SpecContext) {
expectedPSPPayment := models.PSPPayment{
ParentReference: "test1",
Reference: "test",
CreatedAt: now,
Type: models.PAYMENT_TYPE_PAYIN,
Amount: big.NewInt(50),
Asset: "EUR/2",
Scheme: models.PAYMENT_SCHEME_OTHER,
Status: models.PAYMENT_STATUS_CAPTURE,
DestinationAccountReference: pointer.For("test"),
}
doTranslateCall(
ctx,
plg,
m,
webhook.EventCodeCapture,
50,
now,
expectedPSPPayment,
)
})
It("should handle capture failed", func(ctx SpecContext) {
expectedPSPPayment := models.PSPPayment{
ParentReference: "test1",
Reference: "test",
CreatedAt: now,
Type: models.PAYMENT_TYPE_PAYIN,
Amount: big.NewInt(50),
Asset: "EUR/2",
Scheme: models.PAYMENT_SCHEME_OTHER,
Status: models.PAYMENT_STATUS_CAPTURE_FAILED,
DestinationAccountReference: pointer.For("test"),
}
doTranslateCall(
ctx,
plg,
m,
webhook.EventCodeCaptureFailed,
50,
now,
expectedPSPPayment,
)
})
It("should handle refund", func(ctx SpecContext) {
expectedPSPPayment := models.PSPPayment{
ParentReference: "test1",
Reference: "test",
CreatedAt: now,
Type: models.PAYMENT_TYPE_PAYIN,
Amount: big.NewInt(50),
Asset: "EUR/2",
Scheme: models.PAYMENT_SCHEME_OTHER,
Status: models.PAYMENT_STATUS_REFUNDED,
DestinationAccountReference: pointer.For("test"),
}
doTranslateCall(
ctx,
plg,
m,
webhook.EventCodeRefund,
50,
now,
expectedPSPPayment,
)
})
It("should handle refund failed", func(ctx SpecContext) {
expectedPSPPayment := models.PSPPayment{
ParentReference: "test1",
Reference: "test",
CreatedAt: now,
Type: models.PAYMENT_TYPE_PAYIN,
Amount: big.NewInt(50),
Asset: "EUR/2",
Scheme: models.PAYMENT_SCHEME_OTHER,
Status: models.PAYMENT_STATUS_REFUNDED_FAILURE,
DestinationAccountReference: pointer.For("test"),
}
doTranslateCall(
ctx,
plg,
m,
webhook.EventCodeRefundFailed,
50,
now,
expectedPSPPayment,
)
})
It("should handle refund reversed", func(ctx SpecContext) {
expectedPSPPayment := models.PSPPayment{
ParentReference: "test1",
Reference: "test",
CreatedAt: now,
Type: models.PAYMENT_TYPE_PAYIN,
Amount: big.NewInt(100),
Asset: "EUR/2",
Scheme: models.PAYMENT_SCHEME_OTHER,
Status: models.PAYMENT_STATUS_REFUND_REVERSED,
DestinationAccountReference: pointer.For("test"),
}
doTranslateCall(
ctx,
plg,
m,
webhook.EventCodeRefundedReversed,
100,
now,
expectedPSPPayment,
)
})
It("should handle refund with data", func(ctx SpecContext) {
expectedPSPPayment := models.PSPPayment{
ParentReference: "test1",
Reference: "test",
CreatedAt: now,
Type: models.PAYMENT_TYPE_PAYIN,
Amount: big.NewInt(100),
Asset: "EUR/2",
Scheme: models.PAYMENT_SCHEME_OTHER,
Status: models.PAYMENT_STATUS_REFUNDED,
DestinationAccountReference: pointer.For("test"),
}
doTranslateCall(
ctx,
plg,
m,
webhook.EventCodeRefundWithData,
100,
now,
expectedPSPPayment,
)
})
It("should handle payouts to third party", func(ctx SpecContext) {
expectedPSPPayment := models.PSPPayment{
Reference: "test",
CreatedAt: now,
Type: models.PAYMENT_TYPE_PAYOUT,
Amount: big.NewInt(100),
Asset: "EUR/2",
Scheme: models.PAYMENT_SCHEME_OTHER,
Status: models.PAYMENT_STATUS_SUCCEEDED,
SourceAccountReference: pointer.For("test"),
}
doTranslateCall(
ctx,
plg,
m,
webhook.EventCodePayoutThirdparty,
100,
now,
expectedPSPPayment,
)
})
It("should handle payouts declined", func(ctx SpecContext) {
expectedPSPPayment := models.PSPPayment{
ParentReference: "test1",
Reference: "test",
CreatedAt: now,
Type: models.PAYMENT_TYPE_PAYOUT,
Amount: big.NewInt(100),
Asset: "EUR/2",
Scheme: models.PAYMENT_SCHEME_OTHER,
Status: models.PAYMENT_STATUS_FAILED,
SourceAccountReference: pointer.For("test"),
}
doTranslateCall(
ctx,
plg,
m,
webhook.EventCodePayoutDecline,
100,
now,
expectedPSPPayment,
)
})
It("should handle payouts expired", func(ctx SpecContext) {
expectedPSPPayment := models.PSPPayment{
ParentReference: "test1",
Reference: "test",
CreatedAt: now,
Type: models.PAYMENT_TYPE_PAYOUT,
Amount: big.NewInt(100),
Asset: "EUR/2",
Scheme: models.PAYMENT_SCHEME_OTHER,
Status: models.PAYMENT_STATUS_EXPIRED,
SourceAccountReference: pointer.For("test"),
}
doTranslateCall(
ctx,
plg,
m,
webhook.EventCodePayoutExpire,
100,
now,
expectedPSPPayment,
)
})
})
func doTranslateCall(
ctx context.Context,
plg models.Plugin,
m *client.MockClient,
eventCode string,
amount int64,
now time.Time,
expectedPSPPayment models.PSPPayment,
) {
w := webhook.Webhook{
Live: "false",
NotificationItems: &[]webhook.NotificationItem{
{
NotificationRequestItem: webhook.NotificationRequestItem{
OriginalReference: "test1",
PspReference: "test",
Amount: webhook.Amount{
Currency: "EUR",
Value: amount,
},
EventCode: eventCode,
EventDate: &now,
MerchantReference: "test",
MerchantAccountCode: "test",
PaymentMethod: "visa",
Success: "true",
},
},
},
}
b, _ := json.Marshal(&w)
req := models.TranslateWebhookRequest{
Name: "standard",
Webhook: models.PSPWebhook{
QueryValues: map[string][]string{},
Headers: map[string][]string{},
Body: b,
},
}
m.EXPECT().TranslateWebhook(string(req.Webhook.Body)).Return(&w, nil)
resp, err := plg.TranslateWebhook(ctx, req)
Expect(err).To(BeNil())
Expect(len(resp.Responses)).To(Equal(1))
comparePayments(*resp.Responses[0].Payment, expectedPSPPayment)
}
func comparePayments(a, b models.PSPPayment) {
Expect(a.ParentReference).To(Equal(b.ParentReference))
Expect(a.Reference).To(Equal(b.Reference))
Expect(a.CreatedAt).To(Equal(b.CreatedAt))
Expect(a.Type).To(Equal(b.Type))
Expect(a.Amount.String()).To(Equal(b.Amount.String()))
Expect(a.Asset).To(Equal(b.Asset))
Expect(a.Scheme).To(Equal(b.Scheme))
Expect(a.Status).To(Equal(b.Status))
switch {
case a.SourceAccountReference != nil && b.SourceAccountReference != nil:
Expect(*a.SourceAccountReference).To(Equal(*b.SourceAccountReference))
case a.SourceAccountReference == nil && b.SourceAccountReference == nil:
default:
Fail("SourceAccountReference is not equal")
}
switch {
case a.DestinationAccountReference != nil && b.DestinationAccountReference != nil:
Expect(*a.DestinationAccountReference).To(Equal(*b.DestinationAccountReference))
case a.DestinationAccountReference == nil && b.DestinationAccountReference == nil:
default:
Fail("DestinationAccountReference is not equal")
}
Expect(len(a.Metadata)).To(Equal(len(b.Metadata)))
for k, v := range a.Metadata {
Expect(b.Metadata[k]).To(Equal(v))
}
}