diff --git a/apps/generator-cli/src/app/app.module.ts b/apps/generator-cli/src/app/app.module.ts index f67633860..78d61420e 100644 --- a/apps/generator-cli/src/app/app.module.ts +++ b/apps/generator-cli/src/app/app.module.ts @@ -7,31 +7,39 @@ import { VersionManagerController } from './controllers/version-manager.controll import { ConfigService, GeneratorService, + NpmrcService, PassThroughService, UIService, VersionManagerService, } from './services'; -import { HttpsProxyAgent } from 'https-proxy-agent'; +import { Agent } from 'https'; -const proxyUrl = process.env.HTTP_PROXY || process.env.HTTPS_PROXY; -const httpModuleConfig: HttpModuleOptions = {}; - -if (proxyUrl) { - httpModuleConfig.proxy = false; - httpModuleConfig.httpsAgent = new HttpsProxyAgent(proxyUrl); -} +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 { + httpsAgent: httpsAgent, + }; +}; @Module({ imports: [ - HttpModule.register({ - ...httpModuleConfig, + HttpModule.registerAsync({ + imports: [AppModule], + useFactory: httpModuleConfigFactory, + inject: [ConfigService, NpmrcService], }), ], controllers: [VersionManagerController], + exports: [ConfigService, NpmrcService], providers: [ UIService, ConfigService, GeneratorService, + NpmrcService, PassThroughService, VersionManagerService, { 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..aee385e7c 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 @@ -56,7 +57,7 @@ describe('ConfigService', () => { expect(fixture.get('unknown', 'foo')).toEqual('foo'); }); }); - + describe('the config has values', () => { beforeEach(() => { fs.readJSONSync.mockReturnValue({ @@ -90,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()', () => { @@ -178,7 +214,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/config.service.ts b/apps/generator-cli/src/app/services/config.service.ts index f968eadd0..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, @@ -53,17 +57,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 +77,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 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/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 a68af3990..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(); @@ -142,7 +149,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 +192,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 +228,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 +338,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 +441,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 +468,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 +490,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 +573,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 +581,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 +599,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(); @@ -601,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 84aaf6926..6a544534d 100644 --- a/apps/generator-cli/src/config.schema.json +++ b/apps/generator-cli/src/config.schema.json @@ -41,6 +41,17 @@ "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": { + "$ref": "#/definitions/http" + } + }, "generators": { "type": "object", "additionalProperties": { @@ -62,6 +73,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": [ 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();