diff --git a/jest.config.js b/jest.config.js index 2b5aafe4d3..9173a24198 100644 --- a/jest.config.js +++ b/jest.config.js @@ -27,7 +27,7 @@ module.exports = { statements: 84, branches: 74, functions: 84, - lines: 85, + lines: 84, }, }, testMatch: ['**/__tests__/**/*.test.ts', '**/*.test.ts'], diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index f80b2c53fa..794c4c6f6b 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -19,7 +19,7 @@ import { notifyUpdateCliVersion, version, } from './utils/update-version-notifier'; -import { commandWrapper } from './wrapper'; +import { type CommandArgs, commandWrapper } from './wrapper'; import { previewProject } from './commands/preview-project'; import { handleTranslations } from './commands/translations'; import { handleEject } from './commands/eject'; @@ -31,7 +31,7 @@ import type { Arguments } from 'yargs'; import type { OutputFormat, RuleSeverity } from '@redocly/openapi-core'; import type { BuildDocsArgv } from './commands/build-docs/types'; import type { PushStatusOptions } from './reunite/commands/push-status'; -import type { PushArguments } from './types'; +import type { CommandOptions, PushArguments } from './types'; import type { EjectOptions } from './commands/eject'; dotenv.config({ path: path.resolve(__dirname, '../.env') }); @@ -915,11 +915,6 @@ yargs describe: 'JSON file output name', type: 'string', }, - residency: { - describe: 'Residency of Reunite application. Defaults to US.', - type: 'string', - default: 'us', - }, 'client-cert': { describe: 'Mutual TLS client certificate', type: 'string', @@ -964,21 +959,15 @@ yargs 'Generate config with populated values from description using success criteria', type: 'boolean', }, - residency: { - describe: 'Residency of Reunite application. Defaults to US.', - type: 'string', - default: 'us', - }, }); + }, + async (argv) => { + process.env.REDOCLY_CLI_COMMAND = 'generate-arazzo'; + const { handleGenerate } = await import('@redocly/respect-core'); + commandWrapper( + handleGenerate as (wrapperArgs: CommandArgs) => Promise + )(argv); } - // async (argv) => { - // try { - // await handleGenerate(argv as GenerateConfigFileArgv); - // } catch { - // logger.error(`❌ Auto config generation failed.`); - // process.exit(1); - // } - // }, ) .completion('completion', 'Generate autocomplete script for `redocly` command.') .demandCommand(1) diff --git a/packages/respect-core/src/handlers/generate.ts b/packages/respect-core/src/handlers/generate.ts index 638a73313f..fcb0e19a4f 100644 --- a/packages/respect-core/src/handlers/generate.ts +++ b/packages/respect-core/src/handlers/generate.ts @@ -3,19 +3,29 @@ import { writeFileSync } from 'fs'; import { stringifyYaml } from '../utils/yaml'; import { generateTestConfig } from '../modules/test-config-generator'; import { DefaultLogger } from '../utils/logger/logger'; +import { exitWithError } from '../utils/exit-with-error'; +import { type CommandArgs } from '../types'; -import type { GenerateConfigFileArgv } from '../types'; +export type GenerateArazzoFileOptions = { + descriptionPath: string; + 'output-file'?: string; + extended?: boolean; +}; const logger = DefaultLogger.getInstance(); -export async function handleGenerate(argv: GenerateConfigFileArgv) { - logger.log(gray('\n Generating test configuration... \n')); +export async function handleGenerate({ argv }: CommandArgs) { + try { + logger.log(gray('\n Generating test configuration... \n')); - const generatedConfig = await generateTestConfig(argv as GenerateConfigFileArgv); - const content = stringifyYaml(generatedConfig); + const generatedConfig = await generateTestConfig(argv); + const content = stringifyYaml(generatedConfig); - const fileName = argv?.outputFile || 'auto-generated.yaml'; - writeFileSync(fileName, content); + const fileName = argv['output-file'] || 'auto-generated.arazzo.yaml'; + writeFileSync(fileName, content); - logger.log('\n' + blue(`Config ${yellow(fileName)} successfully generated.`) + '\n'); + logger.log('\n' + blue(`Config ${yellow(fileName)} successfully generated.`) + '\n'); + } catch (_err) { + exitWithError('\n' + '❌ Auto config generation failed.'); + } } diff --git a/packages/respect-core/src/handlers/run.ts b/packages/respect-core/src/handlers/run.ts index 34c191981e..bfe2757c6a 100644 --- a/packages/respect-core/src/handlers/run.ts +++ b/packages/respect-core/src/handlers/run.ts @@ -1,4 +1,5 @@ -import { bgRed, red } from 'colorette'; +import { red } from 'colorette'; +import { type CollectFn } from '@redocly/openapi-core/src/utils'; import { runTestFile } from '../modules/flow-runner'; import { displayErrors, @@ -7,17 +8,8 @@ import { calculateTotals, } from '../modules/cli-output'; import { DefaultLogger } from '../utils/logger/logger'; - -import type { Config } from '@redocly/openapi-core'; -import type { CollectFn } from '@redocly/openapi-core/src/utils'; -import type { RunArgv } from '../types'; - -export type CommandArgs = { - argv: T; - config: Config; - version: string; - collectSpecData?: CollectFn; -}; +import { type CommandArgs, type RunArgv } from '../types'; +import { exitWithError } from '../utils/exit-with-error'; export type RespectOptions = { files: string[]; @@ -28,7 +20,6 @@ export type RespectOptions = { verbose?: boolean; 'har-output'?: string; 'json-output'?: string; - residency?: string; 'client-cert'?: string; 'client-key'?: string; 'ca-cert'?: string; @@ -113,9 +104,3 @@ async function runFile( return { hasProblems, file: argv.file, workflows, argv }; } - -const exitWithError = (message: string) => { - logger.error(bgRed(message)); - logger.printNewLine(); - throw new Error(message); -}; diff --git a/packages/respect-core/src/index.ts b/packages/respect-core/src/index.ts index 7f4179d3fb..421ec07bd4 100644 --- a/packages/respect-core/src/index.ts +++ b/packages/respect-core/src/index.ts @@ -1 +1,3 @@ export { handleGenerate, handleRun } from './handlers/index'; +export type { GenerateArazzoFileOptions } from './handlers/generate'; +export type { RespectOptions } from './handlers/run'; diff --git a/packages/respect-core/src/modules/__tests__/flow-runner/context/create-test-context.test.ts b/packages/respect-core/src/modules/__tests__/flow-runner/context/create-test-context.test.ts index 3a772ed41e..010581e057 100644 --- a/packages/respect-core/src/modules/__tests__/flow-runner/context/create-test-context.test.ts +++ b/packages/respect-core/src/modules/__tests__/flow-runner/context/create-test-context.test.ts @@ -553,7 +553,7 @@ describe('createTestContext', () => { const options: AppOptions = { workflowPath: 'test.test.yaml', workflow: undefined, - harLogsFile: 'har-output', + harOutput: 'har-output', metadata: {}, verbose: false, }; @@ -597,7 +597,7 @@ describe('createTestContext', () => { input: JSON.stringify({ testInput: 'testValue' }), workflow: undefined, skip: undefined, - harLogsFile: 'har-output', + harOutput: 'har-output', metadata: {}, verbose: false, }; diff --git a/packages/respect-core/src/modules/__tests__/test-config-generator/generate-test-config.test.ts b/packages/respect-core/src/modules/__tests__/test-config-generator/generate-test-config.test.ts index 9bd9983d7d..f2ea859a11 100644 --- a/packages/respect-core/src/modules/__tests__/test-config-generator/generate-test-config.test.ts +++ b/packages/respect-core/src/modules/__tests__/test-config-generator/generate-test-config.test.ts @@ -90,7 +90,7 @@ describe('generateTestConfig', () => { expect( await generateTestConfig({ descriptionPath: 'description.yaml', - outputFile: './final-test-location/output.yaml', + 'output-file': './final-test-location/output.yaml', extended: false, }) ).toEqual({ diff --git a/packages/respect-core/src/modules/flow-runner/context/create-test-context.ts b/packages/respect-core/src/modules/flow-runner/context/create-test-context.ts index 4e9dde53c9..600ac97bcb 100644 --- a/packages/respect-core/src/modules/flow-runner/context/create-test-context.ts +++ b/packages/respect-core/src/modules/flow-runner/context/create-test-context.ts @@ -82,7 +82,7 @@ export async function createTestContext( secretFields: new Set(), mtlsCerts: options.mutualTls?.clientCert || options.mutualTls?.clientKey || options.mutualTls?.caCert - ? resolveMtlsCertificates(options.mutualTls) + ? resolveMtlsCertificates(options.mutualTls, options.workflowPath) : undefined, severity: resolveSeverityConfiguration(options.severity), apiClient, diff --git a/packages/respect-core/src/modules/flow-runner/runner.ts b/packages/respect-core/src/modules/flow-runner/runner.ts index d19021abb9..f1e8dda1cd 100644 --- a/packages/respect-core/src/modules/flow-runner/runner.ts +++ b/packages/respect-core/src/modules/flow-runner/runner.ts @@ -47,8 +47,8 @@ export async function runTestFile( input, skip, server, - harOutput, - jsonOutput, + 'har-output': harOutput, + 'json-output': jsonOutput, severity, } = argv; @@ -64,9 +64,9 @@ export async function runTestFile( server, severity, mutualTls: { - clientCert: argv?.clientCert, - clientKey: argv?.clientKey, - caCert: argv?.caCert, + clientCert: argv['client-cert'], + clientKey: argv['client-key'], + caCert: argv['ca-cert'], }, }; @@ -98,7 +98,7 @@ export async function runTestFile( } async function runWorkflows(testDescription: TestDescription, options: AppOptions) { - const harLogs = options.metadata?.harOutput && createHarLog(); + const harLogs = options?.harOutput && createHarLog(); const apiClient = new ApiFetcher({ harLogs, }); @@ -123,7 +123,7 @@ async function runWorkflows(testDescription: TestDescription, options: AppOption } // json logs should be composed after all workflows are run - const jsonLogs = options.jsonLogsFile ? composeJsonLogs(ctx) : undefined; + const jsonLogs = options.jsonOutput ? composeJsonLogs(ctx) : undefined; return { ...ctx, harLogs, jsonLogs }; } diff --git a/packages/respect-core/src/modules/test-config-generator/generate-test-config.ts b/packages/respect-core/src/modules/test-config-generator/generate-test-config.ts index 013f120ea8..69effb4bd6 100644 --- a/packages/respect-core/src/modules/test-config-generator/generate-test-config.ts +++ b/packages/respect-core/src/modules/test-config-generator/generate-test-config.ts @@ -2,13 +2,8 @@ import * as path from 'path'; import { sortMethods } from '../../utils/sort'; import { bundleOpenApi } from '../description-parser'; -import type { - OperationMethod, - TestDescription, - GenerateConfigFileArgv, - Workflow, - Step, -} from '../../types'; +import type { OperationMethod, TestDescription, Workflow, Step } from '../../types'; +import type { GenerateArazzoFileOptions } from '../../handlers/generate'; type WorkflowsFromDescriptionInput = { descriptionPaths: any; @@ -102,9 +97,9 @@ function resolveDescriptionNameFromPath(descriptionPath: string): string { export async function generateTestConfig({ descriptionPath, - outputFile, + 'output-file': outputFile, extended, -}: GenerateConfigFileArgv) { +}: GenerateArazzoFileOptions) { const { paths: pathsObject, info } = (await bundleOpenApi(descriptionPath, '')) || {}; const sourceDescriptionName = resolveDescriptionNameFromPath(descriptionPath); const resolvedDescriptionPath = outputFile diff --git a/packages/respect-core/src/types.ts b/packages/respect-core/src/types.ts index 517121399c..4b93119947 100644 --- a/packages/respect-core/src/types.ts +++ b/packages/respect-core/src/types.ts @@ -18,6 +18,9 @@ import type { Faker } from './modules/faker'; import type { OperationDetails } from './modules/description-parser'; import type { RuleSeverity } from '@redocly/openapi-core/lib/config/types'; import type { ApiFetcher } from './utils/api-fetcher'; +import type { RespectOptions } from './handlers/run'; +import type { Config } from '@redocly/openapi-core'; +import type { CollectFn } from '@redocly/openapi-core/src/utils'; export type OperationMethod = FromSchema; export type ResponseContext = { @@ -73,20 +76,19 @@ type AdditionalStepProps = { }; export type Step = ArazzoStep & AdditionalStepProps; export type Workflow = Omit & { steps: Step[]; time?: number }; -export type RunArgv = { +export type RunArgv = Omit & { file: string; testDescription?: TestDescription; - workflow?: string[]; - skip?: string[]; - verbose?: boolean; - harOutput?: string; - jsonOutput?: string; input?: string | string[]; server?: string | string[]; severity?: string | string[]; - clientCert?: NonNullable['clientCert']; - clientKey?: NonNullable['clientKey']; - caCert?: NonNullable['caCert']; +}; + +export type CommandArgs = { + argv: T; + config: Config; + version: string; + collectSpecData?: CollectFn; }; export interface RequestContext { @@ -108,8 +110,8 @@ export type AppOptions = { workflow?: string | string[]; skip?: string | string[]; verbose?: boolean; - harLogsFile?: string; - jsonLogsFile?: string; + harOutput?: string; + jsonOutput?: string; metadata?: Record; input?: string | string[]; server?: string | string[]; @@ -230,12 +232,6 @@ export type Check = { additionalMessage?: string; }; -export type GenerateConfigFileArgv = { - descriptionPath: string; - outputFile?: string; - extended?: boolean; -}; - export interface ResultsOfTests { passed: number; failed: number; diff --git a/packages/respect-core/src/utils/__tests__/get-reunite-url.test.ts b/packages/respect-core/src/utils/__tests__/get-reunite-url.test.ts deleted file mode 100644 index cf38eb73cc..0000000000 --- a/packages/respect-core/src/utils/__tests__/get-reunite-url.test.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { getReuniteUrl } from '../get-reunite-url'; - -describe('getReuniteUrl', () => { - it('should return US Reunite url when no residency provided', () => { - expect(getReuniteUrl()).toBe('https://app.cloud.redocly.com/api'); - }); - - it('should return EU Reunite url when residency is set to eu', () => { - expect(getReuniteUrl('eu')).toBe('https://app.cloud.eu.redocly.com/api'); - }); - - it('should return residency url when url provided', () => { - expect(getReuniteUrl('http://someenvironment.redocly.com')).toBe( - 'http://someenvironment.redocly.com/api' - ); - }); -}); diff --git a/packages/respect-core/src/utils/__tests__/mtls/resolve-mtls-certificates.test.ts b/packages/respect-core/src/utils/__tests__/mtls/resolve-mtls-certificates.test.ts index ca8079182d..bf5fc3bf1d 100644 --- a/packages/respect-core/src/utils/__tests__/mtls/resolve-mtls-certificates.test.ts +++ b/packages/respect-core/src/utils/__tests__/mtls/resolve-mtls-certificates.test.ts @@ -33,26 +33,29 @@ describe('resolveMtlsCertificates', () => { // Default successful mock implementations mockAccessSync.mockImplementation(() => undefined); // successful access returns undefined mockReadFileSync.mockImplementation((path: string) => { - switch (path) { - case 'clientCert.pem': - return '-----BEGIN CERTIFICATE-----\nclientCert\n-----END CERTIFICATE-----'; - case 'clientKey.pem': - return '-----BEGIN PRIVATE KEY-----\nclientKey\n-----END PRIVATE KEY-----'; - case 'caCert.pem': - return '-----BEGIN CERTIFICATE-----\ncaCert\n-----END CERTIFICATE-----'; - default: - throw new Error('File not found'); + if (path.includes('clientCert')) { + return '-----BEGIN CERTIFICATE-----\nclientCert\n-----END CERTIFICATE-----'; + } else if (path.includes('clientKey')) { + return '-----BEGIN PRIVATE KEY-----\nclientKey\n-----END PRIVATE KEY-----'; + } else if (path.includes('caCert')) { + return '-----BEGIN CERTIFICATE-----\ncaCert\n-----END CERTIFICATE-----'; + } else { + throw new Error('File not found'); } }); }); it('should resolve certificates', () => { - const certs = resolveMtlsCertificates({ - clientCert: - '-----BEGIN CERTIFICATE-----\nMIICWDCCAd+gAwIBAgIJAP8L\n-----END CERTIFICATE-----', - clientKey: '-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0B\n-----END PRIVATE KEY-----', - caCert: '-----BEGIN CERTIFICATE-----\nMIIDXTCCAkWgAwIBAgIJAK7P\n-----END CERTIFICATE-----', - }); + const certs = resolveMtlsCertificates( + { + clientCert: + '-----BEGIN CERTIFICATE-----\nMIICWDCCAd+gAwIBAgIJAP8L\n-----END CERTIFICATE-----', + clientKey: + '-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0B\n-----END PRIVATE KEY-----', + caCert: '-----BEGIN CERTIFICATE-----\nMIIDXTCCAkWgAwIBAgIJAK7P\n-----END CERTIFICATE-----', + }, + 'test.yaml' + ); expect(certs).toEqual({ clientCert: @@ -63,11 +66,14 @@ describe('resolveMtlsCertificates', () => { }); it('should resolve certificates from file', () => { - const certs = resolveMtlsCertificates({ - clientCert: 'clientCert.pem', - clientKey: 'clientKey.pem', - caCert: 'caCert.pem', - }); + const certs = resolveMtlsCertificates( + { + clientCert: 'clientCert.pem', + clientKey: 'clientKey.pem', + caCert: 'caCert.pem', + }, + 'test.yaml' + ); expect(certs).toEqual({ clientCert: '-----BEGIN CERTIFICATE-----\nclientCert\n-----END CERTIFICATE-----', @@ -83,19 +89,25 @@ describe('resolveMtlsCertificates', () => { }); expect(() => - resolveMtlsCertificates({ - clientCert: 'clientCert.pem', - clientKey: 'clientKey.pem', - caCert: 'caCert.pem', - }) + resolveMtlsCertificates( + { + clientCert: 'clientCert.pem', + clientKey: 'clientKey.pem', + caCert: 'caCert.pem', + }, + 'test.yaml' + ) ).toThrow('Failed to read certificate: File not found'); }); it('should resolve certificate content in case some cert is not provided', () => { - const certs = resolveMtlsCertificates({ - clientCert: 'clientCert.pem', - clientKey: 'clientKey.pem', - }); + const certs = resolveMtlsCertificates( + { + clientCert: 'clientCert.pem', + clientKey: 'clientKey.pem', + }, + 'test.yaml' + ); expect(certs).toEqual({ clientCert: '-----BEGIN CERTIFICATE-----\nclientCert\n-----END CERTIFICATE-----', @@ -105,7 +117,7 @@ describe('resolveMtlsCertificates', () => { }); it('should return undefined if cert is not provided', () => { - const certs = resolveMtlsCertificates({}); + const certs = resolveMtlsCertificates({}, 'test.yaml'); expect(certs).toEqual({ clientCert: undefined, @@ -116,9 +128,12 @@ describe('resolveMtlsCertificates', () => { it('should throw error if certificate is not valid', () => { expect(() => - resolveMtlsCertificates({ - clientCert: '-----BEGIN CERTIFICATE--22323-----END CERTIFICATE-----', - }) + resolveMtlsCertificates( + { + clientCert: '-----BEGIN CERTIFICATE--22323-----END CERTIFICATE-----', + }, + 'test.yaml' + ) ).toThrow('Invalid certificate format'); }); }); diff --git a/packages/respect-core/src/utils/exit-with-error.ts b/packages/respect-core/src/utils/exit-with-error.ts new file mode 100644 index 0000000000..a0da4d14cb --- /dev/null +++ b/packages/respect-core/src/utils/exit-with-error.ts @@ -0,0 +1,10 @@ +import { bgRed } from 'colorette'; +import { DefaultLogger } from '../utils/logger/logger'; + +const logger = DefaultLogger.getInstance(); + +export const exitWithError = (message: string) => { + logger.error(bgRed(message)); + logger.printNewLine(); + throw new Error(message); +}; diff --git a/packages/respect-core/src/utils/get-reunite-url.ts b/packages/respect-core/src/utils/get-reunite-url.ts deleted file mode 100644 index cfe5b2487f..0000000000 --- a/packages/respect-core/src/utils/get-reunite-url.ts +++ /dev/null @@ -1,17 +0,0 @@ -const reuniteUrls = { - us: 'https://app.cloud.redocly.com', - eu: 'https://app.cloud.eu.redocly.com', -} as const; - -export function getReuniteUrl(residency?: string) { - if (!residency) residency = 'us'; - - let reuniteUrl: string = reuniteUrls[residency as keyof typeof reuniteUrls]; - - if (!reuniteUrl) { - reuniteUrl = residency; - } - - const url = new URL('/api', reuniteUrl).toString(); - return url; -} diff --git a/packages/respect-core/src/utils/mtls/resolve-mtls-certificates.ts b/packages/respect-core/src/utils/mtls/resolve-mtls-certificates.ts index c2a1cf3969..18ef66c75a 100644 --- a/packages/respect-core/src/utils/mtls/resolve-mtls-certificates.ts +++ b/packages/respect-core/src/utils/mtls/resolve-mtls-certificates.ts @@ -1,17 +1,21 @@ import * as fs from 'node:fs'; import { type TestContext } from '../../types'; +import * as path from 'node:path'; -export function resolveMtlsCertificates(mtlsCertificates: Partial = {}) { +export function resolveMtlsCertificates( + mtlsCertificates: Partial = {}, + arazzoFilePath: string +) { const { clientCert, clientKey, caCert } = mtlsCertificates; return { - clientCert: resolveCertificate(clientCert), - clientKey: resolveCertificate(clientKey), - caCert: resolveCertificate(caCert), + clientCert: resolveCertificate(clientCert, arazzoFilePath), + clientKey: resolveCertificate(clientKey, arazzoFilePath), + caCert: resolveCertificate(caCert, arazzoFilePath), }; } -function resolveCertificate(cert: string | undefined): string | undefined { +function resolveCertificate(cert: string | undefined, arazzoFilePath: string): string | undefined { if (!cert) return undefined; try { @@ -19,9 +23,12 @@ function resolveCertificate(cert: string | undefined): string | undefined { const isCertContent = cert.includes('-----BEGIN') && cert.includes('-----END'); if (!isCertContent) { + const currentArazzoFileFolder = path.dirname(arazzoFilePath); + const certPath = path.resolve(currentArazzoFileFolder, cert); + // If not a certificate content, treat as file path - fs.accessSync(cert, fs.constants.R_OK); - return fs.readFileSync(cert, 'utf-8'); + fs.accessSync(certPath, fs.constants.R_OK); + return fs.readFileSync(certPath, 'utf-8'); } // Return the certificate content as-is