Skip to content

Commit

Permalink
Merge pull request #26 from bcgov/feat/reflect-url-params
Browse files Browse the repository at this point in the history
Feat/reflect url params
  • Loading branch information
hannah-macdonald1 authored Nov 7, 2024
2 parents 6486d26 + a45875d commit 3720ea5
Show file tree
Hide file tree
Showing 20 changed files with 105 additions and 59 deletions.
3 changes: 2 additions & 1 deletion src/common/constants/parameter-constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@ const CONTENT_TYPE = 'application/json';
const PAGINATION = 'N';

const idRegex = /[0-9\-A-Za-z]+/;
const idName = 'rowId';

export { VIEW_MODE, CHILD_LINKS, CONTENT_TYPE, PAGINATION, idRegex };
export { VIEW_MODE, CHILD_LINKS, CONTENT_TYPE, PAGINATION, idRegex, idName };
2 changes: 2 additions & 0 deletions src/common/constants/upstream-constants.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
const baseUrlEnvVarName = 'UPSTREAM_BASE_URL';
const supportNetworkEndpointEnvVarName = 'SUPPORT_NETWORK_ENDPOINT';
const inPersonVisitsEndpointEnvVarName = 'IN_PERSON_VISITS_ENDPOINT';
const idirUsernameHeaderField = 'x-idir-username';

export {
baseUrlEnvVarName,
supportNetworkEndpointEnvVarName,
inPersonVisitsEndpointEnvVarName,
idirUsernameHeaderField,
};
6 changes: 6 additions & 0 deletions src/common/guards/auth/auth.guard.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,9 @@ describe('AuthGuard', () => {
switchToHttp: () => ({
getRequest: () => getMockReq(),
}),
getClass: () => {
return TestController;
},
};
const isAuthed = await guard.canActivate(execContext as ExecutionContext);
expect(authSpy).toHaveBeenCalledTimes(0);
Expand Down Expand Up @@ -123,6 +126,9 @@ describe('AuthGuard', () => {
switchToHttp: () => ({
getRequest: () => getMockReq(),
}),
getClass: () => {
return TestController;
},
};
const isAuthed = await guard.canActivate(execContext);
expect(authSpy).toHaveBeenCalledTimes(1);
Expand Down
4 changes: 3 additions & 1 deletion src/common/guards/auth/auth.guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ export class AuthGuard implements CanActivate {
return true;
}
const request = context.switchToHttp().getRequest();
return await this.authService.getRecordAndValidate(request);
const controllerPath =
Reflect.getMetadata('path', context.getClass()) || '';
return await this.authService.getRecordAndValidate(request, controllerPath);
}
}
51 changes: 30 additions & 21 deletions src/common/guards/auth/auth.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import { RecordType } from '../../../common/constants/enumerations';
import { EnumTypeError } from '../../../common/errors/errors';
import { UtilitiesService } from '../../../helpers/utilities/utilities.service';
import { TokenRefresherService } from '../../../external-api/token-refresher/token-refresher.service';
import { idirUsernameHeaderField } from '../../../common/constants/upstream-constants';
import { idName } from '../../../common/constants/parameter-constants';

