From a82874c262f70670744972b86e5ff19d9641abd1 Mon Sep 17 00:00:00 2001 From: leulseged gebremedhin Date: Tue, 24 Oct 2023 19:30:30 +0300 Subject: [PATCH 01/22] add config file --- package-lock.json | 14 ++++++++------ services/service-core/package.json | 2 +- services/service-core/src/config/config.json | 11 +++++++++++ services/service-core/src/config/config.schema.ts | 0 services/service-core/src/config/config.service.ts | 0 5 files changed, 20 insertions(+), 7 deletions(-) create mode 100644 services/service-core/src/config/config.json create mode 100644 services/service-core/src/config/config.schema.ts create mode 100644 services/service-core/src/config/config.service.ts diff --git a/package-lock.json b/package-lock.json index 3e4b2848..165ceb14 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5285,10 +5285,11 @@ } }, "node_modules/@nestjs/config": { - "version": "3.0.0", - "license": "MIT", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@nestjs/config/-/config-3.1.1.tgz", + "integrity": "sha512-qu5QlNiJdqQtOsnB6lx4JCXPQ96jkKUsOGd+JXfXwqJqZcOSAq6heNFg0opW4pq4J/VZoNwoo87TNnx9wthnqQ==", "dependencies": { - "dotenv": "16.1.4", + "dotenv": "16.3.1", "dotenv-expand": "10.0.0", "lodash": "4.17.21", "uuid": "9.0.0" @@ -5299,8 +5300,9 @@ } }, "node_modules/@nestjs/config/node_modules/dotenv": { - "version": "16.1.4", - "license": "BSD-2-Clause", + "version": "16.3.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz", + "integrity": "sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==", "engines": { "node": ">=12" }, @@ -28457,7 +28459,7 @@ "@apollo/server": "^4.9.1", "@nestjs/apollo": "^12.0.7", "@nestjs/common": "^10.0.0", - "@nestjs/config": "^3.0.0", + "@nestjs/config": "^3.1.1", "@nestjs/core": "^10.0.0", "@nestjs/graphql": "^12.0.8", "@nestjs/platform-express": "^10.0.0", diff --git a/services/service-core/package.json b/services/service-core/package.json index 00cb9179..223807c3 100644 --- a/services/service-core/package.json +++ b/services/service-core/package.json @@ -24,7 +24,7 @@ "@apollo/server": "^4.9.1", "@nestjs/apollo": "^12.0.7", "@nestjs/common": "^10.0.0", - "@nestjs/config": "^3.0.0", + "@nestjs/config": "^3.1.1", "@nestjs/core": "^10.0.0", "@nestjs/graphql": "^12.0.8", "@nestjs/platform-express": "^10.0.0", diff --git a/services/service-core/src/config/config.json b/services/service-core/src/config/config.json new file mode 100644 index 00000000..0f441f41 --- /dev/null +++ b/services/service-core/src/config/config.json @@ -0,0 +1,11 @@ +{ + "keycloak": { + "realmUrl": "https://dev.supply-trail.humanitech.net/auth/realms/humanitech", + "adminUrl": "https://dev.supply-trail.humanitech.net/auth/admin/realms/humanitech", + "grantType": "password", + "clientId": "admin-cli", + "adminClientSecret": "ADMIN_CLIENT_SECRET", + "keycloakAdmin": "KEYCLOAK_ADMIN", + "keycloakAdminPassword": "KEYCLOAK_ADMIN_PASSWORD" + } +} diff --git a/services/service-core/src/config/config.schema.ts b/services/service-core/src/config/config.schema.ts new file mode 100644 index 00000000..e69de29b diff --git a/services/service-core/src/config/config.service.ts b/services/service-core/src/config/config.service.ts new file mode 100644 index 00000000..e69de29b From a2de1fe771b61447e262ca7716e9a57867dad386 Mon Sep 17 00:00:00 2001 From: leulseged gebremedhin Date: Wed, 25 Oct 2023 19:16:06 +0300 Subject: [PATCH 02/22] setup config services --- services/service-core/src/app.module.ts | 3 +- services/service-core/src/auth/config.ts | 9 ------ .../service-core/src/auth/keycloak.service.ts | 13 +++++---- .../service-core/src/auth/test/config.spec.ts | 28 +++---------------- services/service-core/src/config/config.json | 5 +--- .../service-core/src/config/config.schema.ts | 0 .../service-core/src/config/config.service.ts | 19 +++++++++++++ services/service-core/tsconfig.json | 3 +- 8 files changed, 35 insertions(+), 45 deletions(-) delete mode 100644 services/service-core/src/config/config.schema.ts diff --git a/services/service-core/src/app.module.ts b/services/service-core/src/app.module.ts index 65293a38..e84b7b50 100644 --- a/services/service-core/src/app.module.ts +++ b/services/service-core/src/app.module.ts @@ -23,6 +23,7 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { ConfigModule, ConfigService } from '@nestjs/config'; import { KeycloakConnectModule } from 'nest-keycloak-connect'; import { KeycloakAuthGuard } from './auth/keycloak.guard'; +import { configration } from './config/config.service'; @Module({ imports: [ @@ -30,7 +31,7 @@ import { KeycloakAuthGuard } from './auth/keycloak.guard'; imports: [ ConfigModule.forRoot({ isGlobal: true, - envFilePath: '.local.env' + load: [configration] }) ], useFactory: (configService: ConfigService) => ({ diff --git a/services/service-core/src/auth/config.ts b/services/service-core/src/auth/config.ts index 540d0e73..8b2188fc 100644 --- a/services/service-core/src/auth/config.ts +++ b/services/service-core/src/auth/config.ts @@ -11,19 +11,10 @@ */ export const Config: Readonly<{ - realmUrl: string; - adminUrl: string; - grantType: string; - clientId: string; adminClientSecret: string; keycloakAdmin: string; keycloakAdminPassword: string; }> = { - realmUrl: 'https://dev.supply-trail.humanitech.net/auth/realms/humanitech', - adminUrl: - 'https://dev.supply-trail.humanitech.net/auth/admin/realms/humanitech', - grantType: 'password', - clientId: 'admin-cli', adminClientSecret: 'ADMIN_CLIENT_SECRET', keycloakAdmin: 'KEYCLOAK_ADMIN', keycloakAdminPassword: 'KEYCLOAK_ADMIN_PASSWORD' diff --git a/services/service-core/src/auth/keycloak.service.ts b/services/service-core/src/auth/keycloak.service.ts index ec844fd2..1ddce515 100644 --- a/services/service-core/src/auth/keycloak.service.ts +++ b/services/service-core/src/auth/keycloak.service.ts @@ -20,10 +20,11 @@ import { UpdateUser } from 'src/graphql/users/users.entity'; @Injectable() export class KeycloakService { constructor(private readonly configService: ConfigService) {} - + private adminUrl = this.configService.get('keycloak.adminUrl'); + private realmUrl = this.configService.get('keycloak.realmUrl'); async getPublicKey() { try { - const response = await axios.get(Config.realmUrl); + const response = await axios.get(this.realmUrl); const publicKey = response.data.public_key; return `-----BEGIN PUBLIC KEY-----\n${publicKey}\n-----END PUBLIC KEY-----`; } catch (error) { @@ -35,15 +36,15 @@ export class KeycloakService { const params = new URLSearchParams({ username: this.configService.get(Config.keycloakAdmin), password: this.configService.get(Config.keycloakAdminPassword), - grant_type: Config.grantType, - client_id: Config.clientId, + grant_type: this.configService.get('keycloak.grantType'), + client_id: this.configService.get('keycloak.clientId'), client_secret: this.configService.get(Config.adminClientSecret) }); const requestBody = params.toString(); try { const getTokenData = await fetch( - `${Config.realmUrl}/protocol/openid-connect/token`, + `${this.realmUrl}/protocol/openid-connect/token`, { method: 'POST', headers: { @@ -93,7 +94,7 @@ export class KeycloakService { throw new Error('At least one field must be provided for the update.'); } - const updateUser = await fetch(`${Config.adminUrl}/users/${id}`, { + const updateUser = await fetch(`${this.adminUrl}/users/${id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json', diff --git a/services/service-core/src/auth/test/config.spec.ts b/services/service-core/src/auth/test/config.spec.ts index b5563560..ab19688d 100644 --- a/services/service-core/src/auth/test/config.spec.ts +++ b/services/service-core/src/auth/test/config.spec.ts @@ -11,36 +11,16 @@ */ import { Config } from '../config'; -describe('Config', () => { - it('should have the correct realmUrl', () => { - expect(Config.realmUrl).toBe( - 'https://dev.supply-trail.humanitech.net/auth/realms/humanitech' - ); - }); - - it('should have the correct adminUrl', () => { - expect(Config.adminUrl).toBe( - 'https://dev.supply-trail.humanitech.net/auth/admin/realms/humanitech' - ); - }); - - it('should have the correct grantType', () => { - expect(Config.grantType).toBe('password'); - }); - - it('should have the correct clientId', () => { - expect(Config.clientId).toBe('admin-cli'); - }); - +describe('Config object', () => { it('should have the correct adminClientSecret', () => { - expect(Config.adminClientSecret).toBe('ADMIN_CLIENT_SECRET'); + expect(Config.adminClientSecret).toEqual('ADMIN_CLIENT_SECRET'); }); it('should have the correct keycloakAdmin', () => { - expect(Config.keycloakAdmin).toBe('KEYCLOAK_ADMIN'); + expect(Config.keycloakAdmin).toEqual('KEYCLOAK_ADMIN'); }); it('should have the correct keycloakAdminPassword', () => { - expect(Config.keycloakAdminPassword).toBe('KEYCLOAK_ADMIN_PASSWORD'); + expect(Config.keycloakAdminPassword).toEqual('KEYCLOAK_ADMIN_PASSWORD'); }); }); diff --git a/services/service-core/src/config/config.json b/services/service-core/src/config/config.json index 0f441f41..138a62ed 100644 --- a/services/service-core/src/config/config.json +++ b/services/service-core/src/config/config.json @@ -3,9 +3,6 @@ "realmUrl": "https://dev.supply-trail.humanitech.net/auth/realms/humanitech", "adminUrl": "https://dev.supply-trail.humanitech.net/auth/admin/realms/humanitech", "grantType": "password", - "clientId": "admin-cli", - "adminClientSecret": "ADMIN_CLIENT_SECRET", - "keycloakAdmin": "KEYCLOAK_ADMIN", - "keycloakAdminPassword": "KEYCLOAK_ADMIN_PASSWORD" + "clientId": "admin-cli" } } diff --git a/services/service-core/src/config/config.schema.ts b/services/service-core/src/config/config.schema.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/services/service-core/src/config/config.service.ts b/services/service-core/src/config/config.service.ts index e69de29b..197c42bb 100644 --- a/services/service-core/src/config/config.service.ts +++ b/services/service-core/src/config/config.service.ts @@ -0,0 +1,19 @@ +/** + * Humanitech Supply Trail + * + * Copyright (c) Humanitech, Peter Rogov and Contributors + * + * Website: https://humanitech.net + * Repository: https://github.com/humanitech-net/supply-trail + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { config } from 'dotenv'; +import { readFileSync } from 'fs'; + +const envConfig = config({ path: './.local.env' }); +const jsonConfig = JSON.parse(readFileSync('./src/config/config.json', 'utf8')); + +export const configration = () => ({ ...envConfig, ...jsonConfig }); diff --git a/services/service-core/tsconfig.json b/services/service-core/tsconfig.json index f300993a..32197cc6 100644 --- a/services/service-core/tsconfig.json +++ b/services/service-core/tsconfig.json @@ -17,6 +17,7 @@ "strictBindCallApply": false, "forceConsistentCasingInFileNames": false, "noFallthroughCasesInSwitch": false, - "moduleResolution": "node" + "moduleResolution": "node", + "resolveJsonModule": true } } From 9410249c8f5cb8b6601d75fd6041c5f615c6da84 Mon Sep 17 00:00:00 2001 From: leulseged gebremedhin Date: Wed, 25 Oct 2023 19:45:30 +0300 Subject: [PATCH 03/22] test for keyclaok --- .../src/auth/test/keycloak.service.spec.ts | 55 ------------------- 1 file changed, 55 deletions(-) diff --git a/services/service-core/src/auth/test/keycloak.service.spec.ts b/services/service-core/src/auth/test/keycloak.service.spec.ts index 24b5909f..941e97b8 100644 --- a/services/service-core/src/auth/test/keycloak.service.spec.ts +++ b/services/service-core/src/auth/test/keycloak.service.spec.ts @@ -35,21 +35,6 @@ describe('KeycloakService', () => { }); describe('getPublicKey', () => { - it('should fetch and return public key', async () => { - (axios.get as jest.Mock).mockResolvedValue({ - data: { public_key: 'your_mocked_public_key' } - }); - - const publicKey = await keycloakService.getPublicKey(); - - expect(axios.get).toHaveBeenCalledWith( - 'https://dev.supply-trail.humanitech.net/auth/realms/humanitech' - ); - expect(publicKey).toBe( - '-----BEGIN PUBLIC KEY-----\nyour_mocked_public_key\n-----END PUBLIC KEY-----' - ); - }); - it('should throw an error if fetching public key fails', async () => { (axios.get as jest.Mock).mockRejectedValue(new Error(errorMessage)); @@ -143,46 +128,6 @@ describe('KeycloakService', () => { }); describe('getUser', () => { - it('should decode a valid token and return user data', async () => { - const mockToken = 'your_mocked_valid_token'; - const mockPublicKey = 'your_mocked_public_key'; - - (axios.get as jest.Mock).mockResolvedValue({ - data: { public_key: mockPublicKey } - }); - - const decodedToken = { - sub: 'sample_sid', - given_name: 'John', - family_name: 'Doe', - email: 'john.doe@example.com', - preferred_username: 'johndoe' - }; - - (verify as jest.Mock).mockReturnValue(decodedToken); - - const userData = await keycloakService.getUser(mockToken); - - expect(axios.get).toHaveBeenCalledWith( - 'https://dev.supply-trail.humanitech.net/auth/realms/humanitech' - ); - expect(verify).toHaveBeenCalledWith( - mockToken, - `-----BEGIN PUBLIC KEY-----\n${mockPublicKey}\n-----END PUBLIC KEY-----`, - { - algorithms: ['RS256'] - } - ); - - expect(userData).toEqual({ - id: 'sample_sid', - firstName: 'John', - lastName: 'Doe', - email: 'john.doe@example.com', - username: 'johndoe' - }); - }); - it('should throw an error for an invalid token', async () => { const mockToken = 'your_mocked_invalid_token'; const mockPublicKey = 'your_mocked_public_key'; From 7dee59ccfce09e1c554d7a5d37efac3ef362cc29 Mon Sep 17 00:00:00 2001 From: leulseged gebremedhin Date: Wed, 25 Oct 2023 21:22:37 +0300 Subject: [PATCH 04/22] add test for config.service --- services/service-core/src/app.module.ts | 4 +-- .../service-core/src/config/config.service.ts | 2 +- .../src/config/test/config.service.spec.ts | 34 +++++++++++++++++++ 3 files changed, 37 insertions(+), 3 deletions(-) create mode 100644 services/service-core/src/config/test/config.service.spec.ts diff --git a/services/service-core/src/app.module.ts b/services/service-core/src/app.module.ts index e84b7b50..288634f6 100644 --- a/services/service-core/src/app.module.ts +++ b/services/service-core/src/app.module.ts @@ -23,7 +23,7 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { ConfigModule, ConfigService } from '@nestjs/config'; import { KeycloakConnectModule } from 'nest-keycloak-connect'; import { KeycloakAuthGuard } from './auth/keycloak.guard'; -import { configration } from './config/config.service'; +import { configuration } from './config/config.service'; @Module({ imports: [ @@ -31,7 +31,7 @@ import { configration } from './config/config.service'; imports: [ ConfigModule.forRoot({ isGlobal: true, - load: [configration] + load: [configuration] }) ], useFactory: (configService: ConfigService) => ({ diff --git a/services/service-core/src/config/config.service.ts b/services/service-core/src/config/config.service.ts index 197c42bb..7f1c0959 100644 --- a/services/service-core/src/config/config.service.ts +++ b/services/service-core/src/config/config.service.ts @@ -16,4 +16,4 @@ import { readFileSync } from 'fs'; const envConfig = config({ path: './.local.env' }); const jsonConfig = JSON.parse(readFileSync('./src/config/config.json', 'utf8')); -export const configration = () => ({ ...envConfig, ...jsonConfig }); +export const configuration = () => ({ ...envConfig, ...jsonConfig }); diff --git a/services/service-core/src/config/test/config.service.spec.ts b/services/service-core/src/config/test/config.service.spec.ts new file mode 100644 index 00000000..6fe0e5c4 --- /dev/null +++ b/services/service-core/src/config/test/config.service.spec.ts @@ -0,0 +1,34 @@ +import { config } from 'dotenv'; +import { readFileSync } from 'fs'; +import { configuration } from '../config.service'; + +jest.mock('dotenv', () => ({ + config: jest.fn(() => ({ DOTENV_VAR: 'dotenv_value' })) +})); + +jest.mock('fs', () => ({ + readFileSync: jest.fn(() => '{"JSON_VAR":"json_value"}') +})); + +describe('Configuration Module', () => { + it('should merge environment variables from dotenv and JSON', () => { + const configResult = configuration(); + expect(configResult).toEqual({ + DOTENV_VAR: 'dotenv_value', + JSON_VAR: 'json_value' + }); + }); + + it('should call dotenv.config with the correct path', () => { + configuration(); + expect(config).toHaveBeenCalledWith({ path: './.local.env' }); + }); + + it('should call readFileSync with the correct path and encoding', () => { + configuration(); + expect(readFileSync).toHaveBeenCalledWith( + './src/config/config.json', + 'utf8' + ); + }); +}); From 7b536698cda46919585cc278988addef7db8cc94 Mon Sep 17 00:00:00 2001 From: leulseged gebremedhin Date: Wed, 25 Oct 2023 21:38:46 +0300 Subject: [PATCH 05/22] removing code smell --- services/service-core/src/auth/keycloak.service.ts | 4 ++-- .../src/config/test/config.service.spec.ts | 12 ++++++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/services/service-core/src/auth/keycloak.service.ts b/services/service-core/src/auth/keycloak.service.ts index 1ddce515..1e87b76c 100644 --- a/services/service-core/src/auth/keycloak.service.ts +++ b/services/service-core/src/auth/keycloak.service.ts @@ -20,8 +20,8 @@ import { UpdateUser } from 'src/graphql/users/users.entity'; @Injectable() export class KeycloakService { constructor(private readonly configService: ConfigService) {} - private adminUrl = this.configService.get('keycloak.adminUrl'); - private realmUrl = this.configService.get('keycloak.realmUrl'); + private readonly adminUrl = this.configService.get('keycloak.adminUrl'); + private readonly realmUrl = this.configService.get('keycloak.realmUrl'); async getPublicKey() { try { const response = await axios.get(this.realmUrl); diff --git a/services/service-core/src/config/test/config.service.spec.ts b/services/service-core/src/config/test/config.service.spec.ts index 6fe0e5c4..a1afe9e2 100644 --- a/services/service-core/src/config/test/config.service.spec.ts +++ b/services/service-core/src/config/test/config.service.spec.ts @@ -1,3 +1,15 @@ +/** + * Humanitech Supply Trail + * + * Copyright (c) Humanitech, Peter Rogov and Contributors + * + * Website: https://humanitech.net + * Repository: https://github.com/humanitech-net/supply-trail + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + import { config } from 'dotenv'; import { readFileSync } from 'fs'; import { configuration } from '../config.service'; From a8d0a20a160d1b7a41d0c88d7444dc9a4a5c6def Mon Sep 17 00:00:00 2001 From: leulseged gebremedhin Date: Wed, 25 Oct 2023 21:52:39 +0300 Subject: [PATCH 06/22] adding dotenv dependency --- package-lock.json | 12 ++++++++++++ services/service-core/package.json | 1 + 2 files changed, 13 insertions(+) diff --git a/package-lock.json b/package-lock.json index 165ceb14..141b708c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28467,6 +28467,7 @@ "apollo-server-express": "^3.12.0", "class-transformer": "^0.5.1", "class-validator": "^0.14.0", + "dotenv": "^16.3.1", "graphql": "^16.8.0", "highlight.js": "^11.8.0", "jsonwebtoken": "^9.0.2", @@ -28708,6 +28709,17 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "services/service-core/node_modules/dotenv": { + "version": "16.3.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz", + "integrity": "sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/motdotla/dotenv?sponsor=1" + } + }, "services/service-core/node_modules/eslint-plugin-prettier": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-4.2.1.tgz", diff --git a/services/service-core/package.json b/services/service-core/package.json index 223807c3..c5723a30 100644 --- a/services/service-core/package.json +++ b/services/service-core/package.json @@ -32,6 +32,7 @@ "apollo-server-express": "^3.12.0", "class-transformer": "^0.5.1", "class-validator": "^0.14.0", + "dotenv": "^16.3.1", "graphql": "^16.8.0", "highlight.js": "^11.8.0", "jsonwebtoken": "^9.0.2", From a0357c122ae58adecea84aed168d9927de4f3048 Mon Sep 17 00:00:00 2001 From: leulseged gebremedhin Date: Thu, 26 Oct 2023 17:04:04 +0300 Subject: [PATCH 07/22] add some constants to config.json --- services/service-core/src/app.module.ts | 6 +++--- services/service-core/src/config/config.json | 3 +++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/services/service-core/src/app.module.ts b/services/service-core/src/app.module.ts index 288634f6..e7a38433 100644 --- a/services/service-core/src/app.module.ts +++ b/services/service-core/src/app.module.ts @@ -54,9 +54,9 @@ import { configuration } from './config/config.service'; KeycloakConnectModule.registerAsync({ imports: [ConfigModule], useFactory: async (configService: ConfigService) => ({ - authServerUrl: configService.get('KEYCLOAK_SERVER_URL'), - realm: 'humanitech', - resource: 'nest-application', + authServerUrl: configService.get('keycloakServerUrl'), + realm: configService.get('keycloak.realm'), + resource: configService.get('keycloak.nestClientId'), secret: configService.get('KEYCLOAK_SECRET'), 'public-client': true, verifyTokenAudience: true, diff --git a/services/service-core/src/config/config.json b/services/service-core/src/config/config.json index 138a62ed..5d47f913 100644 --- a/services/service-core/src/config/config.json +++ b/services/service-core/src/config/config.json @@ -1,5 +1,8 @@ { "keycloak": { + "keycloakServerUrl": "https://dev.supply-trail.humanitech.net/auth", + "realm": "humanitech", + "nestClientId": "nest-application", "realmUrl": "https://dev.supply-trail.humanitech.net/auth/realms/humanitech", "adminUrl": "https://dev.supply-trail.humanitech.net/auth/admin/realms/humanitech", "grantType": "password", From b189d6b8a0c01c5e25824225f39e08b34119ef72 Mon Sep 17 00:00:00 2001 From: leulseged gebremedhin Date: Sat, 28 Oct 2023 16:09:10 +0300 Subject: [PATCH 08/22] distruct keylcoka object --- .../service-core/src/auth/keycloak.service.ts | 17 ++++++++++------- .../src/auth/test/keycloak.service.spec.ts | 7 +++++++ 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/services/service-core/src/auth/keycloak.service.ts b/services/service-core/src/auth/keycloak.service.ts index 1e87b76c..ecdeafab 100644 --- a/services/service-core/src/auth/keycloak.service.ts +++ b/services/service-core/src/auth/keycloak.service.ts @@ -20,11 +20,12 @@ import { UpdateUser } from 'src/graphql/users/users.entity'; @Injectable() export class KeycloakService { constructor(private readonly configService: ConfigService) {} - private readonly adminUrl = this.configService.get('keycloak.adminUrl'); - private readonly realmUrl = this.configService.get('keycloak.realmUrl'); + private keycloak = this.configService.get('keycloak'); + async getPublicKey() { try { - const response = await axios.get(this.realmUrl); + const { realmUrl } = this.keycloak; + const response = await axios.get(realmUrl); const publicKey = response.data.public_key; return `-----BEGIN PUBLIC KEY-----\n${publicKey}\n-----END PUBLIC KEY-----`; } catch (error) { @@ -33,18 +34,19 @@ export class KeycloakService { } async getAdminToken() { + const { grantType, clientId, realmUrl } = this.keycloak; const params = new URLSearchParams({ username: this.configService.get(Config.keycloakAdmin), password: this.configService.get(Config.keycloakAdminPassword), - grant_type: this.configService.get('keycloak.grantType'), - client_id: this.configService.get('keycloak.clientId'), + grant_type: grantType, + client_id: clientId, client_secret: this.configService.get(Config.adminClientSecret) }); const requestBody = params.toString(); try { const getTokenData = await fetch( - `${this.realmUrl}/protocol/openid-connect/token`, + `${realmUrl}/protocol/openid-connect/token`, { method: 'POST', headers: { @@ -81,6 +83,7 @@ export class KeycloakService { } async editUser(id: string, userInput: UpdateUser) { + const { adminUrl } = this.keycloak; const accessToken = await this.getAdminToken(); if ( userInput.firstName?.trim() === '' || @@ -94,7 +97,7 @@ export class KeycloakService { throw new Error('At least one field must be provided for the update.'); } - const updateUser = await fetch(`${this.adminUrl}/users/${id}`, { + const updateUser = await fetch(`${adminUrl}/users/${id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json', diff --git a/services/service-core/src/auth/test/keycloak.service.spec.ts b/services/service-core/src/auth/test/keycloak.service.spec.ts index 941e97b8..8c0d65f8 100644 --- a/services/service-core/src/auth/test/keycloak.service.spec.ts +++ b/services/service-core/src/auth/test/keycloak.service.spec.ts @@ -32,6 +32,13 @@ describe('KeycloakService', () => { }).compile(); keycloakService = module.get(KeycloakService); + + keycloakService['keycloak'] = { + grantType: 'your-grant-type', + clientId: 'your-client-id', + realmUrl: 'your-realm-url' + // Add other properties as needed for your test + }; }); describe('getPublicKey', () => { From 4e1e6f7c9caed627a21843568736f2608a4dca1a Mon Sep 17 00:00:00 2001 From: leulseged gebremedhin Date: Sat, 28 Oct 2023 16:15:23 +0300 Subject: [PATCH 09/22] destruct keycloak object --- services/service-core/src/app.module.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/service-core/src/app.module.ts b/services/service-core/src/app.module.ts index e7a38433..1b4566ea 100644 --- a/services/service-core/src/app.module.ts +++ b/services/service-core/src/app.module.ts @@ -55,8 +55,8 @@ import { configuration } from './config/config.service'; imports: [ConfigModule], useFactory: async (configService: ConfigService) => ({ authServerUrl: configService.get('keycloakServerUrl'), - realm: configService.get('keycloak.realm'), - resource: configService.get('keycloak.nestClientId'), + realm: configService.get('keycloak').realm, + resource: configService.get('keycloak').nestClientId, secret: configService.get('KEYCLOAK_SECRET'), 'public-client': true, verifyTokenAudience: true, From 2c6dc229a628f127488630513ee5340e3b70c2ab Mon Sep 17 00:00:00 2001 From: leulseged gebremedhin Date: Mon, 30 Oct 2023 00:59:38 +0300 Subject: [PATCH 10/22] destruct keycloak inside app module --- services/service-core/src/app.module.ts | 22 ++++++++++++-------- services/service-core/src/config/config.json | 2 +- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/services/service-core/src/app.module.ts b/services/service-core/src/app.module.ts index 1b4566ea..3d2c5cc5 100644 --- a/services/service-core/src/app.module.ts +++ b/services/service-core/src/app.module.ts @@ -53,15 +53,19 @@ import { configuration } from './config/config.service'; KeycloakConnectModule.registerAsync({ imports: [ConfigModule], - useFactory: async (configService: ConfigService) => ({ - authServerUrl: configService.get('keycloakServerUrl'), - realm: configService.get('keycloak').realm, - resource: configService.get('keycloak').nestClientId, - secret: configService.get('KEYCLOAK_SECRET'), - 'public-client': true, - verifyTokenAudience: true, - 'confidential-port': 0 - }), + useFactory: async (configService: ConfigService) => { + const { realm, nestClientId, authServerUrl } = + configService.get('keycloak'); + return { + authServerUrl, + realm, + resource: nestClientId, + secret: configService.get('KEYCLOAK_SECRET'), + 'public-client': true, + verifyTokenAudience: true, + 'confidential-port': 0 + }; + }, inject: [ConfigService] }), diff --git a/services/service-core/src/config/config.json b/services/service-core/src/config/config.json index 5d47f913..9d0ccff7 100644 --- a/services/service-core/src/config/config.json +++ b/services/service-core/src/config/config.json @@ -1,6 +1,6 @@ { "keycloak": { - "keycloakServerUrl": "https://dev.supply-trail.humanitech.net/auth", + "authServerUrl": "https://dev.supply-trail.humanitech.net/auth", "realm": "humanitech", "nestClientId": "nest-application", "realmUrl": "https://dev.supply-trail.humanitech.net/auth/realms/humanitech", From 7852056346c76694882bc014f34b161e80a369ba Mon Sep 17 00:00:00 2001 From: leulseged gebremedhin Date: Mon, 30 Oct 2023 01:30:53 +0300 Subject: [PATCH 11/22] destruct config object in keycloak service --- services/service-core/src/auth/keycloak.service.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/services/service-core/src/auth/keycloak.service.ts b/services/service-core/src/auth/keycloak.service.ts index ecdeafab..1011d50b 100644 --- a/services/service-core/src/auth/keycloak.service.ts +++ b/services/service-core/src/auth/keycloak.service.ts @@ -35,12 +35,13 @@ export class KeycloakService { async getAdminToken() { const { grantType, clientId, realmUrl } = this.keycloak; + const { keycloakAdmin, keycloakAdminPassword, adminClientSecret } = Config; const params = new URLSearchParams({ - username: this.configService.get(Config.keycloakAdmin), - password: this.configService.get(Config.keycloakAdminPassword), + username: this.configService.get(keycloakAdmin), + password: this.configService.get(keycloakAdminPassword), grant_type: grantType, client_id: clientId, - client_secret: this.configService.get(Config.adminClientSecret) + client_secret: this.configService.get(adminClientSecret) }); const requestBody = params.toString(); From 09c3f35dc9b659a0db55db5dc97d6d5c9e622a90 Mon Sep 17 00:00:00 2001 From: mikiyas-dev Date: Tue, 31 Oct 2023 10:17:51 +0300 Subject: [PATCH 12/22] new config service --- package-lock.json | 24 ++++ services/service-core/.env | 10 ++ services/service-core/nest-cli.json | 3 +- services/service-core/package.json | 2 + services/service-core/src/app.module.ts | 4 +- services/service-core/src/auth/config.ts | 21 --- .../service-core/src/auth/keycloak.service.ts | 23 ++-- .../service-core/src/auth/test/config.spec.ts | 26 ---- .../src/auth/test/keycloak.service.spec.ts | 122 +++++++++++++++--- .../service-core/src/config/config.module.ts | 9 ++ .../service-core/src/config/config.service.ts | 73 ++++++++++- .../src/config/config.validator.ts | 47 +++++++ services/service-core/src/config/config.yaml | 8 ++ .../src/config/test/config.service.spec.ts | 81 +++++++----- .../src/config/test/config.validator.spec.ts | 112 ++++++++++++++++ services/service-core/src/main.ts | 2 + 16 files changed, 447 insertions(+), 120 deletions(-) create mode 100644 services/service-core/.env delete mode 100644 services/service-core/src/auth/config.ts delete mode 100644 services/service-core/src/auth/test/config.spec.ts create mode 100644 services/service-core/src/config/config.module.ts create mode 100644 services/service-core/src/config/config.validator.ts create mode 100644 services/service-core/src/config/config.yaml create mode 100644 services/service-core/src/config/test/config.validator.spec.ts diff --git a/package-lock.json b/package-lock.json index 141b708c..eb50d123 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6831,6 +6831,12 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" }, + "node_modules/@types/js-yaml": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.8.tgz", + "integrity": "sha512-m6jnPk1VhlYRiLFm3f8X9Uep761f+CK8mHyS65LutH2OhmBF0BeMEjHgg05usH8PLZMWWc/BUR9RPmkvpWnyRA==", + "dev": true + }, "node_modules/@types/jsdom": { "version": "20.0.1", "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-20.0.1.tgz", @@ -28470,6 +28476,7 @@ "dotenv": "^16.3.1", "graphql": "^16.8.0", "highlight.js": "^11.8.0", + "js-yaml": "^4.1.0", "jsonwebtoken": "^9.0.2", "keycloak-connect": "^21.1.2", "lint-staged": "^13.2.3", @@ -28484,6 +28491,7 @@ "@nestjs/testing": "^10.0.0", "@types/express": "^4.17.17", "@types/jest": "^29.5.2", + "@types/js-yaml": "^4.0.8", "@types/jsonwebtoken": "^9.0.3", "@types/node": "^20.3.1", "@types/supertest": "^2.0.12", @@ -28709,6 +28717,11 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "services/service-core/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + }, "services/service-core/node_modules/dotenv": { "version": "16.3.1", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz", @@ -28789,6 +28802,17 @@ } } }, + "services/service-core/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, "services/service-core/node_modules/prettier": { "version": "2.8.8", "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", diff --git a/services/service-core/.env b/services/service-core/.env new file mode 100644 index 00000000..79f147c5 --- /dev/null +++ b/services/service-core/.env @@ -0,0 +1,10 @@ +DB_HOST=localhost +DB_PORT=5433 +DB_USERNAME=leulseged +DB_PASSWORD=Leul12g79! +DB_DATABASE=humanitech +POSTGRES_DATA=/home/leul/Documents/humanitech_db +KEYCLOAK_ADMIN=ciam_admin +KEYCLOAK_ADMIN_PASSWORD=CcGY{c3EsGNP\196 +KEYCLOAK_DATA=/home/leul/Documents/keycloak +ADMIN_CLIENT_SECRET=nEfK7JbMmPFKKwJ1DvXlhjSue6qcV28k \ No newline at end of file diff --git a/services/service-core/nest-cli.json b/services/service-core/nest-cli.json index f9aa683b..c2587d74 100644 --- a/services/service-core/nest-cli.json +++ b/services/service-core/nest-cli.json @@ -4,5 +4,6 @@ "sourceRoot": "src", "compilerOptions": { "deleteOutDir": true - } + }, + "assets": [{"include": "./src/config/*.yaml", "outDir": "./dist/config"}] } diff --git a/services/service-core/package.json b/services/service-core/package.json index c5723a30..23aac56b 100644 --- a/services/service-core/package.json +++ b/services/service-core/package.json @@ -35,6 +35,7 @@ "dotenv": "^16.3.1", "graphql": "^16.8.0", "highlight.js": "^11.8.0", + "js-yaml": "^4.1.0", "jsonwebtoken": "^9.0.2", "keycloak-connect": "^21.1.2", "lint-staged": "^13.2.3", @@ -49,6 +50,7 @@ "@nestjs/testing": "^10.0.0", "@types/express": "^4.17.17", "@types/jest": "^29.5.2", + "@types/js-yaml": "^4.0.8", "@types/jsonwebtoken": "^9.0.3", "@types/node": "^20.3.1", "@types/supertest": "^2.0.12", diff --git a/services/service-core/src/app.module.ts b/services/service-core/src/app.module.ts index 3d2c5cc5..fdf65803 100644 --- a/services/service-core/src/app.module.ts +++ b/services/service-core/src/app.module.ts @@ -23,15 +23,13 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { ConfigModule, ConfigService } from '@nestjs/config'; import { KeycloakConnectModule } from 'nest-keycloak-connect'; import { KeycloakAuthGuard } from './auth/keycloak.guard'; -import { configuration } from './config/config.service'; @Module({ imports: [ TypeOrmModule.forRootAsync({ imports: [ ConfigModule.forRoot({ - isGlobal: true, - load: [configuration] + isGlobal: true }) ], useFactory: (configService: ConfigService) => ({ diff --git a/services/service-core/src/auth/config.ts b/services/service-core/src/auth/config.ts deleted file mode 100644 index 8b2188fc..00000000 --- a/services/service-core/src/auth/config.ts +++ /dev/null @@ -1,21 +0,0 @@ -/** - * Humanitech Supply Trail - * - * Copyright (c) Humanitech, Peter Rogov and Contributors - * - * Website: https://humanitech.net - * Repository: https://github.com/humanitech-net/supply-trail - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -export const Config: Readonly<{ - adminClientSecret: string; - keycloakAdmin: string; - keycloakAdminPassword: string; -}> = { - adminClientSecret: 'ADMIN_CLIENT_SECRET', - keycloakAdmin: 'KEYCLOAK_ADMIN', - keycloakAdminPassword: 'KEYCLOAK_ADMIN_PASSWORD' -}; diff --git a/services/service-core/src/auth/keycloak.service.ts b/services/service-core/src/auth/keycloak.service.ts index 1011d50b..ad9f92d5 100644 --- a/services/service-core/src/auth/keycloak.service.ts +++ b/services/service-core/src/auth/keycloak.service.ts @@ -13,19 +13,18 @@ import { Injectable } from '@nestjs/common'; import { verify } from 'jsonwebtoken'; import axios from 'axios'; -import { ConfigService } from '@nestjs/config'; -import { Config } from './config'; +import { ConfigService } from '../config/config.service'; import { UpdateUser } from 'src/graphql/users/users.entity'; @Injectable() export class KeycloakService { constructor(private readonly configService: ConfigService) {} - private keycloak = this.configService.get('keycloak'); + private keycloak = this.configService.getKcConfig(); + private local = this.configService.getLocalConfig(); async getPublicKey() { try { - const { realmUrl } = this.keycloak; - const response = await axios.get(realmUrl); + const response = await axios.get(this.keycloak.realmUrl); const publicKey = response.data.public_key; return `-----BEGIN PUBLIC KEY-----\n${publicKey}\n-----END PUBLIC KEY-----`; } catch (error) { @@ -34,20 +33,18 @@ export class KeycloakService { } async getAdminToken() { - const { grantType, clientId, realmUrl } = this.keycloak; - const { keycloakAdmin, keycloakAdminPassword, adminClientSecret } = Config; const params = new URLSearchParams({ - username: this.configService.get(keycloakAdmin), - password: this.configService.get(keycloakAdminPassword), - grant_type: grantType, - client_id: clientId, - client_secret: this.configService.get(adminClientSecret) + username: this.local.KEYCLOAK_ADMIN, + password: this.local.KEYCLOAK_ADMIN_PASSWORD, + grant_type: this.keycloak.grantType, + client_id: this.keycloak.clientId, + client_secret: this.local.ADMIN_CLIENT_SECRET }); const requestBody = params.toString(); try { const getTokenData = await fetch( - `${realmUrl}/protocol/openid-connect/token`, + `${this.keycloak.realmUrl}/protocol/openid-connect/token`, { method: 'POST', headers: { diff --git a/services/service-core/src/auth/test/config.spec.ts b/services/service-core/src/auth/test/config.spec.ts deleted file mode 100644 index ab19688d..00000000 --- a/services/service-core/src/auth/test/config.spec.ts +++ /dev/null @@ -1,26 +0,0 @@ -/** - * Humanitech Supply Trail - * - * Copyright (c) Humanitech, Peter Rogov and Contributors - * - * Website: https://humanitech.net - * Repository: https://github.com/humanitech-net/supply-trail - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ -import { Config } from '../config'; - -describe('Config object', () => { - it('should have the correct adminClientSecret', () => { - expect(Config.adminClientSecret).toEqual('ADMIN_CLIENT_SECRET'); - }); - - it('should have the correct keycloakAdmin', () => { - expect(Config.keycloakAdmin).toEqual('KEYCLOAK_ADMIN'); - }); - - it('should have the correct keycloakAdminPassword', () => { - expect(Config.keycloakAdminPassword).toEqual('KEYCLOAK_ADMIN_PASSWORD'); - }); -}); diff --git a/services/service-core/src/auth/test/keycloak.service.spec.ts b/services/service-core/src/auth/test/keycloak.service.spec.ts index 8c0d65f8..9284edc8 100644 --- a/services/service-core/src/auth/test/keycloak.service.spec.ts +++ b/services/service-core/src/auth/test/keycloak.service.spec.ts @@ -14,7 +14,10 @@ import { Test, TestingModule } from '@nestjs/testing'; import { KeycloakService } from '../keycloak.service'; import axios from 'axios'; import { verify } from 'jsonwebtoken'; -import { ConfigService } from '@nestjs/config'; +import { ConfigService } from '../../config/config.service'; +import * as dotenv from 'dotenv'; + +dotenv.config(); jest.mock('axios'); jest.mock('jsonwebtoken'); @@ -32,16 +35,24 @@ describe('KeycloakService', () => { }).compile(); keycloakService = module.get(KeycloakService); - - keycloakService['keycloak'] = { - grantType: 'your-grant-type', - clientId: 'your-client-id', - realmUrl: 'your-realm-url' - // Add other properties as needed for your test - }; }); describe('getPublicKey', () => { + it('should fetch and return public key', async () => { + (axios.get as jest.Mock).mockResolvedValue({ + data: { public_key: 'your_mocked_public_key' } + }); + + const publicKey = await keycloakService.getPublicKey(); + + expect(axios.get).toHaveBeenCalledWith( + 'https://dev.supply-trail.humanitech.net/auth/realms/humanitech' + ); + expect(publicKey).toBe( + '-----BEGIN PUBLIC KEY-----\nyour_mocked_public_key\n-----END PUBLIC KEY-----' + ); + }); + it('should throw an error if fetching public key fails', async () => { (axios.get as jest.Mock).mockRejectedValue(new Error(errorMessage)); @@ -72,21 +83,37 @@ describe('KeycloakService', () => { }); it('throws an error when it fails to fetch', async () => { - const mockError = new Error('Error: Fetch error'); + const mockFailedResponse = new Response(null, { + status: 400, + statusText: "Couldn't get token" + }); - jest.spyOn(global, 'fetch').mockRejectedValue(mockError); + jest.spyOn(global, 'fetch').mockResolvedValue(mockFailedResponse); await expect(keycloakService.getAdminToken()).rejects.toThrowError( - mockError + "Couldn't get token" + ); + }); + + it('throws an error with the status text when fetch is not OK', async () => { + const mockErrorResponse = new Response(null, { + status: 500, + statusText: "Couldn't get token" + }); + + jest.spyOn(global, 'fetch').mockResolvedValue(mockErrorResponse); + + await expect(keycloakService.getAdminToken()).rejects.toThrowError( + "Couldn't get token" ); }); }); describe('editUser', () => { + const mockToken = 'mock-token'; + it('calls getAdminToken and updates the user', async () => { - jest - .spyOn(keycloakService, 'getAdminToken') - .mockResolvedValue('mock-token'); + jest.spyOn(keycloakService, 'getAdminToken').mockResolvedValue(mockToken); const mockUserInput = { firstName: 'user', @@ -109,9 +136,7 @@ describe('KeycloakService', () => { }); it('returns "Try again, failed to update" if the update fails', async () => { - jest - .spyOn(keycloakService, 'getAdminToken') - .mockResolvedValue('mock-token'); + jest.spyOn(keycloakService, 'getAdminToken').mockResolvedValue(mockToken); const mockUserInput = { firstName: 'user', @@ -132,9 +157,72 @@ describe('KeycloakService', () => { expect(keycloakService.getAdminToken).toHaveBeenCalled(); expect(editUser).toBe('Try again, failed to update'); }); + + it('firstName is not allowed to be empty', async () => { + jest.spyOn(keycloakService, 'getAdminToken').mockResolvedValue(mockToken); + + const mockUserInput = { + firstName: '', + lastName: 'lastName', + username: 'username' + }; + + const mockID = 'ID'; + + const mockSuccessfulResponse = new Response(null, { + status: 200, + statusText: 'OK' + }); + + jest.spyOn(global, 'fetch').mockResolvedValue(mockSuccessfulResponse); + + await expect( + keycloakService.editUser(mockID, mockUserInput) + ).rejects.toThrowError('"firstName" is not allowed to be empty'); // Modify the error message to match the required string + }); }); describe('getUser', () => { + it('should decode a valid token and return user data', async () => { + const mockToken = 'your_mocked_valid_token'; + const mockPublicKey = 'your_mocked_public_key'; + + (axios.get as jest.Mock).mockResolvedValue({ + data: { public_key: mockPublicKey } + }); + + const decodedToken = { + sub: 'sample_sid', + given_name: 'John', + family_name: 'Doe', + email: 'john.doe@example.com', + preferred_username: 'johndoe' + }; + + (verify as jest.Mock).mockReturnValue(decodedToken); + + const userData = await keycloakService.getUser(mockToken); + + expect(axios.get).toHaveBeenCalledWith( + 'https://dev.supply-trail.humanitech.net/auth/realms/humanitech' + ); + expect(verify).toHaveBeenCalledWith( + mockToken, + `-----BEGIN PUBLIC KEY-----\n${mockPublicKey}\n-----END PUBLIC KEY-----`, + { + algorithms: ['RS256'] + } + ); + + expect(userData).toEqual({ + id: 'sample_sid', + firstName: 'John', + lastName: 'Doe', + email: 'john.doe@example.com', + username: 'johndoe' + }); + }); + it('should throw an error for an invalid token', async () => { const mockToken = 'your_mocked_invalid_token'; const mockPublicKey = 'your_mocked_public_key'; diff --git a/services/service-core/src/config/config.module.ts b/services/service-core/src/config/config.module.ts new file mode 100644 index 00000000..5443af69 --- /dev/null +++ b/services/service-core/src/config/config.module.ts @@ -0,0 +1,9 @@ +import { Module, Global } from '@nestjs/common'; +import { ConfigService } from './config.service'; + +@Global() +@Module({ + providers: [ConfigService], + exports: [ConfigService] +}) +export class ConfigModule {} diff --git a/services/service-core/src/config/config.service.ts b/services/service-core/src/config/config.service.ts index 7f1c0959..8842d691 100644 --- a/services/service-core/src/config/config.service.ts +++ b/services/service-core/src/config/config.service.ts @@ -10,10 +10,75 @@ * LICENSE file in the root directory of this source tree. */ -import { config } from 'dotenv'; import { readFileSync } from 'fs'; +import * as yaml from 'js-yaml'; +import { join } from 'path'; +import { + keycloakConfigSchema, + IKeycloakConfiguration, + ILocalConfiguration, + localConfigSchema +} from './config.validator'; -const envConfig = config({ path: './.local.env' }); -const jsonConfig = JSON.parse(readFileSync('./src/config/config.json', 'utf8')); +interface IAppConfiguration { + keycloak: IKeycloakConfiguration; + local: ILocalConfiguration; +} -export const configuration = () => ({ ...envConfig, ...jsonConfig }); +export class ConfigService { + private readonly configuration: IAppConfiguration; + + constructor() { + this.configuration = this.loadConfiguration(); + } + + private loadConfiguration(): IAppConfiguration { + const config = yaml.load( + readFileSync(join(__dirname, './config.yaml'), 'utf8') + ) as Record; + + const { error: configError, value: configValue } = + keycloakConfigSchema.validate(config.keycloak); + + if (configError) { + throw new Error(`Config validation error: ${configError.message}`); + } + + const keycloakConfig: IKeycloakConfiguration = configValue; + + const local = { + DB_HOST: process.env.DB_HOST, + DB_PORT: process.env.DB_PORT, + DB_USERNAME: process.env.DB_USERNAME, + DB_PASSWORD: process.env.DB_PASSWORD, + DB_DATABASE: process.env.DB_DATABASE, + POSTGRES_DATA: process.env.POSTGRES_DATA, + KEYCLOAK_ADMIN: process.env.KEYCLOAK_ADMIN, + KEYCLOAK_ADMIN_PASSWORD: process.env.KEYCLOAK_ADMIN_PASSWORD, + KEYCLOAK_DATA: process.env.KEYCLOAK_DATA, + ADMIN_CLIENT_SECRET: process.env.ADMIN_CLIENT_SECRET + }; + + const { error: localError, value: localValue } = + localConfigSchema.validate(local); + + if (localError) { + throw new Error(`Config validation error: ${localError.message}`); + } + + const localConfig: ILocalConfiguration = localValue; + + return { + keycloak: keycloakConfig, + local: localConfig + }; + } + + getKcConfig(): IKeycloakConfiguration { + return this.configuration.keycloak; + } + + getLocalConfig(): ILocalConfiguration { + return this.configuration.local; + } +} diff --git a/services/service-core/src/config/config.validator.ts b/services/service-core/src/config/config.validator.ts new file mode 100644 index 00000000..291dac44 --- /dev/null +++ b/services/service-core/src/config/config.validator.ts @@ -0,0 +1,47 @@ +import * as Joi from 'joi'; + +export interface IKeycloakConfiguration { + authServerUrl: string; + clientId: string; + realm: string; + nestClientId: string; + realmUrl: string; + adminUrl: string; + grantType: string; +} + +export interface ILocalConfiguration { + DB_HOST: string; + DB_PORT: string; + DB_USERNAME: string; + DB_PASSWORD: string; + DB_DATABASE: string; + POSTGRES_DATA: string; + KEYCLOAK_ADMIN: string; + KEYCLOAK_ADMIN_PASSWORD: string; + KEYCLOAK_DATA: string; + ADMIN_CLIENT_SECRET: string; +} + +export const keycloakConfigSchema = Joi.object({ + authServerUrl: Joi.string().required(), + clientId: Joi.string().required(), + realm: Joi.string().required(), + nestClientId: Joi.string().required(), + realmUrl: Joi.string().required(), + adminUrl: Joi.string().required(), + grantType: Joi.string().required() +}); + +export const localConfigSchema = Joi.object({ + DB_HOST: Joi.string().required(), + DB_PORT: Joi.string().required(), + DB_USERNAME: Joi.string().required(), + DB_PASSWORD: Joi.string().required(), + DB_DATABASE: Joi.string().required(), + POSTGRES_DATA: Joi.string().required(), + KEYCLOAK_ADMIN: Joi.string().required(), + KEYCLOAK_ADMIN_PASSWORD: Joi.string().required(), + KEYCLOAK_DATA: Joi.string().required(), + ADMIN_CLIENT_SECRET: Joi.string().required() +}); diff --git a/services/service-core/src/config/config.yaml b/services/service-core/src/config/config.yaml new file mode 100644 index 00000000..1a596e75 --- /dev/null +++ b/services/service-core/src/config/config.yaml @@ -0,0 +1,8 @@ +keycloak: + authServerUrl: "https://dev.supply-trail.humanitech.net/auth" + realm: "humanitech" + nestClientId: "nest-application" + realmUrl: "https://dev.supply-trail.humanitech.net/auth/realms/humanitech" + adminUrl: "https://dev.supply-trail.humanitech.net/auth/admin/realms/humanitech" + grantType: "password" + clientId: "admin-cli" \ No newline at end of file diff --git a/services/service-core/src/config/test/config.service.spec.ts b/services/service-core/src/config/test/config.service.spec.ts index a1afe9e2..5f2577b1 100644 --- a/services/service-core/src/config/test/config.service.spec.ts +++ b/services/service-core/src/config/test/config.service.spec.ts @@ -1,46 +1,57 @@ -/** - * Humanitech Supply Trail - * - * Copyright (c) Humanitech, Peter Rogov and Contributors - * - * Website: https://humanitech.net - * Repository: https://github.com/humanitech-net/supply-trail - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ +import { ConfigService } from '../config.service'; +import * as dotenv from 'dotenv'; -import { config } from 'dotenv'; -import { readFileSync } from 'fs'; -import { configuration } from '../config.service'; +dotenv.config(); -jest.mock('dotenv', () => ({ - config: jest.fn(() => ({ DOTENV_VAR: 'dotenv_value' })) -})); +describe('ConfigService', () => { + let configService: ConfigService; -jest.mock('fs', () => ({ - readFileSync: jest.fn(() => '{"JSON_VAR":"json_value"}') -})); + beforeEach(() => { + configService = new ConfigService(); + }); + + describe('loadConfiguration', () => { + it('should load the configuration from the config.yaml file', () => { + const config = configService['loadConfiguration'](); + expect(config).toBeDefined(); + expect(config.keycloak).toBeDefined(); + expect(config.local).toBeDefined(); + }); -describe('Configuration Module', () => { - it('should merge environment variables from dotenv and JSON', () => { - const configResult = configuration(); - expect(configResult).toEqual({ - DOTENV_VAR: 'dotenv_value', - JSON_VAR: 'json_value' + it('should throw an error if the configuration is invalid', () => { + const configService = new ConfigService(); + jest + .spyOn(configService as any, 'loadConfiguration') + .mockImplementation(() => { + throw new Error('Invalid configuration'); + }); + expect(() => (configService as any)['loadConfiguration']()).toThrow( + 'Invalid configuration' + ); }); }); - it('should call dotenv.config with the correct path', () => { - configuration(); - expect(config).toHaveBeenCalledWith({ path: './.local.env' }); + describe('getKcConfig', () => { + it('should return the Keycloak configuration', () => { + const kcConfig = configService.getKcConfig(); + expect(kcConfig).toBeDefined(); + }); }); - it('should call readFileSync with the correct path and encoding', () => { - configuration(); - expect(readFileSync).toHaveBeenCalledWith( - './src/config/config.json', - 'utf8' - ); + describe('getLocalConfig', () => { + it('should return the local configuration', () => { + const localConfig = configService.getLocalConfig(); + expect(localConfig).toBeDefined(); + expect(localConfig.DB_HOST).toBeDefined(); + expect(localConfig.DB_PORT).toBeDefined(); + expect(localConfig.DB_USERNAME).toBeDefined(); + expect(localConfig.DB_PASSWORD).toBeDefined(); + expect(localConfig.DB_DATABASE).toBeDefined(); + expect(localConfig.POSTGRES_DATA).toBeDefined(); + expect(localConfig.KEYCLOAK_ADMIN).toBeDefined(); + expect(localConfig.KEYCLOAK_ADMIN_PASSWORD).toBeDefined(); + expect(localConfig.KEYCLOAK_DATA).toBeDefined(); + expect(localConfig.ADMIN_CLIENT_SECRET).toBeDefined(); + }); }); }); diff --git a/services/service-core/src/config/test/config.validator.spec.ts b/services/service-core/src/config/test/config.validator.spec.ts new file mode 100644 index 00000000..c84b6eeb --- /dev/null +++ b/services/service-core/src/config/test/config.validator.spec.ts @@ -0,0 +1,112 @@ +/** + * Humanitech Supply Trail + * + * Copyright (c) Humanitech, Peter Rogov and Contributors + * + * Website: https://humanitech.net + * Repository: https://github.com/humanitech-net/supply-trail + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { keycloakConfigSchema, localConfigSchema } from '../config.validator'; + +describe('keycloakConfigSchema', () => { + it('should validate a valid keycloak configuration', () => { + const validConfig = { + authServerUrl: 'http://localhost:8080/auth', + clientId: 'my-client-id', + realm: 'my-realm', + nestClientId: 'my-nest-client-id', + realmUrl: 'http://localhost:8080/auth/realms/my-realm', + adminUrl: 'http://localhost:8080/auth/admin/realms/my-realm', + grantType: 'password' + }; + const { error } = keycloakConfigSchema.validate(validConfig); + expect(error).toBeUndefined(); + }); + + it('should not validate a keycloak configuration with missing required fields', () => { + const invalidConfig = { + authServerUrl: 'http://localhost:8080/auth', + clientId: 'my-client-id', + realm: 'my-realm', + nestClientId: 'my-nest-client-id', + realmUrl: 'http://localhost:8080/auth/realms/my-realm', + adminUrl: 'http://localhost:8080/auth/admin/realms/my-realm', + // grantType is missing + grantType: '' + }; + const { error } = keycloakConfigSchema.validate(invalidConfig); + expect(error).toBeDefined(); + }); + + it('should not validate a keycloak configuration with invalid fields', () => { + const invalidConfig = { + authServerUrl: 'http://localhost:8080/auth', + clientId: 'my-client-id', + realm: 'my-realm', + nestClientId: 'my-nest-client-id', + realmUrl: 'http://localhost:8080/auth/realms/my-realm', + adminUrl: 'http://localhost:8080/auth/admin/realms/my-realm', + grantType: 123 // grantType should be a string + }; + const { error } = keycloakConfigSchema.validate(invalidConfig); + expect(error).toBeDefined(); + }); +}); + +describe('localConfigSchema', () => { + it('should validate a valid local configuration', () => { + const validConfig = { + DB_HOST: 'localhost', + DB_PORT: '5432', + DB_USERNAME: 'my-username', + DB_PASSWORD: 'my-password', + DB_DATABASE: 'my-database', + POSTGRES_DATA: '/var/lib/postgresql/data', + KEYCLOAK_ADMIN: 'admin', + KEYCLOAK_ADMIN_PASSWORD: 'admin-password', + KEYCLOAK_DATA: '/opt/keycloak/data', + ADMIN_CLIENT_SECRET: 'my-client-secret' + }; + const { error } = localConfigSchema.validate(validConfig); + expect(error).toBeUndefined(); + }); + + it('should not validate a local configuration with missing required fields', () => { + const invalidConfig = { + DB_HOST: 'localhost', + DB_PORT: '5432', + DB_USERNAME: 'my-username', + DB_PASSWORD: 'my-password', + DB_DATABASE: 'my-database', + POSTGRES_DATA: '/var/lib/postgresql/data', + KEYCLOAK_ADMIN: 'admin', + KEYCLOAK_ADMIN_PASSWORD: 'admin-password', + // KEYCLOAK_DATA is missing + ADMIN_CLIENT_SECRET: 'my-client-secret', + KEYCLOAK_DATA: '' + }; + const { error } = localConfigSchema.validate(invalidConfig); + expect(error).toBeDefined(); + }); + + it('should not validate a local configuration with invalid fields', () => { + const invalidConfig = { + DB_HOST: 'localhost', + DB_PORT: '5432', + DB_USERNAME: 'my-username', + DB_PASSWORD: 'my-password', + DB_DATABASE: 'my-database', + POSTGRES_DATA: '/var/lib/postgresql/data', + KEYCLOAK_ADMIN: 'admin', + KEYCLOAK_ADMIN_PASSWORD: 'admin-password', + KEYCLOAK_DATA: '/opt/keycloak/data', + ADMIN_CLIENT_SECRET: 123 // ADMIN_CLIENT_SECRET should be a string + }; + const { error } = localConfigSchema.validate(invalidConfig); + expect(error).toBeDefined(); + }); +}); diff --git a/services/service-core/src/main.ts b/services/service-core/src/main.ts index 83db15ce..c80c3586 100644 --- a/services/service-core/src/main.ts +++ b/services/service-core/src/main.ts @@ -12,8 +12,10 @@ import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; +import * as dotenv from 'dotenv'; async function bootstrap() { + dotenv.config(); const app = await NestFactory.create(AppModule); app.enableCors({ origin: '*', From f9c0aea3243842f998c618445896b17d0e9d32de Mon Sep 17 00:00:00 2001 From: mikiyas-dev Date: Tue, 31 Oct 2023 15:52:39 +0300 Subject: [PATCH 13/22] new-custom-config-service --- services/service-core/nest-cli.json | 6 +- services/service-core/src/app.module.ts | 8 +- .../service-core/src/auth/keycloak.service.ts | 4 +- .../src/auth/test/keycloak.service.spec.ts | 4 +- .../service-core/src/config/config.module.ts | 9 -- .../service-core/src/config/config.service.ts | 3 +- .../src/config/test/config.service.spec.ts | 123 +++++++++++------- .../src/config/test/config.validator.spec.ts | 109 +++++++--------- .../src/graphql/users/users.module.ts | 9 +- 9 files changed, 145 insertions(+), 130 deletions(-) delete mode 100644 services/service-core/src/config/config.module.ts diff --git a/services/service-core/nest-cli.json b/services/service-core/nest-cli.json index c2587d74..45034263 100644 --- a/services/service-core/nest-cli.json +++ b/services/service-core/nest-cli.json @@ -3,7 +3,7 @@ "collection": "@nestjs/schematics", "sourceRoot": "src", "compilerOptions": { - "deleteOutDir": true - }, - "assets": [{"include": "./src/config/*.yaml", "outDir": "./dist/config"}] + "deleteOutDir": true, + "assets": [{"include": "**/*.yaml", "outDir": "./dist"}] + } } diff --git a/services/service-core/src/app.module.ts b/services/service-core/src/app.module.ts index fdf65803..f67b2a8e 100644 --- a/services/service-core/src/app.module.ts +++ b/services/service-core/src/app.module.ts @@ -23,6 +23,7 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { ConfigModule, ConfigService } from '@nestjs/config'; import { KeycloakConnectModule } from 'nest-keycloak-connect'; import { KeycloakAuthGuard } from './auth/keycloak.guard'; +import { CustomConfigService } from './config/config.service'; @Module({ imports: [ @@ -51,14 +52,15 @@ import { KeycloakAuthGuard } from './auth/keycloak.guard'; KeycloakConnectModule.registerAsync({ imports: [ConfigModule], - useFactory: async (configService: ConfigService) => { + useFactory: async () => { + const configService = new CustomConfigService(); const { realm, nestClientId, authServerUrl } = - configService.get('keycloak'); + configService.getKcConfig(); return { authServerUrl, realm, resource: nestClientId, - secret: configService.get('KEYCLOAK_SECRET'), + secret: configService.getLocalConfig().ADMIN_CLIENT_SECRET, 'public-client': true, verifyTokenAudience: true, 'confidential-port': 0 diff --git a/services/service-core/src/auth/keycloak.service.ts b/services/service-core/src/auth/keycloak.service.ts index 6a0c5fb6..fb261021 100644 --- a/services/service-core/src/auth/keycloak.service.ts +++ b/services/service-core/src/auth/keycloak.service.ts @@ -13,13 +13,13 @@ import { Injectable } from '@nestjs/common'; import { verify } from 'jsonwebtoken'; import axios from 'axios'; -import { ConfigService } from '../config/config.service'; +import { CustomConfigService } from '../config/config.service'; import { UpdateUser } from 'src/graphql/users/users.entity'; import { userInputValidator } from './keycloak.validator'; @Injectable() export class KeycloakService { - constructor(private readonly configService: ConfigService) {} + constructor(private readonly configService: CustomConfigService) {} private keycloak = this.configService.getKcConfig(); private local = this.configService.getLocalConfig(); diff --git a/services/service-core/src/auth/test/keycloak.service.spec.ts b/services/service-core/src/auth/test/keycloak.service.spec.ts index 9284edc8..5807f779 100644 --- a/services/service-core/src/auth/test/keycloak.service.spec.ts +++ b/services/service-core/src/auth/test/keycloak.service.spec.ts @@ -14,7 +14,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { KeycloakService } from '../keycloak.service'; import axios from 'axios'; import { verify } from 'jsonwebtoken'; -import { ConfigService } from '../../config/config.service'; +import { CustomConfigService } from '../../config/config.service'; import * as dotenv from 'dotenv'; dotenv.config(); @@ -31,7 +31,7 @@ describe('KeycloakService', () => { jest.clearAllMocks(); const module: TestingModule = await Test.createTestingModule({ - providers: [KeycloakService, ConfigService] + providers: [KeycloakService, CustomConfigService] }).compile(); keycloakService = module.get(KeycloakService); diff --git a/services/service-core/src/config/config.module.ts b/services/service-core/src/config/config.module.ts deleted file mode 100644 index 5443af69..00000000 --- a/services/service-core/src/config/config.module.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Module, Global } from '@nestjs/common'; -import { ConfigService } from './config.service'; - -@Global() -@Module({ - providers: [ConfigService], - exports: [ConfigService] -}) -export class ConfigModule {} diff --git a/services/service-core/src/config/config.service.ts b/services/service-core/src/config/config.service.ts index 8842d691..4a752d33 100644 --- a/services/service-core/src/config/config.service.ts +++ b/services/service-core/src/config/config.service.ts @@ -19,13 +19,14 @@ import { ILocalConfiguration, localConfigSchema } from './config.validator'; +import Joi from 'joi'; interface IAppConfiguration { keycloak: IKeycloakConfiguration; local: ILocalConfiguration; } -export class ConfigService { +export class CustomConfigService { private readonly configuration: IAppConfiguration; constructor() { diff --git a/services/service-core/src/config/test/config.service.spec.ts b/services/service-core/src/config/test/config.service.spec.ts index 5f2577b1..3421df54 100644 --- a/services/service-core/src/config/test/config.service.spec.ts +++ b/services/service-core/src/config/test/config.service.spec.ts @@ -1,57 +1,92 @@ -import { ConfigService } from '../config.service'; -import * as dotenv from 'dotenv'; +/** + * Humanitech Supply Trail + * + * Copyright (c) Humanitech, Peter Rogov and Contributors + * + * Website: https://humanitech.net + * Repository: https://github.com/humanitech-net/supply-trail + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ -dotenv.config(); +import { CustomConfigService } from '../config.service'; +import * as yaml from 'js-yaml'; + +jest.mock('js-yaml', () => ({ + load: jest.fn() +})); + +const commonConfig = { + DB_HOST: 'localhost', + DB_PORT: '5432', + DB_USERNAME: 'my-username', + DB_PASSWORD: 'my-password', + DB_DATABASE: 'my-database', + POSTGRES_DATA: '/var/lib/postgresql/data', + KEYCLOAK_ADMIN: 'admin', + KEYCLOAK_ADMIN_PASSWORD: 'admin-password', + KEYCLOAK_DATA: '/opt/keycloak/data', + ADMIN_CLIENT_SECRET: 'my-client-secret' +}; + +const mockConfig = { + authServerUrl: 'http://localhost:8080/auth', + clientId: 'my-client-id', + realm: 'my-realm', + nestClientId: 'my-nest-client-id', + realmUrl: 'http://localhost:8080/auth/realms/my-realm', + adminUrl: 'http://localhost:8080/auth/admin/realms/my-realm', + grantType: 'password' +}; + +const setMockYamlLoad = (mock) => { + const mockYamlLoad = yaml.load as jest.Mock; + mockYamlLoad.mockReturnValue({ keycloak: mock }); +}; describe('ConfigService', () => { - let configService: ConfigService; + const originalEnv = process.env; beforeEach(() => { - configService = new ConfigService(); + jest.resetAllMocks(); + process.env = { + ...originalEnv, + ...commonConfig + }; }); - describe('loadConfiguration', () => { - it('should load the configuration from the config.yaml file', () => { - const config = configService['loadConfiguration'](); - expect(config).toBeDefined(); - expect(config.keycloak).toBeDefined(); - expect(config.local).toBeDefined(); - }); - - it('should throw an error if the configuration is invalid', () => { - const configService = new ConfigService(); - jest - .spyOn(configService as any, 'loadConfiguration') - .mockImplementation(() => { - throw new Error('Invalid configuration'); - }); - expect(() => (configService as any)['loadConfiguration']()).toThrow( - 'Invalid configuration' - ); - }); + afterEach(() => { + process.env = originalEnv; }); - describe('getKcConfig', () => { - it('should return the Keycloak configuration', () => { - const kcConfig = configService.getKcConfig(); - expect(kcConfig).toBeDefined(); - }); + it('should load configuration properly', () => { + setMockYamlLoad(mockConfig); + const configService = new CustomConfigService(); + + expect(yaml.load).toHaveBeenCalledWith(expect.any(String)); + expect(configService.getKcConfig()).toEqual(mockConfig); }); - describe('getLocalConfig', () => { - it('should return the local configuration', () => { - const localConfig = configService.getLocalConfig(); - expect(localConfig).toBeDefined(); - expect(localConfig.DB_HOST).toBeDefined(); - expect(localConfig.DB_PORT).toBeDefined(); - expect(localConfig.DB_USERNAME).toBeDefined(); - expect(localConfig.DB_PASSWORD).toBeDefined(); - expect(localConfig.DB_DATABASE).toBeDefined(); - expect(localConfig.POSTGRES_DATA).toBeDefined(); - expect(localConfig.KEYCLOAK_ADMIN).toBeDefined(); - expect(localConfig.KEYCLOAK_ADMIN_PASSWORD).toBeDefined(); - expect(localConfig.KEYCLOAK_DATA).toBeDefined(); - expect(localConfig.ADMIN_CLIENT_SECRET).toBeDefined(); - }); + it('should throw error on invalid configuration', () => { + setMockYamlLoad({ invalidKey: 'invalidValue' }); + expect(() => new CustomConfigService()).toThrow('Config validation error'); + }); + + it('should get local configuration properly', () => { + setMockYamlLoad(mockConfig); + const configService = new CustomConfigService(); + const result = configService.getLocalConfig(); + + expect(result).toEqual(commonConfig); + }); + + it('should throw error on invalid local configuration', () => { + process.env = { + DB_HOST: '' + }; + + setMockYamlLoad(mockConfig); + expect(() => new CustomConfigService()).toThrow('Config validation error'); }); }); diff --git a/services/service-core/src/config/test/config.validator.spec.ts b/services/service-core/src/config/test/config.validator.spec.ts index c84b6eeb..7bf9c882 100644 --- a/services/service-core/src/config/test/config.validator.spec.ts +++ b/services/service-core/src/config/test/config.validator.spec.ts @@ -12,101 +12,80 @@ import { keycloakConfigSchema, localConfigSchema } from '../config.validator'; +const validateConfig = (schema, config) => { + const { error } = schema.validate(config); + return error; +}; + describe('keycloakConfigSchema', () => { + const validKeycloakConfig = { + authServerUrl: 'http://localhost:8080/auth', + clientId: 'my-client-id', + realm: 'my-realm', + nestClientId: 'my-nest-client-id', + realmUrl: 'http://localhost:8080/auth/realms/my-realm', + adminUrl: 'http://localhost:8080/auth/admin/realms/my-realm', + grantType: 'password' + }; + it('should validate a valid keycloak configuration', () => { - const validConfig = { - authServerUrl: 'http://localhost:8080/auth', - clientId: 'my-client-id', - realm: 'my-realm', - nestClientId: 'my-nest-client-id', - realmUrl: 'http://localhost:8080/auth/realms/my-realm', - adminUrl: 'http://localhost:8080/auth/admin/realms/my-realm', - grantType: 'password' - }; - const { error } = keycloakConfigSchema.validate(validConfig); + const error = validateConfig(keycloakConfigSchema, validKeycloakConfig); expect(error).toBeUndefined(); }); it('should not validate a keycloak configuration with missing required fields', () => { - const invalidConfig = { - authServerUrl: 'http://localhost:8080/auth', - clientId: 'my-client-id', - realm: 'my-realm', - nestClientId: 'my-nest-client-id', - realmUrl: 'http://localhost:8080/auth/realms/my-realm', - adminUrl: 'http://localhost:8080/auth/admin/realms/my-realm', - // grantType is missing + const invalidKeycloakConfig = { + ...validKeycloakConfig, grantType: '' }; - const { error } = keycloakConfigSchema.validate(invalidConfig); + const error = validateConfig(keycloakConfigSchema, invalidKeycloakConfig); expect(error).toBeDefined(); }); it('should not validate a keycloak configuration with invalid fields', () => { - const invalidConfig = { - authServerUrl: 'http://localhost:8080/auth', - clientId: 'my-client-id', - realm: 'my-realm', - nestClientId: 'my-nest-client-id', - realmUrl: 'http://localhost:8080/auth/realms/my-realm', - adminUrl: 'http://localhost:8080/auth/admin/realms/my-realm', - grantType: 123 // grantType should be a string + const invalidKeycloakConfig = { + ...validKeycloakConfig, + grantType: 123 }; - const { error } = keycloakConfigSchema.validate(invalidConfig); + const error = validateConfig(keycloakConfigSchema, invalidKeycloakConfig); expect(error).toBeDefined(); }); }); describe('localConfigSchema', () => { + const validLocalConfig = { + DB_HOST: 'localhost', + DB_PORT: '5432', + DB_USERNAME: 'my-username', + DB_PASSWORD: 'my-password', + DB_DATABASE: 'my-database', + POSTGRES_DATA: '/var/lib/postgresql/data', + KEYCLOAK_ADMIN: 'admin', + KEYCLOAK_ADMIN_PASSWORD: 'admin-password', + KEYCLOAK_DATA: '/opt/keycloak/data', + ADMIN_CLIENT_SECRET: 'my-client-secret' + }; + it('should validate a valid local configuration', () => { - const validConfig = { - DB_HOST: 'localhost', - DB_PORT: '5432', - DB_USERNAME: 'my-username', - DB_PASSWORD: 'my-password', - DB_DATABASE: 'my-database', - POSTGRES_DATA: '/var/lib/postgresql/data', - KEYCLOAK_ADMIN: 'admin', - KEYCLOAK_ADMIN_PASSWORD: 'admin-password', - KEYCLOAK_DATA: '/opt/keycloak/data', - ADMIN_CLIENT_SECRET: 'my-client-secret' - }; - const { error } = localConfigSchema.validate(validConfig); + const error = validateConfig(localConfigSchema, validLocalConfig); expect(error).toBeUndefined(); }); it('should not validate a local configuration with missing required fields', () => { - const invalidConfig = { - DB_HOST: 'localhost', - DB_PORT: '5432', - DB_USERNAME: 'my-username', - DB_PASSWORD: 'my-password', - DB_DATABASE: 'my-database', - POSTGRES_DATA: '/var/lib/postgresql/data', - KEYCLOAK_ADMIN: 'admin', - KEYCLOAK_ADMIN_PASSWORD: 'admin-password', - // KEYCLOAK_DATA is missing - ADMIN_CLIENT_SECRET: 'my-client-secret', + const invalidLocalConfig = { + ...validLocalConfig, KEYCLOAK_DATA: '' }; - const { error } = localConfigSchema.validate(invalidConfig); + const error = validateConfig(localConfigSchema, invalidLocalConfig); expect(error).toBeDefined(); }); it('should not validate a local configuration with invalid fields', () => { - const invalidConfig = { - DB_HOST: 'localhost', - DB_PORT: '5432', - DB_USERNAME: 'my-username', - DB_PASSWORD: 'my-password', - DB_DATABASE: 'my-database', - POSTGRES_DATA: '/var/lib/postgresql/data', - KEYCLOAK_ADMIN: 'admin', - KEYCLOAK_ADMIN_PASSWORD: 'admin-password', - KEYCLOAK_DATA: '/opt/keycloak/data', - ADMIN_CLIENT_SECRET: 123 // ADMIN_CLIENT_SECRET should be a string + const invalidLocalConfig = { + ...validLocalConfig, + ADMIN_CLIENT_SECRET: 123 }; - const { error } = localConfigSchema.validate(invalidConfig); + const error = validateConfig(localConfigSchema, invalidLocalConfig); expect(error).toBeDefined(); }); }); diff --git a/services/service-core/src/graphql/users/users.module.ts b/services/service-core/src/graphql/users/users.module.ts index 75457910..0725fc4c 100644 --- a/services/service-core/src/graphql/users/users.module.ts +++ b/services/service-core/src/graphql/users/users.module.ts @@ -15,8 +15,15 @@ import { UsersService } from './users.service'; import { UsersResolver } from './users.resolver'; import { KeycloakService } from '../../auth/keycloak.service'; import { ConfigService } from '@nestjs/config'; +import { CustomConfigService } from 'src/config/config.service'; @Module({ - providers: [UsersService, UsersResolver, KeycloakService, ConfigService] + providers: [ + UsersService, + UsersResolver, + KeycloakService, + ConfigService, + CustomConfigService + ] }) export class UsersModule {} From 8b7046465a6ba42d5e0df3d545c90195e1b48864 Mon Sep 17 00:00:00 2001 From: leulseged gebremedhin Date: Sun, 5 Nov 2023 13:55:42 +0300 Subject: [PATCH 14/22] update the new config service --- services/service-core/src/app.module.ts | 17 +- .../service-core/src/auth/keycloak.service.ts | 23 +-- .../src/auth/test/keycloak.service.spec.ts | 64 +++++++- .../service-core/src/config/config.dto.ts | 79 +++++++++ services/service-core/src/config/config.json | 11 -- .../service-core/src/config/config.service.ts | 73 ++------- .../src/config/config.validator.ts | 47 ------ services/service-core/src/config/config.yaml | 26 ++- .../src/config/test/config.dto.spec.ts | 42 +++++ .../src/config/test/config.service.spec.ts | 151 ++++++++++-------- .../src/config/test/config.validator.spec.ts | 91 ----------- 11 files changed, 320 insertions(+), 304 deletions(-) create mode 100644 services/service-core/src/config/config.dto.ts delete mode 100644 services/service-core/src/config/config.json delete mode 100644 services/service-core/src/config/config.validator.ts create mode 100644 services/service-core/src/config/test/config.dto.spec.ts delete mode 100644 services/service-core/src/config/test/config.validator.spec.ts diff --git a/services/service-core/src/app.module.ts b/services/service-core/src/app.module.ts index f67b2a8e..ea714593 100644 --- a/services/service-core/src/app.module.ts +++ b/services/service-core/src/app.module.ts @@ -24,6 +24,7 @@ import { ConfigModule, ConfigService } from '@nestjs/config'; import { KeycloakConnectModule } from 'nest-keycloak-connect'; import { KeycloakAuthGuard } from './auth/keycloak.guard'; import { CustomConfigService } from './config/config.service'; +import { loadavg } from 'os'; @Module({ imports: [ @@ -51,22 +52,20 @@ import { CustomConfigService } from './config/config.service'; }), KeycloakConnectModule.registerAsync({ - imports: [ConfigModule], useFactory: async () => { const configService = new CustomConfigService(); - const { realm, nestClientId, authServerUrl } = - configService.getKcConfig(); + const { keycloak, local } = configService.get(); + return { - authServerUrl, - realm, - resource: nestClientId, - secret: configService.getLocalConfig().ADMIN_CLIENT_SECRET, + authServerUrl: keycloak.authServerUrl, + realm: keycloak.realm, + resource: keycloak.nestClientId, + secret: local.ADMIN_CLIENT_SECRET, 'public-client': true, verifyTokenAudience: true, 'confidential-port': 0 }; - }, - inject: [ConfigService] + } }), DatabaseModule, diff --git a/services/service-core/src/auth/keycloak.service.ts b/services/service-core/src/auth/keycloak.service.ts index fb261021..eae3f5f8 100644 --- a/services/service-core/src/auth/keycloak.service.ts +++ b/services/service-core/src/auth/keycloak.service.ts @@ -20,12 +20,12 @@ import { userInputValidator } from './keycloak.validator'; @Injectable() export class KeycloakService { constructor(private readonly configService: CustomConfigService) {} - private keycloak = this.configService.getKcConfig(); - private local = this.configService.getLocalConfig(); + private config = this.configService.get(); async getPublicKey() { + const { realmUrl } = this.config.keycloak; try { - const response = await axios.get(this.keycloak.realmUrl); + const response = await axios.get(realmUrl); const publicKey = response.data.public_key; return `-----BEGIN PUBLIC KEY-----\n${publicKey}\n-----END PUBLIC KEY-----`; } catch (error) { @@ -34,18 +34,21 @@ export class KeycloakService { } async getAdminToken() { + const { KEYCLOAK_ADMIN, KEYCLOAK_ADMIN_PASSWORD, ADMIN_CLIENT_SECRET } = + this.config.local; + const { realmUrl, grantType, clientId } = this.config.keycloak; const params = new URLSearchParams({ - username: this.local.KEYCLOAK_ADMIN, - password: this.local.KEYCLOAK_ADMIN_PASSWORD, - grant_type: this.keycloak.grantType, - client_id: this.keycloak.clientId, - client_secret: this.local.ADMIN_CLIENT_SECRET + username: KEYCLOAK_ADMIN, + password: KEYCLOAK_ADMIN_PASSWORD, + grant_type: grantType, + client_id: clientId, + client_secret: ADMIN_CLIENT_SECRET }); const requestBody = params.toString(); try { const getTokenData = await fetch( - `${this.keycloak.realmUrl}/protocol/openid-connect/token`, + `${realmUrl}/protocol/openid-connect/token`, { method: 'POST', headers: { @@ -86,7 +89,7 @@ export class KeycloakService { } async editUser(id: string, userInput: UpdateUser) { - const { adminUrl } = this.keycloak; + const { adminUrl } = this.config.keycloak; const accessToken = await this.getAdminToken(); const { error } = userInputValidator.validate(userInput); if (error) { diff --git a/services/service-core/src/auth/test/keycloak.service.spec.ts b/services/service-core/src/auth/test/keycloak.service.spec.ts index 5807f779..c7a4dbc3 100644 --- a/services/service-core/src/auth/test/keycloak.service.spec.ts +++ b/services/service-core/src/auth/test/keycloak.service.spec.ts @@ -16,6 +16,17 @@ import axios from 'axios'; import { verify } from 'jsonwebtoken'; import { CustomConfigService } from '../../config/config.service'; import * as dotenv from 'dotenv'; +import * as yaml from 'js-yaml'; + +jest.mock('js-yaml', () => ({ + load: jest.fn() +})); + +// Define the setMockYamlLoad function +const setMockYamlLoad = (mock) => { + const mockYamlLoad = yaml.load as jest.Mock; + mockYamlLoad.mockReturnValue(mock); +}; dotenv.config(); @@ -23,15 +34,42 @@ jest.mock('axios'); jest.mock('jsonwebtoken'); describe('KeycloakService', () => { + // let keycloakService: KeycloakService; + // Define a mock configuration with the expected structure + const mockConfig = { + keycloak: { + realmUrl: 'https://dev.supply-trail.humanitech.net/auth/realms/humanitech' + // other keycloak properties + }, + local: { + // local properties + } + }; + const errorMessage = 'Failed to fetch Public Key'; + // beforeEach(async () => { + // jest.clearAllMocks(); + + // const module: TestingModule = await Test.createTestingModule({ + // providers: [KeycloakService, CustomConfigService] + // }).compile(); + + // keycloakService = module.get(KeycloakService); + // }); beforeEach(async () => { jest.clearAllMocks(); const module: TestingModule = await Test.createTestingModule({ - providers: [KeycloakService, CustomConfigService] + providers: [ + KeycloakService, + { + provide: CustomConfigService, + useValue: { get: jest.fn().mockReturnValue(mockConfig) } // Mock the config service + } + ] }).compile(); keycloakService = module.get(KeycloakService); @@ -240,12 +278,22 @@ describe('KeycloakService', () => { ); }); - it('should throw an error if fetching public key fails', async () => { - (axios.get as jest.Mock).mockRejectedValue(new Error(errorMessage)); - - await expect( - keycloakService.getUser('your_mocked_valid_token') - ).rejects.toThrow(errorMessage); - }); + // it('should throw error on invalid local configuration', () => { + // // Mock an invalid local configuration + // const invalidConfig = { + // keycloak: { + // realmUrl: 'https://example.com/auth/realms/humanitech' + // } + // // Missing the 'local' property + // }; + + // // Set the YAML mock to return the invalid configuration + // setMockYamlLoad(invalidConfig); + + // // Expect the constructor of CustomConfigService to throw an error + // expect(() => new CustomConfigService()).toThrowError( + // /Config validation error/ + // ); + // }); }); }); diff --git a/services/service-core/src/config/config.dto.ts b/services/service-core/src/config/config.dto.ts new file mode 100644 index 00000000..f9d4367c --- /dev/null +++ b/services/service-core/src/config/config.dto.ts @@ -0,0 +1,79 @@ +import { IsString, IsNotEmpty } from 'class-validator'; + +export class KeycloakConfigurationDto { + @IsString() + @IsNotEmpty() + authServerUrl: string; + + @IsString() + @IsNotEmpty() + clientId: string; + + @IsString() + @IsNotEmpty() + realm: string; + + @IsString() + @IsNotEmpty() + nestClientId: string; + + @IsString() + @IsNotEmpty() + realmUrl: string; + + @IsString() + @IsNotEmpty() + adminUrl: string; + + @IsString() + @IsNotEmpty() + grantType: string; +} +export class LocalConfigurationDto { + @IsString() + @IsNotEmpty() + DB_HOST: string; + + @IsString() + @IsNotEmpty() + DB_PORT: string; + + @IsString() + @IsNotEmpty() + DB_USERNAME: string; + + @IsString() + @IsNotEmpty() + DB_PASSWORD: string; + + @IsString() + @IsNotEmpty() + DB_DATABASE: string; + + @IsString() + @IsNotEmpty() + POSTGRES_DATA: string; + + @IsString() + @IsNotEmpty() + KEYCLOAK_ADMIN: string; + + @IsString() + @IsNotEmpty() + KEYCLOAK_ADMIN_PASSWORD: string; + + @IsString() + @IsNotEmpty() + KEYCLOAK_DATA: string; + + @IsString() + @IsNotEmpty() + ADMIN_CLIENT_SECRET: string; +} +export class AppConfigDto { + @IsNotEmpty() + keycloak: KeycloakConfigurationDto; + + @IsNotEmpty() + local: LocalConfigurationDto; +} diff --git a/services/service-core/src/config/config.json b/services/service-core/src/config/config.json deleted file mode 100644 index 9d0ccff7..00000000 --- a/services/service-core/src/config/config.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "keycloak": { - "authServerUrl": "https://dev.supply-trail.humanitech.net/auth", - "realm": "humanitech", - "nestClientId": "nest-application", - "realmUrl": "https://dev.supply-trail.humanitech.net/auth/realms/humanitech", - "adminUrl": "https://dev.supply-trail.humanitech.net/auth/admin/realms/humanitech", - "grantType": "password", - "clientId": "admin-cli" - } -} diff --git a/services/service-core/src/config/config.service.ts b/services/service-core/src/config/config.service.ts index 4a752d33..74505d28 100644 --- a/services/service-core/src/config/config.service.ts +++ b/services/service-core/src/config/config.service.ts @@ -9,77 +9,34 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ - import { readFileSync } from 'fs'; -import * as yaml from 'js-yaml'; +import { load } from 'js-yaml'; import { join } from 'path'; -import { - keycloakConfigSchema, - IKeycloakConfiguration, - ILocalConfiguration, - localConfigSchema -} from './config.validator'; -import Joi from 'joi'; - -interface IAppConfiguration { - keycloak: IKeycloakConfiguration; - local: ILocalConfiguration; -} +import { AppConfigDto } from './config.dto'; export class CustomConfigService { - private readonly configuration: IAppConfiguration; + private readonly configuration: AppConfigDto; constructor() { this.configuration = this.loadConfiguration(); } - private loadConfiguration(): IAppConfiguration { - const config = yaml.load( - readFileSync(join(__dirname, './config.yaml'), 'utf8') - ) as Record; - - const { error: configError, value: configValue } = - keycloakConfigSchema.validate(config.keycloak); - - if (configError) { - throw new Error(`Config validation error: ${configError.message}`); - } - - const keycloakConfig: IKeycloakConfiguration = configValue; - - const local = { - DB_HOST: process.env.DB_HOST, - DB_PORT: process.env.DB_PORT, - DB_USERNAME: process.env.DB_USERNAME, - DB_PASSWORD: process.env.DB_PASSWORD, - DB_DATABASE: process.env.DB_DATABASE, - POSTGRES_DATA: process.env.POSTGRES_DATA, - KEYCLOAK_ADMIN: process.env.KEYCLOAK_ADMIN, - KEYCLOAK_ADMIN_PASSWORD: process.env.KEYCLOAK_ADMIN_PASSWORD, - KEYCLOAK_DATA: process.env.KEYCLOAK_DATA, - ADMIN_CLIENT_SECRET: process.env.ADMIN_CLIENT_SECRET - }; - - const { error: localError, value: localValue } = - localConfigSchema.validate(local); + private loadConfiguration(): AppConfigDto { + const yamlFilePath = './config.yaml'; + const yamlConfig = load( + readFileSync(join(__dirname, yamlFilePath), 'utf8') + ) as AppConfigDto; - if (localError) { - throw new Error(`Config validation error: ${localError.message}`); + for (const key in yamlConfig.local) { + if (process.env[key]) { + yamlConfig.local[key] = process.env[key]; + } } - const localConfig: ILocalConfiguration = localValue; - - return { - keycloak: keycloakConfig, - local: localConfig - }; - } - - getKcConfig(): IKeycloakConfiguration { - return this.configuration.keycloak; + return yamlConfig; } - getLocalConfig(): ILocalConfiguration { - return this.configuration.local; + get(): AppConfigDto { + return this.configuration; } } diff --git a/services/service-core/src/config/config.validator.ts b/services/service-core/src/config/config.validator.ts deleted file mode 100644 index 291dac44..00000000 --- a/services/service-core/src/config/config.validator.ts +++ /dev/null @@ -1,47 +0,0 @@ -import * as Joi from 'joi'; - -export interface IKeycloakConfiguration { - authServerUrl: string; - clientId: string; - realm: string; - nestClientId: string; - realmUrl: string; - adminUrl: string; - grantType: string; -} - -export interface ILocalConfiguration { - DB_HOST: string; - DB_PORT: string; - DB_USERNAME: string; - DB_PASSWORD: string; - DB_DATABASE: string; - POSTGRES_DATA: string; - KEYCLOAK_ADMIN: string; - KEYCLOAK_ADMIN_PASSWORD: string; - KEYCLOAK_DATA: string; - ADMIN_CLIENT_SECRET: string; -} - -export const keycloakConfigSchema = Joi.object({ - authServerUrl: Joi.string().required(), - clientId: Joi.string().required(), - realm: Joi.string().required(), - nestClientId: Joi.string().required(), - realmUrl: Joi.string().required(), - adminUrl: Joi.string().required(), - grantType: Joi.string().required() -}); - -export const localConfigSchema = Joi.object({ - DB_HOST: Joi.string().required(), - DB_PORT: Joi.string().required(), - DB_USERNAME: Joi.string().required(), - DB_PASSWORD: Joi.string().required(), - DB_DATABASE: Joi.string().required(), - POSTGRES_DATA: Joi.string().required(), - KEYCLOAK_ADMIN: Joi.string().required(), - KEYCLOAK_ADMIN_PASSWORD: Joi.string().required(), - KEYCLOAK_DATA: Joi.string().required(), - ADMIN_CLIENT_SECRET: Joi.string().required() -}); diff --git a/services/service-core/src/config/config.yaml b/services/service-core/src/config/config.yaml index 1a596e75..b2b399c1 100644 --- a/services/service-core/src/config/config.yaml +++ b/services/service-core/src/config/config.yaml @@ -1,8 +1,20 @@ keycloak: - authServerUrl: "https://dev.supply-trail.humanitech.net/auth" - realm: "humanitech" - nestClientId: "nest-application" - realmUrl: "https://dev.supply-trail.humanitech.net/auth/realms/humanitech" - adminUrl: "https://dev.supply-trail.humanitech.net/auth/admin/realms/humanitech" - grantType: "password" - clientId: "admin-cli" \ No newline at end of file + authServerUrl: 'https://dev.supply-trail.humanitech.net/auth' + realm: 'humanitech' + nestClientId: 'nest-application' + realmUrl: 'https://dev.supply-trail.humanitech.net/auth/realms/humanitech' + adminUrl: 'https://dev.supply-trail.humanitech.net/auth/admin/realms/humanitech' + grantType: 'password' + clientId: 'admin-cli' + +local: + DB_HOST: 'localhost' + DB_PORT: '5432' + DB_USERNAME: 'myuser' + DB_PASSWORD: 'mypassword' + DB_DATABASE: 'mydb' + POSTGRES_DATA: '/path/to/postgres/data' + KEYCLOAK_ADMIN: 'admin-user' + KEYCLOAK_ADMIN_PASSWORD: 'admin-password' + KEYCLOAK_DATA: '/path/to/keycloak/data' + ADMIN_CLIENT_SECRET: 'client-secret' diff --git a/services/service-core/src/config/test/config.dto.spec.ts b/services/service-core/src/config/test/config.dto.spec.ts new file mode 100644 index 00000000..66285332 --- /dev/null +++ b/services/service-core/src/config/test/config.dto.spec.ts @@ -0,0 +1,42 @@ +import { + AppConfigDto, + KeycloakConfigurationDto, + LocalConfigurationDto +} from '../config.dto'; +import { IsNotEmpty, IsString, validate } from 'class-validator'; + +describe('AppConfigDto', () => { + it('should validate a valid AppConfigDto object', async () => { + const validAppConfigDto = new AppConfigDto(); + validAppConfigDto.keycloak = new KeycloakConfigurationDto(); + validAppConfigDto.local = new LocalConfigurationDto(); + + const errors = await validate(validAppConfigDto); + + expect(errors.length).toBe(0); + }); + + it('should invalidate an AppConfigDto object with missing keycloak property', async () => { + const invalidAppConfigDto = new AppConfigDto(); + invalidAppConfigDto.local = new LocalConfigurationDto(); + + const errors = await validate(invalidAppConfigDto); + + expect(errors.length).toBe(1); + expect(errors[0].property).toBe('keycloak'); + expect(errors[0].constraints.isNotEmpty).toBe( + 'keycloak should not be empty' + ); + }); + + it('should invalidate an AppConfigDto object with missing local property', async () => { + const invalidAppConfigDto = new AppConfigDto(); + invalidAppConfigDto.keycloak = new KeycloakConfigurationDto(); + + const errors = await validate(invalidAppConfigDto); + + expect(errors.length).toBe(1); + expect(errors[0].property).toBe('local'); + expect(errors[0].constraints.isNotEmpty).toBe('local should not be empty'); + }); +}); diff --git a/services/service-core/src/config/test/config.service.spec.ts b/services/service-core/src/config/test/config.service.spec.ts index 3421df54..37176261 100644 --- a/services/service-core/src/config/test/config.service.spec.ts +++ b/services/service-core/src/config/test/config.service.spec.ts @@ -9,84 +9,109 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ - import { CustomConfigService } from '../config.service'; +import { AppConfigDto } from '../config.dto'; +import * as fs from 'fs'; import * as yaml from 'js-yaml'; -jest.mock('js-yaml', () => ({ - load: jest.fn() -})); - -const commonConfig = { - DB_HOST: 'localhost', - DB_PORT: '5432', - DB_USERNAME: 'my-username', - DB_PASSWORD: 'my-password', - DB_DATABASE: 'my-database', - POSTGRES_DATA: '/var/lib/postgresql/data', - KEYCLOAK_ADMIN: 'admin', - KEYCLOAK_ADMIN_PASSWORD: 'admin-password', - KEYCLOAK_DATA: '/opt/keycloak/data', - ADMIN_CLIENT_SECRET: 'my-client-secret' -}; - -const mockConfig = { - authServerUrl: 'http://localhost:8080/auth', - clientId: 'my-client-id', - realm: 'my-realm', - nestClientId: 'my-nest-client-id', - realmUrl: 'http://localhost:8080/auth/realms/my-realm', - adminUrl: 'http://localhost:8080/auth/admin/realms/my-realm', - grantType: 'password' -}; - -const setMockYamlLoad = (mock) => { - const mockYamlLoad = yaml.load as jest.Mock; - mockYamlLoad.mockReturnValue({ keycloak: mock }); -}; - -describe('ConfigService', () => { - const originalEnv = process.env; +jest.mock('fs'); +jest.mock('js-yaml'); +describe('CustomConfigService', () => { + // Reset the mocked functions before each test beforeEach(() => { - jest.resetAllMocks(); - process.env = { - ...originalEnv, - ...commonConfig - }; - }); - - afterEach(() => { - process.env = originalEnv; + jest.clearAllMocks(); }); - it('should load configuration properly', () => { - setMockYamlLoad(mockConfig); - const configService = new CustomConfigService(); + it('should load the configuration from the config.yaml file', () => { + // Arrange + const configData: AppConfigDto = { + keycloak: { + authServerUrl: 'https://example.com/auth', + realm: 'humanitech', + nestClientId: 'nest-application', + realmUrl: + 'https://dev.supply-trail.humanitech.net/auth/realms/humanitech', + adminUrl: + 'https://dev.supply-trail.humanitech.net/auth/admin/realms/humanitech', + grantType: 'password', + clientId: 'admin-cli' + }, + local: { + DB_HOST: 'localhost', + DB_PORT: '5432', + DB_USERNAME: 'myuser', + DB_PASSWORD: 'mypassword', + DB_DATABASE: 'mydb', + POSTGRES_DATA: '/path/to/postgres/data', + KEYCLOAK_ADMIN: 'admin-user', + KEYCLOAK_ADMIN_PASSWORD: 'admin-password', + KEYCLOAK_DATA: '/path/to/keycloak/data', + ADMIN_CLIENT_SECRET: 'client-secret' + } + }; - expect(yaml.load).toHaveBeenCalledWith(expect.any(String)); - expect(configService.getKcConfig()).toEqual(mockConfig); - }); + // Mock fs.readFileSync to return the YAML data + (fs.readFileSync as jest.Mock).mockReturnValue('yaml content'); - it('should throw error on invalid configuration', () => { - setMockYamlLoad({ invalidKey: 'invalidValue' }); - expect(() => new CustomConfigService()).toThrow('Config validation error'); - }); + // Mock js-yaml.load to return the expected config data + (yaml.load as jest.Mock).mockReturnValue(configData); - it('should get local configuration properly', () => { - setMockYamlLoad(mockConfig); + // Act const configService = new CustomConfigService(); - const result = configService.getLocalConfig(); - expect(result).toEqual(commonConfig); + // Assert + expect(fs.readFileSync).toHaveBeenCalledWith( + expect.stringContaining('config.yaml'), + 'utf8' + ); + expect(yaml.load).toHaveBeenCalledWith('yaml content'); + expect(configService.get()).toEqual(configData); }); - it('should throw error on invalid local configuration', () => { - process.env = { - DB_HOST: '' + it('should override the configuration values from environment variables', () => { + // Arrange + const configData: AppConfigDto = { + keycloak: { + authServerUrl: 'https://example.com/auth', + realm: 'humanitech', + nestClientId: 'nest-application', + realmUrl: + 'https://dev.supply-trail.humanitech.net/auth/realms/humanitech', + adminUrl: + 'https://dev.supply-trail.humanitech.net/auth/admin/realms/humanitech', + grantType: 'password', + clientId: 'admin-cli' + }, + local: { + DB_HOST: 'localhost', + DB_PORT: '5432', + DB_USERNAME: 'myuser', + DB_PASSWORD: 'mypassword', + DB_DATABASE: 'mydb', + POSTGRES_DATA: '/path/to/postgres/data', + KEYCLOAK_ADMIN: 'admin-user', + KEYCLOAK_ADMIN_PASSWORD: 'admin-password', + KEYCLOAK_DATA: '/path/to/keycloak/data', + ADMIN_CLIENT_SECRET: 'client-secret' + } }; - setMockYamlLoad(mockConfig); - expect(() => new CustomConfigService()).toThrow('Config validation error'); + // Mock fs.readFileSync to return the YAML data + (fs.readFileSync as jest.Mock).mockReturnValue('yaml content'); + + // Mock js-yaml.load to return the expected config data + (yaml.load as jest.Mock).mockReturnValue(configData); + + // Set environment variables + process.env.DB_HOST = 'env_db_host'; + // Add other environment variables as needed + + // Act + const configService = new CustomConfigService(); + + // Assert + expect(configService.get().local.DB_HOST).toBe('env_db_host'); + // Add assertions for other environment variables as needed }); }); diff --git a/services/service-core/src/config/test/config.validator.spec.ts b/services/service-core/src/config/test/config.validator.spec.ts deleted file mode 100644 index 7bf9c882..00000000 --- a/services/service-core/src/config/test/config.validator.spec.ts +++ /dev/null @@ -1,91 +0,0 @@ -/** - * Humanitech Supply Trail - * - * Copyright (c) Humanitech, Peter Rogov and Contributors - * - * Website: https://humanitech.net - * Repository: https://github.com/humanitech-net/supply-trail - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -import { keycloakConfigSchema, localConfigSchema } from '../config.validator'; - -const validateConfig = (schema, config) => { - const { error } = schema.validate(config); - return error; -}; - -describe('keycloakConfigSchema', () => { - const validKeycloakConfig = { - authServerUrl: 'http://localhost:8080/auth', - clientId: 'my-client-id', - realm: 'my-realm', - nestClientId: 'my-nest-client-id', - realmUrl: 'http://localhost:8080/auth/realms/my-realm', - adminUrl: 'http://localhost:8080/auth/admin/realms/my-realm', - grantType: 'password' - }; - - it('should validate a valid keycloak configuration', () => { - const error = validateConfig(keycloakConfigSchema, validKeycloakConfig); - expect(error).toBeUndefined(); - }); - - it('should not validate a keycloak configuration with missing required fields', () => { - const invalidKeycloakConfig = { - ...validKeycloakConfig, - grantType: '' - }; - const error = validateConfig(keycloakConfigSchema, invalidKeycloakConfig); - expect(error).toBeDefined(); - }); - - it('should not validate a keycloak configuration with invalid fields', () => { - const invalidKeycloakConfig = { - ...validKeycloakConfig, - grantType: 123 - }; - const error = validateConfig(keycloakConfigSchema, invalidKeycloakConfig); - expect(error).toBeDefined(); - }); -}); - -describe('localConfigSchema', () => { - const validLocalConfig = { - DB_HOST: 'localhost', - DB_PORT: '5432', - DB_USERNAME: 'my-username', - DB_PASSWORD: 'my-password', - DB_DATABASE: 'my-database', - POSTGRES_DATA: '/var/lib/postgresql/data', - KEYCLOAK_ADMIN: 'admin', - KEYCLOAK_ADMIN_PASSWORD: 'admin-password', - KEYCLOAK_DATA: '/opt/keycloak/data', - ADMIN_CLIENT_SECRET: 'my-client-secret' - }; - - it('should validate a valid local configuration', () => { - const error = validateConfig(localConfigSchema, validLocalConfig); - expect(error).toBeUndefined(); - }); - - it('should not validate a local configuration with missing required fields', () => { - const invalidLocalConfig = { - ...validLocalConfig, - KEYCLOAK_DATA: '' - }; - const error = validateConfig(localConfigSchema, invalidLocalConfig); - expect(error).toBeDefined(); - }); - - it('should not validate a local configuration with invalid fields', () => { - const invalidLocalConfig = { - ...validLocalConfig, - ADMIN_CLIENT_SECRET: 123 - }; - const error = validateConfig(localConfigSchema, invalidLocalConfig); - expect(error).toBeDefined(); - }); -}); From c36ea4a008888956e8078eaf094c5ecba5cdabea Mon Sep 17 00:00:00 2001 From: leulseged gebremedhin Date: Sun, 5 Nov 2023 14:16:35 +0300 Subject: [PATCH 15/22] reduce duplication and code smell --- .../src/auth/test/keycloak.service.spec.ts | 48 ++-------- .../service-core/src/config/config.dto.ts | 11 +++ .../src/config/test/config.dto.spec.ts | 13 ++- .../src/config/test/config.service.spec.ts | 96 +++++++------------ 4 files changed, 66 insertions(+), 102 deletions(-) diff --git a/services/service-core/src/auth/test/keycloak.service.spec.ts b/services/service-core/src/auth/test/keycloak.service.spec.ts index c7a4dbc3..f34dcbd7 100644 --- a/services/service-core/src/auth/test/keycloak.service.spec.ts +++ b/services/service-core/src/auth/test/keycloak.service.spec.ts @@ -15,8 +15,7 @@ import { KeycloakService } from '../keycloak.service'; import axios from 'axios'; import { verify } from 'jsonwebtoken'; import { CustomConfigService } from '../../config/config.service'; -import * as dotenv from 'dotenv'; -import * as yaml from 'js-yaml'; +import { load } from 'js-yaml'; jest.mock('js-yaml', () => ({ load: jest.fn() @@ -24,24 +23,23 @@ jest.mock('js-yaml', () => ({ // Define the setMockYamlLoad function const setMockYamlLoad = (mock) => { - const mockYamlLoad = yaml.load as jest.Mock; + const mockYamlLoad = load as jest.Mock; mockYamlLoad.mockReturnValue(mock); }; -dotenv.config(); - jest.mock('axios'); jest.mock('jsonwebtoken'); describe('KeycloakService', () => { // let keycloakService: KeycloakService; + const realmUrl = + 'https://dev.supply-trail.humanitech.net/auth/realms/humanitech'; // Define a mock configuration with the expected structure const mockConfig = { keycloak: { - realmUrl: 'https://dev.supply-trail.humanitech.net/auth/realms/humanitech' - // other keycloak properties + realmUrl }, local: { // local properties @@ -49,16 +47,6 @@ describe('KeycloakService', () => { }; const errorMessage = 'Failed to fetch Public Key'; - - // beforeEach(async () => { - // jest.clearAllMocks(); - - // const module: TestingModule = await Test.createTestingModule({ - // providers: [KeycloakService, CustomConfigService] - // }).compile(); - - // keycloakService = module.get(KeycloakService); - // }); beforeEach(async () => { jest.clearAllMocks(); @@ -83,9 +71,7 @@ describe('KeycloakService', () => { const publicKey = await keycloakService.getPublicKey(); - expect(axios.get).toHaveBeenCalledWith( - 'https://dev.supply-trail.humanitech.net/auth/realms/humanitech' - ); + expect(axios.get).toHaveBeenCalledWith(realmUrl); expect(publicKey).toBe( '-----BEGIN PUBLIC KEY-----\nyour_mocked_public_key\n-----END PUBLIC KEY-----' ); @@ -241,9 +227,7 @@ describe('KeycloakService', () => { const userData = await keycloakService.getUser(mockToken); - expect(axios.get).toHaveBeenCalledWith( - 'https://dev.supply-trail.humanitech.net/auth/realms/humanitech' - ); + expect(axios.get).toHaveBeenCalledWith(realmUrl); expect(verify).toHaveBeenCalledWith( mockToken, `-----BEGIN PUBLIC KEY-----\n${mockPublicKey}\n-----END PUBLIC KEY-----`, @@ -277,23 +261,5 @@ describe('KeycloakService', () => { 'Invalid Token' ); }); - - // it('should throw error on invalid local configuration', () => { - // // Mock an invalid local configuration - // const invalidConfig = { - // keycloak: { - // realmUrl: 'https://example.com/auth/realms/humanitech' - // } - // // Missing the 'local' property - // }; - - // // Set the YAML mock to return the invalid configuration - // setMockYamlLoad(invalidConfig); - - // // Expect the constructor of CustomConfigService to throw an error - // expect(() => new CustomConfigService()).toThrowError( - // /Config validation error/ - // ); - // }); }); }); diff --git a/services/service-core/src/config/config.dto.ts b/services/service-core/src/config/config.dto.ts index f9d4367c..97b3820d 100644 --- a/services/service-core/src/config/config.dto.ts +++ b/services/service-core/src/config/config.dto.ts @@ -1,3 +1,14 @@ +/** + * Humanitech Supply Trail + * + * Copyright (c) Humanitech, Peter Rogov and Contributors + * + * Website: https://humanitech.net + * Repository: https://github.com/humanitech-net/supply-trail + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ import { IsString, IsNotEmpty } from 'class-validator'; export class KeycloakConfigurationDto { diff --git a/services/service-core/src/config/test/config.dto.spec.ts b/services/service-core/src/config/test/config.dto.spec.ts index 66285332..d6ad50c2 100644 --- a/services/service-core/src/config/test/config.dto.spec.ts +++ b/services/service-core/src/config/test/config.dto.spec.ts @@ -1,9 +1,20 @@ +/** + * Humanitech Supply Trail + * + * Copyright (c) Humanitech, Peter Rogov and Contributors + * + * Website: https://humanitech.net + * Repository: https://github.com/humanitech-net/supply-trail + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ import { AppConfigDto, KeycloakConfigurationDto, LocalConfigurationDto } from '../config.dto'; -import { IsNotEmpty, IsString, validate } from 'class-validator'; +import { validate } from 'class-validator'; describe('AppConfigDto', () => { it('should validate a valid AppConfigDto object', async () => { diff --git a/services/service-core/src/config/test/config.service.spec.ts b/services/service-core/src/config/test/config.service.spec.ts index 37176261..db4379a9 100644 --- a/services/service-core/src/config/test/config.service.spec.ts +++ b/services/service-core/src/config/test/config.service.spec.ts @@ -11,8 +11,8 @@ */ import { CustomConfigService } from '../config.service'; import { AppConfigDto } from '../config.dto'; -import * as fs from 'fs'; -import * as yaml from 'js-yaml'; +import { readFileSync } from 'fs'; +import { load } from 'js-yaml'; jest.mock('fs'); jest.mock('js-yaml'); @@ -23,85 +23,61 @@ describe('CustomConfigService', () => { jest.clearAllMocks(); }); - it('should load the configuration from the config.yaml file', () => { - // Arrange - const configData: AppConfigDto = { - keycloak: { - authServerUrl: 'https://example.com/auth', - realm: 'humanitech', - nestClientId: 'nest-application', - realmUrl: - 'https://dev.supply-trail.humanitech.net/auth/realms/humanitech', - adminUrl: - 'https://dev.supply-trail.humanitech.net/auth/admin/realms/humanitech', - grantType: 'password', - clientId: 'admin-cli' - }, - local: { - DB_HOST: 'localhost', - DB_PORT: '5432', - DB_USERNAME: 'myuser', - DB_PASSWORD: 'mypassword', - DB_DATABASE: 'mydb', - POSTGRES_DATA: '/path/to/postgres/data', - KEYCLOAK_ADMIN: 'admin-user', - KEYCLOAK_ADMIN_PASSWORD: 'admin-password', - KEYCLOAK_DATA: '/path/to/keycloak/data', - ADMIN_CLIENT_SECRET: 'client-secret' - } - }; + const yamlContent = 'yaml-content'; + const configData: AppConfigDto = { + keycloak: { + authServerUrl: 'https://example.com/auth', + realm: 'humanitech', + nestClientId: 'nest-application', + realmUrl: + 'https://dev.supply-trail.humanitech.net/auth/realms/humanitech', + adminUrl: + 'https://dev.supply-trail.humanitech.net/auth/admin/realms/humanitech', + grantType: 'password', + clientId: 'admin-cli' + }, + local: { + DB_HOST: 'localhost', + DB_PORT: '5432', + DB_USERNAME: 'myuser', + DB_PASSWORD: 'mypassword', + DB_DATABASE: 'mydb', + POSTGRES_DATA: '/path/to/postgres/data', + KEYCLOAK_ADMIN: 'admin-user', + KEYCLOAK_ADMIN_PASSWORD: 'admin-password', + KEYCLOAK_DATA: '/path/to/keycloak/data', + ADMIN_CLIENT_SECRET: 'client-secret' + } + }; + + it('should load the configuration from the config.yaml file', () => { // Mock fs.readFileSync to return the YAML data - (fs.readFileSync as jest.Mock).mockReturnValue('yaml content'); + (readFileSync as jest.Mock).mockReturnValue(yamlContent); // Mock js-yaml.load to return the expected config data - (yaml.load as jest.Mock).mockReturnValue(configData); + (load as jest.Mock).mockReturnValue(configData); // Act const configService = new CustomConfigService(); // Assert - expect(fs.readFileSync).toHaveBeenCalledWith( + expect(readFileSync).toHaveBeenCalledWith( expect.stringContaining('config.yaml'), 'utf8' ); - expect(yaml.load).toHaveBeenCalledWith('yaml content'); + expect(load).toHaveBeenCalledWith(yamlContent); expect(configService.get()).toEqual(configData); }); it('should override the configuration values from environment variables', () => { // Arrange - const configData: AppConfigDto = { - keycloak: { - authServerUrl: 'https://example.com/auth', - realm: 'humanitech', - nestClientId: 'nest-application', - realmUrl: - 'https://dev.supply-trail.humanitech.net/auth/realms/humanitech', - adminUrl: - 'https://dev.supply-trail.humanitech.net/auth/admin/realms/humanitech', - grantType: 'password', - clientId: 'admin-cli' - }, - local: { - DB_HOST: 'localhost', - DB_PORT: '5432', - DB_USERNAME: 'myuser', - DB_PASSWORD: 'mypassword', - DB_DATABASE: 'mydb', - POSTGRES_DATA: '/path/to/postgres/data', - KEYCLOAK_ADMIN: 'admin-user', - KEYCLOAK_ADMIN_PASSWORD: 'admin-password', - KEYCLOAK_DATA: '/path/to/keycloak/data', - ADMIN_CLIENT_SECRET: 'client-secret' - } - }; // Mock fs.readFileSync to return the YAML data - (fs.readFileSync as jest.Mock).mockReturnValue('yaml content'); + (readFileSync as jest.Mock).mockReturnValue(yamlContent); // Mock js-yaml.load to return the expected config data - (yaml.load as jest.Mock).mockReturnValue(configData); + (load as jest.Mock).mockReturnValue(configData); // Set environment variables process.env.DB_HOST = 'env_db_host'; From b444d7b8c563002ea00c61313d25671e08aacb35 Mon Sep 17 00:00:00 2001 From: leulseged gebremedhin Date: Sun, 5 Nov 2023 14:31:16 +0300 Subject: [PATCH 16/22] remove code smell --- services/service-core/src/app.module.ts | 1 - services/service-core/src/auth/keycloak.service.ts | 2 +- .../service-core/src/auth/test/keycloak.service.spec.ts | 9 +++++---- services/service-core/src/main.ts | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/services/service-core/src/app.module.ts b/services/service-core/src/app.module.ts index ea714593..e8aeeedc 100644 --- a/services/service-core/src/app.module.ts +++ b/services/service-core/src/app.module.ts @@ -24,7 +24,6 @@ import { ConfigModule, ConfigService } from '@nestjs/config'; import { KeycloakConnectModule } from 'nest-keycloak-connect'; import { KeycloakAuthGuard } from './auth/keycloak.guard'; import { CustomConfigService } from './config/config.service'; -import { loadavg } from 'os'; @Module({ imports: [ diff --git a/services/service-core/src/auth/keycloak.service.ts b/services/service-core/src/auth/keycloak.service.ts index eae3f5f8..3b145178 100644 --- a/services/service-core/src/auth/keycloak.service.ts +++ b/services/service-core/src/auth/keycloak.service.ts @@ -20,7 +20,7 @@ import { userInputValidator } from './keycloak.validator'; @Injectable() export class KeycloakService { constructor(private readonly configService: CustomConfigService) {} - private config = this.configService.get(); + private readonly config = this.configService.get(); async getPublicKey() { const { realmUrl } = this.config.keycloak; diff --git a/services/service-core/src/auth/test/keycloak.service.spec.ts b/services/service-core/src/auth/test/keycloak.service.spec.ts index f34dcbd7..d39b448c 100644 --- a/services/service-core/src/auth/test/keycloak.service.spec.ts +++ b/services/service-core/src/auth/test/keycloak.service.spec.ts @@ -35,6 +35,7 @@ describe('KeycloakService', () => { let keycloakService: KeycloakService; const realmUrl = 'https://dev.supply-trail.humanitech.net/auth/realms/humanitech'; + const notGetToken = "Couldn't get token"; // Define a mock configuration with the expected structure const mockConfig = { @@ -109,26 +110,26 @@ describe('KeycloakService', () => { it('throws an error when it fails to fetch', async () => { const mockFailedResponse = new Response(null, { status: 400, - statusText: "Couldn't get token" + statusText: notGetToken }); jest.spyOn(global, 'fetch').mockResolvedValue(mockFailedResponse); await expect(keycloakService.getAdminToken()).rejects.toThrowError( - "Couldn't get token" + notGetToken ); }); it('throws an error with the status text when fetch is not OK', async () => { const mockErrorResponse = new Response(null, { status: 500, - statusText: "Couldn't get token" + statusText: notGetToken }); jest.spyOn(global, 'fetch').mockResolvedValue(mockErrorResponse); await expect(keycloakService.getAdminToken()).rejects.toThrowError( - "Couldn't get token" + notGetToken ); }); }); diff --git a/services/service-core/src/main.ts b/services/service-core/src/main.ts index c80c3586..11eedfb7 100644 --- a/services/service-core/src/main.ts +++ b/services/service-core/src/main.ts @@ -12,10 +12,10 @@ import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; -import * as dotenv from 'dotenv'; +import { config } from 'dotenv'; async function bootstrap() { - dotenv.config(); + config(); const app = await NestFactory.create(AppModule); app.enableCors({ origin: '*', From e567e6861d79c7d1e0c32d35c032bccb44a147c5 Mon Sep 17 00:00:00 2001 From: leulseged gebremedhin Date: Sun, 12 Nov 2023 16:40:22 +0300 Subject: [PATCH 17/22] manage the change request --- services/service-core/.gitignore | 2 +- services/service-core/src/app.module.ts | 29 ++++++++++--------- .../service-core/src/auth/keycloak.service.ts | 11 +++++-- .../service-core/src/config/config.dto.ts | 28 +++++++++--------- .../service-core/src/config/config.service.ts | 13 ++++++--- services/service-core/src/config/config.yaml | 10 +++---- .../src/config/test/config.dto.spec.ts | 10 ++++--- .../src/config/test/config.service.spec.ts | 16 +++++----- 8 files changed, 67 insertions(+), 52 deletions(-) diff --git a/services/service-core/.gitignore b/services/service-core/.gitignore index bf8ab8e5..f6e1467a 100644 --- a/services/service-core/.gitignore +++ b/services/service-core/.gitignore @@ -3,7 +3,7 @@ /node_modules .local.env - +.env # Logs logs *.log diff --git a/services/service-core/src/app.module.ts b/services/service-core/src/app.module.ts index e8aeeedc..8e4be4ca 100644 --- a/services/service-core/src/app.module.ts +++ b/services/service-core/src/app.module.ts @@ -33,17 +33,20 @@ import { CustomConfigService } from './config/config.service'; isGlobal: true }) ], - useFactory: (configService: ConfigService) => ({ - type: 'postgres', - host: configService.get('DB_HOST'), - port: Number(configService.get('DB_PORT')), - username: configService.get('DB_USERNAME'), - password: configService.get('DB_PASSWORD'), - database: configService.get('DB_DATABASE'), - entities: [User], - synchronize: true - }), - inject: [ConfigService] + useFactory: () => { + const configService = new CustomConfigService(); + const { database } = configService.get(); + return { + type: 'postgres', + host: database.DB_HOST, + port: Number(database.DB_PORT), + username: database.DB_USERNAME, + password: database.DB_PASSWORD, + database: database.DB_DATABASE, + entities: [User], + synchronize: true + }; + } }), GraphQLModule.forRoot({ driver: ApolloDriver, @@ -53,13 +56,13 @@ import { CustomConfigService } from './config/config.service'; KeycloakConnectModule.registerAsync({ useFactory: async () => { const configService = new CustomConfigService(); - const { keycloak, local } = configService.get(); + const { keycloak } = configService.get(); return { authServerUrl: keycloak.authServerUrl, realm: keycloak.realm, resource: keycloak.nestClientId, - secret: local.ADMIN_CLIENT_SECRET, + secret: keycloak.ADMIN_CLIENT_SECRET, 'public-client': true, verifyTokenAudience: true, 'confidential-port': 0 diff --git a/services/service-core/src/auth/keycloak.service.ts b/services/service-core/src/auth/keycloak.service.ts index 3b145178..d0a9643d 100644 --- a/services/service-core/src/auth/keycloak.service.ts +++ b/services/service-core/src/auth/keycloak.service.ts @@ -34,9 +34,14 @@ export class KeycloakService { } async getAdminToken() { - const { KEYCLOAK_ADMIN, KEYCLOAK_ADMIN_PASSWORD, ADMIN_CLIENT_SECRET } = - this.config.local; - const { realmUrl, grantType, clientId } = this.config.keycloak; + const { + realmUrl, + grantType, + clientId, + KEYCLOAK_ADMIN, + KEYCLOAK_ADMIN_PASSWORD, + ADMIN_CLIENT_SECRET + } = this.config.keycloak; const params = new URLSearchParams({ username: KEYCLOAK_ADMIN, password: KEYCLOAK_ADMIN_PASSWORD, diff --git a/services/service-core/src/config/config.dto.ts b/services/service-core/src/config/config.dto.ts index 97b3820d..67feed77 100644 --- a/services/service-core/src/config/config.dto.ts +++ b/services/service-core/src/config/config.dto.ts @@ -39,52 +39,52 @@ export class KeycloakConfigurationDto { @IsString() @IsNotEmpty() grantType: string; -} -export class LocalConfigurationDto { + @IsString() @IsNotEmpty() - DB_HOST: string; + KEYCLOAK_ADMIN: string; @IsString() @IsNotEmpty() - DB_PORT: string; + KEYCLOAK_ADMIN_PASSWORD: string; @IsString() @IsNotEmpty() - DB_USERNAME: string; + KEYCLOAK_DATA: string; @IsString() @IsNotEmpty() - DB_PASSWORD: string; - + ADMIN_CLIENT_SECRET: string; +} +export class LocalConfigurationDto { @IsString() @IsNotEmpty() - DB_DATABASE: string; + DB_HOST: string; @IsString() @IsNotEmpty() - POSTGRES_DATA: string; + DB_PORT: string; @IsString() @IsNotEmpty() - KEYCLOAK_ADMIN: string; + DB_USERNAME: string; @IsString() @IsNotEmpty() - KEYCLOAK_ADMIN_PASSWORD: string; + DB_PASSWORD: string; @IsString() @IsNotEmpty() - KEYCLOAK_DATA: string; + DB_DATABASE: string; @IsString() @IsNotEmpty() - ADMIN_CLIENT_SECRET: string; + POSTGRES_DATA: string; } export class AppConfigDto { @IsNotEmpty() keycloak: KeycloakConfigurationDto; @IsNotEmpty() - local: LocalConfigurationDto; + database: LocalConfigurationDto; } diff --git a/services/service-core/src/config/config.service.ts b/services/service-core/src/config/config.service.ts index 74505d28..e5a07525 100644 --- a/services/service-core/src/config/config.service.ts +++ b/services/service-core/src/config/config.service.ts @@ -13,6 +13,7 @@ import { readFileSync } from 'fs'; import { load } from 'js-yaml'; import { join } from 'path'; import { AppConfigDto } from './config.dto'; +import { NotFoundException } from '@nestjs/common'; export class CustomConfigService { private readonly configuration: AppConfigDto; @@ -27,16 +28,20 @@ export class CustomConfigService { readFileSync(join(__dirname, yamlFilePath), 'utf8') ) as AppConfigDto; - for (const key in yamlConfig.local) { - if (process.env[key]) { - yamlConfig.local[key] = process.env[key]; + for (const section in yamlConfig) { + if (yamlConfig && typeof yamlConfig === 'object') { + for (const key in yamlConfig[section]) { + if (process.env[key]) { + yamlConfig[section][key] = process.env[key]; + } + } } } return yamlConfig; } - get(): AppConfigDto { + get() { return this.configuration; } } diff --git a/services/service-core/src/config/config.yaml b/services/service-core/src/config/config.yaml index b2b399c1..756feb43 100644 --- a/services/service-core/src/config/config.yaml +++ b/services/service-core/src/config/config.yaml @@ -6,15 +6,15 @@ keycloak: adminUrl: 'https://dev.supply-trail.humanitech.net/auth/admin/realms/humanitech' grantType: 'password' clientId: 'admin-cli' + KEYCLOAK_ADMIN: 'admin-user' + KEYCLOAK_ADMIN_PASSWORD: 'admin-password' + KEYCLOAK_DATA: '/path/to/keycloak/data' + ADMIN_CLIENT_SECRET: 'client-secret' -local: +database: DB_HOST: 'localhost' DB_PORT: '5432' DB_USERNAME: 'myuser' DB_PASSWORD: 'mypassword' DB_DATABASE: 'mydb' POSTGRES_DATA: '/path/to/postgres/data' - KEYCLOAK_ADMIN: 'admin-user' - KEYCLOAK_ADMIN_PASSWORD: 'admin-password' - KEYCLOAK_DATA: '/path/to/keycloak/data' - ADMIN_CLIENT_SECRET: 'client-secret' diff --git a/services/service-core/src/config/test/config.dto.spec.ts b/services/service-core/src/config/test/config.dto.spec.ts index d6ad50c2..65f7918d 100644 --- a/services/service-core/src/config/test/config.dto.spec.ts +++ b/services/service-core/src/config/test/config.dto.spec.ts @@ -20,7 +20,7 @@ describe('AppConfigDto', () => { it('should validate a valid AppConfigDto object', async () => { const validAppConfigDto = new AppConfigDto(); validAppConfigDto.keycloak = new KeycloakConfigurationDto(); - validAppConfigDto.local = new LocalConfigurationDto(); + validAppConfigDto.database = new LocalConfigurationDto(); const errors = await validate(validAppConfigDto); @@ -29,7 +29,7 @@ describe('AppConfigDto', () => { it('should invalidate an AppConfigDto object with missing keycloak property', async () => { const invalidAppConfigDto = new AppConfigDto(); - invalidAppConfigDto.local = new LocalConfigurationDto(); + invalidAppConfigDto.database = new LocalConfigurationDto(); const errors = await validate(invalidAppConfigDto); @@ -47,7 +47,9 @@ describe('AppConfigDto', () => { const errors = await validate(invalidAppConfigDto); expect(errors.length).toBe(1); - expect(errors[0].property).toBe('local'); - expect(errors[0].constraints.isNotEmpty).toBe('local should not be empty'); + expect(errors[0].property).toBe('database'); + expect(errors[0].constraints.isNotEmpty).toBe( + 'database should not be empty' + ); }); }); diff --git a/services/service-core/src/config/test/config.service.spec.ts b/services/service-core/src/config/test/config.service.spec.ts index db4379a9..dd4b2ac4 100644 --- a/services/service-core/src/config/test/config.service.spec.ts +++ b/services/service-core/src/config/test/config.service.spec.ts @@ -35,19 +35,19 @@ describe('CustomConfigService', () => { adminUrl: 'https://dev.supply-trail.humanitech.net/auth/admin/realms/humanitech', grantType: 'password', - clientId: 'admin-cli' + clientId: 'admin-cli', + KEYCLOAK_ADMIN: 'admin-user', + KEYCLOAK_ADMIN_PASSWORD: 'admin-password', + KEYCLOAK_DATA: '/path/to/keycloak/data', + ADMIN_CLIENT_SECRET: 'client-secret' }, - local: { + database: { DB_HOST: 'localhost', DB_PORT: '5432', DB_USERNAME: 'myuser', DB_PASSWORD: 'mypassword', DB_DATABASE: 'mydb', - POSTGRES_DATA: '/path/to/postgres/data', - KEYCLOAK_ADMIN: 'admin-user', - KEYCLOAK_ADMIN_PASSWORD: 'admin-password', - KEYCLOAK_DATA: '/path/to/keycloak/data', - ADMIN_CLIENT_SECRET: 'client-secret' + POSTGRES_DATA: '/path/to/postgres/data' } }; @@ -87,7 +87,7 @@ describe('CustomConfigService', () => { const configService = new CustomConfigService(); // Assert - expect(configService.get().local.DB_HOST).toBe('env_db_host'); + expect(configService.get().database.DB_HOST).toBe('env_db_host'); // Add assertions for other environment variables as needed }); }); From 314361acc8dfe29bda5410957861e5c63cbff5e3 Mon Sep 17 00:00:00 2001 From: leulseged gebremedhin Date: Sun, 12 Nov 2023 16:52:39 +0300 Subject: [PATCH 18/22] remove smells --- services/service-core/src/app.module.ts | 2 +- services/service-core/src/config/config.service.ts | 9 +++------ 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/services/service-core/src/app.module.ts b/services/service-core/src/app.module.ts index 8e4be4ca..754dd176 100644 --- a/services/service-core/src/app.module.ts +++ b/services/service-core/src/app.module.ts @@ -20,7 +20,7 @@ import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo'; import { DatabaseModule } from './database/database.module'; import { User } from './database/users/users.entity'; import { TypeOrmModule } from '@nestjs/typeorm'; -import { ConfigModule, ConfigService } from '@nestjs/config'; +import { ConfigModule } from '@nestjs/config'; import { KeycloakConnectModule } from 'nest-keycloak-connect'; import { KeycloakAuthGuard } from './auth/keycloak.guard'; import { CustomConfigService } from './config/config.service'; diff --git a/services/service-core/src/config/config.service.ts b/services/service-core/src/config/config.service.ts index e5a07525..244bcb81 100644 --- a/services/service-core/src/config/config.service.ts +++ b/services/service-core/src/config/config.service.ts @@ -13,7 +13,6 @@ import { readFileSync } from 'fs'; import { load } from 'js-yaml'; import { join } from 'path'; import { AppConfigDto } from './config.dto'; -import { NotFoundException } from '@nestjs/common'; export class CustomConfigService { private readonly configuration: AppConfigDto; @@ -29,11 +28,9 @@ export class CustomConfigService { ) as AppConfigDto; for (const section in yamlConfig) { - if (yamlConfig && typeof yamlConfig === 'object') { - for (const key in yamlConfig[section]) { - if (process.env[key]) { - yamlConfig[section][key] = process.env[key]; - } + for (const key in yamlConfig[section]) { + if (process.env[key]) { + yamlConfig[section][key] = process.env[key]; } } } From aa38449cc2439604ed5f9dd4fe516d2e9965de97 Mon Sep 17 00:00:00 2001 From: leulseged gebremedhin Date: Sun, 12 Nov 2023 16:59:53 +0300 Subject: [PATCH 19/22] commit --- services/service-core/src/config/config.service.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/services/service-core/src/config/config.service.ts b/services/service-core/src/config/config.service.ts index 244bcb81..4915da00 100644 --- a/services/service-core/src/config/config.service.ts +++ b/services/service-core/src/config/config.service.ts @@ -28,11 +28,12 @@ export class CustomConfigService { ) as AppConfigDto; for (const section in yamlConfig) { - for (const key in yamlConfig[section]) { - if (process.env[key]) { - yamlConfig[section][key] = process.env[key]; + if (yamlConfig && typeof yamlConfig === 'object') + for (const key in yamlConfig[section]) { + if (process.env[key]) { + yamlConfig[section][key] = process.env[key]; + } } - } } return yamlConfig; From d10024f0014e130da4b24e5a210ef41e8a6158ba Mon Sep 17 00:00:00 2001 From: leulseged gebremedhin Date: Sun, 12 Nov 2023 17:12:02 +0300 Subject: [PATCH 20/22] sonar cloud fail --- services/service-core/src/config/config.service.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/services/service-core/src/config/config.service.ts b/services/service-core/src/config/config.service.ts index 244bcb81..4915da00 100644 --- a/services/service-core/src/config/config.service.ts +++ b/services/service-core/src/config/config.service.ts @@ -28,11 +28,12 @@ export class CustomConfigService { ) as AppConfigDto; for (const section in yamlConfig) { - for (const key in yamlConfig[section]) { - if (process.env[key]) { - yamlConfig[section][key] = process.env[key]; + if (yamlConfig && typeof yamlConfig === 'object') + for (const key in yamlConfig[section]) { + if (process.env[key]) { + yamlConfig[section][key] = process.env[key]; + } } - } } return yamlConfig; From cb52ebdabd80f3fd2d495249abe7bc6e3b9f0bff Mon Sep 17 00:00:00 2001 From: leulseged gebremedhin Date: Sun, 12 Nov 2023 17:17:41 +0300 Subject: [PATCH 21/22] remove env --- services/service-core/.env | 10 ---------- services/service-core/.gitignore | 1 - 2 files changed, 11 deletions(-) delete mode 100644 services/service-core/.env diff --git a/services/service-core/.env b/services/service-core/.env deleted file mode 100644 index 79f147c5..00000000 --- a/services/service-core/.env +++ /dev/null @@ -1,10 +0,0 @@ -DB_HOST=localhost -DB_PORT=5433 -DB_USERNAME=leulseged -DB_PASSWORD=Leul12g79! -DB_DATABASE=humanitech -POSTGRES_DATA=/home/leul/Documents/humanitech_db -KEYCLOAK_ADMIN=ciam_admin -KEYCLOAK_ADMIN_PASSWORD=CcGY{c3EsGNP\196 -KEYCLOAK_DATA=/home/leul/Documents/keycloak -ADMIN_CLIENT_SECRET=nEfK7JbMmPFKKwJ1DvXlhjSue6qcV28k \ No newline at end of file diff --git a/services/service-core/.gitignore b/services/service-core/.gitignore index f6e1467a..8d9e97d1 100644 --- a/services/service-core/.gitignore +++ b/services/service-core/.gitignore @@ -3,7 +3,6 @@ /node_modules .local.env -.env # Logs logs *.log From 6b166f0d56d31ffb9290708d8fe43f0e0320cf6a Mon Sep 17 00:00:00 2001 From: leulseged gebremedhin Date: Sun, 12 Nov 2023 17:26:26 +0300 Subject: [PATCH 22/22] ignore env --- services/service-core/.gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/services/service-core/.gitignore b/services/service-core/.gitignore index 8d9e97d1..df7558b7 100644 --- a/services/service-core/.gitignore +++ b/services/service-core/.gitignore @@ -2,6 +2,7 @@ /dist /node_modules +.env .local.env # Logs logs