@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.
- package/.dockerignore +62 -0
- package/.env.example +116 -0
- package/API.md +574 -0
- package/CHANGELOG.md +96 -0
- package/EXAMPLES.md +883 -0
- package/LICENSE +15 -0
- package/MIGRATION.md +374 -0
- package/QUICK_START.md +179 -0
- package/README.md +331 -0
- package/SECURITY.md +465 -0
- package/TESTING.md +357 -0
- package/index.d.ts +309 -0
- package/package.json +53 -0
- package/server/app.js +557 -0
- package/src/handlers/defaultHandlers.js +355 -0
- package/src/handlers/exampleHandlers.js +278 -0
- package/src/handlers/index.js +8 -0
- package/src/index.js +10 -0
- package/src/utils/helpers.js +220 -0
- package/src/webhooks/webhooks.js +72 -0
- package/src/xStripe.js +45 -0
- package/test/handlers.test.js +959 -0
- package/test/xStripe.integration.test.js +409 -0
|
@@ -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
|
+
});
|