@xenterprises/fastify-xstripe 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,409 @@
1
+ // test/xStripe.integration.test.js
2
+ import { test } from "node:test";
3
+ import assert from "node:assert";
4
+ import crypto from "node:crypto";
5
+ import Fastify from "fastify";
6
+ import xStripe from "../src/index.js";
7
+
8
+ // Mock Stripe webhook signing key
9
+ const mockWebhookSecret = "whsec_mock_key_12345678901234567890";
10
+
11
+ // Helper to create a signed webhook
12
+ function createSignedWebhook(payload, secret) {
13
+ const timestamp = Math.floor(Date.now() / 1000);
14
+ const signedContent = `${timestamp}.${JSON.stringify(payload)}`;
15
+ const signature = crypto
16
+ .createHmac("sha256", secret)
17
+ .update(signedContent)
18
+ .digest("base64");
19
+
20
+ return {
21
+ payload,
22
+ timestamp,
23
+ signature: `t=${timestamp},v1=${signature}`,
24
+ };
25
+ }
26
+
27
+ test("xStripe Plugin Integration", async (t) => {
28
+ await t.test("registers successfully with valid config", async () => {
29
+ const fastify = Fastify({ logger: false });
30
+ const config = {
31
+ apiKey: "sk_test_mock_api_key",
32
+ webhookSecret: mockWebhookSecret,
33
+ webhookPath: "/stripe/webhook",
34
+ };
35
+
36
+ try {
37
+ await fastify.register(xStripe, config);
38
+ assert.ok(fastify.stripe !== undefined, "stripe decorator should exist");
39
+ } finally {
40
+ try {
41
+ await fastify.close();
42
+ } catch {
43
+ // Ignore
44
+ }
45
+ }
46
+ });
47
+
48
+ await t.test("throws error without API key", async () => {
49
+ const fastify = Fastify({ logger: false });
50
+ const invalidConfig = {
51
+ webhookSecret: mockWebhookSecret,
52
+ };
53
+
54
+ try {
55
+ await assert.rejects(
56
+ async () => {
57
+ return fastify.register(xStripe, invalidConfig);
58
+ },
59
+ /Stripe API key is required/
60
+ );
61
+ } finally {
62
+ try {
63
+ await fastify.close();
64
+ } catch {
65
+ // Ignore
66
+ }
67
+ }
68
+ });
69
+
70
+ await t.test("throws error without webhook secret", async () => {
71
+ const fastify = Fastify({ logger: false });
72
+ const invalidConfig = {
73
+ apiKey: "sk_test_mock_api_key",
74
+ };
75
+
76
+ try {
77
+ await assert.rejects(
78
+ async () => {
79
+ return fastify.register(xStripe, invalidConfig);
80
+ },
81
+ /Webhook secret is required/
82
+ );
83
+ } finally {
84
+ try {
85
+ await fastify.close();
86
+ } catch {
87
+ // Ignore
88
+ }
89
+ }
90
+ });
91
+
92
+ await t.test("accepts custom webhook path", async () => {
93
+ const fastify = Fastify({ logger: false });
94
+ const config = {
95
+ apiKey: "sk_test_mock_api_key",
96
+ webhookSecret: mockWebhookSecret,
97
+ webhookPath: "/custom/stripe/webhook",
98
+ };
99
+
100
+ try {
101
+ await fastify.register(xStripe, config);
102
+ assert.ok(true, "should register with custom path");
103
+ } finally {
104
+ try {
105
+ await fastify.close();
106
+ } catch {
107
+ // Ignore
108
+ }
109
+ }
110
+ });
111
+
112
+ await t.test("webhook endpoint is created", async () => {
113
+ const fastify = Fastify({ logger: false });
114
+ const config = {
115
+ apiKey: "sk_test_mock_api_key",
116
+ webhookSecret: mockWebhookSecret,
117
+ webhookPath: "/stripe/webhook",
118
+ };
119
+
120
+ try {
121
+ await fastify.register(xStripe, config);
122
+
123
+ // Try to call the webhook endpoint
124
+ const mockEvent = {
125
+ type: "customer.subscription.created",
126
+ id: "evt_test_123",
127
+ data: {
128
+ object: {
129
+ id: "sub_123",
130
+ customer: "cus_123",
131
+ status: "active",
132
+ },
133
+ },
134
+ };
135
+
136
+ const signed = createSignedWebhook(mockEvent, mockWebhookSecret);
137
+
138
+ const response = await fastify.inject({
139
+ method: "POST",
140
+ url: "/stripe/webhook",
141
+ payload: JSON.stringify(signed.payload),
142
+ headers: {
143
+ "stripe-signature": signed.signature,
144
+ "content-type": "application/json",
145
+ },
146
+ });
147
+
148
+ // Should return 2xx or 3xx (webhook processed or already processed)
149
+ assert.ok(
150
+ response.statusCode >= 200 && response.statusCode < 400,
151
+ `webhook should be processed (got ${response.statusCode})`
152
+ );
153
+ } finally {
154
+ try {
155
+ await fastify.close();
156
+ } catch {
157
+ // Ignore
158
+ }
159
+ }
160
+ });
161
+ });
162
+
163
+ test("xStripe Webhook Validation", async (t) => {
164
+ await t.test("rejects webhook with invalid signature", async () => {
165
+ const fastify = Fastify({ logger: false });
166
+ const config = {
167
+ apiKey: "sk_test_mock_api_key",
168
+ webhookSecret: mockWebhookSecret,
169
+ webhookPath: "/stripe/webhook",
170
+ };
171
+
172
+ try {
173
+ await fastify.register(xStripe, config);
174
+
175
+ const mockEvent = {
176
+ type: "customer.subscription.created",
177
+ data: {
178
+ object: {
179
+ id: "sub_123",
180
+ customer: "cus_123",
181
+ },
182
+ },
183
+ };
184
+
185
+ const response = await fastify.inject({
186
+ method: "POST",
187
+ url: "/stripe/webhook",
188
+ payload: JSON.stringify(mockEvent),
189
+ headers: {
190
+ "stripe-signature": "invalid_signature",
191
+ "content-type": "application/json",
192
+ },
193
+ });
194
+
195
+ // Should reject invalid signature
196
+ assert.equal(response.statusCode, 401, "should reject invalid signature");
197
+ } finally {
198
+ try {
199
+ await fastify.close();
200
+ } catch {
201
+ // Ignore
202
+ }
203
+ }
204
+ });
205
+
206
+ await t.test("requires stripe-signature header", async () => {
207
+ const fastify = Fastify({ logger: false });
208
+ const config = {
209
+ apiKey: "sk_test_mock_api_key",
210
+ webhookSecret: mockWebhookSecret,
211
+ webhookPath: "/stripe/webhook",
212
+ };
213
+
214
+ try {
215
+ await fastify.register(xStripe, config);
216
+
217
+ const mockEvent = {
218
+ type: "customer.subscription.created",
219
+ data: {
220
+ object: {
221
+ id: "sub_123",
222
+ },
223
+ },
224
+ };
225
+
226
+ const response = await fastify.inject({
227
+ method: "POST",
228
+ url: "/stripe/webhook",
229
+ payload: JSON.stringify(mockEvent),
230
+ headers: {
231
+ "content-type": "application/json",
232
+ // No stripe-signature header
233
+ },
234
+ });
235
+
236
+ // Should reject missing signature header
237
+ assert.equal(response.statusCode, 401, "should require signature header");
238
+ } finally {
239
+ try {
240
+ await fastify.close();
241
+ } catch {
242
+ // Ignore
243
+ }
244
+ }
245
+ });
246
+ });
247
+
248
+ test("xStripe Handler Configuration", async (t) => {
249
+ await t.test("accepts custom event handlers", async () => {
250
+ const fastify = Fastify({ logger: false });
251
+
252
+ let handlerCalled = false;
253
+ const customHandler = async (event, fastify) => {
254
+ handlerCalled = true;
255
+ };
256
+
257
+ const config = {
258
+ apiKey: "sk_test_mock_api_key",
259
+ webhookSecret: mockWebhookSecret,
260
+ webhookPath: "/stripe/webhook",
261
+ handlers: {
262
+ "customer.subscription.created": customHandler,
263
+ },
264
+ };
265
+
266
+ try {
267
+ await fastify.register(xStripe, config);
268
+ assert.ok(true, "should register with custom handlers");
269
+ } finally {
270
+ try {
271
+ await fastify.close();
272
+ } catch {
273
+ // Ignore
274
+ }
275
+ }
276
+ });
277
+
278
+ await t.test("can override default handlers", async () => {
279
+ const fastify = Fastify({ logger: false });
280
+ const config = {
281
+ apiKey: "sk_test_mock_api_key",
282
+ webhookSecret: mockWebhookSecret,
283
+ webhookPath: "/stripe/webhook",
284
+ handlers: {
285
+ "invoice.payment_succeeded": async (event, fastify) => {
286
+ // Custom implementation
287
+ },
288
+ },
289
+ };
290
+
291
+ try {
292
+ await fastify.register(xStripe, config);
293
+ assert.ok(true, "should register with overridden handlers");
294
+ } finally {
295
+ try {
296
+ await fastify.close();
297
+ } catch {
298
+ // Ignore
299
+ }
300
+ }
301
+ });
302
+ });
303
+
304
+ test("xStripe Multiple Instances", async (t) => {
305
+ await t.test("can register multiple plugin instances", async () => {
306
+ const fastify1 = Fastify({ logger: false });
307
+ const fastify2 = Fastify({ logger: false });
308
+
309
+ const config = {
310
+ apiKey: "sk_test_mock_api_key",
311
+ webhookSecret: mockWebhookSecret,
312
+ webhookPath: "/stripe/webhook",
313
+ };
314
+
315
+ try {
316
+ await fastify1.register(xStripe, config);
317
+ await fastify2.register(xStripe, config);
318
+
319
+ assert.ok(fastify1.stripe !== undefined);
320
+ assert.ok(fastify2.stripe !== undefined);
321
+ } finally {
322
+ try {
323
+ await fastify1.close();
324
+ await fastify2.close();
325
+ } catch {
326
+ // Ignore
327
+ }
328
+ }
329
+ });
330
+ });
331
+
332
+ test("xStripe Error Handling", async (t) => {
333
+ await t.test("handles malformed JSON payload gracefully", async () => {
334
+ const fastify = Fastify({ logger: false });
335
+ const config = {
336
+ apiKey: "sk_test_mock_api_key",
337
+ webhookSecret: mockWebhookSecret,
338
+ webhookPath: "/stripe/webhook",
339
+ };
340
+
341
+ try {
342
+ await fastify.register(xStripe, config);
343
+
344
+ const signed = createSignedWebhook(
345
+ { type: "test", data: { object: {} } },
346
+ mockWebhookSecret
347
+ );
348
+
349
+ const response = await fastify.inject({
350
+ method: "POST",
351
+ url: "/stripe/webhook",
352
+ payload: "not json",
353
+ headers: {
354
+ "stripe-signature": signed.signature,
355
+ "content-type": "application/json",
356
+ },
357
+ });
358
+
359
+ // Should handle error gracefully
360
+ assert.ok(response.statusCode >= 400, "should handle error response");
361
+ } finally {
362
+ try {
363
+ await fastify.close();
364
+ } catch {
365
+ // Ignore
366
+ }
367
+ }
368
+ });
369
+
370
+ await t.test("handles unknown event types", async () => {
371
+ const fastify = Fastify({ logger: false });
372
+ const config = {
373
+ apiKey: "sk_test_mock_api_key",
374
+ webhookSecret: mockWebhookSecret,
375
+ webhookPath: "/stripe/webhook",
376
+ };
377
+
378
+ try {
379
+ await fastify.register(xStripe, config);
380
+
381
+ const unknownEvent = {
382
+ type: "unknown.event.type",
383
+ id: "evt_test_unknown",
384
+ data: { object: {} },
385
+ };
386
+
387
+ const signed = createSignedWebhook(unknownEvent, mockWebhookSecret);
388
+
389
+ const response = await fastify.inject({
390
+ method: "POST",
391
+ url: "/stripe/webhook",
392
+ payload: JSON.stringify(signed.payload),
393
+ headers: {
394
+ "stripe-signature": signed.signature,
395
+ "content-type": "application/json",
396
+ },
397
+ });
398
+
399
+ // Should handle unknown events (usually 200 ok, just not processed)
400
+ assert.ok(response.statusCode >= 200, "should handle unknown event");
401
+ } finally {
402
+ try {
403
+ await fastify.close();
404
+ } catch {
405
+ // Ignore
406
+ }
407
+ }
408
+ });
409
+ });