Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 33 additions & 9 deletions apps/backend/src/__tests__/event.test.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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<FastifyInstance> {
const app = Fastify({ logger: false });
Expand All @@ -77,7 +80,14 @@ async function buildApp(): Promise<FastifyInstance> {
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' });
Expand All @@ -97,7 +107,7 @@ async function createEvent(
app: FastifyInstance,
body: Record<string, unknown>,
authenticated = true,
) {
): Promise<LightMyRequestResponse> {
return app.inject({
method: 'POST',
url: '/api/events',
Expand Down Expand Up @@ -251,14 +261,15 @@ 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 },
});

const res = await app.inject({
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');
Expand All @@ -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' });
});
Expand All @@ -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 },
});

Expand Down Expand Up @@ -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,
Expand All @@ -495,6 +513,7 @@ describe('Events API', () => {
prismaMock.event.findUnique.mockResolvedValue({
...MOCK_EVENT,
attendees: attendeeRows,
_count: { attendees: 2 },
});

const res = await app.inject({
Expand Down Expand Up @@ -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({
Expand All @@ -545,6 +565,7 @@ describe('Events API', () => {
prismaMock.event.findUnique.mockResolvedValue({
...MOCK_EVENT,
attendees: [],
_count: { attendees: 0 },
});

const res = await app.inject({
Expand All @@ -561,6 +582,7 @@ describe('Events API', () => {
prismaMock.event.findUnique.mockResolvedValue({
...MOCK_EVENT,
attendees: [],
_count: { attendees: 0 },
});

const res = await app.inject({
Expand All @@ -577,6 +599,7 @@ describe('Events API', () => {
prismaMock.event.findUnique.mockResolvedValue({
...MOCK_EVENT,
attendees: [],
_count: { attendees: 0 },
});

const res = await app.inject({
Expand All @@ -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({
Expand Down Expand Up @@ -683,4 +707,4 @@ describe('Events API', () => {
expect(slug).not.toMatch(/--/);
});
});
});
});
49 changes: 19 additions & 30 deletions apps/backend/src/routes/event.ts
Original file line number Diff line number Diff line change
@@ -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 = {
Expand All @@ -10,6 +10,7 @@ type EventDetails = {
slug: string;
location: string;
description: string | null;
organizerId: string;
organizerUsername: string;
organizerDisplayName: string;
startDate: Date;
Expand Down Expand Up @@ -57,31 +58,18 @@ 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<void> {
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

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
})
Expand All @@ -95,7 +83,7 @@ export async function eventRoutes(app:FastifyInstance) {
name,
description,
slug: finalSlug,
location: location,
location,
startDate: startDateObj,
endDate: endDateObj,
isPublic: isPublic ?? true,
Expand All @@ -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'})
}
Expand Down Expand Up @@ -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,
Expand All @@ -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({
Expand All @@ -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()
}
})
Expand All @@ -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({
Expand All @@ -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'})
Expand Down