-
Notifications
You must be signed in to change notification settings - Fork 87
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: migrate GET /adminform endpoint to Typescript #575
Merged
+472
−139
Merged
Changes from all commits
Commits
Show all changes
14 commits
Select commit
Hold shift + click to select a range
d1a5f55
refactor(FormModel): move types into form.types
karrui fa50558
feat(FormModel): add getDashboardForms static function
karrui 43c3a80
refactor: use form model static function to list dashboard forms
karrui 5c41c2f
feat(AdminFormSvc): add getDashboardForms service function
karrui 09c078e
feat(AdminFormCtl): add handler for GET /adminforms endpoint
karrui d2b3a81
refactor(AdminFormRoutes): use new handleListDashboardForms impl
karrui 675ff47
test(AdminFormSvc): add tests for getDashboardForms
karrui 81a1573
feat(FormModel): return lean object when retrieving dashboard forms
karrui bbc3f8a
test(FormModel): add tests for getDashboardForms static fn
karrui 346a988
test: remove test cases for removed list form fn
karrui 6fac6a4
test(AdminFormCtl): add tests
karrui 335bb32
Merge branch 'develop' into ref/admin-dashboard-form-ts
karrui 8a0a587
feat(AdminFormsRoutes): update list flow to use middleware
karrui 7e22f6c
test(FormModel): add stronger test case to test return array
karrui File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,18 +1,22 @@ | ||
import BSON from 'bson-ext' | ||
import { compact, filter, pick, uniq } from 'lodash' | ||
import { Model, Mongoose, Schema, SchemaOptions } from 'mongoose' | ||
import { Mongoose, Schema, SchemaOptions } from 'mongoose' | ||
import validator from 'validator' | ||
|
||
import { FORM_DUPLICATE_KEYS } from '../../shared/constants' | ||
import { | ||
AuthType, | ||
BasicField, | ||
Colors, | ||
DashboardFormView, | ||
FormLogoState, | ||
FormOtpData, | ||
IEmailFormModel, | ||
IEmailFormSchema, | ||
IEncryptedFormModel, | ||
IEncryptedFormSchema, | ||
IForm, | ||
IFormModel, | ||
IFormSchema, | ||
IPopulatedForm, | ||
LogicType, | ||
|
@@ -86,23 +90,13 @@ const formSchemaOptions: SchemaOptions = { | |
}, | ||
} | ||
|
||
export interface IFormModel extends Model<IFormSchema> { | ||
getOtpData(formId: string): Promise<FormOtpData | null> | ||
getFullFormById(formId: string): Promise<IPopulatedForm | null> | ||
deactivateById(formId: string): Promise<IFormSchema | null> | ||
} | ||
|
||
type IEncryptedFormModel = Model<IEncryptedFormSchema> & IFormModel | ||
|
||
const EncryptedFormSchema = new Schema<IEncryptedFormSchema>({ | ||
publicKey: { | ||
type: String, | ||
required: true, | ||
}, | ||
}) | ||
|
||
type IEmailFormModel = Model<IEmailFormSchema> & IFormModel | ||
|
||
// Converts '[email protected], [email protected]' to ['[email protected]', '[email protected]'] | ||
function transformEmailString(v: string): string[] { | ||
return v | ||
|
@@ -501,6 +495,32 @@ const compileFormModel = (db: Mongoose): IFormModel => { | |
return form.save() | ||
} | ||
|
||
FormSchema.statics.getDashboardForms = async function ( | ||
this: IFormModel, | ||
userId: IUserSchema['_id'], | ||
userEmail: IUserSchema['email'], | ||
): Promise<DashboardFormView[]> { | ||
return ( | ||
this.find() | ||
// List forms when either the user is an admin or collaborator. | ||
.or([{ 'permissionList.email': userEmail }, { admin: userId }]) | ||
// Filter out archived forms. | ||
.where('status') | ||
.ne(Status.Archived) | ||
// Project selected fields. | ||
.select('_id title admin lastModified status form_fields') | ||
.sort('-lastModified') | ||
.populate({ | ||
path: 'admin', | ||
populate: { | ||
path: 'agency', | ||
}, | ||
}) | ||
.lean() | ||
.exec() | ||
) | ||
} | ||
|
||
// Hooks | ||
FormSchema.pre<IFormSchema>('validate', async function (next) { | ||
// Reject save if form document is too large | ||
|
68 changes: 68 additions & 0 deletions
68
src/app/modules/form/admin-form/__tests__/admin-form.controller.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
import { errAsync, okAsync } from 'neverthrow' | ||
import { mocked } from 'ts-jest/utils' | ||
|
||
import { DatabaseError } from 'src/app/modules/core/core.errors' | ||
import { MissingUserError } from 'src/app/modules/user/user.errors' | ||
|
||
import expressHandler from 'tests/unit/backend/helpers/jest-express' | ||
|
||
import { handleListDashboardForms } from '../admin-form.controller' | ||
import * as AdminFormService from '../admin-form.service' | ||
|
||
jest.mock('../admin-form.service') | ||
const MockAdminFormService = mocked(AdminFormService) | ||
|
||
describe('admin-form.controller', () => { | ||
describe('handleListDashboardForms', () => { | ||
const MOCK_REQ = expressHandler.mockRequest({ | ||
session: { | ||
user: { | ||
_id: 'exists', | ||
}, | ||
}, | ||
}) | ||
it('should return 200 with list of forms', async () => { | ||
// Arrange | ||
const mockRes = expressHandler.mockResponse() | ||
// Mock return array. | ||
MockAdminFormService.getDashboardForms.mockReturnValueOnce(okAsync([])) | ||
|
||
// Act | ||
await handleListDashboardForms(MOCK_REQ, mockRes, jest.fn()) | ||
|
||
// Assert | ||
expect(mockRes.json).toHaveBeenCalledWith([]) | ||
}) | ||
|
||
it('should return 422 on MissingUserError', async () => { | ||
// Arrange | ||
const mockRes = expressHandler.mockResponse() | ||
MockAdminFormService.getDashboardForms.mockReturnValueOnce( | ||
errAsync(new MissingUserError()), | ||
) | ||
|
||
// Act | ||
await handleListDashboardForms(MOCK_REQ, mockRes, jest.fn()) | ||
|
||
// Assert | ||
expect(mockRes.status).toBeCalledWith(422) | ||
expect(mockRes.json).toBeCalledWith({ message: 'User not found' }) | ||
}) | ||
|
||
it('should return 500 when database error occurs', async () => { | ||
// Arrange | ||
const mockRes = expressHandler.mockResponse() | ||
const mockErrorString = 'something went wrong' | ||
MockAdminFormService.getDashboardForms.mockReturnValueOnce( | ||
errAsync(new DatabaseError(mockErrorString)), | ||
) | ||
|
||
// Act | ||
await handleListDashboardForms(MOCK_REQ, mockRes, jest.fn()) | ||
|
||
// Assert | ||
expect(mockRes.status).toBeCalledWith(500) | ||
expect(mockRes.json).toBeCalledWith({ message: mockErrorString }) | ||
}) | ||
}) | ||
}) |
92 changes: 92 additions & 0 deletions
92
src/app/modules/form/admin-form/__tests__/admin-form.service.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,92 @@ | ||
import mongoose from 'mongoose' | ||
import { errAsync, okAsync } from 'neverthrow' | ||
import { mocked } from 'ts-jest/utils' | ||
|
||
import getFormModel from 'src/app/models/form.server.model' | ||
import { DatabaseError } from 'src/app/modules/core/core.errors' | ||
import { MissingUserError } from 'src/app/modules/user/user.errors' | ||
import * as UserService from 'src/app/modules/user/user.service' | ||
import { DashboardFormView, IPopulatedUser, IUserSchema } from 'src/types' | ||
|
||
import { getDashboardForms } from '../admin-form.service' | ||
|
||
const FormModel = getFormModel(mongoose) | ||
|
||
jest.mock('src/app/modules/user/user.service') | ||
const MockUserService = mocked(UserService) | ||
|
||
describe('admin-form.service', () => { | ||
describe('getDashboardForms', () => { | ||
it('should return list of forms user is authorized to view', async () => { | ||
// Arrange | ||
const mockUserId = 'mockUserId' | ||
const mockUser: Partial<IUserSchema> = { | ||
email: '[email protected]', | ||
_id: mockUserId, | ||
} | ||
const mockDashboardForms: DashboardFormView[] = [ | ||
{ | ||
admin: {} as IPopulatedUser, | ||
title: 'test form 1', | ||
}, | ||
{ | ||
admin: {} as IPopulatedUser, | ||
title: 'test form 2', | ||
}, | ||
] | ||
// Mock user admin success. | ||
MockUserService.findAdminById.mockReturnValueOnce( | ||
okAsync(mockUser as IUserSchema), | ||
) | ||
const getSpy = jest | ||
.spyOn(FormModel, 'getDashboardForms') | ||
.mockResolvedValueOnce(mockDashboardForms) | ||
|
||
// Act | ||
const actualResult = await getDashboardForms(mockUserId) | ||
|
||
// Assert | ||
expect(getSpy).toHaveBeenCalledWith(mockUserId, mockUser.email) | ||
expect(actualResult.isOk()).toEqual(true) | ||
expect(actualResult._unsafeUnwrap()).toEqual(mockDashboardForms) | ||
}) | ||
|
||
it('should return MissingUserError when user with userId does not exist', async () => { | ||
// Arrange | ||
const expectedError = new MissingUserError('not found') | ||
MockUserService.findAdminById.mockReturnValueOnce(errAsync(expectedError)) | ||
|
||
// Act | ||
const actualResult = await getDashboardForms('any') | ||
|
||
// Assert | ||
expect(actualResult.isErr()).toEqual(true) | ||
// Error should passthrough. | ||
expect(actualResult._unsafeUnwrapErr()).toEqual(expectedError) | ||
}) | ||
|
||
it('should return DatabaseError when error occurs whilst querying the database', async () => { | ||
// Arrange | ||
const mockUserId = 'mockUserId' | ||
const mockUser: Partial<IUserSchema> = { | ||
email: '[email protected]', | ||
_id: mockUserId, | ||
} | ||
// Mock user admin success. | ||
MockUserService.findAdminById.mockReturnValueOnce( | ||
okAsync(mockUser as IUserSchema), | ||
) | ||
const getSpy = jest | ||
.spyOn(FormModel, 'getDashboardForms') | ||
.mockRejectedValueOnce(new Error('some error')) | ||
|
||
// Act | ||
const actualResult = await getDashboardForms(mockUserId) | ||
|
||
// Assert | ||
expect(getSpy).toHaveBeenCalledWith(mockUserId, mockUser.email) | ||
expect(actualResult.isErr()).toEqual(true) | ||
expect(actualResult._unsafeUnwrapErr()).toEqual(new DatabaseError()) | ||
}) | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
import { RequestHandler } from 'express' | ||
|
||
import { createLoggerWithLabel } from '../../../../config/logger' | ||
|
||
import { getDashboardForms } from './admin-form.service' | ||
import { mapRouteError } from './admin-form.utils' | ||
|
||
const logger = createLoggerWithLabel(module) | ||
|
||
/** | ||
* Handler for GET /adminform endpoint. | ||
* @security session | ||
* | ||
* @returns 200 with list of forms user can access when list is retrieved successfully | ||
* @returns 422 when user of given id cannnot be found in the database | ||
* @returns 500 when database errors occur | ||
*/ | ||
export const handleListDashboardForms: RequestHandler = async (req, res) => { | ||
const authedUserId = (req.session as Express.AuthedSession).user._id | ||
const dashboardResult = await getDashboardForms(authedUserId) | ||
|
||
if (dashboardResult.isErr()) { | ||
const { error } = dashboardResult | ||
logger.error({ | ||
message: 'Error listing dashboard forms', | ||
meta: { | ||
action: 'handleListDashboardForms', | ||
karrui marked this conversation as resolved.
Show resolved
Hide resolved
|
||
userId: authedUserId, | ||
}, | ||
error, | ||
}) | ||
const { errorMessage, statusCode } = mapRouteError(error) | ||
return res.status(statusCode).json({ message: errorMessage }) | ||
} | ||
|
||
// Success. | ||
return res.json(dashboardResult.value) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
import mongoose from 'mongoose' | ||
import { ResultAsync } from 'neverthrow' | ||
|
||
import { createLoggerWithLabel } from '../../../../config/logger' | ||
import { DashboardFormView } from '../../../../types' | ||
import getFormModel from '../../../models/form.server.model' | ||
import { DatabaseError } from '../../core/core.errors' | ||
import { MissingUserError } from '../../user/user.errors' | ||
import { findAdminById } from '../../user/user.service' | ||
|
||
const logger = createLoggerWithLabel(module) | ||
const FormModel = getFormModel(mongoose) | ||
|
||
/** | ||
* Retrieves a list of forms that the user of the given userId can access in | ||
* their dashboard. | ||
* @param userId the id of the user to retrieve accessible forms for. | ||
* @returns ok(DashboardFormViewList) | ||
* @returns err(MissingUserError) if user with userId does not exist in the database | ||
* @returns err(DatabaseError) if error occurs whilst querying the database | ||
*/ | ||
export const getDashboardForms = ( | ||
userId: string, | ||
): ResultAsync<DashboardFormView[], MissingUserError | DatabaseError> => { | ||
// Step 1: Verify user exists. | ||
return ( | ||
findAdminById(userId) | ||
// Step 2: Retrieve lists users are authorized to see. | ||
.andThen((admin) => { | ||
return ResultAsync.fromPromise( | ||
FormModel.getDashboardForms(userId, admin.email), | ||
(error) => { | ||
logger.error({ | ||
message: 'Database error when retrieving admin dashboard forms', | ||
meta: { | ||
action: 'getDashboardForms', | ||
userId, | ||
}, | ||
error, | ||
}) | ||
|
||
return new DatabaseError() | ||
}, | ||
) | ||
}) | ||
) | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
might be cleaner to extract this into a constants file somewhere
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Only used here, no need to extract for now