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

PIMS-2107 Email Field Rework #2701

Merged
merged 15 commits into from
Oct 2, 2024
1 change: 1 addition & 0 deletions express-api/src/controllers/users/usersController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export const submitUserAccessRequest = async (req: Request, res: Response) => {
Number(req.body.AgencyId),
req.body.Position,
req.body.Note,
req.body.Email,
);
const config = getConfig();
const user = await userServices.getUser(req.user.preferred_username);
Expand Down
22 changes: 17 additions & 5 deletions express-api/src/services/users/usersServices.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { Agency } from '@/typeorm/Entities/Agency';
import { randomUUID, UUID } from 'crypto';
import { ErrorWithCode } from '@/utilities/customErrors/ErrorWithCode';
import { UserFiltering } from '@/controllers/users/usersSchema';
import { validateEmail } from '@/utilities/helperFunctions';

interface NormalizedKeycloakUser {
first_name: string;
Expand Down Expand Up @@ -43,21 +44,26 @@ const normalizeKeycloakUser = (kcUser: SSOUser): NormalizedKeycloakUser => {

/**
* Adds a user from Keycloak to the system with 'OnHold' status.
* @param ssoUser The Keycloak user to be added
* @param agencyId The ID of the agency the user belongs to
* @param position The position of the user
* @param note Additional notes about the user
* @param {SSOUser} ssoUser The Keycloak user to be added
* @param {number} agencyId The ID of the agency the user belongs to
* @param {string} position The position of the user
* @param {string} note Additional notes about the user
* @param {string} email the users email
* @returns The inserted user
*/
const addKeycloakUserOnHold = async (
ssoUser: SSOUser,
agencyId: number,
position: string,
note: string,
email: string,
) => {
if (agencyId == null) {
throw new Error('Null argument.');
}
if (!validateEmail(email)) {
throw new Error('Invalid email.');
}
//Iterating through agencies and roles no longer necessary here?
const normalizedKc = normalizeKeycloakUser(ssoUser);
const systemUser = await AppDataSource.getRepository(User).findOne({
Expand All @@ -68,7 +74,7 @@ const addKeycloakUserOnHold = async (
Id: id,
FirstName: normalizedKc.first_name,
LastName: normalizedKc.last_name,
Email: normalizedKc.email,
Email: email,
DisplayName: normalizedKc.display_name,
KeycloakUserId: normalizedKc.guid,
Username: normalizedKc.username,
Expand Down Expand Up @@ -207,6 +213,9 @@ const addUser = async (user: User) => {
if (resource) {
throw new ErrorWithCode('Resource already exists.', 409);
}
if (!validateEmail(user.Email)) {
throw new Error('Invalid email.');
}
const retUser = await AppDataSource.getRepository(User).save(user);
return retUser;
};
Expand All @@ -222,6 +231,9 @@ const updateUser = async (user: DeepPartial<User>) => {
if (!resource) {
throw new ErrorWithCode('Resource does not exist.', 404);
}
if (user.Email && !validateEmail(user.Email)) {
throw new Error('Invalid email.');
}
await AppDataSource.getRepository(User).update(user.Id, {
...user,
DisplayName: `${user.LastName}, ${user.FirstName}`,
Expand Down
4 changes: 4 additions & 0 deletions express-api/src/utilities/helperFunctions.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Equal, FindOptionsWhere, IsNull, Not, Raw } from 'typeorm';
import { z } from 'zod';

/**
* Special case for PID/PIN matching, as general text comparison is not sufficient.
Expand Down Expand Up @@ -244,3 +245,6 @@ export const toPostgresTimestamp = (date: Date) => {
export const getDaysBetween = (earlierDate: Date, laterDate: Date): number => {
return Math.trunc((laterDate.getTime() - earlierDate.getTime()) / (1000 * 60 * 60 * 24));
};

export const validateEmail = (email: string): boolean =>
z.string().email().safeParse(email).success;
84 changes: 77 additions & 7 deletions express-api/tests/unit/services/users/usersServices.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ jest
.spyOn(AppDataSource.getRepository(Agency), 'findOneOrFail')
.mockImplementation(async () => produceAgency());

jest
const _agenciesFind = jest
.spyOn(AppDataSource.getRepository(Agency), 'find')
.mockImplementation(async () => [produceAgency()]);

Expand Down Expand Up @@ -104,22 +104,77 @@ describe('UNIT - User services', () => {
});
});

describe('addAccessRequest', () => {
describe('getUserById', () => {
it('should return a user assuming one is found', async () => {
const user = produceUser();
_usersFindOne.mockImplementationOnce(async () => user);
const result = await userServices.getUserById('123');
expect(result).toEqual(user);
});

it('should return null when no user is found', async () => {
_usersFindOne.mockImplementationOnce(async () => null);
const result = await userServices.getUserById('123');
expect(result).toEqual(null);
});
});

describe('addKeycloakUserOnHold', () => {
it('should add and return an access request', async () => {
const agencyId = faker.number.int();
//const roleId = faker.string.uuid();
const req = await userServices.addKeycloakUserOnHold(ssoUser, agencyId, '', '');
const req = await userServices.addKeycloakUserOnHold(
ssoUser,
agencyId,
'',
'',
'[email protected]',
);
expect(_usersInsert).toHaveBeenCalledTimes(1);
});

it('should throw an error if the agency is null', () => {
expect(
async () =>
await userServices.addKeycloakUserOnHold(ssoUser, null, '', '', '[email protected]'),
).rejects.toThrow('Null argument.');
});

it('should throw an error if the email is invalid', () => {
const agencyId = faker.number.int();
expect(
async () =>
await userServices.addKeycloakUserOnHold(ssoUser, agencyId, '', '', 'email.com'),
).rejects.toThrow('Invalid email.');
});
});

describe('getAgencies', () => {
it('should return an array of agency ids', async () => {
const agencies = await userServices.getAgencies('test');
expect(AppDataSource.getRepository(User).findOneOrFail).toHaveBeenCalledTimes(1);
expect(AppDataSource.getRepository(Agency).find).toHaveBeenCalledTimes(1);
expect(Array.isArray(agencies)).toBe(true);
});

it('should return an empty array if the user is not found', async () => {
_usersFindOneBy.mockImplementationOnce(async () => null);
const agencies = await userServices.getAgencies('test');
expect(Array.isArray(agencies)).toBe(true);
expect(agencies).toHaveLength(0);
});
});

describe('hasAgencies', () => {
it('should return true if the user has the corresponding agencies', async () => {
_agenciesFind.mockImplementationOnce(async () => [produceAgency({ Id: 1 })]);
const result = await userServices.hasAgencies('test', [1]);
expect(result).toEqual(true);
});

it('should return false if the user does not have the corresponding agencies', async () => {
_agenciesFind.mockImplementationOnce(async () => [produceAgency({ Id: 2 })]);
const result = await userServices.hasAgencies('test', [1]);
expect(result).toEqual(false);
});
});

describe('normalizeKeycloakUser', () => {
Expand Down Expand Up @@ -168,7 +223,15 @@ describe('UNIT - User services', () => {
it('should throw an error if the user already exists', async () => {
const user = produceUser();
_usersFindOne.mockResolvedValueOnce(user);
expect(async () => await userServices.addUser(user)).rejects.toThrow();
expect(async () => await userServices.addUser(user)).rejects.toThrow(
'Resource already exists.',
);
});

it('should throw an error if the email is invalid', async () => {
const user = produceUser({ Email: 'blah' });
_usersFindOne.mockResolvedValueOnce(null);
expect(async () => await userServices.addUser(user)).rejects.toThrow('Invalid email.');
});
});
describe('updateUser', () => {
Expand All @@ -187,7 +250,14 @@ describe('UNIT - User services', () => {
it('should throw an error if the user does not exist', () => {
const user = produceUser();
_usersFindOne.mockResolvedValueOnce(undefined);
expect(async () => await userServices.updateUser(user)).rejects.toThrow();
expect(async () => await userServices.updateUser(user)).rejects.toThrow(
'Resource does not exist.',
);
});

it('should throw an error if the email is invalid does not exist', () => {
const user = produceUser({ Email: 'blah' });
expect(async () => await userServices.updateUser(user)).rejects.toThrow('Invalid email.');
});
});
describe('deleteUser', () => {
Expand Down
13 changes: 13 additions & 0 deletions express-api/tests/unit/utilities/helperFunctions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
ILikeWrapper,
TimestampComparisonWrapper,
toPostgresTimestamp,
validateEmail,
} from '@/utilities/helperFunctions';
import { EqualOperator, FindOperator } from 'typeorm';

Expand Down Expand Up @@ -180,4 +181,16 @@ describe('UNIT - helperFunctions', () => {
expect(result.test).toBeUndefined();
});
});

describe('validateEmail', () => {
it('should return true when a valid email is given', () => {
const result = validateEmail('[email protected]');
expect(result).toEqual(true);
});

it('should return false when a invalid email is given', () => {
const result = validateEmail('test@gmaom');
expect(result).toEqual(false);
});
});
});
7 changes: 4 additions & 3 deletions react-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"lint:fix": "npm run lint -- --fix",
"format": "prettier --write \"./src/**/*.{js,jsx,ts,tsx,json,css,scss}\"",
"check": "prettier --check \"./src/**/*.{js,jsx,ts,tsx,css,scss}\"",
"test": "jest",
"test": "jest --passWithNoTests",
"snapshots": "jest --updateSnapshot"
},
"dependencies": {
Expand All @@ -22,7 +22,7 @@
"@mdi/react": "1.6.1",
"@mui/icons-material": "6.1.0",
"@mui/lab": "5.0.0-alpha.170",
"@mui/material": "6.1.0",
"@mui/material": "6.1.2",
"@mui/x-data-grid": "7.18.0",
"@mui/x-date-pickers": "7.18.0",
"@turf/turf": "7.1.0",
Expand All @@ -37,7 +37,8 @@
"react-router-dom": "6.26.0",
"supercluster": "8.0.1",
"typescript-eslint": "8.7.0",
"use-supercluster": "1.2.0"
"use-supercluster": "1.2.0",
"zod": "3.23.8"
},
"devDependencies": {
"@babel/preset-env": "7.25.2",
Expand Down
40 changes: 0 additions & 40 deletions react-app/src/App.test.tsx

This file was deleted.

33 changes: 0 additions & 33 deletions react-app/src/components/form/AutocompleteField.test.tsx

This file was deleted.

21 changes: 0 additions & 21 deletions react-app/src/components/layout/Footer.test.tsx

This file was deleted.

Loading
Loading