Skip to content

Commit

Permalink
Merge pull request #1520 from hmcts/feature/Restricted-Case-Access
Browse files Browse the repository at this point in the history
EUI-6645 Restricted case access
  • Loading branch information
johnbenjamin-hmcts authored Nov 30, 2023
2 parents 815f901 + db52efc commit 97e7c32
Show file tree
Hide file tree
Showing 13 changed files with 128 additions and 29 deletions.
14 changes: 11 additions & 3 deletions RELEASE-NOTES.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
## RELEASE NOTES
### Version 6.19.15-restricted-case-access
**EUI-8816** Restricted case access feature toggle functionality

### Version 6.19.6-restricted-case-access-v4
**EUI-6645** Restricted case access

### Version 6.19.15
**EXUI-987** Fields in page with PageShowCondition are not available as part of about to submit callback
Expand Down Expand Up @@ -44,14 +49,17 @@
**EUI-8687** Fix validation logic for `WriteJudicialUserField` component
**EUI-8732** Fix `WriteJudicialUserField` error handling to allow user to continue searching if an error occurs on calling `getJudicialUsersSearch` API endpoint

### Version 6.18.0-welsh-release-v6
**EUI-5497** Welsh release part 1

### Version 6.18.2-rc2
**EXUI-343** Fix XUI bug - Text not showing in the "Continue" green button
**EXUI-229** DynamicMultiSelectList Updates Required
**EXUI-313** Fix issue with secure document store

### Version 6.18.0-restricted-case-access-v6
**EUI-6645** Restricted case access

### Version 6.18.0-welsh-release-v6
**EUI-5497** Welsh release part 1

