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

Validate confirmation Id (activityId) for Enquiries #121

Merged
merged 1 commit into from
Aug 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
25 changes: 25 additions & 0 deletions app/src/controllers/activity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { activityService } from '../services';

import { ACTIVITY_ID_LENGTH } from '../utils/constants/application';

import type { NextFunction, Request, Response } from '../interfaces/IExpress';

const controller = {
validateActivityId: async (req: Request<{ activityId: string }>, res: Response, next: NextFunction) => {
try {
const { activityId } = req.params;
const hexidecimal = parseInt(activityId, 16);

if (activityId.length !== ACTIVITY_ID_LENGTH || !hexidecimal) {
return res.status(400).json({ message: 'Invalid activity Id format' });
}
const activity = await activityService.getActivity(activityId);

res.status(200).json({ valid: !!activity && !activity.isDeleted });
} catch (e: unknown) {
next(e);
}
}
};

export default controller;
1 change: 1 addition & 0 deletions app/src/controllers/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export { default as activityController } from './activity';
export { default as documentController } from './document';
export { default as enquiryController } from './enquiry';
export { default as noteController } from './note';
Expand Down
15 changes: 15 additions & 0 deletions app/src/routes/v1/activity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import express from 'express';
import { activityController } from '../../controllers';
import { requireSomeAuth } from '../../middleware/requireSomeAuth';

import type { NextFunction, Request, Response } from '../../interfaces/IExpress';

const router = express.Router();
router.use(requireSomeAuth);

//** Validates an Activity Id */
router.get('/validate/:activityId', (req: Request, res: Response, next: NextFunction): void => {
qhanson55 marked this conversation as resolved.
Show resolved Hide resolved
activityController.validateActivityId(req, res, next);
});

export default router;
4 changes: 3 additions & 1 deletion app/src/routes/v1/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { currentUser } from '../../middleware/authentication';

import express from 'express';

import activity from './activity';
import document from './document';
import enquiry from './enquiry';
import note from './note';
Expand All @@ -17,10 +18,11 @@ router.use(currentUser);
// Base v1 Responder
router.get('/', (_req, res) => {
res.status(200).json({
endpoints: ['/document', '/enquiry', '/note', '/permit', '/roadmap', '/sso', '/submission', '/user']
endpoints: ['/activity', '/document', '/enquiry', '/note', '/permit', '/roadmap', '/sso', '/submission', '/user']
});
});

