diff --git a/constants/error.constant.js b/constants/error.constant.js index 3e86aa89..014f53bd 100644 --- a/constants/error.constant.js +++ b/constants/error.constant.js @@ -11,6 +11,7 @@ const SPONSOR_ID_409_MESSAGE = "Conflict with sponsor accountId link"; const VOLUNTEER_ID_409_MESSAGE = "Conflict with volunteer accountId link"; const HACKER_ID_409_MESSAGE = "Conflict with hacker accountId link"; const TEAM_MEMBER_409_MESSAGE = "Conflict with team member being in another team"; +const TEAM_NAME_409_MESSAGE = "Conflict with team name already in use"; const HACKER_STATUS_409_MESSAGE = "Conflict with hacker status"; const TEAM_SIZE_409_MESSAGE = "Team full"; const TEAM_JOIN_SAME_409_MESSAGE = "Hacker is already on receiving team"; @@ -70,5 +71,6 @@ module.exports = { TEAM_SIZE_409_MESSAGE: TEAM_SIZE_409_MESSAGE, ROLE_DUPLICATE_422_MESSAGE: ROLE_DUPLICATE_422_MESSAGE, ROLE_CREATE_500_MESSAGE: ROLE_CREATE_500_MESSAGE, + TEAM_NAME_409_MESSAGE: TEAM_NAME_409_MESSAGE, TEAM_JOIN_SAME_409_MESSAGE: TEAM_JOIN_SAME_409_MESSAGE, }; \ No newline at end of file diff --git a/constants/role.constant.js b/constants/role.constant.js index 34c1dedd..3915e170 100644 --- a/constants/role.constant.js +++ b/constants/role.constant.js @@ -41,6 +41,7 @@ const hackerRole = { Constants.Routes.hackerRoutes.getSelf, Constants.Routes.teamRoutes.join, + Constants.Routes.teamRoutes.post, Constants.Routes.teamRoutes.get ] }; diff --git a/docs/api/api_data.js b/docs/api/api_data.js index 349f37de..b6d9a872 100644 --- a/docs/api/api_data.js +++ b/docs/api/api_data.js @@ -2158,7 +2158,7 @@ define({ { "type": "post", "url": "/team/", - "title": "create a new team", + "title": "create a new team consisting of only the logged in user", "name": "createTeam", "group": "Team", "version": "0.0.8", @@ -2171,13 +2171,6 @@ define({ "field": "name", "description": "

Name of the team.

" }, - { - "group": "body", - "type": "MongoID[]", - "optional": true, - "field": "members", - "description": "

Array of members in team.

" - }, { "group": "body", "type": "String", diff --git a/docs/api/api_data.json b/docs/api/api_data.json index f4f7358e..cc112aa3 100644 --- a/docs/api/api_data.json +++ b/docs/api/api_data.json @@ -2157,7 +2157,7 @@ { "type": "post", "url": "/team/", - "title": "create a new team", + "title": "create a new team consisting of only the logged in user", "name": "createTeam", "group": "Team", "version": "0.0.8", @@ -2170,13 +2170,6 @@ "field": "name", "description": "

Name of the team.

" }, - { - "group": "body", - "type": "MongoID[]", - "optional": true, - "field": "members", - "description": "

Array of members in team.

" - }, { "group": "body", "type": "String", diff --git a/docs/api/api_project.js b/docs/api/api_project.js index 27ce5afa..0e8c6e6d 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-07T19:14:21.760Z", + "time": "2019-01-05T01:35:07.317Z", "url": "http://apidocjs.com", "version": "0.17.7" } diff --git a/docs/api/api_project.json b/docs/api/api_project.json index 3c6ef37d..6f2d6d2e 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-07T19:14:21.760Z", + "time": "2019-01-05T01:35:07.317Z", "url": "http://apidocjs.com", "version": "0.17.7" } diff --git a/middlewares/team.middleware.js b/middlewares/team.middleware.js index 3bc4b7a2..611e06e0 100644 --- a/middlewares/team.middleware.js +++ b/middlewares/team.middleware.js @@ -55,7 +55,42 @@ async function ensureUniqueHackerId(req, res, next) { /** * @async - * @function ensureSpance + * @function createTeam + * @param {{body: {teamDetails: {_id: ObjectId, name: string, members: ObjectId[], devpostURL?: string, projectName: string}}}} req + * @param {*} res + * @description create a team from information in req.body.teamDetails. + */ +async function createTeam(req, res, next) { + const teamDetails = req.body.teamDetails; + + const team = await Services.Team.createTeam(teamDetails); + + if (!team) { + return res.status(500).json({ + message: Constants.Error.TEAM_CREATE_500_MESSAGE, + data: {} + }); + } + + for (const hackerId of teamDetails.members) { + const hacker = await Services.Hacker.updateOne(hackerId, { + teamId: team._id + }); + + if (!hacker) { + return res.status(500).json({ + message: Constants.Error.HACKER_UPDATE_500_MESSAGE, + data: {} + }); + } + } + + req.body.team = team; + return next(); +} + +/** + * @function ensureSpace * @param {{body: {name: string}}} req * @param {JSON} res * @param {(err?)=>void} next @@ -85,26 +120,49 @@ async function ensureSpace(req, res, next) { } /** - * @async - * @function createTeam - * @param {{body: {teamDetails: {_id: ObjectId, name: string, members: ObjectId[], devpostURL?: string, projectName: string}}}} req - * @param {*} res - * @description create a team from information in req.body.teamDetails. + * @function ensureFreeTeamName + * @param {{body: {teamDetails: {name: String}}}} req + * @param {JSON} res + * @param {(err?)=>void} next + * @return {void} + * @description Checks to see that the team name is not in use. */ -async function createTeam(req, res, next) { +async function ensureFreeTeamName(req, res, next) { const teamDetails = req.body.teamDetails; - const team = await Services.Team.createTeam(teamDetails); + const team = await Services.Team.findByName(teamDetails.name); + + if (team) { + return next({ + status: 409, + message: Constants.Error.TEAM_NAME_409_MESSAGE, + data: teamDetails.name + }); + } + + return next(); +} + +/** + * @async + * @function findById + * @param {{body: {id: ObjectId}}} req + * @param {*} res + * @return {JSON} Success or error status + * @description Finds a team by it's mongoId that's specified in req.param.id in route parameters. The id is moved to req.body.id from req.params.id by validation. + */ +async function findById(req, res, next) { + const team = await Services.Team.findById(req.body.id); if (!team) { - return res.status(500).json({ - message: Constants.Error.TEAM_CREATE_500_MESSAGE, + return res.status(404).json({ + message: Constants.Error.TEAM_404_MESSAGE, data: {} }); - } else { - req.body.team = team; - return next(); } + + req.body.team = team; + return next(); } /** @@ -145,7 +203,7 @@ async function updateHackerTeam(req, res, next) { return next({ status: 409, message: Constants.Error.TEAM_JOIN_SAME_409_MESSAGE, - data: req.body.teamName + data: req.body.name }); } @@ -155,7 +213,6 @@ async function updateHackerTeam(req, res, next) { await Services.Team.removeTeamIfEmpty(previousTeamId); } - // add hacker to the new team and change teamId of hacker const update = await Services.Team.addMember(receivingTeam._id, hacker._id); @@ -258,6 +315,47 @@ function parseTeam(req, res, next) { return next(); } +async function parseNewTeam(req, res, next) { + const teamDetails = { + _id: mongoose.Types.ObjectId(), + name: req.body.name, + members: [], + devpostURL: req.body.devpostURL, + projectName: req.body.projectName + }; + + delete req.body.name; + delete req.body.members; + delete req.body.devpostURL; + delete req.body.projectName; + + // hacker should exist because of authorization + const hacker = await Services.Hacker.findByAccountId(req.user.id); + + if (!hacker) { + return next({ + status: 404, + message: Constants.Error.HACKER_404_MESSAGE, + data: { + id: req.user.id + } + }); + } + + // hacker should not be in another team + if (hacker.teamId !== undefined) { + return next({ + status: 409, + message: Constants.Error.TEAM_MEMBER_409_MESSAGE, + }); + } + + teamDetails.members.push(hacker._id); + + req.body.teamDetails = teamDetails; + return next(); +} + module.exports = { parseTeam: parseTeam, findById: Util.asyncMiddleware(findById), @@ -265,5 +363,7 @@ module.exports = { ensureUniqueHackerId: Util.asyncMiddleware(ensureUniqueHackerId), ensureSpace: Util.asyncMiddleware(ensureSpace), updateHackerTeam: Util.asyncMiddleware(updateHackerTeam), + parseNewTeam: Util.asyncMiddleware(parseNewTeam), + ensureFreeTeamName: Util.asyncMiddleware(ensureFreeTeamName), populateMemberAccountsById: Util.asyncMiddleware(populateMemberAccountsById), }; \ No newline at end of file diff --git a/middlewares/validators/team.validator.js b/middlewares/validators/team.validator.js index 9b248946..20371d1a 100644 --- a/middlewares/validators/team.validator.js +++ b/middlewares/validators/team.validator.js @@ -5,8 +5,6 @@ const Constants = require("../../constants/general.constant"); module.exports = { newTeamValidator: [ VALIDATOR.asciiValidator("body", "name", false), - // members by mongoID if the team creator is able to provide - VALIDATOR.mongoIdArrayValidator("body", "members", true), VALIDATOR.regexValidator("body", "devpostURL", true, Constants.DEVPOST_REGEX), VALIDATOR.asciiValidator("body", "projectName", true) ], diff --git a/models/team.model.js b/models/team.model.js index cd69ced9..9905ab20 100644 --- a/models/team.model.js +++ b/models/team.model.js @@ -6,8 +6,8 @@ const Constants = require("../constants/general.constant"); const TeamSchema = new mongoose.Schema({ name: { type: String, + required: true, unique: true, - required: true }, members: { type: [{ diff --git a/routes/api/team.js b/routes/api/team.js index ff255e9d..03ad14e5 100644 --- a/routes/api/team.js +++ b/routes/api/team.js @@ -16,22 +16,18 @@ const Middleware = { Team: require("../../middlewares/team.middleware"), Auth: require("../../middlewares/auth.middleware"), }; -const Services = { - Hacker: require("../../services/hacker.service"), -}; module.exports = { activate: function (apiRouter) { const teamRouter = new express.Router(); /** - * @api {post} /team/ create a new team + * @api {post} /team/ create a new team consisting of only the logged in user * @apiName createTeam * @apiGroup Team * @apiVersion 0.0.8 * * @apiParam (body) {String} name Name of the team. - * @apiParam (body) {MongoID[]} [members] Array of members in team. * @apiParam (body) {String} [devpostURL] Devpost link to hack. Once the link is sent, the hack will be considered to be submitted. * @apiParam (body) {String} [projectName] Name of the team. * @@ -49,15 +45,14 @@ module.exports = { * {"message": "Error while creating team", "data": {}} */ teamRouter.route("/").post( + Middleware.Auth.ensureAuthenticated(), + Middleware.Auth.ensureAuthorized(), // Validators Middleware.Validator.Team.newTeamValidator, - Middleware.parseBody.middleware, + Middleware.Team.parseNewTeam, - Middleware.Team.parseTeam, - - // check that member is not already in a team - Middleware.Team.ensureUniqueHackerId, + Middleware.Team.ensureFreeTeamName, Middleware.Team.createTeam, Controllers.Team.createdTeam diff --git a/services/team.service.js b/services/team.service.js index 2107eafa..274a41c2 100644 --- a/services/team.service.js +++ b/services/team.service.js @@ -30,7 +30,7 @@ function findTeamByHackerId(hackerId) { * @return {Promise} The promise will resolve to a team object if save was successful. * @description Adds a new team to database. */ -function createTeam(teamDetails) { +async function createTeam(teamDetails) { const TAG = `[Team Service # createTeam]:`; const team = new Team(teamDetails); diff --git a/tests/team.test.js b/tests/team.test.js index db79ac31..4e2d590d 100644 --- a/tests/team.test.js +++ b/tests/team.test.js @@ -88,26 +88,136 @@ describe("GET team", function () { }); describe("POST create team", function () { - it("should SUCCEED and create a new team", function (done) { + it("should FAIL to create a new team due to lack of authentication", function (done) { chai.request(server.app) .post(`/api/team/`) .type("application/json") .send(util.team.newTeam1) .end(function (err, res) { - res.should.have.status(200); + res.should.have.status(401); res.should.be.json; res.body.should.have.property("message"); - res.body.message.should.equal(Constants.Success.TEAM_CREATE); + res.body.message.should.equal(Constants.Error.AUTH_401_MESSAGE); res.body.should.have.property("data"); - // deleting _id because that was generated, and not part of original data - const team = (new Team(util.team.newTeam1)).toJSON(); - delete res.body.data.id; - delete team.id; - chai.assert.equal(JSON.stringify(res.body.data), JSON.stringify(team)); done(); }); }); + + it("should FAIL to create a new team due to lack of authorization", function (done) { + util.auth.login(agent, util.account.Account3, (error) => { + if (error) { + agent.close(); + return done(error); + } + return agent + .post(`/api/team/`) + .type("application/json") + .send(util.team.newTeam1) + .end(function (err, res) { + 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(); + }); + }); + }); + + it("should FAIL to create a new team due to logged in user not being a hacker", function (done) { + util.auth.login(agent, util.account.Admin1, (error) => { + if (error) { + agent.close(); + return done(error); + } + return agent + .post(`/api/team/`) + .type("application/json") + .send(util.team.newTeam1) + .end(function (err, res) { + res.should.have.status(404); + res.should.be.json; + res.body.should.have.property("message"); + res.body.message.should.equal(Constants.Error.HACKER_404_MESSAGE); + res.body.should.have.property("data"); + + done(); + }); + }); + }); + + it("should FAIL to create a new team due to duplicate team name", function (done) { + util.auth.login(agent, util.account.Account2, (error) => { + if (error) { + agent.close(); + return done(error); + } + return agent + .post(`/api/team/`) + .type("application/json") + .send(util.team.duplicateTeamName1) + .end(function (err, res) { + res.should.have.status(409); + res.should.be.json; + res.body.should.have.property("message"); + res.body.message.should.equal(Constants.Error.TEAM_NAME_409_MESSAGE); + res.body.should.have.property("data"); + res.body.data.should.equal(util.team.duplicateTeamName1.name); + + done(); + }); + }); + }); + + it("should Fail to create a new team due to hacker already being in a team", function (done) { + util.auth.login(agent, util.account.Account1, (error) => { + if (error) { + agent.close(); + return done(error); + } + return agent + .post(`/api/team/`) + .type("application/json") + .send(util.team.newTeam1) + .end(function (err, res) { + res.should.have.status(409); + res.should.be.json; + res.body.should.have.property("message"); + res.body.message.should.equal(Constants.Error.TEAM_MEMBER_409_MESSAGE); + res.body.should.have.property("data"); + done(); + }); + }); + }); + + it("should SUCCEED and create a new team", function (done) { + util.auth.login(agent, util.account.Account2, (error) => { + if (error) { + agent.close(); + return done(error); + } + return agent + .post(`/api/team/`) + .type("application/json") + .send(util.team.newTeam1) + .end(function (err, res) { + res.should.have.status(200); + res.should.be.json; + res.body.should.have.property("message"); + res.body.message.should.equal(Constants.Success.TEAM_CREATE); + res.body.should.have.property("data"); + + // deleting _id because that was generated, and not part of original data + const team = (new Team(util.team.createdNewTeam1)).toJSON(); + delete res.body.data.id; + delete team.id; + chai.assert.equal(JSON.stringify(res.body.data), JSON.stringify(team)); + done(); + }); + }); + }); }); describe("PATCH change team", function () { diff --git a/tests/util/hacker.test.util.js b/tests/util/hacker.test.util.js index 457c1f11..ae8bea84 100644 --- a/tests/util/hacker.test.util.js +++ b/tests/util/hacker.test.util.js @@ -155,6 +155,7 @@ const HackerA = { "major": "EE", "graduationYear": 2019, "codeOfConduct": true, + "teamId": mongoose.Types.ObjectId.createFromTime(101), }; const HackerB = { "_id": mongoose.Types.ObjectId(), diff --git a/tests/util/team.test.util.js b/tests/util/team.test.util.js index d64046cf..66506f42 100644 --- a/tests/util/team.test.util.js +++ b/tests/util/team.test.util.js @@ -6,15 +6,25 @@ const Team = require("../../models/team.model"); const mongoose = require("mongoose"); const logger = require("../../services/logger.service"); +const duplicateTeamName1 = { + "name": "SilverTeam", + "projectName": "AProject" +}; + const newTeam1 = { "_id": mongoose.Types.ObjectId(), - "name": "BronzeTeam", + "name": "BronzeTeam1", + "projectName": "YetAnotherProject" +}; + +const createdNewTeam1 = { + "name": "BronzeTeam1", "members": [Util.Hacker.HackerB._id], "projectName": "YetAnotherProject" }; const Team1 = { - "_id": mongoose.Types.ObjectId(), + "_id": mongoose.Types.ObjectId.createFromTime(101), "name": "BronzeTeam", "members": [Util.Hacker.HackerA._id], "devpostURL": "justanother.devpost.com", @@ -66,6 +76,8 @@ async function dropAll() { module.exports = { newTeam1: newTeam1, + createdNewTeam1: createdNewTeam1, + duplicateTeamName1: duplicateTeamName1, Team1: Team1, Team2: Team2, Team3: Team3,