@apiquest/fracture 1.0.4 → 1.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +90 -2
- package/dist/CollectionRunner.d.ts +2 -0
- package/dist/CollectionRunner.d.ts.map +1 -1
- package/dist/CollectionRunner.js +20 -2
- package/dist/CollectionRunner.js.map +1 -1
- package/dist/LibraryLoader.d.ts +49 -0
- package/dist/LibraryLoader.d.ts.map +1 -0
- package/dist/LibraryLoader.js +198 -0
- package/dist/LibraryLoader.js.map +1 -0
- package/dist/PluginLoader.d.ts.map +1 -1
- package/dist/PluginLoader.js +9 -6
- package/dist/PluginLoader.js.map +1 -1
- package/dist/PluginResolver.d.ts +1 -1
- package/dist/PluginResolver.d.ts.map +1 -1
- package/dist/PluginResolver.js +1 -1
- package/dist/PluginResolver.js.map +1 -1
- package/dist/ScriptEngine.d.ts +2 -1
- package/dist/ScriptEngine.d.ts.map +1 -1
- package/dist/ScriptEngine.js +15 -8
- package/dist/ScriptEngine.js.map +1 -1
- package/dist/cli/index.js +35 -3
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/plugin-commands.d.ts.map +1 -1
- package/dist/cli/plugin-commands.js +47 -81
- package/dist/cli/plugin-commands.js.map +1 -1
- package/dist/cli/plugin-installer.d.ts +48 -0
- package/dist/cli/plugin-installer.d.ts.map +1 -0
- package/dist/cli/plugin-installer.js +136 -0
- package/dist/cli/plugin-installer.js.map +1 -0
- package/dist/cli/plugin-registry.d.ts +17 -0
- package/dist/cli/plugin-registry.d.ts.map +1 -0
- package/dist/cli/plugin-registry.js +77 -0
- package/dist/cli/plugin-registry.js.map +1 -0
- package/package.json +1 -1
- package/src/CollectionAnalyzer.ts +0 -102
- package/src/CollectionRunner.ts +0 -1423
- package/src/CollectionRunner.types.ts +0 -9
- package/src/CollectionValidator.ts +0 -289
- package/src/ConsoleReporter.ts +0 -143
- package/src/CookieJar.ts +0 -258
- package/src/DagScheduler.ts +0 -439
- package/src/Logger.ts +0 -85
- package/src/PluginLoader.ts +0 -126
- package/src/PluginManager.ts +0 -208
- package/src/PluginResolver.ts +0 -154
- package/src/QuestAPI.ts +0 -764
- package/src/QuestAPI.types.ts +0 -33
- package/src/QuestTestAPI.ts +0 -164
- package/src/RequestFilter.ts +0 -224
- package/src/ScriptEngine.ts +0 -219
- package/src/ScriptValidator.ts +0 -428
- package/src/TaskGraph.ts +0 -598
- package/src/TestCounter.ts +0 -109
- package/src/VariableResolver.ts +0 -114
- package/src/cli/index.ts +0 -480
- package/src/cli/plugin-commands.ts +0 -342
- package/src/cli/plugin-discovery.ts +0 -44
- package/src/index.ts +0 -24
- package/src/utils.ts +0 -52
package/src/VariableResolver.ts
DELETED
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
import type { ExecutionContext, IterationData } from '@apiquest/types';
|
|
2
|
-
import { extractValue, isNullOrEmpty } from './utils.js';
|
|
3
|
-
import { Logger } from './Logger.js';
|
|
4
|
-
|
|
5
|
-
export class VariableResolver {
|
|
6
|
-
private logger: Logger;
|
|
7
|
-
|
|
8
|
-
constructor(baseLogger?: Logger) {
|
|
9
|
-
this.logger = baseLogger?.createLogger('VariableResolver') ?? new Logger('VariableResolver');
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Resolve {{variables}} in a template string
|
|
14
|
-
* Priority: iteration data > local > collection > environment > global
|
|
15
|
-
*/
|
|
16
|
-
resolve(template: string, context: ExecutionContext): string {
|
|
17
|
-
if (isNullOrEmpty(template) || typeof template !== 'string') {
|
|
18
|
-
return template;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
// Find all {{variable}} patterns
|
|
22
|
-
const variableCount = (template.match(/\{\{([^}]+)\}\}/g) ?? []).length;
|
|
23
|
-
if (variableCount > 0) {
|
|
24
|
-
this.logger.trace(`Resolving ${variableCount} variable(s) in template`);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
return template.replace(/\{\{([^}]+)\}\}/g, (match: string, varName: string) => {
|
|
28
|
-
const trimmedName = varName.trim();
|
|
29
|
-
const value = this.getVariable(trimmedName, context);
|
|
30
|
-
|
|
31
|
-
if (value !== null && value !== undefined) {
|
|
32
|
-
this.logger.trace(`Resolved {{${trimmedName}}} -> ${typeof value === 'string' && value.length > 50 ? value.substring(0, 50) + '...' : value}`);
|
|
33
|
-
return String(value);
|
|
34
|
-
} else {
|
|
35
|
-
this.logger.trace(`Variable {{${trimmedName}}} not found, keeping placeholder`);
|
|
36
|
-
return match;
|
|
37
|
-
}
|
|
38
|
-
});
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Resolve all values in an object (recursively)
|
|
43
|
-
*/
|
|
44
|
-
resolveAll(obj: unknown, context: ExecutionContext): unknown {
|
|
45
|
-
if (obj === null || obj === undefined) {
|
|
46
|
-
return obj;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
if (typeof obj === 'string') {
|
|
50
|
-
return this.resolve(obj, context);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
if (Array.isArray(obj)) {
|
|
54
|
-
return obj.map(item => this.resolveAll(item, context));
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
if (typeof obj === 'object') {
|
|
58
|
-
const resolved: Record<string, unknown> = {};
|
|
59
|
-
for (const [key, value] of Object.entries(obj)) {
|
|
60
|
-
resolved[key] = this.resolveAll(value, context);
|
|
61
|
-
}
|
|
62
|
-
return resolved;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
return obj;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Get variable value with cascading priority
|
|
70
|
-
* Priority: iteration data > scope stack (innermost to outermost) > collection > environment > global
|
|
71
|
-
*/
|
|
72
|
-
private getVariable(name: string, context: ExecutionContext): unknown {
|
|
73
|
-
// 1. Iteration data (highest priority)
|
|
74
|
-
const currentIterationData = context.iterationData?.[context.iterationCurrent - 1];
|
|
75
|
-
if (currentIterationData !== null && currentIterationData !== undefined && name in currentIterationData) {
|
|
76
|
-
this.logger.trace(`Variable '${name}' found in iteration data`);
|
|
77
|
-
return currentIterationData[name];
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// 2. Scope stack (hierarchical scope variables - search from innermost to outermost)
|
|
81
|
-
// This represents quest.scope.variables which flows through the script inheritance chain
|
|
82
|
-
if (context.scopeStack !== null && context.scopeStack !== undefined && context.scopeStack.length > 0) {
|
|
83
|
-
// Search from top of stack (most specific) to bottom (least specific)
|
|
84
|
-
for (let i = context.scopeStack.length - 1; i >= 0; i--) {
|
|
85
|
-
const frame = context.scopeStack[i];
|
|
86
|
-
if (name in frame.vars) {
|
|
87
|
-
this.logger.trace(`Variable '${name}' found in scope stack (frame ${context.scopeStack.length - 1 - i})`);
|
|
88
|
-
return frame.vars[name];
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// 3. Collection variables
|
|
94
|
-
if (name in context.collectionVariables) {
|
|
95
|
-
this.logger.trace(`Variable '${name}' found in collection variables`);
|
|
96
|
-
return extractValue(context.collectionVariables[name]);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// 4. Environment variables
|
|
100
|
-
if (context.environment !== null && context.environment !== undefined && name in context.environment.variables) {
|
|
101
|
-
this.logger.trace(`Variable '${name}' found in environment variables`);
|
|
102
|
-
return extractValue(context.environment.variables[name]);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// 5. Global variables (lowest priority)
|
|
106
|
-
if (name in context.globalVariables) {
|
|
107
|
-
this.logger.trace(`Variable '${name}' found in global variables`);
|
|
108
|
-
return extractValue(context.globalVariables[name]);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
this.logger.trace(`Variable '${name}' not found in any scope`);
|
|
112
|
-
return null;
|
|
113
|
-
}
|
|
114
|
-
}
|
package/src/cli/index.ts
DELETED
|
@@ -1,480 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import { Command } from 'commander';
|
|
4
|
-
import { readFileSync } from 'fs';
|
|
5
|
-
import { CollectionRunner } from '../CollectionRunner.js';
|
|
6
|
-
import { ConsoleReporter } from '../ConsoleReporter.js';
|
|
7
|
-
import type { Collection, Environment, IterationData, RuntimeOptions, EventPayloads, ValidationResult } from '@apiquest/types';
|
|
8
|
-
import { LogLevel } from '@apiquest/types';
|
|
9
|
-
import { getPluginDirectories } from './plugin-discovery.js';
|
|
10
|
-
import { addPluginCommands } from './plugin-commands.js';
|
|
11
|
-
|
|
12
|
-
interface CLIOptions {
|
|
13
|
-
config?: string;
|
|
14
|
-
logLevel?: string;
|
|
15
|
-
environment?: string;
|
|
16
|
-
envVar?: Record<string, string>;
|
|
17
|
-
global?: Record<string, string>;
|
|
18
|
-
data?: string;
|
|
19
|
-
iterations?: number;
|
|
20
|
-
filter?: string;
|
|
21
|
-
excludeDeps?: boolean;
|
|
22
|
-
parallel?: boolean;
|
|
23
|
-
concurrency?: number;
|
|
24
|
-
bail?: boolean;
|
|
25
|
-
delay?: number;
|
|
26
|
-
timeout?: number;
|
|
27
|
-
sslCert?: string;
|
|
28
|
-
sslKey?: string;
|
|
29
|
-
sslKeyPassphrase?: string;
|
|
30
|
-
sslCa?: string;
|
|
31
|
-
insecure?: boolean;
|
|
32
|
-
proxy?: string;
|
|
33
|
-
proxyAuth?: string;
|
|
34
|
-
noProxy?: string;
|
|
35
|
-
followRedirects?: boolean;
|
|
36
|
-
maxRedirects?: number;
|
|
37
|
-
cookie?: string | string[];
|
|
38
|
-
cookieJar?: boolean;
|
|
39
|
-
cookieJarPersist?: boolean;
|
|
40
|
-
silent?: boolean;
|
|
41
|
-
color?: boolean;
|
|
42
|
-
strictMode?: boolean;
|
|
43
|
-
reporters?: string;
|
|
44
|
-
out?: string;
|
|
45
|
-
pluginsDir?: string[];
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Load configuration from a JSON file
|
|
50
|
-
*/
|
|
51
|
-
function loadConfigFile(configPath: string): Partial<CLIOptions> {
|
|
52
|
-
try {
|
|
53
|
-
const configContent = readFileSync(configPath, 'utf-8');
|
|
54
|
-
return JSON.parse(configContent) as Partial<CLIOptions>;
|
|
55
|
-
} catch (error) {
|
|
56
|
-
console.error(`Error loading config file '${configPath}':`, error instanceof Error ? error.message : String(error));
|
|
57
|
-
process.exit(4);
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Merge config file options with CLI options
|
|
63
|
-
* CLI options take precedence over config file options
|
|
64
|
-
*/
|
|
65
|
-
function mergeOptions(configOptions: Partial<CLIOptions>, cliOptions: CLIOptions): CLIOptions {
|
|
66
|
-
// Create a merged options object
|
|
67
|
-
// CLI options override config file options
|
|
68
|
-
const merged: CLIOptions = { ...configOptions, ...cliOptions };
|
|
69
|
-
|
|
70
|
-
// Special handling for objects that should be merged rather than replaced
|
|
71
|
-
if (configOptions.envVar !== undefined && cliOptions.envVar !== undefined) {
|
|
72
|
-
merged.envVar = { ...configOptions.envVar, ...cliOptions.envVar };
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
if (configOptions.global !== undefined && cliOptions.global !== undefined) {
|
|
76
|
-
merged.global = { ...configOptions.global, ...cliOptions.global };
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// Arrays that should be merged
|
|
80
|
-
if (configOptions.cookie !== undefined && cliOptions.cookie !== undefined) {
|
|
81
|
-
const configCookies = Array.isArray(configOptions.cookie) ? configOptions.cookie : [configOptions.cookie];
|
|
82
|
-
const cliCookies = Array.isArray(cliOptions.cookie) ? cliOptions.cookie : [cliOptions.cookie];
|
|
83
|
-
merged.cookie = [...configCookies, ...cliCookies];
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
return merged;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
const program = new Command();
|
|
90
|
-
|
|
91
|
-
// Get command name from process.argv[1] (the bin script name)
|
|
92
|
-
const commandName = process.argv[1]?.split(/[\\/]/).pop()?.replace('.js', '') ?? 'fracture';
|
|
93
|
-
|
|
94
|
-
program
|
|
95
|
-
.name(commandName)
|
|
96
|
-
.description('ApiQuest/Fracture - API testing tool')
|
|
97
|
-
.version('1.0.0');
|
|
98
|
-
|
|
99
|
-
// Add plugin management commands
|
|
100
|
-
addPluginCommands(program);
|
|
101
|
-
|
|
102
|
-
program
|
|
103
|
-
.command('run')
|
|
104
|
-
.description('Run a collection')
|
|
105
|
-
.argument('<collection>', 'Path to collection JSON file')
|
|
106
|
-
// Variables & Environment
|
|
107
|
-
.option('-g, --global <key=value...>', 'Set global variable (repeatable)', collectKeyValue, {} as Record<string, string>)
|
|
108
|
-
.option('-e, --environment <file>', 'Environment JSON file')
|
|
109
|
-
.option('--env-var <key=value...>', 'Set environment variable (repeatable)', collectKeyValue, {} as Record<string, string>)
|
|
110
|
-
// Data & Iterations
|
|
111
|
-
.option('-d, --data <file>', 'Iteration data file (CSV/JSON)')
|
|
112
|
-
.option('-n, --iterations <count>', 'Limit number of iterations', parseInt)
|
|
113
|
-
// Filtering & Selection
|
|
114
|
-
.option('--filter <pattern>', 'Filter requests by path using regex pattern')
|
|
115
|
-
.option('--exclude-deps', 'Exclude dependencies when filtering')
|
|
116
|
-
// Execution Control
|
|
117
|
-
.option('--parallel', 'Enable parallel execution')
|
|
118
|
-
.option('--concurrency <number>', 'Max concurrent requests', parseInt)
|
|
119
|
-
.option('--bail', 'Stop on first test failure')
|
|
120
|
-
.option('--delay <ms>', 'Delay between requests in milliseconds', parseInt)
|
|
121
|
-
// Timeouts
|
|
122
|
-
.option('--timeout <ms>', 'Request timeout in milliseconds', parseInt)
|
|
123
|
-
// SSL/TLS
|
|
124
|
-
.option('--ssl-cert <path>', 'Client certificate file (PEM format)')
|
|
125
|
-
.option('--ssl-key <path>', 'Client private key file')
|
|
126
|
-
.option('--ssl-key-passphrase <password>', 'Client key passphrase')
|
|
127
|
-
.option('--ssl-ca <path>', 'CA certificate bundle')
|
|
128
|
-
.option('--insecure', 'Disable SSL certificate validation')
|
|
129
|
-
// Proxy
|
|
130
|
-
.option('--proxy <url>', 'HTTP/HTTPS proxy URL (http://host:port)')
|
|
131
|
-
.option('--proxy-auth <user:pass>', 'Proxy authentication credentials')
|
|
132
|
-
.option('--no-proxy <hosts>', 'Bypass proxy for hosts (comma-separated)')
|
|
133
|
-
// Redirects
|
|
134
|
-
.option('--follow-redirects', 'Follow HTTP redirects (default: true)')
|
|
135
|
-
.option('--no-follow-redirects', 'Don\'t follow HTTP redirects')
|
|
136
|
-
.option('--max-redirects <count>', 'Maximum redirects to follow (default: 20)', parseInt)
|
|
137
|
-
// Cookies
|
|
138
|
-
.option('--cookie <name=value>', 'Set cookie for requests (repeatable)')
|
|
139
|
-
.option('--cookie-jar', 'Enable persistent cookie jar')
|
|
140
|
-
.option('--cookie-jar-persist', 'Persist cookies across runs')
|
|
141
|
-
// Output & Reporting
|
|
142
|
-
.option('-r, --reporters <types>', 'Output reporters (comma-separated)', 'cli')
|
|
143
|
-
.option('-o, --out <directory>', 'Output directory for reports')
|
|
144
|
-
.option('--no-color', 'Disable colored output')
|
|
145
|
-
.option('--silent', 'Suppress console output')
|
|
146
|
-
.option('--log-level <level>', 'Log level: error, warn, info, debug, trace (default: info)')
|
|
147
|
-
// Validation & Testing
|
|
148
|
-
.option('--no-strict-mode', 'Disable strict validation mode')
|
|
149
|
-
// Plugins
|
|
150
|
-
.option('--plugin-dir <path>', 'Plugin directory to scan (repeatable, appended to auto-discovered paths)', collectArray, [] as string[])
|
|
151
|
-
// Configuration
|
|
152
|
-
.option('--config <file>', 'Load options from config file')
|
|
153
|
-
.action(async (collectionPath: string, cliOptions: CLIOptions) => {
|
|
154
|
-
// Load and merge config file if specified
|
|
155
|
-
let options = cliOptions;
|
|
156
|
-
if (cliOptions.config !== undefined) {
|
|
157
|
-
const configOptions = loadConfigFile(cliOptions.config);
|
|
158
|
-
options = mergeOptions(configOptions, cliOptions);
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
// Validate log level if provided
|
|
162
|
-
const validLogLevels = ['error', 'warn', 'info', 'debug', 'trace'];
|
|
163
|
-
if (options.logLevel !== undefined && !validLogLevels.includes(options.logLevel)) {
|
|
164
|
-
console.error(`Error: Invalid log level '${options.logLevel}'. Valid levels: ${validLogLevels.join(', ')}`);
|
|
165
|
-
process.exit(2);
|
|
166
|
-
}
|
|
167
|
-
try {
|
|
168
|
-
// Load collection
|
|
169
|
-
const collectionContent = readFileSync(collectionPath, 'utf-8');
|
|
170
|
-
const collection: Collection = JSON.parse(collectionContent) as Collection;
|
|
171
|
-
|
|
172
|
-
// Load environment if specified
|
|
173
|
-
let environment: Environment | undefined;
|
|
174
|
-
if (options.environment !== undefined) {
|
|
175
|
-
const envContent = readFileSync(options.environment, 'utf-8');
|
|
176
|
-
environment = JSON.parse(envContent) as Environment;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
// Merge env-var options into environment
|
|
180
|
-
if (options.envVar !== undefined && Object.keys(options.envVar).length > 0) {
|
|
181
|
-
environment ??= { name: 'CLI Environment', variables: {} };
|
|
182
|
-
environment.variables = { ...environment.variables, ...options.envVar };
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
// Load iteration data if specified
|
|
186
|
-
let iterationData: IterationData[] | undefined;
|
|
187
|
-
if (options.data !== undefined) {
|
|
188
|
-
const dataContent = readFileSync(options.data, 'utf-8');
|
|
189
|
-
if (options.data.endsWith('.json')) {
|
|
190
|
-
iterationData = JSON.parse(dataContent) as IterationData[];
|
|
191
|
-
} else if (options.data.endsWith('.csv')) {
|
|
192
|
-
iterationData = parseCSV(dataContent);
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
// Configure HTTP plugin for SSL validation
|
|
197
|
-
if (options.insecure === true) {
|
|
198
|
-
// This will be passed to axios config in the HTTP plugin
|
|
199
|
-
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
// Get plugin directories for auto-discovery
|
|
203
|
-
const pluginDirs = getPluginDirectories();
|
|
204
|
-
|
|
205
|
-
// Append user-specified plugin directories (if any)
|
|
206
|
-
if (options.pluginsDir !== undefined && options.pluginsDir.length > 0) {
|
|
207
|
-
pluginDirs.push(...options.pluginsDir);
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
// Convert string log level to LogLevel enum
|
|
211
|
-
let logLevel: LogLevel | undefined;
|
|
212
|
-
if (options.logLevel !== undefined) {
|
|
213
|
-
const levelMap: Record<string, LogLevel> = {
|
|
214
|
-
'error': LogLevel.ERROR,
|
|
215
|
-
'warn': LogLevel.WARN,
|
|
216
|
-
'info': LogLevel.INFO,
|
|
217
|
-
'debug': LogLevel.DEBUG,
|
|
218
|
-
'trace': LogLevel.TRACE
|
|
219
|
-
};
|
|
220
|
-
logLevel = levelMap[options.logLevel];
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
// Create runner with plugin auto-discovery and log level
|
|
224
|
-
const runner = new CollectionRunner({
|
|
225
|
-
pluginsDir: pluginDirs,
|
|
226
|
-
logLevel
|
|
227
|
-
});
|
|
228
|
-
|
|
229
|
-
// Set up console reporter
|
|
230
|
-
const silent = options.silent;
|
|
231
|
-
const color = options.color; // --no-color sets this to false
|
|
232
|
-
|
|
233
|
-
if (silent !== true) {
|
|
234
|
-
const reporter = new ConsoleReporter({
|
|
235
|
-
logLevel,
|
|
236
|
-
color,
|
|
237
|
-
runner
|
|
238
|
-
});
|
|
239
|
-
|
|
240
|
-
// Wire up reporter to runner events
|
|
241
|
-
runner.on('beforeRun', (payload: EventPayloads['beforeRun']) => {
|
|
242
|
-
reporter.onRunStarted(collection, payload.options as Record<string, unknown>);
|
|
243
|
-
|
|
244
|
-
// Show expected test count if available
|
|
245
|
-
if (payload.expectedTestCount !== undefined && payload.expectedTestCount >= 0) {
|
|
246
|
-
console.log(`Expected tests: ${payload.expectedTestCount}`);
|
|
247
|
-
console.log('');
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
// Show validation errors if any (shouldn't reach here but defensive)
|
|
251
|
-
if (payload.validationResult?.valid === false && payload.validationResult.errors !== undefined) {
|
|
252
|
-
console.error('\nValidation errors detected:');
|
|
253
|
-
for (const error of payload.validationResult.errors) {
|
|
254
|
-
console.error(` ${error.location}: ${error.message}`);
|
|
255
|
-
}
|
|
256
|
-
console.error('');
|
|
257
|
-
}
|
|
258
|
-
});
|
|
259
|
-
|
|
260
|
-
runner.on('beforeRequest', (payload: EventPayloads['beforeRequest']) => {
|
|
261
|
-
reporter.onBeforeRequest?.(payload);
|
|
262
|
-
});
|
|
263
|
-
|
|
264
|
-
runner.on('afterRequest', (payload: EventPayloads['afterRequest']) => {
|
|
265
|
-
reporter.onAfterRequest?.(payload);
|
|
266
|
-
});
|
|
267
|
-
|
|
268
|
-
runner.on('assertion', (payload: EventPayloads['assertion']) => {
|
|
269
|
-
reporter.onAssertion?.(payload);
|
|
270
|
-
});
|
|
271
|
-
|
|
272
|
-
runner.on('afterRun', (payload: EventPayloads['afterRun']) => {
|
|
273
|
-
reporter.onRunCompleted(payload.result);
|
|
274
|
-
});
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
// Build RuntimeOptions from CLI options
|
|
278
|
-
const runOptions: Record<string, unknown> = {
|
|
279
|
-
// CLI-specific options
|
|
280
|
-
environment,
|
|
281
|
-
globalVariables: options.global,
|
|
282
|
-
data: iterationData,
|
|
283
|
-
iterations: options.iterations,
|
|
284
|
-
filter: options.filter,
|
|
285
|
-
excludeDeps: options.excludeDeps,
|
|
286
|
-
|
|
287
|
-
// RuntimeOptions - Execution
|
|
288
|
-
execution: {
|
|
289
|
-
...(options.parallel !== undefined ? { allowParallel: options.parallel } : {}),
|
|
290
|
-
...(options.concurrency !== undefined ? { maxConcurrency: options.concurrency } : {}),
|
|
291
|
-
...(options.bail !== undefined ? { bail: options.bail } : {}),
|
|
292
|
-
...(options.delay !== undefined ? { delay: options.delay } : {})
|
|
293
|
-
},
|
|
294
|
-
|
|
295
|
-
// RuntimeOptions - Timeout
|
|
296
|
-
...(options.timeout !== undefined ? {
|
|
297
|
-
timeout: { request: options.timeout }
|
|
298
|
-
} : {}),
|
|
299
|
-
|
|
300
|
-
// RuntimeOptions - SSL
|
|
301
|
-
...(options.sslCert !== undefined || options.sslKey !== undefined || options.sslCa !== undefined || options.insecure !== undefined ? {
|
|
302
|
-
ssl: {
|
|
303
|
-
...(options.insecure !== undefined ? { validateCertificates: options.insecure === false } : {}),
|
|
304
|
-
...(options.sslCert !== undefined || options.sslKey !== undefined ? {
|
|
305
|
-
clientCertificate: {
|
|
306
|
-
...(options.sslCert !== undefined ? { cert: readFileSync(options.sslCert, 'utf-8') } : {}),
|
|
307
|
-
...(options.sslKey !== undefined ? { key: readFileSync(options.sslKey, 'utf-8') } : {}),
|
|
308
|
-
...(options.sslKeyPassphrase !== undefined ? { passphrase: options.sslKeyPassphrase } : {})
|
|
309
|
-
}
|
|
310
|
-
} : {}),
|
|
311
|
-
...(options.sslCa !== undefined ? { ca: readFileSync(options.sslCa, 'utf-8') } : {})
|
|
312
|
-
}
|
|
313
|
-
} : {}),
|
|
314
|
-
|
|
315
|
-
// RuntimeOptions - Proxy
|
|
316
|
-
...(options.proxy !== undefined ? {
|
|
317
|
-
proxy: {
|
|
318
|
-
enabled: true,
|
|
319
|
-
...parseProxyUrl(options.proxy)!,
|
|
320
|
-
...(options.proxyAuth !== undefined ? { auth: parseProxyAuth(options.proxyAuth)! } : {}),
|
|
321
|
-
...(options.noProxy !== undefined ? { bypass: parseNoProxy(options.noProxy) } : {})
|
|
322
|
-
}
|
|
323
|
-
} : {}),
|
|
324
|
-
|
|
325
|
-
// RuntimeOptions - Cookies
|
|
326
|
-
...(options.cookie !== undefined ? {
|
|
327
|
-
cookies: (Array.isArray(options.cookie) ? options.cookie : [options.cookie]).map(parseCookie).filter((c): c is { name: string; value: string } => c !== null)
|
|
328
|
-
} : {}),
|
|
329
|
-
|
|
330
|
-
// RuntimeOptions - Cookie Jar
|
|
331
|
-
...(options.cookieJar !== undefined || options.cookieJarPersist !== undefined ? {
|
|
332
|
-
jar: {
|
|
333
|
-
enabled: options.cookieJar ?? false,
|
|
334
|
-
...(options.cookieJarPersist !== undefined ? { persist: options.cookieJarPersist } : {})
|
|
335
|
-
}
|
|
336
|
-
} : {}),
|
|
337
|
-
|
|
338
|
-
// RuntimeOptions - Redirects
|
|
339
|
-
...(options.followRedirects !== undefined ? { followRedirects: options.followRedirects } : {}),
|
|
340
|
-
...(options.maxRedirects !== undefined ? { maxRedirects: options.maxRedirects } : {}),
|
|
341
|
-
|
|
342
|
-
// RuntimeOptions - Validation
|
|
343
|
-
strictMode: options.strictMode !== false // --no-strict-mode sets this to false, default is true
|
|
344
|
-
};
|
|
345
|
-
|
|
346
|
-
// Run collection
|
|
347
|
-
const result = await runner.run(collection, runOptions);
|
|
348
|
-
|
|
349
|
-
// Check for validation errors (pre-run validation failed)
|
|
350
|
-
if (result.validationErrors !== undefined && result.validationErrors.length > 0) {
|
|
351
|
-
if (silent !== true) {
|
|
352
|
-
console.error('\nPre-run validation failed:\n');
|
|
353
|
-
for (const error of result.validationErrors) {
|
|
354
|
-
console.error(` ${error.location}: ${error.message}`);
|
|
355
|
-
if (error.details?.line !== undefined) {
|
|
356
|
-
console.error(` at line ${error.details.line}${error.details.column !== undefined ? `:${error.details.column}` : ''}`);
|
|
357
|
-
}
|
|
358
|
-
if (error.details?.suggestion !== undefined) {
|
|
359
|
-
console.error(` > ${error.details.suggestion}`);
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
process.exit(3); // Exit code 3 for validation failures
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
// Results are displayed by the reporter
|
|
367
|
-
// Determine exit code based on TEST results because request errors may be expected
|
|
368
|
-
const hasErrors = result.failedTests > 0;
|
|
369
|
-
process.exit(hasErrors ? 1 : 0);
|
|
370
|
-
|
|
371
|
-
} catch (error) {
|
|
372
|
-
console.error('Error:', error instanceof Error ? error.message : String(error));
|
|
373
|
-
process.exit(4);
|
|
374
|
-
}
|
|
375
|
-
});
|
|
376
|
-
|
|
377
|
-
program.parse();
|
|
378
|
-
|
|
379
|
-
function collectKeyValue(value: string, previous: Record<string, string>): Record<string, string> {
|
|
380
|
-
const [key, val] = value.split('=');
|
|
381
|
-
if (key === undefined || key === '' || val === undefined) {
|
|
382
|
-
throw new Error(`Invalid key=value format: ${value}`);
|
|
383
|
-
}
|
|
384
|
-
return { ...previous, [key.trim()]: val.trim() };
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
function collectArray(value: string, previous: string[]): string[] {
|
|
388
|
-
return [...previous, value];
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
function parseCSV(content: string): IterationData[] {
|
|
392
|
-
const lines = content.split('\n').filter(line => line.trim() !== '');
|
|
393
|
-
if (lines.length === 0) return [];
|
|
394
|
-
|
|
395
|
-
const headers = lines[0].split(',').map(h => h.trim());
|
|
396
|
-
const data: IterationData[] = [];
|
|
397
|
-
|
|
398
|
-
for (let i = 1; i < lines.length; i++) {
|
|
399
|
-
const values = lines[i].split(',').map(v => v.trim());
|
|
400
|
-
const row: IterationData = {};
|
|
401
|
-
|
|
402
|
-
headers.forEach((header, index) => {
|
|
403
|
-
const value = values[index];
|
|
404
|
-
// Try to parse as number
|
|
405
|
-
const numValue = Number(value);
|
|
406
|
-
if (Number.isNaN(numValue) === false && value !== '') {
|
|
407
|
-
row[header] = numValue;
|
|
408
|
-
} else if (value === 'true') {
|
|
409
|
-
row[header] = true;
|
|
410
|
-
} else if (value === 'false') {
|
|
411
|
-
row[header] = false;
|
|
412
|
-
} else {
|
|
413
|
-
row[header] = value;
|
|
414
|
-
}
|
|
415
|
-
});
|
|
416
|
-
|
|
417
|
-
data.push(row);
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
return data;
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
/**
|
|
424
|
-
* Parse proxy URL string into ProxyOptions
|
|
425
|
-
* Format: http://host:port or https://host:port
|
|
426
|
-
*/
|
|
427
|
-
function parseProxyUrl(proxyUrl: string): { host: string; port: number } | null {
|
|
428
|
-
try {
|
|
429
|
-
const url = new URL(proxyUrl);
|
|
430
|
-
return {
|
|
431
|
-
host: url.hostname,
|
|
432
|
-
port: parseInt(url.port) > 0 ? parseInt(url.port) : (url.protocol === 'https:' ? 443 : 80)
|
|
433
|
-
};
|
|
434
|
-
} catch {
|
|
435
|
-
console.error(`Error: Invalid proxy URL format: ${proxyUrl}`);
|
|
436
|
-
console.error('Expected format: http://host:port or https://host:port');
|
|
437
|
-
process.exit(2);
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
/**
|
|
442
|
-
* Parse proxy auth string into username/password
|
|
443
|
-
* Format: username:password
|
|
444
|
-
*/
|
|
445
|
-
function parseProxyAuth(authString: string): { username: string; password: string } | null {
|
|
446
|
-
const parts = authString.split(':');
|
|
447
|
-
if (parts.length !== 2) {
|
|
448
|
-
console.error(`Error: Invalid proxy auth format: ${authString}`);
|
|
449
|
-
console.error('Expected format: username:password');
|
|
450
|
-
process.exit(2);
|
|
451
|
-
}
|
|
452
|
-
return {
|
|
453
|
-
username: parts[0],
|
|
454
|
-
password: parts[1]
|
|
455
|
-
};
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
/**
|
|
459
|
-
* Parse comma-separated host list for proxy bypass
|
|
460
|
-
*/
|
|
461
|
-
function parseNoProxy(noProxyString: string): string[] {
|
|
462
|
-
return noProxyString.split(',').map(h => h.trim()).filter(h => h.length > 0);
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
/**
|
|
466
|
-
* Parse cookie string into Cookie object
|
|
467
|
-
* Format: name=value
|
|
468
|
-
*/
|
|
469
|
-
function parseCookie(cookieString: string): { name: string; value: string } | null {
|
|
470
|
-
const index = cookieString.indexOf('=');
|
|
471
|
-
if (index === -1) {
|
|
472
|
-
console.error(`Error: Invalid cookie format: ${cookieString}`);
|
|
473
|
-
console.error('Expected format: name=value');
|
|
474
|
-
process.exit(2);
|
|
475
|
-
}
|
|
476
|
-
return {
|
|
477
|
-
name: cookieString.substring(0, index).trim(),
|
|
478
|
-
value: cookieString.substring(index + 1).trim()
|
|
479
|
-
};
|
|
480
|
-
}
|