474 lines
21 KiB
JavaScript
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);
|
|
});
|
|
});
|
|
});
|