Skip to content

Commit

Permalink
fe/be tests for contact management
Browse files Browse the repository at this point in the history
  • Loading branch information
sanjaytkbabu committed Jan 9, 2025
1 parent 82849d4 commit 8adf7e4
Show file tree
Hide file tree
Showing 9 changed files with 429 additions and 23 deletions.
2 changes: 1 addition & 1 deletion app/src/validators/contact.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ const schema = {
email: Joi.string().max(255).required(),
firstName: Joi.string().max(255).required(),
lastName: Joi.string().max(255).required(),
phoneNumber: Joi.number().required(),
phoneNumber: phoneNumber.required(),
contactApplicantRelationship: Joi.string()
.required()
.valid(...PROJECT_RELATIONSHIP_LIST),
Expand Down
219 changes: 219 additions & 0 deletions app/tests/unit/controllers/contact.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
import { contactService } from '../../../src/services';
import contactController from '../../../src/controllers/contact';
import { Request, Response } from 'express';

// 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 = mockResponse();
beforeEach(() => {
res = mockResponse();
});

afterEach(() => {
jest.resetAllMocks();
});

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

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

describe('searchContacts', () => {
const searchContactsSpy = jest.spyOn(contactService, 'searchContacts');

it('should return 200 if all good', async () => {
const req = {
query: { userId: '5e3f0c19-8664-4a43-ac9e-210da336e923' },
currentContext: CURRENT_CONTEXT
} as unknown as Request;

const contacts = [
{
contactId: 'contact123',
userId: 'user123',
firstName: 'John',
lastName: 'Doe',
phoneNumber: '123-456-7890',
email: '[email protected]',
contactPreference: 'email',
contactApplicantRelationship: 'applicant'
}
];

searchContactsSpy.mockResolvedValue(contacts);

// eslint-disable-next-line @typescript-eslint/no-explicit-any
await contactController.searchContacts(req as any, res as unknown as Response, next);

expect(searchContactsSpy).toHaveBeenCalledTimes(1);
expect(searchContactsSpy).toHaveBeenCalledWith({
userId: ['5e3f0c19-8664-4a43-ac9e-210da336e923'],
contactId: undefined,
email: undefined,
firstName: undefined,
lastName: undefined,
contactApplicantRelationship: undefined,
phoneNumber: undefined
});
expect(res.status).toHaveBeenCalledWith(200);
expect(res.json).toHaveBeenCalledWith(contacts);
});

it('adds dashes to user IDs', async () => {
const req = {
query: { userId: '5e3f0c1986644a43ac9e210da336e923,8b9dedd279d442c6b82f52844a8e2757' },
currentContext: CURRENT_CONTEXT
} as unknown as Request;

const contacts = [
{
contactId: 'contact123',
userId: 'user123',
firstName: 'John',
lastName: 'Doe',
phoneNumber: '123-456-7890',
email: '[email protected]',
contactPreference: 'email',
contactApplicantRelationship: 'applicant'
}
];

searchContactsSpy.mockResolvedValue(contacts);

// eslint-disable-next-line @typescript-eslint/no-explicit-any
await contactController.searchContacts(req as any, res as unknown as Response, next);

expect(searchContactsSpy).toHaveBeenCalledTimes(1);
expect(searchContactsSpy).toHaveBeenCalledWith({
userId: ['5e3f0c19-8664-4a43-ac9e-210da336e923', '8b9dedd2-79d4-42c6-b82f-52844a8e2757'],
contactId: undefined,
email: undefined,
firstName: undefined,
lastName: undefined,
contactApplicantRelationship: undefined,
phoneNumber: undefined
});
});

it('adds dashes to contact IDs', async () => {
const req = {
query: { contactId: '5e3f0c1986644a43ac9e210da336e923,8b9dedd279d442c6b82f52844a8e2757' },
currentContext: CURRENT_CONTEXT
} as unknown as Request;

const contacts = [
{
contactId: 'contact123',
userId: 'user123',
firstName: 'John',
lastName: 'Doe',
phoneNumber: '123-456-7890',
email: '[email protected]',
contactPreference: 'email',
contactApplicantRelationship: 'applicant'
}
];

searchContactsSpy.mockResolvedValue(contacts);

// eslint-disable-next-line @typescript-eslint/no-explicit-any
await contactController.searchContacts(req as any, res as unknown as Response, next);

expect(searchContactsSpy).toHaveBeenCalledTimes(1);
expect(searchContactsSpy).toHaveBeenCalledWith({
contactId: ['5e3f0c19-8664-4a43-ac9e-210da336e923', '8b9dedd2-79d4-42c6-b82f-52844a8e2757'],
userId: undefined,
email: undefined,
firstName: undefined,
lastName: undefined,
contactApplicantRelationship: undefined,
phoneNumber: undefined
});
});

it('calls next if the contact service fails to list contacts', async () => {
const req = {
query: { userId: '5e3f0c19-8664-4a43-ac9e-210da336e923' },
currentContext: CURRENT_CONTEXT
} as unknown as Request;

searchContactsSpy.mockImplementationOnce(() => {
throw new Error();
});

// eslint-disable-next-line @typescript-eslint/no-explicit-any
await contactController.searchContacts(req as any, res as unknown as Response, next);

expect(searchContactsSpy).toHaveBeenCalledTimes(1);
expect(searchContactsSpy).toHaveBeenCalledWith({
userId: ['5e3f0c19-8664-4a43-ac9e-210da336e923'],
contactId: undefined,
email: undefined,
firstName: undefined,
lastName: undefined,
contactApplicantRelationship: undefined,
phoneNumber: undefined
});
expect(res.status).toHaveBeenCalledTimes(0);
expect(next).toHaveBeenCalledTimes(1);
});
});

describe('updateContact', () => {
const upsertContactsSpy = jest.spyOn(contactService, 'upsertContacts');

it('should return 200 if contact is updated successfully', async () => {
const req = {
body: {
userId: '5e3f0c19-8664-4a43-ac9e-210da336e923',
email: '[email protected]',
firstName: 'First',
lastName: 'Last'
},
currentContext: CURRENT_CONTEXT
} as unknown as Request;

upsertContactsSpy.mockResolvedValue();

// eslint-disable-next-line @typescript-eslint/no-explicit-any
await contactController.updateContact(req as any, res as unknown as Response, next);

expect(upsertContactsSpy).toHaveBeenCalledTimes(1);
expect(upsertContactsSpy).toHaveBeenCalledWith([req.body], req.currentContext);
expect(res.status).toHaveBeenCalledWith(200);
});

it('calls next if the contact service fails to update contact', async () => {
const req = {
body: {
userId: '5e3f0c19-8664-4a43-ac9e-210da336e923',
email: '[email protected]',
firstName: 'First',
lastName: 'Last'
},
currentContext: CURRENT_CONTEXT
} as unknown as Request;

upsertContactsSpy.mockImplementationOnce(() => {
throw new Error();
});

// eslint-disable-next-line @typescript-eslint/no-explicit-any
await contactController.updateContact(req as any, res as unknown as Response, next);

expect(upsertContactsSpy).toHaveBeenCalledTimes(1);
expect(upsertContactsSpy).toHaveBeenCalledWith([req.body], req.currentContext);
expect(res.status).toHaveBeenCalledTimes(0);
expect(next).toHaveBeenCalledTimes(1);
});
});
});
13 changes: 9 additions & 4 deletions frontend/src/components/form/InputMask.vue
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ const {
placeholder = '',
disabled = false,
bold = true,
floatLabel = false
floatLabel = false,
showError = true
} = defineProps<{
helpText?: string;
label?: string;
Expand All @@ -23,21 +24,25 @@ const {
disabled?: boolean;
bold?: boolean;
floatLabel?: boolean;
showError?: boolean;
}>();
</script>

<template>
<div class="field">
<FloatLabel v-if="floatLabel">
<InputMaskInternal v-bind="{ label, name, mask, placeholder, disabled, bold }" />
<InputMaskInternal v-bind="{ label, name, mask, placeholder, disabled, bold, showError }" />
</FloatLabel>
<InputMaskInternal
v-else
v-bind="{ label, name, mask, placeholder, disabled, bold }"
v-bind="{ label, name, mask, placeholder, disabled, bold, showError }"
/>

<small :id="`${name}-help`">{{ helpText }}</small>
<div class="mt-2">
<div
v-if="!disabled && showError"
class="mt-2"
>
<ErrorMessage
:name="name"
class="app-error-message"
Expand Down
13 changes: 11 additions & 2 deletions frontend/src/components/form/internal/InputMaskInternal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,22 @@ import { useField } from 'vee-validate';
import { InputMask } from '@/lib/primevue';
// Props
const { label, name, mask, placeholder, disabled, bold } = defineProps<{
const {
label,
name,
mask,
placeholder,
disabled,
bold,
showError = true
} = defineProps<{
label: string;
name: string;
mask: string;
placeholder?: string;
disabled: boolean;
bold: boolean;
showError: boolean;
}>();
const { errorMessage, value } = useField<string>(name);
Expand Down Expand Up @@ -42,7 +51,7 @@ const normalizedValue = computed({
:mask="mask"
:placeholder="placeholder"
class="w-full"
:class="{ 'p-invalid': errorMessage }"
:class="!disabled && showError ? { 'p-invalid': errorMessage } : ''"
:disabled="disabled"
/>
</template>
2 changes: 1 addition & 1 deletion frontend/src/locales/en-CA.json
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@
"onThisPage": "On this page",
"of": "of",
"projectState": "Project state",
"projectsEmpty": "Submit a new project to the Navigator Service to found out what permits are required",
"projectsEmpty": "Submit a new project to the Navigator Service to find out what permits are required",
"projectsTooltip": "Submit a housing project to the Navigator service to find out what permits are required for your projects, track permit applications, and submit related enquiries.",
"submitNewEnquiry": "Submit a new enquiry",
"submitNewProject": "Submit a new project"
Expand Down
1 change: 1 addition & 0 deletions frontend/src/services/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export { default as atsService } from './atsService';
export { default as AuthService } from './authService';
export { default as ConfigService } from './configService';
export { default as comsService } from './comsService';
export { default as contactService } from './contactService';
export { default as documentService } from './documentService';
export { default as enquiryService } from './enquiryService';
export { default as externalApiService } from './externalApiService';
Expand Down
Loading

0 comments on commit 8adf7e4

Please sign in to comment.