Skip to content
Closed
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
30 changes: 24 additions & 6 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 } from 'fastify';

// ─── Shared mock data ────────────────────────────────────────────────────────

const MOCK_USER_ID = 'user-uuid-001';
Expand Down Expand Up @@ -64,7 +67,7 @@
//
// 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 @@
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 @@ -93,7 +103,7 @@
}

/** Injects a POST /api/events request */
async function createEvent(

Check warning on line 106 in apps/backend/src/__tests__/event.test.ts

View workflow job for this annotation

GitHub Actions / backend-ci

Missing return type on function
app: FastifyInstance,
body: Record<string, unknown>,
authenticated = true,
Expand Down Expand Up @@ -251,21 +261,22 @@
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');
expect(body.attendeesCount).toBe(42);
expect(body.location).toBe('San Francisco, CA');
// organizerId is exposed (public info)
expect(body.organizerId).toBe(MOCK_USER_ID);

Check failure on line 279 in apps/backend/src/__tests__/event.test.ts

View workflow job for this annotation

GitHub Actions / backend-ci

src/__tests__/event.test.ts > Events API > GET /api/events/:slug — event details > 200 — returns event info with attendee count

AssertionError: expected undefined to be 'user-uuid-001' // Object.is equality - Expected: "user-uuid-001" + Received: undefined ❯ src/__tests__/event.test.ts:279:32
});