describe('AuthService', () => {
let service: AuthService;
Expand All @@ -25,10 +27,7 @@ describe('AuthService', () => {

const validId = 'id1234';
const validRecordType = RecordType.Case;
const validPath = `/${validRecordType}/${validId}/testendpoint`;
const notinEnumPath = `/fjofijp/${validId}/testendpoint`;
const noIdPath = `/${validRecordType}`;
const incorrectFormatPath = 'abcdefg';
const notinEnumPath = `fjofijp`;
const testIdir = 'IDIRTEST';

beforeEach(async () => {
Expand Down Expand Up @@ -93,65 +92,75 @@ describe('AuthService', () => {
} as AxiosResponse<any, any>),
);
const mockRequest = getMockReq({
path: validPath,
header: jest.fn((key: string): string => {
const headerVal: { [key: string]: string } = {
'x-idir-username': testIdir,
[idirUsernameHeaderField]: testIdir,
};
return headerVal[key];
}),
params: { [idName]: 'id' },
});
const isAuthed = await service.getRecordAndValidate(mockRequest);
const isAuthed = await service.getRecordAndValidate(
mockRequest,
validRecordType,
);
expect(spy).toHaveBeenCalledTimes(1);
expect(cacheSpy).toHaveBeenCalledTimes(2);
expect(isAuthed).toBe(true);
});

it.each([
[{}, undefined, 0],
[{ 'x-idir-username': testIdir }, null, 1],
[{ [idirUsernameHeaderField]: testIdir }, null, 1],
])(
'should return false with invalid record',
async (headers, cacheReturn, cacheSpyCallTimes) => {
const cacheSpy = jest
.spyOn(cache, 'get')
.mockResolvedValueOnce(cacheReturn);
const mockRequest = getMockReq({
path: validPath,
header: jest.fn((key: string): string => {
const headerVal: { [key: string]: string } = headers;
return headerVal[key];
}),
params: { [idName]: 'id' },
});
const isAuthed = await service.getRecordAndValidate(mockRequest);
const isAuthed = await service.getRecordAndValidate(
mockRequest,
validRecordType,
);
expect(cacheSpy).toHaveBeenCalledTimes(cacheSpyCallTimes);
expect(isAuthed).toBe(false);
},
);
});

describe('grabRecordInfoFromPath tests', () => {
describe('grabRecordInfo tests', () => {
it('returns an array of [id, type] when the correct url format is passed', () => {
const [id, recordType] = service.grabRecordInfoFromPath(validPath);
const mockRequest = getMockReq({
params: { [idName]: validId },
});
const [id, recordType] = service.grabRecordInfo(
mockRequest,
validRecordType,
);
expect(id).toEqual(validId);
expect(recordType).toEqual(validRecordType);
});

it(`throws an error when the enum doesn't match the record type`, () => {
const mockRequest = getMockReq({
params: { [idName]: validId },
});
expect(() => {
service.grabRecordInfoFromPath(notinEnumPath);
service.grabRecordInfo(mockRequest, notinEnumPath);
}).toThrow(EnumTypeError);
});

it(`throws an error when the url doesn't match the correct format`, () => {
expect(() => {
service.grabRecordInfoFromPath(incorrectFormatPath);
}).toThrow(Error);
});

it(`throws an error when the url doen't have an id`, () => {
it(`throws an error when the parameter doesn't exist for id`, () => {
const mockRequest = getMockReq({ params: {} });
expect(() => {
service.grabRecordInfoFromPath(noIdPath);
service.grabRecordInfo(mockRequest, validRecordType);
}).toThrow(Error);
});
});
Expand Down
29 changes: 18 additions & 11 deletions src/common/guards/auth/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,16 @@ import { UtilitiesService } from '../../../helpers/utilities/utilities.service';
import {
CHILD_LINKS,
CONTENT_TYPE,
idName,
VIEW_MODE,
} from '../../../common/constants/parameter-constants';
import { firstValueFrom } from 'rxjs';
import { AxiosError } from 'axios';
import { TokenRefresherService } from '../../../external-api/token-refresher/token-refresher.service';
import { baseUrlEnvVarName } from '../../../common/constants/upstream-constants';
import {
baseUrlEnvVarName,
idirUsernameHeaderField,
} from '../../../common/constants/upstream-constants';

@Injectable()
export class AuthService {
Expand All @@ -36,11 +40,14 @@ export class AuthService {
this.buildNumber = this.configService.get<string>('buildInfo.buildNumber');
}

async getRecordAndValidate(req: Request): Promise<boolean> {
async getRecordAndValidate(
req: Request,
controllerPath: string,
): Promise<boolean> {
let idir: string, id: string, recordType: RecordType;
try {
idir = req.header('x-idir-username').trim();
[id, recordType] = this.grabRecordInfoFromPath(req.path);
idir = req.header(idirUsernameHeaderField).trim();
[id, recordType] = this.grabRecordInfo(req, controllerPath);
} catch (error: any) {
this.logger.error({ error });
return false;
Expand All @@ -59,15 +66,15 @@ export class AuthService {
return true;
}

grabRecordInfoFromPath(path: string): [string, RecordType] {
const pathParts = path.trim().slice(1).split('/', 2); // slice removes the leading / in the url
if (!this.utilitiesService.enumTypeGuard(RecordType, pathParts[0].trim())) {
throw new EnumTypeError(pathParts[0]);
grabRecordInfo(req: Request, controllerPath: string): [string, RecordType] {
if (!this.utilitiesService.enumTypeGuard(RecordType, controllerPath)) {
throw new EnumTypeError(controllerPath);
}
if (pathParts.length < 2) {
throw new Error(`Id not found in path: '${path}'`);
const rowId = req.params[idName];
if (rowId === undefined) {
throw new Error(`Id not found in path`);
}
return [pathParts[1].trim(), pathParts[0].trim() as RecordType];
return [rowId, controllerPath as RecordType];
}

async getAssignedIdirUpstream(
Expand Down
5 changes: 3 additions & 2 deletions src/controllers/cases/cases.controller.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
InPersonVisitsEntity,
InPersonVisitsSingleResponseCaseExample,
} from '../../entities/in-person-visits.entity';
import { idName } from '../../common/constants/parameter-constants';

describe('CasesController', () => {
let controller: CasesController;
Expand Down Expand Up @@ -55,7 +56,7 @@ describe('CasesController', () => {
it.each([
[
SupportNetworkSingleResponseCaseExample,
{ id: 'test' } as IdPathParams,
{ [idName]: 'test' } as IdPathParams,
{ since: '2020-02-02' } as SinceQueryParams,
],
])(
Expand Down Expand Up @@ -83,7 +84,7 @@ describe('CasesController', () => {
it.each([
[
InPersonVisitsSingleResponseCaseExample,
{ id: 'test' } as IdPathParams,
{ [idName]: 'test' } as IdPathParams,
{ since: '2020-02-02' } as SinceQueryParams,
],
])(
Expand Down
9 changes: 6 additions & 3 deletions src/controllers/cases/cases.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,10 @@ import {
import { IdPathParams } from '../../dto/id-path-params.dto';
import { SinceQueryParams } from '../../dto/since-query-params.dto';
import { ApiNotFoundEntity } from '../../entities/api-not-found.entity';
import { CONTENT_TYPE } from '../../common/constants/parameter-constants';
import {
CONTENT_TYPE,
idName,
} from '../../common/constants/parameter-constants';
import { ApiInternalServerErrorEntity } from '../../entities/api-internal-server-error.entity';
import { AuthGuard } from '../../common/guards/auth/auth.guard';
import {
Expand All @@ -45,7 +48,7 @@ export class CasesController {
constructor(private readonly casesService: CasesService) {}

@UseInterceptors(ClassSerializerInterceptor)
@Get(':id/support-network')
@Get(`:${idName}/support-network`)
@ApiOperation({
description:
'Find all Support Network entries related to a given Case entity by Case id.',
Expand Down Expand Up @@ -98,7 +101,7 @@ export class CasesController {
}

@UseInterceptors(ClassSerializerInterceptor)
@Get(':id/visits')
@Get(`:${idName}/visits`)
@ApiOperation({
description:
'Find all In Person Child / Youth Visits related to a given Case entity by Case id.',
Expand Down
5 changes: 3 additions & 2 deletions src/controllers/cases/cases.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
InPersonVisitsEntity,
InPersonVisitsSingleResponseCaseExample,
} from '../../entities/in-person-visits.entity';
import { idName } from '../../common/constants/parameter-constants';

describe('CasesService', () => {
let service: CasesService;
Expand Down Expand Up @@ -62,7 +63,7 @@ describe('CasesService', () => {
it.each([
[
SupportNetworkSingleResponseCaseExample,
{ id: 'test' } as IdPathParams,
{ [idName]: 'test' } as IdPathParams,
{ since: '2024-12-01' } as SinceQueryParams,
],
])(
Expand Down Expand Up @@ -94,7 +95,7 @@ describe('CasesService', () => {
it.each([
[
InPersonVisitsSingleResponseCaseExample,
{ id: 'test' } as IdPathParams,
{ [idName]: 'test' } as IdPathParams,
{ since: '2024-12-01' } as SinceQueryParams,
],
])(
Expand Down
3 changes: 2 additions & 1 deletion src/controllers/incidents/incidents.controller.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { SupportNetworkService } from '../../helpers/support-network/support-net
import { TokenRefresherService } from '../../external-api/token-refresher/token-refresher.service';
import { UtilitiesService } from '../../helpers/utilities/utilities.service';
import { RequestPreparerService } from '../../external-api/request-preparer/request-preparer.service';
import { idName } from '../../common/constants/parameter-constants';

describe('IncidentsController', () => {
let controller: IncidentsController;
Expand Down Expand Up @@ -49,7 +50,7 @@ describe('IncidentsController', () => {
it.each([
[
SupportNetworkSingleResponseIncidentExample,
{ id: 'test' } as IdPathParams,
{ [idName]: 'test' } as IdPathParams,
{ since: '2020-02-02' } as SinceQueryParams,
],
])(
Expand Down
7 changes: 5 additions & 2 deletions src/controllers/incidents/incidents.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,10 @@ import {
import { IdPathParams } from '../../dto/id-path-params.dto';
import { SinceQueryParams } from '../../dto/since-query-params.dto';
import { ApiNotFoundEntity } from '../../entities/api-not-found.entity';
import { CONTENT_TYPE } from '../../common/constants/parameter-constants';
import {
CONTENT_TYPE,
idName,
} from '../../common/constants/parameter-constants';
import { ApiInternalServerErrorEntity } from '../../entities/api-internal-server-error.entity';
import { AuthGuard } from '../../common/guards/auth/auth.guard';

Expand All @@ -40,7 +43,7 @@ export class IncidentsController {
constructor(private readonly incidentsService: IncidentsService) {}

@UseInterceptors(ClassSerializerInterceptor)
@Get(':id/support-network')
@Get(`:${idName}/support-network`)
@ApiOperation({
description:
'Find all Support Network entries related to a given Incident entity by Incident id.',
Expand Down
3 changes: 2 additions & 1 deletion src/controllers/incidents/incidents.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { IdPathParams } from '../../dto/id-path-params.dto';
import { RecordType } from '../../common/constants/enumerations';
import { TokenRefresherService } from '../../external-api/token-refresher/token-refresher.service';
import { RequestPreparerService } from '../../external-api/request-preparer/request-preparer.service';
import { idName } from '../../common/constants/parameter-constants';

describe('IncidentsService', () => {
let service: IncidentsService;
Expand Down Expand Up @@ -52,7 +53,7 @@ describe('IncidentsService', () => {
it.each([
[
SupportNetworkSingleResponseIncidentExample,
{ id: 'test' } as IdPathParams,
{ [idName]: 'test' } as IdPathParams,
{ since: '2024-12-01' } as SinceQueryParams,
],
])(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { SupportNetworkService } from '../../helpers/support-network/support-net
import { TokenRefresherService } from '../../external-api/token-refresher/token-refresher.service';
import { UtilitiesService } from '../../helpers/utilities/utilities.service';
import { RequestPreparerService } from '../../external-api/request-preparer/request-preparer.service';
import { idName } from '../../common/constants/parameter-constants';

describe('ServiceRequestsController', () => {
let controller: ServiceRequestsController;
Expand Down Expand Up @@ -51,7 +52,7 @@ describe('ServiceRequestsController', () => {
it.each([
[
SupportNetworkSingleResponseSRExample,
{ id: 'test' } as IdPathParams,
{ [idName]: 'test' } as IdPathParams,
{ since: '2020-02-02' } as SinceQueryParams,
],
])(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,10 @@ import {
import { IdPathParams } from '../../dto/id-path-params.dto';
import { SinceQueryParams } from '../../dto/since-query-params.dto';
import { ApiNotFoundEntity } from '../../entities/api-not-found.entity';
import { CONTENT_TYPE } from '../../common/constants/parameter-constants';
import {
CONTENT_TYPE,
idName,
} from '../../common/constants/parameter-constants';
import { ApiInternalServerErrorEntity } from '../../entities/api-internal-server-error.entity';

@Controller('sr')
Expand All @@ -36,7 +39,7 @@ export class ServiceRequestsController {
constructor(private readonly serviceRequestService: ServiceRequestsService) {}

@UseInterceptors(ClassSerializerInterceptor)
@Get(':id/support-network')
@Get(`:${idName}/support-network`)
@ApiOperation({
description:
'Find all Support Network entries related to a given Service Request entity by Service Request id.',
Expand Down
Loading

0 comments on commit 3720ea5

Please sign in to comment.