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

Bug/e UI 2028 tasks not being unassigned #3794

Merged
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
3e9861b
Add the new pre-sign-in caseworker calls
connorpgpmcelroy Jul 10, 2024
572ffa5
Fix lint issues
connorpgpmcelroy Jul 10, 2024
09f30f8
Merge branch 'master' into bug/EUI-2028-tasks-not-being-unassigned
connorpgpmcelroy Jul 10, 2024
716fb73
Create caseWorkerUserDataCacheService.spec.ts
connorpgpmcelroy Jul 10, 2024
eda874e
Merge branch 'bug/EUI-2028-tasks-not-being-unassigned' of https://git…
connorpgpmcelroy Jul 10, 2024
e2b431e
Fix linting issues
connorpgpmcelroy Jul 10, 2024
c5be833
Merge branch 'master' into bug/EUI-2028-tasks-not-being-unassigned
connorpgpmcelroy Jul 11, 2024
35327e0
Attempt to fix functional tests
connorpgpmcelroy Jul 11, 2024
174da5e
Merge branch 'master' into bug/EUI-2028-tasks-not-being-unassigned
connorpgpmcelroy Jul 11, 2024
55d2c2f
Merge branch 'master' into bug/EUI-2028-tasks-not-being-unassigned
connorpgpmcelroy Jul 11, 2024
bf36715
Fix functional tests
connorpgpmcelroy Jul 11, 2024
b0a7f6e
Merge branch 'bug/EUI-2028-tasks-not-being-unassigned' of https://git…
connorpgpmcelroy Jul 11, 2024
052cf31
Merge branch 'master' into bug/EUI-2028-tasks-not-being-unassigned
connorpgpmcelroy Jul 12, 2024
ef094a1
Merge branch 'master' into bug/EUI-2028-tasks-not-being-unassigned
connorpgpmcelroy Jul 15, 2024
dc2d84b
Update caseWorkerUserDataCacheService.ts
connorpgpmcelroy Jul 17, 2024
9d02d74
Update caseWorkerUserDataCacheService.ts
connorpgpmcelroy Jul 17, 2024
fbb9746
Merge branch 'master' into bug/EUI-2028-tasks-not-being-unassigned
connorpgpmcelroy Jul 17, 2024
75e44c0
Merge branch 'master' into bug/EUI-2028-tasks-not-being-unassigned
andywilkinshmcts Jul 18, 2024
ff9848d
Merge branch 'master' into bug/EUI-2028-tasks-not-being-unassigned
RiteshHMCTS Jul 22, 2024
3f44244
Merge branch 'mc-srt-release-22-07-24' into bug/EUI-2028-tasks-not-be…
RiteshHMCTS Jul 22, 2024
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
3 changes: 3 additions & 0 deletions api/application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { initProxy } from './proxy.config';
import routes from './routes';
import workAllocationRouter from './workAllocation/routes';
import { idamCheck } from './idamCheck';
import { getNewUsersByServiceName } from './workAllocation';

export const app = express();

Expand Down Expand Up @@ -129,3 +130,5 @@ const logger: JUILogger = log4jui.getLogger('Application');
logger.info(`Started up using ${getConfigValue(PROTOCOL)}`);

new Promise(idamCheck).then(() => 'IDAM is up and running');
// EUI-2028 - Get the caseworkers, ideally prior to a user logging into application
new Promise(getNewUsersByServiceName).then(() => 'Caseworkers have been loaded');
16 changes: 16 additions & 0 deletions api/workAllocation/caseWorkerService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ export async function handleUsersGet(path: string, req: EnhancedRequest): Promis
return response.data;
}

export async function handleNewUsersGet(path: string, headers: any): Promise<any> {
logger.info('getting users for', path);
const response: AxiosResponse = await http.get(path, { headers });
return response.data;
}

