diff --git a/package-lock.json b/package-lock.json index 5d461e75..01a66ad4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5882,9 +5882,9 @@ "license": "ISC" }, "node_modules/ip": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ip/-/ip-2.0.0.tgz", - "integrity": "sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ==" + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ip/-/ip-2.0.1.tgz", + "integrity": "sha512-lJUL9imLTNi1ZfXT+DU6rBBdbiKGBuay9B6xGSPVjUeQwaH1RIGqef8RZkUtHioLmSNpPR5M4HVKJGm1j8FWVQ==" }, "node_modules/ipaddr.js": { "version": "1.9.1", @@ -7258,8 +7258,9 @@ "license": "MIT" }, "node_modules/nodemailer": { - "version": "6.8.0", - "license": "MIT", + "version": "6.9.12", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.12.tgz", + "integrity": "sha512-pnLo7g37Br3jXbF0bl5DekBJihm2q+3bB3l2o/B060sWmb5l+VqeScAQCBqaQ+5ezRZFzW5SciZNGdRDEbq89w==", "engines": { "node": ">=6.0.0" } @@ -12850,9 +12851,9 @@ "version": "2.0.4" }, "ip": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ip/-/ip-2.0.0.tgz", - "integrity": "sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ==" + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ip/-/ip-2.0.1.tgz", + "integrity": "sha512-lJUL9imLTNi1ZfXT+DU6rBBdbiKGBuay9B6xGSPVjUeQwaH1RIGqef8RZkUtHioLmSNpPR5M4HVKJGm1j8FWVQ==" }, "ipaddr.js": { "version": "1.9.1" @@ -13760,7 +13761,9 @@ "version": "2.0.7" }, "nodemailer": { - "version": "6.8.0" + "version": "6.9.12", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.12.tgz", + "integrity": "sha512-pnLo7g37Br3jXbF0bl5DekBJihm2q+3bB3l2o/B060sWmb5l+VqeScAQCBqaQ+5ezRZFzW5SciZNGdRDEbq89w==" }, "nodemailer-express-handlebars": { "version": "5.0.0", diff --git a/src/models/CompanyApplication.js b/src/models/CompanyApplication.js index 766f8962..7dfb041e 100644 --- a/src/models/CompanyApplication.js +++ b/src/models/CompanyApplication.js @@ -80,18 +80,22 @@ export const CompanyApplicationProps = { return !!this.rejectedAt; }, }, + state: { + type: String, + enum: ApplicationStatus, + default: function() { + if (this.rejectedAt) return ApplicationStatus.REJECTED; + if (this.approvedAt) return ApplicationStatus.APPROVED; + return ApplicationStatus.PENDING; + }, + required: true, + }, }; const CompanyApplicationSchema = new Schema(CompanyApplicationProps); CompanyApplicationSchema.index({ companyName: "text" }); -CompanyApplicationSchema.virtual("state").get(function() { - if (!this.approvedAt && !this.rejectedAt) return ApplicationStatus.PENDING; - else if (this.approvedAt) return ApplicationStatus.APPROVED; - else return ApplicationStatus.REJECTED; -}); - async function validateEmailUniqueAccount(value) { try { await checkDuplicatedEmail(value); @@ -107,7 +111,6 @@ function validateDecisionDate(value) { } function validateMutuallyExclusiveEvents(field) { - return function(value) { return !value || !this[field]; }; @@ -153,6 +156,7 @@ export const isRejectable = (application) => { CompanyApplicationSchema.methods.approve = function() { isApprovable(this); this.approvedAt = Date.now(); + this.state = ApplicationStatus.APPROVED; // Need to prevent validation, otherwise it will fail the email uniqueness, // Since there is already an application with same email: itself :) return this.save({ validateModifiedOnly: true }); @@ -162,6 +166,7 @@ CompanyApplicationSchema.methods.reject = function(reason) { isRejectable(this); this.rejectedAt = Date.now(); this.rejectReason = reason; + this.state = ApplicationStatus.REJECTED; // Need to prevent validation, otherwise it will fail the email uniqueness, // Since there is already an application with same email: itself :) return this.save({ validateModifiedOnly: true }); @@ -170,6 +175,7 @@ CompanyApplicationSchema.methods.reject = function(reason) { CompanyApplicationSchema.methods.undoApproval = function() { if (!this.approvedAt) throw new Error("Cannot undo approval of yet-to-be approved Company Application"); this.approvedAt = undefined; + this.state = ApplicationStatus.PENDING; // Need to prevent validation, otherwise it will fail the email uniqueness, // Since there is already an application with same email: itself :) return this.save({ validateModifiedOnly: true }); diff --git a/test/company_application_schema.js b/test/company_application_schema.js index 2331eed7..522a1daa 100644 --- a/test/company_application_schema.js +++ b/test/company_application_schema.js @@ -30,34 +30,122 @@ describe("# CompanyApplication schema tests", () => { companyApplicationTester.maxLength("rejectReason", CompanyApplicationConstants.rejectReason.max_length); }); - describe("Virtual field tests", () => { - describe("Test `status` virtual field", () => { - test("should return PENDING", () => { - const application = new CompanyApplication({ - submittedAt: new Date("01/01/2020"), + describe("Application Status tests", () => { + const rejectReason = "Rejected for a good reason"; + const submittedAt = new Date("01/01/2020"); + const baseApplication = { + submittedAt: submittedAt, + companyName: "Testing company", + password: "password123", + motivation: "This company has a very valid motivation because otherwise, the tests would not exist.", + }; + beforeAll(async () => { + await CompanyApplication.deleteMany({}); + }); + afterAll(async () => { + await CompanyApplication.deleteMany({}); + }); + + describe("Approve and reject functions", () => { + test("should return PENDING", async () => { + const application = await CompanyApplication.create({ + ...baseApplication, + email: "pendingApplication@test.com" }); expect(application.state).toBe(ApplicationStatus.PENDING); + + expect(application.rejectedAt).toBeUndefined(); + expect(application.approvedAt).toBeUndefined(); + expect(application.rejectReason).toBeUndefined(); }); - test("should return REJECTED", () => { - const application = new CompanyApplication({ - submittedAt: new Date("01/01/2020"), - rejectedAt: new Date("02/01/2020"), + test("should return REJECTED", async () => { + const application = await CompanyApplication.create({ + ...baseApplication, + email: "rejectApplication@test.com" }); + await application.reject(rejectReason); + expect(application.state).toBe(ApplicationStatus.REJECTED); + expect(application.rejectReason).toBe(rejectReason); + expect(application.rejectedAt).toBeDefined(); + + expect(application.approvedAt).toBeUndefined(); + }); + + test("should return APPROVED", async () => { + const application = await CompanyApplication.create({ + ...baseApplication, + email: "approveApplication@test.com" + }); + await application.approve(); + + expect(application.state).toBe(ApplicationStatus.APPROVED); + expect(application.approvedAt).toBeDefined(); + + expect(application.rejectedAt).toBeUndefined(); + expect(application.rejectReason).toBeUndefined(); }); - test("should return APPROVED", () => { - const application = new CompanyApplication({ - submittedAt: new Date("01/01/2020"), - approvedAt: new Date("02/01/2020"), + test("should return PENDING after undoApproval", async () => { + const application = await CompanyApplication.create({ + ...baseApplication, + email: "undoApprovalApplication@test.com" }); + + await application.approve(); expect(application.state).toBe(ApplicationStatus.APPROVED); + + await application.undoApproval(); + expect(application.state).toBe(ApplicationStatus.PENDING); + + expect(application.rejectedAt).toBeUndefined(); + expect(application.approvedAt).toBeUndefined(); + expect(application.rejectReason).toBeUndefined(); }); + test("should throw error when trying to approve a rejected application", async () => { + const application = await CompanyApplication.create({ + ...baseApplication, + email: "invalidApprove@test.com", + }); + + await application.reject(rejectReason); + expect(() => application.approve()).toThrow(); + }); + + test("should throw error when trying to reject an approved application", async () => { + const application = await CompanyApplication.create({ + ...baseApplication, + email: "invalidReject@test.com", + }); + await application.approve(); + expect(() => application.reject(rejectReason)).toThrow(); + }); }); - }); + describe("Migration of application state with correct default value", () => { + const assessedAt = new Date(submittedAt.getTime() + 1); + test("should return APPROVED if approvedAt is defined", async () => { + const application = await CompanyApplication.create({ + ...baseApplication, + email: "approvedAtStatus@test.com", + approvedAt: assessedAt, + }); + expect(application.state).toBe(ApplicationStatus.APPROVED); + }); + + test("should return REJECTED if rejectedAt is defined", async () => { + const application = await CompanyApplication.create({ + ...baseApplication, + email: "rejectedAtStatus@test.com", + rejectedAt: assessedAt, + rejectReason: rejectReason, + }); + expect(application.state).toBe(ApplicationStatus.REJECTED); + }); + }); + }); }); diff --git a/test/end-to-end/review.js b/test/end-to-end/review.js index aab25cdc..96992054 100644 --- a/test/end-to-end/review.js +++ b/test/end-to-end/review.js @@ -94,12 +94,15 @@ describe("Company application review endpoint test", () => { ...pendingApplication, submittedAt: new Date("2019-11-24"), approvedAt: pendingApplication.submittedAt.getTime() + 1, + state: ApplicationStatus.APPROVED, companyName: "approved Testing company", email: `approved${pendingApplication.email}`, }; + const rejectedApplication = { ...pendingApplication, submittedAt: new Date("2019-11-23"), rejectedAt: pendingApplication.submittedAt.getTime() + 1, + state: ApplicationStatus.REJECTED, companyName: "rejected Testing company", email: `rejected${pendingApplication.email}`, rejectReason: "2bad4nij0bs",