diff --git a/apps/backend/src/__tests__/event.test.ts b/apps/backend/src/__tests__/event.test.ts index 1931223b..26e0352b 100644 --- a/apps/backend/src/__tests__/event.test.ts +++ b/apps/backend/src/__tests__/event.test.ts @@ -1,10 +1,9 @@ -import Fastify from 'fastify'; +import { type PrismaClient, Prisma } from '@prisma/client'; +import Fastify, { type FastifyInstance } from 'fastify'; import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; import { eventRoutes } from '../routes/event'; -import type { PrismaClient } from '@prisma/client'; -import type { FastifyInstance,LightMyRequestResponse } from 'fastify'; // ─── Shared mock data ──────────────────────────────────────────────────────── @@ -107,7 +106,7 @@ async function createEvent( app: FastifyInstance, body: Record, authenticated = true, -): Promise { +): Promise>> { return app.inject({ method: 'POST', url: '/api/events', @@ -367,9 +366,10 @@ describe('Events API', () => { it('409 — returns 409 when user already joined the event', async () => { prismaMock.event.findUnique.mockResolvedValue(MOCK_EVENT); // Prisma unique constraint error - const uniqueError = Object.assign(new Error('Unique constraint'), { - code: 'P2002', - }); + const uniqueError = new Prisma.PrismaClientKnownRequestError( + 'Unique constraint failed', + { code: 'P2002', clientVersion: '6.0.0' }, + ); prismaMock.eventAttendee.create.mockRejectedValue(uniqueError); const res = await app.inject({ @@ -452,9 +452,10 @@ describe('Events API', () => { it('404 — returns 404 when user was never an attendee (P2025)', async () => { prismaMock.event.findUnique.mockResolvedValue(MOCK_EVENT); // Prisma record-not-found error - const notFoundError = Object.assign(new Error('Record not found'), { - code: 'P2025', - }); + const notFoundError = new Prisma.PrismaClientKnownRequestError( + 'Record not found', + { code: 'P2025', clientVersion: '6.0.0' }, + ); prismaMock.eventAttendee.delete.mockRejectedValue(notFoundError); const res = await app.inject({ @@ -488,13 +489,13 @@ describe('Events API', () => { /** Builds a raw EventAttendee row as Prisma returns it (with nested user) */ function makeAttendeeRow( profile: typeof MOCK_USER_PROFILE | typeof MOCK_OTHER_USER_PROFILE, - ) : { - id: string; - userId: string; - eventId: string; - joinedAt: Date; - user: typeof MOCK_USER_PROFILE | typeof MOCK_OTHER_USER_PROFILE; - } { + ): { + id: string; + userId: string; + eventId: string; + joinedAt: Date; + user: typeof profile; + } { return { id: `attendee-${profile.id}`, userId: profile.id, diff --git a/apps/backend/src/routes/event.ts b/apps/backend/src/routes/event.ts index 5c24b4ff..ad7b34c7 100644 --- a/apps/backend/src/routes/event.ts +++ b/apps/backend/src/routes/event.ts @@ -1,8 +1,10 @@ -import {generateUniqueSlug} from '../utils/slug.js' -import { createEventSchema} from '../validations/event.validation.js'; +import { Prisma } from '@prisma/client'; -import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify'; +import { getErrorMessage } from '../utils/error.util'; +import { generateUniqueSlug } from '../utils/slug'; +import { createEventSchema } from '../validations/event.validation'; +import type { FastifyInstance } from 'fastify'; type EventDetails = { id: string; @@ -16,8 +18,8 @@ type EventDetails = { startDate: Date; endDate: Date; createdAt: Date; - attendeesCount: number -} + attendeesCount: number; +}; type AttendeePublicProfile = { id: string; @@ -28,17 +30,16 @@ type AttendeePublicProfile = { company: string | null; avatarUrl: string | null; accentColor: string; -} - +}; type PaginatedAttendeesResponse = { attendees: AttendeePublicProfile[]; pagination: { page: number; limit: number; - total: number; + total: number; }; -} +}; type EventWithAttendees = { _count: { @@ -56,219 +57,220 @@ type EventWithAttendees = { accentColor: string; }; }[]; -} +}; -export async function eventRoutes(app:FastifyInstance): Promise { - app.post<{Body: { name: string; description?: string; startDate: string; location: string; endDate: string; isPublic?: boolean; }}>('/', { preHandler: [(req, reply) => app.authenticate(req, reply)] }, async (request, reply) => { - const userId = request.user.id; - const parsed = createEventSchema.safeParse(request.body); - if(!parsed.success){ - return reply.status(400).send({error: 'Bad request'}) +export async function eventRoutes( + app: FastifyInstance, +): Promise { + app.post<{ + Body: { + name: string; + description?: string; + startDate: string; + location: string; + endDate: string; + isPublic?: boolean; + }; + }>('/', { preHandler: [async (request, reply) => { await app.authenticate(request, reply); }] }, + async (request, reply) => { + const userId = request.user.id; + const parsed = createEventSchema.safeParse(request.body); + if (!parsed.success) { + return reply.status(400).send({ error: 'Bad request' }); } - - const {name, description, startDate, endDate, isPublic ,location} = parsed.data - const finalSlug = await generateUniqueSlug(name, async(slug) => { - const existing = await app.prisma.event.findUnique({where: {slug}}) - - return !!existing - }) + const { name, description, startDate, endDate, isPublic, location } = parsed.data; + + const finalSlug = await generateUniqueSlug(name, async (slug) => { + const existing = await app.prisma.event.findUnique({ where: { slug } }); + return !!existing; + }); - const startDateObj = new Date(startDate); - const endDateObj = new Date(endDate); + const startDateObj = new Date(startDate); + const endDateObj = new Date(endDate); try { const newEvent = await app.prisma.event.create({ data: { - name, - description, - slug: finalSlug, + name, + description, + slug: finalSlug, location, - startDate: startDateObj, - endDate: endDateObj, - isPublic: isPublic ?? true, - organizerId: userId - } - }) - - return reply.status(201).send(newEvent); - } catch (_error) { - app.log.error('Failed to create event'); - return reply.status(500).send({error: 'Failed to create event'}) - } - - }) - - //Returns event details and attendees count - app.get('/:slug', async(request: FastifyRequest<{Params: {slug: string}}>, reply: FastifyReply) => { - const paramsSlug = request.params.slug; - const details = await app.prisma.event.findUnique({ - where: { - slug: paramsSlug, - }, - include: { - _count: { - select: { - attendees: true - } + startDate: startDateObj, + endDate: endDateObj, + isPublic: isPublic ?? true, + organizerId: userId, }, - organizer: { - select: { - username: true, - displayName: true - } - } - } - }) - if(!details){ - return reply.status(404).send({error: 'Event not found'}) + }); + return reply.status(201).send(newEvent); + } catch { + app.log.error('Failed to create event'); + return reply.status(500).send({ error: 'Failed to create event' }); } + }); - const response: EventDetails = { - id: details.id, - name: details.name, - slug: details.slug, - description: details.description, - location: details.location, - organizerId: details.organizerId, - organizerUsername: details.organizer.username, - organizerDisplayName: details.organizer.displayName, - startDate: details.startDate, - endDate: details.endDate, - createdAt: details.createdAt, - attendeesCount: details._count.attendees - } - - return response; - }) + // Returns event details and attendees count + app.get<{ Params: { slug: string } }>( + '/:slug', + async (request, reply) => { + const paramsSlug = request.params.slug; + const details = await app.prisma.event.findUnique({ + where: { slug: paramsSlug }, + include: { + _count: { select: { attendees: true } }, + organizer: { select: { username: true, displayName: true } }, + }, + }); - app.post<{ Params: { slug: string } }>('/:slug/join', {preHandler: [(req, reply) => app.authenticate(req, reply)]}, async(request, reply) => { - const userId = request.user.id; - const paramsSlug = request.params.slug; - - const event = await app.prisma.event.findUnique({ - where: { - slug: paramsSlug + if (!details) { + return reply.status(404).send({ error: 'Event not found' }); } - }) - if(!event){ - return reply.status(404).send({error: 'Event not found'}) - } + const response: EventDetails = { + id: details.id, + name: details.name, + slug: details.slug, + description: details.description, + location: details.location, + organizerUsername: details.organizer.username, + organizerDisplayName: details.organizer.displayName, + startDate: details.startDate, + endDate: details.endDate, + createdAt: details.createdAt, + attendeesCount: details._count.attendees, + }; - try { - await app.prisma.eventAttendee.create({ - data: { - eventId: event.id, - userId, - joinedAt: new Date() - } - }) - - return reply.status(201).send({message: 'User joined successfully'}) - } catch (error:any) { - if(error.code === "P2002" ){ - return reply.status(409).send({error: 'Already joined'}) - } - app.log.error((error as Error).message); - return reply.status(500).send({error: 'Failed to join'}) - } + return response; + }, + ); - }) - app.delete<{Params: {slug: string}}>('/:slug/leave',{preHandler: [(req, reply) => app.authenticate(req, reply)]}, async(request, reply) => { + app.post<{ Params: { slug: string } }>( + '/:slug/join', + { preHandler: [async (request, reply) => { await app.authenticate(request, reply); }] }, + async (request, reply) => { + const userId = request.user.id; + const paramsSlug = request.params.slug; - const userId = request.user.id; - const paramsSlug = request.params.slug; + const event = await app.prisma.event.findUnique({ where: { slug: paramsSlug } }); + if (!event) { + return reply.status(404).send({ error: 'Event not found' }); + } - const event = await app.prisma.event.findUnique({ - where: { - slug: paramsSlug + try { + await app.prisma.eventAttendee.create({ + data: { + eventId: event.id, + userId, + joinedAt: new Date(), + }, + }); + return reply.status(201).send({ message: 'User joined successfully' }); + } catch (error: unknown) { + if ( + error instanceof Prisma.PrismaClientKnownRequestError && + error.code === 'P2002' + ) { + return reply.status(409).send({ error: 'Already joined' }); + } + app.log.error(getErrorMessage(error)); + return reply.status(500).send({ error: 'Failed to join' }); } - }) + }, + ); - if(!event){ - return reply.status(404).send({error: 'Event not found'}) - } + app.delete<{ Params: { slug: string } }>( + '/:slug/leave', + { preHandler: [async (request, reply) => { await app.authenticate(request, reply); }] }, + async (request, reply) => { + const userId = request.user.id; + const paramsSlug = request.params.slug; - try { - await app.prisma.eventAttendee.delete({ - where: { - userId_eventId: { - userId, - eventId: event.id - } - } - }) - return reply.status(204).send() - } catch (error:any) { - if(error.code === 'P2025'){ - return reply.status(404).send({error: 'User not found'}) + const event = await app.prisma.event.findUnique({ where: { slug: paramsSlug } }); + if (!event) { + return reply.status(404).send({ error: 'Event not found' }); } - app.log.error((error as Error).message) - return reply.status(500).send({error: 'Failed to leave'}) - } - }) - app.get('/:slug/attendees', async(request: FastifyRequest<{Params: {slug: string}, Querystring: {page?:string; limit?: string}}>, reply: FastifyReply) => { - const paramsSlug = request.params.slug; - const page = Math.max(1, Number(request.query.page) || 1); - const limit = Math.min(50, Number(request.query.limit) || 10); - const skip = (page - 1) * limit - const event = await app.prisma.event.findUnique({ - where: { - slug: paramsSlug - }, - include: { - _count: { - select: { attendees: true } - }, - attendees : { - include: { - user: { - select: { - id: true, - username: true, - displayName:true, - bio: true, - pronouns: true, - company: true, - avatarUrl: true, - accentColor: true - } - } - }, - skip, - take: limit, - orderBy: {joinedAt: 'desc'} + try { + await app.prisma.eventAttendee.delete({ + where: { + userId_eventId: { userId, eventId: event.id }, + }, + }); + return reply.status(204).send({ message: 'User left' }); + } catch (error: unknown) { + if ( + error instanceof Prisma.PrismaClientKnownRequestError && + error.code === 'P2025' + ) { + return reply.status(404).send({ error: 'User not found' }); } - }, - })as EventWithAttendees | null; + app.log.error(getErrorMessage(error)); + return reply.status(500).send({ error: 'Failed to leave' }); + } + }, + ); - if(!event){ - return reply.status(404).send({error: 'Event not found'}) - } + app.get<{ + Params: { slug: string }; + Querystring: { page?: string; limit?: string }; + }>( + '/:slug/attendees', + async (request, reply) => { + const paramsSlug = request.params.slug; + const page = Math.max(1, Number(request.query.page) || 1); + const limit = Math.min(50, Number(request.query.limit) || 10); + const skip = (page - 1) * limit; - - const attendees = event.attendees.map((attendee: EventWithAttendees['attendees'][number]) => ({ - id: attendee.user.id, - username: attendee.user.username, - displayName: attendee.user.displayName, - bio: attendee.user.bio, - pronouns: attendee.user.pronouns, - company: attendee.user.company, - avatarUrl: attendee.user.avatarUrl, - accentColor: attendee.user.accentColor, - })); + const event = (await app.prisma.event.findUnique({ + where: { slug: paramsSlug }, + include: { + _count: { select: { attendees: true } }, + attendees: { + include: { + user: { + select: { + id: true, + username: true, + displayName: true, + bio: true, + pronouns: true, + company: true, + avatarUrl: true, + accentColor: true, + }, + }, + }, + skip, + take: limit, + orderBy: { joinedAt: 'desc' }, + }, + }, + })) as EventWithAttendees | null; - const response: PaginatedAttendeesResponse = { - attendees, - pagination: { - page, - limit, - total : event._count.attendees, + if (!event) { + return reply.status(404).send({ error: 'Event not found' }); } - } - return response; - }) + const attendees = event.attendees.map( + (attendee: EventWithAttendees['attendees'][number]) => ( + { + id: attendee.user.id, + username: attendee.user.username, + displayName: attendee.user.displayName, + bio: attendee.user.bio, + pronouns: attendee.user.pronouns, + company: attendee.user.company, + avatarUrl: attendee.user.avatarUrl, + accentColor: attendee.user.accentColor, + }), + ); + + const response: PaginatedAttendeesResponse = { + attendees, + pagination: { page, limit, total: event._count.attendees }, + }; + + return response; + }, + ); }