134 lines
4.0 KiB
JavaScript
Executable File
134 lines
4.0 KiB
JavaScript
Executable File
import Fastify from 'fastify'
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
import { NOTIFICATION_EVENTS } from '../../../../shared/constants/notification-events.js'
|
|
|
|
const { mockPrisma } = vi.hoisted(() => ({
|
|
mockPrisma: {
|
|
payment: { findFirst: vi.fn(), update: vi.fn() },
|
|
order: { findFirst: vi.fn(), updateMany: vi.fn() },
|
|
},
|
|
}))
|
|
|
|
vi.mock('../../lib/prisma.js', () => ({
|
|
prisma: mockPrisma,
|
|
}))
|
|
|
|
vi.mock('../../lib/yookassa.js', () => ({
|
|
validateWebhook: vi.fn(),
|
|
}))
|
|
|
|
import { validateWebhook } from '../../lib/yookassa.js'
|
|
import { registerYookassaWebhookRoute } from '../webhook-yookassa.js'
|
|
|
|
function buildApp(eventBusMock) {
|
|
const app = Fastify({ logger: false })
|
|
app.decorate('eventBus', eventBusMock || { emit: () => {} })
|
|
return app
|
|
}
|
|
|
|
describe('POST /api/webhooks/yookassa', () => {
|
|
let app
|
|
let eventBus
|
|
|
|
beforeEach(async () => {
|
|
eventBus = { emit: vi.fn() }
|
|
validateWebhook.mockImplementation((_ip, body) => {
|
|
if (!body || typeof body !== 'object') throw new Error('Invalid webhook body')
|
|
if (body.type !== 'notification') throw new Error('Expected notification type in webhook body')
|
|
if (!body.event || !body.object) throw new Error('Missing event or object in webhook body')
|
|
return { event: body.event, paymentObject: body.object }
|
|
})
|
|
app = buildApp(eventBus)
|
|
await registerYookassaWebhookRoute(app)
|
|
await app.ready()
|
|
})
|
|
|
|
afterEach(async () => {
|
|
await app.close()
|
|
vi.clearAllMocks()
|
|
})
|
|
|
|
it('returns 400 for invalid body', async () => {
|
|
const res = await app.inject({
|
|
method: 'POST',
|
|
url: '/api/webhooks/yookassa',
|
|
payload: { not: 'valid' },
|
|
})
|
|
expect(res.statusCode).toBe(400)
|
|
})
|
|
|
|
it('returns 404 when payment not found', async () => {
|
|
mockPrisma.payment.findFirst.mockResolvedValue(null)
|
|
const res = await app.inject({
|
|
method: 'POST',
|
|
url: '/api/webhooks/yookassa',
|
|
payload: {
|
|
type: 'notification',
|
|
event: 'payment.succeeded',
|
|
object: { id: 'unknown-id', status: 'succeeded', paid: true },
|
|
},
|
|
})
|
|
expect(res.statusCode).toBe(404)
|
|
})
|
|
|
|
it('updates payment and order on payment.succeeded', async () => {
|
|
mockPrisma.payment.findFirst.mockResolvedValue({
|
|
id: 'payment-1',
|
|
yookassaPaymentId: 'yk-id',
|
|
status: 'pending',
|
|
orderId: 'order-1',
|
|
})
|
|
mockPrisma.payment.update.mockResolvedValue({})
|
|
mockPrisma.order.findFirst.mockResolvedValue({
|
|
id: 'order-1',
|
|
status: 'PENDING_PAYMENT',
|
|
userId: 'user-1',
|
|
})
|
|
mockPrisma.order.updateMany.mockResolvedValue({ count: 1 })
|
|
|
|
const res = await app.inject({
|
|
method: 'POST',
|
|
url: '/api/webhooks/yookassa',
|
|
payload: {
|
|
type: 'notification',
|
|
event: 'payment.succeeded',
|
|
object: { id: 'yk-id', status: 'succeeded', paid: true },
|
|
},
|
|
})
|
|
expect(res.statusCode).toBe(200)
|
|
|
|
const updateData = mockPrisma.payment.update.mock.calls[0][0].data
|
|
expect(updateData.status).toBe('succeeded')
|
|
|
|
const orderUpdateData = mockPrisma.order.updateMany.mock.calls[0][0].data
|
|
expect(orderUpdateData.status).toBe('PAID')
|
|
expect(eventBus.emit).toHaveBeenCalledWith(NOTIFICATION_EVENTS.PAYMENT_STATUS_CHANGED, {
|
|
orderId: 'order-1',
|
|
userId: 'user-1',
|
|
paymentStatus: 'paid',
|
|
})
|
|
})
|
|
|
|
it('updates payment on payment.canceled without changing order', async () => {
|
|
mockPrisma.payment.findFirst.mockResolvedValue({
|
|
id: 'payment-1',
|
|
yookassaPaymentId: 'yk-id',
|
|
status: 'pending',
|
|
orderId: 'order-1',
|
|
})
|
|
mockPrisma.payment.update.mockResolvedValue({})
|
|
|
|
const res = await app.inject({
|
|
method: 'POST',
|
|
url: '/api/webhooks/yookassa',
|
|
payload: {
|
|
type: 'notification',
|
|
event: 'payment.canceled',
|
|
object: { id: 'yk-id', status: 'canceled', paid: false },
|
|
},
|
|
})
|
|
expect(res.statusCode).toBe(200)
|
|
expect(mockPrisma.order.findFirst).not.toHaveBeenCalled()
|
|
})
|
|
})
|