TreasureTrails/backend/dist/routes/games.test.js

474 lines
21 KiB
JavaScript

"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);
});
});
});