Skip to content

Commit

Permalink
test(webhook): functional tests for Stripe webhook
Browse files Browse the repository at this point in the history
  • Loading branch information
József Kozma committed Jan 29, 2025
1 parent 9723093 commit 8e4cc80
Show file tree
Hide file tree
Showing 2 changed files with 143 additions and 6 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { LoggerService } from "@akashnetwork/logging";
import Stripe from "stripe";
import { singleton } from "tsyringe";

import { CheckoutSessionRepository } from "@src/billing/repositories";
Expand All @@ -22,27 +21,26 @@ export class StripeWebhookService {
this.logger.info({ event: "STRIPE_EVENT_RECEIVED", type: event.type });

if (event.type === "checkout.session.completed" || event.type === "checkout.session.async_payment_succeeded") {
await this.tryToTopUpWallet(event);
await this.tryToTopUpWallet(event.data.object.id);
}
}

@WithTransaction()
async tryToTopUpWallet(event: Stripe.CheckoutSessionCompletedEvent | Stripe.CheckoutSessionAsyncPaymentSucceededEvent) {
const sessionId = event.data.object.id;
async tryToTopUpWallet(sessionId: string) {
const checkoutSessionCache = await this.checkoutSessionRepository.findOneByAndLock({ sessionId });

if (!checkoutSessionCache) {
this.logger.info({ event: "SESSION_NOT_FOUND", sessionId });
return;
}

const checkoutSession = await this.stripe.checkout.sessions.retrieve(event.data.object.id, {
const checkoutSession = await this.stripe.checkout.sessions.retrieve(sessionId, {
expand: ["line_items"]
});

if (checkoutSession.payment_status !== "unpaid") {
await this.refillService.topUpWallet(checkoutSession.amount_subtotal, checkoutSessionCache.userId);
await this.checkoutSessionRepository.deleteBy({ sessionId: event.data.object.id });
await this.checkoutSessionRepository.deleteBy({ sessionId });
} else {
this.logger.error({ event: "PAYMENT_NOT_COMPLETED", sessionId });
}
Expand Down
139 changes: 139 additions & 0 deletions apps/api/test/functional/stripe-webhook.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import { UserSetting } from "@akashnetwork/database/dbSchemas/user";
import { faker } from "@faker-js/faker";
import crypto from 'crypto';
import { eq } from "drizzle-orm";
import nock from 'nock';
import { container } from "tsyringe";

import { app } from "@src/app";
import { CheckoutSessionRepository } from "@src/billing/repositories";
import { ApiPgDatabase, POSTGRES_DB, resolveTable } from "@src/core";

import { DbTestingService } from "@test/services/db-testing.service";

jest.setTimeout(20000);

describe("Stripe webhook", () => {
const userWalletsTable = resolveTable("UserWallets");
const db = container.resolve<ApiPgDatabase>(POSTGRES_DB);
const userWalletsQuery = db.query.UserWallets;
const checkoutSessionRepository = container.resolve(CheckoutSessionRepository);
const dbService = container.resolve(DbTestingService);

afterEach(async () => {
await dbService.cleanAll();
});

const signPayload = (payload: string, secret: string) => {
const timestamp = Date.now();
const data = `${timestamp}.${payload}`;
const signature = crypto.createHmac('sha256', secret).update(data).digest('hex');
return `t=${timestamp},v1=${signature}`;
};

const generatePayload = (sessionId: string, eventType: string) => JSON.stringify({
data: {
object: {
id: sessionId
}
},
type: eventType
});

const getWebhookResponse = async (sessionId: string, eventType: string) => {
const payload = generatePayload(sessionId, eventType);

return await app.request("/v1/stripe-webhook", {
method: "POST",
body: payload,
headers: new Headers({
"Content-Type": "text/plain",
"Stripe-Signature": signPayload(payload, process.env.STRIPE_WEBHOOK_SECRET),
}),
});
};

describe("POST /v1/stripe-webhook", () => {
['checkout.session.completed', 'checkout.session.async_payment_succeeded'].forEach(eventType => {
it(`tops up wallet and drops session from cache for event ${eventType}`, async () => {
const sessionId = faker.string.uuid();
const userId = faker.string.uuid();
await UserSetting.create({ id: userId });
await checkoutSessionRepository.create({
sessionId,
userId,
});
nock('https://api.stripe.com')
.get(`/v1/checkout/sessions/${sessionId}?expand[0]=line_items`)
.reply(200, {
payment_status: 'paid',
amount_subtotal: 100,
});

const webhookResponse = await getWebhookResponse(sessionId, eventType);

const userWallet = await userWalletsQuery.findFirst({ where: eq(userWalletsTable.userId, userId) });
const checkoutSession = await checkoutSessionRepository.findOneBy({
sessionId
});
expect(webhookResponse.status).toBe(200);
expect(userWallet).toMatchObject({
userId,
deploymentAllowance: `1000000.00`,
isTrialing: false
});
expect(checkoutSession).toBeUndefined();
});
});

it("does not top up wallet and keeps cache if the payment is not done", async () => {
const sessionId = faker.string.uuid();
const userId = faker.string.uuid();
await UserSetting.create({ id: userId });
await checkoutSessionRepository.create({
sessionId,
userId,
});
nock('https://api.stripe.com')
.get(`/v1/checkout/sessions/${sessionId}?expand[0]=line_items`)
.reply(200, {
payment_status: 'unpaid',
amount_subtotal: 100,
});

const webhookResponse = await getWebhookResponse(sessionId, 'checkout.session.completed');

const userWallet = await userWalletsQuery.findFirst({ where: eq(userWalletsTable.userId, userId) });
const checkoutSession = await checkoutSessionRepository.findOneBy({
sessionId
});
expect(webhookResponse.status).toBe(200);
expect(userWallet).toBeUndefined();
expect(checkoutSession).toMatchObject({
sessionId
});
});

it("does not top up wallet and keeps cache if the event is different", async () => {
const sessionId = faker.string.uuid();
const userId = faker.string.uuid();
await UserSetting.create({ id: userId });
await checkoutSessionRepository.create({
sessionId,
userId,
});

const webhookResponse = await getWebhookResponse(sessionId, 'checkout.session.not-found');

const userWallet = await userWalletsQuery.findFirst({ where: eq(userWalletsTable.userId, userId) });
const checkoutSession = await checkoutSessionRepository.findOneBy({
sessionId
});
expect(webhookResponse.status).toBe(200);
expect(userWallet).toBeUndefined();
expect(checkoutSession).toMatchObject({
sessionId
});
});
});
});

0 comments on commit 8e4cc80

Please sign in to comment.