Skip to content

Commit

Permalink
Merge branch 'main' into bcassessment-lookup
Browse files Browse the repository at this point in the history
  • Loading branch information
GrahamS-Quartech committed Mar 18, 2024
2 parents 1ee5b27 + 2b2b798 commit 3c9efe6
Show file tree
Hide file tree
Showing 13 changed files with 1,561 additions and 66 deletions.
2 changes: 2 additions & 0 deletions express-api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"express": "4.18.2",
"express-rate-limit": "7.2.0",
"morgan": "1.10.0",
"node-cron": "3.0.3",
"node-sql-reader": "0.1.3",
"pg": "8.11.3",
"reflect-metadata": "0.2.1",
Expand All @@ -50,6 +51,7 @@
"@types/jest": "29.5.10",
"@types/morgan": "1.9.9",
"@types/node": "20.11.13",
"@types/node-cron": "3.0.11",
"@types/supertest": "6.0.2",
"@types/swagger-ui-express": "4.1.6",
"@typescript-eslint/eslint-plugin": "7.2.0",
Expand Down
2 changes: 0 additions & 2 deletions express-api/src/controllers/admin/roles/rolesController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { Request, Response } from 'express';
import rolesServices from '@/services/admin/rolesServices';
import { RolesFilter, RolesFilterSchema } from '@/controllers/admin/roles/rolesSchema';
import { UUID } from 'crypto';
import KeycloakService from '@/services/keycloak/keycloakService';

