Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: update getPublicForm endpoint to typescript #1398

Merged
merged 89 commits into from
Apr 8, 2021
Merged
Show file tree
Hide file tree
Changes from 79 commits
Commits
Show all changes
89 commits
Select commit Hold shift + click to select a range
e5bd2cd
refactor(modules/spcp): shifts get spcp session out into service
seaerchin Mar 17, 2021
f40eee1
refactor(modules/form): adds utility methods and refactored checkForm…
seaerchin Mar 17, 2021
beada89
refactor(modules/form): refactors GET public forms end point from js …
seaerchin Mar 17, 2021
e6a1455
refactor(utils): adds utility type for all possible db errors
seaerchin Mar 17, 2021
b5e5718
fix(types/form): fixed the type of submissionLimit to allow for null
seaerchin Mar 17, 2021
7b25c88
refactor(modules/form): updated deactiveForm so that it uses never th…
seaerchin Mar 17, 2021
de6e0a2
refactor(modules/form): updated form limit checking to account for su…
seaerchin Mar 17, 2021
c0ccea5
refactor(modules/form): adds logging to getSpcpSession
seaerchin Mar 17, 2021
f167eb4
refactor(modules/form): refactored handleGetPublicForm for clarity an…
seaerchin Mar 17, 2021
ce212d7
refactor(publicformctrl): refactored handler for GET public forms to …
seaerchin Mar 18, 2021
5dab71d
refactor(spcpsvc): added logging to extract JWT; updated typings for …
seaerchin Mar 18, 2021
3aeb40d
revert(formsvc): removes retrievePublicForm (will be done in another …
seaerchin Mar 18, 2021
65e6505
style(public-form/controller): updated object to use variables
seaerchin Mar 18, 2021
6e9e470
style(services/types): updated documentation for functions and remove…
seaerchin Mar 18, 2021
2ac213a
refactor(public-form/controller): refactored spcp flow so it's clearer
seaerchin Mar 18, 2021
42c4a37
refactor(public-form/controller): wip for myInfo
seaerchin Mar 18, 2021
44f6059
refactor(myinfo): extracts myInfoCookiePayload to 2 types and adds ut…
seaerchin Mar 19, 2021
4549ca8
refactor(public-form/controller): refactored myinfo chunk so it's neater
seaerchin Mar 19, 2021
4d02a8f
test(form/service): fixes failing tests due to checkFormSubmissionLim…
seaerchin Mar 19, 2021
10cfafd
refactor(myinfo): removed unneeded cookie type and updated extractSuc…
seaerchin Mar 19, 2021
a78ae17
docs(public-form/controller): adds docs to confusing sections of hand…
seaerchin Mar 19, 2021
1440851
test(public-form/controller/test): add test for database error when G…
seaerchin Mar 19, 2021
be961d0
test(public-form/controller/tests): add more tests
seaerchin Mar 22, 2021
d707ead
test(public-form/controller/test): adds test for success cases
seaerchin Mar 23, 2021
bf51474
refactor(myinfo): extracts out chunks form public-form controller int…
seaerchin Mar 23, 2021
77117a7
refactor(public-form/controller): updated controller to use MyInfoFac…
seaerchin Mar 23, 2021
e30b482
test(public-form/controller/test): updated tests to use mocks
seaerchin Mar 23, 2021
2eab1d6
fix(public-form/controller): fixed succesful 200 when auth using myI…
seaerchin Mar 24, 2021
c2fe4c9
test(public-form/controller/test): adds remaining tests for myInfo
seaerchin Mar 24, 2021
78e142d
refactor(spcp): combined controller calls to spcp services into a sin…
seaerchin Mar 25, 2021
e09d227
refactor(myinfo): refactored multiple controller calls to myInfo into…
seaerchin Mar 25, 2021
c4637a1
refactor(public-form): updated typings and refactored getPublicForm t…
seaerchin Mar 25, 2021
62d40e4
style(public-form/controller/test): updated comments in tests to use …
seaerchin Mar 29, 2021
b058465
refactor(form/service): updated deactiveForm to return the form itsel…
seaerchin Mar 29, 2021
31d4d5e
style(myinfo): extractMyInfoData renamed to fetchMyInfoData
seaerchin Mar 29, 2021
8775701
fix(public-form/controller): fixed bug where wrong extract cookie met…
seaerchin Mar 29, 2021
6aff7e9
refactor(spcp/myinfo): removed extra logging in spcp; updated names i…
seaerchin Mar 29, 2021
ee32077
test(spcp/service/test): adds service tests
seaerchin Mar 29, 2021
3918298
test(spcp/service/test): adds unit tests for createFormWithSpcpSession
seaerchin Mar 30, 2021
c611843
test(myinfo.service): adds tests for createFormWithMyInfo
seaerchin Mar 30, 2021
55b075c
feat(form): adds new errors and utility methods
seaerchin Mar 30, 2021
11804b9
refactor(public-form): refactor to account for intarnet
seaerchin Mar 30, 2021
0ba608a
refactor(public-form/controller): added compatability for checking in…
seaerchin Mar 30, 2021
a05c899
test(public-form/controller): adds tests for checking intranet; fixes…
seaerchin Mar 30, 2021
2a54c1c
style(spcp/service/test): changed naming to camelCase for variables
seaerchin Mar 31, 2021
0341f24
fix(form.service): fixed intranet ip checking
seaerchin Mar 31, 2021
938e129
refactor(public-form): shifts utility method out into public form ser…
seaerchin Mar 31, 2021
8b1f740
test(public-form/controller): updates tests
seaerchin Mar 31, 2021
18157cb
refactor(public-forms/server/routes): swaps to new controller for exp…
seaerchin Mar 31, 2021
7fc19ac
fix(myinfo/util): added cookie access check
seaerchin Apr 1, 2021
cf306b6
fix(form/service): adds error recovery for missing feature error when…
seaerchin Apr 1, 2021
98aca70
refactor(public-form/controller): tightened logic for myinfo error
seaerchin Apr 1, 2021
f38ed95
build(package.json): added ts-essential for UnreachableCaseException
seaerchin Apr 6, 2021
933b9c9
refactor(forms): added new type for intranet form
seaerchin Apr 6, 2021
fc7da63
refactor(spcp/service): updated service methods
seaerchin Apr 6, 2021
aa2a687
refactor(intranet/factory): updated isIntranetIp and factory signatur…
seaerchin Apr 6, 2021
479702d
refactor(myinfo): updated typings and factory methods to remove respo…
seaerchin Apr 6, 2021
937fe08
refactor(public-form/controller): wip
seaerchin Apr 6, 2021
3a4e4b8
refactor(form/service): remove result wrapping as only error is Missi…
seaerchin Apr 6, 2021
a0b58a7
refactor(myinfo): updated typings and comments for myInfo
seaerchin Apr 6, 2021
43bc87f
refactor(publicform): updated handleGetPublicForm method and removed …
seaerchin Apr 6, 2021
beaa9e1
refactor(public-form/service): removed unnused method
seaerchin Apr 6, 2021
5f47ee6
fix(public-form/controller): changed returned form to be a publicForm
seaerchin Apr 6, 2021
4eb9804
test(myinfo/service): updated tests for myinfo service
seaerchin Apr 6, 2021
f77d712
test(public-form/controller): updated tests to fit iwth refactor
seaerchin Apr 6, 2021
de70fd8
chore(intranet): removed unused variables
seaerchin Apr 6, 2021
3c18db4
test(intranet/service): fixes failing tests due to refactor
seaerchin Apr 6, 2021
95ab3cc
Merge branch 'develop' into refactor/public-forms-endpoint
seaerchin Apr 6, 2021
5630a45
refactor(app/utils): removed duplicate datatype in handle mongo errors
seaerchin Apr 6, 2021
e75f3dc
style(form/service): updated logger message
seaerchin Apr 6, 2021
acca808
test(spcp/service): removed tests for deleted method; updated test fo…
seaerchin Apr 6, 2021
0ab99bb
refactor(public-form/controller): extracts logger meta property into …
seaerchin Apr 6, 2021
6d272b7
style(form/service): updated action property of logger meta
seaerchin Apr 6, 2021
00e5262
docs(public-form/controller): updated comments for getPublicForm on c…
seaerchin Apr 6, 2021
8bc8314
refactor(myinfo): deleted unused middleware; made fetchMyInfoPersonDa…
seaerchin Apr 6, 2021
50ef886
refactor(myinfo): changed fetchMyInfoPersonData to become a private f…
seaerchin Apr 7, 2021
d066a5d
test(myinfo): fixed factory and service tests due to making myInfoPer…
seaerchin Apr 7, 2021
6ae2fec
chore(webhook/service/test): fixed import
seaerchin Apr 7, 2021
3303781
chore(types/forms): removed unused type declaration
seaerchin Apr 7, 2021
55b5be7
style(form/service): chains calls together for clarity
seaerchin Apr 7, 2021
dcc6276
refactor(myinfo): refactored methods so that atomic operations are pe…
seaerchin Apr 7, 2021
a372a18
test(myinfo): fixes tests for myinfo
seaerchin Apr 7, 2021
38757ff
style(spcp): renamed service methods for clarity; removed unused error
seaerchin Apr 7, 2021
bdd5268
test(spcp): fixes spcp tests
seaerchin Apr 7, 2021
7d7c50f
refactor(public-form): updated controller to account for myinfo/spcp …
seaerchin Apr 7, 2021
f7fdc1a
test(public-form): updated tests
seaerchin Apr 7, 2021
bac3da7
fix(public-form/controller): fixed cp returning userInfo as ewll
seaerchin Apr 7, 2021
93bd679
Merge branch 'develop' into refactor/public-forms-endpoint
seaerchin Apr 8, 2021
646b797
Merge branch 'develop' into refactor/public-forms-endpoint
seaerchin Apr 8, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,7 @@
"supertest-session": "^4.1.0",
"terser-webpack-plugin": "^1.2.3",
"testcafe": "^1.13.0",
"ts-essentials": "^7.0.1",
"ts-jest": "^26.5.4",
"ts-loader": "^7.0.5",
"ts-mock-imports": "^1.3.3",
Expand Down
8 changes: 4 additions & 4 deletions src/app/modules/form/__tests__/form.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -242,10 +242,10 @@ describe('FormService', () => {
)

// Assert
expect(actual._unsafeUnwrap()).toEqual(true)
expect(actual._unsafeUnwrap()).toEqual(form)
})

