From 2b34f2a2208ead53c2f77797fad74f87c360564c Mon Sep 17 00:00:00 2001 From: Pierre Theo Klein Date: Thu, 10 Jan 2019 18:25:45 -0500 Subject: [PATCH] Finish route, add tests, update documentation --- constants/role.constant.js | 1 + constants/routes.constant.js | 8 ++ docs/api/api_data.js | 75 ++++++++++++++++- docs/api/api_data.json | 75 ++++++++++++++++- docs/api/api_project.js | 2 +- docs/api/api_project.json | 2 +- middlewares/hacker.middleware.js | 22 +++++ .../validators/routeParam.validator.js | 7 +- routes/api/hacker.js | 60 +++++++++++++- tests/hacker.test.js | 82 +++++++++++++++++++ 10 files changed, 328 insertions(+), 6 deletions(-) diff --git a/constants/role.constant.js b/constants/role.constant.js index 89cf78eb..33ba7aae 100644 --- a/constants/role.constant.js +++ b/constants/role.constant.js @@ -35,6 +35,7 @@ const hackerRole = { Constants.Routes.hackerRoutes.post, Constants.Routes.hackerRoutes.getSelfById, + Constants.Routes.hackerRoutes.getSelfByEmail, Constants.Routes.hackerRoutes.getSelfResumeById, Constants.Routes.hackerRoutes.patchSelfById, Constants.Routes.hackerRoutes.patchSelfConfirmationById, diff --git a/constants/routes.constant.js b/constants/routes.constant.js index 74fa8806..ad667a6e 100644 --- a/constants/routes.constant.js +++ b/constants/routes.constant.js @@ -68,6 +68,14 @@ const hackerRoutes = { requestType: Constants.REQUEST_TYPES.GET, uri: "/api/hacker/" + Constants.ROLE_CATEGORIES.ALL, }, + "getSelfByEmail": { + requestType: Constants.REQUEST_TYPES.GET, + uri: "/api/hacker/email/" + Constants.ROLE_CATEGORIES.SELF, + }, + "getAnyByEmail": { + requestType: Constants.REQUEST_TYPES.GET, + uri: "/api/hacker/email/" + Constants.ROLE_CATEGORIES.ALL, + }, "getSelfResumeById": { requestType: Constants.REQUEST_TYPES.GET, uri: "/api/hacker/resume/" + Constants.ROLE_CATEGORIES.SELF, diff --git a/docs/api/api_data.js b/docs/api/api_data.js index 7af97641..78626e40 100644 --- a/docs/api/api_data.js +++ b/docs/api/api_data.js @@ -1248,6 +1248,78 @@ define({ "url": "https://api.mchacks.ca/api/hacker/" }] }, + { + "type": "get", + "url": "/hacker/email/:email", + "title": "get a hacker's information", + "name": "getHacker", + "group": "Hacker", + "version": "0.0.8", + "parameter": { + "fields": { + "param": [{ + "group": "param", + "type": "String", + "optional": false, + "field": "email", + "description": "

a hacker's unique email

" + }] + } + }, + "success": { + "fields": { + "Success 200": [{ + "group": "Success 200", + "type": "String", + "optional": false, + "field": "message", + "description": "

Success message

" + }, + { + "group": "Success 200", + "type": "Object", + "optional": false, + "field": "data", + "description": "

Hacker object

" + } + ] + }, + "examples": [{ + "title": "Success-Response: ", + "content": "{\n \"message\": \"Successfully retrieved hacker information\", \n \"data\": {\n \"id\":\"5bff4d736f86be0a41badb91\",\n \"application\":{\n \"portfolioURL\":{\n \"resume\":\"resumes/1543458163426-5bff4d736f86be0a41badb91\",\n \"github\":\"https://github.com/abcd\",\n \"dropler\":\"https://dribbble.com/abcd\",\n \"personal\":\"https://www.hi.com/\",\n \"linkedIn\":\"https://linkedin.com/in/abcd\",\n \"other\":\"https://github.com/hackmcgill/hackerAPI/issues/168\"\n },\n \"jobInterest\":\"Internship\",\n \"skills\":[\"Javascript\",\"Typescript\"],\n \"comments\":\"hi!\",\n \"essay\":\"Pls accept me\"\n },\n \"status\":\"Applied\",\n \"ethnicity\":[\"White or Caucasian\",\" Asian or Pacific Islander\"],\n \"accountId\":\"5bff2a35e533b0f6562b4998\",\n \"school\":\"McPherson College\",\n \"gender\":\"Female\",\n \"needsBus\":false,\n \"major\":\"Accounting\",\n \"graduationYear\":2019,\n \"codeOfConduct\":true,\n }\n }", + "type": "object" + }] + }, + "error": { + "fields": { + "Error 4xx": [{ + "group": "Error 4xx", + "type": "String", + "optional": false, + "field": "message", + "description": "

Error message

" + }, + { + "group": "Error 4xx", + "type": "Object", + "optional": false, + "field": "data", + "description": "

empty

" + } + ] + }, + "examples": [{ + "title": "Error-Response: ", + "content": "{\"message\": \"Hacker not found\", \"data\": {}}", + "type": "object" + }] + }, + "filename": "routes/api/hacker.js", + "groupTitle": "Hacker", + "sampleRequest": [{ + "url": "https://api.mchacks.ca/api/hacker/email/:email" + }] + }, { "type": "get", "url": "/hacker/:id", @@ -1280,7 +1352,7 @@ define({ "type": "Object", "optional": false, "field": "data", - "description": "

Sponsor object

" + "description": "

Hacker object

" } ] }, @@ -2425,6 +2497,7 @@ define({ "name": "patchTeam", "group": "Team", "version": "0.0.8", + "description": "

We use hackerId instead of teamId because authorization requires a one-to-one mapping from param id to accountId, but we are not able to have that from teamId to accountId due to multiple members in a team. Instead, we use hackerId, as there is a 1 to 1 link between hackerId to teamId, and a 1 to 1 link between hackerId and accountId

", "parameter": { "fields": { "param": [{ diff --git a/docs/api/api_data.json b/docs/api/api_data.json index 03cc58e8..a85b8382 100644 --- a/docs/api/api_data.json +++ b/docs/api/api_data.json @@ -1247,6 +1247,78 @@ "url": "https://api.mchacks.ca/api/hacker/" }] }, + { + "type": "get", + "url": "/hacker/email/:email", + "title": "get a hacker's information", + "name": "getHacker", + "group": "Hacker", + "version": "0.0.8", + "parameter": { + "fields": { + "param": [{ + "group": "param", + "type": "String", + "optional": false, + "field": "email", + "description": "

a hacker's unique email

" + }] + } + }, + "success": { + "fields": { + "Success 200": [{ + "group": "Success 200", + "type": "String", + "optional": false, + "field": "message", + "description": "

Success message

" + }, + { + "group": "Success 200", + "type": "Object", + "optional": false, + "field": "data", + "description": "

Hacker object

" + } + ] + }, + "examples": [{ + "title": "Success-Response: ", + "content": "{\n \"message\": \"Successfully retrieved hacker information\", \n \"data\": {\n \"id\":\"5bff4d736f86be0a41badb91\",\n \"application\":{\n \"portfolioURL\":{\n \"resume\":\"resumes/1543458163426-5bff4d736f86be0a41badb91\",\n \"github\":\"https://github.com/abcd\",\n \"dropler\":\"https://dribbble.com/abcd\",\n \"personal\":\"https://www.hi.com/\",\n \"linkedIn\":\"https://linkedin.com/in/abcd\",\n \"other\":\"https://github.com/hackmcgill/hackerAPI/issues/168\"\n },\n \"jobInterest\":\"Internship\",\n \"skills\":[\"Javascript\",\"Typescript\"],\n \"comments\":\"hi!\",\n \"essay\":\"Pls accept me\"\n },\n \"status\":\"Applied\",\n \"ethnicity\":[\"White or Caucasian\",\" Asian or Pacific Islander\"],\n \"accountId\":\"5bff2a35e533b0f6562b4998\",\n \"school\":\"McPherson College\",\n \"gender\":\"Female\",\n \"needsBus\":false,\n \"major\":\"Accounting\",\n \"graduationYear\":2019,\n \"codeOfConduct\":true,\n }\n }", + "type": "object" + }] + }, + "error": { + "fields": { + "Error 4xx": [{ + "group": "Error 4xx", + "type": "String", + "optional": false, + "field": "message", + "description": "

Error message

" + }, + { + "group": "Error 4xx", + "type": "Object", + "optional": false, + "field": "data", + "description": "

empty

" + } + ] + }, + "examples": [{ + "title": "Error-Response: ", + "content": "{\"message\": \"Hacker not found\", \"data\": {}}", + "type": "object" + }] + }, + "filename": "routes/api/hacker.js", + "groupTitle": "Hacker", + "sampleRequest": [{ + "url": "https://api.mchacks.ca/api/hacker/email/:email" + }] + }, { "type": "get", "url": "/hacker/:id", @@ -1279,7 +1351,7 @@ "type": "Object", "optional": false, "field": "data", - "description": "

Sponsor object

" + "description": "

Hacker object

" } ] }, @@ -2424,6 +2496,7 @@ "name": "patchTeam", "group": "Team", "version": "0.0.8", + "description": "

We use hackerId instead of teamId because authorization requires a one-to-one mapping from param id to accountId, but we are not able to have that from teamId to accountId due to multiple members in a team. Instead, we use hackerId, as there is a 1 to 1 link between hackerId to teamId, and a 1 to 1 link between hackerId and accountId

", "parameter": { "fields": { "param": [{ diff --git a/docs/api/api_project.js b/docs/api/api_project.js index 22cdc45c..1165ba2f 100644 --- a/docs/api/api_project.js +++ b/docs/api/api_project.js @@ -9,7 +9,7 @@ define({ "apidoc": "0.3.0", "generator": { "name": "apidoc", - "time": "2019-01-08T22:07:07.661Z", + "time": "2019-01-10T23:24:21.462Z", "url": "http://apidocjs.com", "version": "0.17.7" } diff --git a/docs/api/api_project.json b/docs/api/api_project.json index 543c38d6..38a99543 100644 --- a/docs/api/api_project.json +++ b/docs/api/api_project.json @@ -9,7 +9,7 @@ "apidoc": "0.3.0", "generator": { "name": "apidoc", - "time": "2019-01-08T22:07:07.661Z", + "time": "2019-01-10T23:24:21.462Z", "url": "http://apidocjs.com", "version": "0.17.7" } diff --git a/middlewares/hacker.middleware.js b/middlewares/hacker.middleware.js index 7d55d466..2e4fbd71 100644 --- a/middlewares/hacker.middleware.js +++ b/middlewares/hacker.middleware.js @@ -178,6 +178,27 @@ async function findById(req, res, next) { next(); } +async function findByEmail(req, res, next) { + const account = await Services.Account.findByEmail(req.body.email); + if (!account) { + return next({ + status: 404, + message: Constants.Error.ACCOUNT_404_MESSAGE, + error: {} + }); + } + const hacker = await Services.Hacker.findByAccountId(account._id); + if (!hacker) { + return res.status(404).json({ + message: Constants.Error.HACKER_404_MESSAGE, + data: {} + }); + } + + req.body.hacker = hacker; + next(); +} + /** * Verifies that the current signed in user is linked to the hacker passed in via req.body.id * @param {{body: {id: ObjectId}}} req @@ -512,4 +533,5 @@ module.exports = { findSelf: Middleware.Util.asyncMiddleware(findSelf), getStats: Middleware.Util.asyncMiddleware(getStats), findById: Middleware.Util.asyncMiddleware(findById), + findByEmail: Middleware.Util.asyncMiddleware(findByEmail), }; \ No newline at end of file diff --git a/middlewares/validators/routeParam.validator.js b/middlewares/validators/routeParam.validator.js index b7b28f63..7ea53979 100644 --- a/middlewares/validators/routeParam.validator.js +++ b/middlewares/validators/routeParam.validator.js @@ -1,5 +1,6 @@ "use strict"; const VALIDATOR = require("./validator.helper"); +const Constants = require("../../constants/general.constant"); module.exports = { idValidator: [ @@ -8,5 +9,9 @@ module.exports = { hackeridValidator: [ VALIDATOR.mongoIdValidator("param", "hackerId", false), - ] + ], + + emailValidator: [ + VALIDATOR.regexValidator("param", "email", false, Constants.EMAIL_REGEX), + ], }; \ No newline at end of file diff --git a/routes/api/hacker.js b/routes/api/hacker.js index 548e1317..5c04897c 100644 --- a/routes/api/hacker.js +++ b/routes/api/hacker.js @@ -18,6 +18,7 @@ const Middleware = { }; const Services = { Hacker: require("../../services/hacker.service"), + Account: require("../../services/account.service"), } const CONSTANTS = require("../../constants/general.constant"); @@ -369,7 +370,7 @@ module.exports = { * @apiParam (param) {String} id a hacker's unique mongoID * * @apiSuccess {String} message Success message - * @apiSuccess {Object} data Sponsor object + * @apiSuccess {Object} data Hacker object * @apiSuccessExample {object} Success-Response: * { "message": "Successfully retrieved hacker information", @@ -417,6 +418,63 @@ module.exports = { Controllers.Hacker.showHacker ); + /** + * @api {get} /hacker/email/:email get a hacker's information + * @apiName getHacker + * @apiGroup Hacker + * @apiVersion 0.0.8 + * + * @apiParam (param) {String} email a hacker's unique email + * + * @apiSuccess {String} message Success message + * @apiSuccess {Object} data Hacker object + * @apiSuccessExample {object} Success-Response: + * { + "message": "Successfully retrieved hacker information", + "data": { + "id":"5bff4d736f86be0a41badb91", + "application":{ + "portfolioURL":{ + "resume":"resumes/1543458163426-5bff4d736f86be0a41badb91", + "github":"https://github.com/abcd", + "dropler":"https://dribbble.com/abcd", + "personal":"https://www.hi.com/", + "linkedIn":"https://linkedin.com/in/abcd", + "other":"https://github.com/hackmcgill/hackerAPI/issues/168" + }, + "jobInterest":"Internship", + "skills":["Javascript","Typescript"], + "comments":"hi!", + "essay":"Pls accept me" + }, + "status":"Applied", + "ethnicity":["White or Caucasian"," Asian or Pacific Islander"], + "accountId":"5bff2a35e533b0f6562b4998", + "school":"McPherson College", + "gender":"Female", + "needsBus":false, + "major":"Accounting", + "graduationYear":2019, + "codeOfConduct":true, + } + } + + * @apiError {String} message Error message + * @apiError {Object} data empty + * @apiErrorExample {object} Error-Response: + * {"message": "Hacker not found", "data": {}} + */ + hackerRouter.route("/email/:email").get( + Middleware.Auth.ensureAuthenticated(), + Middleware.Auth.ensureAuthorized([Services.Account.findByEmail]), + + Middleware.Validator.RouteParam.emailValidator, + Middleware.parseBody.middleware, + + Middleware.Hacker.findByEmail, + Controllers.Hacker.showHacker + ); + hackerRouter.route("/resume/:id") /** * @api {post} /hacker/resume/:id upload or update resume for a hacker. diff --git a/tests/hacker.test.js b/tests/hacker.test.js index 0204e5f8..ec8c347a 100644 --- a/tests/hacker.test.js +++ b/tests/hacker.test.js @@ -209,6 +209,88 @@ describe("GET hacker", function () { }); }); }); + + // succeed on admin case + it("should list a hacker's information using admin power on /api/hacker/email/:email GET", function (done) { + util.auth.login(agent, Admin1, (error) => { + if (error) { + agent.close(); + return done(error); + } + return agent + .get(`/api/hacker/email/${storedAccount1.email}`) + // does not have password because of to stripped json + .end(function (err, res) { + if (err) { + return done(err); + } + res.should.have.status(200); + res.should.be.json; + res.body.should.have.property("message"); + res.body.message.should.equal(Constants.Success.HACKER_READ); + res.body.should.have.property("data"); + + let hacker = new Hacker(storedHacker1); + chai.assert.equal(JSON.stringify(res.body.data), JSON.stringify(hacker.toJSON())); + + done(); + }); + }); + }); + + // succeed on :self case + it("should list the user's hacker information on /api/hacker/email/:email GET", function (done) { + util.auth.login(agent, storedAccount1, (error) => { + if (error) { + agent.close(); + return done(error); + } + return agent + .get(`/api/hacker/email/${storedAccount1.email}`) + // does not have password because of to stripped json + .end(function (err, res) { + if (err) { + return done(err); + } + res.should.have.status(200); + res.should.be.json; + res.body.should.have.property("message"); + res.body.message.should.equal(Constants.Success.HACKER_READ); + res.body.should.have.property("data"); + + let hacker = new Hacker(storedHacker1); + + chai.assert.equal(JSON.stringify(res.body.data), JSON.stringify(hacker.toJSON())); + + done(); + }); + }); + }); + + // fail due to lack of authorization + it("should fail to list a hacker information due to lack of authorization on /api/hacker/email/:id GET", function (done) { + util.auth.login(agent, storedAccount2, (error) => { + if (error) { + agent.close(); + return done(error); + } + return agent + .get(`/api/hacker/email/${storedAccount1.email}`) + // does not have password because of to stripped json + .end(function (err, res) { + if (err) { + return done(err); + } + res.should.have.status(403); + res.should.be.json; + res.body.should.have.property("message"); + res.body.message.should.equal(Constants.Error.AUTH_403_MESSAGE); + res.body.should.have.property("data"); + + done(); + }); + }); + }); }); describe("POST create hacker", function () {