da1f9437ab
- Extract Express app creation into src/createApp.js to enable testing without starting the server
- Add Jest, Supertest, test helpers (createTestApp, auth token generator)
- Write 14 integration tests for skydive/jumps: auth protection, CRUD, ownership checks
Bugs caught and fixed by the test suite:
- MongoDB models: author field typed as Schema.Types.UUID (Mongoose 6 rejects string UUIDs) → changed to String
- Jump.toJSONFor: called toProfileJSONFor on a UUID string → guarded with typeof check
- exceptions.handler: used err.statusCode but express-jwt throws err.status → returns 500 on 401 errors
- Jump.count(query): deprecated Mongoose method was ignoring the filter → replaced with countDocuments
- jumps.routes GET /: $or query included non-schema field "title" stripped by strictQuery, leaving {} (match-all) → replaced with slug/lieu search
- $regex: was passing a JS RegExp object which serializes to {} in MongoDB → now passes raw string
237 lines
7.5 KiB
JavaScript
237 lines
7.5 KiB
JavaScript
const request = require('supertest');
|
|
const mongoose = require('mongoose');
|
|
|
|
const { connectMongo, disconnectMongo, buildApp } = require('../../helpers/createTestApp');
|
|
const { generateToken, mockUser, TEST_USER_ID, TEST_USERNAME } = require('../../helpers/auth');
|
|
|
|
jest.mock('../../../src/services', () => ({
|
|
ArticleService: {},
|
|
CommentService: {},
|
|
ProductService: {},
|
|
SkydiverProfileService: {},
|
|
TagService: {},
|
|
UserService: {
|
|
getUserById: jest.fn(),
|
|
getUserByUsername: jest.fn(),
|
|
getUserByEmail: jest.fn(),
|
|
},
|
|
ClanService: {},
|
|
MemberService: {},
|
|
}));
|
|
|
|
const { UserService } = require('../../../src/services');
|
|
|
|
let app;
|
|
let token;
|
|
let testJump;
|
|
|
|
beforeAll(async () => {
|
|
await connectMongo();
|
|
app = buildApp('/api/skydive/jumps', require('../../../src/routes/api/skydive/jumps.routes'));
|
|
token = generateToken();
|
|
|
|
UserService.getUserById.mockResolvedValue(mockUser);
|
|
UserService.getUserByUsername.mockResolvedValue(null);
|
|
|
|
const Jump = mongoose.model('Jump');
|
|
testJump = await Jump.create({
|
|
numero: 9001,
|
|
date: new Date('2024-06-15'),
|
|
lieu: 'Perris',
|
|
oaci: 'KPRB',
|
|
aeronef: 'Otter',
|
|
imat: 'N123DZ',
|
|
hauteur: 4000,
|
|
voile: 'Pilot',
|
|
taille: 188,
|
|
categorie: 'Formation',
|
|
module: 'RW',
|
|
participants: 4,
|
|
author: TEST_USER_ID,
|
|
});
|
|
});
|
|
|
|
afterAll(async () => {
|
|
await disconnectMongo();
|
|
});
|
|
|
|
describe('GET /api/skydive/jumps — auth protection', () => {
|
|
it('returns 401 without token', async () => {
|
|
const res = await request(app).get('/api/skydive/jumps');
|
|
expect(res.status).toBe(401);
|
|
});
|
|
|
|
it('returns 401 with an invalid token', async () => {
|
|
const res = await request(app)
|
|
.get('/api/skydive/jumps')
|
|
.set('Authorization', 'Token bad.token.value');
|
|
expect(res.status).toBe(401);
|
|
});
|
|
});
|
|
|
|
describe('GET /api/skydive/jumps — list', () => {
|
|
it('returns jump list with correct structure', async () => {
|
|
const res = await request(app)
|
|
.get('/api/skydive/jumps')
|
|
.set('Authorization', `Token ${token}`);
|
|
|
|
expect(res.status).toBe(200);
|
|
expect(res.body).toHaveProperty('jumps');
|
|
expect(res.body).toHaveProperty('jumpsCount');
|
|
expect(Array.isArray(res.body.jumps)).toBe(true);
|
|
});
|
|
|
|
it('includes the seeded jump', async () => {
|
|
const res = await request(app)
|
|
.get('/api/skydive/jumps')
|
|
.set('Authorization', `Token ${token}`);
|
|
|
|
expect(res.body.jumpsCount).toBeGreaterThanOrEqual(1);
|
|
const found = res.body.jumps.find(j => j.numero === 9001);
|
|
expect(found).toBeDefined();
|
|
expect(found.lieu).toBe('Perris');
|
|
});
|
|
|
|
it('filters by title search', async () => {
|
|
const res = await request(app)
|
|
.get('/api/skydive/jumps?title=nomatch_xyz')
|
|
.set('Authorization', `Token ${token}`);
|
|
|
|
expect(res.status).toBe(200);
|
|
expect(res.body.jumpsCount).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe('POST /api/skydive/jumps — create', () => {
|
|
it('creates a new jump', async () => {
|
|
const res = await request(app)
|
|
.post('/api/skydive/jumps')
|
|
.set('Authorization', `Token ${token}`)
|
|
.send({
|
|
jump: {
|
|
numero: 9002,
|
|
date: '2024-07-01',
|
|
lieu: 'Empuriabrava',
|
|
oaci: 'LEEC',
|
|
aeronef: 'Caravan',
|
|
hauteur: 4200,
|
|
voile: 'Pilot',
|
|
taille: 188,
|
|
categorie: 'Formation',
|
|
module: 'RW',
|
|
participants: 8,
|
|
}
|
|
});
|
|
|
|
expect(res.status).toBe(200);
|
|
expect(res.body).toHaveProperty('jump');
|
|
expect(res.body.jump.numero).toBe(9002);
|
|
expect(res.body.jump.lieu).toBe('Empuriabrava');
|
|
});
|
|
|
|
it('returns 401 when UserService returns null', async () => {
|
|
UserService.getUserById.mockResolvedValueOnce(null);
|
|
|
|
const res = await request(app)
|
|
.post('/api/skydive/jumps')
|
|
.set('Authorization', `Token ${token}`)
|
|
.send({ jump: { numero: 9999, lieu: 'Test' } });
|
|
|
|
expect(res.status).toBe(401);
|
|
});
|
|
});
|
|
|
|
describe('GET /api/skydive/jumps/:slug — single jump', () => {
|
|
it('returns the jump for its author', async () => {
|
|
const res = await request(app)
|
|
.get(`/api/skydive/jumps/${testJump.slug}`)
|
|
.set('Authorization', `Token ${token}`);
|
|
|
|
expect(res.status).toBe(200);
|
|
expect(res.body.jump.numero).toBe(9001);
|
|
});
|
|
|
|
it('returns 404 for unknown slug', async () => {
|
|
const res = await request(app)
|
|
.get('/api/skydive/jumps/slug-does-not-exist')
|
|
.set('Authorization', `Token ${token}`);
|
|
|
|
expect(res.status).toBe(404);
|
|
});
|
|
|
|
it('returns 403 if authenticated user is not the author', async () => {
|
|
const otherToken = generateToken('b0000000-0000-0000-0000-000000000002', 'other_user');
|
|
UserService.getUserById.mockResolvedValueOnce({
|
|
id: 'b0000000-0000-0000-0000-000000000002',
|
|
username: 'other_user',
|
|
});
|
|
|
|
const res = await request(app)
|
|
.get(`/api/skydive/jumps/${testJump.slug}`)
|
|
.set('Authorization', `Token ${otherToken}`);
|
|
|
|
expect(res.status).toBe(403);
|
|
});
|
|
});
|
|
|
|
describe('PUT /api/skydive/jumps/:slug — update', () => {
|
|
it('updates the jump', async () => {
|
|
const res = await request(app)
|
|
.put(`/api/skydive/jumps/${testJump.slug}`)
|
|
.set('Authorization', `Token ${token}`)
|
|
.send({ jump: { lieu: 'Spa-La-Sauvenière' } });
|
|
|
|
expect(res.status).toBe(200);
|
|
expect(res.body.jump.lieu).toBe('Spa-La-Sauvenière');
|
|
});
|
|
|
|
it('returns 403 if not the author', async () => {
|
|
const otherToken = generateToken('b0000000-0000-0000-0000-000000000002', 'other_user');
|
|
UserService.getUserById.mockResolvedValueOnce({
|
|
id: 'b0000000-0000-0000-0000-000000000002',
|
|
username: 'other_user',
|
|
});
|
|
|
|
const res = await request(app)
|
|
.put(`/api/skydive/jumps/${testJump.slug}`)
|
|
.set('Authorization', `Token ${otherToken}`)
|
|
.send({ jump: { lieu: 'Hacker' } });
|
|
|
|
expect(res.status).toBe(403);
|
|
});
|
|
});
|
|
|
|
describe('DELETE /api/skydive/jumps/:slug — delete', () => {
|
|
it('returns 403 if not the author', async () => {
|
|
const otherToken = generateToken('b0000000-0000-0000-0000-000000000002', 'other_user');
|
|
UserService.getUserById.mockResolvedValueOnce({
|
|
id: 'b0000000-0000-0000-0000-000000000002',
|
|
username: 'other_user',
|
|
});
|
|
|
|
const res = await request(app)
|
|
.delete(`/api/skydive/jumps/${testJump.slug}`)
|
|
.set('Authorization', `Token ${otherToken}`);
|
|
|
|
expect(res.status).toBe(403);
|
|
});
|
|
|
|
it('deletes the jump if author', async () => {
|
|
const Jump = mongoose.model('Jump');
|
|
const toDelete = await Jump.create({
|
|
numero: 9003,
|
|
date: new Date(),
|
|
lieu: 'ToDelete',
|
|
autor: TEST_USER_ID,
|
|
author: TEST_USER_ID,
|
|
});
|
|
|
|
const res = await request(app)
|
|
.delete(`/api/skydive/jumps/${toDelete.slug}`)
|
|
.set('Authorization', `Token ${token}`);
|
|
|
|
expect(res.status).toBe(200);
|
|
expect(res.body.deleted).toBe(true);
|
|
});
|
|
});
|