/**
* @description Gets a paged list of roles.
Expand All @@ -20,7 +19,6 @@ export const getRoles = async (req: Request, res: Response) => {
*/
try {
const filter = RolesFilterSchema.safeParse(req.query);
await KeycloakService.syncKeycloakRoles();
if (filter.success) {
const roles = await rolesServices.getRoles(filter.data as RolesFilter); //await rolesServices.getRoles(filter.data as RolesFilter);
return res.status(200).send(roles);
Expand Down
5 changes: 1 addition & 4 deletions express-api/src/controllers/users/usersController.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import userServices from '@/services/users/usersServices';
import { Request, Response } from 'express';
import { KeycloakUser } from '@bcgov/citz-imb-kc-express';
import KeycloakService from '@/services/keycloak/keycloakService';
import { decodeJWT } from '@/utilities/decodeJWT';
/**
* @description Redirects user to the keycloak user info endpoint.
Expand Down Expand Up @@ -165,12 +164,10 @@ export const getUserAgencies = async (req: Request, res: Response) => {

export const getSelf = async (req: Request, res: Response) => {
try {
await KeycloakService.syncKeycloakRoles();
const user = userServices.normalizeKeycloakUser(req.user as KeycloakUser);
const result = await userServices.getUser(user.username);
if (result) {
const syncedUser = await KeycloakService.syncKeycloakUser(user.username);
return res.status(200).send(syncedUser);
return res.status(200).send(result);
} else {
return res.status(204).send(); //Valid request, but no user for this keycloak login.
}
Expand Down
11 changes: 10 additions & 1 deletion express-api/src/middleware/keycloak/keycloakOptions.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,19 @@
import { KCOptions, KeycloakUser } from '@bcgov/citz-imb-kc-express';
import logger from '@/utilities/winstonLogger';
import KeycloakService from '@/services/keycloak/keycloakService';
import userServices from '@/services/users/usersServices';

export const KEYCLOAK_OPTIONS: KCOptions = {
afterUserLogin: (user: KeycloakUser) => {
afterUserLogin: async (user: KeycloakUser) => {
if (user) {
logger.info(`${user.display_name} has logged in.`);
// Try to sync the user's roles from Keycloak
try {
const normalizedUser = userServices.normalizeKeycloakUser(user);
await KeycloakService.syncKeycloakUser(normalizedUser.username);
} catch (e) {
logger.warn(e.message);
}
}
},
afterUserLogout: (user: KeycloakUser) => {
Expand Down
4 changes: 4 additions & 0 deletions express-api/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import app from '@/express';
import { AppDataSource } from '@/appDataSource';
import { Application } from 'express';
import { IncomingMessage, Server, ServerResponse } from 'http';
import cronSyncKeycloakRoles from '@/utilities/cronJobs/syncKeycloakRoles';

const { API_PORT } = constants;

Expand All @@ -26,6 +27,9 @@ const startApp = (app: Application) => {
.catch((err?: Error) => {
logger.error('Error during data source initialization. With error: ', err);
});

// Starting cron jobs
cronSyncKeycloakRoles();
});

return server;
Expand Down
27 changes: 11 additions & 16 deletions express-api/src/services/agencies/agencySchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,29 +12,24 @@ export const AgencyCreationSchema = z.object({
sortOrder: z.coerce.number(),
type: z.string(),
code: z.string(),
parentId: z.string(),
parentId: z.number().int(),
description: z.string(),
email: z.string(),
ccEmail: z.string(),
sendEmail: z.string(),
addressTo: z.string(),
});

//The swagger for the .NET verison was once again misleading about what fields you can filter by.
//This rendition is based off the actual AgencyFilter C# Model.
export const AgencyFilterSchema = AgencyCreationSchema.partial()
.pick({
name: true,
parentId: true,
isDisabled: true,
sortOrder: true,
})
.extend({
page: z.coerce.number().optional(),
quantity: z.coerce.number().optional(),
sort: z.string().optional(),
id: z.coerce.number().optional(),
});
export const AgencyFilterSchema = z.object({
name: z.string().optional(),
parentId: z.coerce.number().int().optional(),
isDisabled: z.coerce.boolean().optional(),
sortOrder: z.coerce.number().optional(),
page: z.coerce.number().optional(),
quantity: z.coerce.number().optional(),
sort: z.string().optional(),
id: z.coerce.number().optional(),
});

export const AgencyPublicResponseSchema = z.object({
Id: z.number(),
Expand Down
19 changes: 19 additions & 0 deletions express-api/src/utilities/cronJobs/syncKeycloakRoles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import KeycloakService from '@/services/keycloak/keycloakService';
import logger from '@/utilities/winstonLogger';
import cron from 'node-cron';

const cronSyncKeycloakRoles = () => {
// minute, hour, day of month, month, day of week
const cronSchedule = '*/10 * * * *'; // Every 10 minutes
cron.schedule(cronSchedule, async () => {
logger.info(`Starting Sync Keycloak Roles routine at ${new Date().toISOString()}.`);
try {
await KeycloakService.syncKeycloakRoles();
logger.info(`Sync Keycloak Roles routine complete at ${new Date().toISOString()}.`);
} catch (e) {
logger.warn(e.message);
}
});
};

export default cronSyncKeycloakRoles;
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@ describe('UNIT - Agencies Admin', () => {
});

describe('Controller getAgencies', () => {
// TODO: enable other tests when controller is complete
it('should return status 200 and a list of agencies', async () => {
await controllers.getAgencies(mockRequest, mockResponse);
expect(mockResponse.statusValue).toBe(200);
Expand All @@ -61,7 +60,7 @@ describe('UNIT - Agencies Admin', () => {
it('should return status 200 and a list of agencies', async () => {
mockRequest.query = {
name: 'a',
parentId: 'a',
parentId: '0',
id: '1',
};
await controllers.getAgencies(mockRequest, mockResponse);
Expand Down
10 changes: 0 additions & 10 deletions express-api/tests/unit/controllers/users/usersController.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,16 +39,6 @@ jest.mock('@/services/users/usersServices', () => ({
normalizeKeycloakUser: () => _normalizeKeycloakUser(),
}));

const _syncKeycloakRoles = jest.fn();
const _syncKeycloakUser = jest
.fn()
.mockImplementation((username: string) => ({ ...produceUser(), Username: username }));

jest.mock('@/services/keycloak/keycloakService.ts', () => ({
syncKeycloakRoles: () => _syncKeycloakRoles(),
syncKeycloakUser: () => _syncKeycloakUser(),
}));

describe('UNIT - Testing controllers for users routes.', () => {
let mockRequest: Request & MockReq, mockResponse: Response & MockRes;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import cronSyncKeycloakRoles from '@/utilities/cronJobs/syncKeycloakRoles';
import cron, { ScheduledTask } from 'node-cron';
import logger from '@/utilities/winstonLogger';

// Mocking keycloak service.
const _syncKeycloakRoles = jest.fn();
jest.mock('@/services/keycloak/keycloakService.ts', () => ({
syncKeycloakRoles: () => _syncKeycloakRoles(),
}));

// cron.schedule has a weird return type expected.
// This makes sure the inner function is still called.
const _scheduleSpy = jest
.spyOn(cron, 'schedule')
.mockImplementation((cronExpression: string, func: () => void) => {
func();
return func as unknown as ScheduledTask;
});

const _loggerSpy = jest.spyOn(logger, 'warn');

describe('UNIT - cronSyncKeycloakRoles', () => {
beforeEach(() => {
jest.clearAllMocks();
});

it('should call the schedule task one time', () => {
cronSyncKeycloakRoles();
expect(_scheduleSpy).toHaveBeenCalledTimes(1);
});

it('should call the syncKeycloakRoles service one time', () => {
cronSyncKeycloakRoles();
expect(_syncKeycloakRoles).toHaveBeenCalledTimes(1);
});

it('should log a warning if the sync fails', () => {
_syncKeycloakRoles.mockImplementationOnce(() => {
throw new Error();
});
cronSyncKeycloakRoles();
expect(_loggerSpy).toHaveBeenCalledTimes(1);
});
});
19 changes: 15 additions & 4 deletions react-app/src/hooks/api/useAgencyApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,16 @@ import { IFetch } from '../useFetch';
export interface Agency {
Id: number;
Name: string;
Description: string | null;
Code: string;
Description?: string;
SortOrder: number;
ParentId: number;
children: Agency[];
ParentId?: number;
IsDisabled: boolean;
Email?: string;
Code: string;
AddressTo?: string;
CCEmail?: string;
SendEmail: boolean;
children?: Agency[];
}

const useAgencyApi = (absoluteFetch: IFetch) => {
Expand All @@ -16,8 +21,14 @@ const useAgencyApi = (absoluteFetch: IFetch) => {
return parsedBody as Agency[];
};

const getAgenciesNotDisabled = async (): Promise<Agency[]> => {
const { parsedBody } = await absoluteFetch.get(`/agencies?isDisabled=false`);
return parsedBody as Agency[];
};

return {
getAgencies,
getAgenciesNotDisabled,
};
};

Expand Down
18 changes: 10 additions & 8 deletions react-app/src/hooks/api/useGroupedAgenciesApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,17 @@ export const useGroupedAgenciesApi = () => {

// Populate groups
agencyData?.forEach((agency) => {
if (agency.ParentId === null) {
parentAgencies.push(agency);
} else {
const parentAgency = agencyData.find((parent) => parent.Id === agency.ParentId);
if (parentAgency) {
if (!groups[parentAgency.Name]) {
groups[parentAgency.Name] = [];
if (!agency.IsDisabled) {
if (agency.ParentId === null) {
parentAgencies.push(agency);
} else {
const parentAgency = agencyData.find((parent) => parent.Id === agency.ParentId);
if (parentAgency) {
if (!groups[parentAgency.Name]) {
groups[parentAgency.Name] = [];
}
groups[parentAgency.Name].push(agency);
}
groups[parentAgency.Name].push(agency);
}
}
});
Expand Down
Loading

0 comments on commit 3c9efe6

Please sign in to comment.