"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const vitest_1 = require("vitest"); const express_1 = __importDefault(require("express")); const bcryptjs_1 = __importDefault(require("bcryptjs")); const jsonwebtoken_1 = __importDefault(require("jsonwebtoken")); const supertest_1 = __importDefault(require("supertest")); const client_1 = require("@prisma/client"); const prisma = new client_1.PrismaClient(); const JWT_SECRET = 'test-secret-key'; function createApp() { const app = (0, express_1.default)(); app.use(express_1.default.json()); const authenticate = async (req, res, next) => { const authHeader = req.headers.authorization; if (!authHeader || !authHeader.startsWith('Bearer ')) { return res.status(401).json({ error: 'No token provided' }); } const token = authHeader.split(' ')[1]; try { const decoded = jsonwebtoken_1.default.verify(token, JWT_SECRET); const user = await prisma.user.findUnique({ where: { id: decoded.userId }, select: { id: true, email: true, name: true } }); if (!user) { return res.status(401).json({ error: 'User not found' }); } req.user = user; next(); } catch { return res.status(401).json({ error: 'Invalid token' }); } }; app.get('/games', async (req, res) => { try { const { search, status } = req.query; const where = { visibility: 'PUBLIC' }; if (status) where.status = status; if (search) where.name = { contains: search, mode: 'insensitive' }; const games = await prisma.game.findMany({ where, include: { gameMaster: { select: { id: true, name: true } }, _count: { select: { teams: true, routes: true } } }, orderBy: { createdAt: 'desc' } }); res.json(games); } catch (error) { res.status(500).json({ error: 'Failed to list games' }); } }); app.get('/games/my-games', authenticate, async (req, res) => { try { const games = await prisma.game.findMany({ where: { gameMasterId: req.user.id }, include: { _count: { select: { teams: true, routes: true } } }, orderBy: { createdAt: 'desc' } }); res.json(games); } catch (error) { res.status(500).json({ error: 'Failed to get games' }); } }); app.get('/games/:id', async (req, res) => { try { const game = await prisma.game.findUnique({ where: { id: req.params.id }, include: { gameMaster: { select: { id: true, name: true } }, routes: { include: { routeLegs: { orderBy: { sequenceNumber: 'asc' } } } }, teams: { include: { members: { include: { user: { select: { id: true, name: true, email: true } } } } } } } }); if (!game) return res.status(404).json({ error: 'Game not found' }); const isOwner = req.user?.id === game.gameMasterId; if (game.visibility === 'PRIVATE' && !isOwner) { return res.status(403).json({ error: 'Access denied' }); } res.json({ ...game, rules: game.rules ? JSON.parse(game.rules) : [] }); } catch (error) { res.status(500).json({ error: 'Failed to get game' }); } }); app.post('/games', authenticate, async (req, res) => { try { const { name, description, visibility, startDate, locationLat, locationLng, searchRadius } = req.body; if (!name) return res.status(400).json({ error: 'Name is required' }); const game = await prisma.game.create({ data: { name, description, visibility: visibility || 'PUBLIC', startDate: startDate ? new Date(startDate) : null, locationLat, locationLng, searchRadius, gameMasterId: req.user.id, inviteCode: Math.random().toString(36).slice(2, 10) } }); res.json(game); } catch (error) { res.status(500).json({ error: 'Failed to create game' }); } }); app.put('/games/:id', authenticate, async (req, res) => { try { const game = await prisma.game.findUnique({ where: { id: req.params.id } }); if (!game) return res.status(404).json({ error: 'Game not found' }); if (game.gameMasterId !== req.user.id) return res.status(403).json({ error: 'Not authorized' }); const updated = await prisma.game.update({ where: { id: req.params.id }, data: req.body }); res.json(updated); } catch (error) { res.status(500).json({ error: 'Failed to update game' }); } }); app.delete('/games/:id', authenticate, async (req, res) => { try { const game = await prisma.game.findUnique({ where: { id: req.params.id } }); if (!game) return res.status(404).json({ error: 'Game not found' }); if (game.gameMasterId !== req.user.id) return res.status(403).json({ error: 'Not authorized' }); if (game.status !== 'DRAFT') return res.status(400).json({ error: 'Only draft games can be deleted' }); await prisma.game.delete({ where: { id: req.params.id } }); res.json({ message: 'Game deleted' }); } catch (error) { res.status(500).json({ error: 'Failed to delete game' }); } }); app.post('/games/:id/publish', authenticate, async (req, res) => { try { const game = await prisma.game.findUnique({ where: { id: req.params.id }, include: { routes: { include: { routeLegs: true } } } }); if (!game) return res.status(404).json({ error: 'Game not found' }); if (game.gameMasterId !== req.user.id) return res.status(403).json({ error: 'Not authorized' }); if (!game.routes?.length || !game.routes.some((r) => r.routeLegs?.length)) { return res.status(400).json({ error: 'Game must have at least one route with legs' }); } const updated = await prisma.game.update({ where: { id: req.params.id }, data: { status: 'LIVE' } }); res.json(updated); } catch (error) { res.status(500).json({ error: 'Failed to publish game' }); } }); app.post('/games/:id/end', authenticate, async (req, res) => { try { const game = await prisma.game.findUnique({ where: { id: req.params.id } }); if (!game) return res.status(404).json({ error: 'Game not found' }); if (game.gameMasterId !== req.user.id) return res.status(403).json({ error: 'Not authorized' }); const updated = await prisma.game.update({ where: { id: req.params.id }, data: { status: 'ENDED' } }); res.json(updated); } catch (error) { res.status(500).json({ error: 'Failed to end game' }); } }); app.post('/games/:id/archive', authenticate, async (req, res) => { try { const game = await prisma.game.findUnique({ where: { id: req.params.id } }); if (!game) return res.status(404).json({ error: 'Game not found' }); if (game.gameMasterId !== req.user.id) return res.status(403).json({ error: 'Not authorized' }); const updated = await prisma.game.update({ where: { id: req.params.id }, data: { status: 'ARCHIVED' } }); res.json(updated); } catch (error) { res.status(500).json({ error: 'Failed to archive game' }); } }); app.get('/games/invite/:code', async (req, res) => { try { const game = await prisma.game.findUnique({ where: { inviteCode: req.params.code }, include: { gameMaster: { select: { id: true, name: true } } } }); if (!game) return res.status(404).json({ error: 'Game not found' }); res.json(game); } catch (error) { res.status(500).json({ error: 'Failed to get game' }); } }); return app; } (0, vitest_1.describe)('Games API', () => { let app; let userToken; let userId; let otherToken; let otherUserId; async function cleanup() { await prisma.routeLeg.deleteMany(); await prisma.route.deleteMany(); await prisma.teamRoute.deleteMany(); await prisma.teamMember.deleteMany(); await prisma.team.deleteMany(); await prisma.photoSubmission.deleteMany(); await prisma.chatMessage.deleteMany(); await prisma.locationHistory.deleteMany(); await prisma.game.deleteMany(); await prisma.user.deleteMany(); } (0, vitest_1.beforeAll)(async () => { app = createApp(); await cleanup(); }); (0, vitest_1.afterAll)(async () => { await cleanup(); await prisma.$disconnect(); }); (0, vitest_1.beforeEach)(async () => { await cleanup(); const passwordHash = await bcryptjs_1.default.hash('password123', 10); const user = await prisma.user.create({ data: { email: 'owner@test.com', passwordHash, name: 'Owner User' } }); userId = user.id; userToken = jsonwebtoken_1.default.sign({ userId: user.id }, JWT_SECRET, { expiresIn: '7d' }); const otherUser = await prisma.user.create({ data: { email: 'other@test.com', passwordHash, name: 'Other User' } }); otherUserId = otherUser.id; otherToken = jsonwebtoken_1.default.sign({ userId: otherUser.id }, JWT_SECRET, { expiresIn: '7d' }); }); (0, vitest_1.describe)('GET /games', () => { (0, vitest_1.it)('should list public games', async () => { await prisma.game.create({ data: { name: 'Public Game', gameMasterId: userId, visibility: 'PUBLIC' } }); await prisma.game.create({ data: { name: 'Private Game', gameMasterId: userId, visibility: 'PRIVATE' } }); const res = await (0, supertest_1.default)(app).get('/games'); (0, vitest_1.expect)(res.status).toBe(200); (0, vitest_1.expect)(res.body.length).toBe(1); (0, vitest_1.expect)(res.body[0].name).toBe('Public Game'); }); (0, vitest_1.it)('should filter by status', async () => { await prisma.game.create({ data: { name: 'Draft Game', gameMasterId: userId, status: 'DRAFT' } }); await prisma.game.create({ data: { name: 'Live Game', gameMasterId: userId, status: 'LIVE' } }); const res = await (0, supertest_1.default)(app).get('/games?status=LIVE'); (0, vitest_1.expect)(res.status).toBe(200); (0, vitest_1.expect)(res.body.length).toBe(1); (0, vitest_1.expect)(res.body[0].name).toBe('Live Game'); }); (0, vitest_1.it)('should search by name', async () => { await prisma.game.create({ data: { name: 'Treasure Hunt', gameMasterId: userId } }); await prisma.game.create({ data: { name: 'Scavenger Race', gameMasterId: userId } }); const res = await (0, supertest_1.default)(app).get('/games?search=treasure'); (0, vitest_1.expect)(res.status).toBe(200); (0, vitest_1.expect)(res.body.length).toBe(1); (0, vitest_1.expect)(res.body[0].name).toBe('Treasure Hunt'); }); }); (0, vitest_1.describe)('GET /games/my-games', () => { (0, vitest_1.it)('should list games created by user', async () => { await prisma.game.create({ data: { name: 'My Game', gameMasterId: userId } }); await prisma.game.create({ data: { name: 'Other Game', gameMasterId: otherUserId } }); const res = await (0, supertest_1.default)(app) .get('/games/my-games') .set('Authorization', `Bearer ${userToken}`); (0, vitest_1.expect)(res.status).toBe(200); (0, vitest_1.expect)(res.body.length).toBe(1); (0, vitest_1.expect)(res.body[0].name).toBe('My Game'); }); (0, vitest_1.it)('should return 401 without token', async () => { const res = await (0, supertest_1.default)(app).get('/games/my-games'); (0, vitest_1.expect)(res.status).toBe(401); }); }); (0, vitest_1.describe)('POST /games', () => { (0, vitest_1.it)('should create a game', async () => { const res = await (0, supertest_1.default)(app) .post('/games') .set('Authorization', `Bearer ${userToken}`) .send({ name: 'New Game', description: 'A test game' }); (0, vitest_1.expect)(res.status).toBe(200); (0, vitest_1.expect)(res.body.name).toBe('New Game'); (0, vitest_1.expect)(res.body.gameMasterId).toBe(userId); }); (0, vitest_1.it)('should require name', async () => { const res = await (0, supertest_1.default)(app) .post('/games') .set('Authorization', `Bearer ${userToken}`) .send({ description: 'No name' }); (0, vitest_1.expect)(res.status).toBe(400); (0, vitest_1.expect)(res.body.error).toBe('Name is required'); }); (0, vitest_1.it)('should return 401 without token', async () => { const res = await (0, supertest_1.default)(app) .post('/games') .send({ name: 'Test' }); (0, vitest_1.expect)(res.status).toBe(401); }); }); (0, vitest_1.describe)('GET /games/:id', () => { (0, vitest_1.it)('should get a game by id', async () => { const game = await prisma.game.create({ data: { name: 'Test Game', gameMasterId: userId } }); const res = await (0, supertest_1.default)(app).get(`/games/${game.id}`); (0, vitest_1.expect)(res.status).toBe(200); (0, vitest_1.expect)(res.body.name).toBe('Test Game'); }); (0, vitest_1.it)('should return 404 for non-existent game', async () => { const res = await (0, supertest_1.default)(app).get('/games/non-existent-id'); (0, vitest_1.expect)(res.status).toBe(404); }); }); (0, vitest_1.describe)('PUT /games/:id', () => { (0, vitest_1.it)('should update a game', async () => { const game = await prisma.game.create({ data: { name: 'Original Name', gameMasterId: userId } }); const res = await (0, supertest_1.default)(app) .put(`/games/${game.id}`) .set('Authorization', `Bearer ${userToken}`) .send({ name: 'Updated Name' }); (0, vitest_1.expect)(res.status).toBe(200); (0, vitest_1.expect)(res.body.name).toBe('Updated Name'); }); (0, vitest_1.it)('should not allow update by non-owner', async () => { const game = await prisma.game.create({ data: { name: 'Test Game', gameMasterId: userId } }); const res = await (0, supertest_1.default)(app) .put(`/games/${game.id}`) .set('Authorization', `Bearer ${otherToken}`) .send({ name: 'Hacked' }); (0, vitest_1.expect)(res.status).toBe(403); }); }); (0, vitest_1.describe)('DELETE /games/:id', () => { (0, vitest_1.it)('should delete a draft game', async () => { const game = await prisma.game.create({ data: { name: 'Draft Game', gameMasterId: userId, status: 'DRAFT' } }); const res = await (0, supertest_1.default)(app) .delete(`/games/${game.id}`) .set('Authorization', `Bearer ${userToken}`); (0, vitest_1.expect)(res.status).toBe(200); (0, vitest_1.expect)(res.body.message).toBe('Game deleted'); }); (0, vitest_1.it)('should not delete non-draft game', async () => { const game = await prisma.game.create({ data: { name: 'Live Game', gameMasterId: userId, status: 'LIVE' } }); const res = await (0, supertest_1.default)(app) .delete(`/games/${game.id}`) .set('Authorization', `Bearer ${userToken}`); (0, vitest_1.expect)(res.status).toBe(400); (0, vitest_1.expect)(res.body.error).toContain('Only draft'); }); }); (0, vitest_1.describe)('POST /games/:id/publish', () => { (0, vitest_1.it)('should publish a game with routes and legs', async () => { const game = await prisma.game.create({ data: { name: 'Test Game', gameMasterId: userId } }); const route = await prisma.route.create({ data: { name: 'Test Route', gameId: game.id } }); await prisma.routeLeg.create({ data: { routeId: route.id, sequenceNumber: 1, description: 'First leg' } }); const res = await (0, supertest_1.default)(app) .post(`/games/${game.id}/publish`) .set('Authorization', `Bearer ${userToken}`); (0, vitest_1.expect)(res.status).toBe(200); (0, vitest_1.expect)(res.body.status).toBe('LIVE'); }); (0, vitest_1.it)('should not publish game without routes', async () => { const game = await prisma.game.create({ data: { name: 'Empty Game', gameMasterId: userId } }); const res = await (0, supertest_1.default)(app) .post(`/games/${game.id}/publish`) .set('Authorization', `Bearer ${userToken}`); (0, vitest_1.expect)(res.status).toBe(400); (0, vitest_1.expect)(res.body.error).toContain('at least one route'); }); }); (0, vitest_1.describe)('POST /games/:id/end', () => { (0, vitest_1.it)('should end a live game', async () => { const game = await prisma.game.create({ data: { name: 'Live Game', gameMasterId: userId, status: 'LIVE' } }); const res = await (0, supertest_1.default)(app) .post(`/games/${game.id}/end`) .set('Authorization', `Bearer ${userToken}`); (0, vitest_1.expect)(res.status).toBe(200); (0, vitest_1.expect)(res.body.status).toBe('ENDED'); }); }); (0, vitest_1.describe)('POST /games/:id/archive', () => { (0, vitest_1.it)('should archive an ended game', async () => { const game = await prisma.game.create({ data: { name: 'Ended Game', gameMasterId: userId, status: 'ENDED' } }); const res = await (0, supertest_1.default)(app) .post(`/games/${game.id}/archive`) .set('Authorization', `Bearer ${userToken}`); (0, vitest_1.expect)(res.status).toBe(200); (0, vitest_1.expect)(res.body.status).toBe('ARCHIVED'); }); }); (0, vitest_1.describe)('GET /games/invite/:code', () => { (0, vitest_1.it)('should find game by invite code', async () => { await prisma.game.create({ data: { name: 'Invite Game', gameMasterId: userId, inviteCode: 'TESTCODE123' } }); const res = await (0, supertest_1.default)(app).get('/games/invite/TESTCODE123'); (0, vitest_1.expect)(res.status).toBe(200); (0, vitest_1.expect)(res.body.name).toBe('Invite Game'); }); (0, vitest_1.it)('should return 404 for invalid code', async () => { const res = await (0, supertest_1.default)(app).get('/games/invite/INVALID'); (0, vitest_1.expect)(res.status).toBe(404); }); }); });