From 67d713f73a431414087a03b7e2d0d88c6799473b Mon Sep 17 00:00:00 2001 From: GONZALEZ Adrian Date: Sun, 13 Oct 2024 22:58:00 +0200 Subject: [PATCH 1/6] feat(http): restore axios default proxy handling The current implementation does not handle NO_PROXY env var. I removed explicit proxy handling, since axios already handle proxy related environment variables. See https://github.com/axios/axios#request-config: `proxy` defines the hostname, port, and protocol of the proxy server. You can also define your proxy using the conventional `http_proxy` and `https_proxy` environment variables. If you are using environment variables for your proxy configuration, you can also define a `no_proxy` environment variable as a comma-separated list of domains that should not be proxied. --- apps/generator-cli/src/app/app.module.ts | 7 ------- 1 file changed, 7 deletions(-) diff --git a/apps/generator-cli/src/app/app.module.ts b/apps/generator-cli/src/app/app.module.ts index f67633860..b592c4265 100644 --- a/apps/generator-cli/src/app/app.module.ts +++ b/apps/generator-cli/src/app/app.module.ts @@ -11,16 +11,9 @@ import { UIService, VersionManagerService, } from './services'; -import { HttpsProxyAgent } from 'https-proxy-agent'; -const proxyUrl = process.env.HTTP_PROXY || process.env.HTTPS_PROXY; const httpModuleConfig: HttpModuleOptions = {}; -if (proxyUrl) { - httpModuleConfig.proxy = false; - httpModuleConfig.httpsAgent = new HttpsProxyAgent(proxyUrl); -} - @Module({ imports: [ HttpModule.register({ From 9dc53a613cb2ca6de89cb55240b012b275ca0ded Mon Sep 17 00:00:00 2001 From: GONZALEZ Adrian Date: Mon, 14 Oct 2024 22:49:33 +0200 Subject: [PATCH 2/6] feat: activate logging --- apps/generator-cli/src/main.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/generator-cli/src/main.ts b/apps/generator-cli/src/main.ts index ae588ee46..d9ea6c193 100644 --- a/apps/generator-cli/src/main.ts +++ b/apps/generator-cli/src/main.ts @@ -7,7 +7,9 @@ import {NestFactory} from '@nestjs/core'; import {AppModule} from './app/app.module'; async function bootstrap() { - await NestFactory.createApplicationContext(AppModule, {logger: false}); + await NestFactory.createApplicationContext(AppModule, { + logger: ['fatal', 'error', 'warn'], + }); } bootstrap(); From 58df5725db3748eefad217789432bea8730b05c5 Mon Sep 17 00:00:00 2001 From: GONZALEZ Adrian Date: Mon, 14 Oct 2024 22:50:25 +0200 Subject: [PATCH 3/6] feat(http): add rejectUnauthorized configuration This helps to avoid the error `unable to get local issuer certificate` when using a corporate maven repository. Another solution would be to declare the env var NODE_TLS_REJECT_UNAUTHORIZED=0. --- apps/generator-cli/src/app/app.module.ts | 17 ++++++++++++++--- apps/generator-cli/src/config.schema.json | 16 ++++++++++++++++ 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/apps/generator-cli/src/app/app.module.ts b/apps/generator-cli/src/app/app.module.ts index b592c4265..0fb70a80e 100644 --- a/apps/generator-cli/src/app/app.module.ts +++ b/apps/generator-cli/src/app/app.module.ts @@ -11,16 +11,27 @@ import { UIService, VersionManagerService, } from './services'; +import { Agent } from 'https'; -const httpModuleConfig: HttpModuleOptions = {}; +export const httpModuleConfigFactory = async (configService: ConfigService): Promise => { + const httpsAgent = !configService.get('generator-cli.http.rejectUnauthorized', true) ? new Agent({ + rejectUnauthorized: false, + }) : undefined; + return { + httpsAgent: httpsAgent, + }; +}; @Module({ imports: [ - HttpModule.register({ - ...httpModuleConfig, + HttpModule.registerAsync({ + imports: [AppModule], + useFactory: httpModuleConfigFactory, + inject: [ConfigService], }), ], controllers: [VersionManagerController], + exports: [ConfigService], providers: [ UIService, ConfigService, diff --git a/apps/generator-cli/src/config.schema.json b/apps/generator-cli/src/config.schema.json index 84aaf6926..013f7a428 100644 --- a/apps/generator-cli/src/config.schema.json +++ b/apps/generator-cli/src/config.schema.json @@ -41,6 +41,12 @@ "type": "string", "default": "openapitools/openapi-generator-cli" }, + "http": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/http" + } + }, "generators": { "type": "object", "additionalProperties": { @@ -62,6 +68,16 @@ } ] }, + "http": { + "type": "object", + "properties": { + "rejectUnauthorized": { + "description": "rejectUnauthorized option is used in HTTPS requests to determine whether the client should reject connections to servers with invalid or self-signed SSL certificates. Defaults to true.", + "type": "boolean", + "default": true + } + } + }, "generator": { "type": "object", "anyOf": [ From a7e54a7a0e1fa7d9690f111090a22f2864d6ead9 Mon Sep 17 00:00:00 2001 From: GONZALEZ Adrian Date: Thu, 17 Oct 2024 00:16:36 +0200 Subject: [PATCH 4/6] test: fix tests on windows --- .../src/app/services/config.service.spec.ts | 3 +- .../app/services/generator.service.spec.ts | 27 ++++++------- .../services/version-manager.service.spec.ts | 39 ++++++++++--------- 3 files changed, 37 insertions(+), 32 deletions(-) diff --git a/apps/generator-cli/src/app/services/config.service.spec.ts b/apps/generator-cli/src/app/services/config.service.spec.ts index 92c81446c..439e48e93 100644 --- a/apps/generator-cli/src/app/services/config.service.spec.ts +++ b/apps/generator-cli/src/app/services/config.service.spec.ts @@ -2,6 +2,7 @@ import { Test } from '@nestjs/testing'; import { Command, createCommand } from 'commander'; import { ConfigService } from './config.service'; import { LOGGER, COMMANDER_PROGRAM } from '../constants'; +import * as path from 'path'; jest.mock('fs-extra'); // eslint-disable-next-line @typescript-eslint/no-var-requires @@ -178,7 +179,7 @@ describe('ConfigService', () => { it('returns default path, if openapitools argument not provided', () => { expect( fixture.configFile.endsWith( - 'openapi-generator-cli/openapitools.json' + path.join('openapi-generator-cli', 'openapitools.json') ) ).toBeTruthy(); }); diff --git a/apps/generator-cli/src/app/services/generator.service.spec.ts b/apps/generator-cli/src/app/services/generator.service.spec.ts index 5a90c8375..41473abd6 100644 --- a/apps/generator-cli/src/app/services/generator.service.spec.ts +++ b/apps/generator-cli/src/app/services/generator.service.spec.ts @@ -3,6 +3,7 @@ import { GeneratorService } from './generator.service'; import { LOGGER } from '../constants'; import { VersionManagerService } from './version-manager.service'; import { ConfigService } from './config.service'; +import * as path from 'path'; jest.mock('fs-extra'); jest.mock('glob'); @@ -139,7 +140,7 @@ describe('GeneratorService', () => { appendix: string[] ) => ({ name, - command: `java -cp "/path/to/4.2.1.jar:${customJar}" org.openapitools.codegen.OpenAPIGenerator generate ${appendix.join( + command: `java -cp "/path/to/4.2.1.jar${path.delimiter}${customJar}" org.openapitools.codegen.OpenAPIGenerator generate ${appendix.join( ' ' )}`, }); @@ -149,25 +150,25 @@ describe('GeneratorService', () => { 'foo.json', [ cmd('[angular] abc/app/pet.yaml', [ - `--input-spec="${cwd}/abc/app/pet.yaml"`, + `--input-spec="${path.resolve(cwd, 'abc/app/pet.yaml')}"`, `--output="${cwd}/generated-sources/openapi/typescript-angular/pet"`, `--generator-name="typescript-angular"`, `--additional-properties="fileNaming=kebab-case,apiModulePrefix=Pet,npmName=petRestClient,supportsES6=true,withInterfaces=true"`, ]), cmd('[angular] abc/app/car.yaml', [ - `--input-spec="${cwd}/abc/app/car.yaml"`, + `--input-spec="${path.resolve(cwd, 'abc/app/car.yaml')}"`, `--output="${cwd}/generated-sources/openapi/typescript-angular/car"`, `--generator-name="typescript-angular"`, `--additional-properties="fileNaming=kebab-case,apiModulePrefix=Car,npmName=carRestClient,supportsES6=true,withInterfaces=true"`, ]), cmd('[baz] def/app/pet.yaml', [ - `--input-spec="${cwd}/def/app/pet.yaml"`, + `--input-spec="${path.resolve(cwd, 'def/app/pet.yaml')}"`, `--name="pet"`, `--name-uc-first="Pet"`, `--cwd="${cwd}"`, `--base="pet.yaml"`, - `--dir="${cwd}/def/app"`, - `--path="${cwd}/def/app/pet.yaml"`, + `--dir="${path.resolve(cwd, 'def/app')}"`, + `--path="${path.resolve(cwd, 'def/app/pet.yaml')}"`, `--rel-dir="def/app"`, `--rel-path="def/app/pet.yaml"`, `--ext="yaml"`, @@ -175,13 +176,13 @@ describe('GeneratorService', () => { '--some-int=1', ]), cmd('[baz] def/app/car.json', [ - `--input-spec="${cwd}/def/app/car.json"`, + `--input-spec="${path.resolve(cwd, 'def/app/car.json')}"`, `--name="car"`, `--name-uc-first="Car"`, `--cwd="${cwd}"`, `--base="car.json"`, - `--dir="${cwd}/def/app"`, - `--path="${cwd}/def/app/car.json"`, + `--dir="${path.resolve(cwd, 'def/app')}"`, + `--path="${path.resolve(cwd, 'def/app/car.json')}"`, `--rel-dir="def/app"`, `--rel-path="def/app/car.json"`, `--ext="json"`, @@ -194,12 +195,12 @@ describe('GeneratorService', () => { 'bar.json', [ cmd('[bar] api/cat.yaml', [ - `--input-spec="${cwd}/api/cat.yaml"`, + `--input-spec="${path.resolve(cwd, 'api/cat.yaml')}"`, `--output="bar/cat"`, '--some-bool', ]), cmd('[bar] api/bird.json', [ - `--input-spec="${cwd}/api/bird.json"`, + `--input-spec="${path.resolve(cwd, 'api/bird.json')}"`, `--output="bar/bird"`, '--some-bool', ]), @@ -209,12 +210,12 @@ describe('GeneratorService', () => { 'bar.json', [ cmdWithCustomJar('[bar] api/cat.yaml', '../some/custom.jar', [ - `--input-spec="${cwd}/api/cat.yaml"`, + `--input-spec="${path.resolve(cwd, 'api/cat.yaml')}"`, `--output="bar/cat"`, '--some-bool', ]), cmdWithCustomJar('[bar] api/bird.json', '../some/custom.jar', [ - `--input-spec="${cwd}/api/bird.json"`, + `--input-spec="${path.resolve(cwd, 'api/bird.json')}"`, `--output="bar/bird"`, '--some-bool', ]), diff --git a/apps/generator-cli/src/app/services/version-manager.service.spec.ts b/apps/generator-cli/src/app/services/version-manager.service.spec.ts index a68af3990..89e056e0b 100644 --- a/apps/generator-cli/src/app/services/version-manager.service.spec.ts +++ b/apps/generator-cli/src/app/services/version-manager.service.spec.ts @@ -142,7 +142,8 @@ describe('VersionManagerService', () => { it('executes one get request', () => { expect(get).toHaveBeenNthCalledWith( 1, - 'https://search.maven.org/solrsearch/select?q=g:org.openapitools+AND+a:openapi-generator-cli&core=gav&start=0&rows=200' + 'https://search.maven.org/solrsearch/select?q=g:org.openapitools+AND+a:openapi-generator-cli&core=gav&start=0&rows=200', + { headers: {} } ); }); @@ -184,7 +185,8 @@ describe('VersionManagerService', () => { it('executes one get request', () => { expect(get).toHaveBeenNthCalledWith( 1, - 'https://search.maven.org/solrsearch/select?q=g:org.openapitools+AND+a:openapi-generator-cli&core=gav&start=0&rows=200' + 'https://search.maven.org/solrsearch/select?q=g:org.openapitools+AND+a:openapi-generator-cli&core=gav&start=0&rows=200', + { headers: {} } ); }); @@ -219,7 +221,8 @@ describe('VersionManagerService', () => { it('executes one get request', () => { expect(get).toHaveBeenNthCalledWith( 1, - 'https://search.maven.org/solrsearch/select?q=g:org.openapitools+AND+a:openapi-generator-cli&core=gav&start=0&rows=200' + 'https://search.maven.org/solrsearch/select?q=g:org.openapitools+AND+a:openapi-generator-cli&core=gav&start=0&rows=200', + { headers: {} } ); }); @@ -328,7 +331,7 @@ describe('VersionManagerService', () => { it('removes the correct file', () => { expect(fs.removeSync).toHaveBeenNthCalledWith( 1, - `${fixture.storage}/4.3.1.jar` + path.resolve(fixture.storage, '4.3.1.jar') ); }); @@ -431,8 +434,8 @@ describe('VersionManagerService', () => { describe('there is a custom storage location', () => { it.each([ - ['/c/w/d/custom/dir', './custom/dir'], - ['/custom/dir', '/custom/dir'], + [path.resolve('/c/w/d', 'custom/dir'), './custom/dir'], + [path.resolve('/custom/dir'), '/custom/dir'], ])('returns %s for %s', async (expected, cfgValue) => { getStorageDir.mockReturnValue(cfgValue); logMessages = { before: [], after: [] }; @@ -458,7 +461,7 @@ describe('VersionManagerService', () => { expect(get).toHaveBeenNthCalledWith( 1, 'https://repo1.maven.org/maven2/org/openapitools/openapi-generator-cli/4.2.0/openapi-generator-cli-4.2.0.jar', - { responseType: 'stream' } + { responseType: 'stream', headers: {} } ); }); @@ -480,15 +483,15 @@ describe('VersionManagerService', () => { it('creates the correct write stream', () => { expect(fs.createWriteStream).toHaveBeenNthCalledWith( 1, - '/tmp/generator-cli-abcDEF/4.2.0' + path.join('/tmp/generator-cli-abcDEF', '4.2.0') ); }); it('moves the file to the target location', () => { expect(fs.moveSync).toHaveBeenNthCalledWith( 1, - '/tmp/generator-cli-abcDEF/4.2.0', - `${fixture.storage}/4.2.0.jar`, + path.join('/tmp/generator-cli-abcDEF', '4.2.0'), + path.resolve(fixture.storage, '4.2.0.jar'), { overwrite: true } ); }); @@ -563,7 +566,7 @@ describe('VersionManagerService', () => { fixture.isDownloaded('4.3.1'); expect(fs.existsSync).toHaveBeenNthCalledWith( 1, - fixture.storage + '/4.3.1.jar' + path.resolve(fixture.storage, '4.3.1.jar') ); }); }); @@ -571,12 +574,12 @@ describe('VersionManagerService', () => { describe('filePath()', () => { it('returns the path to the given version name', () => { expect(fixture.filePath('1.2.3')).toEqual( - `${fixture.storage}/1.2.3.jar` + path.resolve(fixture.storage, '1.2.3.jar') ); }); it('returns the path to the selected version name as default', () => { - expect(fixture.filePath()).toEqual(`${fixture.storage}/4.3.0.jar`); + expect(fixture.filePath()).toEqual(path.resolve(fixture.storage, '4.3.0.jar')); }); }); @@ -589,11 +592,11 @@ describe('VersionManagerService', () => { describe('there is a custom storage location', () => { it.each([ - ['/c/w/d/custom/dir', './custom/dir'], - ['/custom/dir', '/custom/dir'], - ['/custom/dir', '/custom/dir/'], - [`${os.homedir()}/oa`, '~/oa/'], - [`${os.homedir()}/oa`, '~/oa'], + [path.resolve('/c/w/d', 'custom/dir'), './custom/dir'], + [path.resolve('/c/w/d', '/custom/dir'), '/custom/dir'], + [path.resolve('/c/w/d', '/custom/dir'), '/custom/dir/'], + [path.resolve(os.homedir(), 'oa'), '~/oa/'], + [path.resolve(os.homedir(), 'oa'), '~/oa'], ])('returns %s for %s', async (expected, cfgValue) => { getStorageDir.mockReturnValue(cfgValue); await compile(); From 5cf18edd3a39cf9a4c8a3a7b6a7266444376d654 Mon Sep 17 00:00:00 2001 From: GONZALEZ Adrian Date: Mon, 14 Oct 2024 23:13:05 +0200 Subject: [PATCH 5/6] feat(config): add support for environment variable placeholders - Implemented replaceEnvPlaceholders function to replace ${VAR} placeholders in config values with corresponding environment variable values. - If the environment variable does not exist, the placeholder remains unchanged. --- .../src/app/services/config.service.spec.ts | 37 ++++++++++++++++++- .../src/app/services/config.service.ts | 30 +++++++++++++-- 2 files changed, 63 insertions(+), 4 deletions(-) diff --git a/apps/generator-cli/src/app/services/config.service.spec.ts b/apps/generator-cli/src/app/services/config.service.spec.ts index 439e48e93..aee385e7c 100644 --- a/apps/generator-cli/src/app/services/config.service.spec.ts +++ b/apps/generator-cli/src/app/services/config.service.spec.ts @@ -57,7 +57,7 @@ describe('ConfigService', () => { expect(fixture.get('unknown', 'foo')).toEqual('foo'); }); }); - + describe('the config has values', () => { beforeEach(() => { fs.readJSONSync.mockReturnValue({ @@ -91,6 +91,41 @@ describe('ConfigService', () => { ); }); }); + + describe('the config has values having placeholders', () => { + beforeEach(() => { + fs.readJSONSync.mockReturnValue({ + $schema: 'foo.json', + spaces: 4, + 'generator-cli': { + version: '1.2.3', + repository: { + queryUrl: 'https://${__unit_test_username}:${__unit_test_password}@server/api', + downloadUrl: 'https://${__unit_test_non_matching}@server/api' + } + }, + }); + process.env['__unit_test_username'] = 'myusername'; + process.env['__unit_test_password'] = 'mypassword'; + }); + + afterEach(() => { + delete process.env['__unit_test_username']; + delete process.env['__unit_test_password']; + }) + + it('verify placeholder replaced with env vars', () => { + const value = fixture.get('generator-cli.repository.queryUrl'); + + expect(value).toEqual('https://myusername:mypassword@server/api'); + }); + + it('verify placeholders not matching env vars are not replaced', () => { + const value = fixture.get('generator-cli.repository.downloadUrl'); + + expect(value).toEqual('https://${__unit_test_non_matching}@server/api'); + }); + }); }); describe('has()', () => { diff --git a/apps/generator-cli/src/app/services/config.service.ts b/apps/generator-cli/src/app/services/config.service.ts index f968eadd0..92cc6d8b2 100644 --- a/apps/generator-cli/src/app/services/config.service.ts +++ b/apps/generator-cli/src/app/services/config.service.ts @@ -53,17 +53,19 @@ export class ConfigService { } set(path: string, value: unknown) { - this.write(set(this.read(), path, value)) + this.write(set(this.read(false), path, value)) return this } - private read() { + private read(replaceEnvVars: boolean = true) { fs.ensureFileSync(this.configFile) - return merge( + const config = merge( this.defaultConfig, fs.readJSONSync(this.configFile, {throws: false, encoding: 'utf8'}), ) + + return replaceEnvVars ? replaceEnvPlaceholders(config) : config } private write(config) { @@ -71,3 +73,25 @@ export class ConfigService { } } + +function replaceEnvPlaceholders(config: any): any { + const envPlaceholderPattern = /\${(\w+)}/g; + + const replacePlaceholders = (value: any): any => { + if (typeof value === 'string') { + return value.replace(envPlaceholderPattern, (_, varName) => { + return process.env[varName] || `\${${varName}}`; + }); + } else if (Array.isArray(value)) { + return value.map(replacePlaceholders); + } else if (typeof value === 'object' && value !== null) { + return Object.keys(value).reduce((acc, key) => { + acc[key] = replacePlaceholders(value[key]); + return acc; + }, {}); + } + return value; + }; + + return replacePlaceholders(config); +} \ No newline at end of file From 9a09e943d5a0e035a97aa5db69e4fc640d337f5d Mon Sep 17 00:00:00 2001 From: GONZALEZ Adrian Date: Tue, 15 Oct 2024 00:27:32 +0200 Subject: [PATCH 6/6] feat: add support for querying Maven repository using npmrc settings - Implemented functionality to query Maven repository using authToken and strict-ssl settings from npmrc. - Updated httpModuleConfigFactory to configure HTTPS agent based on npmrc settings. - Enhanced NpmrcService to retrieve authToken and strict-ssl settings. --- apps/generator-cli/src/app/app.module.ts | 12 +- .../src/app/services/config.service.ts | 4 + apps/generator-cli/src/app/services/index.ts | 1 + .../src/app/services/npmrc.service.spec.ts | 153 ++++++++++++++++++ .../src/app/services/npmrc.service.ts | 112 +++++++++++++ .../services/version-manager.service.spec.ts | 62 +++++++ .../app/services/version-manager.service.ts | 21 ++- apps/generator-cli/src/config.schema.json | 5 + 8 files changed, 363 insertions(+), 7 deletions(-) create mode 100644 apps/generator-cli/src/app/services/npmrc.service.spec.ts create mode 100644 apps/generator-cli/src/app/services/npmrc.service.ts diff --git a/apps/generator-cli/src/app/app.module.ts b/apps/generator-cli/src/app/app.module.ts index 0fb70a80e..78d61420e 100644 --- a/apps/generator-cli/src/app/app.module.ts +++ b/apps/generator-cli/src/app/app.module.ts @@ -7,14 +7,17 @@ import { VersionManagerController } from './controllers/version-manager.controll import { ConfigService, GeneratorService, + NpmrcService, PassThroughService, UIService, VersionManagerService, } from './services'; import { Agent } from 'https'; -export const httpModuleConfigFactory = async (configService: ConfigService): Promise => { - const httpsAgent = !configService.get('generator-cli.http.rejectUnauthorized', true) ? new Agent({ +export const httpModuleConfigFactory = async (configService: ConfigService, npmrcService: NpmrcService): Promise => { + const strictSsl = configService.useNpmrc() ? npmrcService.getStrictSsl() : true; + const rejectUnauthorized = configService.get('generator-cli.http.rejectUnauthorized', true); + const httpsAgent = !strictSsl || !rejectUnauthorized ? new Agent({ rejectUnauthorized: false, }) : undefined; return { @@ -27,15 +30,16 @@ export const httpModuleConfigFactory = async (configService: ConfigService): Pro HttpModule.registerAsync({ imports: [AppModule], useFactory: httpModuleConfigFactory, - inject: [ConfigService], + inject: [ConfigService, NpmrcService], }), ], controllers: [VersionManagerController], - exports: [ConfigService], + exports: [ConfigService, NpmrcService], providers: [ UIService, ConfigService, GeneratorService, + NpmrcService, PassThroughService, VersionManagerService, { diff --git a/apps/generator-cli/src/app/services/config.service.ts b/apps/generator-cli/src/app/services/config.service.ts index 92cc6d8b2..4b10aba3c 100644 --- a/apps/generator-cli/src/app/services/config.service.ts +++ b/apps/generator-cli/src/app/services/config.service.ts @@ -30,6 +30,10 @@ export class ConfigService { return this.get('generator-cli.dockerImageName', 'openapitools/openapi-generator-cli'); } + public useNpmrc() { + return this.get('generator-cli.useNpmrc', false); + } + private readonly defaultConfig = { $schema: './node_modules/@openapitools/openapi-generator-cli/config.schema.json', spaces: 2, diff --git a/apps/generator-cli/src/app/services/index.ts b/apps/generator-cli/src/app/services/index.ts index 5d8e9d90f..b9532fd9e 100644 --- a/apps/generator-cli/src/app/services/index.ts +++ b/apps/generator-cli/src/app/services/index.ts @@ -3,3 +3,4 @@ export * from './config.service' export * from './generator.service' export * from './pass-through.service' export * from './version-manager.service' +export * from './npmrc.service' \ No newline at end of file diff --git a/apps/generator-cli/src/app/services/npmrc.service.spec.ts b/apps/generator-cli/src/app/services/npmrc.service.spec.ts new file mode 100644 index 000000000..34ebdcdbe --- /dev/null +++ b/apps/generator-cli/src/app/services/npmrc.service.spec.ts @@ -0,0 +1,153 @@ +import { Test } from '@nestjs/testing'; +import { Command, createCommand } from 'commander'; +import { NpmrcService } from './npmrc.service'; +import { LOGGER, COMMANDER_PROGRAM } from '../constants'; +import * as path from 'path'; + +jest.mock('fs-extra'); +// eslint-disable-next-line @typescript-eslint/no-var-requires +const fs = jest.mocked(require('fs-extra')); + +describe('NpmrcService', () => { + let fixture: NpmrcService; + + const log = jest.fn(); + + beforeEach(async () => { + + const moduleRef = await Test.createTestingModule({ + providers: [ + NpmrcService, + { provide: LOGGER, useValue: { log } }, + ], + }).compile(); + + fixture = moduleRef.get(NpmrcService); + }); + + describe('API', () => { + + describe('getStrictSsl()', () => { + + describe('npmrc file does not exists', () => { + let savedHome: string; + beforeEach(() => { + fixture.clear(); + savedHome = process.env.HOME; + process.env.HOME = '/home/user'; + fs.existsSync + .mockReset() + .mockReturnValue(false); + }); + + afterEach(() => { + process.env.HOME = savedHome; + }); + + it('should return true', () => { + expect(fixture.getStrictSsl()).toBeTruthy(); + }); + }); + + describe.each([ + ['empty file', '', true], + ['commented line', '#strict-ssl=false', true], + ['strict-ssl set to true', 'strict-ssl=true', true], + ['strict-ssl set to false', 'strict-ssl=false', false], + [ + 'npmrc has multiple lines', + ` + first line + second line + strict-ssl=false + `, + false + ], + ])('%s', (_, npmrcFileContents, expectation) => { + describe('npmrc file exists', () => { + let savedHome: string; + beforeEach(() => { + fixture.clear(); + savedHome = process.env.HOME; + process.env.HOME = '/home/user'; + fs.existsSync + .mockReset() + .mockReturnValue(npmrcFileContents); + fs.readFileSync + .mockReset() + .mockReturnValue(npmrcFileContents); + }); + + afterEach(() => { + process.env.HOME = savedHome; + }); + + it(`should return true ${expectation}`, () => { + expect(fixture.getStrictSsl()).toEqual(expectation); + }); + }); + }); + }); + + + describe('getAuthToken()', () => { + + describe.each([ + [ + 'undefined file', + 'https://testrepository.com', + undefined, + null + ], + [ + 'commented line', + 'https://testrepository.com', + '#//testrepository.com/npm/proxy/:_authToken=NpmToken.01', + null + ], + [ + 'base url matches', + 'https://testrepository.com', + '//testrepository.com/npm/proxy/:_authToken=NpmToken.01', + 'NpmToken.01' + ], + [ + 'base url does not match', + 'https://anotherrepository.com', + '//testrepository.com/npm/proxy/:_authToken=NpmToken.01', + null + ], + [ + 'have a comme base url', + 'https://testrepository.com/mvn', + '//testrepository.com/npm/proxy/:_authToken=NpmToken.01', + 'NpmToken.01' + ], + ])('%s', (_, url, npmrcFileContents, expectation) => { + describe('npmrc file exists', () => { + let savedHome: string; + beforeEach(() => { + fixture.clear(); + savedHome = process.env.HOME; + process.env.HOME = '/home/user'; + fs.existsSync + .mockReset() + .mockReturnValue(npmrcFileContents); + fs.readFileSync + .mockReset() + .mockReturnValue(npmrcFileContents); + }); + + afterEach(() => { + process.env.HOME = savedHome; + }); + + it(`should return true ${expectation}`, () => { + expect(fixture.getAuthToken(url)).toEqual(expectation); + }); + }); + }); + }); + + }); +}); diff --git a/apps/generator-cli/src/app/services/npmrc.service.ts b/apps/generator-cli/src/app/services/npmrc.service.ts new file mode 100644 index 000000000..b05465abf --- /dev/null +++ b/apps/generator-cli/src/app/services/npmrc.service.ts @@ -0,0 +1,112 @@ +import { Injectable } from "@nestjs/common"; +import path from "path"; +import * as os from 'os'; +import * as fs from 'fs-extra'; + +class BaseUrl { + + constructor(public protocol: string, public hostname: string, public port: string) { + } + + static fromUrl(url: string): BaseUrl | null { + try { + const parsedUrl = new URL(url); + const protocol = parsedUrl.protocol; + const hostname = parsedUrl.hostname; + const port = parsedUrl.port || (protocol === 'https:' ? '443' : '80'); + return new BaseUrl(protocol, hostname, port); + } catch (error) { + console.debug('Error parsing URL:', error); + return null; + } + } + + public equals(other: BaseUrl): boolean { + return this.protocol === other.protocol && this.hostname === other.hostname && this.port === other.port; + } +} + +@Injectable() +export class NpmrcService { + + private _npmrc: string | null | undefined; + private get npmrc(): string | null { + if (this._npmrc === undefined) { + this._npmrc = this.readNpmrc(); + } + return this._npmrc; + } + + public getStrictSsl(): boolean { + for (const line of this.npmrcLines) { + const [key, value] = line.split('='); + if (key.trim() === 'strict-ssl') { + return !(value.trim() === 'false'); + } + } + return true; + } + + public getAuthToken(url: string): string | null { + const baseUrl = BaseUrl.fromUrl(url); + + if (baseUrl === null) { + return null; + } + + for (const line of this.npmrcLines) { + // Skip comment lines + if (line.trim().startsWith('#')) { + continue; + } + const [key, value] = line.split('='); + // Only process lines referring to _authToken (syntax //host/:) + if (!key.trim().endsWith('/:_authToken')) { + continue; + } + // in some cases, maven repository and npm repository are handled by the same server + // and the auth token can be reused on both + // so we need to check if the baseurl matches the key + let currentUrl = key.trim().replace(/\/:_authToken$/, ''); + if (!currentUrl.startsWith('http://') && !currentUrl.startsWith('https://')) { + currentUrl = `https:${currentUrl}`; + } + if (baseUrl.equals(BaseUrl.fromUrl(currentUrl))) { + return value.trim(); + } + } + return null; + } + + /** for testing purposes */ + public clear() { + this._npmrc = undefined; + } + + private get npmrcLines(): string[] { + const rawLines = this.npmrc ? this.npmrc.split('\n') : []; + return rawLines.filter(line => !line.trim().startsWith('#')); + } + + private readNpmrc(): string | null { + try { + // Check for .npmrc in the current directory + const currentDirNpmrcPath = path.resolve(process.cwd(), '.npmrc'); + if (fs.existsSync(currentDirNpmrcPath)) { + return fs.readFileSync(currentDirNpmrcPath, 'utf-8'); + } + + // Fallback to .npmrc in the user's home directory + const homeDir = process.env.HOME || process.env.USERPROFILE || os.homedir(); + const homeDirNpmrcPath = path.resolve(homeDir, '.npmrc'); + if (fs.existsSync(homeDirNpmrcPath)) { + return fs.readFileSync(homeDirNpmrcPath, 'utf-8'); + } + + return null; + } catch (error) { + console.error('Error reading .npmrc file:', error); + return null; + } + } +} \ No newline at end of file diff --git a/apps/generator-cli/src/app/services/version-manager.service.spec.ts b/apps/generator-cli/src/app/services/version-manager.service.spec.ts index 89e056e0b..c28f6d16d 100644 --- a/apps/generator-cli/src/app/services/version-manager.service.spec.ts +++ b/apps/generator-cli/src/app/services/version-manager.service.spec.ts @@ -9,6 +9,7 @@ import { resolve } from 'path'; import * as os from 'os'; import { TestingModule } from '@nestjs/testing/testing-module'; import * as path from 'path'; +import { NpmrcService } from './npmrc.service'; jest.mock('fs-extra'); @@ -25,6 +26,10 @@ describe('VersionManagerService', () => { const getStorageDir = jest.fn().mockReturnValue(undefined); const setVersion = jest.fn(); + const getStrictSsl = jest.fn().mockReturnValue(true); + const getAuthToken = jest.fn().mockReturnValue(undefined); + const useNpmrc = jest.fn().mockReturnValue(false); + let testBed: TestingModule; const compile = async () => { @@ -54,8 +59,10 @@ describe('VersionManagerService', () => { }, set: setVersion, cwd: '/c/w/d', + useNpmrc, }, }, + { provide: NpmrcService, useValue: { getStrictSsl, getAuthToken } }, { provide: LOGGER, useValue: { log } }, ], }).compile(); @@ -604,5 +611,60 @@ describe('VersionManagerService', () => { }); }); }); + + describe.each([ + [ + 'useNpmrc with getAll()', + () => fixture.getAll(), + 'https://search.maven.org/solrsearch/select?q=g:org.openapitools+AND+a:openapi-generator-cli&core=gav&start=0&rows=200', + {} + ], + [ + 'useNpmrc with download()', + () => fixture.download('4.2.0'), + 'https://repo1.maven.org/maven2/org/openapitools/openapi-generator-cli/4.2.0/openapi-generator-cli-4.2.0.jar', + {responseType: "stream"} + ] + ])('%s', (_, action, expectedUrl, additionalHeaders) => { + it('should send authToken when useNpmrc=true npmrc contains a matching token', async () => { + useNpmrc.mockReturnValue(true); + getAuthToken.mockReturnValue('token-01'); + + await action(); + + expect(get).toHaveBeenNthCalledWith( + 1, + expectedUrl, + { headers: {Authorization: 'Bearer token-01'}, ...additionalHeaders } + ); + expect(getAuthToken).toHaveBeenCalledWith(expectedUrl); + }); + + it('should not send authToken when useNpmrc=true npmrc does not contain a matching token', async () => { + useNpmrc.mockReturnValue(true); + getAuthToken.mockReturnValue(null); + + await action(); + + expect(get).toHaveBeenNthCalledWith( + 1, + expectedUrl, + { headers: {}, ...additionalHeaders } + ); + }); + + it('should not send authToken when useNpmrc=false', async () => { + useNpmrc.mockReturnValue(false); + + await action(); + + expect(get).toHaveBeenNthCalledWith( + 1, + expectedUrl, + { headers: {}, ...additionalHeaders } + ); + }); + }); + }); }); diff --git a/apps/generator-cli/src/app/services/version-manager.service.ts b/apps/generator-cli/src/app/services/version-manager.service.ts index 5bba0abde..12e48c0c7 100644 --- a/apps/generator-cli/src/app/services/version-manager.service.ts +++ b/apps/generator-cli/src/app/services/version-manager.service.ts @@ -14,6 +14,7 @@ import { LOGGER } from '../constants'; import { ConfigService } from './config.service'; import * as configSchema from '../../config.schema.json'; import { spawn, spawnSync } from 'child_process'; +import { NpmrcService } from './npmrc.service'; export interface Version { version: string; @@ -45,7 +46,8 @@ export class VersionManagerService { constructor( @Inject(LOGGER) private readonly logger: LOGGER, private httpService: HttpService, - private configService: ConfigService + private configService: ConfigService, + private npmrcService: NpmrcService ) { // pre-process intsalled in versions this.versions.forEach( (item) => { @@ -69,7 +71,8 @@ export class VersionManagerService { .default ); - return this.httpService.get(queryUrl).pipe( + const headers = this.getRequestHeaders(queryUrl); + return this.httpService.get(queryUrl, { headers }).pipe( map(({ data }) => data.response.docs), map((docs) => docs.map((doc) => ({ @@ -165,9 +168,10 @@ export class VersionManagerService { const downloadLink = this.createDownloadLink(versionName); const filePath = this.filePath(versionName); + const headers = this.getRequestHeaders(downloadLink); try { await this.httpService - .get(downloadLink, { responseType: 'stream' }) + .get(downloadLink, { responseType: 'stream', headers: headers }) .pipe( switchMap( (res) => @@ -227,6 +231,17 @@ export class VersionManagerService { return fs.existsSync(path.resolve(this.storage, `${versionName}.jar`)); } + private getRequestHeaders(url: string): { [key: string]: string } { + let headers = {}; + + if (this.configService.useNpmrc()) { + const authToken = this.npmrcService.getAuthToken(url); + headers = authToken ? { Authorization: `Bearer ${authToken}` } : {}; + } + + return headers; + } + private filterVersionsByTags(versions: Version[], tags: string[]) { if (tags.length < 1) { return versions; diff --git a/apps/generator-cli/src/config.schema.json b/apps/generator-cli/src/config.schema.json index 013f7a428..6a544534d 100644 --- a/apps/generator-cli/src/config.schema.json +++ b/apps/generator-cli/src/config.schema.json @@ -41,6 +41,11 @@ "type": "string", "default": "openapitools/openapi-generator-cli" }, + "useNpmrc": { + "type": "boolean", + "default": false, + "description": "use .npmrc file for using authToken to call the maven repository in case maven and npm repository are handled by the same server" + }, "http": { "type": "object", "additionalProperties": {