diff --git a/package-lock.json b/package-lock.json index b56989e..f8e4b3f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5299,10 +5299,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" @@ -5313,8 +5314,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" }, @@ -6861,6 +6863,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", @@ -28501,7 +28509,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", @@ -28509,9 +28517,11 @@ "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", "joi": "^17.11.0", + "js-yaml": "^4.1.0", "jsonwebtoken": "^9.0.2", "keycloak-connect": "^21.1.2", "lint-staged": "^13.2.3", @@ -28526,6 +28536,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", @@ -28751,6 +28762,22 @@ "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", + "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", @@ -28820,6 +28847,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/.gitignore b/services/service-core/.gitignore index bf8ab8e..df7558b 100644 --- a/services/service-core/.gitignore +++ b/services/service-core/.gitignore @@ -2,8 +2,8 @@ /dist /node_modules +.env .local.env - # Logs logs *.log diff --git a/services/service-core/nest-cli.json b/services/service-core/nest-cli.json index f9aa683..4503426 100644 --- a/services/service-core/nest-cli.json +++ b/services/service-core/nest-cli.json @@ -3,6 +3,7 @@ "collection": "@nestjs/schematics", "sourceRoot": "src", "compilerOptions": { - "deleteOutDir": true - } + "deleteOutDir": true, + "assets": [{"include": "**/*.yaml", "outDir": "./dist"}] + } } diff --git a/services/service-core/package.json b/services/service-core/package.json index a44c37e..ff6e48a 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", @@ -32,8 +32,10 @@ "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", + "js-yaml": "^4.1.0", "joi": "^17.11.0", "jsonwebtoken": "^9.0.2", "keycloak-connect": "^21.1.2", @@ -49,6 +51,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 65293a3..754dd17 100644 --- a/services/service-core/src/app.module.ts +++ b/services/service-core/src/app.module.ts @@ -20,30 +20,33 @@ 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'; @Module({ imports: [ TypeOrmModule.forRootAsync({ imports: [ ConfigModule.forRoot({ - isGlobal: true, - envFilePath: '.local.env' + 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, @@ -51,17 +54,20 @@ import { KeycloakAuthGuard } from './auth/keycloak.guard'; }), KeycloakConnectModule.registerAsync({ - imports: [ConfigModule], - useFactory: async (configService: ConfigService) => ({ - authServerUrl: configService.get('KEYCLOAK_SERVER_URL'), - realm: 'humanitech', - resource: 'nest-application', - secret: configService.get('KEYCLOAK_SECRET'), - 'public-client': true, - verifyTokenAudience: true, - 'confidential-port': 0 - }), - inject: [ConfigService] + useFactory: async () => { + const configService = new CustomConfigService(); + const { keycloak } = configService.get(); + + return { + authServerUrl: keycloak.authServerUrl, + realm: keycloak.realm, + resource: keycloak.nestClientId, + secret: keycloak.ADMIN_CLIENT_SECRET, + 'public-client': true, + verifyTokenAudience: true, + 'confidential-port': 0 + }; + } }), DatabaseModule, diff --git a/services/service-core/src/auth/config.ts b/services/service-core/src/auth/config.ts deleted file mode 100644 index 540d0e7..0000000 --- a/services/service-core/src/auth/config.ts +++ /dev/null @@ -1,30 +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<{ - 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 eec9d91..d0a9643 100644 --- a/services/service-core/src/auth/keycloak.service.ts +++ b/services/service-core/src/auth/keycloak.service.ts @@ -13,18 +13,19 @@ import { Injectable } from '@nestjs/common'; import { verify } from 'jsonwebtoken'; import axios from 'axios'; -import { ConfigService } from '@nestjs/config'; -import { Config } from './config'; +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 readonly config = this.configService.get(); async getPublicKey() { + const { realmUrl } = this.config.keycloak; try { - const response = await axios.get(Config.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) { @@ -33,18 +34,26 @@ export class KeycloakService { } async getAdminToken() { + const { + realmUrl, + grantType, + clientId, + KEYCLOAK_ADMIN, + KEYCLOAK_ADMIN_PASSWORD, + ADMIN_CLIENT_SECRET + } = this.config.keycloak; const params = new URLSearchParams({ - username: this.configService.get(Config.keycloakAdmin), - password: this.configService.get(Config.keycloakAdminPassword), - grant_type: Config.grantType, - client_id: Config.clientId, - client_secret: this.configService.get(Config.adminClientSecret) + 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( - `${Config.realmUrl}/protocol/openid-connect/token`, + `${realmUrl}/protocol/openid-connect/token`, { method: 'POST', headers: { @@ -85,13 +94,14 @@ export class KeycloakService { } async editUser(id: string, userInput: UpdateUser) { + const { adminUrl } = this.config.keycloak; const accessToken = await this.getAdminToken(); const { error } = userInputValidator.validate(userInput); if (error) { throw new Error(error.details[0].message); } - const updateUser = await fetch(`${Config.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/config.spec.ts b/services/service-core/src/auth/test/config.spec.ts deleted file mode 100644 index b556356..0000000 --- a/services/service-core/src/auth/test/config.spec.ts +++ /dev/null @@ -1,46 +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', () => { - 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'); - }); - - it('should have the correct adminClientSecret', () => { - expect(Config.adminClientSecret).toBe('ADMIN_CLIENT_SECRET'); - }); - - it('should have the correct keycloakAdmin', () => { - expect(Config.keycloakAdmin).toBe('KEYCLOAK_ADMIN'); - }); - - it('should have the correct keycloakAdminPassword', () => { - expect(Config.keycloakAdminPassword).toBe('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 c0b3cfb..d39b448 100644 --- a/services/service-core/src/auth/test/keycloak.service.spec.ts +++ b/services/service-core/src/auth/test/keycloak.service.spec.ts @@ -14,22 +14,51 @@ 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 Joi from 'joi'; +import { CustomConfigService } from '../../config/config.service'; +import { load } from 'js-yaml'; + +jest.mock('js-yaml', () => ({ + load: jest.fn() +})); + +// Define the setMockYamlLoad function +const setMockYamlLoad = (mock) => { + const mockYamlLoad = load as jest.Mock; + mockYamlLoad.mockReturnValue(mock); +}; jest.mock('axios'); jest.mock('jsonwebtoken'); 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 = { + keycloak: { + realmUrl + }, + local: { + // local properties + } + }; const errorMessage = 'Failed to fetch Public Key'; - beforeEach(async () => { jest.clearAllMocks(); const module: TestingModule = await Test.createTestingModule({ - providers: [KeycloakService, ConfigService] + providers: [ + KeycloakService, + { + provide: CustomConfigService, + useValue: { get: jest.fn().mockReturnValue(mockConfig) } // Mock the config service + } + ] }).compile(); keycloakService = module.get(KeycloakService); @@ -43,9 +72,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-----' ); @@ -83,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 ); }); }); @@ -201,9 +228,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-----`, @@ -237,13 +262,5 @@ describe('KeycloakService', () => { 'Invalid Token' ); }); - - 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); - }); }); }); 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 0000000..67feed7 --- /dev/null +++ b/services/service-core/src/config/config.dto.ts @@ -0,0 +1,90 @@ +/** + * 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 { + @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; + + @IsString() + @IsNotEmpty() + KEYCLOAK_ADMIN: string; + + @IsString() + @IsNotEmpty() + KEYCLOAK_ADMIN_PASSWORD: string; + + @IsString() + @IsNotEmpty() + KEYCLOAK_DATA: string; + + @IsString() + @IsNotEmpty() + ADMIN_CLIENT_SECRET: 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; +} +export class AppConfigDto { + @IsNotEmpty() + keycloak: KeycloakConfigurationDto; + + @IsNotEmpty() + database: LocalConfigurationDto; +} 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 0000000..4915da0 --- /dev/null +++ b/services/service-core/src/config/config.service.ts @@ -0,0 +1,45 @@ +/** + * 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 { readFileSync } from 'fs'; +import { load } from 'js-yaml'; +import { join } from 'path'; +import { AppConfigDto } from './config.dto'; + +export class CustomConfigService { + private readonly configuration: AppConfigDto; + + constructor() { + this.configuration = this.loadConfiguration(); + } + + private loadConfiguration(): AppConfigDto { + const yamlFilePath = './config.yaml'; + const yamlConfig = load( + readFileSync(join(__dirname, yamlFilePath), 'utf8') + ) 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]; + } + } + } + + return yamlConfig; + } + + get() { + return this.configuration; + } +} diff --git a/services/service-core/src/config/config.yaml b/services/service-core/src/config/config.yaml new file mode 100644 index 0000000..756feb4 --- /dev/null +++ b/services/service-core/src/config/config.yaml @@ -0,0 +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' + KEYCLOAK_ADMIN: 'admin-user' + KEYCLOAK_ADMIN_PASSWORD: 'admin-password' + KEYCLOAK_DATA: '/path/to/keycloak/data' + ADMIN_CLIENT_SECRET: 'client-secret' + +database: + DB_HOST: 'localhost' + DB_PORT: '5432' + DB_USERNAME: 'myuser' + DB_PASSWORD: 'mypassword' + DB_DATABASE: 'mydb' + POSTGRES_DATA: '/path/to/postgres/data' 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 0000000..65f7918 --- /dev/null +++ b/services/service-core/src/config/test/config.dto.spec.ts @@ -0,0 +1,55 @@ +/** + * 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 { validate } from 'class-validator'; + +describe('AppConfigDto', () => { + it('should validate a valid AppConfigDto object', async () => { + const validAppConfigDto = new AppConfigDto(); + validAppConfigDto.keycloak = new KeycloakConfigurationDto(); + validAppConfigDto.database = 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.database = 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('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 new file mode 100644 index 0000000..dd4b2ac --- /dev/null +++ b/services/service-core/src/config/test/config.service.spec.ts @@ -0,0 +1,93 @@ +/** + * 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 { CustomConfigService } from '../config.service'; +import { AppConfigDto } from '../config.dto'; +import { readFileSync } from 'fs'; +import { load } from 'js-yaml'; + +jest.mock('fs'); +jest.mock('js-yaml'); + +describe('CustomConfigService', () => { + // Reset the mocked functions before each test + beforeEach(() => { + jest.clearAllMocks(); + }); + + 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', + KEYCLOAK_ADMIN: 'admin-user', + KEYCLOAK_ADMIN_PASSWORD: 'admin-password', + KEYCLOAK_DATA: '/path/to/keycloak/data', + ADMIN_CLIENT_SECRET: 'client-secret' + }, + database: { + DB_HOST: 'localhost', + DB_PORT: '5432', + DB_USERNAME: 'myuser', + DB_PASSWORD: 'mypassword', + DB_DATABASE: 'mydb', + POSTGRES_DATA: '/path/to/postgres/data' + } + }; + + it('should load the configuration from the config.yaml file', () => { + // Mock fs.readFileSync to return the YAML data + (readFileSync as jest.Mock).mockReturnValue(yamlContent); + + // Mock js-yaml.load to return the expected config data + (load as jest.Mock).mockReturnValue(configData); + + // Act + const configService = new CustomConfigService(); + + // Assert + expect(readFileSync).toHaveBeenCalledWith( + expect.stringContaining('config.yaml'), + 'utf8' + ); + expect(load).toHaveBeenCalledWith(yamlContent); + expect(configService.get()).toEqual(configData); + }); + + it('should override the configuration values from environment variables', () => { + // Arrange + + // Mock fs.readFileSync to return the YAML data + (readFileSync as jest.Mock).mockReturnValue(yamlContent); + + // Mock js-yaml.load to return the expected config data + (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().database.DB_HOST).toBe('env_db_host'); + // Add assertions for other environment variables as needed + }); +}); diff --git a/services/service-core/src/graphql/users/users.module.ts b/services/service-core/src/graphql/users/users.module.ts index 7545791..0725fc4 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 {} diff --git a/services/service-core/src/main.ts b/services/service-core/src/main.ts index 83db15c..11eedfb 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 { config } from 'dotenv'; async function bootstrap() { + config(); const app = await NestFactory.create(AppModule); app.enableCors({ origin: '*', diff --git a/services/service-core/tsconfig.json b/services/service-core/tsconfig.json index e5c8bc9..7821e40 100644 --- a/services/service-core/tsconfig.json +++ b/services/service-core/tsconfig.json @@ -18,6 +18,7 @@ "forceConsistentCasingInFileNames": false, "noFallthroughCasesInSwitch": false, "moduleResolution": "node", + "resolveJsonModule": true, "esModuleInterop": true } }