router.use('/activity', activity);
router.use('/document', document);
router.use('/enquiry', enquiry);
router.use('/note', note);
Expand Down
2 changes: 2 additions & 0 deletions app/src/utils/constants/application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,5 @@ export const DEFAULTCORS = Object.freeze({
export const YES_NO_LIST = [BasicResponse.YES, BasicResponse.NO];

export const YES_NO_UNSURE_LIST = [BasicResponse.YES, BasicResponse.NO, BasicResponse.UNSURE];

export const ACTIVITY_ID_LENGTH = 8;
146 changes: 146 additions & 0 deletions app/tests/unit/controllers/activity.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import { activityController } from '../../../src/controllers';
import { activityService } from '../../../src/services';

// Mock config library - @see {@link https://stackoverflow.com/a/64819698}
jest.mock('config');

const mockResponse = () => {
const res: { status?: jest.Mock; json?: jest.Mock; end?: jest.Mock } = {};
res.status = jest.fn().mockReturnValue(res);
res.json = jest.fn().mockReturnValue(res);

return res;
};

let res: { status?: jest.Mock; json?: jest.Mock; end?: jest.Mock };
beforeEach(() => {
res = mockResponse();
});

afterEach(() => {
/*
* Must use clearAllMocks when using the mocked config
* resetAllMocks seems to cause strange issues such as
* functions not calling as expected
*/
jest.clearAllMocks();
});

const CURRENT_USER = { authType: 'BEARER', tokenPayload: null };

const ACTIVITY = {
activityId: '12345678',
initiativeId: '59cd9e86-7cce-4791-b071-69002c731315',
isDeleted: false
};

const DELETED_ACTIVITY = {
activityId: '87654321',
initiativeId: '59cd9e86-7cce-4791-b071-69002c731315',
isDeleted: true
};

describe('validateActivityId', () => {
const next = jest.fn();

// Mock service calls
const activityServiceSpy = jest.spyOn(activityService, 'getActivity');

it('shoulld return status 200 and valid true if activityId exists and isDeleted is false', async () => {
const req = {
params: { activityId: '12345678' },
currentUser: CURRENT_USER
};

activityServiceSpy.mockResolvedValue(ACTIVITY);

// eslint-disable-next-line @typescript-eslint/no-explicit-any
await activityController.validateActivityId(req as any, res as any, next);

expect(activityServiceSpy).toHaveBeenCalledTimes(1);
expect(activityServiceSpy).toHaveBeenCalledWith(req.params.activityId);
expect(res.status).toHaveBeenCalledWith(200);
expect(res.json).toHaveBeenCalledWith({ valid: true });
});

it('shoulld return status 200 and valid false if activityId exists but isDeleted is true', async () => {
const req = {
params: { activityId: '87654321' },
currentUser: CURRENT_USER
};

activityServiceSpy.mockResolvedValue(DELETED_ACTIVITY);

// eslint-disable-next-line @typescript-eslint/no-explicit-any
await activityController.validateActivityId(req as any, res as any, next);

expect(activityServiceSpy).toHaveBeenCalledTimes(1);
expect(activityServiceSpy).toHaveBeenCalledWith(req.params.activityId);
expect(res.status).toHaveBeenCalledWith(200);
expect(res.json).toHaveBeenCalledWith({ valid: false });
});

it('shoulld return status 200 and valid false if activityId does not exist', async () => {
const req = {
params: { activityId: 'FFFFFFFF' },
currentUser: CURRENT_USER
};

activityServiceSpy.mockResolvedValue(null);

// eslint-disable-next-line @typescript-eslint/no-explicit-any
await activityController.validateActivityId(req as any, res as any, next);

expect(activityServiceSpy).toHaveBeenCalledTimes(1);
expect(activityServiceSpy).toHaveBeenCalledWith(req.params.activityId);
expect(res.status).toHaveBeenCalledWith(200);
expect(res.json).toHaveBeenCalledWith({ valid: false });
});

it('Should return 400 and error message if activityId does not have correct length', async () => {
const req = {
params: { activityId: '12345' },
currentUser: CURRENT_USER
};

// eslint-disable-next-line @typescript-eslint/no-explicit-any
await activityController.validateActivityId(req as any, res as any, next);

expect(activityServiceSpy).toHaveBeenCalledTimes(0);
expect(res.status).toHaveBeenCalledWith(400);
expect(res.json).toHaveBeenCalledWith({ message: 'Invalid activity Id format' });
});

it('Should return 400 and error message if activityId is not hexidecimal value', async () => {
const req = {
params: { activityId: 'GGGGGGGG' },
currentUser: CURRENT_USER
};

// eslint-disable-next-line @typescript-eslint/no-explicit-any
await activityController.validateActivityId(req as any, res as any, next);

expect(activityServiceSpy).toHaveBeenCalledTimes(0);
expect(res.status).toHaveBeenCalledWith(400);
expect(res.json).toHaveBeenCalledWith({ message: 'Invalid activity Id format' });
});

it('Should call next with error if getActivity fails', async () => {
const req = {
params: { activityId: '12345678' },
currentUser: CURRENT_USER
};

const error = new Error();

activityServiceSpy.mockRejectedValue(error);

// eslint-disable-next-line @typescript-eslint/no-explicit-any
await activityController.validateActivityId(req as any, res as any, next);

expect(activityServiceSpy).toHaveBeenCalledTimes(1);
expect(activityServiceSpy).toHaveBeenCalledWith(req.params.activityId);
expect(next).toHaveBeenCalledTimes(1);
expect(next).toHaveBeenCalledWith(error);
});
});
22 changes: 20 additions & 2 deletions frontend/src/components/housing/enquiry/EnquiryIntakeForm.vue
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ import { Dropdown, InputMask, RadioList, InputText, StepperNavigation, TextArea
import CollectionDisclaimer from '@/components/housing/CollectionDisclaimer.vue';
import EnquiryIntakeConfirmation from '@/components/housing/enquiry/EnquiryIntakeConfirmation.vue';
import { Button, Card, Divider, useConfirm, useToast } from '@/lib/primevue';
import { enquiryService, submissionService } from '@/services';
import { activityService, enquiryService, submissionService } from '@/services';
import { useConfigStore } from '@/store';
import { YES_NO_LIST } from '@/utils/constants/application';
import { ACTIVITY_ID_LENGTH, YES_NO_LIST } from '@/utils/constants/application';
import { CONTACT_PREFERENCE_LIST, PROJECT_RELATIONSHIP_LIST } from '@/utils/constants/housing';
import { BasicResponse, Regex, RouteName } from '@/utils/enums/application';
import { IntakeFormCategory, IntakeStatus } from '@/utils/enums/housing';
Expand Down Expand Up @@ -234,6 +234,23 @@ async function emailConfirmation(activityId: string) {
};
await submissionService.emailConfirmation(emailData);
}
async function checkActivityIdValidity(event: Event) {
const target = event.target as HTMLInputElement;
const activityId = target.value;
const hexidecimal = parseInt(activityId, 16);
if (activityId) {
if (activityId.length === ACTIVITY_ID_LENGTH && hexidecimal) {
const valid = (await activityService.checkActivityIdValidity(activityId)).data.valid;
if (!valid) {
toast.warn(`Confirmation ID ${activityId} does not exist, please check again.`);
}
} else {
toast.warn('Confirmation ID is not the right format, please check again.');
}
}
}
</script>

<template>
Expand Down Expand Up @@ -369,6 +386,7 @@ async function emailConfirmation(activityId: string) {
name="basic.relatedActivityId"
placeholder="Confirmation ID"
:disabled="!editable"
@on-change="checkActivityIdValidity"
/>
</div>
</template>
Expand Down
12 changes: 12 additions & 0 deletions frontend/src/services/activityService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { appAxios } from './interceptors';

export default {
/**
* @function checkActivityIdValidity
* Checks if an activity ID is valid
* @returns {Promise} An axios response
*/
checkActivityIdValidity(activityId: string) {
return appAxios().get(`activity/validate/${activityId}`);
}
};
1 change: 1 addition & 0 deletions frontend/src/services/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export { default as AuthService } from './authService';
export { default as ConfigService } from './configService';
export { default as PermissionService } from './permissionService';
export { default as activityService } from './activityService';
export { default as comsService } from './comsService';
export { default as documentService } from './documentService';
export { default as enquiryService } from './enquiryService';
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/utils/constants/application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,5 @@ export const SYSTEM_USER = NIL;

export const YES_NO_LIST = [BasicResponse.YES, BasicResponse.NO];
export const YES_NO_UNSURE_LIST = [BasicResponse.YES, BasicResponse.NO, BasicResponse.UNSURE];

export const ACTIVITY_ID_LENGTH = 8;
4 changes: 0 additions & 4 deletions frontend/src/views/DeveloperView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,6 @@ const { getConfig } = storeToRefs(useConfigStore());
const permissionService = new PermissionService();
const router = useRouter();
async function ssoRequestBasicAccess() {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

deleted as was triggering unused warning

await permissionService.requestBasicAccess();
}
async function searchIdirUsers() {
await permissionService.searchIdirUsers({ firstName: 'Kyle' });
}
Expand Down
Loading