diff --git a/src/api/middleware/company.js b/src/api/middleware/company.js index 9cfc51ca..33da4a14 100644 --- a/src/api/middleware/company.js +++ b/src/api/middleware/company.js @@ -110,15 +110,18 @@ export const isNotDisabled = (owner) => async (req, res, next) => { }; export const canManageAccountSettings = (companyId) => async (req, res, next) => { - const company = await (new CompanyService()).findById(companyId, true); - - // only god or the same company can change account settings - if (!req.hasAdminPrivileges && company._id.toString() !== req.targetOwner) { - return next(new APIError( - HTTPStatus.FORBIDDEN, - ErrorTypes.FORBIDDEN, - ValidationReasons.INSUFFICIENT_PERMISSIONS_COMPANY_SETTINGS - )); + try { + const company = await (new CompanyService()).findById(companyId, true); + if (!req.hasAdminPrivileges && company._id.toString() !== req.targetOwner) { + return next(new APIError( + HTTPStatus.FORBIDDEN, + ErrorTypes.FORBIDDEN, + ValidationReasons.INSUFFICIENT_PERMISSIONS_COMPANY_SETTINGS + )); + } + return next(); + } catch (err) { + console.error(err); + return next(err); } - return next(); }; diff --git a/src/api/middleware/validators/company.js b/src/api/middleware/validators/company.js index 34c2b27a..6c905d95 100644 --- a/src/api/middleware/validators/company.js +++ b/src/api/middleware/validators/company.js @@ -75,7 +75,6 @@ export const deleteCompany = useExpressValidators([ existingCompanyParamValidator, ]); - export const getOffers = useExpressValidators([ existingCompanyParamValidator, ]); @@ -106,3 +105,28 @@ export const setDefaultValuesConcurrent = (req, res, next) => { } return next(); }; + +export const edit = useExpressValidators([ + existingCompanyParamValidator, + body("name", ValidationReasons.DEFAULT) + .optional() + .isString().withMessage(ValidationReasons.STRING).bail() + .isLength({ min: CompanyConstants.companyName.min_length }) + .withMessage(ValidationReasons.TOO_SHORT(CompanyConstants.companyName.min_length)) + .isLength({ max: CompanyConstants.companyName.max_length }) + .withMessage(ValidationReasons.TOO_LONG(CompanyConstants.companyName.max_length)), + body("bio", ValidationReasons.DEFAULT) + .optional() + .isString().withMessage(ValidationReasons.STRING).bail() + .isLength({ max: CompanyConstants.bio.max_length }) + .withMessage(ValidationReasons.TOO_LONG(CompanyConstants.bio.max_length)), + body("contacts", ValidationReasons.DEFAULT) + .optional() + .customSanitizer(ensureArray) + .isArray({ min: CompanyConstants.contacts.min_length, max: CompanyConstants.contacts.max_length }) + .withMessage(ValidationReasons.ARRAY_SIZE(CompanyConstants.contacts.min_length, CompanyConstants.contacts.max_length)), + body("logo", ValidationReasons.DEFAULT) + .optional() + .isString().withMessage(ValidationReasons.STRING).bail() + .isURL().withMessage(ValidationReasons.URL), +]); diff --git a/src/api/routes/company.js b/src/api/routes/company.js index 4b927961..bf9e83b6 100644 --- a/src/api/routes/company.js +++ b/src/api/routes/company.js @@ -202,4 +202,33 @@ export default (app) => { return next(err); } }); + + + /** + * Edit company details. + * Company or admin can edit. + */ + + router.put("/:companyId/edit", + or([ + authMiddleware.isCompany, + authMiddleware.isAdmin, + authMiddleware.isGod + ], { status_code: HTTPStatus.UNAUTHORIZED, error_code: ErrorTypes.FORBIDDEN, msg: ValidationReasons.INSUFFICIENT_PERMISSIONS }), + validators.edit, + (req, res, next) => companyMiddleware.canManageAccountSettings(req.params.companyId)(req, res, next), + (req, res, next) => companyMiddleware.isNotBlocked(req.params.companyId)(req, res, next), + (req, res, next) => companyMiddleware.isNotDisabled(req.params.companyId)(req, res, next), + async (req, res, next) => { + try { + const companyService = new CompanyService(); + const offerService = new OfferService(); + const company = await companyService.changeAttributes(req.params.companyId, req.body); + await offerService.updateAllOffersByCompanyId(company); + return res.json(company); + } catch (err) { + return next(err); + } + } + ); }; diff --git a/src/services/company.js b/src/services/company.js index 4fbed913..c3e42490 100644 --- a/src/services/company.js +++ b/src/services/company.js @@ -1,8 +1,9 @@ -import { COMPANY_BLOCKED_NOTIFICATION, +import { + COMPANY_BLOCKED_NOTIFICATION, COMPANY_UNBLOCKED_NOTIFICATION, COMPANY_DISABLED_NOTIFICATION, COMPANY_ENABLED_NOTIFICATION, - COMPANY_DELETED_NOTIFICATION + COMPANY_DELETED_NOTIFICATION, } from "../email-templates/companyManagement.js"; import EmailService from "../lib/emailService.js"; import Account from "../models/Account.js"; @@ -60,14 +61,14 @@ class CompanyService { if (!showHidden) query.withoutBlocked().withoutDisabled(); if (!showAdminReason) query.hideAdminReason(); const company = await query; - return company; + return company; } /** * @param {@param} companyId Id of the company */ async block(companyId, adminReason) { - const company = await Company.findByIdAndUpdate( + const company = await Company.findByIdAndUpdate( companyId, { isBlocked: true, @@ -96,12 +97,19 @@ class CompanyService { * @param {*} company_id id of the company * @param {*} attributes object containing the attributes to change in company */ - async changeAttributes(company_id, attributes) { - const company = await Company.findOneAndUpdate( - { _id: company_id }, - attributes, - { new: true }); - return company; + async changeAttributes(companyId, companyDetails) { + try { + const company = await Company.findOneAndUpdate( + { _id: companyId }, + companyDetails, + { new: true } + ); + + return company; + } catch (err) { + console.error(err); + throw err; + } } /** @@ -188,7 +196,6 @@ class CompanyService { throw err; } } - } export default CompanyService; diff --git a/src/services/offer.js b/src/services/offer.js index 4dc21254..bf63bb7f 100644 --- a/src/services/offer.js +++ b/src/services/offer.js @@ -468,6 +468,15 @@ class OfferService { await Offer.deleteMany({ owner: companyId }); } + async updateAllOffersByCompanyId(company) { + const changes = { + ownerName: company.name, + ownerLogo: company.logo, + contacts: company.contacts, + }; + const offers = await Offer.updateMany({ owner: company._id }, changes, { new: true }); + return offers; + } } export default OfferService; diff --git a/test/end-to-end/company.js b/test/end-to-end/company.js index 32c10d9c..59ce8090 100644 --- a/test/end-to-end/company.js +++ b/test/end-to-end/company.js @@ -49,6 +49,15 @@ describe("Company endpoint", () => { ...params, }); + const generateTestCompany = (params) => ({ + name: "Big Company", + bio: "Big Company Bio", + logo: "http://awebsite.com/alogo.jpg", + contacts: ["112", "122"], + hasFinishedRegistration: true, + ...params, + }); + describe("GET /company", () => { beforeAll(async () => { @@ -1933,4 +1942,268 @@ describe("Company endpoint", () => { expect(res.body).toHaveProperty("maxOffersReached", true); }); }); + + describe("PUT /company/edit", () => { + let test_companies; + let test_company, test_company_blocked, test_company_disabled; + let test_offer; + + const changing_values = { + name: "Changed name", + bio: "Changed bio", + logo: "http://awebsite.com/changedlogo.jpg", + contacts: ["123", "456"], + }; + + /* Admin, Company, Blocked, Disabled*/ + const test_users = Array(4).fill({}).map((_c, idx) => ({ + email: `test_email_${idx}@email.com`, + password: "password123", + })); + + const [test_user_admin, test_user_company, test_user_company_blocked, test_user_company_disabled] = test_users; + + const test_agent = agent(); + + beforeAll(async () => { + await Account.deleteMany({}); + + const test_company_data = await generateTestCompany(); + const test_company_blocked_data = await generateTestCompany({ isBlocked: true }); + const test_company_disabled_data = await generateTestCompany({ isDisabled: true }); + + test_companies = await Company.create( + [test_company_data, test_company_blocked_data, test_company_disabled_data], + { session: null } + ); + + [test_company, test_company_blocked, test_company_disabled] = test_companies; + + test_offer = await Offer.create( + generateTestOffer({ + owner: test_company._id, + ownerName: test_company.name, + ownerLogo: test_company.logo, + }) + ); + + for (let i = 0; i < test_users.length; i++) { + if (i === 0) { // Admin + await Account.create({ + email: test_users[i].email, + password: await hash(test_users[i].password), + isAdmin: true, + }); + } else { // Company + await Account.create({ + email: test_users[i].email, + password: await hash(test_users[i].password), + company: test_companies[i - 1]._id, + }); + } + } + }); + + afterEach(async () => { + await test_agent + .delete("/auth/login") + .expect(HTTPStatus.OK); + }); + + afterAll(async () => { + await Company.deleteMany({}); + await Account.deleteMany({}); + }); + + describe("ID Validation", () => { + beforeEach(async () => { + await test_agent + .post("/auth/login") + .send(test_user_admin) + .expect(HTTPStatus.OK); + }); + + test("Should fail if id is not a valid ObjectID", async () => { + const id = "123"; + const res = await test_agent + .put(`/company/${id}/edit`) + .send() + .expect(HTTPStatus.UNPROCESSABLE_ENTITY); + + expect(res.body.errors).toContainEqual( + { "location": "params", "msg": ValidationReasons.OBJECT_ID, "param": "companyId", "value": id } + ); + }); + + test("Should fail if id is not a valid company", async () => { + const id = "111111111111111111111111"; + + const res = await test_agent + .put(`/company/${id}/edit`) + .send() + .expect(HTTPStatus.UNPROCESSABLE_ENTITY); + + expect(res.body.errors).toContainEqual( + { "location": "params", "msg": ValidationReasons.COMPANY_NOT_FOUND(id), "param": "companyId", "value": id } + ); + }); + }); + + describe("Using a bad user", () => { + test("Should fail if different user", async () => { + await test_agent + .post("/auth/login") + .send(test_user_company_blocked) + .expect(HTTPStatus.OK); + + const res = await test_agent + .put(`/company/${test_company._id}/edit`) + .send({ + name: changing_values.name, + }) + .expect(HTTPStatus.FORBIDDEN); + + expect(res.body.errors).toContainEqual({ "msg": ValidationReasons.INSUFFICIENT_PERMISSIONS_COMPANY_SETTINGS }); + }); + + test("Should fail if not logged in", async () => { + const res = await test_agent + .put(`/company/${test_company._id}/edit`) + .send({ + bio: changing_values.bio, + contacts: changing_values.contacts, + }) + .expect(HTTPStatus.UNAUTHORIZED); + + expect(res.body.errors).toContainEqual({ "msg": ValidationReasons.INSUFFICIENT_PERMISSIONS }); + }); + }); + + describe("Using a good user", () => { + test("Should pass if god", async () => { + const res = await test_agent + .put(`/company/${test_company._id}/edit`) + .send(withGodToken({ + name: changing_values.name, + bio: changing_values.bio, + })) + .expect(HTTPStatus.OK); + + expect(res.body).toHaveProperty("name", changing_values.name); + expect(res.body).toHaveProperty("bio", changing_values.bio); + }); + + test("Should pass if admin", async () => { + await test_agent + .post("/auth/login") + .send(test_user_admin) + .expect(HTTPStatus.OK); + + const res = await test_agent + .put(`/company/${test_company._id}/edit`) + .send({ + name: changing_values.name + }) + .expect(HTTPStatus.OK); + + expect(res.body).toHaveProperty("name", changing_values.name); + }); + + test("Should pass if same company", async () => { + await test_agent + .post("/auth/login") + .send(test_user_company) + .expect(HTTPStatus.OK); + + const res = await test_agent + .put(`/company/${test_company._id}/edit`) + .send({ + name: changing_values.name, + }) + .expect(HTTPStatus.OK); + + expect(res.body).toHaveProperty("name", changing_values.name); + const changed_company = await Company.findById(test_company._id); + expect(changed_company.name).toBe(changing_values.name); + }); + }); + + test("Offer should be updated", async () => { + await test_agent + .post("/auth/login") + .send(test_user_company) + .expect(HTTPStatus.OK); + + const res = await test_agent + .put(`/company/${test_company._id}/edit`) + .send({ + name: changing_values.name, + contacts: changing_values.contacts, + }) + .expect(HTTPStatus.OK); + + test_offer = await Offer.findById(test_offer._id); + + expect(res.body).toHaveProperty("name", changing_values.name); + expect(res.body).toHaveProperty("contacts", changing_values.contacts); + + expect(test_offer.ownerName).toEqual(changing_values.name); + expect(test_offer.contacts).toEqual(changing_values.contacts); + }); + + describe("Using disabled/blocked company (god)", () => { + test("Should fail if company is blocked (god)", async () => { + const res = await test_agent + .put(`/company/${test_company_blocked._id}/edit`) + .send(withGodToken({ + name: "Changing Blocked Company", + })) + .expect(HTTPStatus.FORBIDDEN); + expect(res.body.errors).toContainEqual({ "msg": ValidationReasons.COMPANY_BLOCKED }); + }); + + test("Should fail if company is disabled (god)", async () => { + const res = await test_agent + .put(`/company/${test_company_disabled._id}/edit`) + .send(withGodToken({ + name: "Changing Disabled Company", + })) + .expect(HTTPStatus.FORBIDDEN); + expect(res.body.errors).toContainEqual({ "msg": ValidationReasons.COMPANY_DISABLED }); + }); + }); + + describe("Using disabled/blocked company (user)", () => { + test("Should fail if company is blocked (user)", async () => { + await test_agent + .post("/auth/login") + .send(test_user_company_blocked) + .expect(HTTPStatus.OK); + + const res = await test_agent + .put(`/company/${test_company_blocked._id}/edit`) + .send({ + name: "Changing Blocked Company", + }) + .expect(HTTPStatus.FORBIDDEN); + + expect(res.body.errors).toContainEqual({ "msg": ValidationReasons.COMPANY_BLOCKED }); + }); + + test("Should fail if company is disabled (user)", async () => { + await test_agent + .post("/auth/login") + .send(test_user_company_disabled) + .expect(HTTPStatus.OK); + + const res = await test_agent + .put(`/company/${test_company_disabled._id}/edit`) + .send({ + bio: "As user", + }) + .expect(HTTPStatus.FORBIDDEN); + expect(res.body.errors).toContainEqual({ "msg": ValidationReasons.COMPANY_DISABLED }); + }); + }); + }); });