feat(tests): add Jest+Supertest infrastructure with jump route suite (14 tests)
- 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
This commit is contained in:
@@ -3,31 +3,24 @@ if (appEnv == undefined) {
|
|||||||
appEnv = 'development';
|
appEnv = 'development';
|
||||||
}
|
}
|
||||||
require('dotenv').config({ path: `config/env/.env.${appEnv}` });
|
require('dotenv').config({ path: `config/env/.env.${appEnv}` });
|
||||||
|
|
||||||
const { promisify } = require('util'),
|
const { promisify } = require('util'),
|
||||||
chalk = require('chalk');
|
chalk = require('chalk'),
|
||||||
//bodyParser = require('body-parser')
|
|
||||||
var express = require('express'),
|
|
||||||
session = require('express-session'),
|
|
||||||
cors = require('cors'),
|
|
||||||
errorhandler = require('errorhandler'),
|
|
||||||
morgan = require('morgan');
|
morgan = require('morgan');
|
||||||
|
|
||||||
const DateUtilities = require('./src/utils/dateUtilities'),
|
const DateUtilities = require('./src/utils/dateUtilities'),
|
||||||
exceptionHandler = require('./src/middlewares/exceptions.handler'),
|
exceptionHandler = require('./src/middlewares/exceptions.handler'),
|
||||||
connectDb = require('./src/database/connectDb'),
|
connectDb = require('./src/database/connectDb'),
|
||||||
config = require('./src/config'),
|
config = require('./src/config'),
|
||||||
swaggerUi = require('swagger-ui-express'),
|
createApp = require('./src/createApp');
|
||||||
swaggerSpec = require('./src/config/swagger');
|
|
||||||
|
|
||||||
// Create global app object
|
const app = createApp();
|
||||||
var app = express();
|
|
||||||
app.use(cors());
|
|
||||||
|
|
||||||
// Morgan status code color config
|
// Morgan status code color config
|
||||||
morgan.token('statusColor', (req, res) => {
|
morgan.token('statusColor', (req, res) => {
|
||||||
let status = (typeof res.headersSent !== 'boolean' ? Boolean(res.header) : res.headersSent)
|
const status = (typeof res.headersSent !== 'boolean' ? Boolean(res.header) : res.headersSent)
|
||||||
? res.statusCode
|
? res.statusCode
|
||||||
: undefined
|
: undefined;
|
||||||
return status >= 500 ? chalk.red(status)
|
return status >= 500 ? chalk.red(status)
|
||||||
: status >= 429 ? chalk.magenta(status)
|
: status >= 429 ? chalk.magenta(status)
|
||||||
: status >= 400 ? chalk.yellow(status)
|
: status >= 400 ? chalk.yellow(status)
|
||||||
@@ -35,66 +28,32 @@ morgan.token('statusColor', (req, res) => {
|
|||||||
: status >= 200 ? chalk.green(status)
|
: status >= 200 ? chalk.green(status)
|
||||||
: chalk.underline(status);
|
: chalk.underline(status);
|
||||||
});
|
});
|
||||||
// Morgan log line config
|
|
||||||
app.use(morgan('[:date[iso]] ":method :url HTTP/:http-version" :statusColor :res[content-length] ":referrer" ":user-agent" :remote-addr'));
|
app.use(morgan('[:date[iso]] ":method :url HTTP/:http-version" :statusColor :res[content-length] ":referrer" ":user-agent" :remote-addr'));
|
||||||
|
|
||||||
//use express.json() to parse incoming requests with JSON payloads
|
|
||||||
app.use(express.json());
|
|
||||||
//parse incoming requests with urlencoded payloads
|
|
||||||
app.use(express.urlencoded({ extended: true }));
|
|
||||||
|
|
||||||
//app.use(bodyParser.urlencoded({ extended: false }));
|
|
||||||
//app.use(bodyParser.json());
|
|
||||||
|
|
||||||
app.use(require('method-override')());
|
|
||||||
app.use(express.static(__dirname + '/public'));
|
|
||||||
|
|
||||||
app.use(session({ secret: config.secret, cookie: { maxAge: config.session_lifetime }, resave: false, saveUninitialized: false }));
|
|
||||||
|
|
||||||
if (appEnv !== 'production') {
|
|
||||||
app.use(errorhandler());
|
|
||||||
}
|
|
||||||
|
|
||||||
require('./src/database/models/mongo');
|
require('./src/database/models/mongo');
|
||||||
config.initAuth();
|
config.initAuth();
|
||||||
//config.initApiKey();
|
|
||||||
|
|
||||||
// Setup Swagger documentation
|
const setupRoutes = (app) => {
|
||||||
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec, {
|
|
||||||
swaggerOptions: {
|
|
||||||
persistAuthorization: true,
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Register routes AFTER connections are established
|
|
||||||
const setupRoutes = () => {
|
|
||||||
app.use(require('./src/routes'));
|
app.use(require('./src/routes'));
|
||||||
|
|
||||||
// Exception handlers MUST be AFTER routes
|
|
||||||
app.use(exceptionHandler.notFoundErrorHandler);
|
app.use(exceptionHandler.notFoundErrorHandler);
|
||||||
app.use(exceptionHandler.logErrorHandler);
|
app.use(exceptionHandler.logErrorHandler);
|
||||||
//app.use(exceptionHandler.clientErrorHandler);
|
|
||||||
app.use(exceptionHandler.fianlErrorHandler);
|
app.use(exceptionHandler.fianlErrorHandler);
|
||||||
};
|
};
|
||||||
|
|
||||||
const startServer = async () => {
|
const startServer = async () => {
|
||||||
try {
|
try {
|
||||||
// Initialize database connections and create relationships FIRST
|
|
||||||
await connectDb.connectAllDb();
|
await connectDb.connectAllDb();
|
||||||
console.log(`${DateUtilities.getDateTimeISOString()}`, chalk.green('All database connections established, relationships loaded'));
|
console.log(`${DateUtilities.getDateTimeISOString()}`, chalk.green('All database connections established, relationships loaded'));
|
||||||
|
|
||||||
// NOW register the routes after connections are ready
|
setupRoutes(app);
|
||||||
setupRoutes();
|
|
||||||
|
|
||||||
// Start the HTTP server
|
|
||||||
const port = process.env.SERVER_PORT || 3200;
|
const port = process.env.SERVER_PORT || 3200;
|
||||||
await promisify(app.listen).bind(app)(port);
|
await promisify(app.listen).bind(app)(port);
|
||||||
console.log(`${DateUtilities.getDateTimeISOString()}`, chalk.green(`${process.env.APP_ENV} API server listening on port ${port}`));
|
console.log(`${DateUtilities.getDateTimeISOString()}`, chalk.green(`${appEnv} API server listening on port ${port}`));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`${DateUtilities.getDateTimeISOString()}`, chalk.red('Failed to start server:'), error);
|
console.error(`${DateUtilities.getDateTimeISOString()}`, chalk.red('Failed to start server:'), error);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
// finally, let's start our server...
|
|
||||||
startServer();
|
startServer();
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
module.exports = {
|
||||||
|
testEnvironment: 'node',
|
||||||
|
setupFiles: ['./tests/jest.env.js'],
|
||||||
|
testMatch: ['**/tests/api/**/*.test.js'],
|
||||||
|
testTimeout: 15000,
|
||||||
|
maxWorkers: 1,
|
||||||
|
verbose: true,
|
||||||
|
forceExit: true,
|
||||||
|
};
|
||||||
Generated
+3707
-92
File diff suppressed because it is too large
Load Diff
+5
-2
@@ -11,7 +11,8 @@
|
|||||||
"start": "APP_ENV=production node ./app.js",
|
"start": "APP_ENV=production node ./app.js",
|
||||||
"dev": "APP_ENV=development node ./app.js",
|
"dev": "APP_ENV=development node ./app.js",
|
||||||
"local": "APP_ENV=local nodemon ./app.js",
|
"local": "APP_ENV=local nodemon ./app.js",
|
||||||
"test": "newman run ./tests/adastra-api-tests.postman_collection.json -e ./tests/adastra-api-tests.postman_environment.json",
|
"test": "jest",
|
||||||
|
"test:postman": "newman run ./tests/adastra-api-tests.postman_collection.json -e ./tests/adastra-api-tests.postman_environment.json",
|
||||||
"stop": "lsof -ti :3200 | xargs kill",
|
"stop": "lsof -ti :3200 | xargs kill",
|
||||||
"mongo:start": "docker run --name hu-mongo -p 27017:27017 mongo & sleep 5",
|
"mongo:start": "docker run --name hu-mongo -p 27017:27017 mongo & sleep 5",
|
||||||
"mongo:stop": "docker stop hu-mongo && docker rm hu-mongo",
|
"mongo:stop": "docker stop hu-mongo && docker rm hu-mongo",
|
||||||
@@ -59,8 +60,10 @@
|
|||||||
"@eslint/js": "^9.0.0",
|
"@eslint/js": "^9.0.0",
|
||||||
"eslint": "^9.0.0",
|
"eslint": "^9.0.0",
|
||||||
"globals": "^15.0.0",
|
"globals": "^15.0.0",
|
||||||
|
"jest": "^30.3.0",
|
||||||
"newman": "^5.3.2",
|
"newman": "^5.3.2",
|
||||||
"nodemon": "^2.0.22",
|
"nodemon": "^2.0.22",
|
||||||
"sequelize-cli": "^6.6.2"
|
"sequelize-cli": "^6.6.2",
|
||||||
|
"supertest": "^7.2.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
var express = require('express'),
|
||||||
|
session = require('express-session'),
|
||||||
|
cors = require('cors'),
|
||||||
|
errorhandler = require('errorhandler');
|
||||||
|
|
||||||
|
const config = require('./config'),
|
||||||
|
swaggerUi = require('swagger-ui-express'),
|
||||||
|
swaggerSpec = require('./config/swagger');
|
||||||
|
|
||||||
|
function createApp() {
|
||||||
|
const app = express();
|
||||||
|
app.use(cors());
|
||||||
|
app.use(express.json());
|
||||||
|
app.use(express.urlencoded({ extended: true }));
|
||||||
|
app.use(require('method-override')());
|
||||||
|
app.use(express.static(__dirname + '/../public'));
|
||||||
|
app.use(session({
|
||||||
|
secret: config.secret,
|
||||||
|
cookie: { maxAge: config.session_lifetime },
|
||||||
|
resave: false,
|
||||||
|
saveUninitialized: false
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (process.env.APP_ENV !== 'production') {
|
||||||
|
app.use(errorhandler());
|
||||||
|
}
|
||||||
|
|
||||||
|
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec, {
|
||||||
|
swaggerOptions: { persistAuthorization: true },
|
||||||
|
}));
|
||||||
|
|
||||||
|
return app;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = createApp;
|
||||||
@@ -5,7 +5,7 @@ var AeronefSchema = new mongoose.Schema({
|
|||||||
slug: { type: String, lowercase: true, unique: true },
|
slug: { type: String, lowercase: true, unique: true },
|
||||||
aeronef: String,
|
aeronef: String,
|
||||||
imat: String,
|
imat: String,
|
||||||
author: { type: mongoose.Schema.Types.UUID, ref: 'User' }
|
author: { type: String }
|
||||||
}, {timestamps: true, toJSON: {virtuals: true}});
|
}, {timestamps: true, toJSON: {virtuals: true}});
|
||||||
|
|
||||||
AeronefSchema.pre('validate', function (next) {
|
AeronefSchema.pre('validate', function (next) {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ var ApplicationSchema = new mongoose.Schema({
|
|||||||
slug: { type: String, lowercase: true, unique: true },
|
slug: { type: String, lowercase: true, unique: true },
|
||||||
title: String,
|
title: String,
|
||||||
description: String,
|
description: String,
|
||||||
author: { type: mongoose.Schema.Types.UUID, ref: 'User' }
|
author: { type: String }
|
||||||
}, { timestamps: true, toJSON: { virtuals: true } });
|
}, { timestamps: true, toJSON: { virtuals: true } });
|
||||||
|
|
||||||
ApplicationSchema.plugin(uniqueValidator, { message: 'is already taken' });
|
ApplicationSchema.plugin(uniqueValidator, { message: 'is already taken' });
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ var CanopySchema = new mongoose.Schema({
|
|||||||
slug: { type: String, lowercase: true, unique: true },
|
slug: { type: String, lowercase: true, unique: true },
|
||||||
voile: String,
|
voile: String,
|
||||||
taille: Number,
|
taille: Number,
|
||||||
author: { type: mongoose.Schema.Types.UUID, ref: 'User' }
|
author: { type: String }
|
||||||
}, {timestamps: true, toJSON: {virtuals: true}});
|
}, {timestamps: true, toJSON: {virtuals: true}});
|
||||||
|
|
||||||
CanopySchema.pre('validate', function (next) {
|
CanopySchema.pre('validate', function (next) {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ var DropzoneSchema = new mongoose.Schema({
|
|||||||
slug: { type: String, lowercase: true, unique: true },
|
slug: { type: String, lowercase: true, unique: true },
|
||||||
lieu: String,
|
lieu: String,
|
||||||
oaci: String,
|
oaci: String,
|
||||||
author: { type: mongoose.Schema.Types.UUID, ref: 'User' }
|
author: { type: String }
|
||||||
}, {timestamps: true, toJSON: {virtuals: true}});
|
}, {timestamps: true, toJSON: {virtuals: true}});
|
||||||
|
|
||||||
DropzoneSchema.pre('validate', function (next) {
|
DropzoneSchema.pre('validate', function (next) {
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ var JumpSchema = new mongoose.Schema({
|
|||||||
video: String,
|
video: String,
|
||||||
files: [{ type: mongoose.Schema.Types.ObjectId, ref: 'JumpFile' }],
|
files: [{ type: mongoose.Schema.Types.ObjectId, ref: 'JumpFile' }],
|
||||||
x2data: { type: mongoose.Schema.Types.ObjectId, ref: 'X2DataLog' },
|
x2data: { type: mongoose.Schema.Types.ObjectId, ref: 'X2DataLog' },
|
||||||
author: { type: mongoose.Schema.Types.UUID, ref: 'User' }
|
author: { type: String }
|
||||||
}, {timestamps: true, toJSON: {virtuals: true}});
|
}, {timestamps: true, toJSON: {virtuals: true}});
|
||||||
|
|
||||||
JumpSchema.plugin(uniqueValidator, { message: 'is already taken' });
|
JumpSchema.plugin(uniqueValidator, { message: 'is already taken' });
|
||||||
@@ -79,7 +79,9 @@ JumpSchema.methods.toJSONFor = function(user) {
|
|||||||
x2data: this.x2data ? this.x2data.toJSONFor() : null,
|
x2data: this.x2data ? this.x2data.toJSONFor() : null,
|
||||||
createdAt: this.createdAt,
|
createdAt: this.createdAt,
|
||||||
updatedAt: this.updatedAt,
|
updatedAt: this.updatedAt,
|
||||||
author: this.author.toProfileJSONFor(user)
|
author: this.author && typeof this.author.toProfileJSONFor === 'function'
|
||||||
|
? this.author.toProfileJSONFor(user)
|
||||||
|
: this.author ?? null
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ var JumpFileSchema = new mongoose.Schema({
|
|||||||
enum : ['csv', 'image', 'kml', 'video'],
|
enum : ['csv', 'image', 'kml', 'video'],
|
||||||
default: 'video'
|
default: 'video'
|
||||||
},
|
},
|
||||||
author: { type: mongoose.Schema.Types.UUID, ref: 'User' }
|
author: { type: String }
|
||||||
}, {timestamps: true, toJSON: {virtuals: true}});
|
}, {timestamps: true, toJSON: {virtuals: true}});
|
||||||
|
|
||||||
JumpFileSchema.plugin(uniqueValidator, { message: 'is already taken' });
|
JumpFileSchema.plugin(uniqueValidator, { message: 'is already taken' });
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ var X2DataLogSchema = new mongoose.Schema({
|
|||||||
],
|
],
|
||||||
date: String,
|
date: String,
|
||||||
createdOn: String,
|
createdOn: String,
|
||||||
author: { type: mongoose.Schema.Types.UUID, ref: 'User' }
|
author: { type: String }
|
||||||
}, {timestamps: true, toJSON: {virtuals: true}});
|
}, {timestamps: true, toJSON: {virtuals: true}});
|
||||||
|
|
||||||
X2DataLogSchema.methods.toJSONFor = function() {
|
X2DataLogSchema.methods.toJSONFor = function() {
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ var HWClanSchema = new mongoose.Schema({
|
|||||||
title: String,
|
title: String,
|
||||||
topActivity: String,
|
topActivity: String,
|
||||||
topDungeon: String,
|
topDungeon: String,
|
||||||
author: { type: mongoose.Schema.Types.UUID, ref: 'User' }
|
author: { type: String }
|
||||||
}, {timestamps: true, toJSON: {virtuals: true}});
|
}, {timestamps: true, toJSON: {virtuals: true}});
|
||||||
|
|
||||||
HWClanSchema.plugin(uniqueValidator, { message: 'is already taken' });
|
HWClanSchema.plugin(uniqueValidator, { message: 'is already taken' });
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ var HWMemberSchemaOld = new mongoose.Schema({
|
|||||||
scoreGifts: Number,
|
scoreGifts: Number,
|
||||||
warGifts: Number,
|
warGifts: Number,
|
||||||
rewards: Number,
|
rewards: Number,
|
||||||
author: { type: mongoose.Schema.Types.UUID, ref: 'User' }
|
author: { type: String }
|
||||||
}, {timestamps: true, toJSON: {virtuals: true}});
|
}, {timestamps: true, toJSON: {virtuals: true}});
|
||||||
*/
|
*/
|
||||||
var HWMemberSchema = new mongoose.Schema({
|
var HWMemberSchema = new mongoose.Schema({
|
||||||
@@ -104,7 +104,7 @@ var HWMemberSchema = new mongoose.Schema({
|
|||||||
scoreGifts: Number,
|
scoreGifts: Number,
|
||||||
warGifts: Number,
|
warGifts: Number,
|
||||||
rewards: Number,
|
rewards: Number,
|
||||||
author: { type: mongoose.Schema.Types.UUID, ref: 'User' }
|
author: { type: String }
|
||||||
}, {timestamps: true, toJSON: {virtuals: true}});
|
}, {timestamps: true, toJSON: {virtuals: true}});
|
||||||
|
|
||||||
HWMemberSchema.plugin(uniqueValidator, { message: 'is already taken' });
|
HWMemberSchema.plugin(uniqueValidator, { message: 'is already taken' });
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ const exceptionHandler = {
|
|||||||
},
|
},
|
||||||
// eslint-disable-next-line no-unused-vars
|
// eslint-disable-next-line no-unused-vars
|
||||||
fianlErrorHandler: (err, req, res, next) => {
|
fianlErrorHandler: (err, req, res, next) => {
|
||||||
res.status(err.statusCode || 500);
|
res.status(err.statusCode || err.status || 500);
|
||||||
if (!isProduction) {
|
if (!isProduction) {
|
||||||
/* development error handler will print stacktrace */
|
/* development error handler will print stacktrace */
|
||||||
res.json({errors: {
|
res.json({errors: {
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ router.get('/', auth.required, function (req, res, next) {
|
|||||||
.sort({ createdAt: 'desc' })
|
.sort({ createdAt: 'desc' })
|
||||||
.populate('author')
|
.populate('author')
|
||||||
.exec(),
|
.exec(),
|
||||||
Application.count(query).exec(),
|
Application.countDocuments(query).exec(),
|
||||||
req.payload ? UserService.getUserById(req.payload.id) : null
|
req.payload ? UserService.getUserById(req.payload.id) : null
|
||||||
]).then(function (results) {
|
]).then(function (results) {
|
||||||
let applications = results[0];
|
let applications = results[0];
|
||||||
|
|||||||
@@ -39,13 +39,12 @@ router.get('/', auth.required, function (req, res, next) {
|
|||||||
offset = req.query.offset;
|
offset = req.query.offset;
|
||||||
}
|
}
|
||||||
if (typeof req.query.title !== 'undefined') {
|
if (typeof req.query.title !== 'undefined') {
|
||||||
const rgx = new RegExp(`.*${req.query.title}.*`);
|
|
||||||
query = {
|
query = {
|
||||||
$or: [
|
$or: [
|
||||||
{ title: { $regex: rgx, $options: "i" } },
|
{ slug: { $regex: req.query.title, $options: 'i' } },
|
||||||
{ slug: { $regex: rgx, $options: "i" } },
|
{ lieu: { $regex: req.query.title, $options: 'i' } },
|
||||||
],
|
],
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
if (typeof req.query.numeroRangeStart !== 'undefined' && typeof req.query.numeroRangeEnd !== 'undefined') {
|
if (typeof req.query.numeroRangeStart !== 'undefined' && typeof req.query.numeroRangeEnd !== 'undefined') {
|
||||||
query.numero = {
|
query.numero = {
|
||||||
@@ -103,7 +102,7 @@ router.get('/', auth.required, function (req, res, next) {
|
|||||||
.populate('files')
|
.populate('files')
|
||||||
.populate('x2data')
|
.populate('x2data')
|
||||||
.exec(),
|
.exec(),
|
||||||
Jump.count(query).exec(),
|
Jump.countDocuments(query).exec(),
|
||||||
req.payload ? UserService.getUserById(req.payload.id) : null
|
req.payload ? UserService.getUserById(req.payload.id) : null
|
||||||
]).then(function (results) {
|
]).then(function (results) {
|
||||||
let jumps = results[0];
|
let jumps = results[0];
|
||||||
|
|||||||
@@ -0,0 +1,236 @@
|
|||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
const jwt = require('jsonwebtoken');
|
||||||
|
|
||||||
|
const TEST_USER_ID = 'a0000000-0000-0000-0000-000000000001';
|
||||||
|
const TEST_USERNAME = 'test_user';
|
||||||
|
|
||||||
|
function generateToken(userId = TEST_USER_ID, username = TEST_USERNAME) {
|
||||||
|
const secret = process.env.SECRET;
|
||||||
|
const exp = Math.floor(Date.now() / 1000) + 86400;
|
||||||
|
return jwt.sign({ id: userId, username, exp }, secret, { algorithm: 'HS256' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const mockUser = {
|
||||||
|
id: TEST_USER_ID,
|
||||||
|
username: TEST_USERNAME,
|
||||||
|
email: 'test@example.com',
|
||||||
|
role: 'Admin',
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = { generateToken, mockUser, TEST_USER_ID, TEST_USERNAME };
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
const mongoose = require('mongoose');
|
||||||
|
const createApp = require('../../src/createApp');
|
||||||
|
const config = require('../../src/config');
|
||||||
|
const exceptionHandler = require('../../src/middlewares/exceptions.handler');
|
||||||
|
|
||||||
|
async function connectMongo() {
|
||||||
|
require('../../src/database/models/mongo');
|
||||||
|
await mongoose.connect(process.env.MONGODB_URI);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function disconnectMongo() {
|
||||||
|
await mongoose.connection.dropDatabase();
|
||||||
|
await mongoose.connection.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildApp(routePrefix, routeModule) {
|
||||||
|
config.initAuth();
|
||||||
|
const app = createApp();
|
||||||
|
app.use(routePrefix, routeModule);
|
||||||
|
app.use(exceptionHandler.notFoundErrorHandler);
|
||||||
|
app.use(exceptionHandler.logErrorHandler);
|
||||||
|
app.use(exceptionHandler.fianlErrorHandler);
|
||||||
|
return app;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { connectMongo, disconnectMongo, buildApp };
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
process.env.APP_ENV = 'test';
|
||||||
|
require('dotenv').config({ path: 'config/env/.env.test' });
|
||||||
Reference in New Issue
Block a user