export async function handleCaseWorkerGetAll(path: string, req: EnhancedRequest): Promise<any> {
logger.info('getting all caseworkers for', path);
const headers = setHeaders(req);
Expand Down Expand Up @@ -69,6 +75,16 @@ export async function handlePostRoleAssignments(path: string, payload: any, req:
return response;
}

export async function handlePostRoleAssignmentsWithNewUsers(path: string, payload: any, headers: any): Promise<any> {
// sort
// direction
const response: AxiosResponse = await http.post(path, payload, { headers });
if (response.data.roleAssignmentResponse.length >= MAX_RECORDS) {
logger.warn('Case workers now returning MAX_RECORDS', response.data.roleAssignmentResponse.length);
andywilkinshmcts marked this conversation as resolved.
Show resolved Hide resolved
}
return response;
}

export async function handleCaseWorkersForServicesPost(path: string, payloads: CaseworkerPayload [], req: EnhancedRequest):
Promise<ServiceCaseworkerData[]> {
const headers = setHeaders(req);
Expand Down
86 changes: 86 additions & 0 deletions api/workAllocation/caseWorkerUserDataCacheService.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import * as chai from 'chai';
import { expect } from 'chai';
import 'mocha';
import * as sinon from 'sinon';
import * as sinonChai from 'sinon-chai';
import { mockRes } from 'sinon-express-mock';
import { http } from '../lib/http';
import { fetchNewUserData, fetchRoleAssignmentsForNewUsers } from './caseWorkerUserDataCacheService';
import { StaffUserDetails } from './interfaces/staffUserDetails';

chai.use(sinonChai);

describe('Caseworker Cache Service', () => {
let sandbox: sinon.SinonSandbox;

beforeEach(() => {
sandbox = sinon.createSandbox();
});

afterEach(() => {
sandbox.restore();
});

describe('fetchNewUserData', () => {
it('should make a get request', async () => {
const mockStaffDetails = [{}, {}, {}];
const res = mockRes({ status: 200, data: mockStaffDetails });
sandbox.stub(http, 'get').resolves(res);
const data = await fetchNewUserData();
expect(data).to.equal(mockStaffDetails);
});
});

describe('fetchRoleAssignmentsForNewUsers', () => {
it('should make a post request', async () => {
const mockStaffUserDetails: StaffUserDetails[] = [
{
ccd_service_name: 'EMPLOYMENT',
staff_profile: {
id: '0bdd43aa-527b-40ac-9d68-d72bd45054f4',
first_name: 'Latest',
last_name: 'New',
region_id: '1',
user_type: 'CTSC',
suspended: 'false',
case_allocator: 'Y',
task_supervisor: 'Y',
staff_admin: 'N',
email_id: '[email protected]',
region: 'London',
base_location: [{
location_id: '637145',
location: 'Wrexham',
is_primary: true
}],
user_type_id: '1',
role: [],
skills: [],
work_area: [],
idam_roles: '',
created_time: new Date(),
last_updated_time: new Date()
}
}];
const mockRoleAssignments = [{
id: '123',
attributes: {},
roleCategory: 'ADMIN',
actorId: '0bdd43aa-527b-40ac-9d68-d72bd45054f4'
}];
const finalCaseworkers = [{
email: '[email protected]',
firstName: 'Latest',
idamId: '0bdd43aa-527b-40ac-9d68-d72bd45054f4',
lastName: 'New',
location: { id: '637145', locationName: 'Wrexham', services: undefined },
roleCategory: 'ADMIN',
service: 'EMPLOYMENT'
}];
const res = mockRes({ status: 200, data: { roleAssignmentResponse: mockRoleAssignments } });
sandbox.stub(http, 'post').resolves(res);
const data = await fetchRoleAssignmentsForNewUsers(mockStaffUserDetails);
expect(data).to.deep.equal(finalCaseworkers);
});
});
});
137 changes: 115 additions & 22 deletions api/workAllocation/caseWorkerUserDataCacheService.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import { NextFunction } from 'express';
import { authenticator } from 'otplib';

import { baseCaseWorkerRefUrl, baseRoleAssignmentUrl } from './index';
import { ServiceUserDetailList, StaffUserDetails } from './interfaces/staffUserDetails';
import { StaffUserDetails } from './interfaces/staffUserDetails';
import { getConfigValue } from '../configuration';
import { IDAM_SECRET, MICROSERVICE, S2S_SECRET, SERVICES_IDAM_API_URL, SERVICES_IDAM_CLIENT_ID, SERVICE_S2S_PATH, STAFF_SUPPORTED_JURISDICTIONS, SYSTEM_USER_NAME, SYSTEM_USER_PASSWORD } from '../configuration/references';
import { http } from '../lib/http';
import { EnhancedRequest } from '../lib/models';
import { getStaffSupportedJurisdictionsList } from '../staffSupportedJurisdictions';
import { mapUsersToCaseworkers, prepareGetUsersUrl, prepareRoleApiRequest, prepareRoleApiUrl } from './util';
import { handlePostRoleAssignments, handleUsersGet } from './caseWorkerService';
import { NextFunction } from 'express';
import { handleNewUsersGet, handlePostRoleAssignments, handlePostRoleAssignmentsWithNewUsers, handleUsersGet } from './caseWorkerService';
import { Caseworker } from './interfaces/common';
import { FullUserDetailCache } from './fullUserDetailCache';

Expand All @@ -13,37 +18,66 @@ const TTL = 600;

let timestamp: Date;
let refreshRoles: boolean;
let initialServiceAuthToken: string;
let initialAuthToken: string;

let cachedUsers: ServiceUserDetailList[];
let cachedUsers: StaffUserDetails[];
let cachedUsersWithRoles: Caseworker[];

export async function fetchUserData(req: EnhancedRequest, next: NextFunction): Promise<StaffUserDetails[]> {
try {
if (hasTTLExpired() || !cachedUsers) {
if (hasTTLExpired() || (!cachedUsers || cachedUsers.length === 0)) {
// hasTTLExpired to determine whether roles require refreshing
// cachedUsers to ensure rerun if user restarts request early
refreshRoles = true;
const jurisdictions = getStaffSupportedJurisdictionsList();
cachedUsers = [];
for (const jurisdiction of jurisdictions) {
const getUsersPath: string = prepareGetUsersUrl(baseCaseWorkerRefUrl, jurisdiction);
const userResponse = await handleUsersGet(getUsersPath, req);
// TODO: Response will be cached eventually via API so caching below should be removed eventually
cachedUsers.push({ service: jurisdiction, staffUserList: userResponse });
}
const jurisdictions = getConfigValue(STAFF_SUPPORTED_JURISDICTIONS);
const getUsersPath: string = prepareGetUsersUrl(baseCaseWorkerRefUrl, jurisdictions);
const userResponse = await handleUsersGet(getUsersPath, req);
// TODO: Response will be cached eventually via API so caching below should be removed eventually
cachedUsers = userResponse;
} else {
refreshRoles = false;
}
return getCachedDataForServices();
return cachedUsers;
} catch (error) {
if (cachedUsers) {
return getCachedDataForServices();
return cachedUsers;
}
next(error);
}
}

export async function fetchRoleAssignments(cachedUserData: StaffUserDetails[], req: EnhancedRequest, next: NextFunction): Promise<any> {
// Note: May not be needed once API caching is in effect
export async function fetchNewUserData(): Promise<StaffUserDetails[]> {
try {
// first ensure that there is no immediate re-caching
timestamp = new Date();
refreshRoles = true;
await getAuthTokens();
// add the tokens to the request headers
const caseworkerHeaders = {
andywilkinshmcts marked this conversation as resolved.
Show resolved Hide resolved
'content-type': 'application/json',
'serviceAuthorization': `Bearer ${initialServiceAuthToken}`,
'authorization': `Bearer ${initialAuthToken}`
};
const jurisdictions = getConfigValue(STAFF_SUPPORTED_JURISDICTIONS);
cachedUsers = [];
const getUsersPath: string = prepareGetUsersUrl(baseCaseWorkerRefUrl, jurisdictions);
const userResponse = await handleNewUsersGet(getUsersPath, caseworkerHeaders);
cachedUsers = userResponse;
return cachedUsers;
} catch (error) {
if (cachedUsers) {
return cachedUsers;
}
// in case of error, reset the cache on application start up
timestamp = null;
refreshRoles = false;
}
}

export async function fetchRoleAssignments(cachedUserData: StaffUserDetails[], req: EnhancedRequest, next: NextFunction): Promise<Caseworker[]> {
// note: this has been done to cache role categories
// it is separate from the above as above caching will be done by backend
const fullUserDetailCache = FullUserDetailCache.getInstance();
Expand All @@ -68,6 +102,63 @@ export async function fetchRoleAssignments(cachedUserData: StaffUserDetails[], r
}
}

export async function fetchRoleAssignmentsForNewUsers(cachedUserData: StaffUserDetails[]): Promise<Caseworker[]> {
// note: this has been done to cache role categories
// it is separate from the above as above caching will be done by backend
const fullUserDetailCache = FullUserDetailCache.getInstance();
try {
if (refreshRoles && !cachedUsersWithRoles) {
// refreshRoles to determine whether roles require refreshing
// cachedUsersWithRoles to ensure rerun if user restarts request early
const roleApiPath: string = prepareRoleApiUrl(baseRoleAssignmentUrl);
const jurisdictions = getStaffSupportedJurisdictionsList();
const payload = prepareRoleApiRequest(jurisdictions);
const roleAssignmentHeaders = {
'content-type': 'application/json',
'serviceAuthorization': `Bearer ${initialServiceAuthToken}`,
'authorization': `Bearer ${initialAuthToken}`,
pageNumber: 0,
size: 10000
};
const { data } = await handlePostRoleAssignmentsWithNewUsers(roleApiPath, payload, roleAssignmentHeaders);
const roleAssignments = data.roleAssignmentResponse;
cachedUsersWithRoles = mapUsersToCaseworkers(cachedUserData, roleAssignments);
fullUserDetailCache.setUserDetails(cachedUsersWithRoles);
}
return fullUserDetailCache.getAllUserDetails();
} catch (error) {
if (fullUserDetailCache.getAllUserDetails()) {
return fullUserDetailCache.getAllUserDetails();
}
}
}

export async function getAuthTokens(): Promise<void> {
try {
// get config values for getting auth tokens
const microservice = getConfigValue(MICROSERVICE);
const s2sEndpointUrl = `${getConfigValue(SERVICE_S2S_PATH)}/lease`;
const s2sSecret = getConfigValue(S2S_SECRET).trim();
const oneTimePassword = authenticator.generate(s2sSecret);
const idamPath = getConfigValue(SERVICES_IDAM_API_URL);
const authURL = `${idamPath}/o/token`;
const axiosConfig = {
headers: { 'content-type': 'application/x-www-form-urlencoded' }
};
const authBody = getRequestBody();
// get auth token to use for pre-sign-in API calls
const serviceAuthResponse = await http.post(`${s2sEndpointUrl}`, {
microservice,
oneTimePassword
});
initialServiceAuthToken = serviceAuthResponse.data;
const authResponse = await http.post(authURL, authBody, axiosConfig);
initialAuthToken = authResponse.data.access_token;
} catch (error) {
console.log('Cannot get auth tokens');
}
}

export function hasTTLExpired(): boolean {
if (!timestamp) {
// started caching
Expand All @@ -86,10 +177,12 @@ export function timestampExists(): boolean {
return !!timestamp;
}

export function getCachedDataForServices(): StaffUserDetails[] {
let staffDetailsList = [];
for (const cachedServiceData of cachedUsers) {
staffDetailsList = [...staffDetailsList, ...cachedServiceData.staffUserList];
}
return staffDetailsList;
}
// get the request information from config - similarly in node-lib
export const getRequestBody = (): string => {
const userName = getConfigValue(SYSTEM_USER_NAME);
const userPassword = encodeURIComponent(getConfigValue(SYSTEM_USER_PASSWORD));
const scope = 'openid profile roles manage-user create-user search-user';
const clientSecret = getConfigValue(IDAM_SECRET);
const idamClient = getConfigValue(SERVICES_IDAM_CLIENT_ID);
return `grant_type=password&password=${userPassword}&username=${userName}&scope=${scope}&client_id=${idamClient}&client_secret=${clientSecret}`;
};
20 changes: 17 additions & 3 deletions api/workAllocation/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
SERVICES_ROLE_ASSIGNMENT_API_PATH,
SERVICES_WORK_ALLOCATION_TASK_API_PATH
} from '../configuration/references';
import { trackTrace } from '../lib/appInsights';
import * as log4jui from '../lib/log4jui';
import { EnhancedRequest, JUILogger } from '../lib/models';
import { refreshRoleAssignmentForUser } from '../user';
Expand All @@ -21,7 +22,9 @@ import {
handleCaseWorkerForService,
handlePostSearch
} from './caseWorkerService';
import { fetchNewUserData, fetchRoleAssignments, fetchRoleAssignmentsForNewUsers, fetchUserData, timestampExists } from './caseWorkerUserDataCacheService';
import { ViewType } from './constants/actions';
import { FullUserDetailCache } from './fullUserDetailCache';
import { CaseList } from './interfaces/case';
import { PaginationParameter } from './interfaces/caseSearchParameter';
import { CaseDataType } from './interfaces/common';
Expand Down Expand Up @@ -56,9 +59,6 @@ import {
searchCasesById,
searchUsers
} from './util';
import { trackTrace } from '../lib/appInsights';
import { fetchRoleAssignments, fetchUserData, timestampExists } from './caseWorkerUserDataCacheService';
import { FullUserDetailCache } from './fullUserDetailCache';

caseServiceMock.init();
roleServiceMock.init();
Expand Down Expand Up @@ -538,3 +538,17 @@ export async function getUsersByServiceName(req: EnhancedRequest, res: Response,
next(error);
}
}

/**
* getNewUsersByServiceName
*/
export const getNewUsersByServiceName = async (resolve, reject) => {
try {
const cachedUserData = await fetchNewUserData();
await fetchRoleAssignmentsForNewUsers(cachedUserData);
} catch (error) {
console.log('Error getting caseworkers');
reject(error);
}
resolve();
};
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@
"ngx-markdown": "^17.2.1",
"ngx-pagination": "^6.0.3",
"node-http-proxy-json": "^0.1.9",
"otplib": "^12.0.1",
"p-iteration": "^1.1.7",
"pegjs": "^0.10.0",
"portfinder": "^1.0.28",
Expand Down
4 changes: 2 additions & 2 deletions test_codecept/backendMock/services/rdCaseworker/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ class RDCaseworkerService {
}


getUsersByService(service) {
const filteredCaseworkers = this.caseworkers.filter(user => service === user.ccd_service_name);
getUsersByService(services) {
const filteredCaseworkers = this.caseworkers.filter(user => services.split(',').includes(user.ccd_service_name));
return filteredCaseworkers;
}

Expand Down
1 change: 1 addition & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -21887,6 +21887,7 @@ __metadata:
node-http-proxy-json: ^0.1.9
nodemon: ^2.0.19
npm-run-all: ^4.1.5
otplib: ^12.0.1
p-iteration: ^1.1.7
pa11y: ^5.3.0
pegjs: ^0.10.0
Expand Down