it('404 — returns 404 for unknown slug', async () => {
Expand All @@ -275,7 +286,7 @@
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 @@
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 @@ -474,7 +486,7 @@

describe('GET /api/events/:slug/attendees — paginated attendee list', () => {
/** Builds a raw EventAttendee row as Prisma returns it (with nested user) */
function makeAttendeeRow(

Check warning on line 489 in apps/backend/src/__tests__/event.test.ts

View workflow job for this annotation

GitHub Actions / backend-ci

Missing return type on function
profile: typeof MOCK_USER_PROFILE | typeof MOCK_OTHER_USER_PROFILE,
) {
return {
Expand All @@ -495,6 +507,7 @@
prismaMock.event.findUnique.mockResolvedValue({
...MOCK_EVENT,
attendees: attendeeRows,
_count: { attendees: 2 },
});

const res = await app.inject({
Expand Down Expand Up @@ -523,6 +536,7 @@
prismaMock.event.findUnique.mockResolvedValue({
...MOCK_EVENT,
attendees: [makeAttendeeRow(MOCK_OTHER_USER_PROFILE)],
_count: { attendees: 1 },
});

const res = await app.inject({
Expand All @@ -545,6 +559,7 @@
prismaMock.event.findUnique.mockResolvedValue({
...MOCK_EVENT,
attendees: [],
_count: { attendees: 0 },
});

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

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

const res = await app.inject({
Expand All @@ -594,6 +611,7 @@
prismaMock.event.findUnique.mockResolvedValue({
...MOCK_EVENT,
attendees: [makeAttendeeRow(MOCK_USER_PROFILE)],
_count: { attendees: 1 },
});

const res = await app.inject({
Expand Down
18 changes: 6 additions & 12 deletions apps/backend/src/routes/cards.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import { hashIp } from '../utils/refreshToken';
import { createCardSchema ,updateCardSchema, addPlatformLinkSchema} from '../validations/card.validation';

import type { CardResponse, UpdateCardBody, UpdatedCardResponse } from '../services/cardService';

Check failure on line 6 in apps/backend/src/routes/cards.ts

View workflow job for this annotation

GitHub Actions / backend-ci

'UpdatedCardResponse' is defined but never used. Allowed unused vars must match /^_/u
import type { Card } from '@devcard/shared/src/types.js';
import type { CardVisibility } from '@prisma/client';
import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
Expand Down Expand Up @@ -62,15 +62,9 @@
}

export async function cardRoutes(app: FastifyInstance): Promise<void> {
app.addHook('preHandler', async (request, reply) => {
const server = request.server;
if (typeof server?.authenticate === 'function') { await server.authenticate(request, reply); return }
if (typeof app.authenticate === 'function') { await app.authenticate(request, reply); return }
try { await request.jwtVerify() } catch (_e) { reply.status(401).send({ error: 'Unauthorized' }) }
});


// ─── List Cards ───
app.get('/', async (request: FastifyRequest, reply: FastifyReply): Promise<CardResponse[] | void> => {
app.get('/', {preHandler: [(req, reply) => app.authenticate(req, reply)] },async (request: FastifyRequest, reply: FastifyReply): Promise<CardResponse[] | void> => {
const userId = request.user.id;
try {
return await cardService.listCards(app, userId)
Expand All @@ -80,7 +74,7 @@
});

// ─── Creates Card ───
app.post('/', async (request: FastifyRequest<{ Body: CreateCardBody }>, reply: FastifyReply): Promise<Card | void> => {
app.post<{ Body: CreateCardBody }>('/', { preHandler: [(req, reply) => app.authenticate(req, reply)]}, async (request, reply): Promise<Card | void> => {
const userId = request.user.id;
const parsed = createCardSchema.safeParse(request.body);

Expand All @@ -99,7 +93,7 @@

// ─── Update Card ───

app.put('/:id', async (request: FastifyRequest<{ Params: CardParams; Body: UpdateCardBody }>, reply: FastifyReply): Promise<UpdatedCardResponse> => {
app.put<{ Params: CardParams; Body: UpdateCardBody }>('/:id', {preHandler: [(req, reply) => app.authenticate(req, reply)] }, async (request, reply): Promise<CardResponse> => {
const userId = request.user.id;
const { id } = request.params;

Expand All @@ -117,7 +111,7 @@

// ─── Delete Card ───

app.delete('/:id', async (request: FastifyRequest<{ Params: CardParams }>, reply: FastifyReply): Promise<void> => {
app.delete<{ Params: CardParams }>('/:id', { preHandler: [(req, reply) => app.authenticate(req, reply)]}, async (request, reply): Promise<void> => {
const userId = request.user.id;
const { id } = request.params;

Expand All @@ -139,7 +133,7 @@
});

// ─── Set Default Card ───
app.put('/:id/default', async (request: FastifyRequest<{ Params: CardParams }>, reply: FastifyReply): Promise<object | void> => {
app.put<{ Params: CardParams }>('/:id/default', {preHandler: [(req, reply) => app.authenticate(req, reply)]}, async (request, reply): Promise<object | void> => {
const userId = request.user.id;
const { id } = request.params;

Expand Down
43 changes: 15 additions & 28 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 Down Expand Up @@ -57,31 +57,18 @@
}[];
}

export async function eventRoutes(app:FastifyInstance) {

Check warning on line 60 in apps/backend/src/routes/event.ts

View workflow job for this annotation

GitHub Actions / backend-ci

Missing return type on function
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;
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 +82,7 @@
name,
description,
slug: finalSlug,
location: location,
location,
startDate: startDateObj,
endDate: endDateObj,
isPublic: isPublic ?? true,
Expand All @@ -104,7 +91,7 @@
})

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 @@ -153,8 +140,8 @@
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 +158,7 @@
await app.prisma.eventAttendee.create({
data: {
eventId: event.id,
userId: userId,
userId,
joinedAt: new Date()
}
})
Expand All @@ -186,9 +173,9 @@
}

})
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,7 +192,7 @@
await app.prisma.eventAttendee.delete({
where: {
userId_eventId: {
userId: userId,
userId,
eventId: event.id
}
}
Expand Down
32 changes: 8 additions & 24 deletions apps/backend/src/routes/nfc.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
import { z } from 'zod';

import type { FastifyInstance} from 'fastify';

type NfcPayloadResponse = {
type: 'URI';
payload: string;
Expand All @@ -10,33 +11,16 @@
card: z.string().uuid('Invalid card ID format').optional(),
});

export async function nfcRoutes(app: FastifyInstance) {

Check warning on line 14 in apps/backend/src/routes/nfc.ts

View workflow job for this annotation

GitHub Actions / backend-ci

Missing return type on function
app.addHook('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' });
}
});


// GET /api/nfc/payload — returns NDEF URI payload for user's default DevCard URL
// GET /api/nfc/payload?card=<cardId> — returns payload for a specific card
app.get(
'/payload',
async (
request: FastifyRequest<{ Querystring: { card?: string } }>,
reply: FastifyReply
) => {
const userId = (request.user as any).id;
app.get<{ Querystring: { card?: string } }>(
'/payload',
{ preHandler: [(req, reply) => app.authenticate(req, reply)] },
async (request, reply) => {
const userId = request.user.id;

// Validate query params with Zod
const parseResult = nfcQuerySchema.safeParse(request.query);
Expand Down
Loading