Skip to content

Commit de80d01

Browse files
committed
add stripe webhook unit tests
1 parent 5e1caa8 commit de80d01

File tree

3 files changed

+183
-1
lines changed

3 files changed

+183
-1
lines changed

src/api/routes/stripe.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,7 @@ const stripeRoutes: FastifyPluginAsync = async (fastify, _options) => {
230230
};
231231
const paymentLinkId = event.data.object.payment_link.toString();
232232
if (!paymentLinkId || !paymentCurrency || !paymentAmount) {
233+
request.log.info("Missing required fields.");
233234
return reply
234235
.code(200)
235236
.send({ handled: false, requestId: request.id });
@@ -289,7 +290,7 @@ const stripeRoutes: FastifyPluginAsync = async (fastify, _options) => {
289290
payload: {
290291
to: [unmarshalledEntry.userId],
291292
subject: `Payment Recieved for Invoice ${unmarshalledEntry.invoiceId}`,
292-
content: `Received payment of ${withCurrency} by ${name} (${email}) for invoice ID ${unmarshalledEntry.invoiceId}. Please contact [email protected] with any questions.`,
293+
content: `Received payment of ${withCurrency} by ${name} (${email}) for Invoice ${unmarshalledEntry.invoiceId}. Please contact [email protected] with any questions.`,
293294
},
294295
};
295296
if (!fastify.sqsClient) {

tests/unit/vitest.config.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@ export default defineConfig({
1212
provider: "istanbul",
1313
include: ["src/api/**/*.ts", "src/common/**/*.ts"],
1414
exclude: ["src/api/lambda.ts"],
15+
thresholds: {
16+
statements: 54,
17+
functions: 65,
18+
lines: 54,
19+
},
1520
},
1621
},
1722
resolve: {

tests/unit/webhooks.test.ts

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
import { afterAll, expect, test, beforeEach, vi, describe } from "vitest";
2+
import init from "../../src/api/index.js";
3+
import { mockClient } from "aws-sdk-client-mock";
4+
import { secretObject } from "./secret.testdata.js";
5+
import { DynamoDBClient, QueryCommand } from "@aws-sdk/client-dynamodb";
6+
import supertest from "supertest";
7+
import { v4 as uuidv4 } from "uuid";
8+
import { marshall } from "@aws-sdk/util-dynamodb";
9+
import stripe from "stripe";
10+
import { genericConfig } from "../../src/common/config.js";
11+
import { SendMessageCommand, SQSClient } from "@aws-sdk/client-sqs";
12+
import { Z } from "vitest/dist/chunks/reporters.d.79o4mouw.js";
13+
14+
const ddbMock = mockClient(DynamoDBClient);
15+
const sqsMock = mockClient(SQSClient);
16+
17+
const linkId = uuidv4();
18+
const paymentLinkMock = {
19+
id: linkId,
20+
url: `https://buy.stripe.com/${linkId}`,
21+
};
22+
23+
const app = await init();
24+
describe("Test Stripe webhooks", async () => {
25+
test("Stripe Payment Link skips non-existing links", async () => {
26+
const queueId = uuidv4();
27+
sqsMock.on(SendMessageCommand).rejects();
28+
ddbMock
29+
.on(QueryCommand, {
30+
TableName: genericConfig.StripeLinksDynamoTableName,
31+
IndexName: "LinkIdIndex",
32+
})
33+
.resolvesOnce({
34+
Items: [],
35+
});
36+
const payload = JSON.stringify({
37+
type: "checkout.session.completed",
38+
id: "evt_abc123",
39+
data: {
40+
object: {
41+
payment_link: linkId,
42+
amount_total: 10000,
43+
currency: "usd",
44+
customer_details: {
45+
name: "Test User",
46+
47+
},
48+
},
49+
},
50+
});
51+
await app.ready();
52+
const response = await supertest(app.server)
53+
.post("/api/v1/stripe/webhook")
54+
.set("content-type", "application/json")
55+
.set(
56+
"stripe-signature",
57+
stripe.webhooks.generateTestHeaderString({
58+
payload,
59+
secret: secretObject.stripe_links_endpoint_secret,
60+
}),
61+
)
62+
.send(payload);
63+
expect(response.statusCode).toBe(200);
64+
expect(response.body).toEqual(
65+
expect.objectContaining({
66+
handled: false,
67+
}),
68+
);
69+
});
70+
test("Stripe Payment Link validates webhook signature", async () => {
71+
const queueId = uuidv4();
72+
sqsMock.on(SendMessageCommand).rejects();
73+
ddbMock
74+
.on(QueryCommand, {
75+
TableName: genericConfig.StripeLinksDynamoTableName,
76+
IndexName: "LinkIdIndex",
77+
})
78+
.rejects();
79+
const payload = JSON.stringify({
80+
type: "checkout.session.completed",
81+
id: "evt_abc123",
82+
data: {
83+
object: {
84+
payment_link: linkId,
85+
amount_total: 10000,
86+
currency: "usd",
87+
customer_details: {
88+
name: "Test User",
89+
90+
},
91+
},
92+
},
93+
});
94+
await app.ready();
95+
const response = await supertest(app.server)
96+
.post("/api/v1/stripe/webhook")
97+
.set("content-type", "application/json")
98+
.set(
99+
"stripe-signature",
100+
stripe.webhooks.generateTestHeaderString({ payload, secret: "nah" }),
101+
)
102+
.send(payload);
103+
expect(response.statusCode).toBe(400);
104+
expect(response.body).toStrictEqual({
105+
error: true,
106+
id: 104,
107+
message: "Stripe webhook could not be validated.",
108+
name: "ValidationError",
109+
});
110+
});
111+
test("Stripe Payment Link emails successfully", async () => {
112+
const queueId = uuidv4();
113+
sqsMock.on(SendMessageCommand).resolves({ MessageId: queueId });
114+
ddbMock
115+
.on(QueryCommand, {
116+
TableName: genericConfig.StripeLinksDynamoTableName,
117+
IndexName: "LinkIdIndex",
118+
})
119+
.resolves({
120+
Count: 1,
121+
Items: [
122+
marshall({
123+
linkId,
124+
userId: "[email protected]",
125+
url: paymentLinkMock.url,
126+
active: true,
127+
invoiceId: "ACM102",
128+
amount: 100,
129+
createdAt: "2025-02-09T17:11:30.762Z",
130+
}),
131+
],
132+
});
133+
const payload = JSON.stringify({
134+
type: "checkout.session.completed",
135+
id: "evt_abc123",
136+
data: {
137+
object: {
138+
payment_link: linkId,
139+
amount_total: 10000,
140+
currency: "usd",
141+
customer_details: {
142+
name: "Test User",
143+
144+
},
145+
},
146+
},
147+
});
148+
await app.ready();
149+
const response = await supertest(app.server)
150+
.post("/api/v1/stripe/webhook")
151+
.set("content-type", "application/json")
152+
.set(
153+
"stripe-signature",
154+
stripe.webhooks.generateTestHeaderString({
155+
payload,
156+
secret: secretObject.stripe_links_endpoint_secret,
157+
}),
158+
)
159+
.send(payload);
160+
expect(response.statusCode).toBe(200);
161+
expect(response.body).toEqual(
162+
expect.objectContaining({
163+
handled: true,
164+
queueId,
165+
}),
166+
);
167+
});
168+
afterAll(async () => {
169+
await app.close();
170+
});
171+
beforeEach(() => {
172+
(app as any).nodeCache.flushAll();
173+
ddbMock.reset();
174+
sqsMock.reset();
175+
});
176+
});

0 commit comments

Comments
 (0)