diff --git a/.gitignore b/.gitignore index 6efc65d90cff64414b7d1fda48ee0570a7727299..c1ac0278e1fa1c390503f3f8ff11b396a5a6645e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ node_modules/ package-lock.json .env -.vscode/ \ No newline at end of file +.vscode/ +.nyc_output \ No newline at end of file diff --git a/app.js b/app.js index 37888bdfff393e34c53a51dd93c09868edf59b83..ace05dc1b3762f670805a908b67ba7b3d5160038 100644 --- a/app.js +++ b/app.js @@ -18,7 +18,9 @@ const mongoose = require('mongoose'); require('./src/helpers').addHelpers(); require('dotenv').config(); -mongoose.connect(process.env.DB_CONNECTION_STRING, { +const connectionString = + JSON.parse(process.env.DB_CONTEXT)[process.env.NODE_ENV || 'prod']; +mongoose.connect(connectionString, { useNewUrlParser: true, useUnifiedTopology: true, useFindAndModify: false, diff --git a/package.json b/package.json index a3bd53fb69ad3402eba91101c6fd214f69cb37b2..8a9d8a12e9b306382ef0b4aae07d6903099fcfab 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,8 @@ "version": "0.0.0", "private": true, "scripts": { - "start": "node ./bin/www" + "start": "node ./bin/www", + "test": "mocha --exit --timeout 5000" }, "dependencies": { "connect-flash": "^0.1.1", @@ -20,9 +21,14 @@ "passport-local": "^1.0.0" }, "devDependencies": { + "chai": "^4.2.0", + "chai-http": "^4.3.0", "eslint": "^6.6.0", "eslint-config-google": "^0.14.0", "http-errors": "^1.6.3", + "mocha": "^6.2.2", + "node-html-parser": "^1.1.16", + "nyc": "^14.1.1", "prettier": "^1.18.2" } } diff --git a/routes/ads.js b/routes/ads.js index de4c4df8ae708a41c3f4f4f3b6b6cf191c3eaf5c..7f8b76d8b7d2c5dcf2977b9d3e6928e1d862eab3 100644 --- a/routes/ads.js +++ b/routes/ads.js @@ -10,76 +10,76 @@ const upload = multer({ }); /* GET to get to get to the ad creation form */ -router - .get('/create', authorize('agent'), function(req, res) { - res.render('ads/create'); - }) - .post('/create', authorize('agent'), upload.array('pictures', 3), function( - req, - res, - ) { - const body = req.body; - const id = body.id; - - const formData = { - title: body.title, - type: body.type, - transactionStatus: body.transactionStatus, - price: body.price.replace(',', '.'), - published: body.published === 'on', - description: body.description, - availabilityDate: - body.availabilityDate === '' ? null : body.availabilityDate, - }; +router.get('/create', authorize('agent'), function(req, res, next) { + res.render('ads/create'); +}) + .post('/create', authorize('agent'), + upload.array('pictures', 3), function(req, res, next) { + const body = req.body; + const id = body.id; + + const formData = { + title: body.title, + type: body.type, + transactionStatus: body.transactionStatus, + price: body.price && body.price.replace(',', '.'), + published: body.published === 'on', + description: body.description, + availabilityDate: body.availabilityDate === '' ? + null : body.availabilityDate, + }; - if (req.files.length) { - formData.pictures = req.files.map((f) => ({ - name: f.fieldName, - body: f.buffer, - })); - } - - if (id) { - // Peut-être charger l'objet en amont et le retourner si erreur ? - adModel.Ad.updateOne({_id: id}, {$set: formData}) - .exec() - .then(() => { - req.flash('success', 'Mise à jour réussie !'); - res.redirect('/ads/'); - }) - .catch((reason) => { - res.render('ads/create', reason); - }); - } else { - const newAd = new adModel.Ad(formData); + errCallback = (reason) => res.status(406).render('ads/create', reason); - newAd - .save() - .then(() => { - req.flash('success', 'Mise à jour réussie !'); - res.redirect('/ads/'); - }) - .catch((reason) => { - console.error(reason); - res.render('ads/create', reason); - }); - } - }) + if (req.files && req.files.length) { + formData.pictures = req.files.map((f) => ({ + name: f.fieldName, + body: f.buffer, + })); + } + + if (id) { + // Peut-être charger l'objet en amont et le retourner si erreur ? + adModel.Ad.updateOne({_id: id}, {$set: formData}).exec() + .then((value) => { + req.flash('success', 'Mise à jour réussie !'); + res.redirect('/ads/'); + }) + .catch((reason) => { + errCallback(reason); + }); + } else { + const newAd = new adModel.Ad(formData); + + newAd.save() + .then((value) => { + res.redirect('/ads/'); + }) + .catch((reason) => { + errCallback(reason); + }); + } + }) .get('/update/:id', authorize('agent'), function(req, res, next) { const id = req.params.id; + let status; adModel.Ad.findOne({_id: id}, {'pictures.body': 0, 'questions': 0}, function(err, ad) { const errors = []; if (err) { errors.push(err.message); - } else if (!ad) { + } + if (!ad) { + status = 404; errors.push('L\'annonce cherchée n\'a pas été trouvée'); } else { + status = 200; req.flash('success', 'Mise à jour réussie !'); } - res.render('ads/create', {ad: ad, errors_update: errors}); + res.status(status) + .render('ads/create', {ad: ad, errors_update: errors}); }); }) .get('/delete/:id', authorize('agent'), deleteAdAction) @@ -100,17 +100,15 @@ router }, ) .then((ad) => { - res.status(201); req.flash('success', 'Question publiée.'); res.redirect('/ads/' + ad.id); }) .catch((err) => { - console.error(err); req.flash( 'error', 'Une erreur est survenue lors du chargement des données.', ); - res.redirect('/ads/' + ad.id); + res.redirect(`/ads/${ad.id}`); }); }) .post( @@ -135,9 +133,8 @@ router }, ) .then((ad) => { - res.status(201); req.flash('success', 'Réponse publiée.'); - res.redirect('/ads/' + ad.id); + res.redirect(201, `/ads/${ad.id}`); }) .catch((err) => { console.error(err); @@ -145,7 +142,7 @@ router 'error', 'Une erreur est survenue lors du chargement des données.', ); - res.redirect('/ads/' + ad.id); + res.redirect(406, `/ads/${ad.id}`); }); }, ) @@ -205,17 +202,14 @@ function renderAdAction(published) { res.render('ads/show', {ad}); } else { req.flash('error', 'L\'annonce demandé n\'a pas été trouvé'); - res.status(404); res.redirect('/'); } }) .catch((err) => { - console.error(err); req.flash( - 'error', + 'log', 'Un problème est survenu lors du chargement des données', ); - res.status(500); res.redirect('/'); }); }; @@ -231,13 +225,17 @@ function deleteAdAction(req, res, next) { const id = req.params.id; adModel.Ad.deleteOne({_id: id}) - .then(() => { - req.flash('success', 'L\'annonce a bien été supprimée'); + .then((value) => { + if (value.n !== 0) { + req.flash('success', 'L\'annonce a bien été supprimée'); + } else { + req.flash('info', 'L\'annonce n\'a pas été trouvée'); + } }) .catch((reason) => { req.flash('error', 'L\'annonce n\'a pas pu être supprimée'); }) - .finally(() => res.redirect(303, '/ads/')); + .finally(() => res.redirect('/ads/')); } module.exports = router; diff --git a/routes/auth.js b/routes/auth.js index bdb5174f1021f4b9daee1149c220baacf2c5f075..eff9aabe56034cc7d2dd88f0fd053dd33cdcd043 100644 --- a/routes/auth.js +++ b/routes/auth.js @@ -44,13 +44,26 @@ router .get('/login', function(req, res, next) { res.render('login'); }) - .post( - '/login', - passport.authenticate('local', { - failureRedirect: '/login', - successRedirect: '/', - }), - ) + .post('/login', function(req, res, next) { + passport.authenticate('local', function(err, user, info) { + if (err) { + return next(err); + } + + if (!user) { + req.flash('error', 'Ce compte n\'est pas enregistré'); + return res.redirect('/login'); + } + + req.logIn(user, function(err) { + if (err) { + return next(err); + } + + return res.redirect('/ads/'); + }); + })(req, res, next); + }) .get('/logout', function(req, res) { req.logout(); res.redirect('/'); diff --git a/src/permissions.js b/src/permissions.js index b8020d7c23a471efc0d3e52af74601a212472793..30eab4ff79acab6e23c65f4557576f4d3a50397b 100644 --- a/src/permissions.js +++ b/src/permissions.js @@ -20,8 +20,7 @@ function authorize(level) { if (!requiredPermissionLevel) { console.error(`checkPermissions: Le rôle ${level} n'existe pas !`); req.flash('log', 'Erreur dev : Rôle non défini (' + level + ')'); - res.status(401); - res.redirect('/'); + res.redirect(409, '/'); return; } diff --git a/test/testAds.js b/test/testAds.js new file mode 100644 index 0000000000000000000000000000000000000000..4826681786b5126f931e1d054fd47c5214cd4cf3 --- /dev/null +++ b/test/testAds.js @@ -0,0 +1,322 @@ +const adModel = require('../models/ad'); +const userModel = require('../models/user'); + +const chai = require('chai'); +const chaiHttp = require('chai-http'); +const htmlParser = require('node-html-parser'); + +// mocha needs it +// eslint-disable-next-line no-unused-vars +const should = chai.should(); + +process.env.NODE_ENV = 'test'; +require('dotenv').config(); +const server = require('../app'); + +chai.use(chaiHttp); + +describe('Les annonces', () => { + const agent = chai.request.agent(server); + const agentData = { + name: 'agent', + role: 'agent', + email: 'test@agent.test.fr', + password: 'Test123!', + }; + + before(async () => { + await adModel.Ad.deleteMany({}, (err) => { + if (err) { + console.error(err); + } + }).exec(); + + await userModel.User.deleteMany({}, (err) => { + if (err) { + console.error(err); + } + }).exec(); + + await agent + .post('/register') + .send(agentData); + + await agent + .post('/login') + .send({ + username: agentData.email, + password: agentData.password, + }); + }); + + const formData = { + '_method': 'put', + 'title': 'Test', + 'type': adModel.AD_TYPE.VALUE.RENTAL, + 'transactionStatus': adModel.AD_TRANSACTIONSTATUS.VALUE.AVAILABLE, + 'price': '420,69', + 'published': undefined, + 'pictures': [], + }; + + // Requête de consultation des annonces + describe('Action de listing des annonces', () => { + it('Supposément obtient les annonces décrites ci-dessous', (done) => { + agent + .get('/ads/') + .end((err, res) => { + if (err) { + chai.assert.fail(err); + } + + res.should.be.html; + res.should.have.status(200); + done(); + }); + }); + }); + + // Requêtes de création d'annonce + describe('Actions de création des annonces', () => { + it('Supposément obtient la page de création d\'annonce', (done) => { + agent + .get('/ads/create') + .end((err, res) => { + if (err) { + chai.assert.fail(err); + } + + res.should.be.html; + res.should.have.status(200); + done(); + }); + }); + + it('Supposément crée une annonce correcte', (done) => { + agent + .post('/ads/create') + .type('form') + .send(formData) + .end((err, res) => { + if (err) { + chai.assert.fail(err); + } + + res.should.have.status(200); + done(); + }); + }); + + it('Supposément rejette l\'annonce ' + + 'car il lui manque des champs requis', (done) => { + const formDataVide = { + '_method': 'put', + 'title': 'Manque des champs obligatoires', + }; + + agent + .post('/ads/create') + .type('form') + .send(formDataVide) + .end((err, res) => { + if (err) { + chai.assert.fail(err); + } + + res.should.be.html; + done(); + }); + }); + + it('Supposément rejette l\'annonce ' + + 'car le champ price pas conforme', (done) => { + const formDataFaux = { + '_method': 'put', + 'title': 'Champ price pas conforme', + 'type': adModel.AD_TYPE.VALUE.RENTAL, + 'transactionStatus': adModel.AD_TRANSACTIONSTATUS.VALUE.AVAILABLE, + 'price': '99999,969', + 'published': 'on', + }; + + agent + .post('/ads/create') + .type('form') + .send(formDataFaux) + .end((err, res) => { + if (err) { + chai.assert.fail(err); + } + + res.should.be.html; + res.should.have.status(406); + done(); + }); + }); + }); + + // Requêtes liées aux questions/réponses + describe('Actions liées aux questions/réponses', () => { + const question = { + name: 'Yoann Eichelberge', + body: 'Y\'a pas d\'arraignéees, t\'es sûr ?', + }; + + let adId = 0; + agent.post('/ads/create').type('form').send(formData) + .end((err, res) => { + if (err) { + chai.assert.fail(err); + } + + adModel.Ad.findOne({}).exec() + .then((value) => { + adId = value && value.id; + }) + .catch((reason) => chai.assert.fail(reason)); + }); + + + it('Supposément crée une question correcte', (done) => { + agent + .post(`/ads/${adId}/questions/create`) + .send(question) + .end((err, res) => { + if (err) { + chai.assert.fail(err); + } + + const html = htmlParser.parse(res.text); + chai.assert.isTrue(html.querySelector('div.msg-success') !== null); + + done(); + }); + }); + + it('Supposément plante car question incorrecte', (done) => { + question.body = null; + + agent + .post(`/ads/${adId}/questions/create`) + .send(question) + .end((err, res) => { + if (err) { + chai.assert.fail(err); + } + + const html = htmlParser.parse(res.text); + chai.assert.isTrue(html.querySelector('div.msg-error') !== null); + + done(); + }); + }); + }); + + // Requêtes de mise à jour d'annonce + describe('Action de mise à jour d\'annonce', () => { + it('Supposément fais bien la mise à jour', (done) => { + adModel.Ad.findOne({}).exec() + .then((value) => { + chai.assert.isTrue(value !== null); + + agent + .get(`/ads/update/${value.id}`) + .end((err, res) => { + if (err) { + chai.assert.fail(err); + } + + res.should.be.html; + res.should.have.status(200); + + formData.title = 'Plus le même'; + formData['id'] = value.id; + + agent + .post('/ads/create') + .send(formData) + .end((err, res) => { + if (err) { + chai.assert.fail(err); + } + + const html = htmlParser.parse(res.text); + chai.assert.isTrue( + html.querySelector('div.msg-success') !== null, + ); + done(); + }); + }); + }) + .catch((reason) => chai.assert.fail(reason)); + }); + + it('Supposément plante vu que l\'id existe pas en fait', (done) => { + agent + .get(`/ads/update/5`) + .end((err, res) => { + if (err) { + chai.assert.fail(err); + } + + res.should.be.html; + res.should.have.status(404); + done(); + }); + }); + }); + + // Requêtes de suppression d'annonce + describe('Action de suppression d\'annonce', () => { + it('Supposément plante vu que l\'id existe pas en fait', (done) => { + agent + .delete(`/ads/delete/aaaaaaaaaaaaaaaaaaaaaaaa`) + .end((err, res) => { + if (err) { + chai.assert.fail(err); + } + + const html = htmlParser.parse(res.text); + chai.assert.isTrue(html.querySelector('div.msg-info') !== null); + + done(); + }); + }); + + it('Supposément supprime bien vu que l\'id existe', (done) => { + adModel.Ad.findOne({}).exec() + .then((value) => { + agent + .delete(`/ads/delete/${value.id}`) + .end((err, res) => { + if (err) { + chai.assert.fail(err); + } + + const html = htmlParser.parse(res.text); + chai.assert.isTrue( + html.querySelector('div.msg-success') !== null, + ); + }); + }) + .catch((reason) => chai.assert.fail(reason)) + .finally(() => done()); + }); + + it('Supposément part en couille ' + + 'vu que l\'id est pas sous le bon format', (done) => { + agent + .delete(`/ads/delete/5`) + .end((err, res) => { + if (err) { + chai.assert.fail(err); + } + + const html = htmlParser.parse(res.text); + chai.assert.isTrue(html.querySelector('div.msg-error') !== null); + + done(); + }); + }); + }); +}); diff --git a/test/testUsers.js b/test/testUsers.js new file mode 100644 index 0000000000000000000000000000000000000000..a3b25ff9232d22896c70753fb9cee8cee315a555 --- /dev/null +++ b/test/testUsers.js @@ -0,0 +1,194 @@ +const userModel = require('../models/user'); + +const chai = require('chai'); +const chaiHttp = require('chai-http'); +const htmlParser = require('node-html-parser'); + +// mocha needs it +// eslint-disable-next-line no-unused-vars +const should = chai.should(); + +process.env.NODE_ENV = 'test'; +require('dotenv').config(); +const server = require('../app'); + +chai.use(chaiHttp); + +describe('Les utilisateurs', () => { + before((done) => { + userModel.User.deleteMany({}, (err) => { + if (err) { + console.error(err); + } + + done(); + }); + }); + + const clientData = { + name: 'Client', + role: 'client', + email: 'client.test@test.test', + password: '123mdr', + }; + + const agentData = { + name: 'Agent', + role: 'agent', + email: 'agent.test@test.test', + password: '123mdr', + }; + + // Requête de consultation des annonces + describe('Action de création d\'utilisateur', () => { + it('Supposément récupère la page de register', (done) => { + chai.request(server) + .get('/register') + .end((err, res) => { + if (err) { + chai.assert.fail(err); + } + + res.should.be.html; + res.should.have.status(200); + done(); + }); + }); + + it('Supposément crée un user client', (done) => { + chai.request(server) + .post('/register') + .type('form') + .send(clientData) + .end((err, res) => { + if (err) { + chai.assert.fail(err); + } + + res.should.have.status(200); + + userModel.User.findOne({email: clientData.email}).exec() + .then((value) => chai.assert.isTrue(value !== null)) + .catch((err) => chai.assert.fail(err)) + .finally(() => done()); + }); + }); + + it('Supposément crée un user agent', (done) => { + chai.request(server) + .post('/register') + .type('form') + .send(agentData) + .end((err, res) => { + if (err) { + chai.assert.fail(err); + } + + res.should.have.status(200); + + userModel.User.findOne({email: agentData.email}).exec() + .then((value) => chai.assert.isTrue(value !== null)) + .catch((err) => chai.assert.fail(err)) + .finally(() => done()); + }); + }); + + it('Supposément refuse de créer car email existe déjà', (done) => { + chai.request.agent(server) + .post('/register') + .type('form') + .send(agentData) + .end((err, res) => { + if (err) { + chai.assert.fail(err); + } + + const html = htmlParser.parse(res.text); + chai.assert.isTrue(html.querySelector('div.msg-info') !== null); + done(); + }); + }); + + it('Supposément refuse de créer car email incorrect', (done) => { + const vraiEmail = agentData.email; + agentData.email = 'null'; + + chai.request.agent(server) + .post('/register') + .type('form') + .send(agentData) + .end((err, res) => { + if (err) { + chai.assert.fail(err); + } + + const html = htmlParser.parse(res.text); + chai.assert.isTrue(html.querySelector('div.msg-info') !== null); + + agentData.email = vraiEmail; + done(); + }); + }); + }); + + describe('Actions d\'authentification', () => { + it('Supposément récupère la page de login', (done) => { + chai.request(server) + .get('/login') + .end((err, res) => { + if (err) { + chai.assert.fail(err); + } + + res.should.be.html; + res.should.have.status(200); + done(); + }); + }); + + it('Supposément se connecte avec un user client', (done) => { + chai.request(server) + .post('/login') + .send({ + username: clientData.email, + password: clientData.password, + }) + .end((err, res) => { + if (err) { + chai.assert.fail(err); + } + + res.should.have.status(200); + done(); + }); + }); + + it('Supposément se connecte avec un user agent', (done) => { + const agent = chai.request.agent(server); + + agent + .post('/login') + .send({ + username: agentData.email, + password: agentData.password, + }) + .end((err, res) => { + if (err) { + chai.assert.fail(err); + } + + res.should.have.status(200); + + agent.get('/ads/create').end((err, res) => { + if (err) { + chai.assert.fail(err); + } + + res.should.be.html; + res.should.have.status(200); + done(); + }); + }); + }); + }); +});