Added unit and api tests
This commit is contained in:
parent
9f4204cc73
commit
fedf1eb4c5
34 changed files with 9205 additions and 20 deletions
549
backend/dist/routes/users.test.js
vendored
Normal file
549
backend/dist/routes/users.test.js
vendored
Normal file
|
|
@ -0,0 +1,549 @@
|
|||
"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, isAdmin: 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('/me', authenticate, async (req, res) => {
|
||||
try {
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: req.user.id },
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
name: true,
|
||||
screenName: true,
|
||||
avatarUrl: true,
|
||||
unitPreference: true,
|
||||
createdAt: true
|
||||
}
|
||||
});
|
||||
if (!user) {
|
||||
return res.status(404).json({ error: 'User not found' });
|
||||
}
|
||||
res.json(user);
|
||||
}
|
||||
catch {
|
||||
res.status(500).json({ error: 'Failed to get user' });
|
||||
}
|
||||
});
|
||||
app.put('/me', authenticate, async (req, res) => {
|
||||
try {
|
||||
const { name, screenName, avatarUrl, unitPreference } = req.body;
|
||||
const updated = await prisma.user.update({
|
||||
where: { id: req.user.id },
|
||||
data: {
|
||||
name: name || undefined,
|
||||
screenName: screenName !== undefined ? screenName || null : undefined,
|
||||
avatarUrl: avatarUrl !== undefined ? avatarUrl || null : undefined,
|
||||
unitPreference: unitPreference || undefined
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
name: true,
|
||||
screenName: true,
|
||||
avatarUrl: true,
|
||||
unitPreference: true,
|
||||
createdAt: true
|
||||
}
|
||||
});
|
||||
res.json(updated);
|
||||
}
|
||||
catch {
|
||||
res.status(500).json({ error: 'Failed to update user' });
|
||||
}
|
||||
});
|
||||
app.get('/me/location-history', authenticate, async (req, res) => {
|
||||
try {
|
||||
const locations = await prisma.locationHistory.findMany({
|
||||
where: { userId: req.user.id },
|
||||
include: {
|
||||
game: {
|
||||
select: { id: true, name: true }
|
||||
}
|
||||
},
|
||||
orderBy: { recordedAt: 'desc' }
|
||||
});
|
||||
const games = await prisma.game.findMany({
|
||||
where: {
|
||||
teams: {
|
||||
some: {
|
||||
members: {
|
||||
some: { userId: req.user.id }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
select: { id: true, name: true }
|
||||
});
|
||||
const locationByGame = games.map(game => {
|
||||
const gameLocations = locations.filter(l => l.gameId === game.id);
|
||||
return {
|
||||
game: game,
|
||||
locations: gameLocations,
|
||||
locationCount: gameLocations.length
|
||||
};
|
||||
}).filter(g => g.locationCount > 0);
|
||||
res.json({
|
||||
totalLocations: locations.length,
|
||||
byGame: locationByGame
|
||||
});
|
||||
}
|
||||
catch {
|
||||
res.status(500).json({ error: 'Failed to get location history' });
|
||||
}
|
||||
});
|
||||
app.get('/me/games', authenticate, async (req, res) => {
|
||||
try {
|
||||
const memberships = await prisma.teamMember.findMany({
|
||||
where: { userId: req.user.id },
|
||||
include: {
|
||||
team: {
|
||||
include: {
|
||||
game: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
status: true,
|
||||
startDate: true,
|
||||
locationLat: true,
|
||||
locationLng: true,
|
||||
gameMasterId: true,
|
||||
gameMaster: { select: { name: true } }
|
||||
}
|
||||
},
|
||||
teamRoutes: {
|
||||
include: {
|
||||
route: {
|
||||
include: {
|
||||
routeLegs: {
|
||||
orderBy: { sequenceNumber: 'asc' }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
photoSubmissions: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
const gamesWithDetails = memberships.map(m => {
|
||||
const team = m.team;
|
||||
const game = team.game;
|
||||
const teamRoute = team.teamRoutes[0];
|
||||
const route = teamRoute?.route;
|
||||
const photoSubmissions = team.photoSubmissions;
|
||||
const routeLegs = route?.routeLegs || [];
|
||||
const proofLocations = routeLegs.filter(leg => photoSubmissions.some(p => p.routeLegId === leg.id));
|
||||
let totalDistance = 0;
|
||||
if (game.locationLat && game.locationLng) {
|
||||
let prevLat = game.locationLat;
|
||||
let prevLng = game.locationLng;
|
||||
for (const leg of routeLegs) {
|
||||
if (leg.locationLat && leg.locationLng) {
|
||||
const R = 6371;
|
||||
const dLat = (leg.locationLat - prevLat) * Math.PI / 180;
|
||||
const dLng = (leg.locationLng - prevLng) * Math.PI / 180;
|
||||
const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
|
||||
Math.cos(prevLat * Math.PI / 180) * Math.cos(leg.locationLat * Math.PI / 180) *
|
||||
Math.sin(dLng / 2) * Math.sin(dLng / 2);
|
||||
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
||||
totalDistance += R * c;
|
||||
prevLat = leg.locationLat;
|
||||
prevLng = leg.locationLng;
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
gameId: game.id,
|
||||
gameName: game.name,
|
||||
gameStatus: game.status,
|
||||
gameMaster: game.gameMaster.name,
|
||||
startDate: game.startDate,
|
||||
teamId: team.id,
|
||||
teamName: team.name,
|
||||
teamStatus: team.status,
|
||||
routeId: route?.id || null,
|
||||
routeName: route?.name || null,
|
||||
routeColor: route?.color || null,
|
||||
totalLegs: routeLegs.length,
|
||||
totalDistance: Math.round(totalDistance * 100) / 100,
|
||||
proofLocations: proofLocations.map(leg => ({
|
||||
legNumber: leg.sequenceNumber,
|
||||
description: leg.description,
|
||||
locationLat: leg.locationLat,
|
||||
locationLng: leg.locationLng,
|
||||
hasPhotoProof: photoSubmissions.some(p => p.routeLegId === leg.id)
|
||||
}))
|
||||
};
|
||||
});
|
||||
res.json(gamesWithDetails);
|
||||
}
|
||||
catch {
|
||||
res.status(500).json({ error: 'Failed to get user games' });
|
||||
}
|
||||
});
|
||||
app.delete('/me/location-data', authenticate, async (req, res) => {
|
||||
try {
|
||||
await prisma.locationHistory.deleteMany({
|
||||
where: { userId: req.user.id }
|
||||
});
|
||||
res.json({ message: 'Location data deleted' });
|
||||
}
|
||||
catch {
|
||||
res.status(500).json({ error: 'Failed to delete location data' });
|
||||
}
|
||||
});
|
||||
app.delete('/me/account', authenticate, async (req, res) => {
|
||||
try {
|
||||
await prisma.user.delete({
|
||||
where: { id: req.user.id }
|
||||
});
|
||||
res.json({ message: 'Account deleted' });
|
||||
}
|
||||
catch {
|
||||
res.status(500).json({ error: 'Failed to delete account' });
|
||||
}
|
||||
});
|
||||
return app;
|
||||
}
|
||||
(0, vitest_1.describe)('Users API', () => {
|
||||
let app;
|
||||
let userToken;
|
||||
let userId;
|
||||
async function cleanup() {
|
||||
await prisma.photoSubmission.deleteMany();
|
||||
await prisma.routeLeg.deleteMany();
|
||||
await prisma.teamRoute.deleteMany();
|
||||
await prisma.teamMember.deleteMany();
|
||||
await prisma.team.deleteMany();
|
||||
await prisma.route.deleteMany();
|
||||
await prisma.chatMessage.deleteMany();
|
||||
await prisma.locationHistory.deleteMany();
|
||||
await prisma.game.deleteMany();
|
||||
await prisma.user.deleteMany();
|
||||
await prisma.systemSettings.deleteMany();
|
||||
await prisma.bannedEmail.deleteMany();
|
||||
await prisma.apiKey.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: 'testuser@test.com',
|
||||
passwordHash,
|
||||
name: 'Test User',
|
||||
unitPreference: 'METRIC'
|
||||
}
|
||||
});
|
||||
userId = user.id;
|
||||
userToken = jsonwebtoken_1.default.sign({ userId: user.id }, JWT_SECRET, { expiresIn: '7d' });
|
||||
});
|
||||
(0, vitest_1.describe)('GET /me', () => {
|
||||
(0, vitest_1.it)('should get current user profile', async () => {
|
||||
const res = await (0, supertest_1.default)(app)
|
||||
.get('/me')
|
||||
.set('Authorization', `Bearer ${userToken}`);
|
||||
(0, vitest_1.expect)(res.status).toBe(200);
|
||||
(0, vitest_1.expect)(res.body.id).toBe(userId);
|
||||
(0, vitest_1.expect)(res.body.email).toBe('testuser@test.com');
|
||||
(0, vitest_1.expect)(res.body.name).toBe('Test User');
|
||||
(0, vitest_1.expect)(res.body.unitPreference).toBe('METRIC');
|
||||
});
|
||||
(0, vitest_1.it)('should return 401 without token', async () => {
|
||||
const res = await (0, supertest_1.default)(app).get('/me');
|
||||
(0, vitest_1.expect)(res.status).toBe(401);
|
||||
});
|
||||
(0, vitest_1.it)('should return 401 with invalid token', async () => {
|
||||
const res = await (0, supertest_1.default)(app)
|
||||
.get('/me')
|
||||
.set('Authorization', 'Bearer invalid-token');
|
||||
(0, vitest_1.expect)(res.status).toBe(401);
|
||||
});
|
||||
});
|
||||
(0, vitest_1.describe)('PUT /me', () => {
|
||||
(0, vitest_1.it)('should update user name', async () => {
|
||||
const res = await (0, supertest_1.default)(app)
|
||||
.put('/me')
|
||||
.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.expect)(res.body.email).toBe('testuser@test.com');
|
||||
});
|
||||
(0, vitest_1.it)('should update screen name', async () => {
|
||||
const res = await (0, supertest_1.default)(app)
|
||||
.put('/me')
|
||||
.set('Authorization', `Bearer ${userToken}`)
|
||||
.send({ screenName: 'CoolPlayer' });
|
||||
(0, vitest_1.expect)(res.status).toBe(200);
|
||||
(0, vitest_1.expect)(res.body.screenName).toBe('CoolPlayer');
|
||||
});
|
||||
(0, vitest_1.it)('should update avatar URL', async () => {
|
||||
const res = await (0, supertest_1.default)(app)
|
||||
.put('/me')
|
||||
.set('Authorization', `Bearer ${userToken}`)
|
||||
.send({ avatarUrl: 'https://example.com/avatar.png' });
|
||||
(0, vitest_1.expect)(res.status).toBe(200);
|
||||
(0, vitest_1.expect)(res.body.avatarUrl).toBe('https://example.com/avatar.png');
|
||||
});
|
||||
(0, vitest_1.it)('should update unit preference to imperial', async () => {
|
||||
const res = await (0, supertest_1.default)(app)
|
||||
.put('/me')
|
||||
.set('Authorization', `Bearer ${userToken}`)
|
||||
.send({ unitPreference: 'IMPERIAL' });
|
||||
(0, vitest_1.expect)(res.status).toBe(200);
|
||||
(0, vitest_1.expect)(res.body.unitPreference).toBe('IMPERIAL');
|
||||
});
|
||||
(0, vitest_1.it)('should allow clearing optional fields with empty string', async () => {
|
||||
await prisma.user.update({
|
||||
where: { id: userId },
|
||||
data: { screenName: 'HasScreenName' }
|
||||
});
|
||||
const res = await (0, supertest_1.default)(app)
|
||||
.put('/me')
|
||||
.set('Authorization', `Bearer ${userToken}`)
|
||||
.send({ screenName: '' });
|
||||
(0, vitest_1.expect)(res.status).toBe(200);
|
||||
(0, vitest_1.expect)(res.body.screenName).toBe(null);
|
||||
});
|
||||
(0, vitest_1.it)('should update multiple fields at once', async () => {
|
||||
const res = await (0, supertest_1.default)(app)
|
||||
.put('/me')
|
||||
.set('Authorization', `Bearer ${userToken}`)
|
||||
.send({
|
||||
name: 'Multi Update',
|
||||
screenName: 'Multi',
|
||||
unitPreference: 'IMPERIAL'
|
||||
});
|
||||
(0, vitest_1.expect)(res.status).toBe(200);
|
||||
(0, vitest_1.expect)(res.body.name).toBe('Multi Update');
|
||||
(0, vitest_1.expect)(res.body.screenName).toBe('Multi');
|
||||
(0, vitest_1.expect)(res.body.unitPreference).toBe('IMPERIAL');
|
||||
});
|
||||
(0, vitest_1.it)('should return 401 without token', async () => {
|
||||
const res = await (0, supertest_1.default)(app)
|
||||
.put('/me')
|
||||
.send({ name: 'Hacker' });
|
||||
(0, vitest_1.expect)(res.status).toBe(401);
|
||||
});
|
||||
});
|
||||
(0, vitest_1.describe)('GET /me/location-history', () => {
|
||||
(0, vitest_1.it)('should return location history summary', async () => {
|
||||
const res = await (0, supertest_1.default)(app)
|
||||
.get('/me/location-history')
|
||||
.set('Authorization', `Bearer ${userToken}`);
|
||||
(0, vitest_1.expect)(res.status).toBe(200);
|
||||
(0, vitest_1.expect)(res.body).toHaveProperty('totalLocations');
|
||||
(0, vitest_1.expect)(res.body).toHaveProperty('byGame');
|
||||
(0, vitest_1.expect)(res.body.totalLocations).toBe(0);
|
||||
(0, vitest_1.expect)(res.body.byGame).toEqual([]);
|
||||
});
|
||||
(0, vitest_1.it)('should include location history with game info', async () => {
|
||||
const gm = await prisma.user.create({
|
||||
data: {
|
||||
email: 'gm@test.com',
|
||||
passwordHash: await bcryptjs_1.default.hash('pass', 10),
|
||||
name: 'GM'
|
||||
}
|
||||
});
|
||||
const game = await prisma.game.create({
|
||||
data: { name: 'Location Game', gameMasterId: gm.id }
|
||||
});
|
||||
const team = await prisma.team.create({
|
||||
data: { name: 'Loc Team', gameId: game.id, captainId: userId }
|
||||
});
|
||||
await prisma.teamMember.create({
|
||||
data: { teamId: team.id, userId }
|
||||
});
|
||||
await prisma.locationHistory.create({
|
||||
data: {
|
||||
userId,
|
||||
gameId: game.id,
|
||||
teamId: team.id,
|
||||
lat: 40.7128,
|
||||
lng: -74.0060,
|
||||
recordedAt: new Date()
|
||||
}
|
||||
});
|
||||
const res = await (0, supertest_1.default)(app)
|
||||
.get('/me/location-history')
|
||||
.set('Authorization', `Bearer ${userToken}`);
|
||||
(0, vitest_1.expect)(res.status).toBe(200);
|
||||
(0, vitest_1.expect)(res.body.totalLocations).toBe(1);
|
||||
(0, vitest_1.expect)(res.body.byGame.length).toBe(1);
|
||||
(0, vitest_1.expect)(res.body.byGame[0].game.name).toBe('Location Game');
|
||||
(0, vitest_1.expect)(res.body.byGame[0].locationCount).toBe(1);
|
||||
});
|
||||
(0, vitest_1.it)('should return 401 without token', async () => {
|
||||
const res = await (0, supertest_1.default)(app).get('/me/location-history');
|
||||
(0, vitest_1.expect)(res.status).toBe(401);
|
||||
});
|
||||
});
|
||||
(0, vitest_1.describe)('GET /me/games', () => {
|
||||
(0, vitest_1.it)('should return empty array when user has no games', async () => {
|
||||
const res = await (0, supertest_1.default)(app)
|
||||
.get('/me/games')
|
||||
.set('Authorization', `Bearer ${userToken}`);
|
||||
(0, vitest_1.expect)(res.status).toBe(200);
|
||||
(0, vitest_1.expect)(res.body).toEqual([]);
|
||||
});
|
||||
(0, vitest_1.it)('should return user games with details', async () => {
|
||||
const gm = await prisma.user.create({
|
||||
data: {
|
||||
email: 'gm@test.com',
|
||||
passwordHash: await bcryptjs_1.default.hash('pass', 10),
|
||||
name: 'Game Master'
|
||||
}
|
||||
});
|
||||
const game = await prisma.game.create({
|
||||
data: { name: 'My Game', gameMasterId: gm.id, status: 'LIVE' }
|
||||
});
|
||||
const route = await prisma.route.create({
|
||||
data: { name: 'My Route', gameId: game.id, color: '#FF0000' }
|
||||
});
|
||||
await prisma.routeLeg.create({
|
||||
data: {
|
||||
routeId: route.id,
|
||||
sequenceNumber: 1,
|
||||
description: 'First stop',
|
||||
locationLat: 40.7128,
|
||||
locationLng: -74.0060
|
||||
}
|
||||
});
|
||||
const team = await prisma.team.create({
|
||||
data: { name: 'My Team', gameId: game.id, captainId: userId }
|
||||
});
|
||||
await prisma.teamMember.create({
|
||||
data: { teamId: team.id, userId }
|
||||
});
|
||||
await prisma.teamRoute.create({
|
||||
data: { teamId: team.id, routeId: route.id }
|
||||
});
|
||||
const res = await (0, supertest_1.default)(app)
|
||||
.get('/me/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].gameName).toBe('My Game');
|
||||
(0, vitest_1.expect)(res.body[0].teamName).toBe('My Team');
|
||||
(0, vitest_1.expect)(res.body[0].routeName).toBe('My Route');
|
||||
(0, vitest_1.expect)(res.body[0].totalLegs).toBe(1);
|
||||
(0, vitest_1.expect)(res.body[0].teamStatus).toBe('ACTIVE');
|
||||
});
|
||||
(0, vitest_1.it)('should return 401 without token', async () => {
|
||||
const res = await (0, supertest_1.default)(app).get('/me/games');
|
||||
(0, vitest_1.expect)(res.status).toBe(401);
|
||||
});
|
||||
});
|
||||
(0, vitest_1.describe)('DELETE /me/location-data', () => {
|
||||
(0, vitest_1.it)('should delete user location history', async () => {
|
||||
const gm = await prisma.user.create({
|
||||
data: {
|
||||
email: 'gm@test.com',
|
||||
passwordHash: await bcryptjs_1.default.hash('pass', 10),
|
||||
name: 'GM'
|
||||
}
|
||||
});
|
||||
const game = await prisma.game.create({
|
||||
data: { name: 'Del Game', gameMasterId: gm.id }
|
||||
});
|
||||
const team = await prisma.team.create({
|
||||
data: { name: 'Del Team', gameId: game.id, captainId: userId }
|
||||
});
|
||||
await prisma.teamMember.create({
|
||||
data: { teamId: team.id, userId }
|
||||
});
|
||||
await prisma.locationHistory.create({
|
||||
data: {
|
||||
userId,
|
||||
gameId: game.id,
|
||||
teamId: team.id,
|
||||
lat: 40.7128,
|
||||
lng: -74.0060,
|
||||
recordedAt: new Date()
|
||||
}
|
||||
});
|
||||
const res = await (0, supertest_1.default)(app)
|
||||
.delete('/me/location-data')
|
||||
.set('Authorization', `Bearer ${userToken}`);
|
||||
(0, vitest_1.expect)(res.status).toBe(200);
|
||||
(0, vitest_1.expect)(res.body.message).toBe('Location data deleted');
|
||||
const locations = await prisma.locationHistory.count({
|
||||
where: { userId }
|
||||
});
|
||||
(0, vitest_1.expect)(locations).toBe(0);
|
||||
});
|
||||
(0, vitest_1.it)('should return 401 without token', async () => {
|
||||
const res = await (0, supertest_1.default)(app).delete('/me/location-data');
|
||||
(0, vitest_1.expect)(res.status).toBe(401);
|
||||
});
|
||||
});
|
||||
(0, vitest_1.describe)('DELETE /me/account', () => {
|
||||
(0, vitest_1.it)('should delete user account', async () => {
|
||||
const res = await (0, supertest_1.default)(app)
|
||||
.delete('/me/account')
|
||||
.set('Authorization', `Bearer ${userToken}`);
|
||||
(0, vitest_1.expect)(res.status).toBe(200);
|
||||
(0, vitest_1.expect)(res.body.message).toBe('Account deleted');
|
||||
const user = await prisma.user.findUnique({ where: { id: userId } });
|
||||
(0, vitest_1.expect)(user).toBeNull();
|
||||
});
|
||||
(0, vitest_1.it)('should return 401 without token', async () => {
|
||||
const res = await (0, supertest_1.default)(app).delete('/me/account');
|
||||
(0, vitest_1.expect)(res.status).toBe(401);
|
||||
});
|
||||
(0, vitest_1.it)('should not allow login after account deletion', async () => {
|
||||
await (0, supertest_1.default)(app)
|
||||
.delete('/me/account')
|
||||
.set('Authorization', `Bearer ${userToken}`);
|
||||
const res = await (0, supertest_1.default)(app)
|
||||
.get('/me')
|
||||
.set('Authorization', `Bearer ${userToken}`);
|
||||
(0, vitest_1.expect)(res.status).toBe(401);
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue