Skip to content

Commit

Permalink
fix: prevent discriminated models from being created before their bas…
Browse files Browse the repository at this point in the history
…e model (#244)

* fix(FormModel): return discriminated model only after compiling base

* fix(SubmissionModel): return discriminated model after compiling base

* Fix tests

Co-authored-by: Arshad Ali <arshadali@Arshads-MacBook-Pro.local>
  • Loading branch information
karrui and Arshad Ali committed Sep 1, 2020
1 parent f6bc8b5 commit c72d433
Show file tree
Hide file tree
Showing 5 changed files with 98 additions and 74 deletions.
38 changes: 10 additions & 28 deletions src/app/models/form.server.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -493,42 +493,24 @@ const compileFormModel = (db: Mongoose): IFormModel => {
return FormModel
}

const compileEmailFormModel = (db: Mongoose) => {
return db.model<IEmailFormSchema, IEmailFormModel>(
ResponseMode.Email,
EmailFormSchema,
)
}

export const getEmailFormModel = (db: Mongoose) => {
const getFormModel = (db: Mongoose) => {
try {
return db.model(ResponseMode.Email) as IEmailFormModel
return db.model(FORM_SCHEMA_ID) as IFormModel
} catch {
return compileEmailFormModel(db)
return compileFormModel(db)
}
}

const compileEncryptedFormModel = (db: Mongoose) => {
return db.model<IEncryptedFormSchema, IEncryptedFormModel>(
ResponseMode.Encrypt,
EncryptedFormSchema,
)
export const getEmailFormModel = (db: Mongoose) => {
// Load or build base model first
getFormModel(db)
return db.model(ResponseMode.Email) as IEmailFormModel
}

export const getEncryptedFormModel = (db: Mongoose) => {
try {
return db.model(ResponseMode.Encrypt) as IEncryptedFormModel
} catch {
return compileEncryptedFormModel(db)
}
}

const getFormModel = (db: Mongoose) => {
try {
return db.model(FORM_SCHEMA_ID) as IFormModel
} catch {
return compileFormModel(db)
}
// Load or build base model first
getFormModel(db)
return db.model(ResponseMode.Encrypt) as IEncryptedFormModel
}

export default getFormModel
38 changes: 10 additions & 28 deletions src/app/models/submission.server.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,34 +152,6 @@ encryptSubmissionSchema.methods.getWebhookView = function (
}
}

export const getEmailSubmissionModel = (db: Mongoose) => {
try {
return db.model(SubmissionType.Email) as IEmailSubmissionModel
} catch {
return db.model<IEmailSubmissionSchema, IEmailSubmissionModel>(
SubmissionType.Email,
emailSubmissionSchema,
)
}
}

export const getEncryptSubmissionModel = (db: Mongoose) => {
try {
return db.model(SubmissionType.Encrypt) as IEncryptSubmissionModel
} catch {
return db.model<IEncryptedSubmissionSchema, IEncryptSubmissionModel>(
SubmissionType.Encrypt,
encryptSubmissionSchema,
)
}
}

/**
* Form Submission Schema
* @param {Object} db - Active DB Connection
* @return {Object} Mongoose Model
*/

const compileSubmissionModel = (db: Mongoose) => {
const Submission = db.model('Submission', SubmissionSchema)
Submission.discriminator(SubmissionType.Email, emailSubmissionSchema)
Expand All @@ -195,4 +167,14 @@ const getSubmissionModel = (db: Mongoose) => {
}
}

export const getEmailSubmissionModel = (db: Mongoose) => {
getSubmissionModel(db)
return db.model(SubmissionType.Email) as IEmailSubmissionModel
}

export const getEncryptSubmissionModel = (db: Mongoose) => {
getSubmissionModel(db)
return db.model(SubmissionType.Encrypt) as IEncryptSubmissionModel
}

export default getSubmissionModel
2 changes: 1 addition & 1 deletion src/types/form.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ export interface IForm {
webhook?: Webhook
msgSrvcName?: string

responseMode?: ResponseMode
responseMode: ResponseMode

// Schema properties
_id: Document['_id']
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -656,18 +656,16 @@ describe('Email Submissions Controller', () => {
it('errors with 400 if submission fail', (done) => {
const badSubmission = jasmine.createSpyObj('Submission', ['save'])
badSubmission.save.and.callFake((callback) => callback(new Error('boom')))

const badSubmissionModel = jasmine.createSpy()
badSubmissionModel.and.returnValue(badSubmission)
const mongoose = jasmine.createSpyObj('mongoose', ['model'])
mongoose.model
.withArgs('emailSubmission')
.and.returnValue(badSubmissionModel)

const getEmailSubmissionModel = jasmine.createSpy(
'getEmailSubmissionModel',
)
getEmailSubmissionModel.and.returnValue(badSubmissionModel)
const badController = spec(
'dist/backend/app/controllers/email-submissions.server.controller',
{
mongoose,
'../models/submission.server.model': { getEmailSubmissionModel },
},
)

Expand Down
82 changes: 72 additions & 10 deletions tests/unit/backend/models/form.server.model.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,12 @@ import getFormModel, {
getEmailFormModel,
getEncryptedFormModel,
} from 'src/app/models/form.server.model'
import { IAgencySchema, IEncryptedForm, IUserSchema } from 'src/types'
import {
IAgencySchema,
IEncryptedForm,
IUserSchema,
ResponseMode,
} from 'src/types'

import dbHandler from '../helpers/jest-db'

Expand All @@ -25,12 +30,12 @@ const MOCK_FORM_PARAMS = {
const MOCK_ENCRYPTED_FORM_PARAMS = {
...MOCK_FORM_PARAMS,
publicKey: 'mockPublicKey',
responseMode: 'encrypt',
responseMode: ResponseMode.Encrypt,
}
const MOCK_EMAIL_FORM_PARAMS = {
...MOCK_FORM_PARAMS,
emails: [MOCK_ADMIN_EMAIL],
responseMode: 'email',
responseMode: ResponseMode.Email,
}

const FORM_DEFAULTS = {
Expand Down Expand Up @@ -676,13 +681,44 @@ describe('Form Model', () => {
expect(form).toBeNull()
})

it('should return the populated form when formId is valid', async () => {
it('should return the populated email form when formId is valid', async () => {
// Arrange
const formParams = merge({}, MOCK_FORM_PARAMS, {
const emailFormParams = merge({}, MOCK_EMAIL_FORM_PARAMS, {
admin: preloadedAdmin,
})
// Create a form
const form = (await Form.create(emailFormParams)).toObject()

// Act
const actualForm = (await Form.getFullFormById(form._id)).toObject()

// Assert
// Form should be returned
expect(actualForm).not.toBeNull()
// Omit admin key since it is populated is not ObjectId anymore.
expect(omit(actualForm, 'admin')).toEqual(omit(form, 'admin'))
// Verify populated admin shape
expect(actualForm.admin).not.toBeNull()
expect(actualForm.admin.email).toEqual(preloadedAdmin.email)
// Remove indeterministic keys
const expectedAgency = omit(preloadedAgency.toObject(), [
'_id',
'created',
'lastModified',
'__v',
])
expect(actualForm.admin.agency).toEqual(
expect.objectContaining(expectedAgency),
)
})

it('should return the populated encrypt form when formId is valid', async () => {
// Arrange
const encryptFormParams = merge({}, MOCK_ENCRYPTED_FORM_PARAMS, {
admin: preloadedAdmin,
})
// Create a form
const form = (await Form.create(formParams)).toObject()
const form = (await Form.create(encryptFormParams)).toObject()

// Act
const actualForm = (await Form.getFullFormById(form._id)).toObject()
Expand Down Expand Up @@ -720,13 +756,39 @@ describe('Form Model', () => {
expect(form).toBeNull()
})

it('should return otpData when formId is valid', async () => {
it('should return otpData of an email form when formId is valid', async () => {
// Arrange
const formParams = merge({}, MOCK_FORM_PARAMS, {
const emailFormParams = merge({}, MOCK_EMAIL_FORM_PARAMS, {
msgSrvcName: 'mockSrvcName',
})
// Create a form with msgSrvcName
const form = await Form.create(emailFormParams)

// Act
const actualOtpData = await Form.getOtpData(form._id)

// Assert
// OtpData should be returned
expect(actualOtpData).not.toBeNull()
// Check shape
const expectedOtpData = {
form: form._id,
formAdmin: {
email: preloadedAdmin.email,
userId: preloadedAdmin._id,
},
msgSrvcName: emailFormParams.msgSrvcName,
}
expect(actualOtpData).toEqual(expectedOtpData)
})

it('should return otpData of an encrypt form when formId is valid', async () => {
// Arrange
const encryptFormParams = merge({}, MOCK_ENCRYPTED_FORM_PARAMS, {
msgSrvcName: 'mockSrvcName',
})
// Create a form with msgSrvcName
const form = await Form.create(formParams)
const form = await Form.create(encryptFormParams)

// Act
const actualOtpData = await Form.getOtpData(form._id)
Expand All @@ -741,7 +803,7 @@ describe('Form Model', () => {
email: preloadedAdmin.email,
userId: preloadedAdmin._id,
},
msgSrvcName: formParams.msgSrvcName,
msgSrvcName: encryptFormParams.msgSrvcName,
}
expect(actualOtpData).toEqual(expectedOtpData)
})
Expand Down

0 comments on commit c72d433

Please sign in to comment.