### Version 6.16-hotfix-EUI-8515-case-flags-submission
**EUI-8515** Fix Case Flags and Linked Cases submissions not to depend on presence of "Check your answers" page
**EUI-8550** Fix incorrect behaviour of showEventNotes() function on Case Event submission, introduced in error by EUI-6693
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@hmcts/ccd-case-ui-toolkit",
"version": "6.19.15",
"version": "6.19.15-restricted-case-access",
"engines": {
"yarn": "^3.5.0",
"npm": "^8.10.0"
Expand Down
2 changes: 1 addition & 1 deletion projects/ccd-case-ui-toolkit/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@hmcts/ccd-case-ui-toolkit",
"version": "6.19.15",
"version": "6.19.15-restricted-case-access",
"engines": {
"yarn": "^3.5.0",
"npm": "^8.10.0"
Expand Down
4 changes: 4 additions & 0 deletions projects/ccd-case-ui-toolkit/src/lib/app-config.mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,10 @@ export class AppMockConfig implements AbstractAppConfig {
return '';
}

public getEnableRestrictedCaseAccessConfig(): boolean {
return true;
}

public getEnableCaseFileViewVersion1_1(): boolean {
return true;
}
Expand Down
2 changes: 2 additions & 0 deletions projects/ccd-case-ui-toolkit/src/lib/app.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ export abstract class AbstractAppConfig {
public abstract getCaseFlagsRefdataApiUrl(): string;
public abstract getRDCommonDataApiUrl(): string;
public abstract getCaseDataStoreApiUrl(): string;
public abstract getEnableRestrictedCaseAccessConfig(): boolean;
public abstract getEnableCaseFileViewVersion1_1(): boolean;
}

Expand Down Expand Up @@ -172,5 +173,6 @@ export class CaseEditorConfig {
public case_flags_refdata_api_url: string;
public rd_common_data_api_url: string;
public case_data_store_api_url: string;
public enable_restricted_case_access: boolean;
public enable_case_file_view_version_1_1: boolean;
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { CaseField, CaseTab, CaseView } from '../../../domain';
import { CaseNotifier } from './case.notifier';
import { CasesService } from './cases.service';
import { Observable } from 'rxjs';

describe('setBasicFields', () => {
let caseNotifier: CaseNotifier;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import { NavigationEnd } from '@angular/router';
import { Observable, of, throwError } from 'rxjs';
import { AbstractAppConfig } from '../../../../app.config';
import { CaseView } from '../../../domain';
import { AlertService, DraftService, NavigationNotifierService, NavigationOrigin } from '../../../services';
import {
AlertService,
DraftService,
NavigationNotifierService,
NavigationOrigin
} from '../../../services';
import { CaseResolver } from './case.resolver';
import createSpyObj = jasmine.createSpyObj;

Expand All @@ -25,14 +31,14 @@ describe('CaseResolver', () => {
let navigationNotifierService: NavigationNotifierService;
let sessionStorageService: any;
let route: any;

let router: any;
let mockAppConfig: any;

beforeEach(() => {
router = {
navigate: jasmine.createSpy('navigate'),
events: of( new NavigationEnd(0, '/case', '/home'))
};
};
caseNotifier = createSpyObj('caseNotifier', ['announceCase', 'fetchAndRefresh']);
casesService = createSpyObj('casesService', ['getCaseViewV2']);
draftService = createSpyObj('draftService', ['getDraft']);
Expand All @@ -41,7 +47,8 @@ describe('CaseResolver', () => {
spyOn(navigationNotifierService, 'announceNavigation').and.callThrough();
caseNotifier.fetchAndRefresh.and.returnValue(of(CASE));
sessionStorageService.getItem.and.returnValue(null);
caseResolver = new CaseResolver(caseNotifier, draftService, navigationNotifierService, router, sessionStorageService);
mockAppConfig = createSpyObj<AbstractAppConfig>('AppConfig', ['getEnableRestrictedCaseAccessConfig']);
caseResolver = new CaseResolver(caseNotifier, draftService, navigationNotifierService, router, sessionStorageService, mockAppConfig);

route = {
firstChild: {
Expand Down Expand Up @@ -172,7 +179,7 @@ describe('CaseResolver', () => {
events: of( new NavigationEnd(0, '/trigger/COMPLETE/submit', '/home'))
};

caseResolver = new CaseResolver(caseNotifier, draftService, navigationNotifierService, router, sessionStorageService);
caseResolver = new CaseResolver(caseNotifier, draftService, navigationNotifierService, router, sessionStorageService, mockAppConfig);

caseResolver
.resolve(route)
Expand All @@ -196,7 +203,7 @@ describe('CaseResolver', () => {
events: of( new NavigationEnd(0, '/trigger/COMPLETE/process', '/home'))
};

caseResolver = new CaseResolver(caseNotifier, draftService, navigationNotifierService, router, sessionStorageService);
caseResolver = new CaseResolver(caseNotifier, draftService, navigationNotifierService, router, sessionStorageService, mockAppConfig);

caseResolver
.resolve(route)
Expand All @@ -220,7 +227,7 @@ describe('CaseResolver', () => {
events: of( new NavigationEnd(0, '/trigger/COMPLETE/submit', '/home'))
};

caseResolver = new CaseResolver(caseNotifier, draftService, navigationNotifierService, router, sessionStorageService);
caseResolver = new CaseResolver(caseNotifier, draftService, navigationNotifierService, router, sessionStorageService, mockAppConfig);

caseResolver
.resolve(route)
Expand Down Expand Up @@ -254,7 +261,7 @@ describe('CaseResolver', () => {
};
sessionStorageService.getItem.and.returnValue(JSON.stringify(userInfo));

caseResolver = new CaseResolver(caseNotifier, draftService, navigationNotifierService, router, sessionStorageService);
caseResolver = new CaseResolver(caseNotifier, draftService, navigationNotifierService, router, sessionStorageService, mockAppConfig);

caseResolver
.resolve(route)
Expand Down Expand Up @@ -289,6 +296,49 @@ describe('CaseResolver', () => {
expect(caseNotifier.fetchAndRefresh).not.toHaveBeenCalled();
expect(caseNotifier.cachedCaseView).toBe(CASE);
});

describe('Restricted case access', () => {
beforeEach(() => {
const error = {
status: 403
};
caseNotifier.fetchAndRefresh.and.returnValue(throwError(error));

const userInfo = {
id: '2',
forename: 'G',
surname: 'Testing',
email: '[email protected]',
active: true,
roles: ['caseworker-ia-caseofficer']
};
sessionStorageService.getItem.and.returnValue(JSON.stringify(userInfo));
});

it('should navigate to restricted case access if feature enabled and error code is 403', () => {
mockAppConfig.getEnableRestrictedCaseAccessConfig.and.returnValue(true);
caseResolver = new CaseResolver(caseNotifier, draftService, navigationNotifierService, router, sessionStorageService, mockAppConfig);
caseResolver
.resolve(route)
.then(
data => expect(data).toBeFalsy(),
err => expect(err).toBeTruthy()
);
expect(router.navigate).toHaveBeenCalledWith(['/cases/restricted-case-access/42']);
});

it('should not navigate to restricted case access if feature not enabled and error code is 403', () => {
mockAppConfig.getEnableRestrictedCaseAccessConfig.and.returnValue(false);
caseResolver = new CaseResolver(caseNotifier, draftService, navigationNotifierService, router, sessionStorageService, mockAppConfig);
caseResolver
.resolve(route)
.then(
data => expect(data).toBeFalsy(),
err => expect(err).toBeTruthy()
);
expect(router.navigate).not.toHaveBeenCalledWith(['/cases/restricted-case-access/42']);
});
});
});

describe('resolve()', () => {
Expand All @@ -312,8 +362,8 @@ describe('CaseResolver', () => {
let navigationNotifierService: NavigationNotifierService;
let sessionStorageService: any;
let route: any;

let router: any;
let mockAppConfig: any;

beforeEach(() => {
router = {
Expand All @@ -328,7 +378,9 @@ describe('CaseResolver', () => {
alertService = createSpyObj('alertService', ['success']);
navigationNotifierService = createSpyObj('navigationNotifierService', ['announceNavigation']);
sessionStorageService.getItem.and.returnValue(null);
caseResolver = new CaseResolver(caseNotifier, draftService, navigationNotifierService, router, sessionStorageService);
mockAppConfig = createSpyObj<AbstractAppConfig>('AppConfig', ['getEnableRestrictedCaseAccessConfig']);
mockAppConfig.getEnableRestrictedCaseAccessConfig.and.returnValue(true);
caseResolver = new CaseResolver(caseNotifier, draftService, navigationNotifierService, router, sessionStorageService, mockAppConfig);

route = {
firstChild: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { ActivatedRouteSnapshot, NavigationEnd, Resolve, Router } from '@angular
import { plainToClassFromExist } from 'class-transformer';
import { of, throwError } from 'rxjs';
import { catchError, filter, map } from 'rxjs/operators';

import { AbstractAppConfig } from '../../../../app.config';
import { CaseView, Draft } from '../../../domain';
import { DraftService, NavigationOrigin, SessionStorageService } from '../../../services';
import { NavigationNotifierService } from '../../../services/navigation/navigation-notifier.service';
Expand All @@ -25,7 +25,8 @@ export class CaseResolver implements Resolve<CaseView> {
private draftService: DraftService,
private navigationNotifierService: NavigationNotifierService,
private router: Router,
private sessionStorage: SessionStorageService) {
private sessionStorage: SessionStorageService,
private readonly appConfig: AbstractAppConfig) {
router.events.pipe(filter(event => event instanceof NavigationEnd))
.subscribe((event: NavigationEnd) => {
this.previousUrl = event.url;
Expand Down Expand Up @@ -73,7 +74,7 @@ export class CaseResolver implements Resolve<CaseView> {
} else {
console.info('getAndCacheCaseView - Path B.');
return this.caseNotifier.fetchAndRefresh(cid)
.pipe(catchError(error => this.processErrorInCaseFetch(error)))
.pipe(catchError(error => this.processErrorInCaseFetch(error, cid)))
.toPromise();
}
}
Expand All @@ -88,24 +89,28 @@ export class CaseResolver implements Resolve<CaseView> {
this.caseNotifier.announceCase(this.caseNotifier.cachedCaseView);
return this.caseNotifier.cachedCaseView;
}),
catchError(error => this.processErrorInCaseFetch(error))
catchError(error => this.processErrorInCaseFetch(error, cid))
).toPromise();
}

private processErrorInCaseFetch(error: any) {
private processErrorInCaseFetch(error: any, caseReference: string) {
console.error('!!! processErrorInCaseFetch !!!');
console.error(error);
// TODO Should be logged to remote logging infrastructure
if (error.status === 400) {
this.router.navigate(['/search/noresults']);
return of(null);
}
console.error(error);
if (CaseResolver.EVENT_REGEX.test(this.previousUrl) && error.status === 404) {
this.router.navigate(['/list/case']);
return of(null);
}
if (error.status !== 401 && error.status !== 403) {
// Error 403 and enable-restricted-case-access Launch Darkly flag is enabled, navigate to restricted case access page
if (error.status === 403 && this.appConfig.getEnableRestrictedCaseAccessConfig()) {
this.router.navigate([`/cases/restricted-case-access/${caseReference}`]);
return of(null);
}
if (error.status !== 401) {
this.router.navigate(['/error']);
}
this.goToDefaultPage();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ const APP_CONFIG: AbstractAppConfig = {
getRDCommonDataApiUrl: () => 'rd_common_data_api_url',
getCaseDataStoreApiUrl: () => 'case_data_store_api_url',
getWAServiceConfig: () => 'waServiceConfig',
getEnableRestrictedCaseAccessConfig: () => true,
getEnableCaseFileViewVersion1_1: () => true
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,16 @@ describe('HttpError', () => {
path: '/caseworkers/0/jurisdictions/TEST/case-types/TestAddressBookCase/cases'
};

const ERROR_FORBIDDEN = {
error: {},
message: 'Http failure response for http://localhost:3000/data/internal/cases/1234123412341234: 403 Forbidden',
name: 'HttpErrorResponse',
ok: false,
status: 403,
statusText: 'Forbidden',
url: 'http://localhost:3000/data/internal/cases/1234123412341234'
};

it('should return default error when given null', () => {
const error = HttpError.from(null);

Expand Down Expand Up @@ -64,6 +74,17 @@ describe('HttpError', () => {
expect(error).toEqual(expectedError);
});

it('should return the error properties for forbidden error', () => {
const error = HttpError.from(new HttpErrorResponse(ERROR_FORBIDDEN));

const expectedError = new HttpError();
expectedError.error = ERROR_FORBIDDEN.statusText;
expectedError.status = ERROR_FORBIDDEN.status;
expectedError.message = ERROR_FORBIDDEN.message;

expect(error).toEqual(expectedError);
});

it('should ignore additional properties of object', () => {
const error = HttpError.from(new HttpErrorResponse({ error: { unknown: 'xxx' } }));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export class HttpError {
public details?: any;
public callbackErrors?: any;
public callbackWarnings?: any;

public static from(response: HttpErrorResponse): HttpError {
const error = new HttpError();

Expand All @@ -36,6 +37,14 @@ export class HttpError {
});
}

// Error object in HttpErrorResponse will be empty for 403 errors
// Set the error properties of HttpError accordingly
if (response?.status === 403) {
error.error = response.statusText;
error.status = response.status;
error.message = response.message;
}

return error;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -149,9 +149,9 @@ describe('HttpErrorService', () => {
expect(authService.signIn).toHaveBeenCalled();
});

it('should trigger sign-in when IDAM returns HTTP-403 as response', () => {
it('should not trigger sign-in when IDAM returns HTTP-403 as response', () => {
errorService.handle(HTTP_403_RESPONSE);
expect(authService.signIn).toHaveBeenCalled();
expect(authService.signIn).not.toHaveBeenCalled();
});

it('should empty error when removed', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,7 @@ export class HttpErrorService {
}
let httpError = new HttpError();
if (error instanceof HttpErrorResponse) {
if (error.headers
&& error.headers.get(HttpErrorService.CONTENT_TYPE)
&& error.headers.get(HttpErrorService.CONTENT_TYPE).indexOf(HttpErrorService.JSON) !== -1) {
if (error.headers?.get(HttpErrorService.CONTENT_TYPE).indexOf(HttpErrorService.JSON) !== -1) {
try {
httpError = HttpError.from(error);
} catch (e) {
Expand Down Expand Up @@ -57,7 +55,7 @@ export class HttpErrorService {
console.error('Handling error in http error service.');
console.error(error);
const httpError: HttpError = HttpErrorService.convertToHttpError(error);
if (redirectIfNotAuthorised && (httpError.status === 401 || httpError.status === 403)) {
if (redirectIfNotAuthorised && httpError.status === 401) {
this.authService.signIn();
}
return throwError(httpError);
Expand Down

0 comments on commit 97e7c32

Please sign in to comment.