Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
gitpod-io
GitHub Repository: gitpod-io/gitpod
Path: blob/main/components/public-api-server/pkg/webhooks/stripe_test.go
2500 views
1
// Copyright (c) 2022 Gitpod GmbH. All rights reserved.
2
// Licensed under the GNU Affero General Public License (AGPL).
3
// See License.AGPL.txt in the project root for license information.
4
5
package webhooks
6
7
import (
8
"bytes"
9
"encoding/hex"
10
"fmt"
11
"net/http"
12
"testing"
13
"time"
14
15
"github.com/stripe/stripe-go/v72/webhook"
16
17
"github.com/gitpod-io/gitpod/common-go/baseserver"
18
"github.com/gitpod-io/gitpod/public-api-server/pkg/billingservice"
19
mockbillingservice "github.com/gitpod-io/gitpod/public-api-server/pkg/billingservice/mock_billingservice"
20
"github.com/golang/mock/gomock"
21
"github.com/stretchr/testify/require"
22
)
23
24
// https://stripe.com/docs/api/events/types
25
const (
26
invoiceUpdatedEventType = "invoice.updated"
27
customerCreatedEventType = "customer.created"
28
)
29
30
const (
31
testWebhookSecret = "whsec_random_secret"
32
)
33
34
func TestWebhookAcceptsPostRequests(t *testing.T) {
35
scenarios := []struct {
36
HttpMethod string
37
ExpectedStatusCode int
38
}{
39
{
40
HttpMethod: http.MethodPost,
41
ExpectedStatusCode: http.StatusOK,
42
},
43
{
44
HttpMethod: http.MethodGet,
45
ExpectedStatusCode: http.StatusMethodNotAllowed,
46
},
47
{
48
HttpMethod: http.MethodPut,
49
ExpectedStatusCode: http.StatusMethodNotAllowed,
50
},
51
}
52
53
srv := baseServerWithStripeWebhook(t, &billingservice.NoOpClient{})
54
55
payload := payloadForStripeEvent(t, InvoiceFinalizedEventType)
56
57
url := fmt.Sprintf("%s%s", srv.HTTPAddress(), "/webhook")
58
59
for _, scenario := range scenarios {
60
t.Run(scenario.HttpMethod, func(t *testing.T) {
61
req, err := http.NewRequest(scenario.HttpMethod, url, bytes.NewReader(payload))
62
require.NoError(t, err)
63
64
req.Header.Set("Stripe-Signature", generateHeader(payload, testWebhookSecret))
65
66
resp, err := http.DefaultClient.Do(req)
67
require.NoError(t, err)
68
69
require.Equal(t, scenario.ExpectedStatusCode, resp.StatusCode)
70
})
71
}
72
}
73
74
func TestWebhookIgnoresIrrelevantEvents_NoopClient(t *testing.T) {
75
scenarios := []struct {
76
EventType string
77
ExpectedStatusCode int
78
}{
79
{
80
EventType: InvoiceFinalizedEventType,
81
ExpectedStatusCode: http.StatusOK,
82
},
83
{
84
EventType: CustomerSubscriptionDeletedEventType,
85
ExpectedStatusCode: http.StatusOK,
86
},
87
{
88
EventType: invoiceUpdatedEventType,
89
ExpectedStatusCode: http.StatusBadRequest,
90
},
91
{
92
EventType: customerCreatedEventType,
93
ExpectedStatusCode: http.StatusBadRequest,
94
},
95
{
96
EventType: ChargeDisputeCreatedEventType,
97
ExpectedStatusCode: http.StatusOK,
98
},
99
}
100
101
srv := baseServerWithStripeWebhook(t, &billingservice.NoOpClient{})
102
103
url := fmt.Sprintf("%s%s", srv.HTTPAddress(), "/webhook")
104
105
for _, scenario := range scenarios {
106
t.Run(scenario.EventType, func(t *testing.T) {
107
payload := payloadForStripeEvent(t, scenario.EventType)
108
109
req, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(payload))
110
require.NoError(t, err)
111
112
req.Header.Set("Stripe-Signature", generateHeader(payload, testWebhookSecret))
113
114
resp, err := http.DefaultClient.Do(req)
115
require.NoError(t, err)
116
117
require.Equal(t, scenario.ExpectedStatusCode, resp.StatusCode)
118
})
119
}
120
}
121
122
// TestWebhookInvokesFinalizeInvoiceRPC ensures that when the webhook is hit with a
123
// `invoice.finalized` event, the `FinalizeInvoice` method on the billing service is invoked
124
// with the invoice id from the event payload.
125
func TestWebhookInvokesFinalizeInvoiceRPC(t *testing.T) {
126
ctrl := gomock.NewController(t)
127
m := mockbillingservice.NewMockInterface(ctrl)
128
m.EXPECT().FinalizeInvoice(gomock.Any(), gomock.Eq("in_1LUQi7GadRXm50o36jWK7ehs"))
129
130
srv := baseServerWithStripeWebhook(t, m)
131
132
url := fmt.Sprintf("%s%s", srv.HTTPAddress(), "/webhook")
133
134
payload := payloadForStripeEvent(t, InvoiceFinalizedEventType)
135
req, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(payload))
136
require.NoError(t, err)
137
138
req.Header.Set("Stripe-Signature", generateHeader(payload, testWebhookSecret))
139
140
resp, err := http.DefaultClient.Do(req)
141
require.NoError(t, err)
142
require.Equal(t, http.StatusOK, resp.StatusCode)
143
}
144
145
func TestWebhookInvokesCancelSubscriptionRPC(t *testing.T) {
146
ctrl := gomock.NewController(t)
147
m := mockbillingservice.NewMockInterface(ctrl)
148
m.EXPECT().CancelSubscription(gomock.Any(), gomock.Eq("in_1LUQi7GadRXm50o36jWK7ehs"))
149
150
srv := baseServerWithStripeWebhook(t, m)
151
152
url := fmt.Sprintf("%s%s", srv.HTTPAddress(), "/webhook")
153
154
payload := payloadForStripeEvent(t, CustomerSubscriptionDeletedEventType)
155
req, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(payload))
156
require.NoError(t, err)
157
158
req.Header.Set("Stripe-Signature", generateHeader(payload, testWebhookSecret))
159
160
resp, err := http.DefaultClient.Do(req)
161
require.NoError(t, err)
162
require.Equal(t, http.StatusOK, resp.StatusCode)
163
}
164
165
func baseServerWithStripeWebhook(t *testing.T, billingService billingservice.Interface) *baseserver.Server {
166
t.Helper()
167
168
srv := baseserver.NewForTests(t,
169
baseserver.WithHTTP(baseserver.MustUseRandomLocalAddress(t)),
170
)
171
baseserver.StartServerForTests(t, srv)
172
173
srv.HTTPMux().Handle("/webhook", NewStripeWebhookHandler(billingService, testWebhookSecret))
174
175
return srv
176
}
177
178
func payloadForStripeEvent(t *testing.T, eventType string) []byte {
179
t.Helper()
180
181
return []byte(`{
182
"data": {
183
"object": {
184
"id": "in_1LUQi7GadRXm50o36jWK7ehs"
185
}
186
},
187
"type": "` + eventType + `"
188
}`)
189
}
190
191
func generateHeader(payload []byte, secret string) string {
192
now := time.Now()
193
signature := webhook.ComputeSignature(now, payload, secret)
194
return fmt.Sprintf("t=%d,%s=%s", now.Unix(), "v1", hex.EncodeToString(signature))
195
}
196
197