Files
ecommerce-platform/test/integration/api/webhooks/stripe.test.ts
T
davide 6a5d5a6119 test: add integration tests for API routes (auth, Stripe webhook)
27 tests covering POST /api/auth/login (9), POST /api/auth/register (9), and
POST /api/webhooks/stripe (11). Routes are tested by importing handlers directly
as functions, no HTTP server needed. Stripe false-positive fixed: thrown error
message now differs from the hardcoded 400 response to verify sanitization.
2026-05-19 14:07:53 +02:00

167 lines
6.0 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from 'vitest'
import '../../../__mocks__/prisma'
import { NextRequest } from 'next/server'
import { POST } from '@/app/api/webhooks/stripe/route'
import { prisma } from '@/lib/prisma'
import {
mockOrder,
mockPayment,
mockStripeCheckoutEvent,
mockStripePaymentSucceededEvent,
mockStripePaymentFailedEvent,
} from '../../../fixtures/orders'
vi.mock('@/lib/stripe', () => ({
constructWebhookEvent: vi.fn(),
}))
vi.mock('@/lib/email', () => ({
sendOrderConfirmationEmail: vi.fn().mockResolvedValue(undefined),
}))
import { constructWebhookEvent } from '@/lib/stripe'
import { sendOrderConfirmationEmail } from '@/lib/email'
function makeRequest(body: string, signature = 'valid-sig') {
return new NextRequest(
new Request('http://localhost/api/webhooks/stripe', {
method: 'POST',
headers: { 'stripe-signature': signature, 'Content-Type': 'text/plain' },
body,
})
)
}
beforeEach(() => {
vi.clearAllMocks()
vi.mocked(prisma.order.update).mockResolvedValue(mockOrder as any)
vi.mocked(prisma.payment.updateMany).mockResolvedValue({ count: 1 })
vi.mocked(prisma.payment.update).mockResolvedValue(mockPayment as any)
vi.mocked(prisma.payment.findFirst).mockResolvedValue(mockPayment as any)
vi.mocked(prisma.order.findUnique).mockResolvedValue({ ...mockOrder, user: mockOrder.user } as any)
})
describe('POST /api/webhooks/stripe', () => {
it('returns 400 when stripe-signature header is missing', async () => {
const req = new NextRequest(
new Request('http://localhost/api/webhooks/stripe', {
method: 'POST',
headers: { 'Content-Type': 'text/plain' },
body: '{}',
})
)
const res = await POST(req)
expect(res.status).toBe(400)
expect((await res.json()).error).toMatch(/signature/)
})
it('returns 400 when signature verification fails', async () => {
vi.mocked(constructWebhookEvent).mockImplementation(() => {
throw new Error('stripe_signature_mismatch')
})
const res = await POST(makeRequest('{}', 'bad-sig'))
expect(res.status).toBe(400)
expect((await res.json()).error).toBe('Invalid signature')
})
it('processes checkout.session.completed: updates order to PAID', async () => {
vi.mocked(constructWebhookEvent).mockReturnValue(mockStripeCheckoutEvent as any)
const res = await POST(makeRequest(JSON.stringify(mockStripeCheckoutEvent)))
expect(res.status).toBe(200)
expect(prisma.order.update).toHaveBeenCalledWith(
expect.objectContaining({ where: { id: mockOrder.id }, data: { status: 'PAID' } })
)
})
it('processes checkout.session.completed: updates payment record', async () => {
vi.mocked(constructWebhookEvent).mockReturnValue(mockStripeCheckoutEvent as any)
await POST(makeRequest(JSON.stringify(mockStripeCheckoutEvent)))
expect(prisma.payment.updateMany).toHaveBeenCalledWith(
expect.objectContaining({
where: { orderId: mockOrder.id },
data: expect.objectContaining({ status: 'paid' }),
})
)
})
it('processes checkout.session.completed: sends confirmation email', async () => {
vi.mocked(constructWebhookEvent).mockReturnValue(mockStripeCheckoutEvent as any)
await POST(makeRequest(JSON.stringify(mockStripeCheckoutEvent)))
expect(sendOrderConfirmationEmail).toHaveBeenCalledWith(
mockOrder.user.email,
expect.objectContaining({ orderId: mockOrder.id })
)
})
it('skips email send if order user has no email', async () => {
vi.mocked(constructWebhookEvent).mockReturnValue(mockStripeCheckoutEvent as any)
vi.mocked(prisma.order.findUnique).mockResolvedValue({ ...mockOrder, user: null } as any)
await POST(makeRequest(JSON.stringify(mockStripeCheckoutEvent)))
expect(sendOrderConfirmationEmail).not.toHaveBeenCalled()
})
it('processes checkout.session.completed without orderId: no DB update', async () => {
const eventWithoutOrderId = {
type: 'checkout.session.completed',
data: { object: { metadata: {}, payment_intent: 'pi_test' } },
}
vi.mocked(constructWebhookEvent).mockReturnValue(eventWithoutOrderId as any)
const res = await POST(makeRequest('{}'))
expect(res.status).toBe(200)
expect(prisma.order.update).not.toHaveBeenCalled()
})
it('processes payment_intent.succeeded: updates payment and order', async () => {
vi.mocked(constructWebhookEvent).mockReturnValue(mockStripePaymentSucceededEvent as any)
const res = await POST(makeRequest(JSON.stringify(mockStripePaymentSucceededEvent)))
expect(res.status).toBe(200)
expect(prisma.payment.update).toHaveBeenCalledWith(
expect.objectContaining({ data: expect.objectContaining({ status: 'paid' }) })
)
expect(prisma.order.update).toHaveBeenCalledWith(
expect.objectContaining({ data: { status: 'PAID' } })
)
})
it('processes payment_intent.succeeded: no-op when payment not found', async () => {
vi.mocked(constructWebhookEvent).mockReturnValue(mockStripePaymentSucceededEvent as any)
vi.mocked(prisma.payment.findFirst).mockResolvedValue(null)
const res = await POST(makeRequest('{}'))
expect(res.status).toBe(200)
expect(prisma.payment.update).not.toHaveBeenCalled()
})
it('processes payment_intent.payment_failed: sets status to failed and CANCELLED', async () => {
vi.mocked(constructWebhookEvent).mockReturnValue(mockStripePaymentFailedEvent as any)
const res = await POST(makeRequest(JSON.stringify(mockStripePaymentFailedEvent)))
expect(res.status).toBe(200)
expect(prisma.payment.update).toHaveBeenCalledWith(
expect.objectContaining({ data: expect.objectContaining({ status: 'failed' }) })
)
expect(prisma.order.update).toHaveBeenCalledWith(
expect.objectContaining({ data: { status: 'CANCELLED' } })
)
})
it('returns 200 for unhandled event types', async () => {
vi.mocked(constructWebhookEvent).mockReturnValue({ type: 'customer.created', data: { object: {} } } as any)
const res = await POST(makeRequest('{}'))
expect(res.status).toBe(200)
expect((await res.json()).received).toBe(true)
})
})