diff --git a/apps/backend/src/__tests__/event.test.ts b/apps/backend/src/__tests__/event.test.ts index 44806af1..1931223b 100644 --- a/apps/backend/src/__tests__/event.test.ts +++ b/apps/backend/src/__tests__/event.test.ts @@ -1,8 +1,11 @@ +import Fastify from 'fastify'; import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; -import Fastify, { FastifyInstance } from 'fastify'; -import { PrismaClient } from '@prisma/client'; + import { eventRoutes } from '../routes/event'; +import type { PrismaClient } from '@prisma/client'; +import type { FastifyInstance,LightMyRequestResponse } from 'fastify'; + // ─── Shared mock data ──────────────────────────────────────────────────────── const MOCK_USER_ID = 'user-uuid-001'; @@ -64,7 +67,7 @@ const prismaMock = { // // This mirrors the real app setup without touching a real DB or real JWT keys. -let mockJwtVerify = vi.fn(); +const mockJwtVerify = vi.fn(); async function buildApp(): Promise { const app = Fastify({ logger: false }); @@ -77,7 +80,14 @@ async function buildApp(): Promise { app.decorateRequest('jwtVerify', function () { return mockJwtVerify(); }); - + app.decorate('authenticate', async function (request, reply) { + try { + const payload = await request.jwtVerify(); + if (payload) { request.user = payload as typeof request.user; } + } catch { + return reply.status(401).send({ error: 'Unauthorized' }); + } + }); // Register with the same prefix used in production (app.ts) so that // tests exercise routes at their real paths — /api/events, /api/events/:slug, etc. await app.register(eventRoutes, { prefix: '/api/events' }); @@ -97,7 +107,7 @@ async function createEvent( app: FastifyInstance, body: Record, authenticated = true, -) { +): Promise { return app.inject({ method: 'POST', url: '/api/events', @@ -251,6 +261,7 @@ describe('Events API', () => { it('200 — returns event info with attendee count', async () => { prismaMock.event.findUnique.mockResolvedValue({ ...MOCK_EVENT, + organizer: { username: 'johndoe', displayName: 'John Doe' }, _count: { attendees: 42 }, }); @@ -258,7 +269,7 @@ describe('Events API', () => { method: 'GET', url: '/api/events/devcard-conf-2025', }); - + console.log(JSON.stringify(res.json(), null, 2)); expect(res.statusCode).toBe(200); const body = res.json(); expect(body.slug).toBe('devcard-conf-2025'); @@ -275,7 +286,7 @@ describe('Events API', () => { method: 'GET', url: '/api/events/ghost-event', }); - + expect(res.statusCode).toBe(404); expect(res.json()).toMatchObject({ error: 'Event not found' }); }); @@ -285,6 +296,7 @@ describe('Events API', () => { mockJwtVerify.mockRejectedValue(new Error('Should not be called')); prismaMock.event.findUnique.mockResolvedValue({ ...MOCK_EVENT, + organizer: { username: 'johndoe', displayName: 'John Doe' }, _count: { attendees: 0 }, }); @@ -476,7 +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 MOCK_USER_PROFILE | typeof MOCK_OTHER_USER_PROFILE; + } { return { id: `attendee-${profile.id}`, userId: profile.id, @@ -495,6 +513,7 @@ describe('Events API', () => { prismaMock.event.findUnique.mockResolvedValue({ ...MOCK_EVENT, attendees: attendeeRows, + _count: { attendees: 2 }, }); const res = await app.inject({ @@ -523,6 +542,7 @@ describe('Events API', () => { prismaMock.event.findUnique.mockResolvedValue({ ...MOCK_EVENT, attendees: [makeAttendeeRow(MOCK_OTHER_USER_PROFILE)], + _count: { attendees: 1 }, }); const res = await app.inject({ @@ -545,6 +565,7 @@ describe('Events API', () => { prismaMock.event.findUnique.mockResolvedValue({ ...MOCK_EVENT, attendees: [], + _count: { attendees: 0 }, }); const res = await app.inject({ @@ -561,6 +582,7 @@ describe('Events API', () => { prismaMock.event.findUnique.mockResolvedValue({ ...MOCK_EVENT, attendees: [], + _count: { attendees: 0 }, }); const res = await app.inject({ @@ -577,6 +599,7 @@ describe('Events API', () => { prismaMock.event.findUnique.mockResolvedValue({ ...MOCK_EVENT, attendees: [], + _count: { attendees: 0 }, }); const res = await app.inject({ @@ -594,6 +617,7 @@ describe('Events API', () => { prismaMock.event.findUnique.mockResolvedValue({ ...MOCK_EVENT, attendees: [makeAttendeeRow(MOCK_USER_PROFILE)], + _count: { attendees: 1 }, }); const res = await app.inject({ @@ -683,4 +707,4 @@ describe('Events API', () => { expect(slug).not.toMatch(/--/); }); }); -}); \ No newline at end of file +}); diff --git a/apps/backend/src/routes/event.ts b/apps/backend/src/routes/event.ts index 8d7bc566..1999f58b 100644 --- a/apps/backend/src/routes/event.ts +++ b/apps/backend/src/routes/event.ts @@ -1,7 +1,7 @@ -import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify'; -import { createEventSchema, joinEventSchema} from '../validations/event.validation.js'; - import {generateUniqueSlug} from '../utils/slug.js' +import { createEventSchema} from '../validations/event.validation.js'; + +import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify'; type EventDetails = { @@ -10,6 +10,7 @@ type EventDetails = { slug: string; location: string; description: string | null; + organizerId: string; organizerUsername: string; organizerDisplayName: string; startDate: Date; @@ -57,22 +58,9 @@ type EventWithAttendees = { }[]; } -export async function eventRoutes(app:FastifyInstance) { - app.post('/', { preHandler: [async (request, reply) => { - const server = request.server as any; - if (typeof server?.authenticate === 'function') { await server.authenticate(request, reply); return } - if (typeof (app as any).authenticate === 'function') { await (app as any).authenticate(request, reply); return } - try { await request.jwtVerify() } catch (e) { reply.status(401).send({ error: 'Unauthorized' }) } - }] }, async (request: FastifyRequest<{ - Body: { - name: string, - description?: string, - startDate: string, - location: string, - endDate: string, - isPublic?: boolean - }}>, reply: FastifyReply) => { - const userId = (request.user as any).id; +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'}) @@ -80,8 +68,8 @@ export async function eventRoutes(app:FastifyInstance) { const {name, description, startDate, endDate, isPublic ,location} = parsed.data - let finalSlug = await generateUniqueSlug(name, async(slug) => { - const existing = await app.prisma.event.findUnique({where: {slug : slug}}) + const finalSlug = await generateUniqueSlug(name, async(slug) => { + const existing = await app.prisma.event.findUnique({where: {slug}}) return !!existing }) @@ -95,7 +83,7 @@ export async function eventRoutes(app:FastifyInstance) { name, description, slug: finalSlug, - location: location, + location, startDate: startDateObj, endDate: endDateObj, isPublic: isPublic ?? true, @@ -104,7 +92,7 @@ export async function eventRoutes(app:FastifyInstance) { }) return reply.status(201).send(newEvent); - } catch (error) { + } catch (_error) { app.log.error('Failed to create event'); return reply.status(500).send({error: 'Failed to create event'}) } @@ -142,6 +130,7 @@ export async function eventRoutes(app:FastifyInstance) { slug: details.slug, description: details.description, location: details.location, + organizerId: details.organizerId, organizerUsername: details.organizer.username, organizerDisplayName: details.organizer.displayName, startDate: details.startDate, @@ -153,8 +142,8 @@ export async function eventRoutes(app:FastifyInstance) { return response; }) - app.post('/:slug/join', { preHandler: [async (request, reply) => { const server = request.server as any; if (typeof server?.authenticate === 'function') { await server.authenticate(request, reply); return } if (typeof (app as any).authenticate === 'function') { await (app as any).authenticate(request, reply); return } try { await request.jwtVerify() } catch (e) { reply.status(401).send({ error: 'Unauthorized' }) } }] }, async(request: FastifyRequest<{Params: {slug: string}}>, reply: FastifyReply) => { - const userId = (request.user as any).id; + 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({ @@ -171,7 +160,7 @@ export async function eventRoutes(app:FastifyInstance) { await app.prisma.eventAttendee.create({ data: { eventId: event.id, - userId: userId, + userId, joinedAt: new Date() } }) @@ -186,9 +175,9 @@ export async function eventRoutes(app:FastifyInstance) { } }) + app.delete<{Params: {slug: string}}>('/:slug/leave',{preHandler: [(req, reply) => app.authenticate(req, reply)]}, async(request, reply) => { - app.delete('/:slug/leave', { preHandler: [async (request, reply) => { const server = request.server as any; if (typeof server?.authenticate === 'function') { await server.authenticate(request, reply); return } if (typeof (app as any).authenticate === 'function') { await (app as any).authenticate(request, reply); return } try { await request.jwtVerify() } catch (e) { reply.status(401).send({ error: 'Unauthorized' }) } }] }, async(request: FastifyRequest<{Params: {slug: string}}>, reply: FastifyReply) => { - const userId = (request.user as any).id; + const userId = request.user.id; const paramsSlug = request.params.slug; const event = await app.prisma.event.findUnique({ @@ -205,12 +194,12 @@ export async function eventRoutes(app:FastifyInstance) { await app.prisma.eventAttendee.delete({ where: { userId_eventId: { - userId: userId, + userId, eventId: event.id } } }) - return reply.status(204).send({message: 'User left'}) + return reply.status(204).send() } catch (error:any) { if(error.code === 'P2025'){ return reply.status(404).send({error: 'User not found'})