@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/QuestAPI.ts
DELETED
|
@@ -1,764 +0,0 @@
|
|
|
1
|
-
import type { ExecutionContext, TestResult, Cookie, CookieSetOptions } from '@apiquest/types';
|
|
2
|
-
import { ScriptType } from '@apiquest/types';
|
|
3
|
-
import { createQuestTestAPI } from './QuestTestAPI.js';
|
|
4
|
-
import type { CookieJar } from './CookieJar.js';
|
|
5
|
-
import type { RequestConfig, ResponseObject, HistoryFilterCriteria } from './QuestAPI.types.js';
|
|
6
|
-
import { isNullOrWhitespace } from './utils.js';
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Helper: Execute HTTP request and return response object
|
|
10
|
-
* Used by quest.sendRequest() to make requests from scripts
|
|
11
|
-
*/
|
|
12
|
-
async function executeHttpRequest(config: RequestConfig, signal: AbortSignal): Promise<ResponseObject> {
|
|
13
|
-
// Use native fetch
|
|
14
|
-
const url = config.url;
|
|
15
|
-
const method = config.method ?? 'GET';
|
|
16
|
-
const headers = config.header ?? config.headers ?? {};
|
|
17
|
-
|
|
18
|
-
if (isNullOrWhitespace(url)) {
|
|
19
|
-
throw new Error('sendRequest requires a "url" property');
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
const fetchOptions: RequestInit = {
|
|
23
|
-
method,
|
|
24
|
-
headers,
|
|
25
|
-
signal
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
// Handle body
|
|
29
|
-
if (config.body !== null && config.body !== undefined) {
|
|
30
|
-
if (typeof config.body === 'object' && 'mode' in config.body && config.body.mode !== null && config.body.mode !== undefined) {
|
|
31
|
-
// Handle different body modes
|
|
32
|
-
if (config.body.mode === 'raw') {
|
|
33
|
-
fetchOptions.body = config.body.raw;
|
|
34
|
-
} else if (config.body.mode === 'urlencoded' && config.body.urlencoded !== null && config.body.urlencoded !== undefined) {
|
|
35
|
-
// Convert to URLSearchParams
|
|
36
|
-
const params = new URLSearchParams();
|
|
37
|
-
for (const item of config.body.urlencoded) {
|
|
38
|
-
params.append(item.key, item.value);
|
|
39
|
-
}
|
|
40
|
-
fetchOptions.body = params.toString();
|
|
41
|
-
(fetchOptions.headers as Record<string, string>)['Content-Type'] = 'application/x-www-form-urlencoded';
|
|
42
|
-
} else if (config.body.mode === 'formdata' && config.body.formdata !== null && config.body.formdata !== undefined) {
|
|
43
|
-
// FormData
|
|
44
|
-
const formData = new FormData();
|
|
45
|
-
for (const item of config.body.formdata) {
|
|
46
|
-
formData.append(item.key, item.value);
|
|
47
|
-
}
|
|
48
|
-
fetchOptions.body = formData;
|
|
49
|
-
}
|
|
50
|
-
} else if (typeof config.body === 'string') {
|
|
51
|
-
fetchOptions.body = config.body;
|
|
52
|
-
} else {
|
|
53
|
-
// Assume JSON
|
|
54
|
-
fetchOptions.body = JSON.stringify(config.body);
|
|
55
|
-
const headersRecord = fetchOptions.headers as Record<string, string>;
|
|
56
|
-
headersRecord['Content-Type'] ??= 'application/json';
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
try {
|
|
61
|
-
const startTime = Date.now();
|
|
62
|
-
const response = await fetch(url, fetchOptions);
|
|
63
|
-
const duration = Date.now() - startTime;
|
|
64
|
-
const body = await response.text();
|
|
65
|
-
|
|
66
|
-
// Convert headers, preserving multiple values (e.g., set-cookie)
|
|
67
|
-
const headers: Record<string, string | string[]> = {};
|
|
68
|
-
const headerCounts: Record<string, number> = {};
|
|
69
|
-
|
|
70
|
-
response.headers.forEach((value, key) => {
|
|
71
|
-
const lowerKey = key.toLowerCase();
|
|
72
|
-
if (headerCounts[lowerKey] === 0 || headerCounts[lowerKey] === undefined) {
|
|
73
|
-
headerCounts[lowerKey] = 0;
|
|
74
|
-
headers[lowerKey] = value;
|
|
75
|
-
} else {
|
|
76
|
-
// Multiple values for this header - convert to array
|
|
77
|
-
const existing = headers[lowerKey];
|
|
78
|
-
if (!Array.isArray(existing)) {
|
|
79
|
-
headers[lowerKey] = [existing];
|
|
80
|
-
}
|
|
81
|
-
(headers[lowerKey] as string[]).push(value);
|
|
82
|
-
}
|
|
83
|
-
headerCounts[lowerKey]++;
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
// Return response object compatible with quest API
|
|
87
|
-
const responseObj: ResponseObject = {
|
|
88
|
-
status: response.status,
|
|
89
|
-
statusText: response.statusText,
|
|
90
|
-
body: body,
|
|
91
|
-
headers: headers,
|
|
92
|
-
time: duration,
|
|
93
|
-
// Helper methods
|
|
94
|
-
json() {
|
|
95
|
-
try {
|
|
96
|
-
return JSON.parse(body) as unknown;
|
|
97
|
-
} catch {
|
|
98
|
-
return null;
|
|
99
|
-
}
|
|
100
|
-
},
|
|
101
|
-
text() {
|
|
102
|
-
return body;
|
|
103
|
-
}
|
|
104
|
-
};
|
|
105
|
-
|
|
106
|
-
return responseObj;
|
|
107
|
-
} catch (error: unknown) {
|
|
108
|
-
const errorName = (error as { name?: string }).name;
|
|
109
|
-
const errorMsg = (error as { message?: string }).message ?? 'Unknown error';
|
|
110
|
-
|
|
111
|
-
// Handle abort error
|
|
112
|
-
if (errorName === 'AbortError') {
|
|
113
|
-
throw new Error('Request aborted');
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
throw new Error(`Request failed: ${errorMsg}`);
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
/**
|
|
121
|
-
* Creates the complete quest API object
|
|
122
|
-
* Returns all quest.* methods and properties for script execution
|
|
123
|
-
*/
|
|
124
|
-
export function createQuestAPI(
|
|
125
|
-
context: ExecutionContext,
|
|
126
|
-
scriptType: ScriptType,
|
|
127
|
-
tests: TestResult[], // Array to collect test results
|
|
128
|
-
emitAssertion: (test: TestResult) => void
|
|
129
|
-
): Record<string, unknown> {
|
|
130
|
-
// Create test API (test, skip, fail) with abort signal
|
|
131
|
-
const testAPI = createQuestTestAPI(tests, scriptType, emitAssertion, context.abortSignal);
|
|
132
|
-
|
|
133
|
-
return {
|
|
134
|
-
// Test API
|
|
135
|
-
test: testAPI.test,
|
|
136
|
-
skip: testAPI.skip,
|
|
137
|
-
fail: testAPI.fail,
|
|
138
|
-
|
|
139
|
-
// Send HTTP request - supports BOTH async/await and callback patterns
|
|
140
|
-
sendRequest(config: RequestConfig, callback?: (err: Error | null, res: ResponseObject | null) => void) {
|
|
141
|
-
const requestPromise = executeHttpRequest(config, context.abortSignal);
|
|
142
|
-
|
|
143
|
-
// If callback provided, use callback pattern
|
|
144
|
-
if (callback !== null && callback !== undefined && typeof callback === 'function') {
|
|
145
|
-
requestPromise
|
|
146
|
-
.then((res) => {
|
|
147
|
-
callback(null, res);
|
|
148
|
-
})
|
|
149
|
-
.catch((err) => {
|
|
150
|
-
callback(err as Error, null);
|
|
151
|
-
});
|
|
152
|
-
return undefined; // Don't return promise in callback mode
|
|
153
|
-
} else {
|
|
154
|
-
// No callback, return Promise for async/await
|
|
155
|
-
return requestPromise;
|
|
156
|
-
}
|
|
157
|
-
},
|
|
158
|
-
|
|
159
|
-
// Wait/delay execution
|
|
160
|
-
wait(ms: number) {
|
|
161
|
-
if (typeof ms !== 'number' || isNaN(ms)) {
|
|
162
|
-
throw new Error('quest.wait() requires a valid number of milliseconds');
|
|
163
|
-
}
|
|
164
|
-
if (ms < 0) {
|
|
165
|
-
throw new Error('quest.wait() milliseconds must be non-negative');
|
|
166
|
-
}
|
|
167
|
-
return new Promise(resolve => setTimeout(resolve, ms));
|
|
168
|
-
},
|
|
169
|
-
|
|
170
|
-
// Variables API
|
|
171
|
-
variables: (() => {
|
|
172
|
-
const variablesAPI = {
|
|
173
|
-
get(key: string) {
|
|
174
|
-
// Priority: iteration > scope stack > collection > env => global
|
|
175
|
-
const currentIterationData = context.iterationData?.[context.iterationCurrent - 1];
|
|
176
|
-
if (currentIterationData !== null && currentIterationData !== undefined && key in currentIterationData) {
|
|
177
|
-
return String(currentIterationData[key]);
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
// Search scope stack (top to bottom)
|
|
181
|
-
for (let i = context.scopeStack.length - 1; i >= 0; i--) {
|
|
182
|
-
if (key in context.scopeStack[i].vars) {
|
|
183
|
-
return context.scopeStack[i].vars[key];
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
if (key in context.collectionVariables) {
|
|
188
|
-
return context.collectionVariables[key];
|
|
189
|
-
}
|
|
190
|
-
if (context.environment !== null && context.environment !== undefined && key in context.environment.variables) {
|
|
191
|
-
return context.environment.variables[key];
|
|
192
|
-
}
|
|
193
|
-
if (key in context.globalVariables) {
|
|
194
|
-
return context.globalVariables[key];
|
|
195
|
-
}
|
|
196
|
-
return null;
|
|
197
|
-
},
|
|
198
|
-
|
|
199
|
-
set(key: string, value: string) {
|
|
200
|
-
// Search scope stack for existing key, or set in top scope
|
|
201
|
-
for (let i = context.scopeStack.length - 1; i >= 0; i--) {
|
|
202
|
-
if (key in context.scopeStack[i].vars) {
|
|
203
|
-
context.scopeStack[i].vars[key] = value;
|
|
204
|
-
return;
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
// Not found: set in current (top) scope
|
|
209
|
-
if (context.scopeStack.length > 0) {
|
|
210
|
-
context.scopeStack[context.scopeStack.length - 1].vars[key] = value;
|
|
211
|
-
}
|
|
212
|
-
},
|
|
213
|
-
|
|
214
|
-
replaceIn(template: string): string {
|
|
215
|
-
if (isNullOrWhitespace(template)) return template;
|
|
216
|
-
|
|
217
|
-
// Replace all {{variable}} patterns
|
|
218
|
-
return template.replace(/\{\{([^}]+)\}\}/g, (match, varName: string) => {
|
|
219
|
-
const value = variablesAPI.get(varName.trim());
|
|
220
|
-
// If variable not found, leave the placeholder
|
|
221
|
-
return value !== null ? String(value) : match;
|
|
222
|
-
});
|
|
223
|
-
},
|
|
224
|
-
|
|
225
|
-
has(key: string) {
|
|
226
|
-
return variablesAPI.get(key) !== null;
|
|
227
|
-
}
|
|
228
|
-
};
|
|
229
|
-
return variablesAPI;
|
|
230
|
-
})(),
|
|
231
|
-
|
|
232
|
-
// Global variables
|
|
233
|
-
global: {
|
|
234
|
-
variables: {
|
|
235
|
-
get(key: string) {
|
|
236
|
-
return context.globalVariables[key] ?? null;
|
|
237
|
-
},
|
|
238
|
-
set(key: string, value: string) {
|
|
239
|
-
context.globalVariables[key] = value;
|
|
240
|
-
},
|
|
241
|
-
has(key: string) {
|
|
242
|
-
return key in context.globalVariables;
|
|
243
|
-
},
|
|
244
|
-
remove(key: string) {
|
|
245
|
-
if (key in context.globalVariables) {
|
|
246
|
-
delete context.globalVariables[key];
|
|
247
|
-
return true;
|
|
248
|
-
}
|
|
249
|
-
return false;
|
|
250
|
-
},
|
|
251
|
-
clear() {
|
|
252
|
-
context.globalVariables = {};
|
|
253
|
-
},
|
|
254
|
-
toObject() {
|
|
255
|
-
return { ...context.globalVariables };
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
},
|
|
259
|
-
|
|
260
|
-
// Collection API
|
|
261
|
-
collection: {
|
|
262
|
-
// Collection info
|
|
263
|
-
info:
|
|
264
|
-
{
|
|
265
|
-
...context.collectionInfo,
|
|
266
|
-
version:
|
|
267
|
-
context.collectionInfo.version !== null &&
|
|
268
|
-
context.collectionInfo.version !== undefined &&
|
|
269
|
-
context.collectionInfo.version !== ''
|
|
270
|
-
? context.collectionInfo.version
|
|
271
|
-
: null,
|
|
272
|
-
description:
|
|
273
|
-
context.collectionInfo.description !== null &&
|
|
274
|
-
context.collectionInfo.description !== undefined &&
|
|
275
|
-
context.collectionInfo.description !== ''
|
|
276
|
-
? context.collectionInfo.description
|
|
277
|
-
: null,
|
|
278
|
-
},
|
|
279
|
-
|
|
280
|
-
// Collection variables
|
|
281
|
-
variables: {
|
|
282
|
-
get(key: string) {
|
|
283
|
-
return context.collectionVariables[key] ?? null;
|
|
284
|
-
},
|
|
285
|
-
set(key: string, value: string) {
|
|
286
|
-
context.collectionVariables[key] = value;
|
|
287
|
-
},
|
|
288
|
-
has(key: string) {
|
|
289
|
-
return key in context.collectionVariables;
|
|
290
|
-
},
|
|
291
|
-
remove(key: string) {
|
|
292
|
-
if (key in context.collectionVariables) {
|
|
293
|
-
delete context.collectionVariables[key];
|
|
294
|
-
return true;
|
|
295
|
-
}
|
|
296
|
-
return false;
|
|
297
|
-
},
|
|
298
|
-
clear() {
|
|
299
|
-
context.collectionVariables = {};
|
|
300
|
-
},
|
|
301
|
-
toObject() {
|
|
302
|
-
return { ...context.collectionVariables };
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
},
|
|
306
|
-
|
|
307
|
-
// Environment variables
|
|
308
|
-
environment: {
|
|
309
|
-
name: context.environment?.name ?? null,
|
|
310
|
-
variables: {
|
|
311
|
-
get(key: string) {
|
|
312
|
-
return context.environment?.variables[key] ?? null;
|
|
313
|
-
},
|
|
314
|
-
set(key: string, value: string) {
|
|
315
|
-
context.environment ??= { name: 'Runtime Environment', variables: {} };
|
|
316
|
-
context.environment.variables[key] = value;
|
|
317
|
-
},
|
|
318
|
-
has(key: string) {
|
|
319
|
-
return context.environment !== null && context.environment !== undefined ? key in context.environment.variables : false;
|
|
320
|
-
},
|
|
321
|
-
remove(key: string) {
|
|
322
|
-
if (context.environment !== null && context.environment !== undefined && key in context.environment.variables) {
|
|
323
|
-
delete context.environment.variables[key];
|
|
324
|
-
return true;
|
|
325
|
-
}
|
|
326
|
-
return false;
|
|
327
|
-
},
|
|
328
|
-
clear() {
|
|
329
|
-
if (context.environment !== null && context.environment !== undefined) {
|
|
330
|
-
context.environment.variables = {};
|
|
331
|
-
}
|
|
332
|
-
},
|
|
333
|
-
toObject() {
|
|
334
|
-
return context.environment !== null && context.environment !== undefined ? { ...context.environment.variables } : {};
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
},
|
|
338
|
-
|
|
339
|
-
// Scope variables (hierarchical)
|
|
340
|
-
scope: {
|
|
341
|
-
variables: {
|
|
342
|
-
get(key: string) {
|
|
343
|
-
// Search scope stack top to bottom
|
|
344
|
-
for (let i = context.scopeStack.length - 1; i >= 0; i--) {
|
|
345
|
-
if (key in context.scopeStack[i].vars) {
|
|
346
|
-
return context.scopeStack[i].vars[key];
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
return null;
|
|
350
|
-
},
|
|
351
|
-
set(key: string, value: string) {
|
|
352
|
-
// Search stack for existing key, or set in top scope
|
|
353
|
-
for (let i = context.scopeStack.length - 1; i >= 0; i--) {
|
|
354
|
-
if (key in context.scopeStack[i].vars) {
|
|
355
|
-
context.scopeStack[i].vars[key] = value;
|
|
356
|
-
return;
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
// Not found: set in current (top) scope
|
|
361
|
-
if (context.scopeStack.length > 0) {
|
|
362
|
-
context.scopeStack[context.scopeStack.length - 1].vars[key] = value;
|
|
363
|
-
}
|
|
364
|
-
},
|
|
365
|
-
has(key: string) {
|
|
366
|
-
return this.get(key) !== null;
|
|
367
|
-
},
|
|
368
|
-
remove(key: string) {
|
|
369
|
-
// Remove from the scope where it exists
|
|
370
|
-
for (let i = context.scopeStack.length - 1; i >= 0; i--) {
|
|
371
|
-
if (key in context.scopeStack[i].vars) {
|
|
372
|
-
delete context.scopeStack[i].vars[key];
|
|
373
|
-
return true;
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
return false;
|
|
377
|
-
},
|
|
378
|
-
clear() {
|
|
379
|
-
// Clear current (top) scope only
|
|
380
|
-
if (context.scopeStack.length > 0) {
|
|
381
|
-
context.scopeStack[context.scopeStack.length - 1].vars = {};
|
|
382
|
-
}
|
|
383
|
-
},
|
|
384
|
-
toObject() {
|
|
385
|
-
// Merge all scopes (bottom to top, so top overrides)
|
|
386
|
-
const result: Record<string, string> = {};
|
|
387
|
-
for (let i = 0; i < context.scopeStack.length; i++) {
|
|
388
|
-
Object.assign(result, context.scopeStack[i].vars);
|
|
389
|
-
}
|
|
390
|
-
return result;
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
},
|
|
394
|
-
|
|
395
|
-
// Response API
|
|
396
|
-
response: context.currentResponse !== null && context.currentResponse !== undefined ? {
|
|
397
|
-
status: context.currentResponse.status,
|
|
398
|
-
statusText: context.currentResponse.statusText,
|
|
399
|
-
headers: {
|
|
400
|
-
// Method API
|
|
401
|
-
get(name: string) {
|
|
402
|
-
if (context.currentResponse?.headers === null || context.currentResponse?.headers === undefined) return null;
|
|
403
|
-
// Case-insensitive lookup
|
|
404
|
-
const lowerName = name.toLowerCase();
|
|
405
|
-
for (const [key, value] of Object.entries(context.currentResponse.headers)) {
|
|
406
|
-
if (key.toLowerCase() === lowerName) {
|
|
407
|
-
return value;
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
return null;
|
|
411
|
-
},
|
|
412
|
-
has(name: string) {
|
|
413
|
-
if (context.currentResponse?.headers === null || context.currentResponse?.headers === undefined) return false;
|
|
414
|
-
// Case-insensitive lookup
|
|
415
|
-
const lowerName = name.toLowerCase();
|
|
416
|
-
for (const key of Object.keys(context.currentResponse.headers)) {
|
|
417
|
-
if (key.toLowerCase() === lowerName) {
|
|
418
|
-
return true;
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
return false;
|
|
422
|
-
},
|
|
423
|
-
toObject() {
|
|
424
|
-
return context.currentResponse?.headers ?? {};
|
|
425
|
-
}
|
|
426
|
-
},
|
|
427
|
-
body: context.currentResponse.body,
|
|
428
|
-
text() {
|
|
429
|
-
return context.currentResponse?.body ?? '';
|
|
430
|
-
},
|
|
431
|
-
json() {
|
|
432
|
-
try {
|
|
433
|
-
return JSON.parse(context.currentResponse?.body ?? '{}') as unknown;
|
|
434
|
-
} catch {
|
|
435
|
-
return {};
|
|
436
|
-
}
|
|
437
|
-
},
|
|
438
|
-
time: context.currentResponse.duration,
|
|
439
|
-
size: context.currentResponse.body?.length ?? 0,
|
|
440
|
-
// Assertion helpers
|
|
441
|
-
to: {
|
|
442
|
-
be: {
|
|
443
|
-
ok: context.currentResponse.status === 200,
|
|
444
|
-
success: context.currentResponse.status >= 200 && context.currentResponse.status < 300,
|
|
445
|
-
clientError: context.currentResponse.status >= 400 && context.currentResponse.status < 500,
|
|
446
|
-
serverError: context.currentResponse.status >= 500 && context.currentResponse.status < 600
|
|
447
|
-
},
|
|
448
|
-
have: {
|
|
449
|
-
status(code: number) {
|
|
450
|
-
return context.currentResponse?.status === code;
|
|
451
|
-
},
|
|
452
|
-
header(name: string) {
|
|
453
|
-
if (context.currentResponse?.headers === null || context.currentResponse?.headers === undefined) return false;
|
|
454
|
-
const lowerName = name.toLowerCase();
|
|
455
|
-
for (const key of Object.keys(context.currentResponse.headers)) {
|
|
456
|
-
if (key.toLowerCase() === lowerName) {
|
|
457
|
-
return true;
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
|
-
return false;
|
|
461
|
-
},
|
|
462
|
-
jsonBody(field: string) {
|
|
463
|
-
try {
|
|
464
|
-
const data = JSON.parse(context.currentResponse?.body ?? '{}') as Record<string, unknown>;
|
|
465
|
-
return field in data;
|
|
466
|
-
} catch {
|
|
467
|
-
return false;
|
|
468
|
-
}
|
|
469
|
-
}
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
} : null,
|
|
473
|
-
|
|
474
|
-
// Request info and modification API
|
|
475
|
-
request: {
|
|
476
|
-
info: {
|
|
477
|
-
name: context.currentRequest?.name ?? '',
|
|
478
|
-
id: context.currentRequest?.id ?? '',
|
|
479
|
-
protocol: context.protocol,
|
|
480
|
-
description: context.currentRequest?.description ?? ''
|
|
481
|
-
},
|
|
482
|
-
dependsOn: context.currentRequest?.dependsOn ?? null,
|
|
483
|
-
condition: context.currentRequest?.condition ?? null,
|
|
484
|
-
url: (context.currentRequest?.data.url ?? '') as string,
|
|
485
|
-
method: (context.currentRequest?.data.method ?? '') as string,
|
|
486
|
-
body: {
|
|
487
|
-
get() {
|
|
488
|
-
if (context.currentRequest?.data.body === null || context.currentRequest?.data.body === undefined) return null;
|
|
489
|
-
const body = context.currentRequest.data.body as string | Record<string, unknown>;
|
|
490
|
-
|
|
491
|
-
// Handle different body modes
|
|
492
|
-
if (typeof body === 'string') return body;
|
|
493
|
-
if (typeof body === 'object' && 'mode' in body && (body as { mode?: string }).mode === 'raw') return (body as { raw?: string }).raw ?? null;
|
|
494
|
-
if (typeof body === 'object' && 'mode' in body && (body as { mode?: string }).mode === 'urlencoded') return null; // Return null for non-raw modes
|
|
495
|
-
if (typeof body === 'object' && 'mode' in body && (body as { mode?: string }).mode === 'formdata') return null;
|
|
496
|
-
|
|
497
|
-
return null;
|
|
498
|
-
},
|
|
499
|
-
set(content: string) {
|
|
500
|
-
if (context.currentRequest === null || context.currentRequest === undefined) return;
|
|
501
|
-
if (context.currentRequest.data.body === null || context.currentRequest.data.body === undefined) {
|
|
502
|
-
context.currentRequest.data.body = { mode: 'raw', raw: content };
|
|
503
|
-
} else if (typeof context.currentRequest.data.body === 'string') {
|
|
504
|
-
context.currentRequest.data.body = content;
|
|
505
|
-
} else if (typeof context.currentRequest.data.body === 'object') {
|
|
506
|
-
(context.currentRequest.data.body as { raw?: string }).raw = content;
|
|
507
|
-
}
|
|
508
|
-
},
|
|
509
|
-
get mode() {
|
|
510
|
-
if (context.currentRequest?.data.body === null || context.currentRequest?.data.body === undefined) return null;
|
|
511
|
-
const body = context.currentRequest.data.body as string | Record<string, unknown>;
|
|
512
|
-
|
|
513
|
-
if (typeof body === 'string') return 'raw';
|
|
514
|
-
return (typeof body === 'object' && 'mode' in body ? (body as { mode?: string }).mode : 'raw') as string;
|
|
515
|
-
}
|
|
516
|
-
},
|
|
517
|
-
headers: {
|
|
518
|
-
add(header: { key: string; value: string; }) {
|
|
519
|
-
if (context.currentRequest === null || context.currentRequest === undefined) return;
|
|
520
|
-
const headers = context.currentRequest.data.headers as Record<string, string> | undefined;
|
|
521
|
-
if (headers === null || headers === undefined) {
|
|
522
|
-
context.currentRequest.data.headers = {};
|
|
523
|
-
}
|
|
524
|
-
(context.currentRequest.data.headers as Record<string, string>)[header.key] = header.value;
|
|
525
|
-
},
|
|
526
|
-
remove(key: string) {
|
|
527
|
-
if (context.currentRequest?.data.headers === null || context.currentRequest?.data.headers === undefined) return;
|
|
528
|
-
delete (context.currentRequest.data.headers as Record<string, string>)[key];
|
|
529
|
-
},
|
|
530
|
-
get(key: string) {
|
|
531
|
-
if (context.currentRequest?.data.headers === null || context.currentRequest?.data.headers === undefined) return null;
|
|
532
|
-
// Case-insensitive lookup
|
|
533
|
-
const lowerKey = key.toLowerCase();
|
|
534
|
-
for (const [headerKey, value] of Object.entries(context.currentRequest.data.headers as Record<string, string>)) {
|
|
535
|
-
if (headerKey.toLowerCase() === lowerKey) {
|
|
536
|
-
return value;
|
|
537
|
-
}
|
|
538
|
-
}
|
|
539
|
-
return null;
|
|
540
|
-
},
|
|
541
|
-
upsert(header: { key: string; value: string; }) {
|
|
542
|
-
if (context.currentRequest === null || context.currentRequest === undefined) return;
|
|
543
|
-
const headers = context.currentRequest.data.headers as Record<string, string> | undefined;
|
|
544
|
-
if (headers === null || headers === undefined) {
|
|
545
|
-
context.currentRequest.data.headers = {};
|
|
546
|
-
}
|
|
547
|
-
(context.currentRequest.data.headers as Record<string, string>)[header.key] = header.value;
|
|
548
|
-
},
|
|
549
|
-
toObject() {
|
|
550
|
-
return (context.currentRequest?.data.headers ?? {}) as Record<string, string>;
|
|
551
|
-
}
|
|
552
|
-
},
|
|
553
|
-
timeout: {
|
|
554
|
-
set(ms: number) {
|
|
555
|
-
// Only allowed in preRequestScript
|
|
556
|
-
if (scriptType !== ScriptType.PreRequest) {
|
|
557
|
-
throw new Error('quest.request.timeout.set() can only be called in preRequestScript');
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
if (context.currentRequest === null || context.currentRequest === undefined) {
|
|
561
|
-
throw new Error('quest.request.timeout.set() requires an active request');
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
// Validate timeout is a positive number
|
|
565
|
-
if (typeof ms !== 'number' || ms <= 0 || !Number.isFinite(ms)) {
|
|
566
|
-
throw new Error('quest.request.timeout.set() requires a positive finite number in milliseconds');
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
// Initialize options object using nullish coalescing operator
|
|
570
|
-
context.currentRequest.options ??= {};
|
|
571
|
-
|
|
572
|
-
// Initialize timeout object using nullish coalescing operator
|
|
573
|
-
context.currentRequest.options.timeout ??= {};
|
|
574
|
-
|
|
575
|
-
// Set the per-request timeout override
|
|
576
|
-
context.currentRequest.options.timeout.request = ms;
|
|
577
|
-
},
|
|
578
|
-
get() {
|
|
579
|
-
// Can be called from any script
|
|
580
|
-
if (context.currentRequest === null || context.currentRequest === undefined) {
|
|
581
|
-
return null;
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
// Check for per-request timeout override first
|
|
585
|
-
const requestTimeout = context.currentRequest.options?.timeout?.request;
|
|
586
|
-
if (requestTimeout !== null && requestTimeout !== undefined) {
|
|
587
|
-
return requestTimeout;
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
// Fall back to context/CLI timeout
|
|
591
|
-
const contextTimeout = context.options?.timeout?.request;
|
|
592
|
-
return contextTimeout ?? null;
|
|
593
|
-
}
|
|
594
|
-
}
|
|
595
|
-
},
|
|
596
|
-
|
|
597
|
-
// Iteration API
|
|
598
|
-
iteration: {
|
|
599
|
-
current: context.iterationCurrent,
|
|
600
|
-
count: context.iterationCount,
|
|
601
|
-
data: {
|
|
602
|
-
get(key: string) {
|
|
603
|
-
const currentData = context.iterationData?.[context.iterationCurrent - 1];
|
|
604
|
-
return currentData?.[key] ?? null;
|
|
605
|
-
},
|
|
606
|
-
has(key: string) {
|
|
607
|
-
const currentData = context.iterationData?.[context.iterationCurrent - 1];
|
|
608
|
-
return currentData !== null && currentData !== undefined ? key in currentData : false;
|
|
609
|
-
},
|
|
610
|
-
toObject() {
|
|
611
|
-
return context.iterationData?.[context.iterationCurrent - 1] ?? {};
|
|
612
|
-
},
|
|
613
|
-
keys() {
|
|
614
|
-
const currentData = context.iterationData?.[context.iterationCurrent - 1];
|
|
615
|
-
return currentData !== null && currentData !== undefined ? Object.keys(currentData) : [];
|
|
616
|
-
},
|
|
617
|
-
all() {
|
|
618
|
-
return context.iterationData ?? [];
|
|
619
|
-
}
|
|
620
|
-
}
|
|
621
|
-
},
|
|
622
|
-
|
|
623
|
-
// Execution history API
|
|
624
|
-
history: {
|
|
625
|
-
requests: {
|
|
626
|
-
count() {
|
|
627
|
-
return context.executionHistory.length;
|
|
628
|
-
},
|
|
629
|
-
get(idOrName: string) {
|
|
630
|
-
return context.executionHistory.find(
|
|
631
|
-
entry => entry.id === idOrName || entry.name === idOrName
|
|
632
|
-
) ?? null;
|
|
633
|
-
},
|
|
634
|
-
all() {
|
|
635
|
-
return context.executionHistory;
|
|
636
|
-
},
|
|
637
|
-
last() {
|
|
638
|
-
return context.executionHistory.length > 0
|
|
639
|
-
? context.executionHistory[context.executionHistory.length - 1]
|
|
640
|
-
: null;
|
|
641
|
-
},
|
|
642
|
-
filter(criteria: HistoryFilterCriteria) {
|
|
643
|
-
return context.executionHistory.filter(entry => {
|
|
644
|
-
// Filter by path (with wildcard support)
|
|
645
|
-
if (criteria.path !== null && criteria.path !== undefined) {
|
|
646
|
-
const pathPattern = criteria.path.replace(/\*/g, '.*');
|
|
647
|
-
const pathRegex = new RegExp(`^${pathPattern}$`);
|
|
648
|
-
if (!pathRegex.test(entry.path)) {
|
|
649
|
-
return false;
|
|
650
|
-
}
|
|
651
|
-
}
|
|
652
|
-
|
|
653
|
-
// Filter by name
|
|
654
|
-
if (criteria.name !== null && criteria.name !== undefined && entry.name !== criteria.name) {
|
|
655
|
-
return false;
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
// Filter by iteration
|
|
659
|
-
if (criteria.iteration !== null && criteria.iteration !== undefined && entry.iteration !== criteria.iteration) {
|
|
660
|
-
return false;
|
|
661
|
-
}
|
|
662
|
-
|
|
663
|
-
// Filter by id
|
|
664
|
-
if (criteria.id !== null && criteria.id !== undefined && entry.id !== criteria.id) {
|
|
665
|
-
return false;
|
|
666
|
-
}
|
|
667
|
-
|
|
668
|
-
return true;
|
|
669
|
-
});
|
|
670
|
-
}
|
|
671
|
-
}
|
|
672
|
-
},
|
|
673
|
-
|
|
674
|
-
// Cookies API - Uses cookie jar for persistence across requests
|
|
675
|
-
cookies: {
|
|
676
|
-
get(name: string) {
|
|
677
|
-
// Use cookie jar if available
|
|
678
|
-
if (context.cookieJar !== null && context.cookieJar !== undefined) {
|
|
679
|
-
return context.cookieJar.get(name);
|
|
680
|
-
}
|
|
681
|
-
return null;
|
|
682
|
-
},
|
|
683
|
-
set(name: string, value: string, options: CookieSetOptions) {
|
|
684
|
-
if (context.cookieJar !== null && context.cookieJar !== undefined) {
|
|
685
|
-
context.cookieJar.set(name, value, options);
|
|
686
|
-
}
|
|
687
|
-
},
|
|
688
|
-
has(name: string) {
|
|
689
|
-
// Use cookie jar if available
|
|
690
|
-
if (context.cookieJar !== null && context.cookieJar !== undefined) {
|
|
691
|
-
return context.cookieJar.has(name);
|
|
692
|
-
}
|
|
693
|
-
return false;
|
|
694
|
-
},
|
|
695
|
-
remove(name: string) {
|
|
696
|
-
// Use cookie jar if available
|
|
697
|
-
if (context.cookieJar !== null && context.cookieJar !== undefined) {
|
|
698
|
-
context.cookieJar.remove(name);
|
|
699
|
-
}
|
|
700
|
-
},
|
|
701
|
-
clear() {
|
|
702
|
-
// Use cookie jar if available
|
|
703
|
-
if (context.cookieJar !== null && context.cookieJar !== undefined) {
|
|
704
|
-
context.cookieJar.clear();
|
|
705
|
-
}
|
|
706
|
-
},
|
|
707
|
-
toObject() {
|
|
708
|
-
// Use cookie jar if available
|
|
709
|
-
if (context.cookieJar !== null && context.cookieJar !== undefined) {
|
|
710
|
-
return context.cookieJar.toObject();
|
|
711
|
-
}
|
|
712
|
-
return {};
|
|
713
|
-
}
|
|
714
|
-
},
|
|
715
|
-
|
|
716
|
-
// Plugin event API (for PluginEvent script type)
|
|
717
|
-
event: context.currentEvent !== null && context.currentEvent !== undefined ? {
|
|
718
|
-
name: context.currentEvent.eventName,
|
|
719
|
-
timestamp: context.currentEvent.timestamp,
|
|
720
|
-
data: (() => {
|
|
721
|
-
const rawData = context.currentEvent.data as string | Record<string, unknown>;
|
|
722
|
-
// Add json() helper method
|
|
723
|
-
const dataWithHelper: Record<string, unknown> = typeof rawData === 'string' ? { value: rawData } : rawData;
|
|
724
|
-
dataWithHelper.json = function () {
|
|
725
|
-
try {
|
|
726
|
-
return (typeof rawData === 'string' ? JSON.parse(rawData) : rawData) as unknown;
|
|
727
|
-
} catch {
|
|
728
|
-
return null;
|
|
729
|
-
}
|
|
730
|
-
};
|
|
731
|
-
return dataWithHelper;
|
|
732
|
-
})(),
|
|
733
|
-
index: context.currentEvent.index
|
|
734
|
-
} : null,
|
|
735
|
-
|
|
736
|
-
// Hint expected message count (for streaming protocols)
|
|
737
|
-
expectMessages(count: number, timeout?: number) {
|
|
738
|
-
// Only allowed in preRequestScript
|
|
739
|
-
if (scriptType !== ScriptType.PreRequest) {
|
|
740
|
-
throw new Error('quest.expectMessages() can only be called in preRequestScript');
|
|
741
|
-
}
|
|
742
|
-
|
|
743
|
-
// Validate count is positive integer
|
|
744
|
-
if (!Number.isInteger(count) || count <= 0) {
|
|
745
|
-
throw new Error('quest.expectMessages() requires a positive integer count');
|
|
746
|
-
}
|
|
747
|
-
|
|
748
|
-
// Validate protocol has plugin events with canHaveTests
|
|
749
|
-
const protocolPlugin = context.protocolPlugin;
|
|
750
|
-
const hasTestableEvents = protocolPlugin.events?.some((e: { canHaveTests?: boolean }) => e.canHaveTests === true) === true;
|
|
751
|
-
|
|
752
|
-
if (hasTestableEvents === false) {
|
|
753
|
-
throw new Error(
|
|
754
|
-
`quest.expectMessages() is not supported for protocol '${context.protocol}' ` +
|
|
755
|
-
`(no plugin events with canHaveTests)`
|
|
756
|
-
);
|
|
757
|
-
}
|
|
758
|
-
|
|
759
|
-
// Store count in context for plugin optimization and test counting
|
|
760
|
-
context.expectedMessages = count;
|
|
761
|
-
|
|
762
|
-
}
|
|
763
|
-
};
|
|
764
|
-
}
|