it('should let requests through when form has not reached submission limit', async () => {
it('should return the form when the submission limit is not reached', async () => {
// Arrange
const formParams = merge({}, MOCK_ENCRYPTED_FORM_PARAMS, {
status: Status.Public,
Expand All @@ -268,11 +268,11 @@ describe('FormService', () => {

// Act
const actual = await FormService.checkFormSubmissionLimitAndDeactivateForm(
form,
form as IPopulatedForm,
)

// Assert
expect(actual._unsafeUnwrap()).toEqual(true)
expect(actual._unsafeUnwrap()).toEqual(validForm)
})

it('should not let requests through and deactivate form when form has reached submission limit', async () => {
Expand Down
118 changes: 97 additions & 21 deletions src/app/modules/form/form.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { err, errAsync, ok, okAsync, Result, ResultAsync } from 'neverthrow'

import { createLoggerWithLabel } from '../../../config/logger'
import {
AuthType,
IEmailFormModel,
IEncryptedFormModel,
IFormSchema,
Expand All @@ -15,6 +16,7 @@ import getFormModel, {
getEncryptedFormModel,
} from '../../models/form.server.model'
import getSubmissionModel from '../../models/submission.server.model'
import { IntranetFactory } from '../../services/intranet/intranet.factory'
import {
getMongoErrorMessage,
transformMongoError,
Expand All @@ -37,10 +39,42 @@ const EmailFormModel = getEmailFormModel(mongoose)
const EncryptedFormModel = getEncryptedFormModel(mongoose)
const SubmissionModel = getSubmissionModel(mongoose)

export const deactivateForm = async (
/**
* Deactivates a given form by its id
* @param formId the id of the form to deactivate
* @returns ok(true) if the form has been deactivated successfully
* @returns err(PossibleDatabaseError) if an error occurred while trying to deactivate the form
* @returns err(FormNotFoundError) if there is no form with the given formId
*/
export const deactivateForm = (
formId: string,
): Promise<IFormSchema | null> => {
return FormModel.deactivateById(formId)
): ResultAsync<IFormSchema, PossibleDatabaseError | FormNotFoundError> => {
return ResultAsync.fromPromise(FormModel.deactivateById(formId), (error) => {
logger.error({
message: 'Error deactivating form by id',
meta: {
action: 'deactivateForm',
form: formId,
},
error,
})

return transformMongoError(error)
}).andThen((deactivatedForm) => {
if (!deactivatedForm) {
logger.error({
message:
'Attempted to deactivate form that cannot be found in the database',
meta: {
action: 'deactivateForm',
form: formId,
},
})
return errAsync(new FormNotFoundError())
}
// Successfully deactivated.
return okAsync(deactivatedForm)
})
}

/**
Expand Down Expand Up @@ -118,17 +152,16 @@ export const retrieveFormById = (
* Method to ensure given form is available to the public.
* @param form the form to check
* @returns ok(true) if form is public
* @returns err(ApplicationError) if form has an invalid state
* @returns err(FormDeletedError) if form has been deleted
* @returns err(PrivateFormError) if form is private, the message will be the form inactive message
* @returns err(ApplicationError) if form has an invalid state
*/
export const isFormPublic = (
form: IPopulatedForm,
): Result<true, FormDeletedError | PrivateFormError | ApplicationError> => {
if (!form.status) {
return err(new ApplicationError())
}

switch (form.status) {
case Status.Public:
return ok(true)
Expand All @@ -142,20 +175,30 @@ export const isFormPublic = (
/**
* Method to check whether a form has reached submission limits, and deactivate the form if necessary
* @param form the form to check
* @returns ok(true) if submission is allowed because the form has not reached limits
* @returns ok(false) if submission is not allowed because the form has reached limits
* @returns ok(form) if submission is allowed because the form has not reached limits
* @returns err(PossibleDatabaseError) if an error occurred while querying the database for the specified form
* @returns err(FormNotFoundError) if the form has exceeded the submission limits but could not be found and deactivated
* @returns err(PrivateFormError) if the count of the form has been exceeded and the form has been deactivated
*/
export const checkFormSubmissionLimitAndDeactivateForm = (
seaerchin marked this conversation as resolved.
Show resolved Hide resolved
form: IPopulatedForm,
): ResultAsync<true, PrivateFormError | PossibleDatabaseError> => {
): ResultAsync<
IPopulatedForm,
PossibleDatabaseError | PrivateFormError | FormNotFoundError
> => {
const logMeta = {
action: 'checkFormSubmissionLimitAndDeactivateForm',
formId: form._id,
}
if (form.submissionLimit === null) return okAsync(true)
const { submissionLimit } = form
const formId = String(form._id)
// Not using falsey check as submissionLimit === 0 can result in incorrectly
// returning form without any actions.
if (submissionLimit === null) return okAsync(form)

return ResultAsync.fromPromise(
SubmissionModel.countDocuments({
form: form._id,
form: formId,
}).exec(),
(error) => {
logger.error({
Expand All @@ -165,21 +208,18 @@ export const checkFormSubmissionLimitAndDeactivateForm = (
})
return transformMongoError(error)
},
).andThen((count) => {
if (count < form.submissionLimit) return okAsync(true)
).andThen((currentCount) => {
// Limit has not been hit yet, passthrough.
if (currentCount < submissionLimit) return okAsync(form)

logger.info({
message: 'Form reached maximum submission count, deactivating.',
meta: logMeta,
})
return ResultAsync.fromPromise(deactivateForm(form._id), (error) => {
logger.error({
message: 'Error while deactivating form',
meta: logMeta,
error,
})
return transformMongoError(error)
}).andThen(() =>
// Always return err because submission limit was exceeded

// Map success case back into error to display to client as form has been
// deactivated.
return deactivateForm(formId).andThen(() =>
errAsync(
new PrivateFormError(
'Submission made after form submission limit was reached',
Expand All @@ -200,3 +240,39 @@ export const getFormModelByResponseMode = (
return EncryptedFormModel
}
}

/**
* Checks if a form is accessed from within intranet and sets the property accordingly
* @param ip The ip of the request
* @param publicFormView The form to check
* @returns ok(PublicFormView) if the form is accessed from the internet
* @returns err(ApplicationError) if an error occured while checking if the ip of the request is from the intranet
*/
export const checkIsIntranetFormAccess = (
ip: string,
form: IPopulatedForm,
): boolean => {
const isIntranetIpResult = IntranetFactory.isIntranetIp(ip).andThen(
(isIntranetUser) => {
// Warn if form is being accessed from within intranet
// and the form has authentication set
if (
isIntranetUser &&
[AuthType.SP, AuthType.CP, AuthType.MyInfo].includes(form.authType)
) {
logger.warn({
message:
'Attempting to access SingPass, CorpPass or MyInfo form from intranet',
meta: {
action: 'checkIsIntranetFormAccess',
formId: form._id,
},
})
}
return ok(isIntranetUser)
},
)

// This is required becausing the factory can throw missing feature error on initialization
return isIntranetIpResult.unwrapOr(false)
seaerchin marked this conversation as resolved.
Show resolved Hide resolved
seaerchin marked this conversation as resolved.
Show resolved Hide resolved
}
Loading