@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,959 @@
1
+ // test/handlers.test.js
2
+ import { test } from 'node:test';
3
+ import assert from 'node:assert';
4
+
5
+ /**
6
+ * Example tests for webhook handlers
7
+ * Run with: node --test test/handlers.test.js
8
+ */
9
+
10
+ // Mock event data
11
+ const mockSubscriptionCreatedEvent = {
12
+ type: 'customer.subscription.created',
13
+ data: {
14
+ object: {
15
+ id: 'sub_123',
16
+ customer: 'cus_123',
17
+ status: 'active',
18
+ current_period_start: 1234567890,
19
+ current_period_end: 1234567890,
20
+ items: {
21
+ data: [{
22
+ price: {
23
+ id: 'price_123',
24
+ product: 'prod_123',
25
+ unit_amount: 2000,
26
+ currency: 'usd',
27
+ },
28
+ quantity: 1,
29
+ }],
30
+ },
31
+ },
32
+ },
33
+ };
34
+
35
+ const mockPaymentFailedEvent = {
36
+ type: 'invoice.payment_failed',
37
+ data: {
38
+ object: {
39
+ id: 'in_123',
40
+ customer: 'cus_123',
41
+ subscription: 'sub_123',
42
+ amount_due: 2000,
43
+ attempt_count: 2,
44
+ currency: 'usd',
45
+ },
46
+ },
47
+ };
48
+
49
+ // Test subscription created handler
50
+ test('subscription.created handler should log correctly', async () => {
51
+ let loggedInfo = null;
52
+
53
+ const mockFastify = {
54
+ log: {
55
+ info: (data) => { loggedInfo = data; },
56
+ error: () => {},
57
+ warn: () => {},
58
+ },
59
+ };
60
+
61
+ const mockStripe = {};
62
+
63
+ // Import and test default handler
64
+ const { defaultHandlers } = await import('../src/handlers/defaultHandlers.js');
65
+ const handler = defaultHandlers['customer.subscription.created'];
66
+
67
+ await handler(mockSubscriptionCreatedEvent, mockFastify, mockStripe);
68
+
69
+ assert.ok(loggedInfo, 'Should have logged info');
70
+ assert.equal(loggedInfo.subscriptionId, 'sub_123');
71
+ assert.equal(loggedInfo.customerId, 'cus_123');
72
+ assert.equal(loggedInfo.status, 'active');
73
+ });
74
+
75
+ // Test custom handler with database update
76
+ test('custom subscription handler should update database', async () => {
77
+ let updatedData = null;
78
+
79
+ const customHandler = async (event, fastify, stripe) => {
80
+ const subscription = event.data.object;
81
+
82
+ await fastify.prisma.user.update({
83
+ where: { stripeCustomerId: subscription.customer },
84
+ data: {
85
+ subscriptionId: subscription.id,
86
+ subscriptionStatus: subscription.status,
87
+ },
88
+ });
89
+ };
90
+
91
+ const mockFastify = {
92
+ log: {
93
+ info: () => {},
94
+ error: () => {},
95
+ warn: () => {},
96
+ },
97
+ prisma: {
98
+ user: {
99
+ update: async (data) => {
100
+ updatedData = data;
101
+ return { id: 1 };
102
+ },
103
+ },
104
+ },
105
+ };
106
+
107
+ const mockStripe = {};
108
+
109
+ await customHandler(mockSubscriptionCreatedEvent, mockFastify, mockStripe);
110
+
111
+ assert.ok(updatedData, 'Should have updated database');
112
+ assert.equal(updatedData.where.stripeCustomerId, 'cus_123');
113
+ assert.equal(updatedData.data.subscriptionId, 'sub_123');
114
+ assert.equal(updatedData.data.subscriptionStatus, 'active');
115
+ });
116
+
117
+ // Test payment failure handler
118
+ test('payment failure handler should send email', async () => {
119
+ let emailSent = null;
120
+
121
+ const customHandler = async (event, fastify, stripe) => {
122
+ const invoice = event.data.object;
123
+ const customer = await stripe.customers.retrieve(invoice.customer);
124
+
125
+ await fastify.email.send(
126
+ customer.email,
127
+ 'Payment Failed',
128
+ '<p>Please update your payment method.</p>'
129
+ );
130
+ };
131
+
132
+ const mockFastify = {
133
+ log: {
134
+ info: () => {},
135
+ error: () => {},
136
+ warn: () => {},
137
+ },
138
+ email: {
139
+ send: async (to, subject, body) => {
140
+ emailSent = { to, subject, body };
141
+ return { success: true };
142
+ },
143
+ },
144
+ };
145
+
146
+ const mockStripe = {
147
+ customers: {
148
+ retrieve: async (id) => ({
149
+ id,
150
+ email: 'test@example.com',
151
+ }),
152
+ },
153
+ };
154
+
155
+ await customHandler(mockPaymentFailedEvent, mockFastify, mockStripe);
156
+
157
+ assert.ok(emailSent, 'Should have sent email');
158
+ assert.equal(emailSent.to, 'test@example.com');
159
+ assert.equal(emailSent.subject, 'Payment Failed');
160
+ });
161
+
162
+ // Test error handling
163
+ test('handler errors should not throw', async () => {
164
+ const failingHandler = async (event, fastify, stripe) => {
165
+ throw new Error('Database connection failed');
166
+ };
167
+
168
+ const mockFastify = {
169
+ log: {
170
+ info: () => {},
171
+ error: () => {},
172
+ warn: () => {},
173
+ },
174
+ };
175
+
176
+ const mockStripe = {};
177
+
178
+ // Should not throw - errors are caught by webhook handler
179
+ try {
180
+ await failingHandler(mockSubscriptionCreatedEvent, mockFastify, mockStripe);
181
+ assert.fail('Should have thrown an error');
182
+ } catch (error) {
183
+ assert.equal(error.message, 'Database connection failed');
184
+ }
185
+ });
186
+
187
+ // Test helper functions
188
+ test('helper functions work correctly', async () => {
189
+ const { formatAmount, getPlanName, isActiveSubscription } = await import('../src/utils/helpers.js');
190
+
191
+ // Test formatAmount
192
+ const formatted = formatAmount(2000, 'USD');
193
+ assert.equal(formatted, '$20.00');
194
+
195
+ // Test getPlanName
196
+ const subscription = mockSubscriptionCreatedEvent.data.object;
197
+ const planName = getPlanName(subscription);
198
+ assert.ok(planName);
199
+
200
+ // Test isActiveSubscription
201
+ assert.equal(isActiveSubscription(subscription), true);
202
+
203
+ const canceledSub = { ...subscription, status: 'canceled' };
204
+ assert.equal(isActiveSubscription(canceledSub), false);
205
+ });
206
+
207
+ // Test idempotency
208
+ test('handlers should be idempotent', async () => {
209
+ let callCount = 0;
210
+
211
+ const idempotentHandler = async (event, fastify, stripe) => {
212
+ const subscription = event.data.object;
213
+
214
+ // Upsert pattern - safe to call multiple times
215
+ await fastify.prisma.user.upsert({
216
+ where: { stripeCustomerId: subscription.customer },
217
+ update: { subscriptionStatus: subscription.status },
218
+ create: {
219
+ stripeCustomerId: subscription.customer,
220
+ subscriptionStatus: subscription.status,
221
+ },
222
+ });
223
+
224
+ callCount++;
225
+ };
226
+
227
+ const mockFastify = {
228
+ log: {
229
+ info: () => {},
230
+ error: () => {},
231
+ warn: () => {},
232
+ },
233
+ prisma: {
234
+ user: {
235
+ upsert: async () => ({ id: 1 }),
236
+ },
237
+ },
238
+ };
239
+
240
+ const mockStripe = {};
241
+
242
+ // Call handler multiple times
243
+ await idempotentHandler(mockSubscriptionCreatedEvent, mockFastify, mockStripe);
244
+ await idempotentHandler(mockSubscriptionCreatedEvent, mockFastify, mockStripe);
245
+ await idempotentHandler(mockSubscriptionCreatedEvent, mockFastify, mockStripe);
246
+
247
+ assert.equal(callCount, 3, 'Should have been called 3 times');
248
+ // In real scenario, database state should be same after each call
249
+ });
250
+
251
+ // ============================================================================
252
+ // COMPREHENSIVE EVENT HANDLER TESTS
253
+ // ============================================================================
254
+
255
+ test('customer.updated handler should process customer updates', async () => {
256
+ const mockEvent = {
257
+ type: 'customer.updated',
258
+ data: {
259
+ object: {
260
+ id: 'cus_123',
261
+ email: 'updated@example.com',
262
+ name: 'John Updated',
263
+ metadata: { userId: '456' },
264
+ },
265
+ },
266
+ };
267
+
268
+ let loggedData = null;
269
+
270
+ const mockFastify = {
271
+ log: {
272
+ info: (data) => { loggedData = data; },
273
+ error: () => {},
274
+ warn: () => {},
275
+ },
276
+ };
277
+
278
+ const { defaultHandlers } = await import('../src/handlers/defaultHandlers.js');
279
+ const handler = defaultHandlers['customer.updated'];
280
+
281
+ await handler(mockEvent, mockFastify, {});
282
+
283
+ assert.ok(loggedData);
284
+ assert.equal(loggedData.customerId, 'cus_123');
285
+ });
286
+
287
+ test('customer.deleted handler should process customer deletion', async () => {
288
+ const mockEvent = {
289
+ type: 'customer.deleted',
290
+ data: {
291
+ object: {
292
+ id: 'cus_123',
293
+ email: 'deleted@example.com',
294
+ },
295
+ },
296
+ };
297
+
298
+ let loggedData = null;
299
+
300
+ const mockFastify = {
301
+ log: {
302
+ info: (data) => { loggedData = data; },
303
+ error: () => {},
304
+ warn: () => {},
305
+ },
306
+ };
307
+
308
+ const { defaultHandlers } = await import('../src/handlers/defaultHandlers.js');
309
+ const handler = defaultHandlers['customer.deleted'];
310
+
311
+ await handler(mockEvent, mockFastify, {});
312
+
313
+ assert.ok(loggedData);
314
+ assert.equal(loggedData.customerId, 'cus_123');
315
+ });
316
+
317
+ test('invoice.created handler should process invoice creation', async () => {
318
+ const mockEvent = {
319
+ type: 'invoice.created',
320
+ data: {
321
+ object: {
322
+ id: 'in_123',
323
+ customer: 'cus_123',
324
+ subscription: 'sub_123',
325
+ amount_due: 5000,
326
+ currency: 'usd',
327
+ status: 'draft',
328
+ },
329
+ },
330
+ };
331
+
332
+ let loggedData = null;
333
+
334
+ const mockFastify = {
335
+ log: {
336
+ info: (data) => { loggedData = data; },
337
+ error: () => {},
338
+ warn: () => {},
339
+ },
340
+ };
341
+
342
+ const { defaultHandlers } = await import('../src/handlers/defaultHandlers.js');
343
+ const handler = defaultHandlers['invoice.created'];
344
+
345
+ await handler(mockEvent, mockFastify, {});
346
+
347
+ assert.ok(loggedData);
348
+ assert.equal(loggedData.invoiceId, 'in_123');
349
+ });
350
+
351
+ test('invoice.finalized handler should process invoice finalization', async () => {
352
+ const mockEvent = {
353
+ type: 'invoice.finalized',
354
+ data: {
355
+ object: {
356
+ id: 'in_123',
357
+ customer: 'cus_123',
358
+ amount_due: 5000,
359
+ status: 'open',
360
+ hosted_invoice_url: 'https://stripe.com/invoice',
361
+ },
362
+ },
363
+ };
364
+
365
+ let loggedData = null;
366
+
367
+ const mockFastify = {
368
+ log: {
369
+ info: (data) => { loggedData = data; },
370
+ error: () => {},
371
+ warn: () => {},
372
+ },
373
+ };
374
+
375
+ const { defaultHandlers } = await import('../src/handlers/defaultHandlers.js');
376
+ const handler = defaultHandlers['invoice.finalized'];
377
+
378
+ await handler(mockEvent, mockFastify, {});
379
+
380
+ assert.ok(loggedData);
381
+ assert.equal(loggedData.invoiceId, 'in_123');
382
+ });
383
+
384
+ test('invoice.paid handler should process successful invoice payments', async () => {
385
+ const mockEvent = {
386
+ type: 'invoice.paid',
387
+ data: {
388
+ object: {
389
+ id: 'in_123',
390
+ customer: 'cus_123',
391
+ amount_paid: 5000,
392
+ paid: true,
393
+ status: 'paid',
394
+ },
395
+ },
396
+ };
397
+
398
+ let loggedData = null;
399
+
400
+ const mockFastify = {
401
+ log: {
402
+ info: (data) => { loggedData = data; },
403
+ error: () => {},
404
+ warn: () => {},
405
+ },
406
+ };
407
+
408
+ const { defaultHandlers } = await import('../src/handlers/defaultHandlers.js');
409
+ const handler = defaultHandlers['invoice.paid'];
410
+
411
+ await handler(mockEvent, mockFastify, {});
412
+
413
+ assert.ok(loggedData);
414
+ assert.equal(loggedData.invoiceId, 'in_123');
415
+ });
416
+
417
+ test('charge.succeeded handler should process successful charges', async () => {
418
+ const mockEvent = {
419
+ type: 'charge.succeeded',
420
+ data: {
421
+ object: {
422
+ id: 'ch_123',
423
+ customer: 'cus_123',
424
+ amount: 5000,
425
+ currency: 'usd',
426
+ status: 'succeeded',
427
+ payment_method_details: { type: 'card' },
428
+ },
429
+ },
430
+ };
431
+
432
+ let loggedData = null;
433
+
434
+ const mockFastify = {
435
+ log: {
436
+ info: (data) => { loggedData = data; },
437
+ error: () => {},
438
+ warn: () => {},
439
+ },
440
+ };
441
+
442
+ const { defaultHandlers } = await import('../src/handlers/defaultHandlers.js');
443
+ const handler = defaultHandlers['charge.succeeded'];
444
+
445
+ await handler(mockEvent, mockFastify, {});
446
+
447
+ assert.ok(loggedData);
448
+ assert.equal(loggedData.chargeId, 'ch_123');
449
+ });
450
+
451
+ test('charge.failed handler should process failed charges', async () => {
452
+ const mockEvent = {
453
+ type: 'charge.failed',
454
+ data: {
455
+ object: {
456
+ id: 'ch_123',
457
+ customer: 'cus_123',
458
+ amount: 5000,
459
+ currency: 'usd',
460
+ failure_code: 'card_declined',
461
+ failure_message: 'Your card was declined',
462
+ },
463
+ },
464
+ };
465
+
466
+ let loggedData = null;
467
+
468
+ const mockFastify = {
469
+ log: {
470
+ info: () => {},
471
+ error: (data) => { loggedData = data; },
472
+ warn: () => {},
473
+ },
474
+ };
475
+
476
+ const { defaultHandlers } = await import('../src/handlers/defaultHandlers.js');
477
+ const handler = defaultHandlers['charge.failed'];
478
+
479
+ await handler(mockEvent, mockFastify, {});
480
+
481
+ assert.ok(loggedData);
482
+ assert.equal(loggedData.chargeId, 'ch_123');
483
+ });
484
+
485
+ test('charge.refunded handler should process charge refunds', async () => {
486
+ const mockEvent = {
487
+ type: 'charge.refunded',
488
+ data: {
489
+ object: {
490
+ id: 'ch_123',
491
+ customer: 'cus_123',
492
+ amount_refunded: 5000,
493
+ refunded: true,
494
+ refunds: {
495
+ data: [{ id: 're_123', amount: 5000 }],
496
+ },
497
+ },
498
+ },
499
+ };
500
+
501
+ let loggedData = null;
502
+
503
+ const mockFastify = {
504
+ log: {
505
+ info: (data) => { loggedData = data; },
506
+ error: () => {},
507
+ warn: () => {},
508
+ },
509
+ };
510
+
511
+ const { defaultHandlers } = await import('../src/handlers/defaultHandlers.js');
512
+ const handler = defaultHandlers['charge.refunded'];
513
+
514
+ await handler(mockEvent, mockFastify, {});
515
+
516
+ assert.ok(loggedData);
517
+ assert.equal(loggedData.chargeId, 'ch_123');
518
+ });
519
+
520
+ test('payment_method.attached handler should process payment method attachments', async () => {
521
+ const mockEvent = {
522
+ type: 'payment_method.attached',
523
+ data: {
524
+ object: {
525
+ id: 'pm_123',
526
+ customer: 'cus_123',
527
+ type: 'card',
528
+ card: {
529
+ brand: 'visa',
530
+ last4: '4242',
531
+ exp_month: 12,
532
+ exp_year: 2025,
533
+ },
534
+ },
535
+ },
536
+ };
537
+
538
+ let loggedData = null;
539
+
540
+ const mockFastify = {
541
+ log: {
542
+ info: (data) => { loggedData = data; },
543
+ error: () => {},
544
+ warn: () => {},
545
+ },
546
+ };
547
+
548
+ const { defaultHandlers } = await import('../src/handlers/defaultHandlers.js');
549
+ const handler = defaultHandlers['payment_method.attached'];
550
+
551
+ await handler(mockEvent, mockFastify, {});
552
+
553
+ assert.ok(loggedData);
554
+ assert.equal(loggedData.paymentMethodId, 'pm_123');
555
+ });
556
+
557
+ test('payment_method.detached handler should process payment method detachments', async () => {
558
+ const mockEvent = {
559
+ type: 'payment_method.detached',
560
+ data: {
561
+ object: {
562
+ id: 'pm_123',
563
+ customer: null,
564
+ type: 'card',
565
+ },
566
+ },
567
+ };
568
+
569
+ let loggedData = null;
570
+
571
+ const mockFastify = {
572
+ log: {
573
+ info: (data) => { loggedData = data; },
574
+ error: () => {},
575
+ warn: () => {},
576
+ },
577
+ };
578
+
579
+ const { defaultHandlers } = await import('../src/handlers/defaultHandlers.js');
580
+ const handler = defaultHandlers['payment_method.detached'];
581
+
582
+ await handler(mockEvent, mockFastify, {});
583
+
584
+ assert.ok(loggedData);
585
+ assert.equal(loggedData.paymentMethodId, 'pm_123');
586
+ });
587
+
588
+ // ============================================================================
589
+ // SUBSCRIPTION LIFECYCLE TESTS
590
+ // ============================================================================
591
+
592
+ test('subscription.updated handler should process subscription changes', async () => {
593
+ const mockEvent = {
594
+ type: 'customer.subscription.updated',
595
+ data: {
596
+ object: {
597
+ id: 'sub_123',
598
+ customer: 'cus_123',
599
+ status: 'active',
600
+ current_period_end: 1234567890,
601
+ },
602
+ previous_attributes: {
603
+ status: 'trialing',
604
+ },
605
+ },
606
+ };
607
+
608
+ let loggedData = null;
609
+
610
+ const mockFastify = {
611
+ log: {
612
+ info: (data) => { loggedData = data; },
613
+ error: () => {},
614
+ warn: () => {},
615
+ },
616
+ };
617
+
618
+ const { defaultHandlers } = await import('../src/handlers/defaultHandlers.js');
619
+ const handler = defaultHandlers['customer.subscription.updated'];
620
+
621
+ await handler(mockEvent, mockFastify, {});
622
+
623
+ assert.ok(loggedData);
624
+ assert.equal(loggedData.subscriptionId, 'sub_123');
625
+ });
626
+
627
+ test('subscription.deleted handler should process subscription cancellations', async () => {
628
+ const mockEvent = {
629
+ type: 'customer.subscription.deleted',
630
+ data: {
631
+ object: {
632
+ id: 'sub_123',
633
+ customer: 'cus_123',
634
+ status: 'canceled',
635
+ canceled_at: 1234567890,
636
+ },
637
+ },
638
+ };
639
+
640
+ let loggedData = null;
641
+
642
+ const mockFastify = {
643
+ log: {
644
+ info: (data) => { loggedData = data; },
645
+ error: () => {},
646
+ warn: () => {},
647
+ },
648
+ };
649
+
650
+ const { defaultHandlers } = await import('../src/handlers/defaultHandlers.js');
651
+ const handler = defaultHandlers['customer.subscription.deleted'];
652
+
653
+ await handler(mockEvent, mockFastify, {});
654
+
655
+ assert.ok(loggedData);
656
+ assert.equal(loggedData.subscriptionId, 'sub_123');
657
+ });
658
+
659
+ test('subscription.trial_will_end handler should process upcoming trial endings', async () => {
660
+ const mockEvent = {
661
+ type: 'customer.subscription.trial_will_end',
662
+ data: {
663
+ object: {
664
+ id: 'sub_123',
665
+ customer: 'cus_123',
666
+ trial_end: 1234567890,
667
+ status: 'trialing',
668
+ },
669
+ },
670
+ };
671
+
672
+ let loggedData = null;
673
+
674
+ const mockFastify = {
675
+ log: {
676
+ info: (data) => { loggedData = data; },
677
+ error: () => {},
678
+ warn: () => {},
679
+ },
680
+ };
681
+
682
+ const { defaultHandlers } = await import('../src/handlers/defaultHandlers.js');
683
+ const handler = defaultHandlers['customer.subscription.trial_will_end'];
684
+
685
+ await handler(mockEvent, mockFastify, {});
686
+
687
+ assert.ok(loggedData);
688
+ assert.equal(loggedData.subscriptionId, 'sub_123');
689
+ });
690
+
691
+ // ============================================================================
692
+ // HANDLER REGISTRY AND CUSTOMIZATION TESTS
693
+ // ============================================================================
694
+
695
+ test('can override default handlers', async () => {
696
+ let customHandlerCalled = false;
697
+
698
+ const customHandler = async () => {
699
+ customHandlerCalled = true;
700
+ };
701
+
702
+ const mockFastify = {
703
+ log: {
704
+ info: () => {},
705
+ error: () => {},
706
+ warn: () => {},
707
+ },
708
+ };
709
+
710
+ // Store original handler, replace with custom
711
+ const { defaultHandlers } = await import('../src/handlers/defaultHandlers.js');
712
+ const originalHandler = defaultHandlers['customer.subscription.created'];
713
+
714
+ defaultHandlers['customer.subscription.created'] = customHandler;
715
+
716
+ // Call custom handler
717
+ await defaultHandlers['customer.subscription.created'](mockSubscriptionCreatedEvent, mockFastify, {});
718
+
719
+ assert.ok(customHandlerCalled, 'Custom handler should have been called');
720
+
721
+ // Restore original handler
722
+ defaultHandlers['customer.subscription.created'] = originalHandler;
723
+ });
724
+
725
+ test('unknown event types can be handled with fallback', async () => {
726
+ const unknownEvent = {
727
+ type: 'unknown.event.type',
728
+ data: { object: { id: 'test' } },
729
+ };
730
+
731
+ let loggedInfo = null;
732
+
733
+ const mockFastify = {
734
+ log: {
735
+ info: () => {},
736
+ error: () => {},
737
+ warn: (info) => { loggedInfo = info; },
738
+ },
739
+ };
740
+
741
+ const fallbackHandler = async (event, fastify) => {
742
+ fastify.log.warn(`Unhandled event type: ${event.type}`);
743
+ };
744
+
745
+ await fallbackHandler(unknownEvent, mockFastify);
746
+
747
+ assert.ok(loggedInfo, 'Should have warned about unknown event');
748
+ });
749
+
750
+ test('handler receives correct Stripe instance', async () => {
751
+ let stripeInstance = null;
752
+
753
+ const mockStripe = {
754
+ customers: { retrieve: async () => ({ id: 'cus_123' }) },
755
+ subscriptions: { retrieve: async () => ({ id: 'sub_123' }) },
756
+ };
757
+
758
+ const testHandler = async (event, fastify, stripe) => {
759
+ stripeInstance = stripe;
760
+ };
761
+
762
+ const mockFastify = {
763
+ log: { info: () => {}, error: () => {}, warn: () => {} },
764
+ };
765
+
766
+ await testHandler(mockSubscriptionCreatedEvent, mockFastify, mockStripe);
767
+
768
+ assert.strictEqual(stripeInstance, mockStripe);
769
+ assert.ok(stripeInstance.customers);
770
+ assert.ok(stripeInstance.subscriptions);
771
+ });
772
+
773
+ // ============================================================================
774
+ // EVENT DATA VALIDATION TESTS
775
+ // ============================================================================
776
+
777
+ test('handler processes events with complex nested data', async () => {
778
+ const complexEvent = {
779
+ type: 'customer.subscription.created',
780
+ data: {
781
+ object: {
782
+ id: 'sub_complex',
783
+ customer: 'cus_complex',
784
+ status: 'active',
785
+ items: {
786
+ data: [
787
+ {
788
+ id: 'si_1',
789
+ price: {
790
+ id: 'price_1',
791
+ product: 'prod_1',
792
+ unit_amount: 1000,
793
+ currency: 'usd',
794
+ recurring: { interval: 'month', interval_count: 1 },
795
+ },
796
+ quantity: 1,
797
+ },
798
+ {
799
+ id: 'si_2',
800
+ price: {
801
+ id: 'price_2',
802
+ product: 'prod_2',
803
+ unit_amount: 2000,
804
+ currency: 'usd',
805
+ recurring: { interval: 'year', interval_count: 1 },
806
+ },
807
+ quantity: 2,
808
+ },
809
+ ],
810
+ },
811
+ billing_cycle_anchor: 1234567890,
812
+ current_period_start: 1234567890,
813
+ current_period_end: 1234567999,
814
+ },
815
+ },
816
+ };
817
+
818
+ let loggedData = null;
819
+
820
+ const mockFastify = {
821
+ log: {
822
+ info: (data) => { loggedData = data; },
823
+ error: () => {},
824
+ warn: () => {},
825
+ },
826
+ };
827
+
828
+ const { defaultHandlers } = await import('../src/handlers/defaultHandlers.js');
829
+ const handler = defaultHandlers['customer.subscription.created'];
830
+
831
+ await handler(complexEvent, mockFastify, {});
832
+
833
+ assert.ok(loggedData);
834
+ assert.equal(loggedData.subscriptionId, 'sub_complex');
835
+ });
836
+
837
+ test('handler tolerates missing optional fields', async () => {
838
+ const minimalEvent = {
839
+ type: 'customer.subscription.created',
840
+ data: {
841
+ object: {
842
+ id: 'sub_minimal',
843
+ customer: 'cus_minimal',
844
+ status: 'active',
845
+ items: { data: [{ price: { id: 'price_minimal' } }] },
846
+ },
847
+ },
848
+ };
849
+
850
+ let errorThrown = false;
851
+
852
+ const mockFastify = {
853
+ log: {
854
+ info: () => {},
855
+ error: () => {},
856
+ warn: () => {},
857
+ },
858
+ };
859
+
860
+ const { defaultHandlers } = await import('../src/handlers/defaultHandlers.js');
861
+ const handler = defaultHandlers['customer.subscription.created'];
862
+
863
+ try {
864
+ await handler(minimalEvent, mockFastify, {});
865
+ } catch (error) {
866
+ errorThrown = true;
867
+ }
868
+
869
+ assert.equal(errorThrown, false, 'Should handle minimal event data');
870
+ });
871
+
872
+ // ============================================================================
873
+ // CONCURRENCY AND PERFORMANCE TESTS
874
+ // ============================================================================
875
+
876
+ test('handlers can execute concurrently', async () => {
877
+ let executionCount = 0;
878
+
879
+ const concurrentHandler = async (event, fastify, stripe) => {
880
+ await new Promise(resolve => setTimeout(resolve, 10));
881
+ executionCount++;
882
+ };
883
+
884
+ const mockFastify = {
885
+ log: { info: () => {}, error: () => {}, warn: () => {} },
886
+ };
887
+
888
+ const mockStripe = {};
889
+
890
+ // Execute 5 handlers concurrently
891
+ const promises = Array(5)
892
+ .fill(null)
893
+ .map(() => concurrentHandler(mockSubscriptionCreatedEvent, mockFastify, mockStripe));
894
+
895
+ await Promise.all(promises);
896
+
897
+ assert.equal(executionCount, 5, 'All 5 concurrent handlers should execute');
898
+ });
899
+
900
+ test('multiple event types can be handled in sequence', async () => {
901
+ const mockSubscriptionEvent = {
902
+ type: 'customer.subscription.created',
903
+ data: {
904
+ object: {
905
+ id: 'sub_123',
906
+ customer: 'cus_123',
907
+ status: 'active',
908
+ items: {
909
+ data: [{
910
+ price: {
911
+ id: 'price_123',
912
+ product: 'prod_123',
913
+ unit_amount: 2000,
914
+ currency: 'usd',
915
+ },
916
+ quantity: 1,
917
+ }],
918
+ },
919
+ },
920
+ },
921
+ };
922
+
923
+ const mockChargeSucceededEvent = {
924
+ type: 'charge.succeeded',
925
+ data: {
926
+ object: {
927
+ id: 'ch_123',
928
+ customer: 'cus_123',
929
+ amount: 5000,
930
+ currency: 'usd',
931
+ status: 'succeeded',
932
+ payment_method_details: { type: 'card' },
933
+ },
934
+ },
935
+ };
936
+
937
+ const results = [];
938
+
939
+ const mockFastify = {
940
+ log: {
941
+ info: (data) => { results.push(data); },
942
+ error: () => {},
943
+ warn: (data) => { results.push(data); },
944
+ },
945
+ };
946
+
947
+ const { defaultHandlers } = await import('../src/handlers/defaultHandlers.js');
948
+
949
+ const events = [mockSubscriptionEvent, mockChargeSucceededEvent];
950
+
951
+ for (const event of events) {
952
+ const handler = defaultHandlers[event.type];
953
+ if (handler) {
954
+ await handler(event, mockFastify, {});
955
+ }
956
+ }
957
+
958
+ assert.equal(results.length, 2, 'Should have processed 2 events');
959
+ });