diff --git a/package-lock.json b/package-lock.json index dbb2f4d015e09d994bcd89f03f823693af0dace1..794f22e6e9270814a8ffb7fbed1c5b568aceb9ac 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,6 +19,7 @@ "graphql": "^16.8.1", "graphql-http": "^1.22.0", "graphql-playground-middleware-express": "^1.7.23", + "graphql-request": "^6.1.0", "js-yaml": "^3.3.0", "jsonwebtoken": "^9.0.2", "mongoose": "^7.6.3", @@ -1429,6 +1430,14 @@ "node": ">= 0.10" } }, + "node_modules/cross-fetch": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.8.tgz", + "integrity": "sha512-cvA+JwZoU0Xq+h6WkMvAUqPEYy92Obet6UdKLfW60qn99ftItKjB5T+BkyWOFWe2pUyfQ+IJHmpOTznqk1M6Kg==", + "dependencies": { + "node-fetch": "^2.6.12" + } + }, "node_modules/cross-inspect": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/cross-inspect/-/cross-inspect-1.0.0.tgz", @@ -2185,6 +2194,18 @@ "express": "^4.16.2" } }, + "node_modules/graphql-request": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/graphql-request/-/graphql-request-6.1.0.tgz", + "integrity": "sha512-p+XPfS4q7aIpKVcgmnZKhMNqhltk20hfXtkaIkTfjjmiKMJ5xrt5c743cL03y/K7y1rg3WrIC49xGiEQ4mxdNw==", + "dependencies": { + "@graphql-typed-document-node/core": "^3.2.0", + "cross-fetch": "^3.1.5" + }, + "peerDependencies": { + "graphql": "14 - 16" + } + }, "node_modules/graphql-tag": { "version": "2.12.6", "resolved": "https://registry.npmjs.org/graphql-tag/-/graphql-tag-2.12.6.tgz", diff --git a/package.json b/package.json index 2610d18ddd046723abef3d3239d8c1f65084a083..593a4cce20e3a830aeddf42a5c5de087d9e03faa 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "graphql": "^16.8.1", "graphql-http": "^1.22.0", "graphql-playground-middleware-express": "^1.7.23", + "graphql-request": "^6.1.0", "js-yaml": "^3.3.0", "jsonwebtoken": "^9.0.2", "mongoose": "^7.6.3", diff --git a/service/AdService.js b/service/AdService.js index 931c68ed2fd17cc3c58fe09fdb9791a57685b6d9..53f97b39f1030f63f9bd4b722af65c8b13b903de 100644 --- a/service/AdService.js +++ b/service/AdService.js @@ -159,7 +159,8 @@ exports.deleteAd = async function (adId, token) { } }); - return JSON.stringify('Annonce supprimée avec succès'); + return {status: 200, message: 'Annonce supprimée avec succès'}; + } catch (error) { throw error; } @@ -229,7 +230,7 @@ exports.answerQuestion = async function (adId, questionId, answer, token) { } if (user.username !== ad.userName) { - return {status: 403, message: 'User non autorisée.'}; + return {status: 403, message: 'User non autorisé.'}; } const question = ad.questions.id(questionId); diff --git a/test/ad.test.js b/test/ad.test.js index d709c181e914c3e820eb9e279e18279354a8f168..3610760e9cad39bdc06761d5f7324d1740c77f9d 100644 --- a/test/ad.test.js +++ b/test/ad.test.js @@ -5,27 +5,47 @@ const path = require('path'); const app = require('../index'); const expect = chai.expect; const mongoose = require('mongoose'); +const { GraphQLClient } = require('graphql-request'); chai.use(chaiHttp); - -let authToken; +const endpoint = 'http://localhost:8080/graphql'; +const client = new GraphQLClient(endpoint); +let agent1AuthToken; +let agent2AuthToken; +let nonAgentAuthToken; let adId; describe('Ad Routes', () => { before(async () => { try { - const loginResponse = await chai + // Login as 'hajar' + const hajarLoginResponse = await chai .request(app) .post('/user/login') .send({ username: 'hajar', password: 'hajar' }); - authToken = loginResponse.body.token; + agent1AuthToken = hajarLoginResponse.body.token; + + // Login as 'ilham' + const ilhamLoginResponse = await chai + .request(app) + .post('/user/login') + .send({ username: 'ilham', password: 'ilham' }); + + agent2AuthToken = ilhamLoginResponse.body.token; - // Create an ad to get the adId for some tests + const nonAgentLoginResponse = await chai + .request(app) + .post('/user/login') + .send({ username: 'manal', password: 'manal' }); + + nonAgentAuthToken = nonAgentLoginResponse.body.token; + + // Create an ad to get the adId for some tests (using 'hajar' user) const adResponse = await chai .request(app) .post('/ad') - .set('Authorization', `Bearer ${authToken}`) + .set('Authorization', `Bearer ${agent1AuthToken}`) .send({ title: 'Test Ad', propertyType: 'À la vente', @@ -38,7 +58,7 @@ describe('Ad Routes', () => { adId = adResponse.body._id; } catch (error) { - console.error('Error obtaining authentication token:', error); + console.error('Error obtaining authentication tokens:', error); } }); @@ -47,7 +67,7 @@ describe('Ad Routes', () => { const response = await chai .request(app) .post('/ad') - .set('Authorization', `Bearer ${authToken}`) + .set('Authorization', `Bearer ${agent1AuthToken}`) .send({ title: 'Test Ad', propertyType: 'À la vente', @@ -66,7 +86,7 @@ describe('Ad Routes', () => { const response = await chai .request(app) .post('/ad') - .set('Authorization', `Bearer ${authToken}`) + .set('Authorization', `Bearer ${agent1AuthToken}`) .attach('photos', path.join(backupFolderPath, 'img1.jpg')) .attach('photos', path.join(backupFolderPath, 'img2.jpg')) .field('title', 'Test Ad 2') @@ -83,14 +103,6 @@ describe('Ad Routes', () => { }); it('should not allow a non-authorized user (non-agent) to add a new ad', async () => { - // Log in as a non-agent user - const nonAgentLoginResponse = await chai - .request(app) - .post('/user/login') - .send({ username: 'manal', password: 'manal' }); - - const nonAgentAuthToken = nonAgentLoginResponse.body.token; - // Attempt to add a new ad as a non-agent user const response = await chai .request(app) @@ -121,19 +133,11 @@ describe('Ad Routes', () => { describe('PUT /ads/:id', () => { it('should update an existing ad with the agent user (hajar)', async () => { - // Log in as the agent user (hajar) - const agentLoginResponse = await chai - .request(app) - .post('/user/login') - .send({ username: 'hajar', password: 'hajar' }); - - const agentAuthToken = agentLoginResponse.body.token; - // Create a new ad to update const createResponse = await chai .request(app) .post('/ad') - .set('Authorization', `Bearer ${agentAuthToken}`) + .set('Authorization', `Bearer ${agent1AuthToken}`) .send({ title: 'Test Ad to update', propertyType: 'À la vente', @@ -150,7 +154,7 @@ describe('Ad Routes', () => { const updateResponse = await chai .request(app) .put(`/ad/${adIdToUpdate}`) - .set('Authorization', `Bearer ${agentAuthToken}`) + .set('Authorization', `Bearer ${agent1AuthToken}`) .send({ title: 'Updated Test Ad', description: 'Updated description', @@ -162,13 +166,6 @@ describe('Ad Routes', () => { expect(updateResponse.body).to.have.property('description').to.equal('Updated description'); expect(updateResponse.body).to.have.property('price').to.equal(90000); - // Log out the agent user (hajar) - const logoutResponse = await chai - .request(app) - .post('/user/logout') - .set('Authorization', `Bearer ${agentAuthToken}`); - - expect(logoutResponse).to.have.status(200); }); }); describe @@ -179,7 +176,7 @@ describe('Ad Routes', () => { const response = await chai .request(app) .post(`/ad/${adId}/photos`) - .set('Authorization', `Bearer ${authToken}`) + .set('Authorization', `Bearer ${agent1AuthToken}`) .attach('photos', fs.readFileSync(imagePath), 'img3.jpg'); expect(response).to.have.status(200); @@ -193,7 +190,7 @@ describe('Ad Routes', () => { const response = await chai .request(app) .get('/ad') - .set('Authorization', `Bearer ${authToken}`); + .set('Authorization', `Bearer ${agent1AuthToken}`); expect(response).to.have.status(200); expect(response.body).to.be.an('array'); @@ -205,7 +202,7 @@ describe('Ad Routes', () => { const response = await chai .request(app) .get(`/ad/${adId}`) - .set('Authorization', `Bearer ${authToken}`); + .set('Authorization', `Bearer ${agent1AuthToken}`); expect(response).to.have.status(200); expect(response.body).to.be.an('object'); @@ -217,12 +214,490 @@ describe('Ad Routes', () => { const response = await chai .request(app) .get(`/ad/${nonExistentAdId}`) - .set('Authorization', `Bearer ${authToken}`); + .set('Authorization', `Bearer ${agent1AuthToken}`); expect(response).to.have.status(404); expect(response.body).to.have.property('message').to.equal('Cette annonce est introuvable ou non disponible.'); }); }); + describe('DELETE /ad/:id', () => { + it('should delete an existing ad', async () => { + const response = await chai + .request(app) + .delete(`/ad/${adId}`) + .set('Authorization', `Bearer ${agent1AuthToken}`); + + expect(response).to.have.status(200); + expect(response.body).to.have.property('message').to.equal('Annonce supprimée avec succès'); + }); + + it('should return 404 for deleting non-existent ad ID', async () => { + const nonExistentAdId = new mongoose.Types.ObjectId(); + const response = await chai + .request(app) + .delete(`/ad/${nonExistentAdId}`) + .set('Authorization', `Bearer ${agent1AuthToken}`); + + expect(response).to.have.status(404); + expect(response.body).to.have.property('message').to.equal('Ad not found.'); + }); + }); + + describe('PUT /ad/:id/ask', () => { + it('should allow a user to ask a question about an ad', async () => { + const adResponse = await chai + .request(app) + .post('/ad') + .set('Authorization', `Bearer ${agent1AuthToken}`) + .send({ + title: 'Test Ad for Asking Questions', + propertyType: 'À la vente', + publicationStatus: 'Publiée', + propertyStatus: 'Disponible', + description: 'Test description for asking questions.', + price: 80000, + availabilityDate: '2024-12-01', + }); + + const adIdForAskingQuestion = adResponse.body._id; + + const response = await chai + .request(app) + .put(`/ad/${adIdForAskingQuestion}/ask`) + .set('Authorization', `Bearer ${nonAgentAuthToken}`) + .send({ + question: 'This is a test question.', + }); + + expect(response).to.have.status(200); + expect(response.body).to.have.property('questions').to.be.an('array'); + expect(response.body.questions[0]).to.have.property('user').to.equal('manal'); + expect(response.body.questions[0]).to.have.property('question').to.equal('This is a test question.'); + }); + }); + + describe('PUT /ad/:id/question/:questionId/answer', () => { + it('should allow the owner to answer a question about their ad', async () => { + const adResponse = await chai + .request(app) + .post('/ad') + .set('Authorization', `Bearer ${agent1AuthToken}`) + .send({ + title: 'Test Ad for Answering Questions', + propertyType: 'À la vente', + publicationStatus: 'Publiée', + propertyStatus: 'Disponible', + description: 'Test description for answering questions.', + price: 80000, + availabilityDate: '2024-12-01', + }); + + const adIdForAnsweringQuestion = adResponse.body._id; + + const askQuestionResponse = await chai + .request(app) + .put(`/ad/${adIdForAnsweringQuestion}/ask`) + .set('Authorization', `Bearer ${nonAgentAuthToken}`) + .send({ + question: 'This is a test question.', + }); + + const questionId = askQuestionResponse.body.questions[0]._id; + + const answerResponse = await chai + .request(app) + .put(`/ad/${adIdForAnsweringQuestion}/question/${questionId}/answer`) + .set('Authorization', `Bearer ${agent1AuthToken}`) + .send({ + answer: 'This is a test answer.', + }); + expect(answerResponse).to.have.status(200); + expect(answerResponse.body).to.have.property('questions').to.be.an('array'); + expect(answerResponse.body.questions[0].answers[0]).to.have.property('answer').to.equal('This is a test answer.'); + }); + + + it('should not allow a non-owner to answer a question about an ad', async () => { + const adResponse = await chai + .request(app) + .post('/ad') + .set('Authorization', `Bearer ${agent1AuthToken}`) + .send({ + title: 'Test Ad for Non-Owner Answering', + propertyType: 'À la vente', + publicationStatus: 'Publiée', + propertyStatus: 'Disponible', + description: 'Test description for non-owner answering.', + price: 80000, + availabilityDate: '2024-12-01', + }); + + const adIdForNonOwnerAnswering = adResponse.body._id; + + const askQuestionResponse = await chai + .request(app) + .put(`/ad/${adIdForNonOwnerAnswering}/ask`) + .set('Authorization', `Bearer ${nonAgentAuthToken}`) + .send({ + question: 'This is a test question.', + }); + + const questionId = askQuestionResponse.body.questions[0]._id; + + const answerResponse = await chai + .request(app) + .put(`/ad/${adIdForNonOwnerAnswering}/question/${questionId}/answer`) + .set('Authorization', `Bearer ${agent2AuthToken}`) + .send({ + answer: 'This is a test answer.', + }); + expect(answerResponse).to.have.status(403); + expect(answerResponse.body).to.have.property('message').to.equal('User non autorisé.'); + }); + }); + + describe('Query graphQl : getAds and getAd By Id', () => { + it('should return a list of ads', async () => { + const query = ` + query { + getAds(token: "${agent1AuthToken}") { + _id + title + propertyType + publicationStatus + propertyStatus + description + price + availabilityDate + photos + userName + questions { + user + question + } + } + } + `; + + const response = await chai + .request(app) + .post('/graphql') + .send({ query }); + + expect(response).to.have.status(200); + expect(response.body).to.have.property('data'); + expect(response.body.data).to.have.property('getAds'); + }); + it('should return details of a specific ad', async () => { + const query = ` + query { + getAdById(adId: "${adId}", token: "${agent2AuthToken}") { + _id + title + propertyType + publicationStatus + propertyStatus + description + price + availabilityDate + photos + userName + questions { + user + question + } + } + } + `; + + const response = await chai + .request(app) + .post('/graphql') + .send({ query }); + + expect(response).to.have.status(200); + expect(response.body).to.have.property('data'); + expect(response.body.data).to.have.property('getAdById'); + }); + }); + + describe('Mutation: addAd', () => { + it('Adds a new ad and returns the correct fields', async () => { + const variables = { + input: { + title: 'Test Ad 2', + propertyType: 'À la vente', + publicationStatus: 'Publiée', + propertyStatus: 'Disponible', + description: 'teest.', + price: 80000, + availabilityDate: '2024-11-01', + }, + }; + const headers = { + Authorization: `Bearer ${agent1AuthToken}`, + }; + const mutation = ` + mutation AddAd($input: AdInput!) { + addAd(input: $input) { + title + propertyType + publicationStatus + propertyStatus + description + price + availabilityDate + } + } + `; + + const response = await client.request(mutation, variables, headers); + expect(response.addAd).to.deep.equal({ + title: 'Test Ad 2', + propertyType: 'À la vente', + publicationStatus: 'Publiée', + propertyStatus: 'Disponible', + description: 'teest.', + price: 80000, + availabilityDate: '1730419200000', + }); + }); + }); + describe('Mutation: updateAd', () => { + it('Updates an existing ad and returns the correct fields', async () => { + const adToUpdate = { + title: 'Ad to Update', + propertyType: 'À la vente', + publicationStatus: 'Publiée', + propertyStatus: 'Disponible', + description: 'Test description', + price: 80000, + availabilityDate: '2024-12-01', + }; + + const createResponse = await chai + .request(app) + .post('/ad') + .set('Authorization', `Bearer ${agent1AuthToken}`) + .send(adToUpdate); + + const adIdToUpdate = createResponse.body._id; + const updatedAd = { + title: 'Updated Ad', + description: 'Updated description', + price: 90000, + }; + + const updateResponse = await chai + .request(app) + .put(`/ad/${adIdToUpdate}`) + .set('Authorization', `Bearer ${agent1AuthToken}`) + .send(updatedAd); + + expect(updateResponse).to.have.status(200); + expect(updateResponse.body).to.have.property('_id').to.equal(adIdToUpdate); + expect(updateResponse.body).to.have.property('title').to.equal(updatedAd.title); + expect(updateResponse.body).to.have.property('description').to.equal(updatedAd.description); + expect(updateResponse.body).to.have.property('price').to.equal(updatedAd.price); + }); + + it('Does not allow a non-owner to update an ad', async () => { + const adToUpdate = { + title: 'Ad to Update', + propertyType: 'À la vente', + publicationStatus: 'Publiée', + propertyStatus: 'Disponible', + description: 'Test description', + price: 80000, + availabilityDate: '2024-12-01', + }; + const createResponse = await chai + .request(app) + .post('/ad') + .set('Authorization', `Bearer ${agent1AuthToken}`) + .send(adToUpdate); + + const adIdToUpdate = createResponse.body._id; + + const updateResponse = await chai + .request(app) + .put(`/ad/${adIdToUpdate}`) + .set('Authorization', `Bearer ${agent2AuthToken}`) + .send({ + title: 'Attempted Update', + description: 'Attempted update description', + price: 95000, + }); + + expect(updateResponse).to.have.status(403); + expect(updateResponse.body).to.have.property('message').to.equal('User non autorisé'); + }); + }); + describe('Mutation: deleteAd', () => { + it('Deletes an existing ad', async () => { + const createResponse = await chai + .request(app) + .post('/ad') + .set('Authorization', `Bearer ${agent1AuthToken}`) + .send({ + title: 'Ad to Delete', + propertyType: 'À la vente', + publicationStatus: 'Publiée', + propertyStatus: 'Disponible', + description: 'Test description', + price: 80000, + availabilityDate: '2024-12-01', + }); + + const adIdToDelete = createResponse.body._id; + const deleteResponse = await chai + .request(app) + .delete(`/ad/${adIdToDelete}`) + .set('Authorization', `Bearer ${agent1AuthToken}`); + + expect(deleteResponse).to.have.status(200); + expect(deleteResponse.body).to.have.property('message').to.equal('Annonce supprimée avec succès'); + }); + + it('Does not allow a non-owner to delete an ad', async () => { + // Create a new ad to delete + const createResponse = await chai + .request(app) + .post('/ad') + .set('Authorization', `Bearer ${agent1AuthToken}`) + .send({ + title: 'Ad to Delete', + propertyType: 'À la vente', + publicationStatus: 'Publiée', + propertyStatus: 'Disponible', + description: 'Test description', + price: 80000, + availabilityDate: '2024-12-01', + }); + + const adIdToDelete = createResponse.body._id; + + const deleteResponse = await chai + .request(app) + .delete(`/ad/${adIdToDelete}`) + .set('Authorization', `Bearer ${agent2AuthToken}`); + expect(deleteResponse).to.have.status(403); + expect(deleteResponse.body).to.have.property('message').to.equal('User non autorisé'); + }); + }); + + describe('Mutation: askQuestion and answerQuestion', () => { + it('Allows a user to ask a question about an ad', async () => { + const createResponse = await chai + .request(app) + .post('/ad') + .set('Authorization', `Bearer ${agent1AuthToken}`) + .send({ + title: 'Ad for Asking Questions', + propertyType: 'À la vente', + publicationStatus: 'Publiée', + propertyStatus: 'Disponible', + description: 'Test description for asking questions.', + price: 80000, + availabilityDate: '2024-12-01', + }); + + const adIdForAskingQuestion = createResponse.body._id; + + const askQuestionResponse = await chai + .request(app) + .put(`/ad/${adIdForAskingQuestion}/ask`) + .set('Authorization', `Bearer ${nonAgentAuthToken}`) + .send({ + question: 'This is a test question.', + }); + + expect(askQuestionResponse).to.have.status(200); + expect(askQuestionResponse.body).to.have.property('questions').to.be.an('array'); + expect(askQuestionResponse.body.questions[0]).to.have.property('user').to.equal('manal'); + expect(askQuestionResponse.body.questions[0]).to.have.property('question').to.equal('This is a test question.'); + }); + + it('Allows the owner to answer a question about their ad', async () => { + const createResponse = await chai + .request(app) + .post('/ad') + .set('Authorization', `Bearer ${agent1AuthToken}`) + .send({ + title: 'Ad for Answering Questions', + propertyType: 'À la vente', + publicationStatus: 'Publiée', + propertyStatus: 'Disponible', + description: 'Test description for answering questions.', + price: 80000, + availabilityDate: '2024-12-01', + }); + + const adIdForAnsweringQuestion = createResponse.body._id; + + const askQuestionResponse = await chai + .request(app) + .put(`/ad/${adIdForAnsweringQuestion}/ask`) + .set('Authorization', `Bearer ${nonAgentAuthToken}`) + .send({ + question: 'This is a test question.', + }); + + const questionId = askQuestionResponse.body.questions[0]._id; + + const answerResponse = await chai + .request(app) + .put(`/ad/${adIdForAnsweringQuestion}/question/${questionId}/answer`) + .set('Authorization', `Bearer ${agent1AuthToken}`) + .send({ + answer: 'This is a test answer.', + }); + + expect(answerResponse).to.have.status(200); + expect(answerResponse.body).to.have.property('questions').to.be.an('array'); + expect(answerResponse.body.questions[0].answers[0]).to.have.property('answer').to.equal('This is a test answer.'); + }); + + it('Does not allow a non-owner to answer a question about an ad', async () => { + const createResponse = await chai + .request(app) + .post('/ad') + .set('Authorization', `Bearer ${agent1AuthToken}`) + .send({ + title: 'Ad for Answering Questions', + propertyType: 'À la vente', + publicationStatus: 'Publiée', + propertyStatus: 'Disponible', + description: 'Test description for answering questions.', + price: 80000, + availabilityDate: '2024-12-01', + }); + + const adIdForAnsweringQuestion = createResponse.body._id; + + const askQuestionResponse = await chai + .request(app) + .put(`/ad/${adIdForAnsweringQuestion}/ask`) + .set('Authorization', `Bearer ${nonAgentAuthToken}`) + .send({ + question: 'This is a test question.', + }); + + const questionId = askQuestionResponse.body.questions[0]._id; + + const answerResponse = await chai + .request(app) + .put(`/ad/${adIdForAnsweringQuestion}/question/${questionId}/answer`) + .set('Authorization', `Bearer ${agent2AuthToken}`) + .send({ + answer: 'This is a test answer.', + }); + + expect(answerResponse).to.have.status(403); + expect(answerResponse.body).to.have.property('message').to.equal('User non autorisé.'); + }); + }); + // Cleanup after tests after(async () => { try { @@ -231,12 +706,12 @@ describe('Ad Routes', () => { await chai .request(app) .delete(`/ad/${adId}`) - .set('Authorization', `Bearer ${authToken}`); + .set('Authorization', `Bearer ${agent1AuthToken}`); } - const photo1Path = path.join(__dirname, 'uploads', 'img1.jpg'); - const photo2Path = path.join(__dirname, 'uploads', 'img2.jpg'); - + const photo1Path = path.join(__dirname, '..', 'uploads', 'img1.jpg'); + const photo2Path = path.join(__dirname, '..', 'uploads', 'img2.jpg'); + const photo3Path = path.join(__dirname, '..', 'uploads', 'img3.jpg'); // Delete the photos if (fs.existsSync(photo1Path)) { fs.unlinkSync(photo1Path); @@ -245,6 +720,10 @@ describe('Ad Routes', () => { if (fs.existsSync(photo2Path)) { fs.unlinkSync(photo2Path); } + + if (fs.existsSync(photo3Path)) { + fs.unlinkSync(photo3Path); + } } catch (error) { console.error('Error cleaning up:', error); }