diff --git a/apps/backend/src/__tests__/event.test.ts b/apps/backend/src/__tests__/event.test.ts index 26e0352b..1931223b 100644 --- a/apps/backend/src/__tests__/event.test.ts +++ b/apps/backend/src/__tests__/event.test.ts @@ -1,9 +1,10 @@ -import { type PrismaClient, Prisma } from '@prisma/client'; -import Fastify, { type FastifyInstance } from 'fastify'; +import Fastify 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 ──────────────────────────────────────────────────────── @@ -106,7 +107,7 @@ async function createEvent( app: FastifyInstance, body: Record, authenticated = true, -): Promise>> { +): Promise { return app.inject({ method: 'POST', url: '/api/events', @@ -366,10 +367,9 @@ 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 = new Prisma.PrismaClientKnownRequestError( - 'Unique constraint failed', - { code: 'P2002', clientVersion: '6.0.0' }, - ); + const uniqueError = Object.assign(new Error('Unique constraint'), { + code: 'P2002', + }); prismaMock.eventAttendee.create.mockRejectedValue(uniqueError); const res = await app.inject({ @@ -452,10 +452,9 @@ 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 = new Prisma.PrismaClientKnownRequestError( - 'Record not found', - { code: 'P2025', clientVersion: '6.0.0' }, - ); + const notFoundError = Object.assign(new Error('Record not found'), { + code: 'P2025', + }); prismaMock.eventAttendee.delete.mockRejectedValue(notFoundError); const res = await app.inject({ @@ -489,13 +488,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 profile; - } { + ) : { + id: string; + userId: string; + eventId: string; + joinedAt: Date; + user: typeof MOCK_USER_PROFILE | typeof MOCK_OTHER_USER_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 ad7b34c7..5c24b4ff 100644 --- a/apps/backend/src/routes/event.ts +++ b/apps/backend/src/routes/event.ts @@ -1,10 +1,8 @@ -import { Prisma } from '@prisma/client'; +import {generateUniqueSlug} from '../utils/slug.js' +import { createEventSchema} from '../validations/event.validation.js'; -import { getErrorMessage } from '../utils/error.util'; -import { generateUniqueSlug } from '../utils/slug'; -import { createEventSchema } from '../validations/event.validation'; +import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify'; -import type { FastifyInstance } from 'fastify'; type EventDetails = { id: string; @@ -18,8 +16,8 @@ type EventDetails = { startDate: Date; endDate: Date; createdAt: Date; - attendeesCount: number; -}; + attendeesCount: number +} type AttendeePublicProfile = { id: string; @@ -30,16 +28,17 @@ 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: { @@ -57,220 +56,219 @@ 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: [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' }); +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'}) } + + const {name, description, startDate, endDate, isPublic ,location} = parsed.data - 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 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 { - app.log.error('Failed to create event'); - return reply.status(500).send({ error: 'Failed to create event' }); - } - }); + startDate: startDateObj, + endDate: endDateObj, + isPublic: isPublic ?? true, + organizerId: userId + } + }) - // 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 } }, - }, - }); + 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'}) + } + + }) - if (!details) { - return reply.status(404).send({ error: 'Event not found' }); + //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 + } + }, + organizer: { + select: { + username: true, + displayName: true + } + } } + }) + if(!details){ + 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, - }; - - return response; - }, - ); + 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; + }) - 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; + 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 (!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' }); + if(!event){ + return reply.status(404).send({error: 'Event not found'}) + } + + try { + await app.prisma.eventAttendee.create({ + data: { + eventId: event.id, + userId, + joinedAt: new Date() } - app.log.error(getErrorMessage(error)); - return reply.status(500).send({ error: 'Failed to join' }); + }) + + 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'}) + } - 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; + }) + app.delete<{Params: {slug: string}}>('/:slug/leave',{preHandler: [(req, reply) => app.authenticate(req, reply)]}, async(request, reply) => { - const event = await app.prisma.event.findUnique({ where: { slug: paramsSlug } }); - if (!event) { - return reply.status(404).send({ error: 'Event not found' }); + 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'}) + } - 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' }); + try { + await app.prisma.eventAttendee.delete({ + where: { + userId_eventId: { + userId, + eventId: event.id + } } - app.log.error(getErrorMessage(error)); - return reply.status(500).send({ error: 'Failed to leave' }); + }) + return reply.status(204).send() + } catch (error:any) { + if(error.code === 'P2025'){ + return reply.status(404).send({error: 'User 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; + app.log.error((error as Error).message) + return reply.status(500).send({error: 'Failed to leave'}) + } + }) - 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; + 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'} + } + }, + })as EventWithAttendees | null; - if (!event) { - return reply.status(404).send({ error: 'Event not found' }); - } + if(!event){ + return reply.status(404).send({error: 'Event not found'}) + } - 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 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 }, - }; + const response: PaginatedAttendeesResponse = { + attendees, + pagination: { + page, + limit, + total : event._count.attendees, + } + } - return response; - }, - ); + return response; + }) }