Files
CrimsonandGitHub c48bb246b5 feat(Stripe): EN-470 Add balance.available webhooks to Stripe connector (#590)
* Implement Balances Webhooks in Stripe to reduce polling workflows

* Create webhooks on install

* Remove webhook at uninstall time

* Add verifyWebhook function to Stripe

* Pin APIVersion in Stripe Webhook Create call

* Default to root account when receiving balance.update webhook

* Only make call do delete webhooks if webhooks were setup to begin with

* Set up two webhook endpoints - one for connect and one for root account

* Use event.Created to determine balance time in Stripe webhooks

* Start recording root account ID instead of dummy 'root' value

* Create unit test for Stripe create webhooks

* Add tests for Stripe Translate webhook

* Share logic between Stripe balance polling and webhook handle

* Use supported webhooks map

* Update existing stripe webhooks instead of creating new ones at app start time

* Run install sequence for each connector at app start of payments api pods

* Turn of balances periodically for Stripe

* Add nil checks for balance.Available

* Remove potentially sensitive information from error message

* Add migration tool to remove FETCH BALANCES schedules from stripe
2025-11-21 10:25:52 +00:00

199 lines
6.2 KiB
Go

package stripe
import (
"encoding/json"
"testing"
"github.com/formancehq/go-libs/v3/logging"
"github.com/formancehq/payments/internal/connectors/plugins"
"github.com/formancehq/payments/internal/connectors/plugins/public/stripe/client"
"github.com/formancehq/payments/internal/models"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/stripe/stripe-go/v80"
gomock "go.uber.org/mock/gomock"
)
func TestPlugin(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Stripe Plugin Suite")
}
var _ = Describe("Stripe Plugin", func() {
var (
plg *Plugin
ctrl *gomock.Controller
logger = logging.NewDefaultLogger(GinkgoWriter, true, false, false)
)
BeforeEach(func() {
plg = &Plugin{
Plugin: plugins.NewBasePlugin(),
}
ctrl = gomock.NewController(GinkgoT())
})
Context("install", func() {
It("should report errors in config - apiKey", func(ctx SpecContext) {
config := json.RawMessage(`{}`)
_, err := New("stripe", logger, config, nil)
Expect(err.Error()).To(ContainSubstring("APIKey"))
})
It("should return valid install response", func(ctx SpecContext) {
config := json.RawMessage(`{"apiKey": "test"}`)
stripeMock := client.NewMockBackend(ctrl)
stripeMock.EXPECT().Call(gomock.Any(), gomock.Any(), gomock.Any(), nil, &stripe.Account{}).DoAndReturn(
func(_, _, _ string, _ any, account *stripe.Account) error {
account.ID = "rootID"
return nil
})
plg1, err := New("stripe", logger, config, stripeMock)
Expect(err).To(BeNil())
req := models.InstallRequest{}
res, err := plg1.Install(ctx, req)
Expect(err).To(BeNil())
Expect(len(res.Workflow) > 0).To(BeTrue())
Expect(res.Workflow).To(Equal(workflow()))
})
})
Context("uninstall", func() {
It("should return valid uninstall response", func(ctx SpecContext) {
req := models.UninstallRequest{ConnectorID: "test"}
resp, err := plg.Uninstall(ctx, req)
Expect(err).To(BeNil())
Expect(resp).To(Equal(models.UninstallResponse{}))
})
})
Context("fetch next accounts", func() {
It("should fail when called before install", func(ctx SpecContext) {
req := models.FetchNextAccountsRequest{State: json.RawMessage(`{}`)}
_, err := plg.FetchNextAccounts(ctx, req)
Expect(err).To(MatchError(plugins.ErrNotYetInstalled))
})
// Other tests will be in accounts_test.go
})
Context("fetch next balances", func() {
It("should fail when called before install", func(ctx SpecContext) {
req := models.FetchNextBalancesRequest{State: json.RawMessage(`{}`)}
_, err := plg.FetchNextBalances(ctx, req)
Expect(err).To(MatchError(plugins.ErrNotYetInstalled))
})
// Other tests will be in balances_test.go
})
Context("fetch next external accounts", func() {
It("should fail when called before install", func(ctx SpecContext) {
req := models.FetchNextExternalAccountsRequest{State: json.RawMessage(`{}`)}
_, err := plg.FetchNextExternalAccounts(ctx, req)
Expect(err).To(MatchError(plugins.ErrNotYetInstalled))
})
// Other tests will be in external_accounts_test.go
})
Context("fetch next payments", func() {
It("should fail when called before install", func(ctx SpecContext) {
req := models.FetchNextPaymentsRequest{State: json.RawMessage(`{}`)}
_, err := plg.FetchNextPayments(ctx, req)
Expect(err).To(MatchError(plugins.ErrNotYetInstalled))
})
// Other tests will be in payments_test.go
})
Context("fetch next others", func() {
It("should fail because not implemented", func(ctx SpecContext) {
req := models.FetchNextOthersRequest{State: json.RawMessage(`{}`)}
_, err := plg.FetchNextOthers(ctx, req)
Expect(err).To(MatchError(plugins.ErrNotImplemented))
})
})
Context("create bank account", func() {
It("should fail because not implemented", func(ctx SpecContext) {
req := models.CreateBankAccountRequest{}
_, err := plg.CreateBankAccount(ctx, req)
Expect(err).To(MatchError(plugins.ErrNotImplemented))
})
})
Context("create transfer", func() {
It("should fail when called before install", func(ctx SpecContext) {
req := models.CreateTransferRequest{}
_, err := plg.CreateTransfer(ctx, req)
Expect(err).To(MatchError(plugins.ErrNotYetInstalled))
})
// Other tests will be in create_transfers_test.go
})
Context("reverse transfer", func() {
It("should fail when called before install", func(ctx SpecContext) {
req := models.ReverseTransferRequest{}
_, err := plg.ReverseTransfer(ctx, req)
Expect(err).To(MatchError(plugins.ErrNotYetInstalled))
})
// Other tests will be in reverse_transfer_test.go
})
Context("poll transfer status", func() {
It("should fail because not implemented", func(ctx SpecContext) {
req := models.PollTransferStatusRequest{}
_, err := plg.PollTransferStatus(ctx, req)
Expect(err).To(MatchError(plugins.ErrNotImplemented))
})
})
Context("create payout", func() {
It("should fail when called before install", func(ctx SpecContext) {
req := models.CreatePayoutRequest{}
_, err := plg.CreatePayout(ctx, req)
Expect(err).To(MatchError(plugins.ErrNotYetInstalled))
})
// Other tests will be in create_payouts_test.go
})
Context("reverse payout", func() {
It("should fail because not implemented", func(ctx SpecContext) {
req := models.ReversePayoutRequest{}
_, err := plg.ReversePayout(ctx, req)
Expect(err).To(MatchError(plugins.ErrNotImplemented))
})
})
Context("poll payout status", func() {
It("should fail because not implemented", func(ctx SpecContext) {
req := models.PollPayoutStatusRequest{}
_, err := plg.PollPayoutStatus(ctx, req)
Expect(err).To(MatchError(plugins.ErrNotImplemented))
})
})
Context("create webhooks", func() {
It("should fail when called before install", func(ctx SpecContext) {
req := models.CreateWebhooksRequest{}
_, err := plg.CreateWebhooks(ctx, req)
Expect(err).To(MatchError(plugins.ErrNotYetInstalled))
})
// Other tests will be in webhooks_test.go
})
Context("translate webhook", func() {
It("should fail when called before install", func(ctx SpecContext) {
req := models.TranslateWebhookRequest{}
_, err := plg.TranslateWebhook(ctx, req)
Expect(err).To(MatchError(plugins.ErrNotYetInstalled))
})
// Other tests will be in webhooks_test.go
})
})