Path: blob/main/components/public-api-server/pkg/webhooks/stripe_test.go
2500 views
// Copyright (c) 2022 Gitpod GmbH. All rights reserved.1// Licensed under the GNU Affero General Public License (AGPL).2// See License.AGPL.txt in the project root for license information.34package webhooks56import (7"bytes"8"encoding/hex"9"fmt"10"net/http"11"testing"12"time"1314"github.com/stripe/stripe-go/v72/webhook"1516"github.com/gitpod-io/gitpod/common-go/baseserver"17"github.com/gitpod-io/gitpod/public-api-server/pkg/billingservice"18mockbillingservice "github.com/gitpod-io/gitpod/public-api-server/pkg/billingservice/mock_billingservice"19"github.com/golang/mock/gomock"20"github.com/stretchr/testify/require"21)2223// https://stripe.com/docs/api/events/types24const (25invoiceUpdatedEventType = "invoice.updated"26customerCreatedEventType = "customer.created"27)2829const (30testWebhookSecret = "whsec_random_secret"31)3233func TestWebhookAcceptsPostRequests(t *testing.T) {34scenarios := []struct {35HttpMethod string36ExpectedStatusCode int37}{38{39HttpMethod: http.MethodPost,40ExpectedStatusCode: http.StatusOK,41},42{43HttpMethod: http.MethodGet,44ExpectedStatusCode: http.StatusMethodNotAllowed,45},46{47HttpMethod: http.MethodPut,48ExpectedStatusCode: http.StatusMethodNotAllowed,49},50}5152srv := baseServerWithStripeWebhook(t, &billingservice.NoOpClient{})5354payload := payloadForStripeEvent(t, InvoiceFinalizedEventType)5556url := fmt.Sprintf("%s%s", srv.HTTPAddress(), "/webhook")5758for _, scenario := range scenarios {59t.Run(scenario.HttpMethod, func(t *testing.T) {60req, err := http.NewRequest(scenario.HttpMethod, url, bytes.NewReader(payload))61require.NoError(t, err)6263req.Header.Set("Stripe-Signature", generateHeader(payload, testWebhookSecret))6465resp, err := http.DefaultClient.Do(req)66require.NoError(t, err)6768require.Equal(t, scenario.ExpectedStatusCode, resp.StatusCode)69})70}71}7273func TestWebhookIgnoresIrrelevantEvents_NoopClient(t *testing.T) {74scenarios := []struct {75EventType string76ExpectedStatusCode int77}{78{79EventType: InvoiceFinalizedEventType,80ExpectedStatusCode: http.StatusOK,81},82{83EventType: CustomerSubscriptionDeletedEventType,84ExpectedStatusCode: http.StatusOK,85},86{87EventType: invoiceUpdatedEventType,88ExpectedStatusCode: http.StatusBadRequest,89},90{91EventType: customerCreatedEventType,92ExpectedStatusCode: http.StatusBadRequest,93},94{95EventType: ChargeDisputeCreatedEventType,96ExpectedStatusCode: http.StatusOK,97},98}99100srv := baseServerWithStripeWebhook(t, &billingservice.NoOpClient{})101102url := fmt.Sprintf("%s%s", srv.HTTPAddress(), "/webhook")103104for _, scenario := range scenarios {105t.Run(scenario.EventType, func(t *testing.T) {106payload := payloadForStripeEvent(t, scenario.EventType)107108req, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(payload))109require.NoError(t, err)110111req.Header.Set("Stripe-Signature", generateHeader(payload, testWebhookSecret))112113resp, err := http.DefaultClient.Do(req)114require.NoError(t, err)115116require.Equal(t, scenario.ExpectedStatusCode, resp.StatusCode)117})118}119}120121// TestWebhookInvokesFinalizeInvoiceRPC ensures that when the webhook is hit with a122// `invoice.finalized` event, the `FinalizeInvoice` method on the billing service is invoked123// with the invoice id from the event payload.124func TestWebhookInvokesFinalizeInvoiceRPC(t *testing.T) {125ctrl := gomock.NewController(t)126m := mockbillingservice.NewMockInterface(ctrl)127m.EXPECT().FinalizeInvoice(gomock.Any(), gomock.Eq("in_1LUQi7GadRXm50o36jWK7ehs"))128129srv := baseServerWithStripeWebhook(t, m)130131url := fmt.Sprintf("%s%s", srv.HTTPAddress(), "/webhook")132133payload := payloadForStripeEvent(t, InvoiceFinalizedEventType)134req, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(payload))135require.NoError(t, err)136137req.Header.Set("Stripe-Signature", generateHeader(payload, testWebhookSecret))138139resp, err := http.DefaultClient.Do(req)140require.NoError(t, err)141require.Equal(t, http.StatusOK, resp.StatusCode)142}143144func TestWebhookInvokesCancelSubscriptionRPC(t *testing.T) {145ctrl := gomock.NewController(t)146m := mockbillingservice.NewMockInterface(ctrl)147m.EXPECT().CancelSubscription(gomock.Any(), gomock.Eq("in_1LUQi7GadRXm50o36jWK7ehs"))148149srv := baseServerWithStripeWebhook(t, m)150151url := fmt.Sprintf("%s%s", srv.HTTPAddress(), "/webhook")152153payload := payloadForStripeEvent(t, CustomerSubscriptionDeletedEventType)154req, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(payload))155require.NoError(t, err)156157req.Header.Set("Stripe-Signature", generateHeader(payload, testWebhookSecret))158159resp, err := http.DefaultClient.Do(req)160require.NoError(t, err)161require.Equal(t, http.StatusOK, resp.StatusCode)162}163164func baseServerWithStripeWebhook(t *testing.T, billingService billingservice.Interface) *baseserver.Server {165t.Helper()166167srv := baseserver.NewForTests(t,168baseserver.WithHTTP(baseserver.MustUseRandomLocalAddress(t)),169)170baseserver.StartServerForTests(t, srv)171172srv.HTTPMux().Handle("/webhook", NewStripeWebhookHandler(billingService, testWebhookSecret))173174return srv175}176177func payloadForStripeEvent(t *testing.T, eventType string) []byte {178t.Helper()179180return []byte(`{181"data": {182"object": {183"id": "in_1LUQi7GadRXm50o36jWK7ehs"184}185},186"type": "` + eventType + `"187}`)188}189190func generateHeader(payload []byte, secret string) string {191now := time.Now()192signature := webhook.ComputeSignature(now, payload, secret)193return fmt.Sprintf("t=%d,%s=%s", now.Unix(), "v1", hex.EncodeToString(signature))194}195196197