Skip to content

Commit d6dcb08

Browse files
committed
test: add webhook controller unit test
1 parent 62eec4c commit d6dcb08

File tree

2 files changed

+393
-146
lines changed

2 files changed

+393
-146
lines changed
Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
// silence some annoying warnings
2+
const originalWarn = console.warn;
3+
console.warn = (...args) => {
4+
const message = args.join(' ');
5+
if (
6+
message.includes('FIREBASE_CONFIG') ||
7+
message.includes('GCLOUD_PROJECT')
8+
) {
9+
return;
10+
}
11+
originalWarn(...args);
12+
};
13+
14+
import { processStripeEvent } from '../../../../src/controllers/webhook';
15+
import * as productHandler from '../../../../src/handlers/product';
16+
import * as priceHandler from '../../../../src/handlers/price';
17+
import * as taxRateHandler from '../../../../src/handlers/tax-rate';
18+
import * as subscriptionHandler from '../../../../src/handlers/subscription';
19+
import * as paymentHandler from '../../../../src/handlers/payment';
20+
import * as invoiceHandler from '../../../../src/handlers/invoice';
21+
import { stripe } from '../../../../src/services';
22+
23+
// Mocks
24+
jest.mock('../../../../src/handlers/product');
25+
jest.mock('../../../../src/handlers/customer');
26+
jest.mock('../../../../src/handlers/price');
27+
jest.mock('../../../../src/handlers/tax-rate');
28+
jest.mock('../../../../src/handlers/subscription');
29+
jest.mock('../../../../src/handlers/payment');
30+
jest.mock('../../../../src/handlers/invoice');
31+
jest.mock('../../../../src/services', () => ({
32+
stripe: {
33+
paymentIntents: {
34+
retrieve: jest.fn(),
35+
},
36+
},
37+
}));
38+
39+
beforeAll(() => {
40+
jest.spyOn(console, 'warn').mockImplementation(() => {});
41+
});
42+
43+
afterEach(() => {
44+
jest.clearAllMocks();
45+
});
46+
47+
describe('processStripeEvent', () => {
48+
const testConfigs = [
49+
{
50+
type: 'product.created',
51+
object: { id: 'prod_123', name: 'Test Product' },
52+
expectedMock: productHandler.createProductRecord,
53+
},
54+
{
55+
type: 'product.updated',
56+
object: { id: 'prod_456', name: 'Updated Product' },
57+
expectedMock: productHandler.createProductRecord,
58+
},
59+
{
60+
type: 'product.deleted',
61+
object: { id: 'prod_789' },
62+
expectedMock: productHandler.deleteProductOrPrice,
63+
},
64+
{
65+
type: 'price.created',
66+
object: { id: 'price_123' },
67+
expectedMock: priceHandler.insertPriceRecord,
68+
},
69+
{
70+
type: 'price.updated',
71+
object: { id: 'price_456' },
72+
expectedMock: priceHandler.insertPriceRecord,
73+
},
74+
{
75+
type: 'price.deleted',
76+
object: { id: 'price_789' },
77+
expectedMock: productHandler.deleteProductOrPrice,
78+
},
79+
{
80+
type: 'tax_rate.created',
81+
object: { id: 'txr_123' },
82+
expectedMock: taxRateHandler.insertTaxRateRecord,
83+
},
84+
{
85+
type: 'tax_rate.updated',
86+
object: { id: 'txr_456' },
87+
expectedMock: taxRateHandler.insertTaxRateRecord,
88+
},
89+
{
90+
type: 'customer.subscription.created',
91+
object: { id: 'sub_123', customer: 'cus_001' },
92+
expectedMock: subscriptionHandler.manageSubscriptionStatusChange,
93+
expectedArgs: ['sub_123', 'cus_001', true],
94+
},
95+
{
96+
type: 'customer.subscription.updated',
97+
object: { id: 'sub_456', customer: 'cus_002' },
98+
expectedMock: subscriptionHandler.manageSubscriptionStatusChange,
99+
expectedArgs: ['sub_456', 'cus_002', false],
100+
},
101+
{
102+
type: 'customer.subscription.deleted',
103+
object: { id: 'sub_789', customer: 'cus_003' },
104+
expectedMock: subscriptionHandler.manageSubscriptionStatusChange,
105+
expectedArgs: ['sub_789', 'cus_003', false],
106+
},
107+
{
108+
type: 'invoice.paid',
109+
object: { id: 'in_123' },
110+
expectedMock: invoiceHandler.insertInvoiceRecord,
111+
},
112+
{
113+
type: 'invoice.payment_succeeded',
114+
object: { id: 'in_456' },
115+
expectedMock: invoiceHandler.insertInvoiceRecord,
116+
},
117+
{
118+
type: 'invoice.payment_failed',
119+
object: { id: 'in_789' },
120+
expectedMock: invoiceHandler.insertInvoiceRecord,
121+
},
122+
{
123+
type: 'invoice.upcoming',
124+
object: { id: 'in_101' },
125+
expectedMock: invoiceHandler.insertInvoiceRecord,
126+
},
127+
{
128+
type: 'invoice.marked_uncollectible',
129+
object: { id: 'in_102' },
130+
expectedMock: invoiceHandler.insertInvoiceRecord,
131+
},
132+
{
133+
type: 'invoice.payment_action_required',
134+
object: { id: 'in_103' },
135+
expectedMock: invoiceHandler.insertInvoiceRecord,
136+
},
137+
{
138+
type: 'payment_intent.processing',
139+
object: { id: 'pi_123' },
140+
expectedMock: paymentHandler.insertPaymentRecord,
141+
},
142+
{
143+
type: 'payment_intent.succeeded',
144+
object: { id: 'pi_456' },
145+
expectedMock: paymentHandler.insertPaymentRecord,
146+
},
147+
{
148+
type: 'payment_intent.canceled',
149+
object: { id: 'pi_789' },
150+
expectedMock: paymentHandler.insertPaymentRecord,
151+
},
152+
{
153+
type: 'payment_intent.payment_failed',
154+
object: { id: 'pi_101' },
155+
expectedMock: paymentHandler.insertPaymentRecord,
156+
},
157+
];
158+
159+
testConfigs.forEach(({ type, object, expectedMock, expectedArgs }) => {
160+
it(`calls ${expectedMock.name} for event type ${type}`, async () => {
161+
const event = {
162+
id: 'evt_test',
163+
type,
164+
data: {
165+
object,
166+
},
167+
};
168+
169+
await processStripeEvent(event as any);
170+
171+
if (expectedArgs) {
172+
expect(expectedMock).toHaveBeenCalledWith(...expectedArgs);
173+
} else {
174+
expect(expectedMock).toHaveBeenCalledWith(object);
175+
}
176+
});
177+
});
178+
179+
it('calls insertPaymentRecord with retrieved intent for checkout.session', async () => {
180+
const mockIntent = { id: 'pi_checkout' };
181+
(stripe.paymentIntents.retrieve as jest.Mock).mockResolvedValue(mockIntent);
182+
183+
const checkoutEvent = {
184+
id: 'evt_checkout',
185+
type: 'checkout.session.completed',
186+
data: {
187+
object: {
188+
mode: 'payment',
189+
payment_intent: 'pi_abc',
190+
customer: 'cus_abc',
191+
tax_id_collection: { enabled: false },
192+
},
193+
},
194+
};
195+
196+
await processStripeEvent(checkoutEvent as any);
197+
198+
expect(stripe.paymentIntents.retrieve).toHaveBeenCalledWith('pi_abc');
199+
expect(paymentHandler.insertPaymentRecord).toHaveBeenCalledWith(
200+
mockIntent,
201+
checkoutEvent.data.object
202+
);
203+
});
204+
205+
it('calls manageSubscriptionStatusChange for checkout.session with subscription', async () => {
206+
const event = {
207+
id: 'evt_checkout_sub',
208+
type: 'checkout.session.completed',
209+
data: {
210+
object: {
211+
mode: 'subscription',
212+
subscription: 'sub_checkout',
213+
customer: 'cus_checkout',
214+
},
215+
},
216+
};
217+
218+
await processStripeEvent(event as any);
219+
220+
expect(
221+
subscriptionHandler.manageSubscriptionStatusChange
222+
).toHaveBeenCalledWith('sub_checkout', 'cus_checkout', true);
223+
});
224+
});

0 commit comments

Comments
 (0)