From 4e7a61369386595e83c3c55e5428ba61f2aca2f5 Mon Sep 17 00:00:00 2001 From: IITI-tushar <019saxenatushar@gmail.com> Date: Fri, 10 Jan 2025 19:23:21 +0530 Subject: [PATCH 1/3] updated fromTemplate.ts and models.ts in commands/generate folder --- src/commands/generate/fromTemplate.ts | 29 ++++++++++++++++++++++++--- src/commands/generate/models.ts | 19 +++++++++++++++++- 2 files changed, 44 insertions(+), 4 deletions(-) diff --git a/src/commands/generate/fromTemplate.ts b/src/commands/generate/fromTemplate.ts index 8bfe3471f64..461c29cfbd7 100644 --- a/src/commands/generate/fromTemplate.ts +++ b/src/commands/generate/fromTemplate.ts @@ -16,6 +16,8 @@ import { intro, isCancel, spinner, text } from '@clack/prompts'; import { inverse, yellow, magenta, green, red } from 'picocolors'; import fetch from 'node-fetch'; import { fromTemplateFlags } from '../../core/flags/generate/fromTemplate.flags'; +import { proxyFlags } from 'core/flags/proxy.flags'; +import { HttpsProxyAgent } from 'https-proxy-agent'; interface IMapBaseUrlToFlag { url: string, @@ -57,18 +59,31 @@ export default class Template extends Command { 'asyncapi generate fromTemplate asyncapi.yaml @asyncapi/html-template --param version=1.0.0 singleFile=true --output ./docs --force-write' ]; - static flags = fromTemplateFlags(); + static flags = { + ...fromTemplateFlags(), + ...proxyFlags(), + }; static args = { asyncapi: Args.string({ description: '- Local path, url or context-name pointing to AsyncAPI file', required: true }), template: Args.string({ description: '- Name of the generator template like for example @asyncapi/html-template or https://github.com/asyncapi/html-template', required: true }), + proxyHost: Args.string({description: 'Name of the Proxy Host', required: false}), + proxyPort: Args.string({description: 'Name of the Port of the ProxyHost', required: false}), }; parser = new Parser(); async run() { - const { args, flags } = await this.parse(Template); // NOSONAR + const { args, flags } = await this.parse(Template); // NOSONAR const interactive = !flags['no-interactive']; + const proxyHost = flags['proxyHost']; + const proxyPort = flags['proxyPort']; + + let proxyAgent; + if (proxyHost && proxyPort) { + const proxyUrl = `http://${proxyHost}:${proxyPort}`; + proxyAgent = new HttpsProxyAgent(proxyUrl); + } let { asyncapi, template } = args; let output = flags.output as string; @@ -94,7 +109,9 @@ export default class Template extends Command { url: flags['registry-url'], auth: flags['registry-auth'], token: flags['registry-token'] - } + }, + ...(proxyAgent && { fetchOptions: { agent: proxyAgent } }) + }; const asyncapiInput = (await load(asyncapi)) || (await load()); @@ -258,6 +275,9 @@ export default class Template extends Command { try { specification = await load(asyncapi); } catch (err: any) { + if (err.message?.includes('Failed to download')) { + throw new Error('Proxy Connection Error: Unable to establish a connection to the proxy. Check hostName or PortNumber.'); + } return this.error( new ValidationError({ type: 'invalid-file', @@ -283,6 +303,9 @@ export default class Template extends Command { try { specification = await load(asyncapi); } catch (err: any) { + if (err.message?.includes('Failed to download')) { + throw new Error('Proxy Connection Error: Unable to establish a connection to the proxy. Check hostName or PortNumber.'); + } return this.error( new ValidationError({ type: 'invalid-file', diff --git a/src/commands/generate/models.ts b/src/commands/generate/models.ts index 5c7a04f82a2..209a20abf9a 100644 --- a/src/commands/generate/models.ts +++ b/src/commands/generate/models.ts @@ -5,13 +5,19 @@ import { cancel, intro, isCancel, select, spinner, text } from '@clack/prompts'; import { green, inverse } from 'picocolors'; import { generateModels, Languages, ModelinaArgs } from '@asyncapi/modelina-cli'; import { modelsFlags } from '../../core/flags/generate/models.flags'; +import { proxyFlags } from 'core/flags/proxy.flags'; +import { HttpsProxyAgent } from 'https-proxy-agent'; +import fetch, { RequestInit } from 'node-fetch'; export default class Models extends Command { static description = 'Generates typed models'; static readonly args = ModelinaArgs as any; - static flags = modelsFlags(); + static flags = { + ...modelsFlags(), + ...proxyFlags(), + }; async run() { const { args, flags } = await this.parse(Models); @@ -19,6 +25,14 @@ export default class Models extends Command { let { output } = flags; const interactive = !flags['no-interactive']; + const proxyHost = flags['proxyHost']; + const proxyPort = flags['proxyPort']; + let proxyAgent; + + if (proxyHost && proxyPort) { + const proxyUrl = `http://${proxyHost}:${proxyPort}`; + proxyAgent = new HttpsProxyAgent(proxyUrl); + } if (!interactive) { intro(inverse('AsyncAPI Generate Models')); @@ -29,6 +43,9 @@ export default class Models extends Command { output = parsedArgs.output; } + const customFetch = proxyAgent + ? (url: string, options: RequestInit = {}) => fetch(url, { ...options, agent: proxyAgent }) + : fetch; const inputFile = (await load(file)) || (await load()); const { document, diagnostics ,status } = await parse(this, inputFile, flags as ValidateOptions); From 801e52ddebc5db7f4bc2a3e40036a45a6bf8a057 Mon Sep 17 00:00:00 2001 From: IITI-tushar <019saxenatushar@gmail.com> Date: Fri, 17 Jan 2025 14:38:56 +0530 Subject: [PATCH 2/3] added-tests --- .../integration/generate/fromTemplate.test.ts | 51 +++++++++++++++ test/integration/generate/models.test.ts | 63 +++++++++++++++++++ 2 files changed, 114 insertions(+) diff --git a/test/integration/generate/fromTemplate.test.ts b/test/integration/generate/fromTemplate.test.ts index dd3535d1696..27453538cd2 100644 --- a/test/integration/generate/fromTemplate.test.ts +++ b/test/integration/generate/fromTemplate.test.ts @@ -208,4 +208,55 @@ describe('template', () => { } ); }); + describe('fetch, httpsproxyagent, and proxyflags', () => { + test + .stdout() + .command([ + ...generalOptions, + '--output=./test/docs/proxy', + '--force-write', + '--proxyHost=localhost', + '--proxyPort=8080', + nonInteractive + ]) + .it('should use proxy settings when provided', (ctx, done) => { + expect(ctx.stdout).to.contain('Check out your shiny new generated files at ./test/docs/proxy'); + cleanup('./test/docs/proxy'); + done(); + }); + + test + .stdout() + .command([ + 'generate:fromTemplate', + 'https://raw.githubusercontent.com/asyncapi/spec/master/examples/2.0.0/streetlights.yml', + '@asyncapi/html-template', + '--output=./test/docs/fetch', + '--force-write', + nonInteractive + ]) + .it('should fetch remote AsyncAPI file', (ctx, done) => { + expect(ctx.stdout).to.contain('Check out your shiny new generated files at ./test/docs/fetch'); + cleanup('./test/docs/fetch'); + done(); + }); + + test + .stdout() + .command([ + 'generate:fromTemplate', + 'https://raw.githubusercontent.com/asyncapi/spec/master/examples/2.0.0/streetlights.yml', + '@asyncapi/html-template', + '--output=./test/docs/proxy-fetch', + '--force-write', + '--proxyHost=localhost', + '--proxyPort=8080', + nonInteractive + ]) + .it('should fetch remote AsyncAPI file using proxy', (ctx, done) => { + expect(ctx.stdout).to.contain('Check out your shiny new generated files at ./test/docs/proxy-fetch'); + cleanup('./test/docs/proxy-fetch'); + done(); + }); + }); }); diff --git a/test/integration/generate/models.test.ts b/test/integration/generate/models.test.ts index d02be248814..4cc57154280 100644 --- a/test/integration/generate/models.test.ts +++ b/test/integration/generate/models.test.ts @@ -2,6 +2,8 @@ import { expect, test } from '@oclif/test'; import path from 'path'; import rimraf from 'rimraf'; import { createMockServer, stopMockServer } from '../../helpers'; +import { HttpsProxyAgent } from 'https-proxy-agent'; +import fetch from 'node-fetch'; const generalOptions = ['generate:models']; const outputDir = './test/fixtures/generate/models'; @@ -36,6 +38,67 @@ describe('models', () => { done(); }); + describe('proxy-related tests', () => { + test + .stderr() + .stdout() + .env({ PROXY_HOST: '127.0.0.1', PROXY_PORT: '3128' }) // Mock environment variables for proxy + .do(() => { + process.env.PROXY_HOST = '127.0.0.1'; + process.env.PROXY_PORT = '3128'; + }) + .command([...generalOptions, 'typescript', 'http://localhost:8080/dummySpec.yml', '--proxyHost=127.0.0.1', '--proxyPort=3128']) + .it('works with proxy settings', (ctx, done) => { + expect(ctx.stdout).to.contain('Successfully generated the following models: '); + expect(process.env.PROXY_HOST).to.equal('127.0.0.1'); + expect(process.env.PROXY_PORT).to.equal('3128'); + done(); + }); + + test + .stderr() + .stdout() + .command([...generalOptions, 'typescript', 'http://localhost:8080/dummySpec.yml']) + .it('works without proxy settings', (ctx, done) => { + expect(ctx.stdout).to.contain('Successfully generated the following models: '); + done(); + }); + }); + + describe('fetch with proxy and without proxy', () => { + test + .stderr() + .stdout() + .do(() => { + const proxyUrl = 'http://127.0.0.1:3128'; + const proxyAgent = new HttpsProxyAgent(proxyUrl); + const customFetch = (url, options = {}) => fetch(url, { ...options, agent: proxyAgent }); + + customFetch('http://localhost:8080/dummySpec.yml') + .then((response) => { + expect(response.ok).to.be.true; + }) + .catch((err) => { + throw new Error(`Failed to fetch with proxy: ${err.message}`); + }); + }) + .it('tests fetch with proxy agent'); + + test + .stderr() + .stdout() + .do(() => { + fetch('http://localhost:8080/dummySpec.yml') + .then((response) => { + expect(response.ok).to.be.true; + }) + .catch((err) => { + throw new Error(`Failed to fetch without proxy: ${err.message}`); + }); + }) + .it('tests fetch without proxy agent'); + }); + describe('with logging diagnostics', () => { test .stderr() From 16fe0e7ea28c91eb843e474321c986b71f8f7bfd Mon Sep 17 00:00:00 2001 From: IITI-tushar <019saxenatushar@gmail.com> Date: Sat, 18 Jan 2025 12:54:11 +0530 Subject: [PATCH 3/3] updated models.ts --- src/commands/generate/models.ts | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/commands/generate/models.ts b/src/commands/generate/models.ts index 209a20abf9a..83580214b89 100644 --- a/src/commands/generate/models.ts +++ b/src/commands/generate/models.ts @@ -7,7 +7,7 @@ import { generateModels, Languages, ModelinaArgs } from '@asyncapi/modelina-cli' import { modelsFlags } from '../../core/flags/generate/models.flags'; import { proxyFlags } from 'core/flags/proxy.flags'; import { HttpsProxyAgent } from 'https-proxy-agent'; -import fetch, { RequestInit } from 'node-fetch'; +import fetch, { RequestInit, Response } from 'node-fetch'; export default class Models extends Command { static description = 'Generates typed models'; @@ -27,7 +27,8 @@ export default class Models extends Command { const interactive = !flags['no-interactive']; const proxyHost = flags['proxyHost']; const proxyPort = flags['proxyPort']; - let proxyAgent; + let proxyAgent: HttpsProxyAgent | undefined; + if (proxyHost && proxyPort) { const proxyUrl = `http://${proxyHost}:${proxyPort}`; @@ -43,9 +44,12 @@ export default class Models extends Command { output = parsedArgs.output; } - const customFetch = proxyAgent - ? (url: string, options: RequestInit = {}) => fetch(url, { ...options, agent: proxyAgent }) - : fetch; + const customFetch = (url: string, options: RequestInit = {}): Promise => { + return proxyAgent + ? fetch(url, { ...options, agent: proxyAgent }) + : fetch(url, options); + }; + const inputFile = (await load(file)) || (await load()); const { document, diagnostics ,status } = await parse(this, inputFile, flags as ValidateOptions);