Skip to content
Merged
297 changes: 20 additions & 277 deletions package-lock.json

Large diffs are not rendered by default.

8 changes: 7 additions & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
"abort-controller": "^3.0.0",
"chokidar": "^3.5.1",
"colorette": "^1.2.0",
"cookie": "^0.7.2",
"dotenv": "^16.4.7",
"form-data": "^4.0.0",
"glob": "^11.0.1",
Expand All @@ -60,17 +61,22 @@
"react-dom": "^17.0.0 || ^18.2.0 || ^19.0.0",
"redoc": "2.5.0",
"semver": "^7.5.2",
"set-cookie-parser": "^2.3.5",
"simple-websocket": "^9.0.0",
"styled-components": "^6.0.7",
"yargs": "17.0.1"
"yargs": "17.0.1",
"undici": "^6.21.1"
},
"devDependencies": {
"@types/configstore": "^5.0.1",
"@types/cookie": "0.6.0",
"@types/pluralize": "^0.0.29",
"@types/react": "^17.0.0 || ^18.2.21 || ^19.0.0",
"@types/react-dom": "^17.0.0 || ^18.2.7 || ^19.0.0",
"@types/semver": "^7.5.0",
"@types/yargs": "17.0.32",
"@types/har-format": "^1.2.16",
"@types/set-cookie-parser": "2.4.10",
"typescript": "5.5.3"
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { createHarLog } from '../../har-logs/index.js';
import { createHarLog } from '../../../../commands/respect/har-logs/har-logs.js';

describe('createHarLog', () => {
it('should create a har log', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { addHeaders } from '../../../har-logs/helpers/add-headers.js';
import { addHeaders } from '../../../../../commands/respect/har-logs/helpers/add-headers.js';

describe('addHeaders', () => {
it('should add headers to an existing Headers object', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { buildHeaders } from '../../../har-logs/helpers/build-headers.js';
import { buildHeaders } from '../../../../../commands/respect/har-logs/helpers/build-headers.js';

describe('buildHeaders', () => {
it('should build headers from an array', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { buildParams } from '../../../har-logs/helpers/build-params.js';
import { buildParams } from '../../../../../commands/respect/har-logs/helpers/build-params.js';

describe('buildParams', () => {
it('should parse url-encoded form data into HAR params format', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { buildRequestCookies } from '../../../har-logs/helpers/build-request-cookies.js';
import { buildRequestCookies } from '../../../../../commands/respect/har-logs/helpers/build-request-cookies.js';

describe('buildRequestCookies', () => {
it('should build cookies from an array', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { buildResponseCookies } from '../../../har-logs/helpers/build-response-cookies.js';
import { buildResponseCookies } from '../../../../../commands/respect/har-logs/helpers/build-response-cookies.js';

describe('buildResponseCookies', () => {
it('should build response cookies', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Dispatcher } from 'undici';
import * as http from 'http';

import { getAgent } from '../../../har-logs/helpers/get-agent.js';
import { getAgent } from '../../../../../commands/respect/har-logs/helpers/get-agent.js';

describe('getAgent', () => {
it('should return an agent', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { getDuration } from '../../../har-logs/helpers/get-duration.js';
import { getDuration } from '../../../../../commands/respect/har-logs/helpers/get-duration.js';

describe('getDuration', () => {
it('should return the duration in milliseconds', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { getInputUrl } from '../../../har-logs/helpers/get-input-url.js';
import { getInputUrl } from '../../../../../commands/respect/har-logs/helpers/get-input-url.js';

describe('getInputUrl', () => {
it('should return a URL object', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { EventEmitter } from 'events';

import type { Dispatcher } from 'undici';

import { handleRequest } from '../../../har-logs/helpers/handle-request.js';
import { handleRequest } from '../../../../../commands/respect/har-logs/helpers/handle-request.js';

describe('handleRequest', () => {
it('should handle undici request', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { createMtlsClient } from '../../mtls/create-mtls-client.js';
import { createMtlsClient } from '../../../../commands/respect/mtls/create-mtls-client.js';

describe('createMtlsClient', () => {
it('should create a client with the correct certificates', () => {
Expand Down
104 changes: 104 additions & 0 deletions packages/cli/src/__tests__/commands/respect/respect.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { handleRespect, type RespectArgv } from '../../../commands/respect/index.js';
import { handleRun } from '@redocly/respect-core';
import { Config } from '@redocly/openapi-core';

// Mock node:fs
vi.mock('node:fs', async () => {
return {
writeFileSync: vi.fn(),
existsSync: vi.fn(() => false), // Return false to prevent plugin loading
readFileSync: vi.fn(() => ''),
accessSync: vi.fn(() => true),
constants: {
F_OK: 0,
R_OK: 4,
},
};
});

// Mock the handleRun function
vi.mock('@redocly/respect-core', async () => {
const actual = await vi.importActual('@redocly/respect-core');
return {
...actual,
handleRun: vi.fn(),
};
});

// Mock @redocly/openapi-core
vi.mock('@redocly/openapi-core', async () => {
const actual = await vi.importActual('@redocly/openapi-core');
return {
...actual,
logger: {
info: vi.fn(),
output: vi.fn(),
printNewLine: vi.fn(),
indent: vi.fn((text) => text),
},
stringifyYaml: vi.fn(() => 'mocked yaml'),
};
});

// Mock displayFilesSummaryTable
vi.mock('../../../commands/respect/display-files-summary-table.js', () => ({
displayFilesSummaryTable: vi.fn(),
}));

describe('handleRespect', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('should call handleRun with the correct arguments', async () => {
const mockConfig = new Config({});
const commandArgs = {
argv: {
files: ['test.arazzo.yaml'],
input: 'name=John',
server: 'server1=http://localhost:3000',
workflow: ['workflow3'],
verbose: true,
'max-steps': 2000,
severity: 'STATUS_CODE_CHECK=warn',
'max-fetch-timeout': 40_000,
'execution-timeout': 3_600_000,
} as RespectArgv,
config: mockConfig,
version: '1.0.0',
collectSpecData: vi.fn(),
};

vi.mocked(handleRun).mockResolvedValue([
{
hasProblems: false,
hasWarnings: false,
file: 'test.arazzo.yaml',
executedWorkflows: [],
options: {} as any,
ctx: {} as any,
totalTimeMs: 100,
totalRequests: 1,
globalTimeoutError: false,
},
]);

await handleRespect(commandArgs);

expect(handleRun).toHaveBeenCalledWith(
expect.objectContaining({
files: ['test.arazzo.yaml'],
input: 'name=John',
server: 'server1=http://localhost:3000',
workflow: ['workflow3'],
verbose: true,
config: expect.anything(),
version: '1.0.0',
collectSpecData: expect.anything(),
severity: 'STATUS_CODE_CHECK=warn',
maxSteps: 2000,
maxFetchTimeout: 40_000,
executionTimeout: 3_600_000,
})
);
});
});
1 change: 1 addition & 0 deletions packages/cli/src/commands/respect/har-logs/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './with-har.js';
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
// - migrated to be used with undici

import { URL } from 'url';
import { Client } from 'undici';
import { Client, type fetch } from 'undici';
import { addHeaders } from './helpers/add-headers.js';
import { getDuration } from './helpers/get-duration.js';
import { buildRequestCookies } from './helpers/build-request-cookies.js';
Expand All @@ -18,11 +18,14 @@ import { buildResponseCookies } from './helpers/build-response-cookies.js';
const HAR_HEADER_NAME = 'x-har-request-id';
const harEntryMap = new Map<string, any>();
export interface WithHar {
(baseFetch: any, defaults?: any): any;
<T extends typeof fetch>(baseFetch: T, defaults?: any): T;
harEntryMap?: Map<string, any>;
}

export const withHar: WithHar = function (baseFetch: any, defaults: any = {}): any {
export const withHar: WithHar = function <T extends typeof fetch>(
baseFetch: any,
defaults: any = {}
): T {
withHar.harEntryMap = harEntryMap;
return async function fetch(input: any, options: any = {}): Promise<any> {
const {
Expand Down Expand Up @@ -229,5 +232,5 @@ export const withHar: WithHar = function (baseFetch: any, defaults: any = {}): a
}

return responseCopy;
};
} as T;
};
48 changes: 33 additions & 15 deletions packages/cli/src/commands/respect/index.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import { handleRun, maskSecrets, type JsonLogs } from '@redocly/respect-core';
import { HandledError, logger } from '@redocly/openapi-core';
import { type CommandArgs } from '../../wrapper';
import { writeFileSync } from 'node:fs';
import { dirname } from 'node:path';
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
import { dirname, basename } from 'node:path';
import { blue, green } from 'colorette';
import { composeJsonLogsFiles } from './json-logs.js';
import { displayFilesSummaryTable } from './display-files-summary-table.js';
import { readEnvVariables } from '../../utils/read-env-variables.js';
import { resolveMtlsCertificates } from './mtls/resolve-mtls-certificates.js';
import { withMtlsClientIfNeeded } from './mtls/create-mtls-client.js';
import { withHar } from './har-logs/index.js';
import { createHarLog } from './har-logs/har-logs.js';

export type RespectArgv = {
files: string[];
Expand Down Expand Up @@ -35,6 +38,7 @@ export async function handleRespect({
collectSpecData,
}: CommandArgs<RespectArgv>) {
let mtlsCerts;
let harLogs;

try {
const workingDir = config.configPath ? dirname(config.configPath) : process.cwd();
Expand All @@ -53,6 +57,12 @@ export async function handleRespect({
: undefined;
}

let customFetch = withMtlsClientIfNeeded(mtlsCerts);
if (argv['har-output']) {
harLogs = createHarLog({ version });
customFetch = withHar(customFetch, { har: harLogs });
}

const options = {
files: argv.files,
input: argv.input,
Expand All @@ -64,29 +74,37 @@ export async function handleRespect({
version,
collectSpecData,
severity: argv.severity,
harOutput: argv['har-output'],
jsonOutput: argv['json-output'],
mtlsCerts,
maxSteps: argv['max-steps'],
maxFetchTimeout: argv['max-fetch-timeout'],
executionTimeout: argv['execution-timeout'],
requestFileLoader: {
getFileBody: async (filePath: string) => {
if (!existsSync(filePath)) {
throw new Error(`File ${filePath} doesn't exist or isn't readable.`);
}

const buffer = readFileSync(filePath);
return new File([buffer], basename(filePath));
},
},
envVariables: readEnvVariables(workingDir) || {},
logger,
fetch: customFetch as unknown as typeof fetch,
};

if (options.skip && options.workflow) {
throw new Error(`Cannot use both --skip and --workflow flags at the same time.`);
}

if (options.harOutput && !options.harOutput.endsWith('.har')) {
if (argv['har-output'] && !argv['har-output'].endsWith('.har')) {
throw new Error('File for HAR logs should be in .har format');
}

if (options.jsonOutput && !options.jsonOutput.endsWith('.json')) {
if (argv['json-output'] && !argv['json-output'].endsWith('.json')) {
throw new Error('File for JSON logs should be in .json format');
}

if (options.files.length > 1 && options.harOutput) {
if (options.files.length > 1 && argv['har-output']) {
// TODO: implement multiple run files HAR output
throw new Error(
'Currently only a single file can be run with --har-output. Please run a single file at a time.'
Expand All @@ -104,26 +122,26 @@ export async function handleRespect({
const hasProblems = runAllFilesResult.some((result) => result.hasProblems);
const hasWarnings = runAllFilesResult.some((result) => result.hasWarnings);

if (options.jsonOutput) {
if (argv['json-output']) {
const jsonOutputData = {
files: composeJsonLogsFiles(runAllFilesResult),
status: hasProblems ? 'error' : hasWarnings ? 'warn' : 'success',
totalTime: performance.now() - startedAt,
} as JsonLogs;

writeFileSync(options.jsonOutput, JSON.stringify(jsonOutputData, null, 2), 'utf-8');
writeFileSync(argv['json-output'], JSON.stringify(jsonOutputData, null, 2), 'utf-8');

logger.output(blue(logger.indent(`JSON logs saved in ${green(options.jsonOutput)}`, 2)));
logger.output(blue(logger.indent(`JSON logs saved in ${green(argv['json-output'])}`, 2)));
logger.printNewLine();
logger.printNewLine();
}

if (options.harOutput) {
if (argv['har-output']) {
// TODO: implement multiple run files HAR output
for (const result of runAllFilesResult) {
const parsedHarLogs = maskSecrets(result.harLogs, result.ctx.secretFields || new Set());
writeFileSync(options.harOutput, JSON.stringify(parsedHarLogs, null, 2), 'utf-8');
logger.output(blue(`Har logs saved in ${green(options.harOutput)}`));
const parsedHarLogs = maskSecrets(harLogs, result.ctx.secretFields || new Set());
writeFileSync(argv['har-output'], JSON.stringify(parsedHarLogs, null, 2), 'utf-8');
logger.output(blue(`Har logs saved in ${green(argv['har-output'])}`));
logger.printNewLine();
logger.printNewLine();
}
Expand Down
Loading
Loading