From 3e06b6672a56dab8f834a27c2e080bcb448545de Mon Sep 17 00:00:00 2001 From: Rohit Kandimalla Date: Tue, 16 Apr 2024 13:00:28 -0400 Subject: [PATCH 1/3] Adding health check to the service, also added a global prefix for APIs --- package-lock.json | 164 +++++++++++++++++++++++++++++++++++++++++-- package.json | 1 + src/export.module.ts | 6 +- src/main.ts | 1 + 4 files changed, 163 insertions(+), 9 deletions(-) diff --git a/package-lock.json b/package-lock.json index a517d93..1a25555 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "@nestjs/core": "^10.3.5", "@nestjs/jwt": "^10.2.0", "@nestjs/platform-express": "^10.3.5", + "@nestjs/terminus": "^10.2.3", "@okta/jwt-verifier": "^3.1.0", "cqm-models": "4.1.3", "exceljs": "^4.4.0", @@ -2000,6 +2001,75 @@ "integrity": "sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA==", "dev": true }, + "node_modules/@nestjs/terminus": { + "version": "10.2.3", + "resolved": "https://registry.npmjs.org/@nestjs/terminus/-/terminus-10.2.3.tgz", + "integrity": "sha512-iX7gXtAooePcyQqFt57aDke5MzgdkBeYgF5YsFNNFwOiAFdIQEhfv3PR0G+HlH9F6D7nBCDZt9U87Pks/qHijg==", + "dependencies": { + "boxen": "5.1.2", + "check-disk-space": "3.4.0" + }, + "peerDependencies": { + "@grpc/grpc-js": "*", + "@grpc/proto-loader": "*", + "@mikro-orm/core": "*", + "@mikro-orm/nestjs": "*", + "@nestjs/axios": "^1.0.0 || ^2.0.0 || ^3.0.0", + "@nestjs/common": "^9.0.0 || ^10.0.0", + "@nestjs/core": "^9.0.0 || ^10.0.0", + "@nestjs/microservices": "^9.0.0 || ^10.0.0", + "@nestjs/mongoose": "^9.0.0 || ^10.0.0", + "@nestjs/sequelize": "^9.0.0 || ^10.0.0", + "@nestjs/typeorm": "^9.0.0 || ^10.0.0", + "@prisma/client": "*", + "mongoose": "*", + "reflect-metadata": "0.1.x || 0.2.x", + "rxjs": "7.x", + "sequelize": "*", + "typeorm": "*" + }, + "peerDependenciesMeta": { + "@grpc/grpc-js": { + "optional": true + }, + "@grpc/proto-loader": { + "optional": true + }, + "@mikro-orm/core": { + "optional": true + }, + "@mikro-orm/nestjs": { + "optional": true + }, + "@nestjs/axios": { + "optional": true + }, + "@nestjs/microservices": { + "optional": true + }, + "@nestjs/mongoose": { + "optional": true + }, + "@nestjs/sequelize": { + "optional": true + }, + "@nestjs/typeorm": { + "optional": true + }, + "@prisma/client": { + "optional": true + }, + "mongoose": { + "optional": true + }, + "sequelize": { + "optional": true + }, + "typeorm": { + "optional": true + } + } + }, "node_modules/@nestjs/testing": { "version": "10.3.5", "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-10.3.5.tgz", @@ -2905,6 +2975,14 @@ } } }, + "node_modules/ansi-align": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz", + "integrity": "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==", + "dependencies": { + "string-width": "^4.1.0" + } + }, "node_modules/ansi-colors": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", @@ -2945,7 +3023,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, "engines": { "node": ">=8" } @@ -3365,6 +3442,54 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, + "node_modules/boxen": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/boxen/-/boxen-5.1.2.tgz", + "integrity": "sha512-9gYgQKXx+1nP8mP7CzFyaUARhg7D3n1dF/FnErWmu9l6JvGpNUN278h0aSb+QjoiKSWG+iZ3uHrcqk0qrY9RQQ==", + "dependencies": { + "ansi-align": "^3.0.0", + "camelcase": "^6.2.0", + "chalk": "^4.1.0", + "cli-boxes": "^2.2.1", + "string-width": "^4.2.2", + "type-fest": "^0.20.2", + "widest-line": "^3.1.0", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/boxen/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/boxen/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/brace-expansion": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", @@ -3619,6 +3744,14 @@ "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", "dev": true }, + "node_modules/check-disk-space": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/check-disk-space/-/check-disk-space-3.4.0.tgz", + "integrity": "sha512-drVkSqfwA+TvuEhFipiR1OC9boEGZL5RrWvVsOthdcvQNXyCCuKkEiTOTXZ7qxSf/GLwq4GvzfrQD/Wz325hgw==", + "engines": { + "node": ">=16" + } + }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -3673,6 +3806,17 @@ "integrity": "sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==", "dev": true }, + "node_modules/cli-boxes": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-2.2.1.tgz", + "integrity": "sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/cli-cursor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", @@ -4482,8 +4626,7 @@ "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" }, "node_modules/encodeurl": { "version": "1.0.2", @@ -6016,7 +6159,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, "engines": { "node": ">=8" } @@ -9205,7 +9347,6 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -9234,7 +9375,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "dependencies": { "ansi-regex": "^5.0.1" }, @@ -9825,7 +9965,6 @@ "version": "0.20.2", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "dev": true, "engines": { "node": ">=10" }, @@ -10166,6 +10305,17 @@ "node": ">=4" } }, + "node_modules/widest-line": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-3.1.0.tgz", + "integrity": "sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==", + "dependencies": { + "string-width": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/wrap-ansi": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", diff --git a/package.json b/package.json index af3b6ce..cddae80 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "@nestjs/core": "^10.3.5", "@nestjs/jwt": "^10.2.0", "@nestjs/platform-express": "^10.3.5", + "@nestjs/terminus": "^10.2.3", "@okta/jwt-verifier": "^3.1.0", "cqm-models": "4.1.3", "exceljs": "^4.4.0", diff --git a/src/export.module.ts b/src/export.module.ts index ccdb34c..f2657ce 100644 --- a/src/export.module.ts +++ b/src/export.module.ts @@ -2,10 +2,12 @@ import { Module } from '@nestjs/common'; import { ExportController } from './controllers/export.controller'; import { ExportService } from './services/export.service'; import { AuthModule } from './auth/auth.module'; +import { TerminusModule } from '@nestjs/terminus'; +import { HealthController } from './health/health.controller'; @Module({ - imports: [AuthModule], - controllers: [ExportController], + imports: [AuthModule, TerminusModule], + controllers: [ExportController, HealthController], providers: [ExportService], }) export class ExportModule {} diff --git a/src/main.ts b/src/main.ts index a484b76..910443c 100644 --- a/src/main.ts +++ b/src/main.ts @@ -5,6 +5,7 @@ export async function bootstrap() { const app = await NestFactory.create(ExportModule, { logger: ['error', 'log'], }); + app.setGlobalPrefix('/api'); app.enableCors({ origin: [ 'http://localhost:9000', From 457104e5fea97d602c77f421817167d62d5ba117 Mon Sep 17 00:00:00 2001 From: Rohit Kandimalla Date: Tue, 16 Apr 2024 13:05:39 -0400 Subject: [PATCH 2/3] health controller --- src/health/health.controller.spec.ts | 33 ++++++++++++++++++++++++++++ src/health/health.controller.ts | 13 +++++++++++ 2 files changed, 46 insertions(+) create mode 100644 src/health/health.controller.spec.ts create mode 100644 src/health/health.controller.ts diff --git a/src/health/health.controller.spec.ts b/src/health/health.controller.spec.ts new file mode 100644 index 0000000..2e1e171 --- /dev/null +++ b/src/health/health.controller.spec.ts @@ -0,0 +1,33 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { HealthController } from './health.controller'; +import { HealthCheckService, TerminusModule } from '@nestjs/terminus'; + +describe('HealthController', () => { + let controller: HealthController; + const mockedCheckServiceCall = jest.fn().mockResolvedValue(() => { + return { status: 'ok', info: {}, error: {}, details: {} }; + }); + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [HealthController], + providers: [ + { + provide: HealthCheckService, + useValue: { + check: mockedCheckServiceCall, + }, + }, + ], + imports: [TerminusModule], + }).compile(); + + controller = module.get(HealthController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + controller.check(); + expect(mockedCheckServiceCall).toHaveBeenCalledTimes(1); + }); +}); diff --git a/src/health/health.controller.ts b/src/health/health.controller.ts new file mode 100644 index 0000000..6025b35 --- /dev/null +++ b/src/health/health.controller.ts @@ -0,0 +1,13 @@ +import { Controller, Get } from '@nestjs/common'; +import { HealthCheck, HealthCheckService } from '@nestjs/terminus'; + +@Controller('health') +export class HealthController { + constructor(private health: HealthCheckService) {} + + @Get() + @HealthCheck() + check() { + return this.health.check([]); + } +} From 4166bd382e28a7da025cff5e066e1edfadeae38d Mon Sep 17 00:00:00 2001 From: Rohit Kandimalla Date: Wed, 17 Apr 2024 10:46:11 -0400 Subject: [PATCH 3/3] Fixed unit tests by mocking jwtTokenVerifier from okta, Updated test scirpts to not include actual okta instances --- package.json | 6 ++--- src/auth/auth.guard.spec.ts | 50 ++++++++++++++++--------------------- 2 files changed, 24 insertions(+), 32 deletions(-) diff --git a/package.json b/package.json index cddae80..bb7bb6d 100644 --- a/package.json +++ b/package.json @@ -14,9 +14,9 @@ "start:debug": "nest start --debug --watch", "start:prod": "node dist/main", "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", - "test": "ISSUER=\"https://dev-18092578.okta.com/oauth2/default\" CLIENT_ID=\"0oa2fqtaz95fqJqbf5d7\" jest", - "test:watch": "ISSUER=\"https://dev-18092578.okta.com/oauth2/default\" CLIENT_ID=\"0oa2fqtaz95fqJqbf5d7\" jest --watch", - "test:cov": "ISSUER=\"https://dev-18092578.okta.com/oauth2/default\" CLIENT_ID=\"0oa2fqtaz95fqJqbf5d7\" jest --coverage", + "test": "jest", + "test:watch": "jest --watch", + "test:cov": "jest --coverage", "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", "test:e2e": "jest --config ./test/jest-e2e.json" }, diff --git a/src/auth/auth.guard.spec.ts b/src/auth/auth.guard.spec.ts index 84219d8..3765f31 100644 --- a/src/auth/auth.guard.spec.ts +++ b/src/auth/auth.guard.spec.ts @@ -3,11 +3,31 @@ import { AuthGuard } from './auth.guard'; import { JwtService } from '@nestjs/jwt'; import { createMock } from '@golevelup/ts-jest'; import { ExecutionContext } from '@nestjs/common'; +import { Jwt } from '@okta/jwt-verifier'; +import * as process from 'process'; + +jest.mock('@okta/jwt-verifier', () => { + return jest.fn().mockImplementation(() => { + return { + verifyAccessToken: () => + new Promise((resolve) => + resolve({ + claims: { + sub: 'a_user', + }, + } as unknown as Jwt), + ), + }; + }); +}); describe('AuthGuard', () => { let guard: AuthGuard; beforeEach(async () => { + process.env.ISSUER = 'https://test-issuer.com'; + process.env.CLIENT_ID = 'test-client-id'; + const module: TestingModule = await Test.createTestingModule({ providers: [JwtService], }).compile(); @@ -22,33 +42,6 @@ describe('AuthGuard', () => { it('should have a fully mocked Execution Context with good auth token', async () => { const mockExecutionContext = createMock(); expect(mockExecutionContext.switchToHttp()).toBeDefined(); - - mockExecutionContext.switchToHttp = jest.fn().mockResolvedValue({ - getRequest: () => ({ - originalUrl: '/', - method: 'GET', - params: undefined, - query: undefined, - body: undefined, - headers: '', - }), - getResponse: () => ({ - statusCode: 200, - }), - }); - - jest.mock('@okta/jwt-verifier', () => { - return jest.fn().mockImplementation(() => ({ - verifyAccessToken: () => ({ - oktaToken: { - claims: { - sub: 'a_user', - }, - }, - }), - })); - }); - jest .spyOn(mockExecutionContext.switchToHttp(), 'getRequest') .mockImplementation(() => { @@ -59,8 +52,7 @@ describe('AuthGuard', () => { query: undefined, body: undefined, headers: { - authorization: - 'Bearer eyJraWQiOiJNNG9CMW9DSmthdC0tYTNENFFXUFA3RWZCbUl3NG9BV05KYWJxdEJhUnM4IiwiYWxnIjoiUlMyNTYifQ.eyJ2ZXIiOjEsImp0aSI6IkFULl8xc1BEY0hpekhSdm5CSlZsaWQzak5tVXZjMDIzU3FCZDB0UUhnVldkT0EiLCJpc3MiOiJodHRwczovL2Rldi0xODA5MjU3OC5va3RhLmNvbS9vYXV0aDIvZGVmYXVsdCIsImF1ZCI6ImFwaTovL2RlZmF1bHQiLCJpYXQiOjE3MTI5MzcwNjQsImV4cCI6MTcxMjk0MDY2NCwiY2lkIjoiMG9hMmZxdGF6OTVmcUpxYmY1ZDciLCJ1aWQiOiIwMHUzaTNjM3p6WlhLcjkwMTVkNyIsInNjcCI6WyJvcGVuaWQiLCJwcm9maWxlIl0sImF1dGhfdGltZSI6MTcxMjkzMjU5NSwic3ViIjoiY2VjaWxpYS5saXVAc2VtYW50aWNiaXRzLmNvbSJ9.x_vx7uCGXPyme80erURcS87ZUdibdBRKiNB58yg-AcNoF0ZYoro0lOr9up-ev0j32SQvBnhMXRZOARoOy4ALcT_GovwluH2v_sjXhNtjn26GV5UZU1EaWXsdMWfwg_-6eAmlQ9dLkIZerIYsu7Ut8pfwirgbpME4mMKqiJBXEkRWHUkAh5PEnJO4DvaKj6Tis5ERprNLuUKR5M4bWMhBjAMt74fTu5iLiANOi0uqropZscP72HVNEQhkyqM84hvgAvZvzlVYKUTUBuoWQF21eRSOUpYSE0yvDRW5JS5r0NCXUEZwuNfavuvl3gDUae31hcbtOo7kznQ2Q_-GkfVThA', + authorization: 'Bearer asdf', }, } as unknown as Request; });