From c2a11e548456059ebbc70f617b1a9e92520cd31e Mon Sep 17 00:00:00 2001 From: Nuno Pereira Date: Tue, 28 Mar 2023 17:44:04 +0100 Subject: [PATCH] Finished input validation for company application search --- src/api/middleware/validators/application.js | 34 +- src/api/middleware/validators/offer.js | 9 +- .../applications/company/:id/approve.js | 3 + .../applications/company/:id/reject.js | 3 + .../end-to-end/applications/company/search.js | 299 +++++++++++ test/end-to-end/review.js | 492 +++++------------- test/utils/ValidatorTester.js | 48 +- 7 files changed, 503 insertions(+), 385 deletions(-) create mode 100644 test/end-to-end/applications/company/:id/approve.js create mode 100644 test/end-to-end/applications/company/:id/reject.js create mode 100644 test/end-to-end/applications/company/search.js diff --git a/src/api/middleware/validators/application.js b/src/api/middleware/validators/application.js index 59cd85c9..38b3f1d7 100644 --- a/src/api/middleware/validators/application.js +++ b/src/api/middleware/validators/application.js @@ -60,6 +60,13 @@ export const reject = useExpressValidators([ .withMessage(ValidationReasons.TOO_SHORT(CompanyApplicationConstants.rejectReason.min_length)), ]); +const isAfterSubmissionDateFrom = (submissionDateTo, { req }) => { + + const { submissionDateFrom } = req.body; + + return submissionDateFrom <= submissionDateTo; +}; + const sortByParamValidator = (val) => { const regex = /^(\w+(:(desc|asc))?)(,\w+(:(desc|asc))?)*$/; @@ -84,31 +91,38 @@ const parseSortByField = (val) => val.split(","); export const search = useExpressValidators([ query("limit", ValidationReasons.DEFAULT) .optional() - .isInt({ min: 1, max: MAX_LIMIT_RESULTS }) - .withMessage(ValidationReasons.MAX(MAX_LIMIT_RESULTS)), + .isInt().withMessage(ValidationReasons.INT).bail() + .toInt() + .isInt({ min: 1 }).withMessage(ValidationReasons.MIN(1)).bail() + .isInt({ max: MAX_LIMIT_RESULTS }).withMessage(ValidationReasons.MAX(MAX_LIMIT_RESULTS)).bail() + .toInt(), query("offset", ValidationReasons.DEFAULT) .optional() - .isInt({ min: 0 }) - .withMessage(ValidationReasons.MIN(0)), + .isInt().withMessage(ValidationReasons.INT).bail() + .toInt() + .isInt({ min: 0 }).withMessage(ValidationReasons.MIN(0)).bail() + .toInt(), query("companyName", ValidationReasons.DEFAULT) .optional() - .isString().withMessage(ValidationReasons.STRING), + .isString().withMessage(ValidationReasons.STRING).bail(), query("state", ValidationReasons.DEFAULT) .optional() - .customSanitizer(ensureArray) .isArray().withMessage(ValidationReasons.ARRAY).bail() + .customSanitizer(ensureArray) .custom(valuesInSet(Object.keys(ApplicationStatus))), query("submissionDateFrom", ValidationReasons.DEFAULT) .optional() - .toDate() - .isISO8601().withMessage(ValidationReasons.DATE), + .isISO8601().withMessage(ValidationReasons.DATE).bail() + .toDate(), query("submissionDateTo", ValidationReasons.DEFAULT) .optional() + .isISO8601().withMessage(ValidationReasons.DATE).bail() .toDate() - .isISO8601().withMessage(ValidationReasons.DATE), + .if((submissionDateTo, { req }) => req.query.submissionDateFrom !== undefined) + .custom(isAfterSubmissionDateFrom).withMessage(ValidationReasons.MUST_BE_AFTER("submissionDateFrom")), query("sortBy", ValidationReasons.DEFAULT) .optional() - .isString().withMessage(ValidationReasons.STRING) + .isString().withMessage(ValidationReasons.STRING).bail() .custom(sortByParamValidator) .customSanitizer(parseSortByField), ]); diff --git a/src/api/middleware/validators/offer.js b/src/api/middleware/validators/offer.js index c306f099..ad76ce25 100644 --- a/src/api/middleware/validators/offer.js +++ b/src/api/middleware/validators/offer.js @@ -80,7 +80,6 @@ export const create = useExpressValidators([ .custom(publishEndDateAfterPublishDate) .custom(publishEndDateLimit), - body("jobMinDuration", ValidationReasons.DEFAULT) .exists().withMessage(ValidationReasons.REQUIRED).bail() .isInt().withMessage(ValidationReasons.INT), @@ -119,7 +118,7 @@ export const create = useExpressValidators([ body("jobType", ValidationReasons.DEFAULT) .exists().withMessage(ValidationReasons.REQUIRED).bail() .isString().withMessage(ValidationReasons.STRING).bail() - .isIn(JobTypes).withMessage(ValidationReasons.IN_ARRAY(JobTypes)), + .isIn(JobTypes).withMessage((value) => ValidationReasons.IN_ARRAY(JobTypes, value)), body("fields", ValidationReasons.DEFAULT) .exists().withMessage(ValidationReasons.REQUIRED).bail() @@ -388,7 +387,7 @@ export const edit = useExpressValidators([ body("jobType", ValidationReasons.DEFAULT) .optional() .isString().withMessage(ValidationReasons.STRING).bail() - .isIn(JobTypes).withMessage(ValidationReasons.IN_ARRAY(JobTypes)), + .isIn(JobTypes).withMessage((value) => ValidationReasons.IN_ARRAY(JobTypes, value)), body("fields", ValidationReasons.DEFAULT) .optional() @@ -495,7 +494,7 @@ export const get = useExpressValidators([ query("jobType") .optional() .isString().withMessage(ValidationReasons.STRING).bail() - .isIn(JobTypes).withMessage(ValidationReasons.IN_ARRAY(JobTypes)), + .isIn(JobTypes).withMessage((value) => ValidationReasons.IN_ARRAY(JobTypes, value)), query("jobMinDuration", ValidationReasons.DEFAULT) .optional() @@ -522,7 +521,7 @@ export const get = useExpressValidators([ query("sortBy", ValidationReasons.DEFAULT) .optional() .isString().withMessage(ValidationReasons.STRING).bail() - .isIn(OfferConstants.SortableFields).withMessage(ValidationReasons.IN_ARRAY(OfferConstants.SortableFields)), + .isIn(OfferConstants.SortableFields).withMessage((value) => ValidationReasons.IN_ARRAY(OfferConstants.SortableFields, value)), query("descending", ValidationReasons.DEFAULT) .optional() diff --git a/test/end-to-end/applications/company/:id/approve.js b/test/end-to-end/applications/company/:id/approve.js new file mode 100644 index 00000000..8fc357bf --- /dev/null +++ b/test/end-to-end/applications/company/:id/approve.js @@ -0,0 +1,3 @@ +test("should return true", () => { + expect(true).toBe(true); +}); diff --git a/test/end-to-end/applications/company/:id/reject.js b/test/end-to-end/applications/company/:id/reject.js new file mode 100644 index 00000000..8fc357bf --- /dev/null +++ b/test/end-to-end/applications/company/:id/reject.js @@ -0,0 +1,3 @@ +test("should return true", () => { + expect(true).toBe(true); +}); diff --git a/test/end-to-end/applications/company/search.js b/test/end-to-end/applications/company/search.js new file mode 100644 index 00000000..16627df0 --- /dev/null +++ b/test/end-to-end/applications/company/search.js @@ -0,0 +1,299 @@ +import { StatusCodes } from "http-status-codes"; +import CompanyApplication, { CompanyApplicationProps } from "../../../../src/models/CompanyApplication"; +import hash from "../../../../src/lib/passwordHashing"; +import Account from "../../../../src/models/Account"; +import ApplicationStatus from "../../../../src/models/constants/ApplicationStatus"; + +import { MAX_LIMIT_RESULTS } from "../../../../src/api/middleware/validators/application"; + +import ValidatorTester from "../../../utils/ValidatorTester"; +// import ValidationReasons from "../../../../src/api/middleware/validators/validationReasons"; + +describe("GET /applications/company/search", () => { + + const test_agent = agent(); + const test_user_admin = { + email: "admin@email.com", + password: "password123", + }; + + const pendingApplication = { + email: "test2@test.com", + password: "password123", + companyName: "testing company", + motivation: "This company has a very valid motivation, because otherwise the tests would not exist.", + submittedAt: new Date("2019-11-25").toISOString(), + }; + + const approvedApplication = { + ...pendingApplication, + submittedAt: new Date("2019-11-24").toISOString(), + approvedAt: new Date(Date.parse(pendingApplication.submittedAt) + (24 * 60 * 60 * 1000)).toISOString(), + companyName: "approved Testing company", + email: `approved${pendingApplication.email}`, + }; + + const rejectedApplication = { + ...pendingApplication, + submittedAt: new Date("2019-11-23").toISOString(), + rejectedAt: new Date(Date.parse(pendingApplication.submittedAt) + (24 * 60 * 60 * 1000)).toISOString(), + companyName: "rejected Testing company", + email: `rejected${pendingApplication.email}`, + rejectReason: "2bad4nij0bs", + }; + + beforeAll(async () => { + await Account.deleteMany({}); + await Account.create({ email: test_user_admin.email, password: await hash(test_user_admin.password), isAdmin: true }); + }); + + beforeEach(async () => { + await CompanyApplication.deleteMany({}); + + // Login by default + await test_agent + .post("/auth/login") + .send(test_user_admin) + .expect(StatusCodes.OK); + }); + + afterAll(async () => { + await Account.deleteMany({}); + await CompanyApplication.deleteMany({}); + }); + + describe("Input Validation", () => { + + const EndpointValidatorTester = ValidatorTester( + (params) => test_agent.get("/applications/company/search").query(params) + ); + const QueryValidatorTester = EndpointValidatorTester("query"); + + describe("limit", () => { + const FieldValidatorTester = QueryValidatorTester("limit"); + FieldValidatorTester.mustBeNumber(); + FieldValidatorTester.mustBeGreaterThanOrEqualTo(1); + FieldValidatorTester.mustBeLessThanOrEqualTo(MAX_LIMIT_RESULTS); + }); + + describe("offset", () => { + const FieldValidatorTester = QueryValidatorTester("offset"); + FieldValidatorTester.mustBeNumber(); + FieldValidatorTester.mustBeGreaterThanOrEqualTo(0); + }); + + describe("companyName", () => { + // the only validation that could be done on this is to test if the value is a string. + // However, since this is coming from the query, it is always parsed as a string, so this check would never be exercised + }); + + describe("state", () => { + const FieldValidatorTester = QueryValidatorTester("state"); + FieldValidatorTester.mustBeArray(); + }); + + describe("submissionDateFrom", () => { + const FieldValidatorTester = QueryValidatorTester("submissionDateFrom"); + FieldValidatorTester.mustBeDate(); + }); + + describe("submissionDateTo", () => { + const FieldValidatorTester = QueryValidatorTester("submissionDateTo"); + FieldValidatorTester.mustBeDate(); + FieldValidatorTester.mustBeAfter("submissionDateFrom"); + }); + + describe("sortBy", () => { + const FieldValidatorTester = QueryValidatorTester("sortBy"); + // FieldValidatorTester.mustBeString(); Same reason as above + + // Validation for this is harder to perform since there is custom validation employed + // Perhaps we could employ a custom test, leaving as TODO + FieldValidatorTester.mustBeInArray(Object.keys(CompanyApplicationProps)); + }); + }); + + test("Should fail to search company applications if not logged in", async () => { + + await test_agent + .delete("/auth/login") + .expect(StatusCodes.OK); + + const res = await request() + .get("/applications/company/search"); + + expect(res.status).toBe(StatusCodes.UNAUTHORIZED); + }); + + test("Should return empty list if no applications exist", async () => { + const emptyRes = await test_agent + .get("/applications/company/search"); + + expect(emptyRes.status).toBe(StatusCodes.OK); + expect(emptyRes.body.applications).toEqual([]); + }); + + test("Should list existing applications", async () => { + const application = { + email: "test2@test.com", + password: "password123", + companyName: "Testing company", + motivation: "This company has a very valid motivation, because otherwise the tests would not exist.", + }; + + await CompanyApplication.create({ + ...application, + submittedAt: Date.now(), + }); + + const nonEmptyRes = await test_agent + .get("/applications/company/search"); + + expect(nonEmptyRes.status).toBe(StatusCodes.OK); + expect(nonEmptyRes.body.applications.length).toBe(1); + expect(nonEmptyRes.body.applications[0]).toHaveProperty("email", application.email); + + }); + + describe("Filter application results", () => { + + beforeEach(async () => { + await CompanyApplication.create([pendingApplication, approvedApplication, rejectedApplication]); + }); + + afterEach(async () => { + await CompanyApplication.deleteMany({}); + }); + + test("Should filter by company name", async () => { + const fullNameQuery = await test_agent + .get(`/applications/company/search?companyName=${"approved Testing company"}`); + + expect(fullNameQuery.status).toBe(StatusCodes.OK); + expect(fullNameQuery.body.applications).toHaveLength(1); + expect(fullNameQuery.body.applications[0]).toHaveProperty("companyName", approvedApplication.companyName); + + const partialNameQuery = await test_agent + .get(`/applications/company/search?companyName=${"Testing company"}`); + + expect(partialNameQuery.status).toBe(StatusCodes.OK); + expect(partialNameQuery.body.applications).toHaveLength(3); + expect(partialNameQuery.body.applications[0]).toHaveProperty("companyName", pendingApplication.companyName); + expect(partialNameQuery.body.applications[1]).toHaveProperty("companyName", approvedApplication.companyName); + expect(partialNameQuery.body.applications[2]).toHaveProperty("companyName", rejectedApplication.companyName); + }); + + test("Should filter by state", async () => { + + const wrongFormatQuery = await test_agent + .get(`/applications/company/search?state[]=<["${ApplicationStatus.APPROVED}"]`); + + expect(wrongFormatQuery.status).toBe(StatusCodes.UNPROCESSABLE_ENTITY); + expect(wrongFormatQuery.body.errors[0]).toStrictEqual({ + location: "query", + msg: "must-be-in:[PENDING,APPROVED,REJECTED]", // FIXME: ValidationReasons.IN_ARRAY(ApplicationStatus), + param: "state", + value: [`<["${ApplicationStatus.APPROVED}"]`] + }); + + const singleStateQuery = await test_agent + .get(`/applications/company/search?state[]=${ApplicationStatus.APPROVED}`) + .expect(StatusCodes.OK); + + expect(singleStateQuery.body.applications.length).toBe(1); + expect(singleStateQuery.body.applications[0]).toHaveProperty("companyName", approvedApplication.companyName); + + const multiStateQuery = await test_agent + .get("/applications/company/search?").query({ state: [ApplicationStatus.APPROVED, ApplicationStatus.PENDING] }); + + expect(multiStateQuery.status).toBe(StatusCodes.OK); + expect(multiStateQuery.body.applications.length).toBe(2); + expect(multiStateQuery.body.applications[0]).toHaveProperty("companyName", pendingApplication.companyName); + expect(multiStateQuery.body.applications[1]).toHaveProperty("companyName", approvedApplication.companyName); + }); + + test("Should filter by date", async () => { + + const afterQuery = await test_agent + .get(`/applications/company/search?submissionDateFrom=${approvedApplication.submittedAt}`) + .expect(StatusCodes.OK); + + expect(afterQuery.body.applications.length).toBe(2); + expect(afterQuery.body.applications[0]).toHaveProperty("companyName", pendingApplication.companyName); + expect(afterQuery.body.applications[1]).toHaveProperty("companyName", approvedApplication.companyName); + + const untilQuery = await test_agent + .get(`/applications/company/search?submissionDateTo=${approvedApplication.submittedAt}`) + .expect(StatusCodes.OK); + + expect(untilQuery.body.applications.length).toBe(2); + expect(untilQuery.body.applications[0]).toHaveProperty("companyName", approvedApplication.companyName); + expect(untilQuery.body.applications[1]).toHaveProperty("companyName", rejectedApplication.companyName); + + const intervalQuery = await test_agent + .get("/applications/company/search?" + + `submissionDateFrom=${approvedApplication.submittedAt}&` + + `submissionDateTo=${approvedApplication.submittedAt}`); + + console.info(intervalQuery.body); + + expect(intervalQuery.status).toBe(StatusCodes.OK); + expect(intervalQuery.body.applications.length).toBe(1); + expect(intervalQuery.body.applications[0]).toHaveProperty("companyName", approvedApplication.companyName); + + }); + }); + + describe("Sort application results", () => { + + beforeEach(async () => { + await CompanyApplication.create([pendingApplication, approvedApplication, rejectedApplication]); + }); + + afterEach(async () => { + await CompanyApplication.deleteMany({}); + }); + + test("Should sort by company name ascending", async () => { + const query = await test_agent + .get("/applications/company/search?sortBy=companyName:asc"); + + expect(query.status).toBe(StatusCodes.OK); + expect(query.body.applications.length).toBe(3); + expect(query.body.applications[0]).toHaveProperty("companyName", approvedApplication.companyName); + expect(query.body.applications[1]).toHaveProperty("companyName", rejectedApplication.companyName); + expect(query.body.applications[2]).toHaveProperty("companyName", pendingApplication.companyName); + }); + + test("Should sort by company name descending", async () => { + const query = await test_agent + .get("/applications/company/search?sortBy=companyName:desc"); + + expect(query.status).toBe(StatusCodes.OK); + expect(query.body.applications.length).toBe(3); + expect(query.body.applications[0]).toHaveProperty("companyName", pendingApplication.companyName); + expect(query.body.applications[1]).toHaveProperty("companyName", rejectedApplication.companyName); + expect(query.body.applications[2]).toHaveProperty("companyName", approvedApplication.companyName); + }); + + test("Should sort by submissionDate descending", async () => { + const defaultQuery = await test_agent + .get("/applications/company/search"); + + expect(defaultQuery.status).toBe(StatusCodes.OK); + expect(defaultQuery.body.applications.length).toBe(3); + expect(defaultQuery.body.applications[0]).toHaveProperty("companyName", pendingApplication.companyName); + expect(defaultQuery.body.applications[1]).toHaveProperty("companyName", approvedApplication.companyName); + expect(defaultQuery.body.applications[2]).toHaveProperty("companyName", rejectedApplication.companyName); + + const query = await test_agent + .get("/applications/company/search?sortBy=submittedAt:desc"); + + expect(query.status).toBe(StatusCodes.OK); + expect(query.body.applications.length).toBe(3); + expect(query.body.applications[0]).toHaveProperty("companyName", pendingApplication.companyName); + expect(query.body.applications[1]).toHaveProperty("companyName", approvedApplication.companyName); + expect(query.body.applications[2]).toHaveProperty("companyName", rejectedApplication.companyName); + }); + }); +}); diff --git a/test/end-to-end/review.js b/test/end-to-end/review.js index aab25cdc..5405af92 100644 --- a/test/end-to-end/review.js +++ b/test/end-to-end/review.js @@ -1,454 +1,212 @@ jest.mock("../../src/lib/emailService"); import EmailService, { EmailService as EmailServiceClass } from "../../src/lib/emailService"; jest.spyOn(EmailServiceClass.prototype, "verifyConnection").mockImplementation(() => Promise.resolve()); -import { StatusCodes as HTTPStatus } from "http-status-codes"; +import { StatusCodes } from "http-status-codes"; import CompanyApplication, { CompanyApplicationRules } from "../../src/models/CompanyApplication"; -import hash from "../../src/lib/passwordHashing"; import Account from "../../src/models/Account"; import { ErrorTypes } from "../../src/api/middleware/errorHandler"; import ApplicationStatus from "../../src/models/constants/ApplicationStatus"; import { APPROVAL_NOTIFICATION, REJECTION_NOTIFICATION } from "../../src/email-templates/companyApplicationApproval"; import mongoose from "mongoose"; +import hash from "../../src/lib/passwordHashing"; const { ObjectId } = mongoose.Types; describe("Company application review endpoint test", () => { - describe("/applications/company", () => { - - describe("Without Auth", () => { - beforeEach(async () => { - await CompanyApplication.deleteMany({}); - }); + const test_agent = agent(); + const test_user_admin = { + email: "admin@email.com", + password: "password123", + }; - test("Should return HTTP 401 error", async () => { - const emptyRes = await request() - .get("/applications/company/search"); + beforeAll(async () => { + await Account.deleteMany({}); + await Account.create({ email: test_user_admin.email, password: await hash(test_user_admin.password), isAdmin: true }); + }); - expect(emptyRes.status).toBe(HTTPStatus.UNAUTHORIZED); - }); + beforeEach(async () => { + await test_agent + .post("/auth/login") + .send(test_user_admin) + .expect(StatusCodes.OK); + }); - }); + describe("/applications/company", () => { - describe("With Auth", () => { - const test_agent = agent(); - const test_user = { - email: "user@email.com", + describe("Approval/Rejection", () => { + let application; + const pendingApplication = { + email: "test2@test.com", password: "password123", + companyName: "Testing company", + motivation: "This company has a very valid motivation, because otherwise the tests would not exist.", + submittedAt: new Date("2019-11-25"), }; - beforeAll(async () => { - await Account.deleteMany({}); - await Account.create({ email: test_user.email, password: await hash(test_user.password), isAdmin: true }); - - // Login - await test_agent - .post("/auth/login") - .send(test_user) - .expect(200); - }); - - beforeEach(async () => { - await CompanyApplication.deleteMany({}); - }); - - test("Should list existing applications", async () => { - const emptyRes = await test_agent - .get("/applications/company/search"); - - expect(emptyRes.status).toBe(HTTPStatus.OK); - expect(emptyRes.body.applications).toEqual([]); - - const application = { - email: "test2@test.com", - password: "password123", - companyName: "Testing company", - motivation: "This company has a very valid motivation, because otherwise the tests would not exist.", - }; - - await CompanyApplication.create({ - ...application, - submittedAt: Date.now(), - }); - - const nonEmptyRes = await test_agent - .get("/applications/company/search"); - - expect(nonEmptyRes.status).toBe(HTTPStatus.OK); - expect(nonEmptyRes.body.applications.length).toBe(1); - expect(nonEmptyRes.body.applications[0]).toHaveProperty("email", application.email); - }); - - describe("Filter application results", () => { - - const pendingApplication = { - email: "test2@test.com", - password: "password123", - companyName: "Testing company", - motivation: "This company has a very valid motivation, because otherwise the tests would not exist.", - submittedAt: new Date("2019-11-25"), - }; - - const approvedApplication = { - ...pendingApplication, - submittedAt: new Date("2019-11-24"), - approvedAt: pendingApplication.submittedAt.getTime() + 1, - companyName: "approved Testing company", - email: `approved${pendingApplication.email}`, - }; - const rejectedApplication = { ...pendingApplication, - submittedAt: new Date("2019-11-23"), - rejectedAt: pendingApplication.submittedAt.getTime() + 1, - companyName: "rejected Testing company", - email: `rejected${pendingApplication.email}`, - rejectReason: "2bad4nij0bs", - }; + describe("Approve application", () => { beforeEach(async () => { - await CompanyApplication.create(pendingApplication); - await CompanyApplication.create(approvedApplication); - await CompanyApplication.create(rejectedApplication); + await Account.deleteMany({ email: pendingApplication.email }); + application = await CompanyApplication.create(pendingApplication); }); afterEach(async () => { await CompanyApplication.deleteMany({}); }); - test("Should filter by company name", async () => { - const fullNameQuery = await test_agent - .get(`/applications/company/search?companyName=${"approved Testing company"}`); - - expect(fullNameQuery.status).toBe(HTTPStatus.OK); - expect(fullNameQuery.body.applications.length).toBe(1); - expect(fullNameQuery.body.applications[0]).toHaveProperty("companyName", approvedApplication.companyName); + test("Should approve pending application", async () => { - const partialNameQuery = await test_agent - .get(`/applications/company/search?companyName=${"Testing company"}`); + const res = await test_agent + .post(`/applications/company/${application._id}/approve`); - expect(partialNameQuery.status).toBe(HTTPStatus.OK); - expect(partialNameQuery.body.applications.length).toBe(3); - expect(partialNameQuery.body.applications[0]).toHaveProperty("companyName", pendingApplication.companyName); - expect(partialNameQuery.body.applications[1]).toHaveProperty("companyName", approvedApplication.companyName); - expect(partialNameQuery.body.applications[2]).toHaveProperty("companyName", rejectedApplication.companyName); + expect(res.status).toBe(StatusCodes.OK); + expect(res.body.email).toBe(pendingApplication.email); + expect(res.body.companyName).toBe(pendingApplication.companyName); }); - test("Should filter by state", async () => { + test("Should send approval email to company email", async () => { - const wrongFormatQuery = await test_agent - .get(`/applications/company/search?state=<["${ApplicationStatus.APPROVED}"]`); + const res = await test_agent + .post(`/applications/company/${application._id}/approve`); - expect(wrongFormatQuery.status).toBe(HTTPStatus.UNPROCESSABLE_ENTITY); - expect(wrongFormatQuery.body.errors[0]).toStrictEqual({ - location: "query", - msg: "must-be-in:[PENDING,APPROVED,REJECTED]", - param: "state", - value: [`<["${ApplicationStatus.APPROVED}"]`] - }); + expect(res.status).toBe(StatusCodes.OK); + + const emailOptions = APPROVAL_NOTIFICATION(application.companyName); + expect(EmailService.sendMail).toHaveBeenCalledWith({ + subject: emailOptions.subject, + to: application.email, + template: emailOptions.template, + context: emailOptions.context, + }); - const singleStateQuery = await test_agent - .get("/applications/company/search").query({ state: [ApplicationStatus.APPROVED] }); + }); - expect(singleStateQuery.status).toBe(HTTPStatus.OK); - expect(singleStateQuery.body.applications.length).toBe(1); - expect(singleStateQuery.body.applications[0]).toHaveProperty("companyName", approvedApplication.companyName); + test("Should fail if trying to approve inexistent application", async () => { - const multiStateQuery = await test_agent - .get("/applications/company/search?").query({ state: [ApplicationStatus.APPROVED, ApplicationStatus.PENDING] }); + const res = await test_agent + .post(`/applications/company/${new ObjectId()}/approve`); - expect(multiStateQuery.status).toBe(HTTPStatus.OK); - expect(multiStateQuery.body.applications.length).toBe(2); - expect(multiStateQuery.body.applications[0]).toHaveProperty("companyName", pendingApplication.companyName); - expect(multiStateQuery.body.applications[1]).toHaveProperty("companyName", approvedApplication.companyName); + expect(res.status).toBe(StatusCodes.NOT_FOUND); }); - test("Should filter by date", async () => { + test("Should fail if trying to approve already approved application", async () => { + await test_agent + .post(`/applications/company/${application._id}/approve`); + + const res = await test_agent + .post(`/applications/company/${application._id}/approve`); + + expect(res.status).toBe(StatusCodes.CONFLICT); + }); - const afterQuery = await test_agent - .get(`/applications/company/search?submissionDateFrom=${approvedApplication.submittedAt}`); + test("Should fail if trying to approve already rejected application", async () => { + await test_agent + .post(`/applications/company/${application._id}/reject`) + .send({ rejectReason: "Some reason which is valid" }); - expect(afterQuery.status).toBe(HTTPStatus.OK); - expect(afterQuery.body.applications.length).toBe(2); - expect(afterQuery.body.applications[0]).toHaveProperty("companyName", pendingApplication.companyName); - expect(afterQuery.body.applications[1]).toHaveProperty("companyName", approvedApplication.companyName); - const untilQuery = await test_agent - .get(`/applications/company/search?submissionDateTo=${approvedApplication.submittedAt}`); + const res = await test_agent + .post(`/applications/company/${application._id}/approve`); - expect(untilQuery.status).toBe(HTTPStatus.OK); - expect(untilQuery.body.applications.length).toBe(2); - expect(untilQuery.body.applications[0]).toHaveProperty("companyName", approvedApplication.companyName); - expect(untilQuery.body.applications[1]).toHaveProperty("companyName", rejectedApplication.companyName); + expect(res.status).toBe(StatusCodes.CONFLICT); + }); - const intervalQuery = await test_agent - .get("/applications/company/search?" + - `submissionDateFrom=${approvedApplication.submittedAt}&` + - `submissionDateTo=${approvedApplication.submittedAt}`); + test("Should fail if approving application with an existing account with same email, and then rollback", async () => { + await Account.create({ email: application.email, password: "passwordHashedButNotReally", isAdmin: true }); + const res = await test_agent + .post(`/applications/company/${application._id}/approve`); - expect(intervalQuery.status).toBe(HTTPStatus.OK); - expect(intervalQuery.body.applications.length).toBe(1); - expect(intervalQuery.body.applications[0]).toHaveProperty("companyName", approvedApplication.companyName); + expect(res.status).toBe(StatusCodes.CONFLICT); + expect(res.body.error_code).toBe(ErrorTypes.VALIDATION_ERROR); + expect(res.body.errors[0].msg).toBe(CompanyApplicationRules.EMAIL_ALREADY_IN_USE.msg); + const result_application = await CompanyApplication.findById(application._id); + expect(result_application.state).toBe(ApplicationStatus.PENDING); }); - }); - describe("Sort application results", () => { - - const pendingApplication = { - email: "test2@test.com", - password: "password123", - companyName: "testing company", - motivation: "This company has a very valid motivation, because otherwise the tests would not exist.", - submittedAt: new Date("2019-11-25"), - }; - - const approvedApplication = { - ...pendingApplication, - submittedAt: new Date("2019-11-24"), - approvedAt: pendingApplication.submittedAt.getTime() + 1, - companyName: "approved Testing company", - email: `approved${pendingApplication.email}`, - }; - const rejectedApplication = { ...pendingApplication, - submittedAt: new Date("2019-11-23"), - rejectedAt: pendingApplication.submittedAt.getTime() + 1, - companyName: "rejected Testing company", - email: `rejected${pendingApplication.email}`, - rejectReason: "2bad4nij0bs", - }; + describe("Reject application", () => { beforeEach(async () => { - await CompanyApplication.create(pendingApplication); - await CompanyApplication.create(approvedApplication); - await CompanyApplication.create(rejectedApplication); + await Account.deleteMany({ email: pendingApplication.email }); + application = await CompanyApplication.create(pendingApplication); }); afterEach(async () => { await CompanyApplication.deleteMany({}); }); - test("Should sort by company name ascending", async () => { - const query = await test_agent - .get("/applications/company/search?sortBy=companyName:asc"); - - expect(query.status).toBe(HTTPStatus.OK); - expect(query.body.applications.length).toBe(3); - expect(query.body.applications[0]).toHaveProperty("companyName", approvedApplication.companyName); - expect(query.body.applications[1]).toHaveProperty("companyName", rejectedApplication.companyName); - expect(query.body.applications[2]).toHaveProperty("companyName", pendingApplication.companyName); - }); - - test("Should sort by company name descending", async () => { - const query = await test_agent - .get("/applications/company/search?sortBy=companyName:desc"); + test("Should fail if no rejectReason provided", async () => { + const res = await test_agent + .post(`/applications/company/${application._id}/reject`); + expect(res.status).toBe(StatusCodes.UNPROCESSABLE_ENTITY); + expect(res.body.errors[0]).toStrictEqual({ location: "body", msg: "required", param: "rejectReason" }); - expect(query.status).toBe(HTTPStatus.OK); - expect(query.body.applications.length).toBe(3); - expect(query.body.applications[0]).toHaveProperty("companyName", pendingApplication.companyName); - expect(query.body.applications[1]).toHaveProperty("companyName", rejectedApplication.companyName); - expect(query.body.applications[2]).toHaveProperty("companyName", approvedApplication.companyName); }); - test("Should sort by submissionDate descending", async () => { - const defaultQuery = await test_agent - .get("/applications/company/search"); + test("Should reject pending application", async () => { + const res = await test_agent + .post(`/applications/company/${application._id}/reject`) + .send({ rejectReason: "Some reason which is valid" }); - expect(defaultQuery.status).toBe(HTTPStatus.OK); - expect(defaultQuery.body.applications.length).toBe(3); - expect(defaultQuery.body.applications[0]).toHaveProperty("companyName", pendingApplication.companyName); - expect(defaultQuery.body.applications[1]).toHaveProperty("companyName", approvedApplication.companyName); - expect(defaultQuery.body.applications[2]).toHaveProperty("companyName", rejectedApplication.companyName); - - const query = await test_agent - .get("/applications/company/search?sortBy=submittedAt:desc"); - - expect(query.status).toBe(HTTPStatus.OK); - expect(query.body.applications.length).toBe(3); - expect(query.body.applications[0]).toHaveProperty("companyName", pendingApplication.companyName); - expect(query.body.applications[1]).toHaveProperty("companyName", approvedApplication.companyName); - expect(query.body.applications[2]).toHaveProperty("companyName", rejectedApplication.companyName); + expect(res.status).toBe(StatusCodes.OK); + expect(res.body.email).toBe(pendingApplication.email); + expect(res.body.companyName).toBe(pendingApplication.companyName); }); - }); - - describe("Approval/Rejection", () => { - let application; - const pendingApplication = { - email: "test2@test.com", - password: "password123", - companyName: "Testing company", - motivation: "This company has a very valid motivation, because otherwise the tests would not exist.", - submittedAt: new Date("2019-11-25"), - }; + test("Should send rejection email to company email", async () => { + const res = await test_agent + .post(`/applications/company/${application._id}/reject`) + .send({ rejectReason: "Some reason which is valid" }); - describe("Approve application", () => { + expect(res.status).toBe(StatusCodes.OK); - beforeEach(async () => { - await Account.deleteMany({ email: pendingApplication.email }); - application = await CompanyApplication.create(pendingApplication); - }); + const emailOptions = REJECTION_NOTIFICATION(application.companyName); - afterEach(async () => { - await CompanyApplication.deleteMany({}); + expect(EmailService.sendMail).toHaveBeenCalledWith({ + subject: emailOptions.subject, + to: application.email, + template: emailOptions.template, + context: emailOptions.context, }); - test("Should approve pending application", async () => { - - const res = await test_agent - .post(`/applications/company/${application._id}/approve`); - - expect(res.status).toBe(HTTPStatus.OK); - expect(res.body.email).toBe(pendingApplication.email); - expect(res.body.companyName).toBe(pendingApplication.companyName); - }); - - test("Should send approval email to company email", async () => { - - const res = await test_agent - .post(`/applications/company/${application._id}/approve`); - - expect(res.status).toBe(HTTPStatus.OK); - - const emailOptions = APPROVAL_NOTIFICATION(application.companyName); - - expect(EmailService.sendMail).toHaveBeenCalledWith({ - subject: emailOptions.subject, - to: application.email, - template: emailOptions.template, - context: emailOptions.context, - }); - - }); - - test("Should fail if trying to approve inexistent application", async () => { - - const res = await test_agent - .post(`/applications/company/${new ObjectId()}/approve`); - - expect(res.status).toBe(HTTPStatus.NOT_FOUND); - }); - - test("Should fail if trying to approve already approved application", async () => { - await test_agent - .post(`/applications/company/${application._id}/approve`); - - const res = await test_agent - .post(`/applications/company/${application._id}/approve`); - - expect(res.status).toBe(HTTPStatus.CONFLICT); - }); - - test("Should fail if trying to approve already rejected application", async () => { - await test_agent - .post(`/applications/company/${application._id}/reject`) - .send({ rejectReason: "Some reason which is valid" }); - - - const res = await test_agent - .post(`/applications/company/${application._id}/approve`); - - expect(res.status).toBe(HTTPStatus.CONFLICT); - }); - - test("Should fail if approving application with an existing account with same email, and then rollback", async () => { - await Account.create({ email: application.email, password: "passwordHashedButNotReally", isAdmin: true }); - - const res = await test_agent - .post(`/applications/company/${application._id}/approve`); - - expect(res.status).toBe(HTTPStatus.CONFLICT); - expect(res.body.error_code).toBe(ErrorTypes.VALIDATION_ERROR); - expect(res.body.errors[0].msg).toBe(CompanyApplicationRules.EMAIL_ALREADY_IN_USE.msg); - - const result_application = await CompanyApplication.findById(application._id); - expect(result_application.state).toBe(ApplicationStatus.PENDING); - }); }); - describe("Reject application", () => { - - beforeEach(async () => { - await Account.deleteMany({ email: pendingApplication.email }); - application = await CompanyApplication.create(pendingApplication); - }); - - afterEach(async () => { - await CompanyApplication.deleteMany({}); - }); - - test("Should fail if no rejectReason provided", async () => { - const res = await test_agent - .post(`/applications/company/${application._id}/reject`); - - expect(res.status).toBe(HTTPStatus.UNPROCESSABLE_ENTITY); - expect(res.body.errors[0]).toStrictEqual({ location: "body", msg: "required", param: "rejectReason" }); - - }); - - test("Should reject pending application", async () => { - const res = await test_agent - .post(`/applications/company/${application._id}/reject`) - .send({ rejectReason: "Some reason which is valid" }); - - expect(res.status).toBe(HTTPStatus.OK); - expect(res.body.email).toBe(pendingApplication.email); - expect(res.body.companyName).toBe(pendingApplication.companyName); - }); - - test("Should send rejection email to company email", async () => { - - const res = await test_agent - .post(`/applications/company/${application._id}/reject`) - .send({ rejectReason: "Some reason which is valid" }); + test("Should fail if trying to reject inexistent application", async () => { + const res = await test_agent + .post(`/applications/company/${new ObjectId()}/reject`) + .send({ rejectReason: "Some reason which is valid" }); - expect(res.status).toBe(HTTPStatus.OK); - - const emailOptions = REJECTION_NOTIFICATION(application.companyName); - - expect(EmailService.sendMail).toHaveBeenCalledWith({ - subject: emailOptions.subject, - to: application.email, - template: emailOptions.template, - context: emailOptions.context, - }); - - }); - - test("Should fail if trying to reject inexistent application", async () => { - const res = await test_agent - .post(`/applications/company/${new ObjectId()}/reject`) - .send({ rejectReason: "Some reason which is valid" }); - - expect(res.status).toBe(HTTPStatus.NOT_FOUND); - }); + expect(res.status).toBe(StatusCodes.NOT_FOUND); + }); - test("Should fail if trying to reject already approved application", async () => { - await test_agent - .post(`/applications/company/${application._id}/approve`); + test("Should fail if trying to reject already approved application", async () => { + await test_agent + .post(`/applications/company/${application._id}/approve`); - const res = await test_agent - .post(`/applications/company/${application._id}/reject`) - .send({ rejectReason: "Some reason which is valid" }); + const res = await test_agent + .post(`/applications/company/${application._id}/reject`) + .send({ rejectReason: "Some reason which is valid" }); - expect(res.status).toBe(HTTPStatus.CONFLICT); - }); + expect(res.status).toBe(StatusCodes.CONFLICT); + }); - test("Should fail if trying to reject already rejected application", async () => { - await test_agent - .post(`/applications/company/${application._id}/reject`) - .send({ rejectReason: "Some reason which is valid" }); + test("Should fail if trying to reject already rejected application", async () => { + await test_agent + .post(`/applications/company/${application._id}/reject`) + .send({ rejectReason: "Some reason which is valid" }); - const res = await test_agent - .post(`/applications/company/${application._id}/reject`) - .send({ rejectReason: "Some reason which is valid" }); + const res = await test_agent + .post(`/applications/company/${application._id}/reject`) + .send({ rejectReason: "Some reason which is valid" }); - expect(res.status).toBe(HTTPStatus.CONFLICT); - }); + expect(res.status).toBe(StatusCodes.CONFLICT); }); }); }); diff --git a/test/utils/ValidatorTester.js b/test/utils/ValidatorTester.js index f519c2a9..da875e47 100644 --- a/test/utils/ValidatorTester.js +++ b/test/utils/ValidatorTester.js @@ -88,7 +88,7 @@ const ValidatorTester = (requestEndpoint) => (location) => (field_name) => ({ "location": location, "msg": ValidationReasons.DATE, "param": field_name, - "value": params[field_name], + "value": location === "query" ? params[field_name].toString() : params[field_name], }); }); }); @@ -172,10 +172,32 @@ const ValidatorTester = (requestEndpoint) => (location) => (field_name) => ({ }); }, + mustBeArray: () => { + test("should be array", async () => { + const params = { + [field_name]: "not_an_array", + }; + const res = await requestEndpoint(params); + + executeValidatorTestWithContext({ requestEndpoint, location, field_name }, () => { + checkCommonErrorResponse(res); + expect(res.body.errors).toContainEqual({ + "location": location, + "msg": ValidationReasons.ARRAY, + "param": field_name, + "value": params[field_name], + }); + }); + }); + }, + mustBeInArray: (array) => { test(`should be one of: [${array}]`, async () => { + + const value = "not_in_array"; + const params = { - [field_name]: "not_in_array", + [field_name]: value, }; const res = await requestEndpoint(params); @@ -183,7 +205,7 @@ const ValidatorTester = (requestEndpoint) => (location) => (field_name) => ({ checkCommonErrorResponse(res); expect(res.body.errors).toContainEqual({ "location": location, - "msg": ValidationReasons.IN_ARRAY(array), + "msg": ValidationReasons.IN_ARRAY(array, value), "param": field_name, "value": params[field_name], }); @@ -353,6 +375,26 @@ const ValidatorTester = (requestEndpoint) => (location) => (field_name) => ({ }); }, + mustBeLessThanOrEqualTo: (max) => { + test(`should be less than or equal to ${max}`, async () => { + const params = { + [field_name]: max + 1, + }; + + const res = await requestEndpoint(params); + + executeValidatorTestWithContext({ requestEndpoint, location, field_name }, () => { + checkCommonErrorResponse(res); + expect(res.body.errors).toContainEqual({ + "location": location, + "msg": ValidationReasons.MAX(max), + "param": field_name, + "value": params[field_name], + }); + }); + }); + }, + mustBeGreaterThanOrEqualToField: (field_name2) => { test(`should be greater than or equal to ${field_name2}`, async () => { const params = {