From 9aeb7da80861f103219ae53b129c2dd690c5bb8c Mon Sep 17 00:00:00 2001 From: hannah-macdonald1 Date: Fri, 1 Nov 2024 16:05:05 -0600 Subject: [PATCH 01/18] starting boilerplate for visits api --- .../cases/cases.controller.spec.ts | 2 ++ src/controllers/cases/cases.controller.ts | 24 +++++++++++++++++++ src/controllers/cases/cases.service.spec.ts | 2 ++ src/controllers/cases/cases.service.ts | 17 ++++++++++++- src/helpers/helpers.module.ts | 17 +++++++++++-- .../in-person-visits.module.ts | 7 ++++++ .../in-person-visits.service.spec.ts | 18 ++++++++++++++ .../in-person-visits.service.ts | 14 +++++++++++ 8 files changed, 98 insertions(+), 3 deletions(-) create mode 100644 src/helpers/in-person-visits/in-person-visits.module.ts create mode 100644 src/helpers/in-person-visits/in-person-visits.service.spec.ts create mode 100644 src/helpers/in-person-visits/in-person-visits.service.ts diff --git a/src/controllers/cases/cases.controller.spec.ts b/src/controllers/cases/cases.controller.spec.ts index 10cf81d..11c38b1 100644 --- a/src/controllers/cases/cases.controller.spec.ts +++ b/src/controllers/cases/cases.controller.spec.ts @@ -14,6 +14,7 @@ import { AuthService } from '../../common/guards/auth/auth.service'; import { TokenRefresherService } from '../../helpers/token-refresher/token-refresher.service'; import { SupportNetworkService } from '../../helpers/support-network/support-network.service'; import { UtilitiesService } from '../../helpers/utilities/utilities.service'; +import { InPersonVisitsService } from '../../helpers/in-person-visits/in-person-visits.service'; describe('CasesController', () => { let controller: CasesController; @@ -27,6 +28,7 @@ describe('CasesController', () => { AuthService, SupportNetworkService, TokenRefresherService, + InPersonVisitsService, { provide: CACHE_MANAGER, useValue: {} }, ConfigService, UtilitiesService, diff --git a/src/controllers/cases/cases.controller.ts b/src/controllers/cases/cases.controller.ts index c499a97..8c8535d 100644 --- a/src/controllers/cases/cases.controller.ts +++ b/src/controllers/cases/cases.controller.ts @@ -90,4 +90,28 @@ export class CasesController { since, ); } + + // TODO: Add entity and swagger defintions once model is available + @Get(':id/visits') + async getSingleCaseInPersonVisitRecord( + @Param( + new ValidationPipe({ + transform: true, + transformOptions: { enableImplicitConversion: true }, + forbidNonWhitelisted: true, + }), + ) + id: IdPathParams, + @Query( + new ValidationPipe({ + transform: true, + transformOptions: { enableImplicitConversion: true }, + forbidNonWhitelisted: true, + skipMissingProperties: true, + }), + ) + since?: SinceQueryParams, + ) { + return await this.casesService.getSingleCaseInPersonVisitRecord(id, since); + } } diff --git a/src/controllers/cases/cases.service.spec.ts b/src/controllers/cases/cases.service.spec.ts index d0eb883..1e8601b 100644 --- a/src/controllers/cases/cases.service.spec.ts +++ b/src/controllers/cases/cases.service.spec.ts @@ -13,6 +13,7 @@ import { IdPathParams } from '../../dto/id-path-params.dto'; import { SinceQueryParams } from '../../dto/since-query-params.dto'; import { RecordType } from '../../common/constants/enumerations'; import { TokenRefresherService } from '../../helpers/token-refresher/token-refresher.service'; +import { InPersonVisitsService } from '../../helpers/in-person-visits/in-person-visits.service'; describe('CasesService', () => { let service: CasesService; @@ -26,6 +27,7 @@ describe('CasesService', () => { SupportNetworkService, UtilitiesService, TokenRefresherService, + InPersonVisitsService, { provide: CACHE_MANAGER, useValue: { diff --git a/src/controllers/cases/cases.service.ts b/src/controllers/cases/cases.service.ts index d43ab70..890ff05 100644 --- a/src/controllers/cases/cases.service.ts +++ b/src/controllers/cases/cases.service.ts @@ -7,10 +7,14 @@ import { } from '../../entities/support-network.entity'; import { IdPathParams } from '../../dto/id-path-params.dto'; import { SinceQueryParams } from '../../dto/since-query-params.dto'; +import { InPersonVisitsService } from '../../helpers/in-person-visits/in-person-visits.service'; @Injectable() export class CasesService { - constructor(private readonly supportNetworkService: SupportNetworkService) {} + constructor( + private readonly supportNetworkService: SupportNetworkService, + private readonly inPersonVisitsService: InPersonVisitsService, + ) {} async getSingleCaseSupportNetworkInformationRecord( id: IdPathParams, @@ -22,4 +26,15 @@ export class CasesService { since, ); } + + async getSingleCaseInPersonVisitRecord( + id: IdPathParams, + since?: SinceQueryParams, + ) { + return await this.inPersonVisitsService.getSingleInPersonVisitRecord( + RecordType.Case, + id, + since, + ); + } } diff --git a/src/helpers/helpers.module.ts b/src/helpers/helpers.module.ts index 82a8f58..b483970 100644 --- a/src/helpers/helpers.module.ts +++ b/src/helpers/helpers.module.ts @@ -7,6 +7,8 @@ import { UtilitiesModule } from './utilities/utilities.module'; import { UtilitiesService } from './utilities/utilities.service'; import { TokenRefresherModule } from './token-refresher/token-refresher.module'; import { TokenRefresherService } from './token-refresher/token-refresher.service'; +import { InPersonVisitsModule } from './in-person-visits/in-person-visits.module'; +import { InPersonVisitsService } from './in-person-visits/in-person-visits.service'; @Module({ imports: [ @@ -15,8 +17,19 @@ import { TokenRefresherService } from './token-refresher/token-refresher.service ConfigModule, UtilitiesModule, TokenRefresherModule, + InPersonVisitsModule, + ], + providers: [ + SupportNetworkService, + UtilitiesService, + TokenRefresherService, + InPersonVisitsService, + ], + exports: [ + SupportNetworkService, + UtilitiesService, + TokenRefresherService, + InPersonVisitsService, ], - providers: [SupportNetworkService, UtilitiesService, TokenRefresherService], - exports: [SupportNetworkService, UtilitiesService, TokenRefresherService], }) export class HelpersModule {} diff --git a/src/helpers/in-person-visits/in-person-visits.module.ts b/src/helpers/in-person-visits/in-person-visits.module.ts new file mode 100644 index 0000000..92817ab --- /dev/null +++ b/src/helpers/in-person-visits/in-person-visits.module.ts @@ -0,0 +1,7 @@ +import { Module } from '@nestjs/common'; +import { InPersonVisitsService } from './in-person-visits.service'; + +@Module({ + providers: [InPersonVisitsService], +}) +export class InPersonVisitsModule {} diff --git a/src/helpers/in-person-visits/in-person-visits.service.spec.ts b/src/helpers/in-person-visits/in-person-visits.service.spec.ts new file mode 100644 index 0000000..4675a5b --- /dev/null +++ b/src/helpers/in-person-visits/in-person-visits.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { InPersonVisitsService } from './in-person-visits.service'; + +describe('InPersonVisitsService', () => { + let service: InPersonVisitsService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [InPersonVisitsService], + }).compile(); + + service = module.get(InPersonVisitsService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/src/helpers/in-person-visits/in-person-visits.service.ts b/src/helpers/in-person-visits/in-person-visits.service.ts new file mode 100644 index 0000000..6138f57 --- /dev/null +++ b/src/helpers/in-person-visits/in-person-visits.service.ts @@ -0,0 +1,14 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import { Injectable } from '@nestjs/common'; +import { RecordType } from '../../common/constants/enumerations'; +import { IdPathParams } from '../../dto/id-path-params.dto'; +import { SinceQueryParams } from '../../dto/since-query-params.dto'; + +@Injectable() +export class InPersonVisitsService { + async getSingleInPersonVisitRecord( + type: RecordType, + id: IdPathParams, + since?: SinceQueryParams, + ) {} +} From de8b223daf68f312f4980d236ab2faa9ec44519a Mon Sep 17 00:00:00 2001 From: hannah-macdonald1 Date: Mon, 4 Nov 2024 13:02:45 -0700 Subject: [PATCH 02/18] refactor to keep http functions seperate --- src/app.module.ts | 2 + src/common/common.module.ts | 2 +- src/common/guards/auth/auth.guard.spec.ts | 2 +- src/common/guards/auth/auth.module.ts | 4 +- src/common/guards/auth/auth.service.spec.ts | 2 +- src/common/guards/auth/auth.service.ts | 2 +- .../cases/cases.controller.spec.ts | 4 +- src/controllers/cases/cases.module.ts | 12 +- src/controllers/cases/cases.service.spec.ts | 4 +- .../incidents/incidents.controller.spec.ts | 4 +- src/controllers/incidents/incidents.module.ts | 9 +- .../incidents/incidents.service.spec.ts | 4 +- .../service-requests.controller.spec.ts | 4 +- .../service-requests.module.ts | 2 + .../service-requests.service.spec.ts | 4 +- src/external-api/external-api.module.ts | 14 ++ .../request-preparer.module.ts | 19 +++ .../request-preparer.service.spec.ts | 157 ++++++++++++++++++ .../request-preparer.service.ts | 96 +++++++++++ .../token-refresher-constants.ts | 0 .../token-refresher/token-refresher.module.ts | 0 .../token-refresher.service.spec.ts | 0 .../token-refresher.service.ts | 0 src/helpers/helpers.module.ts | 15 +- .../support-network/support-network.module.ts | 18 +- .../support-network.service.spec.ts | 94 ++--------- .../support-network.service.ts | 87 ++-------- 27 files changed, 377 insertions(+), 184 deletions(-) create mode 100644 src/external-api/external-api.module.ts create mode 100644 src/external-api/request-preparer/request-preparer.module.ts create mode 100644 src/external-api/request-preparer/request-preparer.service.spec.ts create mode 100644 src/external-api/request-preparer/request-preparer.service.ts rename src/{helpers => external-api}/token-refresher/token-refresher-constants.ts (100%) rename src/{helpers => external-api}/token-refresher/token-refresher.module.ts (100%) rename src/{helpers => external-api}/token-refresher/token-refresher.service.spec.ts (100%) rename src/{helpers => external-api}/token-refresher/token-refresher.service.ts (100%) diff --git a/src/app.module.ts b/src/app.module.ts index 7b410df..6e7ad97 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -8,6 +8,7 @@ import { ControllersModule } from './controllers/controllers.module'; import { HelpersModule } from './helpers/helpers.module'; import { LoggerModule } from 'nestjs-pino'; import { CacheModule } from '@nestjs/cache-manager'; +import { ExternalApiModule } from './external-api/external-api.module'; @Module({ imports: [ @@ -19,6 +20,7 @@ import { CacheModule } from '@nestjs/cache-manager'; CommonModule, ControllersModule, HelpersModule, + ExternalApiModule, ], controllers: [AppController], providers: [AppService], diff --git a/src/common/common.module.ts b/src/common/common.module.ts index 14c7bc3..216e48a 100644 --- a/src/common/common.module.ts +++ b/src/common/common.module.ts @@ -5,7 +5,7 @@ import { UtilitiesModule } from '../helpers/utilities/utilities.module'; import { UtilitiesService } from '../helpers/utilities/utilities.service'; import { AuthService } from './guards/auth/auth.service'; import { AuthModule } from './guards/auth/auth.module'; -import { TokenRefresherModule } from '../helpers/token-refresher/token-refresher.module'; +import { TokenRefresherModule } from '../external-api/token-refresher/token-refresher.module'; @Module({ providers: [UtilitiesService, AuthService, UtilitiesService, ConfigService], diff --git a/src/common/guards/auth/auth.guard.spec.ts b/src/common/guards/auth/auth.guard.spec.ts index 1023eb7..edfcca5 100644 --- a/src/common/guards/auth/auth.guard.spec.ts +++ b/src/common/guards/auth/auth.guard.spec.ts @@ -7,7 +7,7 @@ import { UtilitiesService } from '../../../helpers/utilities/utilities.service'; import { AuthGuard } from './auth.guard'; import { AuthService } from './auth.service'; import { getMockReq } from '@jest-mock/express'; -import { TokenRefresherService } from '../../../helpers/token-refresher/token-refresher.service'; +import { TokenRefresherService } from '../../../external-api/token-refresher/token-refresher.service'; describe('AuthGuard', () => { let service: AuthService; diff --git a/src/common/guards/auth/auth.module.ts b/src/common/guards/auth/auth.module.ts index a859bfb..a29caa1 100644 --- a/src/common/guards/auth/auth.module.ts +++ b/src/common/guards/auth/auth.module.ts @@ -4,8 +4,8 @@ import { HttpModule } from '@nestjs/axios'; import { AuthService } from './auth.service'; import { UtilitiesModule } from '../../../helpers/utilities/utilities.module'; import { UtilitiesService } from '../../../helpers/utilities/utilities.service'; -import { TokenRefresherService } from '../../../helpers/token-refresher/token-refresher.service'; -import { TokenRefresherModule } from '../../../helpers/token-refresher/token-refresher.module'; +import { TokenRefresherService } from '../../../external-api/token-refresher/token-refresher.service'; +import { TokenRefresherModule } from '../../../external-api/token-refresher/token-refresher.module'; @Module({ providers: [ diff --git a/src/common/guards/auth/auth.service.spec.ts b/src/common/guards/auth/auth.service.spec.ts index 8226d0a..38f67f3 100644 --- a/src/common/guards/auth/auth.service.spec.ts +++ b/src/common/guards/auth/auth.service.spec.ts @@ -15,7 +15,7 @@ import { AuthService } from './auth.service'; import { RecordType } from '../../../common/constants/enumerations'; import { EnumTypeError } from '../../../common/errors/errors'; import { UtilitiesService } from '../../../helpers/utilities/utilities.service'; -import { TokenRefresherService } from '../../../helpers/token-refresher/token-refresher.service'; +import { TokenRefresherService } from '../../../external-api/token-refresher/token-refresher.service'; describe('AuthService', () => { let service: AuthService; diff --git a/src/common/guards/auth/auth.service.ts b/src/common/guards/auth/auth.service.ts index 103bf67..72c64d8 100644 --- a/src/common/guards/auth/auth.service.ts +++ b/src/common/guards/auth/auth.service.ts @@ -14,7 +14,7 @@ import { } from '../../../common/constants/parameter-constants'; import { firstValueFrom } from 'rxjs'; import { AxiosError } from 'axios'; -import { TokenRefresherService } from '../../../helpers/token-refresher/token-refresher.service'; +import { TokenRefresherService } from '../../../external-api/token-refresher/token-refresher.service'; @Injectable() export class AuthService { diff --git a/src/controllers/cases/cases.controller.spec.ts b/src/controllers/cases/cases.controller.spec.ts index 11c38b1..372fe54 100644 --- a/src/controllers/cases/cases.controller.spec.ts +++ b/src/controllers/cases/cases.controller.spec.ts @@ -11,10 +11,11 @@ import { import { SinceQueryParams } from '../../dto/since-query-params.dto'; import { IdPathParams } from '../../dto/id-path-params.dto'; import { AuthService } from '../../common/guards/auth/auth.service'; -import { TokenRefresherService } from '../../helpers/token-refresher/token-refresher.service'; +import { TokenRefresherService } from '../../external-api/token-refresher/token-refresher.service'; import { SupportNetworkService } from '../../helpers/support-network/support-network.service'; import { UtilitiesService } from '../../helpers/utilities/utilities.service'; import { InPersonVisitsService } from '../../helpers/in-person-visits/in-person-visits.service'; +import { RequestPreparerService } from '../../external-api/request-preparer/request-preparer.service'; describe('CasesController', () => { let controller: CasesController; @@ -29,6 +30,7 @@ describe('CasesController', () => { SupportNetworkService, TokenRefresherService, InPersonVisitsService, + RequestPreparerService, { provide: CACHE_MANAGER, useValue: {} }, ConfigService, UtilitiesService, diff --git a/src/controllers/cases/cases.module.ts b/src/controllers/cases/cases.module.ts index 7f4c815..3d6b084 100644 --- a/src/controllers/cases/cases.module.ts +++ b/src/controllers/cases/cases.module.ts @@ -7,10 +7,18 @@ import { AuthService } from '../../common/guards/auth/auth.service'; import { ConfigService } from '@nestjs/config'; import { UtilitiesService } from '../../helpers/utilities/utilities.service'; import { HttpModule } from '@nestjs/axios'; +import { TokenRefresherService } from '../../external-api/token-refresher/token-refresher.service'; +import { ExternalApiModule } from '../../external-api/external-api.module'; @Module({ - providers: [CasesService, AuthService, ConfigService, UtilitiesService], + providers: [ + CasesService, + AuthService, + ConfigService, + UtilitiesService, + TokenRefresherService, + ], controllers: [CasesController], - imports: [HelpersModule, AuthModule, HttpModule], + imports: [HelpersModule, AuthModule, HttpModule, ExternalApiModule], }) export class CasesModule {} diff --git a/src/controllers/cases/cases.service.spec.ts b/src/controllers/cases/cases.service.spec.ts index 1e8601b..5d2c77b 100644 --- a/src/controllers/cases/cases.service.spec.ts +++ b/src/controllers/cases/cases.service.spec.ts @@ -12,8 +12,9 @@ import { import { IdPathParams } from '../../dto/id-path-params.dto'; import { SinceQueryParams } from '../../dto/since-query-params.dto'; import { RecordType } from '../../common/constants/enumerations'; -import { TokenRefresherService } from '../../helpers/token-refresher/token-refresher.service'; +import { TokenRefresherService } from '../../external-api/token-refresher/token-refresher.service'; import { InPersonVisitsService } from '../../helpers/in-person-visits/in-person-visits.service'; +import { RequestPreparerService } from '../../external-api/request-preparer/request-preparer.service'; describe('CasesService', () => { let service: CasesService; @@ -28,6 +29,7 @@ describe('CasesService', () => { UtilitiesService, TokenRefresherService, InPersonVisitsService, + RequestPreparerService, { provide: CACHE_MANAGER, useValue: { diff --git a/src/controllers/incidents/incidents.controller.spec.ts b/src/controllers/incidents/incidents.controller.spec.ts index d07f795..2047e1e 100644 --- a/src/controllers/incidents/incidents.controller.spec.ts +++ b/src/controllers/incidents/incidents.controller.spec.ts @@ -12,8 +12,9 @@ import { SinceQueryParams } from '../../dto/since-query-params.dto'; import { IdPathParams } from '../../dto/id-path-params.dto'; import { AuthService } from '../../common/guards/auth/auth.service'; import { SupportNetworkService } from '../../helpers/support-network/support-network.service'; -import { TokenRefresherService } from '../../helpers/token-refresher/token-refresher.service'; +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'; describe('IncidentsController', () => { let controller: IncidentsController; @@ -27,6 +28,7 @@ describe('IncidentsController', () => { SupportNetworkService, AuthService, TokenRefresherService, + RequestPreparerService, { provide: CACHE_MANAGER, useValue: {} }, ConfigService, UtilitiesService, diff --git a/src/controllers/incidents/incidents.module.ts b/src/controllers/incidents/incidents.module.ts index 01d541e..3ac6330 100644 --- a/src/controllers/incidents/incidents.module.ts +++ b/src/controllers/incidents/incidents.module.ts @@ -7,9 +7,16 @@ import { HelpersModule } from '../../helpers/helpers.module'; import { AuthModule } from '../../common/guards/auth/auth.module'; import { AuthService } from '../../common/guards/auth/auth.service'; import { UtilitiesService } from '../../helpers/utilities/utilities.service'; +import { TokenRefresherService } from '../../external-api/token-refresher/token-refresher.service'; @Module({ - providers: [IncidentsService, AuthService, ConfigService, UtilitiesService], + providers: [ + IncidentsService, + AuthService, + ConfigService, + UtilitiesService, + TokenRefresherService, + ], controllers: [IncidentsController], imports: [HelpersModule, AuthModule, HttpModule], }) diff --git a/src/controllers/incidents/incidents.service.spec.ts b/src/controllers/incidents/incidents.service.spec.ts index 34d2a05..fe5175d 100644 --- a/src/controllers/incidents/incidents.service.spec.ts +++ b/src/controllers/incidents/incidents.service.spec.ts @@ -12,7 +12,8 @@ import { import { SinceQueryParams } from '../../dto/since-query-params.dto'; import { IdPathParams } from '../../dto/id-path-params.dto'; import { RecordType } from '../../common/constants/enumerations'; -import { TokenRefresherService } from '../../helpers/token-refresher/token-refresher.service'; +import { TokenRefresherService } from '../../external-api/token-refresher/token-refresher.service'; +import { RequestPreparerService } from '../../external-api/request-preparer/request-preparer.service'; describe('IncidentsService', () => { let service: IncidentsService; @@ -26,6 +27,7 @@ describe('IncidentsService', () => { SupportNetworkService, UtilitiesService, TokenRefresherService, + RequestPreparerService, { provide: CACHE_MANAGER, useValue: { diff --git a/src/controllers/service-requests/service-requests.controller.spec.ts b/src/controllers/service-requests/service-requests.controller.spec.ts index b5ae841..c17710f 100644 --- a/src/controllers/service-requests/service-requests.controller.spec.ts +++ b/src/controllers/service-requests/service-requests.controller.spec.ts @@ -11,8 +11,9 @@ import { import { IdPathParams } from '../../dto/id-path-params.dto'; import { SinceQueryParams } from '../../dto/since-query-params.dto'; import { SupportNetworkService } from '../../helpers/support-network/support-network.service'; -import { TokenRefresherService } from '../../helpers/token-refresher/token-refresher.service'; +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'; describe('ServiceRequestsController', () => { let controller: ServiceRequestsController; @@ -25,6 +26,7 @@ describe('ServiceRequestsController', () => { ServiceRequestsService, SupportNetworkService, TokenRefresherService, + RequestPreparerService, { provide: CACHE_MANAGER, useValue: {} }, ConfigService, UtilitiesService, diff --git a/src/controllers/service-requests/service-requests.module.ts b/src/controllers/service-requests/service-requests.module.ts index fda72fc..eba7f12 100644 --- a/src/controllers/service-requests/service-requests.module.ts +++ b/src/controllers/service-requests/service-requests.module.ts @@ -7,6 +7,7 @@ import { HelpersModule } from '../../helpers/helpers.module'; import { AuthModule } from '../../common/guards/auth/auth.module'; import { AuthService } from '../../common/guards/auth/auth.service'; import { UtilitiesService } from '../../helpers/utilities/utilities.service'; +import { TokenRefresherService } from '../../external-api/token-refresher/token-refresher.service'; @Module({ providers: [ @@ -14,6 +15,7 @@ import { UtilitiesService } from '../../helpers/utilities/utilities.service'; AuthService, ConfigService, UtilitiesService, + TokenRefresherService, ], controllers: [ServiceRequestsController], imports: [HelpersModule, AuthModule, HttpModule], diff --git a/src/controllers/service-requests/service-requests.service.spec.ts b/src/controllers/service-requests/service-requests.service.spec.ts index b0419fe..3061e5b 100644 --- a/src/controllers/service-requests/service-requests.service.spec.ts +++ b/src/controllers/service-requests/service-requests.service.spec.ts @@ -12,7 +12,8 @@ import { import { RecordType } from '../../common/constants/enumerations'; import { IdPathParams } from '../../dto/id-path-params.dto'; import { SinceQueryParams } from '../../dto/since-query-params.dto'; -import { TokenRefresherService } from '../../helpers/token-refresher/token-refresher.service'; +import { TokenRefresherService } from '../../external-api/token-refresher/token-refresher.service'; +import { RequestPreparerService } from '../../external-api/request-preparer/request-preparer.service'; describe('ServiceRequestsService', () => { let service: ServiceRequestsService; @@ -26,6 +27,7 @@ describe('ServiceRequestsService', () => { SupportNetworkService, UtilitiesService, TokenRefresherService, + RequestPreparerService, { provide: HttpService, useValue: { get: jest.fn() } }, { provide: CACHE_MANAGER, diff --git a/src/external-api/external-api.module.ts b/src/external-api/external-api.module.ts new file mode 100644 index 0000000..c85d4dd --- /dev/null +++ b/src/external-api/external-api.module.ts @@ -0,0 +1,14 @@ +import { Module } from '@nestjs/common'; +import { RequestPreparerService } from './request-preparer/request-preparer.service'; +import { TokenRefresherService } from './token-refresher/token-refresher.service'; +import { UtilitiesModule } from '../helpers/utilities/utilities.module'; +import { HttpModule } from '@nestjs/axios'; +import { TokenRefresherModule } from './token-refresher/token-refresher.module'; +import { ConfigModule } from '@nestjs/config'; + +@Module({ + imports: [UtilitiesModule, HttpModule, TokenRefresherModule, ConfigModule], + providers: [RequestPreparerService, TokenRefresherService], + exports: [RequestPreparerService, TokenRefresherService], +}) +export class ExternalApiModule {} diff --git a/src/external-api/request-preparer/request-preparer.module.ts b/src/external-api/request-preparer/request-preparer.module.ts new file mode 100644 index 0000000..6e27f5f --- /dev/null +++ b/src/external-api/request-preparer/request-preparer.module.ts @@ -0,0 +1,19 @@ +import { Module } from '@nestjs/common'; +import { RequestPreparerService } from './request-preparer.service'; +import { UtilitiesService } from '../../helpers/utilities/utilities.service'; +import { UtilitiesModule } from '../../helpers/utilities/utilities.module'; +import { TokenRefresherService } from '../token-refresher/token-refresher.service'; +import { HttpModule } from '@nestjs/axios'; +import { ConfigService } from '@nestjs/config'; + +@Module({ + imports: [UtilitiesModule, HttpModule], + providers: [ + RequestPreparerService, + UtilitiesService, + TokenRefresherService, + ConfigService, + ], + exports: [RequestPreparerService], +}) +export class RequestPreparerModule {} diff --git a/src/external-api/request-preparer/request-preparer.service.spec.ts b/src/external-api/request-preparer/request-preparer.service.spec.ts new file mode 100644 index 0000000..4aaa815 --- /dev/null +++ b/src/external-api/request-preparer/request-preparer.service.spec.ts @@ -0,0 +1,157 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { RequestPreparerService } from './request-preparer.service'; +import { UtilitiesService } from '../../helpers/utilities/utilities.service'; +import { + RecordEntityMap, + RecordType, +} from '../../common/constants/enumerations'; +import { + CHILD_LINKS, + CONTENT_TYPE, + VIEW_MODE, +} from '../../common/constants/parameter-constants'; +import { TokenRefresherService } from '../token-refresher/token-refresher.service'; +import { HttpService } from '@nestjs/axios'; +import { CACHE_MANAGER } from '@nestjs/cache-manager'; +import { ConfigModule, ConfigService } from '@nestjs/config'; +import { of } from 'rxjs'; +import { + AxiosError, + AxiosResponse, + InternalAxiosRequestConfig, + RawAxiosRequestHeaders, +} from 'axios'; + +describe('RequestPreparerService', () => { + let service: RequestPreparerService; + let httpService: HttpService; + let tokenRefresherService: TokenRefresherService; + const validId = '1234ab'; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + imports: [ConfigModule.forRoot()], + providers: [ + RequestPreparerService, + UtilitiesService, + TokenRefresherService, + { + provide: HttpService, + useValue: { get: () => jest.fn(), post: () => jest.fn() }, + }, + { + provide: CACHE_MANAGER, + useValue: { + set: () => jest.fn(), + get: () => 'Bearer token', + }, + }, + ConfigService, + ], + }).compile(); + + service = module.get(RequestPreparerService); + httpService = module.get(HttpService); + tokenRefresherService = module.get( + TokenRefresherService, + ); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); + + describe('prepareHeadersAndParams tests', () => { + it.each([ + [RecordType.Case, { id: validId }], + [RecordType.SR, { id: validId }], + ])( + 'correctly prepares headers and params with no date parameter', + (type, id) => { + const [headers, params] = service.prepareHeadersAndParams(type, id); + expect(headers).toEqual({ Accept: CONTENT_TYPE }); + expect(params).toEqual({ + ViewMode: VIEW_MODE, + ChildLinks: CHILD_LINKS, + searchspec: `([Entity Id]="${id.id}" AND [Entity Name]="${RecordEntityMap[type]}")`, + }); + }, + ); + + it.each([ + [ + RecordType.Case, + { id: validId }, + { since: '2024-02-20' }, + '02/20/2024 00:00:00', + ], + ])( + 'correctly prepares headers and params with a date parameter', + (type, id, since, expectedDate) => { + const [headers, params] = service.prepareHeadersAndParams( + type, + id, + since, + ); + expect(headers).toEqual({ Accept: CONTENT_TYPE }); + expect(params).toEqual({ + ViewMode: VIEW_MODE, + ChildLinks: CHILD_LINKS, + searchspec: `([Entity Id]="${id.id}" AND [Entity Name]="${RecordEntityMap[type]}" AND [Updated] > "${expectedDate}")`, + }); + }, + ); + }); + + describe('sendGetRequest tests', () => { + it('provides a response on sucessful http service call', async () => { + const spy = jest.spyOn(httpService, 'get').mockReturnValueOnce( + of({ + data: {}, + headers: {}, + status: 200, + statusText: 'OK', + } as AxiosResponse), + ); + const result = await service.sendGetRequest('url', {}); + expect(spy).toHaveBeenCalledTimes(1); + expect(result.data).toEqual({}); + }); + + it.each([[404], [500]])( + `Should return HttpException with matching status on axios error`, + async (status) => { + const spy = jest.spyOn(httpService, 'get').mockImplementation(() => { + throw new AxiosError( + 'Axios Error', + status.toString(), + {} as InternalAxiosRequestConfig, + {}, + { + data: {}, + status: status, + statusText: '', + headers: {} as RawAxiosRequestHeaders, + config: {} as InternalAxiosRequestConfig, + }, + ); + }); + + await expect( + service.sendGetRequest('url', {}, {}), + ).rejects.toHaveProperty('status', status); + expect(spy).toHaveBeenCalledTimes(1); + }, + ); + + it('Should return HttpException with status 500 on bearer token undefined', async () => { + const spy = jest + .spyOn(tokenRefresherService, 'refreshUpstreamBearerToken') + .mockResolvedValueOnce(undefined); + await expect( + service.sendGetRequest('url', {}, {}), + ).rejects.toHaveProperty('status', 500); + expect(spy).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/src/external-api/request-preparer/request-preparer.service.ts b/src/external-api/request-preparer/request-preparer.service.ts new file mode 100644 index 0000000..9829565 --- /dev/null +++ b/src/external-api/request-preparer/request-preparer.service.ts @@ -0,0 +1,96 @@ +import { HttpException, HttpStatus, Injectable, Logger } from '@nestjs/common'; +import { + RecordEntityMap, + RecordType, +} from '../../common/constants/enumerations'; +import { + VIEW_MODE, + CHILD_LINKS, + CONTENT_TYPE, +} from '../../common/constants/parameter-constants'; +import { IdPathParams } from '../../dto/id-path-params.dto'; +import { SinceQueryParams } from '../../dto/since-query-params.dto'; +import { UtilitiesService } from '../../helpers/utilities/utilities.service'; +import { TokenRefresherService } from '../token-refresher/token-refresher.service'; +import { HttpService } from '@nestjs/axios'; +import { firstValueFrom } from 'rxjs'; +import { AxiosError } from 'axios'; + +@Injectable() +export class RequestPreparerService { + private readonly logger = new Logger(RequestPreparerService.name); + constructor( + private readonly utilitiesService: UtilitiesService, + private readonly tokenRefresherService: TokenRefresherService, + private readonly httpService: HttpService, + ) {} + + prepareHeadersAndParams( + type: RecordType, + id: IdPathParams, + since?: SinceQueryParams, + ) { + let searchSpec = `([Entity Id]="${id.id}" AND [Entity Name]="${RecordEntityMap[type]}"`; + let formattedDate: string | undefined; + if ( + since === undefined || + typeof since.since !== 'string' || + (formattedDate = this.utilitiesService.convertISODateToUpstreamFormat( + since.since, + )) === undefined + ) { + searchSpec = searchSpec + `)`; + } else { + searchSpec = searchSpec + ` AND [Updated] > "${formattedDate}")`; + } + const params = { + ViewMode: VIEW_MODE, + ChildLinks: CHILD_LINKS, + searchspec: searchSpec, + }; + const headers = { + Accept: CONTENT_TYPE, + }; + return [headers, params]; + } + + async sendGetRequest(url: string, headers, params?) { + let response; + try { + const token = + await this.tokenRefresherService.refreshUpstreamBearerToken(); + if (token === undefined) { + throw new Error('Upstream auth failed'); + } + headers['Authorization'] = token; + response = await firstValueFrom( + this.httpService.get(url, { params, headers }), + ); + } catch (error) { + if (error instanceof AxiosError) { + this.logger.error(error.message, error.stack, error.cause); + if (error.status === 404) { + throw new HttpException( + { + status: HttpStatus.NOT_FOUND, + error: 'There is no data for the requested resource', + }, + HttpStatus.NOT_FOUND, + { cause: error }, + ); + } + } else { + this.logger.error(error); + } + throw new HttpException( + { + status: HttpStatus.INTERNAL_SERVER_ERROR, + error: error.message, + }, + HttpStatus.INTERNAL_SERVER_ERROR, + { cause: error }, + ); + } + return response; + } +} diff --git a/src/helpers/token-refresher/token-refresher-constants.ts b/src/external-api/token-refresher/token-refresher-constants.ts similarity index 100% rename from src/helpers/token-refresher/token-refresher-constants.ts rename to src/external-api/token-refresher/token-refresher-constants.ts diff --git a/src/helpers/token-refresher/token-refresher.module.ts b/src/external-api/token-refresher/token-refresher.module.ts similarity index 100% rename from src/helpers/token-refresher/token-refresher.module.ts rename to src/external-api/token-refresher/token-refresher.module.ts diff --git a/src/helpers/token-refresher/token-refresher.service.spec.ts b/src/external-api/token-refresher/token-refresher.service.spec.ts similarity index 100% rename from src/helpers/token-refresher/token-refresher.service.spec.ts rename to src/external-api/token-refresher/token-refresher.service.spec.ts diff --git a/src/helpers/token-refresher/token-refresher.service.ts b/src/external-api/token-refresher/token-refresher.service.ts similarity index 100% rename from src/helpers/token-refresher/token-refresher.service.ts rename to src/external-api/token-refresher/token-refresher.service.ts diff --git a/src/helpers/helpers.module.ts b/src/helpers/helpers.module.ts index b483970..74665be 100644 --- a/src/helpers/helpers.module.ts +++ b/src/helpers/helpers.module.ts @@ -5,10 +5,12 @@ import { HttpModule } from '@nestjs/axios'; import { ConfigModule } from '@nestjs/config'; import { UtilitiesModule } from './utilities/utilities.module'; import { UtilitiesService } from './utilities/utilities.service'; -import { TokenRefresherModule } from './token-refresher/token-refresher.module'; -import { TokenRefresherService } from './token-refresher/token-refresher.service'; +import { TokenRefresherModule } from '../external-api/token-refresher/token-refresher.module'; +import { TokenRefresherService } from '../external-api/token-refresher/token-refresher.service'; import { InPersonVisitsModule } from './in-person-visits/in-person-visits.module'; import { InPersonVisitsService } from './in-person-visits/in-person-visits.service'; +import { RequestPreparerModule } from '../external-api/request-preparer/request-preparer.module'; +import { RequestPreparerService } from '../external-api/request-preparer/request-preparer.service'; @Module({ imports: [ @@ -18,18 +20,15 @@ import { InPersonVisitsService } from './in-person-visits/in-person-visits.servi UtilitiesModule, TokenRefresherModule, InPersonVisitsModule, + RequestPreparerModule, ], providers: [ SupportNetworkService, UtilitiesService, TokenRefresherService, InPersonVisitsService, + RequestPreparerService, ], - exports: [ - SupportNetworkService, - UtilitiesService, - TokenRefresherService, - InPersonVisitsService, - ], + exports: [SupportNetworkService, UtilitiesService, InPersonVisitsService], }) export class HelpersModule {} diff --git a/src/helpers/support-network/support-network.module.ts b/src/helpers/support-network/support-network.module.ts index 875d2ab..f0343b6 100644 --- a/src/helpers/support-network/support-network.module.ts +++ b/src/helpers/support-network/support-network.module.ts @@ -1,15 +1,21 @@ import { Module } from '@nestjs/common'; import { HttpModule } from '@nestjs/axios'; import { SupportNetworkService } from './support-network.service'; -import { ConfigModule } from '@nestjs/config'; -import { UtilitiesModule } from '../../helpers/utilities/utilities.module'; +import { ConfigModule, ConfigService } from '@nestjs/config'; +import { RequestPreparerService } from '../../external-api/request-preparer/request-preparer.service'; +import { RequestPreparerModule } from '../../external-api/request-preparer/request-preparer.module'; import { UtilitiesService } from '../utilities/utilities.service'; -import { TokenRefresherModule } from '../token-refresher/token-refresher.module'; -import { TokenRefresherService } from '../token-refresher/token-refresher.service'; +import { TokenRefresherService } from '../../external-api/token-refresher/token-refresher.service'; @Module({ - imports: [HttpModule, ConfigModule, UtilitiesModule, TokenRefresherModule], - providers: [SupportNetworkService, UtilitiesService, TokenRefresherService], + imports: [HttpModule, ConfigModule, RequestPreparerModule], + providers: [ + SupportNetworkService, + RequestPreparerService, + UtilitiesService, + TokenRefresherService, + ConfigService, + ], exports: [SupportNetworkService], }) export class SupportNetworkModule {} diff --git a/src/helpers/support-network/support-network.service.spec.ts b/src/helpers/support-network/support-network.service.spec.ts index d014d1a..7520387 100644 --- a/src/helpers/support-network/support-network.service.spec.ts +++ b/src/helpers/support-network/support-network.service.spec.ts @@ -2,13 +2,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { HttpService } from '@nestjs/axios'; import { CACHE_MANAGER } from '@nestjs/cache-manager'; import { ConfigModule, ConfigService } from '@nestjs/config'; -import { - AxiosError, - AxiosResponse, - InternalAxiosRequestConfig, - RawAxiosRequestHeaders, -} from 'axios'; -import { of } from 'rxjs'; +import { AxiosResponse } from 'axios'; import { UtilitiesService } from '../utilities/utilities.service'; import { RecordType } from '../../common/constants/enumerations'; import { SupportNetworkService } from './support-network.service'; @@ -21,13 +15,12 @@ import { } from '../../entities/support-network.entity'; import { IdPathParams } from '../../dto/id-path-params.dto'; import { SinceQueryParams } from '../../dto/since-query-params.dto'; -import { TokenRefresherService } from '../token-refresher/token-refresher.service'; +import { TokenRefresherService } from '../../external-api/token-refresher/token-refresher.service'; +import { RequestPreparerService } from '../../external-api/request-preparer/request-preparer.service'; describe('SupportNetworkService', () => { let service: SupportNetworkService; - let configService: ConfigService; - let httpService: HttpService; - let tokenRefresherService: TokenRefresherService; + let requestPreparerService: RequestPreparerService; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ @@ -37,6 +30,7 @@ describe('SupportNetworkService', () => { UtilitiesService, ConfigService, TokenRefresherService, + RequestPreparerService, { provide: HttpService, useValue: { get: jest.fn() } }, { provide: CACHE_MANAGER, @@ -49,10 +43,8 @@ describe('SupportNetworkService', () => { }).compile(); service = module.get(SupportNetworkService); - configService = module.get(ConfigService); - httpService = module.get(HttpService); - tokenRefresherService = module.get( - TokenRefresherService, + requestPreparerService = module.get( + RequestPreparerService, ); }); @@ -77,22 +69,14 @@ describe('SupportNetworkService', () => { ])( 'should return single values given good input', async (data, recordType, idPathParams, sinceQueryParams) => { - const spy = jest.spyOn(httpService, 'get').mockReturnValueOnce( - of({ + const spy = jest + .spyOn(requestPreparerService, 'sendGetRequest') + .mockResolvedValueOnce({ data: data, headers: {}, - config: { - url: - configService.get('UPSTREAM_BASE_URL') + - configService - .get('SUPPORT_NETWORK_ENDPOINT') - .replace(/\s/g, '%20'), - headers: {} as RawAxiosRequestHeaders, - }, status: 200, statusText: 'OK', - } as AxiosResponse), - ); + } as AxiosResponse); const result = await service.getSingleSupportNetworkInformationRecord( recordType, @@ -114,22 +98,14 @@ describe('SupportNetworkService', () => { ])( 'should return list values given good input', async (data, recordType, idPathParams, sinceQueryParams) => { - const spy = jest.spyOn(httpService, 'get').mockReturnValueOnce( - of({ + const spy = jest + .spyOn(requestPreparerService, 'sendGetRequest') + .mockResolvedValueOnce({ data: data, headers: {}, - config: { - url: - configService.get('UPSTREAM_BASE_URL') + - configService - .get('SUPPORT_NETWORK_ENDPOINT') - .replace(/\s/g, '%20'), - headers: {} as RawAxiosRequestHeaders, - }, status: 200, statusText: 'OK', - } as AxiosResponse), - ); + } as AxiosResponse); const result = await service.getSingleSupportNetworkInformationRecord( recordType, @@ -140,45 +116,5 @@ describe('SupportNetworkService', () => { expect(result).toEqual(new NestedSupportNetworkEntity(data)); }, ); - - it.each([[404], [500]])( - `Should return HttpException with matching status on axios error`, - async (status) => { - const spy = jest.spyOn(httpService, 'get').mockImplementation(() => { - throw new AxiosError( - 'Axios Error', - status.toString(), - {} as InternalAxiosRequestConfig, - {}, - { - data: {}, - status: status, - statusText: '', - headers: {} as RawAxiosRequestHeaders, - config: {} as InternalAxiosRequestConfig, - }, - ); - }); - - await expect( - service.getSingleSupportNetworkInformationRecord(RecordType.Case, { - id: 'doesNotExist', - } as IdPathParams), - ).rejects.toHaveProperty('status', status); - expect(spy).toHaveBeenCalledTimes(1); - }, - ); - - it('Should return HttpException with status 500 on bearer token undefined', async () => { - const spy = jest - .spyOn(tokenRefresherService, 'refreshUpstreamBearerToken') - .mockResolvedValueOnce(undefined); - await expect( - service.getSingleSupportNetworkInformationRecord(RecordType.Case, { - id: 'doesNotExist', - } as IdPathParams), - ).rejects.toHaveProperty('status', 500); - expect(spy).toHaveBeenCalledTimes(1); - }); }); }); diff --git a/src/helpers/support-network/support-network.service.ts b/src/helpers/support-network/support-network.service.ts index aa798e7..21591e8 100644 --- a/src/helpers/support-network/support-network.service.ts +++ b/src/helpers/support-network/support-network.service.ts @@ -1,35 +1,20 @@ -import { HttpService } from '@nestjs/axios'; -import { HttpException, HttpStatus, Injectable, Logger } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; -import { firstValueFrom } from 'rxjs'; -import { - RecordEntityMap, - RecordType, -} from '../../common/constants/enumerations'; -import { - CHILD_LINKS, - VIEW_MODE, - CONTENT_TYPE, -} from '../../common/constants/parameter-constants'; -import { UtilitiesService } from '../utilities/utilities.service'; +import { RecordType } from '../../common/constants/enumerations'; import { SupportNetworkEntity, NestedSupportNetworkEntity, } from '../../entities/support-network.entity'; -import { AxiosError } from 'axios'; import { IdPathParams } from '../../dto/id-path-params.dto'; import { SinceQueryParams } from '../../dto/since-query-params.dto'; -import { TokenRefresherService } from '../token-refresher/token-refresher.service'; +import { RequestPreparerService } from '../../external-api/request-preparer/request-preparer.service'; @Injectable() export class SupportNetworkService { url: string; - private readonly logger = new Logger(SupportNetworkService.name); constructor( - private readonly httpService: HttpService, private readonly configService: ConfigService, - private readonly utilitiesService: UtilitiesService, - private readonly tokenRefresherService: TokenRefresherService, + private readonly requestPreparerService: RequestPreparerService, ) { this.url = ( this.configService.get('UPSTREAM_BASE_URL') + @@ -42,63 +27,13 @@ export class SupportNetworkService { id: IdPathParams, since?: SinceQueryParams, ) { - let searchSpec = `([Entity Id]="${id.id}" AND [Entity Name]="${RecordEntityMap[type]}"`; - let formattedDate: string | undefined; - if ( - since === undefined || - typeof since.since !== 'string' || - (formattedDate = this.utilitiesService.convertISODateToUpstreamFormat( - since.since, - )) === undefined - ) { - searchSpec = searchSpec + `)`; - } else { - searchSpec = searchSpec + ` AND [Updated] > "${formattedDate}")`; - } - const params = { - ViewMode: VIEW_MODE, - ChildLinks: CHILD_LINKS, - searchspec: searchSpec, - }; - const headers = { - Accept: CONTENT_TYPE, - }; - let response; - try { - const token = - await this.tokenRefresherService.refreshUpstreamBearerToken(); - if (token === undefined) { - throw new Error('Upstream auth failed'); - } - headers['Authorization'] = token; - response = await firstValueFrom( - this.httpService.get(this.url, { params, headers }), - ); - } catch (error) { - if (error instanceof AxiosError) { - this.logger.error(error.message, error.stack, error.cause); - if (error.status === 404) { - throw new HttpException( - { - status: HttpStatus.NOT_FOUND, - error: 'There is no data for the requested resource', - }, - HttpStatus.NOT_FOUND, - { cause: error }, - ); - } - } else { - this.logger.error(error); - } - throw new HttpException( - { - status: HttpStatus.INTERNAL_SERVER_ERROR, - error: error.message, - }, - HttpStatus.INTERNAL_SERVER_ERROR, - { cause: error }, - ); - } + const [headers, params] = + this.requestPreparerService.prepareHeadersAndParams(type, id, since); + const response = await this.requestPreparerService.sendGetRequest( + this.url, + headers, + params, + ); if ((response.data as object).hasOwnProperty('items')) { return new NestedSupportNetworkEntity(response.data); } From 783dc70ba51eb1b4831060b691cbd72ef003c07a Mon Sep 17 00:00:00 2001 From: hannah-macdonald1 Date: Mon, 4 Nov 2024 13:44:40 -0700 Subject: [PATCH 03/18] added in person visits stub --- .env.example | 1 + src/configuration/configuration.ts | 4 +++ .../request-preparer.service.spec.ts | 14 +++++--- .../request-preparer.service.ts | 4 +++ .../in-person-visits.module.ts | 15 ++++++++- .../in-person-visits.service.spec.ts | 23 ++++++++++++- .../in-person-visits.service.ts | 32 +++++++++++++++++-- .../support-network.service.ts | 9 +++++- 8 files changed, 93 insertions(+), 9 deletions(-) diff --git a/.env.example b/.env.example index 9fb523a..09804f5 100644 --- a/.env.example +++ b/.env.example @@ -7,6 +7,7 @@ CLIENT_ID='id here' CLIENT_SECRET='secret here' UPSTREAM_BASE_URL=http://www.google.com SUPPORT_NETWORK_ENDPOINT=/endpoint/path/here +IN_PERSON_VISITS_ENDPOINT=/endpoint/path/here CASE_ENDPOINT=/endpoint/path/here INCIDENT_ENDPOINT=/endpoint/path/here SR_ENDPOINT=/endpoint/path/here diff --git a/src/configuration/configuration.ts b/src/configuration/configuration.ts index 144a0d0..d3311c0 100644 --- a/src/configuration/configuration.ts +++ b/src/configuration/configuration.ts @@ -27,4 +27,8 @@ export default () => ({ clientId: process.env.CLIENT_ID, clientSecret: process.env.CLIENT_SECRET, }, + workspaces: { + supportNetwork: undefined, + inPersonVisits: 'int_lab', + }, }); diff --git a/src/external-api/request-preparer/request-preparer.service.spec.ts b/src/external-api/request-preparer/request-preparer.service.spec.ts index 4aaa815..778e4d0 100644 --- a/src/external-api/request-preparer/request-preparer.service.spec.ts +++ b/src/external-api/request-preparer/request-preparer.service.spec.ts @@ -63,17 +63,22 @@ describe('RequestPreparerService', () => { describe('prepareHeadersAndParams tests', () => { it.each([ - [RecordType.Case, { id: validId }], - [RecordType.SR, { id: validId }], + [RecordType.Case, { id: validId }, undefined], + [RecordType.SR, { id: validId }, 'workspace'], ])( 'correctly prepares headers and params with no date parameter', - (type, id) => { - const [headers, params] = service.prepareHeadersAndParams(type, id); + (type, id, workspace) => { + const [headers, params] = service.prepareHeadersAndParams( + type, + id, + workspace, + ); expect(headers).toEqual({ Accept: CONTENT_TYPE }); expect(params).toEqual({ ViewMode: VIEW_MODE, ChildLinks: CHILD_LINKS, searchspec: `([Entity Id]="${id.id}" AND [Entity Name]="${RecordEntityMap[type]}")`, + workspace: workspace, }); }, ); @@ -91,6 +96,7 @@ describe('RequestPreparerService', () => { const [headers, params] = service.prepareHeadersAndParams( type, id, + undefined, since, ); expect(headers).toEqual({ Accept: CONTENT_TYPE }); diff --git a/src/external-api/request-preparer/request-preparer.service.ts b/src/external-api/request-preparer/request-preparer.service.ts index 9829565..9fac57c 100644 --- a/src/external-api/request-preparer/request-preparer.service.ts +++ b/src/external-api/request-preparer/request-preparer.service.ts @@ -28,6 +28,7 @@ export class RequestPreparerService { prepareHeadersAndParams( type: RecordType, id: IdPathParams, + workspace: string | undefined, since?: SinceQueryParams, ) { let searchSpec = `([Entity Id]="${id.id}" AND [Entity Name]="${RecordEntityMap[type]}"`; @@ -48,6 +49,9 @@ export class RequestPreparerService { ChildLinks: CHILD_LINKS, searchspec: searchSpec, }; + if (typeof workspace !== 'undefined') { + params['workspace'] = workspace; + } const headers = { Accept: CONTENT_TYPE, }; diff --git a/src/helpers/in-person-visits/in-person-visits.module.ts b/src/helpers/in-person-visits/in-person-visits.module.ts index 92817ab..05e4838 100644 --- a/src/helpers/in-person-visits/in-person-visits.module.ts +++ b/src/helpers/in-person-visits/in-person-visits.module.ts @@ -1,7 +1,20 @@ import { Module } from '@nestjs/common'; import { InPersonVisitsService } from './in-person-visits.service'; +import { ConfigService } from '@nestjs/config'; +import { RequestPreparerService } from '../../external-api/request-preparer/request-preparer.service'; +import { UtilitiesService } from '../utilities/utilities.service'; +import { TokenRefresherService } from '../../external-api/token-refresher/token-refresher.service'; +import { HttpModule } from '@nestjs/axios'; @Module({ - providers: [InPersonVisitsService], + imports: [HttpModule], + providers: [ + InPersonVisitsService, + ConfigService, + RequestPreparerService, + UtilitiesService, + TokenRefresherService, + ], + exports: [InPersonVisitsService], }) export class InPersonVisitsModule {} diff --git a/src/helpers/in-person-visits/in-person-visits.service.spec.ts b/src/helpers/in-person-visits/in-person-visits.service.spec.ts index 4675a5b..c41e8e9 100644 --- a/src/helpers/in-person-visits/in-person-visits.service.spec.ts +++ b/src/helpers/in-person-visits/in-person-visits.service.spec.ts @@ -1,12 +1,33 @@ import { Test, TestingModule } from '@nestjs/testing'; import { InPersonVisitsService } from './in-person-visits.service'; +import { ConfigModule, ConfigService } from '@nestjs/config'; +import { RequestPreparerService } from '../../external-api/request-preparer/request-preparer.service'; +import { UtilitiesService } from '../utilities/utilities.service'; +import { TokenRefresherService } from '../../external-api/token-refresher/token-refresher.service'; +import { CACHE_MANAGER } from '@nestjs/cache-manager'; +import { HttpService } from '@nestjs/axios'; describe('InPersonVisitsService', () => { let service: InPersonVisitsService; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ - providers: [InPersonVisitsService], + imports: [ConfigModule.forRoot()], + providers: [ + InPersonVisitsService, + UtilitiesService, + ConfigService, + TokenRefresherService, + RequestPreparerService, + { provide: HttpService, useValue: { get: jest.fn() } }, + { + provide: CACHE_MANAGER, + useValue: { + set: () => jest.fn(), + get: () => 'Bearer token', + }, + }, + ], }).compile(); service = module.get(InPersonVisitsService); diff --git a/src/helpers/in-person-visits/in-person-visits.service.ts b/src/helpers/in-person-visits/in-person-visits.service.ts index 6138f57..d15fba5 100644 --- a/src/helpers/in-person-visits/in-person-visits.service.ts +++ b/src/helpers/in-person-visits/in-person-visits.service.ts @@ -1,14 +1,42 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ import { Injectable } from '@nestjs/common'; import { RecordType } from '../../common/constants/enumerations'; import { IdPathParams } from '../../dto/id-path-params.dto'; import { SinceQueryParams } from '../../dto/since-query-params.dto'; +import { ConfigService } from '@nestjs/config'; +import { RequestPreparerService } from '../../external-api/request-preparer/request-preparer.service'; @Injectable() export class InPersonVisitsService { + url: string; + workspace: string | undefined; + constructor( + private readonly configService: ConfigService, + private readonly requestPreparerService: RequestPreparerService, + ) { + this.url = ( + this.configService.get('UPSTREAM_BASE_URL') + + this.configService.get('IN_PERSON_VISITS_ENDPOINT') + ).replace(/\s/g, '%20'); + this.workspace = this.configService.get('workspaces.inPersonVisits'); + } async getSingleInPersonVisitRecord( type: RecordType, id: IdPathParams, since?: SinceQueryParams, - ) {} + ) { + const [headers, params] = + this.requestPreparerService.prepareHeadersAndParams( + type, + id, + this.workspace, + since, + ); + const response = await this.requestPreparerService.sendGetRequest( + this.url, + headers, + params, + ); + // TODO: Pass info to entity constructs once created + return response.data; + } } diff --git a/src/helpers/support-network/support-network.service.ts b/src/helpers/support-network/support-network.service.ts index 21591e8..4ce8dbf 100644 --- a/src/helpers/support-network/support-network.service.ts +++ b/src/helpers/support-network/support-network.service.ts @@ -12,6 +12,7 @@ import { RequestPreparerService } from '../../external-api/request-preparer/requ @Injectable() export class SupportNetworkService { url: string; + workspace: string | undefined; constructor( private readonly configService: ConfigService, private readonly requestPreparerService: RequestPreparerService, @@ -20,6 +21,7 @@ export class SupportNetworkService { this.configService.get('UPSTREAM_BASE_URL') + this.configService.get('SUPPORT_NETWORK_ENDPOINT') ).replace(/\s/g, '%20'); + this.workspace = this.configService.get('workspaces.supportNetwork'); } async getSingleSupportNetworkInformationRecord( @@ -28,7 +30,12 @@ export class SupportNetworkService { since?: SinceQueryParams, ) { const [headers, params] = - this.requestPreparerService.prepareHeadersAndParams(type, id, since); + this.requestPreparerService.prepareHeadersAndParams( + type, + id, + this.workspace, + since, + ); const response = await this.requestPreparerService.sendGetRequest( this.url, headers, From 8355abf5f107dc83e780699186d9e3d8c72ec7b3 Mon Sep 17 00:00:00 2001 From: hannah-macdonald1 Date: Mon, 4 Nov 2024 16:20:04 -0700 Subject: [PATCH 04/18] Swagger definitions and yaml updates --- .github/workflows/build-and-push-ghcr.yaml | 8 ++ helm/templates/deployment.yaml | 2 + helm/values.yaml | 3 + src/configuration/configuration.ts | 2 +- src/controllers/cases/cases.controller.ts | 38 ++++++- src/controllers/cases/cases.service.ts | 6 +- .../incidents/incidents.controller.ts | 2 +- .../service-requests.controller.ts | 2 +- src/entities/in-person-visits.entity.ts | 98 +++++++++++++++++++ .../request-preparer.service.spec.ts | 31 ++---- .../request-preparer.service.ts | 10 +- .../in-person-visits.service.ts | 19 ++-- .../support-network.service.ts | 9 +- 13 files changed, 184 insertions(+), 46 deletions(-) create mode 100644 src/entities/in-person-visits.entity.ts diff --git a/.github/workflows/build-and-push-ghcr.yaml b/.github/workflows/build-and-push-ghcr.yaml index 86d9b2c..ce8226a 100644 --- a/.github/workflows/build-and-push-ghcr.yaml +++ b/.github/workflows/build-and-push-ghcr.yaml @@ -77,6 +77,14 @@ jobs: value: ${{ secrets.APS_NAMESPACE }} commitChange: false + - name: 'YAML poke: Set APS namespace' + uses: fjogeleit/yaml-update-action@v0.15.0 + with: + valueFile: 'helm/values.yaml' + propertyPath: 'vpiAppBuildLabel.version' + value: "${{ github.ref_name }}-${{ github.run_number }}" + commitChange: false + - name: Authenticate with OpenShift uses: redhat-actions/oc-login@v1 with: diff --git a/helm/templates/deployment.yaml b/helm/templates/deployment.yaml index bb89368..aff36b0 100644 --- a/helm/templates/deployment.yaml +++ b/helm/templates/deployment.yaml @@ -77,4 +77,6 @@ spec: secretKeyRef: name: visitz-api key: CLIENT_SECRET + - name: VPI_APP_LABEL + value: {{ .Values.vpiAppBuildLabel.version }} restartPolicy: Always diff --git a/helm/values.yaml b/helm/values.yaml index 39e2d31..ab576c5 100644 --- a/helm/values.yaml +++ b/helm/values.yaml @@ -20,3 +20,6 @@ affinity: {} aps: namespace: '' + +vpiAppBuildLabel: + version: '' diff --git a/src/configuration/configuration.ts b/src/configuration/configuration.ts index d3311c0..8ee0fa8 100644 --- a/src/configuration/configuration.ts +++ b/src/configuration/configuration.ts @@ -29,6 +29,6 @@ export default () => ({ }, workspaces: { supportNetwork: undefined, - inPersonVisits: 'int_lab', + inPersonVisits: undefined, }, }); diff --git a/src/controllers/cases/cases.controller.ts b/src/controllers/cases/cases.controller.ts index 8c8535d..1a325ef 100644 --- a/src/controllers/cases/cases.controller.ts +++ b/src/controllers/cases/cases.controller.ts @@ -30,6 +30,12 @@ import { ApiNotFoundEntity } from '../../entities/api-not-found.entity'; import { CONTENT_TYPE } from '../../common/constants/parameter-constants'; import { ApiInternalServerErrorEntity } from '../../entities/api-internal-server-error.entity'; import { AuthGuard } from '../../common/guards/auth/auth.guard'; +import { + InPersonVisitsEntity, + InPersonVisitsListResponseCaseExample, + InPersonVisitsSingleResponseCaseExample, + NestedInPersonVisitsEntity, +} from '../../entities/in-person-visits.entity'; @Controller('case') @UseGuards(AuthGuard) @@ -39,7 +45,7 @@ export class CasesController { constructor(private readonly casesService: CasesService) {} @UseInterceptors(ClassSerializerInterceptor) - @Get(':id/supportnetwork') + @Get(':id/support-network') @ApiOperation({ description: 'Find all Support Network entries related to a given Case entity by Case id.', @@ -91,8 +97,34 @@ export class CasesController { ); } - // TODO: Add entity and swagger defintions once model is available + @UseInterceptors(ClassSerializerInterceptor) @Get(':id/visits') + @ApiOperation({ + description: + 'Find all In Person Child / Youth Visits related to a given Case entity by Case id.', + }) + @ApiQuery({ name: 'since', required: false }) + @ApiExtraModels(InPersonVisitsEntity, NestedInPersonVisitsEntity) + @ApiOkResponse({ + content: { + [CONTENT_TYPE]: { + schema: { + oneOf: [ + { $ref: getSchemaPath(InPersonVisitsEntity) }, + { $ref: getSchemaPath(NestedInPersonVisitsEntity) }, + ], + }, + examples: { + InPersonVisitsSingleResponse: { + value: InPersonVisitsSingleResponseCaseExample, + }, + InPersonVisitsListResponse: { + value: InPersonVisitsListResponseCaseExample, + }, + }, + }, + }, + }) async getSingleCaseInPersonVisitRecord( @Param( new ValidationPipe({ @@ -111,7 +143,7 @@ export class CasesController { }), ) since?: SinceQueryParams, - ) { + ): Promise { return await this.casesService.getSingleCaseInPersonVisitRecord(id, since); } } diff --git a/src/controllers/cases/cases.service.ts b/src/controllers/cases/cases.service.ts index 890ff05..8f011bb 100644 --- a/src/controllers/cases/cases.service.ts +++ b/src/controllers/cases/cases.service.ts @@ -8,6 +8,10 @@ import { import { IdPathParams } from '../../dto/id-path-params.dto'; import { SinceQueryParams } from '../../dto/since-query-params.dto'; import { InPersonVisitsService } from '../../helpers/in-person-visits/in-person-visits.service'; +import { + InPersonVisitsEntity, + NestedInPersonVisitsEntity, +} from '../../entities/in-person-visits.entity'; @Injectable() export class CasesService { @@ -30,7 +34,7 @@ export class CasesService { async getSingleCaseInPersonVisitRecord( id: IdPathParams, since?: SinceQueryParams, - ) { + ): Promise { return await this.inPersonVisitsService.getSingleInPersonVisitRecord( RecordType.Case, id, diff --git a/src/controllers/incidents/incidents.controller.ts b/src/controllers/incidents/incidents.controller.ts index 28dddd7..2e388fa 100644 --- a/src/controllers/incidents/incidents.controller.ts +++ b/src/controllers/incidents/incidents.controller.ts @@ -40,7 +40,7 @@ export class IncidentsController { constructor(private readonly incidentsService: IncidentsService) {} @UseInterceptors(ClassSerializerInterceptor) - @Get(':id/supportnetwork') + @Get(':id/support-network') @ApiOperation({ description: 'Find all Support Network entries related to a given Incident entity by Incident id.', diff --git a/src/controllers/service-requests/service-requests.controller.ts b/src/controllers/service-requests/service-requests.controller.ts index 23b7e06..d3cbf42 100644 --- a/src/controllers/service-requests/service-requests.controller.ts +++ b/src/controllers/service-requests/service-requests.controller.ts @@ -36,7 +36,7 @@ export class ServiceRequestsController { constructor(private readonly serviceRequestService: ServiceRequestsService) {} @UseInterceptors(ClassSerializerInterceptor) - @Get(':id/supportnetwork') + @Get(':id/support-network') @ApiOperation({ description: 'Find all Support Network entries related to a given Service Request entity by Service Request id.', diff --git a/src/entities/in-person-visits.entity.ts b/src/entities/in-person-visits.entity.ts new file mode 100644 index 0000000..432d230 --- /dev/null +++ b/src/entities/in-person-visits.entity.ts @@ -0,0 +1,98 @@ +import { ApiProperty, ApiSchema } from '@nestjs/swagger'; +import { Exclude, Expose, Type } from 'class-transformer'; + +/* + * Examples + */ +export const InPersonVisitsSingleResponseCaseExample = { + Name: 'Id-here', + 'Visit Description': 'description', + Id: 'Id-here', + Type: 'In Person Child Youth', + 'Date of visit': '11/09/2024 09:33:23', + 'Visit Details Value': 'comment', + 'Parent Id': 'Entity-Id-here', + 'Login Name': 'Idir-here', +}; + +export const InPersonVisitsListResponseCaseExample = { + items: [ + { + ...InPersonVisitsSingleResponseCaseExample, + 'Date of visit': '11/09/2024 10:36:25', + }, + InPersonVisitsSingleResponseCaseExample, + ], +}; + +/* + * Model definitions + */ +@Exclude() +@ApiSchema({ name: 'InPersonVisitsSingleResponse' }) +export class InPersonVisitsEntity { + @ApiProperty({ + example: InPersonVisitsSingleResponseCaseExample['Name'], + }) + @Expose() + Name: string; + + @ApiProperty({ + example: InPersonVisitsSingleResponseCaseExample['Visit Description'], + }) + @Expose() + 'Visit Description': string; + + @ApiProperty({ + example: InPersonVisitsSingleResponseCaseExample['Id'], + }) + @Expose() + Id: string; + + @ApiProperty({ + example: InPersonVisitsSingleResponseCaseExample['Type'], + }) + @Expose() + Type: string; + + @ApiProperty({ + example: InPersonVisitsSingleResponseCaseExample['Date of visit'], + }) + @Expose() + 'Date of visit': string; + + @ApiProperty({ + example: InPersonVisitsSingleResponseCaseExample['Visit Details Value'], + }) + @Expose() + 'Visit Details Value': string; + + @ApiProperty({ + example: InPersonVisitsSingleResponseCaseExample['Parent Id'], + }) + @Expose() + 'Parent Id': string; + + @ApiProperty({ + example: InPersonVisitsSingleResponseCaseExample['Login Name'], + }) + @Expose() + 'Login Name': string; + + constructor(partial: Partial) { + Object.assign(this, partial); + } +} + +@Exclude() +@ApiSchema({ name: 'InPersonVisitsListResponse' }) +export class NestedInPersonVisitsEntity { + @Expose() + @ApiProperty({ type: InPersonVisitsEntity, isArray: true }) + @Type(() => InPersonVisitsEntity) + items: Array; + + constructor(object) { + Object.assign(this, object); + } +} diff --git a/src/external-api/request-preparer/request-preparer.service.spec.ts b/src/external-api/request-preparer/request-preparer.service.spec.ts index 778e4d0..e42f586 100644 --- a/src/external-api/request-preparer/request-preparer.service.spec.ts +++ b/src/external-api/request-preparer/request-preparer.service.spec.ts @@ -1,10 +1,6 @@ import { Test, TestingModule } from '@nestjs/testing'; import { RequestPreparerService } from './request-preparer.service'; import { UtilitiesService } from '../../helpers/utilities/utilities.service'; -import { - RecordEntityMap, - RecordType, -} from '../../common/constants/enumerations'; import { CHILD_LINKS, CONTENT_TYPE, @@ -63,39 +59,30 @@ describe('RequestPreparerService', () => { describe('prepareHeadersAndParams tests', () => { it.each([ - [RecordType.Case, { id: validId }, undefined], - [RecordType.SR, { id: validId }, 'workspace'], + ['spec', undefined], + ['spec', { id: validId }, 'workspace'], ])( 'correctly prepares headers and params with no date parameter', - (type, id, workspace) => { + (baseSearchSpec, workspace) => { const [headers, params] = service.prepareHeadersAndParams( - type, - id, + baseSearchSpec, workspace, ); expect(headers).toEqual({ Accept: CONTENT_TYPE }); expect(params).toEqual({ ViewMode: VIEW_MODE, ChildLinks: CHILD_LINKS, - searchspec: `([Entity Id]="${id.id}" AND [Entity Name]="${RecordEntityMap[type]}")`, + searchspec: baseSearchSpec + ')', workspace: workspace, }); }, ); - it.each([ - [ - RecordType.Case, - { id: validId }, - { since: '2024-02-20' }, - '02/20/2024 00:00:00', - ], - ])( + it.each([['spec', { since: '2024-02-20' }, '02/20/2024 00:00:00']])( 'correctly prepares headers and params with a date parameter', - (type, id, since, expectedDate) => { + (baseSearchSpec, since, expectedDate) => { const [headers, params] = service.prepareHeadersAndParams( - type, - id, + baseSearchSpec, undefined, since, ); @@ -103,7 +90,7 @@ describe('RequestPreparerService', () => { expect(params).toEqual({ ViewMode: VIEW_MODE, ChildLinks: CHILD_LINKS, - searchspec: `([Entity Id]="${id.id}" AND [Entity Name]="${RecordEntityMap[type]}" AND [Updated] > "${expectedDate}")`, + searchspec: `${baseSearchSpec} AND [Updated] > "${expectedDate}")`, }); }, ); diff --git a/src/external-api/request-preparer/request-preparer.service.ts b/src/external-api/request-preparer/request-preparer.service.ts index 9fac57c..7206c23 100644 --- a/src/external-api/request-preparer/request-preparer.service.ts +++ b/src/external-api/request-preparer/request-preparer.service.ts @@ -1,14 +1,9 @@ import { HttpException, HttpStatus, Injectable, Logger } from '@nestjs/common'; -import { - RecordEntityMap, - RecordType, -} from '../../common/constants/enumerations'; import { VIEW_MODE, CHILD_LINKS, CONTENT_TYPE, } from '../../common/constants/parameter-constants'; -import { IdPathParams } from '../../dto/id-path-params.dto'; import { SinceQueryParams } from '../../dto/since-query-params.dto'; import { UtilitiesService } from '../../helpers/utilities/utilities.service'; import { TokenRefresherService } from '../token-refresher/token-refresher.service'; @@ -26,12 +21,11 @@ export class RequestPreparerService { ) {} prepareHeadersAndParams( - type: RecordType, - id: IdPathParams, + baseSearchSpec: string, workspace: string | undefined, since?: SinceQueryParams, ) { - let searchSpec = `([Entity Id]="${id.id}" AND [Entity Name]="${RecordEntityMap[type]}"`; + let searchSpec = baseSearchSpec; let formattedDate: string | undefined; if ( since === undefined || diff --git a/src/helpers/in-person-visits/in-person-visits.service.ts b/src/helpers/in-person-visits/in-person-visits.service.ts index d15fba5..8865169 100644 --- a/src/helpers/in-person-visits/in-person-visits.service.ts +++ b/src/helpers/in-person-visits/in-person-visits.service.ts @@ -4,6 +4,10 @@ import { IdPathParams } from '../../dto/id-path-params.dto'; import { SinceQueryParams } from '../../dto/since-query-params.dto'; import { ConfigService } from '@nestjs/config'; import { RequestPreparerService } from '../../external-api/request-preparer/request-preparer.service'; +import { + NestedInPersonVisitsEntity, + InPersonVisitsEntity, +} from '../../entities/in-person-visits.entity'; @Injectable() export class InPersonVisitsService { @@ -19,15 +23,16 @@ export class InPersonVisitsService { ).replace(/\s/g, '%20'); this.workspace = this.configService.get('workspaces.inPersonVisits'); } + async getSingleInPersonVisitRecord( - type: RecordType, + _type: RecordType, id: IdPathParams, since?: SinceQueryParams, - ) { + ): Promise { + const baseSearchSpec = `([Parent Id]="${id.id}"`; const [headers, params] = this.requestPreparerService.prepareHeadersAndParams( - type, - id, + baseSearchSpec, this.workspace, since, ); @@ -36,7 +41,9 @@ export class InPersonVisitsService { headers, params, ); - // TODO: Pass info to entity constructs once created - return response.data; + if ((response.data as object).hasOwnProperty('items')) { + return new NestedInPersonVisitsEntity(response.data); + } + return new InPersonVisitsEntity(response.data); } } diff --git a/src/helpers/support-network/support-network.service.ts b/src/helpers/support-network/support-network.service.ts index 4ce8dbf..e970aab 100644 --- a/src/helpers/support-network/support-network.service.ts +++ b/src/helpers/support-network/support-network.service.ts @@ -1,6 +1,9 @@ import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; -import { RecordType } from '../../common/constants/enumerations'; +import { + RecordEntityMap, + RecordType, +} from '../../common/constants/enumerations'; import { SupportNetworkEntity, NestedSupportNetworkEntity, @@ -29,10 +32,10 @@ export class SupportNetworkService { id: IdPathParams, since?: SinceQueryParams, ) { + const baseSearchSpec = `([Entity Id]="${id.id}" AND [Entity Name]="${RecordEntityMap[type]}"`; const [headers, params] = this.requestPreparerService.prepareHeadersAndParams( - type, - id, + baseSearchSpec, this.workspace, since, ); From 313924a2dd0d76bda18cae16cbb4ced4073cebc6 Mon Sep 17 00:00:00 2001 From: hannah-macdonald1 Date: Tue, 5 Nov 2024 10:55:40 -0700 Subject: [PATCH 05/18] add field name parameter and unit tests --- src/configuration/configuration.ts | 4 + .../cases/cases.controller.spec.ts | 31 +++++++ src/controllers/cases/cases.service.spec.ts | 36 +++++++++ .../request-preparer.service.spec.ts | 2 + .../request-preparer.service.ts | 5 +- .../in-person-visits.service.spec.ts | 80 +++++++++++++++++++ .../in-person-visits.service.ts | 5 ++ .../support-network.service.ts | 5 ++ 8 files changed, 167 insertions(+), 1 deletion(-) diff --git a/src/configuration/configuration.ts b/src/configuration/configuration.ts index 8ee0fa8..555bb14 100644 --- a/src/configuration/configuration.ts +++ b/src/configuration/configuration.ts @@ -31,4 +31,8 @@ export default () => ({ supportNetwork: undefined, inPersonVisits: undefined, }, + sinceFieldNamr: { + supportNetwork: 'Updated', + inPersonVisits: undefined, + }, }); diff --git a/src/controllers/cases/cases.controller.spec.ts b/src/controllers/cases/cases.controller.spec.ts index 372fe54..c1e8661 100644 --- a/src/controllers/cases/cases.controller.spec.ts +++ b/src/controllers/cases/cases.controller.spec.ts @@ -16,6 +16,10 @@ import { SupportNetworkService } from '../../helpers/support-network/support-net import { UtilitiesService } from '../../helpers/utilities/utilities.service'; import { InPersonVisitsService } from '../../helpers/in-person-visits/in-person-visits.service'; import { RequestPreparerService } from '../../external-api/request-preparer/request-preparer.service'; +import { + InPersonVisitsEntity, + InPersonVisitsSingleResponseCaseExample, +} from '../../entities/in-person-visits.entity'; describe('CasesController', () => { let controller: CasesController; @@ -74,4 +78,31 @@ describe('CasesController', () => { }, ); }); + + describe('getSingleCaseInPersonVisitRecord tests', () => { + it.each([ + [ + InPersonVisitsSingleResponseCaseExample, + { id: 'test' } as IdPathParams, + { since: '2020-02-02' } as SinceQueryParams, + ], + ])( + 'should return single values given good input', + async (data, idPathParams, sinceQueryParams) => { + const casesServiceSpy = jest + .spyOn(casesService, 'getSingleCaseInPersonVisitRecord') + .mockReturnValueOnce(Promise.resolve(new InPersonVisitsEntity(data))); + + const result = await controller.getSingleCaseInPersonVisitRecord( + idPathParams, + sinceQueryParams, + ); + expect(casesServiceSpy).toHaveBeenCalledWith( + idPathParams, + sinceQueryParams, + ); + expect(result).toEqual(new InPersonVisitsEntity(data)); + }, + ); + }); }); diff --git a/src/controllers/cases/cases.service.spec.ts b/src/controllers/cases/cases.service.spec.ts index 5d2c77b..bda6a6f 100644 --- a/src/controllers/cases/cases.service.spec.ts +++ b/src/controllers/cases/cases.service.spec.ts @@ -15,10 +15,15 @@ import { RecordType } from '../../common/constants/enumerations'; import { TokenRefresherService } from '../../external-api/token-refresher/token-refresher.service'; import { InPersonVisitsService } from '../../helpers/in-person-visits/in-person-visits.service'; import { RequestPreparerService } from '../../external-api/request-preparer/request-preparer.service'; +import { + InPersonVisitsEntity, + InPersonVisitsSingleResponseCaseExample, +} from '../../entities/in-person-visits.entity'; describe('CasesService', () => { let service: CasesService; let supportNetworkService: SupportNetworkService; + let inPersonVisitsService: InPersonVisitsService; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ @@ -44,6 +49,9 @@ describe('CasesService', () => { supportNetworkService = module.get( SupportNetworkService, ); + inPersonVisitsService = module.get( + InPersonVisitsService, + ); }); it('should be defined', () => { @@ -81,4 +89,32 @@ describe('CasesService', () => { }, ); }); + + describe('getSingleCaseInPersonVisitRecord tests', () => { + it.each([ + [ + InPersonVisitsSingleResponseCaseExample, + { id: 'test' } as IdPathParams, + { since: '2024-12-01' } as SinceQueryParams, + ], + ])( + 'should return single values given good input', + async (data, idPathParams, sinceQueryParams) => { + const InPersonVisitsSpy = jest + .spyOn(inPersonVisitsService, 'getSingleInPersonVisitRecord') + .mockReturnValueOnce(Promise.resolve(new InPersonVisitsEntity(data))); + + const result = await service.getSingleCaseInPersonVisitRecord( + idPathParams, + sinceQueryParams, + ); + expect(InPersonVisitsSpy).toHaveBeenCalledWith( + RecordType.Case, + idPathParams, + sinceQueryParams, + ); + expect(result).toEqual(new InPersonVisitsEntity(data)); + }, + ); + }); }); diff --git a/src/external-api/request-preparer/request-preparer.service.spec.ts b/src/external-api/request-preparer/request-preparer.service.spec.ts index e42f586..925d0c6 100644 --- a/src/external-api/request-preparer/request-preparer.service.spec.ts +++ b/src/external-api/request-preparer/request-preparer.service.spec.ts @@ -67,6 +67,7 @@ describe('RequestPreparerService', () => { const [headers, params] = service.prepareHeadersAndParams( baseSearchSpec, workspace, + '', ); expect(headers).toEqual({ Accept: CONTENT_TYPE }); expect(params).toEqual({ @@ -84,6 +85,7 @@ describe('RequestPreparerService', () => { const [headers, params] = service.prepareHeadersAndParams( baseSearchSpec, undefined, + 'Updated', since, ); expect(headers).toEqual({ Accept: CONTENT_TYPE }); diff --git a/src/external-api/request-preparer/request-preparer.service.ts b/src/external-api/request-preparer/request-preparer.service.ts index 7206c23..58b13cc 100644 --- a/src/external-api/request-preparer/request-preparer.service.ts +++ b/src/external-api/request-preparer/request-preparer.service.ts @@ -23,11 +23,13 @@ export class RequestPreparerService { prepareHeadersAndParams( baseSearchSpec: string, workspace: string | undefined, + sinceFieldName: string | undefined, since?: SinceQueryParams, ) { let searchSpec = baseSearchSpec; let formattedDate: string | undefined; if ( + sinceFieldName === undefined || since === undefined || typeof since.since !== 'string' || (formattedDate = this.utilitiesService.convertISODateToUpstreamFormat( @@ -36,7 +38,8 @@ export class RequestPreparerService { ) { searchSpec = searchSpec + `)`; } else { - searchSpec = searchSpec + ` AND [Updated] > "${formattedDate}")`; + searchSpec = + searchSpec + ` AND [${sinceFieldName}] > "${formattedDate}")`; } const params = { ViewMode: VIEW_MODE, diff --git a/src/helpers/in-person-visits/in-person-visits.service.spec.ts b/src/helpers/in-person-visits/in-person-visits.service.spec.ts index c41e8e9..6b70b9e 100644 --- a/src/helpers/in-person-visits/in-person-visits.service.spec.ts +++ b/src/helpers/in-person-visits/in-person-visits.service.spec.ts @@ -6,9 +6,20 @@ import { UtilitiesService } from '../utilities/utilities.service'; import { TokenRefresherService } from '../../external-api/token-refresher/token-refresher.service'; import { CACHE_MANAGER } from '@nestjs/cache-manager'; import { HttpService } from '@nestjs/axios'; +import { AxiosResponse } from 'axios'; +import { IdPathParams } from '../../dto/id-path-params.dto'; +import { RecordType } from '../../common/constants/enumerations'; +import { SinceQueryParams } from '../../dto/since-query-params.dto'; +import { + InPersonVisitsEntity, + InPersonVisitsListResponseCaseExample, + InPersonVisitsSingleResponseCaseExample, + NestedInPersonVisitsEntity, +} from '../../entities/in-person-visits.entity'; describe('InPersonVisitsService', () => { let service: InPersonVisitsService; + let requestPreparerService: RequestPreparerService; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ @@ -31,9 +42,78 @@ describe('InPersonVisitsService', () => { }).compile(); service = module.get(InPersonVisitsService); + requestPreparerService = module.get( + RequestPreparerService, + ); }); it('should be defined', () => { expect(service).toBeDefined(); }); + + describe('getSingleInPersonVisitRecord tests', () => { + it.each([ + [ + InPersonVisitsSingleResponseCaseExample, + RecordType.Case, + { id: 'test' } as IdPathParams, + undefined, + ], + [ + InPersonVisitsListResponseCaseExample.items[0], + RecordType.Case, + { id: 'test' } as IdPathParams, + { since: '2024-12-24' } as SinceQueryParams, + ], + ])( + 'should return single values given good input', + async (data, recordType, idPathParams, sinceQueryParams) => { + const spy = jest + .spyOn(requestPreparerService, 'sendGetRequest') + .mockResolvedValueOnce({ + data: data, + headers: {}, + status: 200, + statusText: 'OK', + } as AxiosResponse); + + const result = await service.getSingleInPersonVisitRecord( + recordType, + idPathParams, + sinceQueryParams, + ); + expect(spy).toHaveBeenCalledTimes(1); + expect(result).toEqual(new InPersonVisitsEntity(data)); + }, + ); + + it.each([ + [ + InPersonVisitsListResponseCaseExample, + RecordType.Case, + { id: 'test' } as IdPathParams, + undefined, + ], + ])( + 'should return list values given good input', + async (data, recordType, idPathParams, sinceQueryParams) => { + const spy = jest + .spyOn(requestPreparerService, 'sendGetRequest') + .mockResolvedValueOnce({ + data: data, + headers: {}, + status: 200, + statusText: 'OK', + } as AxiosResponse); + + const result = await service.getSingleInPersonVisitRecord( + recordType, + idPathParams, + sinceQueryParams, + ); + expect(spy).toHaveBeenCalledTimes(1); + expect(result).toEqual(new NestedInPersonVisitsEntity(data)); + }, + ); + }); }); diff --git a/src/helpers/in-person-visits/in-person-visits.service.ts b/src/helpers/in-person-visits/in-person-visits.service.ts index 8865169..46a47b8 100644 --- a/src/helpers/in-person-visits/in-person-visits.service.ts +++ b/src/helpers/in-person-visits/in-person-visits.service.ts @@ -13,6 +13,7 @@ import { export class InPersonVisitsService { url: string; workspace: string | undefined; + sinceFieldName: string | undefined; constructor( private readonly configService: ConfigService, private readonly requestPreparerService: RequestPreparerService, @@ -22,6 +23,9 @@ export class InPersonVisitsService { this.configService.get('IN_PERSON_VISITS_ENDPOINT') ).replace(/\s/g, '%20'); this.workspace = this.configService.get('workspaces.inPersonVisits'); + this.sinceFieldName = this.configService.get( + 'sinceFieldName.supportNetwork', + ); } async getSingleInPersonVisitRecord( @@ -34,6 +38,7 @@ export class InPersonVisitsService { this.requestPreparerService.prepareHeadersAndParams( baseSearchSpec, this.workspace, + this.sinceFieldName, since, ); const response = await this.requestPreparerService.sendGetRequest( diff --git a/src/helpers/support-network/support-network.service.ts b/src/helpers/support-network/support-network.service.ts index e970aab..3723300 100644 --- a/src/helpers/support-network/support-network.service.ts +++ b/src/helpers/support-network/support-network.service.ts @@ -16,6 +16,7 @@ import { RequestPreparerService } from '../../external-api/request-preparer/requ export class SupportNetworkService { url: string; workspace: string | undefined; + sinceFieldName: string | undefined; constructor( private readonly configService: ConfigService, private readonly requestPreparerService: RequestPreparerService, @@ -25,6 +26,9 @@ export class SupportNetworkService { this.configService.get('SUPPORT_NETWORK_ENDPOINT') ).replace(/\s/g, '%20'); this.workspace = this.configService.get('workspaces.supportNetwork'); + this.sinceFieldName = this.configService.get( + 'sinceFieldName.supportNetwork', + ); } async getSingleSupportNetworkInformationRecord( @@ -37,6 +41,7 @@ export class SupportNetworkService { this.requestPreparerService.prepareHeadersAndParams( baseSearchSpec, this.workspace, + this.sinceFieldName, since, ); const response = await this.requestPreparerService.sendGetRequest( From 4b27e981c82fc2a8ca30f7ecbd11eeaa75964375 Mon Sep 17 00:00:00 2001 From: hannah-macdonald1 Date: Tue, 5 Nov 2024 11:06:36 -0700 Subject: [PATCH 06/18] add env variable to deployment --- helm/templates/deployment.yaml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/helm/templates/deployment.yaml b/helm/templates/deployment.yaml index aff36b0..db5edf5 100644 --- a/helm/templates/deployment.yaml +++ b/helm/templates/deployment.yaml @@ -77,6 +77,11 @@ spec: secretKeyRef: name: visitz-api key: CLIENT_SECRET + - name: IN_PERSON_VISITS_ENDPOINT + valueFrom: + secretKeyRef: + name: visitz-api + key: IN_PERSON_VISITS_ENDPOINT - name: VPI_APP_LABEL value: {{ .Values.vpiAppBuildLabel.version }} restartPolicy: Always From a88836bdeeb807a1c9775c950f87b209fe6a02bc Mon Sep 17 00:00:00 2001 From: hannah-macdonald1 Date: Tue, 5 Nov 2024 11:22:49 -0700 Subject: [PATCH 07/18] turn off pagination --- src/common/constants/parameter-constants.ts | 3 ++- .../request-preparer/request-preparer.service.spec.ts | 3 +++ src/external-api/request-preparer/request-preparer.service.ts | 2 ++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/common/constants/parameter-constants.ts b/src/common/constants/parameter-constants.ts index e0c11bb..1f1b601 100644 --- a/src/common/constants/parameter-constants.ts +++ b/src/common/constants/parameter-constants.ts @@ -1,7 +1,8 @@ const VIEW_MODE = 'Organization'; const CHILD_LINKS = 'None'; const CONTENT_TYPE = 'application/json'; +const PAGINATION = 'N'; const idRegex = /[0-9\-A-Za-z]+/; -export { VIEW_MODE, CHILD_LINKS, CONTENT_TYPE, idRegex }; +export { VIEW_MODE, CHILD_LINKS, CONTENT_TYPE, PAGINATION, idRegex }; diff --git a/src/external-api/request-preparer/request-preparer.service.spec.ts b/src/external-api/request-preparer/request-preparer.service.spec.ts index 925d0c6..d3fcbfb 100644 --- a/src/external-api/request-preparer/request-preparer.service.spec.ts +++ b/src/external-api/request-preparer/request-preparer.service.spec.ts @@ -4,6 +4,7 @@ import { UtilitiesService } from '../../helpers/utilities/utilities.service'; import { CHILD_LINKS, CONTENT_TYPE, + PAGINATION, VIEW_MODE, } from '../../common/constants/parameter-constants'; import { TokenRefresherService } from '../token-refresher/token-refresher.service'; @@ -75,6 +76,7 @@ describe('RequestPreparerService', () => { ChildLinks: CHILD_LINKS, searchspec: baseSearchSpec + ')', workspace: workspace, + pagination: PAGINATION, }); }, ); @@ -93,6 +95,7 @@ describe('RequestPreparerService', () => { ViewMode: VIEW_MODE, ChildLinks: CHILD_LINKS, searchspec: `${baseSearchSpec} AND [Updated] > "${expectedDate}")`, + pagination: PAGINATION, }); }, ); diff --git a/src/external-api/request-preparer/request-preparer.service.ts b/src/external-api/request-preparer/request-preparer.service.ts index 58b13cc..941b605 100644 --- a/src/external-api/request-preparer/request-preparer.service.ts +++ b/src/external-api/request-preparer/request-preparer.service.ts @@ -3,6 +3,7 @@ import { VIEW_MODE, CHILD_LINKS, CONTENT_TYPE, + PAGINATION, } from '../../common/constants/parameter-constants'; import { SinceQueryParams } from '../../dto/since-query-params.dto'; import { UtilitiesService } from '../../helpers/utilities/utilities.service'; @@ -45,6 +46,7 @@ export class RequestPreparerService { ViewMode: VIEW_MODE, ChildLinks: CHILD_LINKS, searchspec: searchSpec, + pagination: PAGINATION, }; if (typeof workspace !== 'undefined') { params['workspace'] = workspace; From 2c328c01b6442144f9a7b673ec77a60c2efb45af Mon Sep 17 00:00:00 2001 From: hannah-macdonald1 Date: Tue, 5 Nov 2024 11:53:32 -0700 Subject: [PATCH 08/18] try to auto rollout pod --- .github/workflows/build-and-push-ghcr.yaml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/build-and-push-ghcr.yaml b/.github/workflows/build-and-push-ghcr.yaml index ce8226a..71288ab 100644 --- a/.github/workflows/build-and-push-ghcr.yaml +++ b/.github/workflows/build-and-push-ghcr.yaml @@ -99,3 +99,9 @@ jobs: --namespace ${{ secrets.OPENSHIFT_NAMESPACE }} \ --set image.repository=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} \ --set image.tag=${{ needs.build_and_push.outputs.image_tag }} + + - name: Restart Pod after image update + run: | + oc rollout restart deployment \ + --namespace ${{ secrets.OPENSHIFT_NAMESPACE }} \ + --selector=app.kubernetes.io/name=vpi-app From c98a378bee10e5c31ffc9258dd03d509ddca72b6 Mon Sep 17 00:00:00 2001 From: hannah-macdonald1 Date: Tue, 5 Nov 2024 15:15:27 -0700 Subject: [PATCH 09/18] changing name of variable to be independent of environment --- .env.example | 3 ++- helm/templates/deployment.yaml | 5 +++++ src/common/guards/auth/auth.guard.spec.ts | 16 ++++++++++------ src/common/guards/auth/auth.guard.ts | 6 +++--- src/configuration/configuration.ts | 1 + 5 files changed, 21 insertions(+), 10 deletions(-) diff --git a/.env.example b/.env.example index 09804f5..8b6d5b8 100644 --- a/.env.example +++ b/.env.example @@ -11,4 +11,5 @@ IN_PERSON_VISITS_ENDPOINT=/endpoint/path/here CASE_ENDPOINT=/endpoint/path/here INCIDENT_ENDPOINT=/endpoint/path/here SR_ENDPOINT=/endpoint/path/here -MEMO_ENDPOINT=/endpoint/path/here \ No newline at end of file +MEMO_ENDPOINT=/endpoint/path/here +SKIP_AUTH_GUARD=false \ No newline at end of file diff --git a/helm/templates/deployment.yaml b/helm/templates/deployment.yaml index db5edf5..7282846 100644 --- a/helm/templates/deployment.yaml +++ b/helm/templates/deployment.yaml @@ -84,4 +84,9 @@ spec: key: IN_PERSON_VISITS_ENDPOINT - name: VPI_APP_LABEL value: {{ .Values.vpiAppBuildLabel.version }} + - name: SKIP_AUTH_GUARD + valueFrom: + secretKeyRef: + name: visitz-api + key: SKIP_AUTH_GUARD restartPolicy: Always diff --git a/src/common/guards/auth/auth.guard.spec.ts b/src/common/guards/auth/auth.guard.spec.ts index edfcca5..6b04a80 100644 --- a/src/common/guards/auth/auth.guard.spec.ts +++ b/src/common/guards/auth/auth.guard.spec.ts @@ -43,7 +43,7 @@ describe('AuthGuard', () => { useValue: { get: jest.fn((key: string) => { const lookup = { - NODE_ENV: 'test', + skipAuthGuard: true, }; return lookup[key]; }), @@ -69,18 +69,23 @@ describe('AuthGuard', () => { }); describe('canActivate tests', () => { - it('should always return true in non-production environment', async () => { + it('should always return true when skipping', async () => { const authSpy = jest .spyOn(service, 'getRecordAndValidate') .mockResolvedValueOnce(false); const guardSpy = jest.spyOn(AuthGuard.prototype, 'canActivate'); - const isAuthed = await guard.canActivate({} as ExecutionContext); + const execContext = { + switchToHttp: () => ({ + getRequest: () => getMockReq(), + }), + }; + const isAuthed = await guard.canActivate(execContext as ExecutionContext); expect(authSpy).toHaveBeenCalledTimes(0); expect(guardSpy).toHaveBeenCalledTimes(1); expect(isAuthed).toBe(true); }); - it('should return the result of getRecordAndValidate in a production environment', async () => { + it('should return the result of getRecordAndValidate when not skipping', async () => { const module: TestingModule = await Test.createTestingModule({ providers: [ AuthService, @@ -92,7 +97,7 @@ describe('AuthGuard', () => { useValue: { get: jest.fn((key: string) => { const lookup = { - NODE_ENV: 'production', + skipAuthGuard: false, }; return lookup[key]; }), @@ -119,7 +124,6 @@ describe('AuthGuard', () => { getRequest: () => getMockReq(), }), }; - const isAuthed = await guard.canActivate(execContext); expect(authSpy).toHaveBeenCalledTimes(1); expect(guardSpy).toHaveBeenCalledTimes(1); diff --git a/src/common/guards/auth/auth.guard.ts b/src/common/guards/auth/auth.guard.ts index 174b652..b9ec944 100644 --- a/src/common/guards/auth/auth.guard.ts +++ b/src/common/guards/auth/auth.guard.ts @@ -5,18 +5,18 @@ import { AuthService } from './auth.service'; @Injectable() export class AuthGuard implements CanActivate { - environment; + skip; constructor( private readonly authService: AuthService, private readonly configService: ConfigService, ) { - this.environment = this.configService.get('NODE_ENV'); + this.skip = this.configService.get('skipAuthGuard'); } canActivate( context: ExecutionContext, ): boolean | Promise | Observable { - if (this.environment !== 'production') { + if (this.skip) { // skip for local development return true; } diff --git a/src/configuration/configuration.ts b/src/configuration/configuration.ts index 555bb14..b9032ad 100644 --- a/src/configuration/configuration.ts +++ b/src/configuration/configuration.ts @@ -35,4 +35,5 @@ export default () => ({ supportNetwork: 'Updated', inPersonVisits: undefined, }, + skipAuthGuard: process.env.SKIP_AUTH_GUARD === 'true', }); From 17ee63adcdc2a9528cc991f7abc1a4542ff95d30 Mon Sep 17 00:00:00 2001 From: hannah-macdonald1 Date: Tue, 5 Nov 2024 16:13:57 -0700 Subject: [PATCH 10/18] change view mode to catalog --- src/common/constants/parameter-constants.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/common/constants/parameter-constants.ts b/src/common/constants/parameter-constants.ts index 1f1b601..f60067b 100644 --- a/src/common/constants/parameter-constants.ts +++ b/src/common/constants/parameter-constants.ts @@ -1,4 +1,4 @@ -const VIEW_MODE = 'Organization'; +const VIEW_MODE = 'Catalog'; const CHILD_LINKS = 'None'; const CONTENT_TYPE = 'application/json'; const PAGINATION = 'N'; From 19b263b4f8b58a3c3812cd5477a2356e80dbd645 Mon Sep 17 00:00:00 2001 From: hannah-macdonald1 Date: Tue, 5 Nov 2024 16:24:35 -0700 Subject: [PATCH 11/18] fix auth header name --- src/common/guards/auth/auth.service.spec.ts | 4 ++-- src/common/guards/auth/auth.service.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/common/guards/auth/auth.service.spec.ts b/src/common/guards/auth/auth.service.spec.ts index 38f67f3..8190e7e 100644 --- a/src/common/guards/auth/auth.service.spec.ts +++ b/src/common/guards/auth/auth.service.spec.ts @@ -96,7 +96,7 @@ describe('AuthService', () => { path: validPath, header: jest.fn((key: string): string => { const headerVal: { [key: string]: string } = { - idir_username: testIdir, + 'x-idir-username': testIdir, }; return headerVal[key]; }), @@ -109,7 +109,7 @@ describe('AuthService', () => { it.each([ [{}, undefined, 0], - [{ idir_username: testIdir }, null, 1], + [{ 'x-idir-username': testIdir }, null, 1], ])( 'should return false with invalid record', async (headers, cacheReturn, cacheSpyCallTimes) => { diff --git a/src/common/guards/auth/auth.service.ts b/src/common/guards/auth/auth.service.ts index 72c64d8..602f6c2 100644 --- a/src/common/guards/auth/auth.service.ts +++ b/src/common/guards/auth/auth.service.ts @@ -36,7 +36,7 @@ export class AuthService { async getRecordAndValidate(req: Request): Promise { let idir: string, id: string, recordType: RecordType; try { - idir = req.header('idir_username').trim(); + idir = req.header('x-idir-username').trim(); [id, recordType] = this.grabRecordInfoFromPath(req.path); } catch (error: any) { this.logger.error({ error }); From 9fa00e728611cfde21f2d6a69b4efa89190bcab2 Mon Sep 17 00:00:00 2001 From: hannah-macdonald1 Date: Tue, 5 Nov 2024 16:52:36 -0700 Subject: [PATCH 12/18] fixing async bug --- src/common/guards/auth/auth.guard.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/common/guards/auth/auth.guard.ts b/src/common/guards/auth/auth.guard.ts index b9ec944..2dbe42b 100644 --- a/src/common/guards/auth/auth.guard.ts +++ b/src/common/guards/auth/auth.guard.ts @@ -1,6 +1,5 @@ import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; -import { Observable } from 'rxjs'; import { AuthService } from './auth.service'; @Injectable() @@ -13,14 +12,12 @@ export class AuthGuard implements CanActivate { this.skip = this.configService.get('skipAuthGuard'); } - canActivate( - context: ExecutionContext, - ): boolean | Promise | Observable { + async canActivate(context: ExecutionContext): Promise { if (this.skip) { // skip for local development return true; } const request = context.switchToHttp().getRequest(); - return this.authService.getRecordAndValidate(request); + return await this.authService.getRecordAndValidate(request); } } From 33531827e3ceb5eb4836ea2af93d346b6b4f2e3c Mon Sep 17 00:00:00 2001 From: hannah-macdonald1 Date: Mon, 4 Nov 2024 13:44:40 -0700 Subject: [PATCH 13/18] added in person visits stub --- src/configuration/configuration.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/configuration/configuration.ts b/src/configuration/configuration.ts index b9032ad..4553a7a 100644 --- a/src/configuration/configuration.ts +++ b/src/configuration/configuration.ts @@ -36,4 +36,8 @@ export default () => ({ inPersonVisits: undefined, }, skipAuthGuard: process.env.SKIP_AUTH_GUARD === 'true', + workspaces: { + supportNetwork: undefined, + inPersonVisits: 'int_lab', + }, }); From 163d1abd3b45dc6dae9be6e4ba1ffba454b97c97 Mon Sep 17 00:00:00 2001 From: hannah-macdonald1 Date: Mon, 4 Nov 2024 16:20:04 -0700 Subject: [PATCH 14/18] Swagger definitions and yaml updates --- helm/templates/deployment.yaml | 2 ++ src/configuration/configuration.ts | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/helm/templates/deployment.yaml b/helm/templates/deployment.yaml index 7282846..776ebf5 100644 --- a/helm/templates/deployment.yaml +++ b/helm/templates/deployment.yaml @@ -89,4 +89,6 @@ spec: secretKeyRef: name: visitz-api key: SKIP_AUTH_GUARD + - name: VPI_APP_LABEL + value: {{ .Values.vpiAppBuildLabel.version }} restartPolicy: Always diff --git a/src/configuration/configuration.ts b/src/configuration/configuration.ts index 4553a7a..f96b816 100644 --- a/src/configuration/configuration.ts +++ b/src/configuration/configuration.ts @@ -38,6 +38,6 @@ export default () => ({ skipAuthGuard: process.env.SKIP_AUTH_GUARD === 'true', workspaces: { supportNetwork: undefined, - inPersonVisits: 'int_lab', + inPersonVisits: undefined, }, }); From ab2f3b95e811577c5ea4e984c2d09997a5666a7c Mon Sep 17 00:00:00 2001 From: hannah-macdonald1 Date: Tue, 5 Nov 2024 11:06:36 -0700 Subject: [PATCH 15/18] add env variable to deployment --- helm/templates/deployment.yaml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/helm/templates/deployment.yaml b/helm/templates/deployment.yaml index 776ebf5..a4a5761 100644 --- a/helm/templates/deployment.yaml +++ b/helm/templates/deployment.yaml @@ -89,6 +89,11 @@ spec: secretKeyRef: name: visitz-api key: SKIP_AUTH_GUARD + - name: IN_PERSON_VISITS_ENDPOINT + valueFrom: + secretKeyRef: + name: visitz-api + key: IN_PERSON_VISITS_ENDPOINT - name: VPI_APP_LABEL value: {{ .Values.vpiAppBuildLabel.version }} restartPolicy: Always From 5402b4476e8070b283706ec5ca7a2eeb8273880f Mon Sep 17 00:00:00 2001 From: hannah-macdonald1 Date: Wed, 6 Nov 2024 09:40:05 -0700 Subject: [PATCH 16/18] fix merge --- src/configuration/configuration.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/configuration/configuration.ts b/src/configuration/configuration.ts index f96b816..b9032ad 100644 --- a/src/configuration/configuration.ts +++ b/src/configuration/configuration.ts @@ -36,8 +36,4 @@ export default () => ({ inPersonVisits: undefined, }, skipAuthGuard: process.env.SKIP_AUTH_GUARD === 'true', - workspaces: { - supportNetwork: undefined, - inPersonVisits: undefined, - }, }); From 5d332c547580bfadf2e93b245034166c78ef4b27 Mon Sep 17 00:00:00 2001 From: hannah-macdonald1 Date: Wed, 6 Nov 2024 09:49:39 -0700 Subject: [PATCH 17/18] change status to 204 on not found --- .github/workflows/build-and-push-ghcr.yaml | 2 +- .../request-preparer.service.spec.ts | 24 ++++++++++++++++++- .../request-preparer.service.ts | 9 +------ 3 files changed, 25 insertions(+), 10 deletions(-) diff --git a/.github/workflows/build-and-push-ghcr.yaml b/.github/workflows/build-and-push-ghcr.yaml index 71288ab..a534c39 100644 --- a/.github/workflows/build-and-push-ghcr.yaml +++ b/.github/workflows/build-and-push-ghcr.yaml @@ -77,7 +77,7 @@ jobs: value: ${{ secrets.APS_NAMESPACE }} commitChange: false - - name: 'YAML poke: Set APS namespace' + - name: 'YAML poke: Set build number' uses: fjogeleit/yaml-update-action@v0.15.0 with: valueFile: 'helm/values.yaml' diff --git a/src/external-api/request-preparer/request-preparer.service.spec.ts b/src/external-api/request-preparer/request-preparer.service.spec.ts index d3fcbfb..bb57f97 100644 --- a/src/external-api/request-preparer/request-preparer.service.spec.ts +++ b/src/external-api/request-preparer/request-preparer.service.spec.ts @@ -116,7 +116,7 @@ describe('RequestPreparerService', () => { expect(result.data).toEqual({}); }); - it.each([[404], [500]])( + it.each([[500]])( `Should return HttpException with matching status on axios error`, async (status) => { const spy = jest.spyOn(httpService, 'get').mockImplementation(() => { @@ -142,6 +142,28 @@ describe('RequestPreparerService', () => { }, ); + it('Should return HttpException with status 204 on 404 from upstream', async () => { + const spy = jest.spyOn(httpService, 'get').mockImplementation(() => { + throw new AxiosError( + 'Axios Error', + '404', + {} as InternalAxiosRequestConfig, + {}, + { + data: {}, + status: 404, + statusText: '', + headers: {} as RawAxiosRequestHeaders, + config: {} as InternalAxiosRequestConfig, + }, + ); + }); + await expect( + service.sendGetRequest('url', {}, {}), + ).rejects.toHaveProperty('status', 204); + expect(spy).toHaveBeenCalledTimes(1); + }); + it('Should return HttpException with status 500 on bearer token undefined', async () => { const spy = jest .spyOn(tokenRefresherService, 'refreshUpstreamBearerToken') diff --git a/src/external-api/request-preparer/request-preparer.service.ts b/src/external-api/request-preparer/request-preparer.service.ts index 941b605..ba59b58 100644 --- a/src/external-api/request-preparer/request-preparer.service.ts +++ b/src/external-api/request-preparer/request-preparer.service.ts @@ -73,14 +73,7 @@ export class RequestPreparerService { if (error instanceof AxiosError) { this.logger.error(error.message, error.stack, error.cause); if (error.status === 404) { - throw new HttpException( - { - status: HttpStatus.NOT_FOUND, - error: 'There is no data for the requested resource', - }, - HttpStatus.NOT_FOUND, - { cause: error }, - ); + throw new HttpException({}, HttpStatus.NO_CONTENT, { cause: error }); } } else { this.logger.error(error); From be016b33d7966770b07161531afad442e4957cb9 Mon Sep 17 00:00:00 2001 From: hannah-macdonald1 Date: Wed, 6 Nov 2024 11:50:26 -0700 Subject: [PATCH 18/18] Add build information to logs and parameterize env var names --- src/app.module.ts | 23 +++++++++++++++++-- src/common/constants/upstream-constants.ts | 9 ++++++++ src/common/guards/auth/auth.service.ts | 16 +++++++++---- src/configuration/configuration.ts | 6 +++++ .../request-preparer.service.ts | 16 ++++++++++--- .../token-refresher.service.ts | 9 +++++++- .../in-person-visits.service.ts | 8 +++++-- .../support-network.service.ts | 8 +++++-- 8 files changed, 80 insertions(+), 15 deletions(-) create mode 100644 src/common/constants/upstream-constants.ts diff --git a/src/app.module.ts b/src/app.module.ts index 6e7ad97..d91f5f7 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -1,5 +1,5 @@ import { Module } from '@nestjs/common'; -import { ConfigModule } from '@nestjs/config'; +import { ConfigModule, ConfigService } from '@nestjs/config'; import { AppController } from './app.controller'; import { AppService } from './app.service'; import configuration from './configuration/configuration'; @@ -15,7 +15,26 @@ import { ExternalApiModule } from './external-api/external-api.module'; ConfigModule.forRoot({ load: [configuration], }), - LoggerModule.forRoot(), + LoggerModule.forRootAsync({ + imports: [ConfigModule], + inject: [ConfigService], + useFactory: (configService: ConfigService) => ({ + pinoHttp: { + customSuccessObject: (req, res, loggableObject) => { + return { + ...loggableObject, + buildNumber: configService.get('buildInfo.buildNumber'), + }; + }, + customErrorObject: (req, res, loggableObject) => { + return { + ...loggableObject, + buildNumber: configService.get('buildInfo.buildNumber'), + }; + }, + }, + }), + }), CacheModule.register({ isGlobal: true }), CommonModule, ControllersModule, diff --git a/src/common/constants/upstream-constants.ts b/src/common/constants/upstream-constants.ts new file mode 100644 index 0000000..fa6b755 --- /dev/null +++ b/src/common/constants/upstream-constants.ts @@ -0,0 +1,9 @@ +const baseUrlEnvVarName = 'UPSTREAM_BASE_URL'; +const supportNetworkEndpointEnvVarName = 'SUPPORT_NETWORK_ENDPOINT'; +const inPersonVisitsEndpointEnvVarName = 'IN_PERSON_VISITS_ENDPOINT'; + +export { + baseUrlEnvVarName, + supportNetworkEndpointEnvVarName, + inPersonVisitsEndpointEnvVarName, +}; diff --git a/src/common/guards/auth/auth.service.ts b/src/common/guards/auth/auth.service.ts index 602f6c2..bd3109f 100644 --- a/src/common/guards/auth/auth.service.ts +++ b/src/common/guards/auth/auth.service.ts @@ -15,11 +15,13 @@ import { 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'; @Injectable() export class AuthService { cacheTime: number; baseUrl: string; + buildNumber: string; private readonly logger = new Logger(AuthService.name); constructor( @@ -30,7 +32,8 @@ export class AuthService { private readonly tokenRefresherService: TokenRefresherService, ) { this.cacheTime = this.configService.get('recordCache.cacheTtlMs'); - this.baseUrl = this.configService.get('UPSTREAM_BASE_URL'); + this.baseUrl = this.configService.get(baseUrlEnvVarName); + this.buildNumber = this.configService.get('buildInfo.buildNumber'); } async getRecordAndValidate(req: Request): Promise { @@ -45,8 +48,6 @@ export class AuthService { const key = `${id}|${recordType}`; let upstreamResult: string | null | undefined = await this.cacheManager.get(key); - // TODO: Remove this console log once guard is verified working - this.logger.log(`Cache result: ${upstreamResult}`); if (upstreamResult === undefined) { upstreamResult = await this.getAssignedIdirUpstream(id, recordType); @@ -114,9 +115,14 @@ export class AuthService { return idir; } catch (error) { if (error instanceof AxiosError) { - this.logger.error(error.message, error.stack, error.cause); + this.logger.error({ + msg: error.message, + stack: error.stack, + cause: error.cause, + buildNumber: this.buildNumber, + }); } else { - this.logger.error(error); + this.logger.error({ error, buildNumber: this.buildNumber }); } } return null; diff --git a/src/configuration/configuration.ts b/src/configuration/configuration.ts index 9583d3e..36311b1 100644 --- a/src/configuration/configuration.ts +++ b/src/configuration/configuration.ts @@ -36,4 +36,10 @@ export default () => ({ inPersonVisits: undefined, }, skipAuthGuard: process.env.SKIP_AUTH_GUARD === 'true', + buildInfo: { + buildNumber: + process.env.VPI_APP_LABEL === undefined + ? 'localBuild' + : process.env.VPI_APP_LABEL, + }, }); diff --git a/src/external-api/request-preparer/request-preparer.service.ts b/src/external-api/request-preparer/request-preparer.service.ts index ba59b58..8200f41 100644 --- a/src/external-api/request-preparer/request-preparer.service.ts +++ b/src/external-api/request-preparer/request-preparer.service.ts @@ -11,15 +11,20 @@ import { TokenRefresherService } from '../token-refresher/token-refresher.servic import { HttpService } from '@nestjs/axios'; import { firstValueFrom } from 'rxjs'; import { AxiosError } from 'axios'; +import { ConfigService } from '@nestjs/config'; @Injectable() export class RequestPreparerService { + buildNumber: string; private readonly logger = new Logger(RequestPreparerService.name); constructor( private readonly utilitiesService: UtilitiesService, private readonly tokenRefresherService: TokenRefresherService, private readonly httpService: HttpService, - ) {} + private readonly configService: ConfigService, + ) { + this.buildNumber = this.configService.get('buildInfo.buildNumber'); + } prepareHeadersAndParams( baseSearchSpec: string, @@ -71,12 +76,17 @@ export class RequestPreparerService { ); } catch (error) { if (error instanceof AxiosError) { - this.logger.error(error.message, error.stack, error.cause); + this.logger.error({ + msg: error.message, + stack: error.stack, + cause: error.cause, + buildNumber: this.buildNumber, + }); if (error.status === 404) { throw new HttpException({}, HttpStatus.NO_CONTENT, { cause: error }); } } else { - this.logger.error(error); + this.logger.error({ error, buildNumber: this.buildNumber }); } throw new HttpException( { diff --git a/src/external-api/token-refresher/token-refresher.service.ts b/src/external-api/token-refresher/token-refresher.service.ts index 925007b..332a79c 100644 --- a/src/external-api/token-refresher/token-refresher.service.ts +++ b/src/external-api/token-refresher/token-refresher.service.ts @@ -16,6 +16,7 @@ export class TokenRefresherService { accessTokenUrl: string; clientId: string; clientSecret: string; + buildNumber: string; private readonly logger = new Logger(TokenRefresherService.name); constructor( @@ -26,6 +27,7 @@ export class TokenRefresherService { this.accessTokenUrl = this.configService.get('oauth.accessTokenUrl'); this.clientId = this.configService.get('oauth.clientId'); this.clientSecret = this.configService.get('oauth.clientSecret'); + this.buildNumber = this.configService.get('buildInfo.buildNumber'); } async refreshUpstreamBearerToken(): Promise { @@ -78,7 +80,12 @@ export class TokenRefresherService { return [bearer_token, expiryMs]; } catch (error) { if (error instanceof AxiosError) { - this.logger.error(error.message, error.stack, error.cause); + this.logger.error({ + msg: error.message, + stack: error.stack, + cause: error.cause, + buildNumber: this.buildNumber, + }); } return [undefined, undefined]; } diff --git a/src/helpers/in-person-visits/in-person-visits.service.ts b/src/helpers/in-person-visits/in-person-visits.service.ts index 46a47b8..1266124 100644 --- a/src/helpers/in-person-visits/in-person-visits.service.ts +++ b/src/helpers/in-person-visits/in-person-visits.service.ts @@ -8,6 +8,10 @@ import { NestedInPersonVisitsEntity, InPersonVisitsEntity, } from '../../entities/in-person-visits.entity'; +import { + baseUrlEnvVarName, + inPersonVisitsEndpointEnvVarName, +} from '../../common/constants/upstream-constants'; @Injectable() export class InPersonVisitsService { @@ -19,8 +23,8 @@ export class InPersonVisitsService { private readonly requestPreparerService: RequestPreparerService, ) { this.url = ( - this.configService.get('UPSTREAM_BASE_URL') + - this.configService.get('IN_PERSON_VISITS_ENDPOINT') + this.configService.get(baseUrlEnvVarName) + + this.configService.get(inPersonVisitsEndpointEnvVarName) ).replace(/\s/g, '%20'); this.workspace = this.configService.get('workspaces.inPersonVisits'); this.sinceFieldName = this.configService.get( diff --git a/src/helpers/support-network/support-network.service.ts b/src/helpers/support-network/support-network.service.ts index 3723300..71eb45c 100644 --- a/src/helpers/support-network/support-network.service.ts +++ b/src/helpers/support-network/support-network.service.ts @@ -11,6 +11,10 @@ import { import { IdPathParams } from '../../dto/id-path-params.dto'; import { SinceQueryParams } from '../../dto/since-query-params.dto'; import { RequestPreparerService } from '../../external-api/request-preparer/request-preparer.service'; +import { + baseUrlEnvVarName, + supportNetworkEndpointEnvVarName, +} from '../../common/constants/upstream-constants'; @Injectable() export class SupportNetworkService { @@ -22,8 +26,8 @@ export class SupportNetworkService { private readonly requestPreparerService: RequestPreparerService, ) { this.url = ( - this.configService.get('UPSTREAM_BASE_URL') + - this.configService.get('SUPPORT_NETWORK_ENDPOINT') + this.configService.get(baseUrlEnvVarName) + + this.configService.get(supportNetworkEndpointEnvVarName) ).replace(/\s/g, '%20'); this.workspace = this.configService.get('workspaces.supportNetwork'); this.sinceFieldName = this.configService.get(