Skip to content

Commit

Permalink
implemented warning in frontend for invalid confirmation(activity) Id…
Browse files Browse the repository at this point in the history
…s and a new endpoint in backend to perform said check
  • Loading branch information
qhanson55 committed Aug 1, 2024
1 parent 4ff1602 commit 25882d0
Show file tree
Hide file tree
Showing 11 changed files with 227 additions and 7 deletions.
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 => {
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() {
await permissionService.requestBasicAccess();
}
async function searchIdirUsers() {
await permissionService.searchIdirUsers({ firstName: 'Kyle' });
}
Expand Down

0 comments on commit 25882d0

Please sign in to comment.