From 317b910710be978198d65fffb87573088f4da2c3 Mon Sep 17 00:00:00 2001 From: Kirill Date: Wed, 20 May 2026 19:12:46 +0500 Subject: [PATCH] fix: email validation, conditional order update, improved tests for payment routes --- .../routes/__tests__/user-payments.test.js | 114 +++++++++++++++++- server/src/routes/user-payments.js | 23 ++-- 2 files changed, 125 insertions(+), 12 deletions(-) diff --git a/server/src/routes/__tests__/user-payments.test.js b/server/src/routes/__tests__/user-payments.test.js index 7ab9b90..525caec 100644 --- a/server/src/routes/__tests__/user-payments.test.js +++ b/server/src/routes/__tests__/user-payments.test.js @@ -1,20 +1,20 @@ -import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from 'vitest' +import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest' import Fastify from 'fastify' import jwt from '@fastify/jwt' import { prisma } from '../../lib/prisma.js' import { registerUserPaymentRoutes } from '../user-payments.js' const JWT_SECRET = 'test-secret' -const TEST_USER_EMAIL = 'test-pay-user@example.com' +const TEST_USER_EMAIL = `test-pay-${Date.now()}@example.com` let testUserId let testOrderId -async function signToken(userId) { +async function signToken(userId, email = TEST_USER_EMAIL) { const fastify = Fastify() await fastify.register(jwt, { secret: JWT_SECRET }) await fastify.ready() - return fastify.jwt.sign({ sub: userId, email: TEST_USER_EMAIL }) + return fastify.jwt.sign({ sub: userId, email }) } async function buildApp() { @@ -37,6 +37,10 @@ describe('POST /api/me/orders/:id/pay', () => { let app beforeAll(async () => { + await prisma.payment.deleteMany() + await prisma.order.deleteMany({ where: { user: { email: TEST_USER_EMAIL } } }) + await prisma.user.deleteMany({ where: { email: TEST_USER_EMAIL } }) + const user = await prisma.user.create({ data: { email: TEST_USER_EMAIL }, }) @@ -57,6 +61,7 @@ describe('POST /api/me/orders/:id/pay', () => { }) afterAll(async () => { + await prisma.payment.deleteMany({ where: { orderId: testOrderId } }) await prisma.order.deleteMany({ where: { userId: testUserId } }) await prisma.user.deleteMany({ where: { email: TEST_USER_EMAIL } }) }) @@ -75,6 +80,7 @@ describe('POST /api/me/orders/:id/pay', () => { afterEach(async () => { await app.close() + vi.restoreAllMocks() }) it('returns 401 without auth', async () => { @@ -136,4 +142,104 @@ describe('POST /api/me/orders/:id/pay', () => { }) expect(res.statusCode).toBe(409) }) + + it('returns 422 when user has no email', async () => { + const noEmailUser = await prisma.user.create({ + data: { email: `noemail-${Date.now()}@test.com` }, + }) + const noEmailOrder = await prisma.order.create({ + data: { + userId: noEmailUser.id, + status: 'PENDING_PAYMENT', + paymentMethod: 'online', + deliveryFeeLocked: true, + totalCents: 100000, + currency: 'RUB', + }, + }) + + const fastify = Fastify() + await fastify.register(jwt, { secret: JWT_SECRET }) + const token = fastify.jwt.sign({ sub: noEmailUser.id }) + await fastify.close() + + const res = await app.inject({ + method: 'POST', + url: `/api/me/orders/${noEmailOrder.id}/pay`, + headers: { authorization: `Bearer ${token}` }, + }) + expect(res.statusCode).toBe(422) + + await prisma.order.deleteMany({ where: { userId: noEmailUser.id } }) + await prisma.user.deleteMany({ where: { id: noEmailUser.id } }) + }) +}) + +describe('GET /api/me/orders/:orderId/payment', () => { + let app + let getTestUserId + let getTestOrderId + + beforeAll(async () => { + const getEmail = `get-pay-${Date.now()}@example.com` + const user = await prisma.user.create({ data: { email: getEmail } }) + getTestUserId = user.id + + const order = await prisma.order.create({ + data: { + userId: getTestUserId, + status: 'PENDING_PAYMENT', + paymentMethod: 'online', + deliveryFeeLocked: true, + totalCents: 100000, + currency: 'RUB', + }, + }) + getTestOrderId = order.id + }) + + afterAll(async () => { + await prisma.payment.deleteMany({ where: { orderId: getTestOrderId } }) + await prisma.order.deleteMany({ where: { userId: getTestUserId } }) + await prisma.user.deleteMany({ where: { id: getTestUserId } }) + }) + + beforeEach(async () => { + app = await buildApp() + }) + + afterEach(async () => { + await app.close() + }) + + it('returns 401 without auth', async () => { + const res = await app.inject({ + method: 'GET', + url: `/api/me/orders/${getTestOrderId}/payment`, + }) + expect(res.statusCode).toBe(401) + }) + + it('returns 404 when order not found', async () => { + const token = await signToken(getTestUserId) + const res = await app.inject({ + method: 'GET', + url: '/api/me/orders/nonexistent-id/payment', + headers: { authorization: `Bearer ${token}` }, + }) + expect(res.statusCode).toBe(404) + }) + + it('returns status null when no payment exists', async () => { + const token = await signToken(getTestUserId) + const res = await app.inject({ + method: 'GET', + url: `/api/me/orders/${getTestOrderId}/payment`, + headers: { authorization: `Bearer ${token}` }, + }) + expect(res.statusCode).toBe(200) + const body = JSON.parse(res.payload) + expect(body.status).toBeNull() + expect(body.paid).toBe(false) + }) }) diff --git a/server/src/routes/user-payments.js b/server/src/routes/user-payments.js index 8941a8c..a1cad89 100644 --- a/server/src/routes/user-payments.js +++ b/server/src/routes/user-payments.js @@ -9,6 +9,11 @@ export async function registerUserPaymentRoutes(fastify) { async (request, reply) => { const userId = request.user.sub const userEmail = request.user.email + + if (!userEmail) { + return reply.code(422).send({ error: 'Для онлайн-оплаты необходим email в профиле' }) + } + const { id } = request.params const order = await prisma.order.findFirst({ @@ -42,7 +47,7 @@ export async function registerUserPaymentRoutes(fastify) { return { confirmationUrl: existingPayment.confirmationUrl } } - const idempotencyKey = `${id}-v1` + const idempotencyKey = `${id}-${Date.now()}` const returnUrl = `${process.env.CLIENT_PUBLIC_URL || 'http://127.0.0.1:5173'}/me/orders/${id}?paid=1` const clientIp = request.ip @@ -123,15 +128,17 @@ export async function registerUserPaymentRoutes(fastify) { }) if (ykPayment.status === 'succeeded' && order.status === 'PENDING_PAYMENT') { - await prisma.order.update({ - where: { id: orderId }, + const updated = await prisma.order.updateMany({ + where: { id: orderId, status: 'PENDING_PAYMENT' }, data: { status: 'PAID' }, }) - request.server.eventBus.emit(NOTIFICATION_EVENTS.PAYMENT_STATUS_CHANGED, { - orderId, - userId: order.userId, - paymentStatus: 'paid', - }) + if (updated.count > 0) { + request.server.eventBus.emit(NOTIFICATION_EVENTS.PAYMENT_STATUS_CHANGED, { + orderId, + userId: order.userId, + paymentStatus: 'paid', + }) + } } }