@loadmill/executer 0.1.50 → 0.1.53

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.
Files changed (46) hide show
  1. package/dist/extraction-combiner.js +6 -6
  2. package/dist/extraction-combiner.js.map +1 -1
  3. package/dist/mill-info.d.ts +4 -0
  4. package/dist/mill-version.js +1 -1
  5. package/dist/post-script/console-log.d.ts +7 -0
  6. package/dist/post-script/console-log.js +31 -0
  7. package/dist/post-script/console-log.js.map +1 -0
  8. package/dist/post-script/post-script-executor.js +7 -4
  9. package/dist/post-script/post-script-executor.js.map +1 -1
  10. package/dist/request-sequence-result.d.ts +1 -0
  11. package/dist/sequence.d.ts +1 -1
  12. package/dist/sequence.js +18 -8
  13. package/dist/sequence.js.map +1 -1
  14. package/dist/single-runner.d.ts +1 -0
  15. package/dist/single-runner.js +1 -1
  16. package/dist/single-runner.js.map +1 -1
  17. package/package.json +3 -3
  18. package/src/asserter.ts +0 -137
  19. package/src/errors.ts +0 -10
  20. package/src/extraction-combiner.ts +0 -110
  21. package/src/failures.ts +0 -79
  22. package/src/message-creators.ts +0 -44
  23. package/src/mill-info.ts +0 -76
  24. package/src/mill-version.ts +0 -7
  25. package/src/post-script/ast-walker/index.ts +0 -160
  26. package/src/post-script/ast-walker/type-guard.ts +0 -73
  27. package/src/post-script/ast-walker/types.ts +0 -35
  28. package/src/post-script/parser/acorn-js-parser.ts +0 -8
  29. package/src/post-script/parser/js-parser.ts +0 -22
  30. package/src/post-script/parser/parser.ts +0 -5
  31. package/src/post-script/post-script-executor.ts +0 -89
  32. package/src/post-script/virtual-machine/virtual-machine.ts +0 -15
  33. package/src/post-script/virtual-machine/vm2-virtual-machine.ts +0 -45
  34. package/src/report-types.ts +0 -127
  35. package/src/request-sequence-result.ts +0 -63
  36. package/src/request-stats.ts +0 -20
  37. package/src/res-keeper.ts +0 -53
  38. package/src/sampler.ts +0 -133
  39. package/src/sequence.ts +0 -1107
  40. package/src/single-runner.ts +0 -66
  41. package/src/test-run-event-emitter.ts +0 -25
  42. package/src/utils.ts +0 -8
  43. package/src/work.ts +0 -17
  44. package/src/ws.ts +0 -286
  45. package/test/post-script-executor.spec.ts +0 -677
  46. package/tsconfig.json +0 -9
package/src/sequence.ts DELETED
@@ -1,1107 +0,0 @@
1
- import URI from 'urijs';
2
- const HTTPSAgent = require('agentkeepalive').HttpsAgent;
3
- const HTTPAgent = require('agentkeepalive');
4
- import randomstring from 'randomstring';
5
- import BluebirdPromise from 'bluebird';
6
-
7
- import isEmpty from 'lodash/isEmpty';
8
- import flatMap from 'lodash/flatMap';
9
- import clamp from 'lodash/clamp';
10
- import find from 'lodash/find';
11
- import map from 'lodash/map';
12
- import filter from 'lodash/filter';
13
-
14
- import { ResolvedRequest } from './request-sequence-result';
15
- import { Asserter } from './asserter';
16
- import { Failures, Histogram, OneFailure } from './failures';
17
- import { PerRequestStats, setReqStats } from './request-stats';
18
- import { millVersionString } from './mill-version';
19
- import { ExtractionCombiner } from './extraction-combiner';
20
- import { PostScriptRunner } from './post-script/post-script-executor';
21
-
22
- import log from '@loadmill/universal/dist/log';
23
- import * as envUtils from '@loadmill/universal/dist/env-utils';
24
- import * as mathUtils from '@loadmill/universal/dist/math-utils';
25
- import * as uriUtils from '@loadmill/universal/dist/uri-utils';
26
- import * as promiseUtils from '@loadmill/universal/dist/promise-utils';
27
- import * as manipulationUtils from '@loadmill/universal/dist/manipulation-utils';
28
- import {
29
- corsErrorStatusPrefix,
30
- corsPreFlightErrorStatusPrefix,
31
- superagentCorsError,
32
- unknownStatusCorsError,
33
- } from '@loadmill/universal/dist/errors';
34
-
35
- import {
36
- parameterFunctionOperations,
37
- Parameters,
38
- parameterUtils,
39
- SequenceExecutorParameters,
40
- valueUtils,
41
- BUILT_IN_VOLATILE_PARAMS,
42
- } from '@loadmill/core/dist/parameters';
43
- import {
44
- isParameterCandidate,
45
- validateIntegerDelay,
46
- confExtrema,
47
- confDefaults,
48
- validate,
49
- } from '@loadmill/core/dist/conf';
50
- const {
51
- DEFAULT_CACHE_CONTROL,
52
- DEFAULT_REQUEST_DELAY,
53
- } = confDefaults;
54
- const {
55
- MAX_RESPONSE_BYTES,
56
- MAX_RESPONSE_COLLECT,
57
- MIN_REQUEST_DELAY,
58
- MAX_REQUEST_LOOPS_ITERATIONS,
59
- } = confExtrema;
60
- import {
61
- Assertion,
62
- Extraction,
63
- ExtractionObj,
64
- Extractions,
65
- findRequestIndex,
66
- LoadmillHeaders,
67
- LoadmillRequest,
68
- resolveUrl,
69
- ALLOWED_RESPONSE_STATUSES,
70
- LoopConf,
71
- DEFAULT_REQUEST_TIMEOUT,
72
- CachePenetrationModes,
73
- RequestPostData,
74
- PostFormData
75
- } from '@loadmill/core/dist/request';
76
-
77
- import { testRunEventsEmitter } from './test-run-event-emitter';
78
- import { WSRequest, WSRequestArguments, WSSequenceHandler, WSMimeType } from './ws';
79
- import { RequestFailuresError } from './errors';
80
- import { getMaxRequestBodySize } from './utils';
81
- import { WSExtractionData } from '@loadmill/core/dist/parameters/extractors/ws-extractor';
82
-
83
- export const reqIdParamName = 'loadmill-request-id';
84
-
85
- declare const Promise: typeof BluebirdPromise; // This is a hack for the loadmill-runner. I hate TypeScript.
86
-
87
- declare const loadmillClearResponses: undefined | (() => Promise<void>);
88
-
89
- declare const loadmillGetResponse:
90
- | undefined
91
- | ((
92
- requestId: string
93
- ) => Promise<{
94
- text: string;
95
- status: number;
96
- preFlight: boolean;
97
- headers: { [name: string]: string };
98
- requestHeaders: { [name: string]: string };
99
- }>);
100
-
101
- const SOCKET_TIMEOUT = DEFAULT_REQUEST_TIMEOUT + 5000; // deafult + some extra
102
-
103
- const isPerformance = typeof performance !== 'undefined';
104
- log.debug('isPerformance =', isPerformance);
105
-
106
- export const sequence = {
107
- execute(
108
- httpAgent,
109
- requests: LoadmillRequest[],
110
- parameters: Parameters[],
111
- domainsWhiteList: string[],
112
- ) {
113
- const sequenceExecutor = new SequenceExecutor(
114
- httpAgent,
115
- parameters,
116
- domainsWhiteList,
117
- );
118
- return sequenceExecutor.startAndPass(requests);
119
- },
120
- };
121
-
122
- class SequenceExecutor {
123
- parameters: SequenceExecutorParameters;
124
- postScriptRunner: PostScriptRunner;
125
-
126
- avgResTime = 0;
127
- successfulHits = 0;
128
- lastStartedIndex = 0;
129
- failures: Failures = {};
130
- requestStats: PerRequestStats = {};
131
- resolvedRequests: ResolvedRequest[] = [];
132
- keepaliveHTTPSAgent;
133
- keepaliveHTTPAgent;
134
- wsHandler: WSSequenceHandler;
135
-
136
- constructor(
137
- private httpAgent,
138
- parameters: Parameters[],
139
- private domainsWhiteList: string[],
140
- ) {
141
- this.parameters = parameterUtils.resolveAllExpressions(parameters);
142
- this.keepaliveHTTPSAgent = new HTTPSAgent({
143
- timeout: SOCKET_TIMEOUT,
144
- freeSocketTimeout: SOCKET_TIMEOUT / 2
145
- });
146
- this.keepaliveHTTPAgent = new HTTPAgent({
147
- timeout: SOCKET_TIMEOUT,
148
- freeSocketTimeout: SOCKET_TIMEOUT / 2
149
- });
150
- this.postScriptRunner = new PostScriptRunner();
151
- this.wsHandler = new WSSequenceHandler();
152
- }
153
-
154
- startAndPass(requests: LoadmillRequest[]) {
155
- return this.start(requests).then(this.passStats, this.passStats);
156
- }
157
-
158
- passStats = () => ({
159
- failures: this.failures,
160
- avgResTime: this.avgResTime,
161
- requestStats: this.requestStats,
162
- successfulHits: this.successfulHits,
163
- resolvedRequests: this.resolvedRequests,
164
- lastStartedIndex: this.lastStartedIndex,
165
- });
166
-
167
- async start(requests: LoadmillRequest[]) {
168
- for (let reqIndex = 0; reqIndex < requests.length; ++reqIndex) {
169
- const request = requests[reqIndex];
170
-
171
- this.resolvedRequests[reqIndex] = {};
172
-
173
- try {
174
- const skipIndex = this.getSkipIndexIfShould(
175
- request,
176
- reqIndex,
177
- requests
178
- );
179
-
180
- if (skipIndex != null) {
181
- reqIndex = skipIndex - 1;
182
- continue;
183
- }
184
-
185
- if (this.shouldStop(request)) {
186
- return;
187
- }
188
-
189
- await this.waitBeforeRequest(request);
190
-
191
- this.lastStartedIndex = reqIndex;
192
- await this.executeRequest(request, reqIndex);
193
-
194
- } catch (error) {
195
- if (error.histogram) {
196
- this.setRequestFailure(reqIndex, error.histogram);
197
- }
198
-
199
- // Failures could be set before error was thrown,
200
- // i.e. !error.histogram && !_.isEmpty(this.failures[reqIndex])
201
- // CAN be true:
202
- if (isEmpty(this.failures[reqIndex])) {
203
- // Reaching here indicates a bug!
204
- const reason = 'Unexpected sampler error';
205
-
206
- this.setSingleFailure(reqIndex, reason);
207
-
208
- const { stack, message } = error;
209
-
210
- const unexpectedError = {
211
- message,
212
- stack: '' + stack,
213
- properties: JSON.stringify(error),
214
- };
215
-
216
- this.resolvedRequests[reqIndex].unexpectedError = unexpectedError;
217
-
218
- log.info(reason, error);
219
- log.info('Unexpected error info:', unexpectedError);
220
- }
221
-
222
- this.wsHandler.closeAllConnections();
223
- throw error;
224
- }
225
- }
226
- this.wsHandler.closeAllConnections();
227
- }
228
-
229
- shouldStop(request: LoadmillRequest) {
230
- return this.isTruthyParameter('stop', request.stopBefore);
231
- }
232
-
233
- shouldStopLooping(loop?: LoopConf) {
234
- const asserter = new Asserter(this.parameters);
235
- return loop && loop.assert && asserter.assert(loop.assert);
236
- }
237
-
238
- hasLoopingFailed(loop?: LoopConf) {
239
- const asserter = new Asserter(this.parameters);
240
- return loop && loop.assert && !asserter.assert(loop.assert);
241
- }
242
-
243
- isTruthyParameter(conditionType, parameterName?: string) {
244
- const value =
245
- parameterName &&
246
- parameterUtils.resolveParameter(parameterName, this.parameters, (err) =>
247
- setParameterErrorHistogram(
248
- err,
249
- `Failed to compute ${conditionType} condition - `
250
- )
251
- );
252
- return valueUtils.isTruthyParameterValue(value);
253
- }
254
-
255
- getSkipIndexIfShould(
256
- request: LoadmillRequest,
257
- reqIndex: number,
258
- requests: LoadmillRequest[]
259
- ) {
260
- const { skipBefore, disabled } = request;
261
-
262
- if (disabled) {
263
- return reqIndex + 1;
264
- }
265
-
266
- if (skipBefore) {
267
- const { goTo, negate, condition } = skipBefore;
268
-
269
- const shouldSkip = this.isTruthyParameter('skip', condition);
270
- if ((shouldSkip && !negate) || (!shouldSkip && negate)) {
271
- if (goTo) {
272
- return findRequestIndex(requests, goTo);
273
- } else {
274
- return reqIndex + 1;
275
- }
276
- }
277
- }
278
- }
279
-
280
- async waitBeforeRequest(request: LoadmillRequest) {
281
- const rawDelay = request.delay || DEFAULT_REQUEST_DELAY;
282
- const delay = clamp(
283
- Math.round(
284
- Number(
285
- isParameterCandidate(rawDelay)
286
- ? parameterUtils.resolveParameter(
287
- rawDelay,
288
- this.parameters,
289
- (err) =>
290
- setParameterErrorHistogram(
291
- err,
292
- 'Failed to compute request delay - '
293
- )
294
- )
295
- : rawDelay
296
- )
297
- ),
298
-
299
- MIN_REQUEST_DELAY,
300
- Infinity
301
- );
302
-
303
- const problem = validateIntegerDelay(delay, false);
304
-
305
- if (problem) {
306
- throw new RequestFailuresError(`Invalid Request Delay: ${problem}`);
307
- }
308
-
309
- await promiseUtils.delay(delay);
310
- }
311
-
312
- setRequestFailure(index: number, histogram: Histogram) {
313
- this.failures[index] = OneFailure(histogram);
314
- }
315
-
316
- async executeRequest(requestConf: LoadmillRequest, index: number) {
317
- let failedAssertionsHistogram, resTime;
318
- let loopIteration = 0;
319
- const maxIterations = getLoopIterations(requestConf.loop);
320
-
321
- while (loopIteration < maxIterations) {
322
- loopIteration++;
323
- this.initVolatileParameters();
324
- const request = this.prepareRequest(requestConf, index);
325
-
326
- const res = await this.sendRequest(request, index);
327
-
328
- // Setting now to avoid possible user overwrite:
329
- resTime = Number(this.parameters.__responseTime);
330
-
331
- const { timeout = DEFAULT_REQUEST_TIMEOUT } = requestConf;
332
- if (resTime > timeout) {
333
- throw new RequestFailuresError(
334
- `Response received after timeout of ${timeout}ms exceeded`
335
- );
336
- }
337
-
338
- res.wsExtractionData = { messages: this.wsHandler.messages, timeLimit: timeout - resTime } as WSExtractionData;
339
-
340
- failedAssertionsHistogram = await this.processSuccessfulResponse(
341
- index,
342
- requestConf,
343
- res
344
- );
345
-
346
- this.resolvedRequests[index].retried = loopIteration > 1 ? loopIteration - 1 : undefined;
347
- if (loopIteration < maxIterations) {
348
- if (this.shouldStopLooping(requestConf.loop)) {
349
- break;
350
- } else {
351
- await promiseUtils.delay(1000); // todo yigal - make this configrable?
352
- }
353
- } else {
354
- if (this.hasLoopingFailed(requestConf.loop)) {
355
- throw new RequestFailuresError(
356
- `Request failed to meet loop condition for ${maxIterations} iterations`
357
- );
358
- }
359
- }
360
- }
361
-
362
- if (!isEmpty(failedAssertionsHistogram)) {
363
- const message = 'Request failed due to assertions.';
364
-
365
- log.debug(message, {
366
- failedAssertionsHistogram,
367
- parameters: this.parameters,
368
- });
369
-
370
- throw new RequestFailuresError(message, failedAssertionsHistogram);
371
- }
372
-
373
- this.onRequestSuccess(index, resTime);
374
- this.removeVolatilePostParameters(index);
375
- }
376
-
377
- onRequestSuccess(index: number, resTime: number) {
378
- this.avgResTime = mathUtils.calcAvg(this.avgResTime, this.successfulHits, resTime, 1);
379
- ++this.successfulHits;
380
-
381
- setReqStats(this.requestStats, index, resTime);
382
- }
383
-
384
- initVolatileParameters() {
385
- Object.values(BUILT_IN_VOLATILE_PARAMS)
386
- .forEach((param: string) => this.parameters[param] = '');
387
- }
388
-
389
- setVolatileParameters(param, val) {
390
- this.parameters[param] = val;
391
- }
392
-
393
- removeVolatilePostParameters(idx: number) {
394
- Object.values(BUILT_IN_VOLATILE_PARAMS).forEach((val: string) => {
395
- const postParams = this.resolvedRequests[idx].postParameters;
396
- if (postParams) {
397
- const toRemove = postParams.findIndex(param => !isEmpty(param[val]));
398
- toRemove >=0 && postParams.splice(toRemove, 1);
399
- }
400
- });
401
- }
402
-
403
- async sendRequest(request, reqIndex: number) {
404
- let res, reqId;
405
- const redirectHeaders: any = [];
406
- const requestStartTime = Date.now();
407
-
408
- const beforeTime = isPerformance ? performance.now() : requestStartTime;
409
-
410
- if (typeof loadmillClearResponses === 'function') {
411
- await loadmillClearResponses();
412
- reqId = randomstring.generate(30);
413
- request.query({ [reqIdParamName]: reqId });
414
- }
415
-
416
- const setTimeParams = () => {
417
- const responseEndTime = Date.now();
418
-
419
- const afterTime = isPerformance ? performance.now() : responseEndTime;
420
-
421
- this.parameters.__requestStartTime = '' + requestStartTime;
422
- this.parameters.__responseEndTime = '' + responseEndTime;
423
- this.parameters.__responseTime = '' + Math.round(afterTime - beforeTime);
424
- };
425
-
426
- const setResolvedResponse = ({
427
- text,
428
- type,
429
- status,
430
- headers,
431
- statusText = '',
432
- resRequest,
433
- }) => {
434
- text = text || '';
435
- type = type || '';
436
- statusText = statusText || '';
437
-
438
- this.parameters.__status = '' + status;
439
- this.parameters.__statusText = statusText;
440
-
441
- if (text.length > MAX_RESPONSE_COLLECT) {
442
- text = text.slice(0, MAX_RESPONSE_COLLECT - 1) + '\n...';
443
- }
444
-
445
- this.resolvedRequests[reqIndex].response = {
446
- type,
447
- text,
448
- status,
449
- statusText,
450
- headers: manipulationUtils.objToSingletonArray(headers),
451
- };
452
-
453
- // add the request! cookies in case we have any
454
- const requestHeaders: LoadmillHeaders[] | undefined = this.resolvedRequests[reqIndex]?.headers;
455
- if (requestHeaders) {
456
- if (resRequest._headers && resRequest._headers.cookie) {
457
- const cookie = { 'cookie': resRequest._headers.cookie };
458
- const idx = requestHeaders.findIndex(item => Object.keys(item).includes('cookie'));
459
- requestHeaders[idx > 0 ? idx : requestHeaders.length] = cookie;
460
- }
461
- }
462
-
463
- // May be reset after extractions.
464
- // This way it is set for failed statuses as well:
465
- this.setPostParameters(reqIndex);
466
- };
467
-
468
- request.on('redirect', res => {
469
- redirectHeaders.push(res.headers.location);
470
- });
471
-
472
- try {
473
- res = await request.ok((res) => {
474
- const status = res.status;
475
-
476
- setTimeParams();
477
- setResolvedResponse({
478
- status,
479
- type: res.type,
480
- text: res.text,
481
- headers: extendResponseHeaders(res.header, redirectHeaders),
482
- statusText: res.xhr ? res.xhr.statusText : res.res!.statusMessage,
483
- resRequest: res.req
484
- });
485
-
486
- return isExpectedStatus(request, status);
487
- });
488
- } catch (error) {
489
- log.debug('Request failed:', error);
490
- const { status, message } = error;
491
-
492
- const prefix = status ? `HTTP status ${status} - ` : '';
493
- let reportedReason = prefix + message;
494
-
495
- if (message === superagentCorsError) {
496
- if (typeof loadmillGetResponse === 'function') {
497
- setTimeParams();
498
-
499
- const response = await loadmillGetResponse(reqId);
500
- log.debug(
501
- 'Got response from oracle:',
502
- JSON.stringify({ reqId, response })
503
- );
504
-
505
- reportedReason =
506
- (response.preFlight
507
- ? corsPreFlightErrorStatusPrefix
508
- : corsErrorStatusPrefix) + response.status;
509
-
510
- this.resolvedRequests[reqIndex].headers = manipulationUtils.objToSingletonArray(
511
- response.requestHeaders
512
- );
513
- setResolvedResponse({
514
- ...(response as any),
515
- type: response.headers['content-type'],
516
- });
517
- } else {
518
- reportedReason = unknownStatusCorsError;
519
- }
520
- }
521
-
522
- throw new RequestFailuresError(message, { [reportedReason]: 1 });
523
- }
524
-
525
- const failure = this.failures[reqIndex];
526
- if (failure) {
527
- log.debug('Request aborted due to failures:', failure.histogram);
528
- throw Error('Request aborted');
529
- }
530
-
531
- return res;
532
- }
533
-
534
- prepareRequest(requestConf: LoadmillRequest, reqIndex: number) {
535
- if (this.isWSRequest(requestConf) && !envUtils.isBrowser()) {
536
- return this.prepareWSRequest(requestConf, reqIndex);
537
- }
538
- return this.prepareHttpRequest(requestConf, reqIndex);
539
- }
540
- private isWSRequest({ url }: LoadmillRequest) {
541
- const resolvedUrl = this.resolve(url, (e) => setParameterErrorHistogram(e, 'Failed to compute URL - '));
542
- return resolvedUrl.startsWith('ws://') || resolvedUrl.startsWith('wss://');
543
- }
544
- private prepareHttpRequest(requestConf: LoadmillRequest, reqIndex: number) {
545
- const urlObj = new URI(
546
- resolveUrl(requestConf.url, this.parameters, (err: Error) =>
547
- setParameterErrorHistogram(err, 'Failed to compute URL - ')
548
- )
549
- );
550
-
551
- // these are empty strings when missing!
552
- const user = urlObj.username();
553
- const password = urlObj.password();
554
-
555
- if (user || password) {
556
- // null to make sure they won't be part of the URL no more:
557
- urlObj.username(null as any);
558
- urlObj.password(null as any);
559
- requestConf.auth = { user, password };
560
- }
561
-
562
- const url = urlObj.toString();
563
- this.resolvedRequests[reqIndex].url = url;
564
-
565
- this.validateDomain(uriUtils.getDomain(url));
566
-
567
- const method = requestConf.method.toLowerCase();
568
-
569
- const request = this.httpAgent[method](url);
570
- if (!envUtils.isBrowser()) {
571
- setTCPReuse(request, this.keepaliveHTTPAgent, this.keepaliveHTTPSAgent);
572
- }
573
-
574
- request.timeout(requestConf.timeout);
575
- requestConf.noRedirects && request.redirects(0);
576
-
577
- const auth = requestConf.auth;
578
-
579
- if (auth != null && request._header['authorization'] == null) {
580
- request.auth(auth.user || '', auth.password || '');
581
- }
582
-
583
- this.preparePostData(requestConf, request, reqIndex);
584
-
585
- // todo itay: instead we should collect from all extractions but some kind of `defer` flag to header extractors:
586
- const extractionHeaders = flatMap(
587
- requestConf.extract,
588
- this.collectExtractionHeaders
589
- ).filter(isNonSimpleResponseHeader);
590
-
591
- if (!isEmpty(extractionHeaders)) {
592
- // this is not required in a node environment but also does not matter
593
- // thus we prefer to keep it in order to reduce dissimilarities:
594
- //todo itay: allow users to overwrite / disable this headers?
595
- request.set(
596
- 'Loadmill-Request-Expose-Headers',
597
- extractionHeaders.join(',')
598
- );
599
- }
600
-
601
- // Prevent large downloads:
602
- const progressHandler = (event) =>
603
- this.checkProgressEvent(reqIndex, request, event);
604
-
605
- if (envUtils.isBrowser()) {
606
- // Because superagent adds upload handlers which cause
607
- // requests to be non-simple (in Firefox) we use the XHR object directly:
608
- request.on('request', ({ xhr }) => (xhr.onprogress = progressHandler));
609
-
610
- // Disable parsing:
611
- request.parse(() => null);
612
-
613
- if (requestConf.useCookies) {
614
- log.trace('Enabling cookies for CORS client.');
615
- request.withCredentials();
616
- }
617
-
618
- // Penetrate browser cache:
619
- if (method === 'get') {
620
- const simpleRequest = isSimpleRequest(request._header);
621
- const cachePenetration = requestConf.cachePenetration || {};
622
- const mode = cachePenetration.mode || CachePenetrationModes.def;
623
-
624
- if (
625
- mode === CachePenetrationModes.alwaysQuery ||
626
- (mode === CachePenetrationModes.def && simpleRequest)
627
- ) {
628
- // Avoid pre-flight:
629
- request.query({ 'loadmill-cache-key': randomstring.generate(15) });
630
- } else if (
631
- request._header['cache-control'] == null &&
632
- (mode === CachePenetrationModes.alwaysHeader ||
633
- (mode === CachePenetrationModes.def && !simpleRequest))
634
- ) {
635
- // Since request is pre-flighted anyway, adding this header shouldn't hurt.
636
- // It is better in this case because pre-flight response can be cached:
637
- request.set(
638
- 'Cache-Control',
639
- cachePenetration.cacheControl || DEFAULT_CACHE_CONTROL
640
- );
641
- }
642
- }
643
- } else {
644
- // todo itay: consider using `_maxResponseSize` in node:
645
- request.on('progress', progressHandler);
646
-
647
- // This makes superagent populate res.text regardless of the response's content type:
648
- request.buffer();
649
-
650
- // Otherwise we expose superagent and its version -
651
- // better have something more general (in case the user didnt set one):
652
- const uaHeader = request.get('User-Agent');
653
- if (!uaHeader || uaHeader.startsWith('node-superagent')) {
654
- request.set('User-Agent', 'loadmill/v' + millVersionString);
655
- }
656
- }
657
-
658
- this.resolvedRequests[reqIndex].headers = manipulationUtils.objToSingletonArray(
659
- request.header
660
- );
661
-
662
- request.expectedStatus = requestConf.expectedStatus || ALLOWED_RESPONSE_STATUSES.SUCCESS;
663
-
664
- return request;
665
- }
666
-
667
- private setHeaders(headers: LoadmillHeaders[] | undefined, request: any, filter = (_h: LoadmillHeaders) => true) {
668
- if (headers && !isEmpty(headers)) {
669
- this.resolveHeaders(headers)
670
- .filter(filter)
671
- .forEach(({ name, value }) => request.set(name, value));
672
- }
673
- }
674
-
675
- private preparePostData(requestConf: LoadmillRequest, request: any, reqIndex: number) {
676
-
677
- // Order matters! It is CRUCIAL that the type is set before the body.
678
- // So first, set only the content-type header, after set the body, then volatiles and then the rest of the headers
679
- // Rest of the headers should be set after the postData so we can use the body in headers
680
-
681
- const headers = requestConf.headers;
682
- this.setHeaders(headers, request, (h) => h.name.toLowerCase() === 'content-type');
683
-
684
- const { postData, postFormData } = requestConf;
685
- let data;
686
- if (postFormData) {
687
- data = this.preparePostFormData(postFormData, request, reqIndex);
688
- }
689
- else if (postData) {
690
- data = this.prepareRawPostData(postData, request, data, reqIndex);
691
- }
692
-
693
- this.setVolatileParameters(BUILT_IN_VOLATILE_PARAMS.requestBody, data);
694
- this.setHeaders(headers, request, (h) => h.name.toLowerCase() !== 'content-type');
695
- }
696
-
697
- private prepareRawPostData(postData: RequestPostData, request: any, data: any, reqIndex: number) {
698
- // It is CRUCIAL that the type is set before the body:
699
- const mimeType = postData.mimeType;
700
- if (mimeType && request._header['content-type'] == null) {
701
- // set Content-Type header if not already present:
702
- request.type(mimeType);
703
- }
704
-
705
- data = this.resolve(postData.text, (err) => setParameterErrorHistogram(err, 'Failed to compute request body - ')
706
- );
707
-
708
- if (data && data.length > getMaxRequestBodySize()) {
709
- throw new RequestFailuresError('Request body size is too large');
710
- }
711
-
712
- // This invocation will behave differently according to different types set on the request:
713
- request.send(data);
714
-
715
- this.resolvedRequests[reqIndex].postData = {
716
- mimeType,
717
- text: data,
718
- };
719
- return data;
720
- }
721
-
722
- private preparePostFormData(postFormData: PostFormData, request: any, reqIndex: number) {
723
- const resolvedPostFormData: PostFormData = [];
724
- let size = 0;
725
- const MAX_BODY_SIZE = getMaxRequestBodySize();
726
- for (const entry of postFormData) {
727
- const { name, value, fileName } = entry;
728
- const resolvedName = this.resolve(name, (err) => setParameterErrorHistogram(err, 'Failed to resolve form-data key - '));
729
- size += resolvedName.length;
730
- if (fileName) {
731
- const resolvedFileName = this.resolve(fileName, (err) => setParameterErrorHistogram(err, 'Failed to resolve form-data fileName - '));
732
- size += (resolvedFileName.length + value.length);
733
- const buffer = Buffer.from(value, 'binary');
734
- request.attach(resolvedName, buffer, resolvedFileName);
735
- resolvedPostFormData.push({ name: resolvedName, value, fileName: resolvedFileName });
736
- } else {
737
- const resolvedValue = this.resolve(value, (err) => setParameterErrorHistogram(err, 'Failed to resolve form-data value - '));
738
- size += resolvedValue.length;
739
- request.field(resolvedName, resolvedValue);
740
- resolvedPostFormData.push({ name: resolvedName, value: resolvedValue });
741
- }
742
- if (size > MAX_BODY_SIZE) {
743
- throw new RequestFailuresError('Form Data size is too large');
744
- }
745
- }
746
-
747
- this.resolvedRequests[reqIndex].postFormData = resolvedPostFormData;
748
- return resolvedPostFormData;
749
- }
750
-
751
- prepareWSRequest(requestConf: LoadmillRequest, reqIndex: number) {
752
- const wsRequestArgs = this.resolveAndSetWSReqData(requestConf, reqIndex);
753
-
754
- return new WSRequest(
755
- wsRequestArgs,
756
- this.wsHandler,
757
- (e: Error) => this.setSingleFailure(reqIndex, 'Websocket error: ' + e.message),
758
- );
759
- }
760
-
761
- resolveAndSetWSReqData = (
762
- { url, headers, postData, timeout = DEFAULT_REQUEST_TIMEOUT, expectedStatus = 'SUCCESS' }: LoadmillRequest,
763
- reqIndex: number
764
- ): WSRequestArguments => {
765
- const preparedUrl = this.prepareWsUrl(url, reqIndex);
766
-
767
- // order matters. headers should be set after the message so we can use the message in headers
768
- const { resolvedMessage: preparedMessage, mimeType } = this.prepareWsMessage(postData, reqIndex);
769
- const preparedHeaders = this.prepareWsHeaders(headers, reqIndex);
770
-
771
- return {
772
- expectedStatus,
773
- headers: preparedHeaders,
774
- message: preparedMessage,
775
- mimeType,
776
- timeout,
777
- url: preparedUrl,
778
- };
779
- };
780
-
781
- prepareWsUrl = (url: string, reqIndex: number) => {
782
- const resolvedUrl = this.resolve(url, (e: Error) => setParameterErrorHistogram(e, 'Failed to compute URL - '));
783
- this.resolvedRequests[reqIndex].url = resolvedUrl;
784
- return resolvedUrl;
785
- };
786
-
787
- prepareWsHeaders = (headers: LoadmillHeaders[] | undefined, reqIndex: number) => {
788
- const resolvedHeadersObj: LoadmillHeaders = {};
789
- if (headers && !isEmpty(headers)) {
790
- this.resolveHeaders(headers)
791
- .forEach(({ name, value }) => resolvedHeadersObj[name] = value);
792
- }
793
- this.resolvedRequests[reqIndex].headers = manipulationUtils.objToSingletonArray(resolvedHeadersObj);
794
- return resolvedHeadersObj;
795
- };
796
-
797
- prepareWsMessage = (postData: RequestPostData | undefined, reqIndex: number): { resolvedMessage: string, mimeType: WSMimeType} => {
798
- let resolvedMessage: string = '';
799
- const mimeType = postData?.mimeType === 'binary' ? 'binary' : 'text';
800
- if (postData) {
801
- resolvedMessage = this.resolve(postData.text, (err: Error) =>
802
- setParameterErrorHistogram(err, 'Failed to compute Websocket message - ')
803
- );
804
- if (resolvedMessage && resolvedMessage.length > getMaxRequestBodySize()) {
805
- throw new RequestFailuresError('Websocket message size is too large');
806
- }
807
- this.resolvedRequests[reqIndex].postData = {
808
- mimeType,
809
- text: resolvedMessage,
810
- };
811
- }
812
- this.setVolatileParameters(BUILT_IN_VOLATILE_PARAMS.requestBody, resolvedMessage);
813
- return { resolvedMessage, mimeType };
814
- };
815
-
816
- checkProgressEvent = (
817
- requestIndex: number,
818
- request,
819
- { total = 0, loaded }
820
- ) => {
821
- log.trace('Progress:', { total, loaded });
822
-
823
- if ((total || loaded) >= MAX_RESPONSE_BYTES) {
824
- this.setSingleFailure(requestIndex, 'Response size is too large');
825
- request.abort();
826
- }
827
- };
828
-
829
- private validateDomain(domain: string) {
830
- if (isEmpty(domain)) {
831
- const message = 'HTTP request domain name is empty';
832
- throw new RequestFailuresError(message, { [message]: 1 });
833
- }
834
-
835
- if (validate.isUrl(domain)) {
836
- const message = `HTTP request domain name [${domain}] is not valid`;
837
- throw new RequestFailuresError(message, { [message]: 1 });
838
- }
839
-
840
- else if (!uriUtils.isVerified(domain, this.domainsWhiteList)) {
841
- const message = `HTTP request domain name [${domain}] is not on white list`;
842
- throw new RequestFailuresError(message, { [message]: 1 });
843
- }
844
- }
845
-
846
- setSingleFailure(reqIndex, reason: string) {
847
- return this.setRequestFailure(reqIndex, { [reason]: 1 });
848
- }
849
-
850
- resolveHeaders(headers: LoadmillHeaders[]) {
851
- return flatMap(headers, (aHeaders) =>
852
- map(aHeaders, (value, name) => ({
853
- name,
854
- value: this.resolve(value, (err) =>
855
- setParameterErrorHistogram(
856
- err,
857
- `Failed to compute ${name} header value - `
858
- )
859
- ),
860
- }))
861
- );
862
- }
863
-
864
- collectExtractionHeaders = (extractions: Extractions) =>
865
- filter(
866
- extractions,
867
- (extraction: any) => extraction.header != null
868
- ).map((extraction: ExtractionObj) =>
869
- this.resolve(extraction.header!, (err) =>
870
- setParameterErrorHistogram(
871
- err,
872
- `Failed to compute extraction header ${extraction.header} - `
873
- )
874
- )
875
- );
876
-
877
- async processSuccessfulResponse(reqIndex, requestConf: LoadmillRequest, res) {
878
- // modifies parameters:
879
- await this.handleExtractions(requestConf, res);
880
-
881
- if (!envUtils.isBrowser()) {
882
- this.handlePostScript(requestConf, res);
883
- }
884
-
885
- this.setPostParameters(reqIndex);
886
-
887
- const assertionResults = this.handleAssertions(requestConf) || [];
888
- log.trace('Assertion results: ', assertionResults);
889
-
890
- const failuresHistogram: any = {};
891
-
892
- assertionResults.forEach((result, index) => {
893
- if (!result) {
894
- failuresHistogram[index] = 1;
895
- }
896
- });
897
-
898
- return failuresHistogram;
899
- }
900
-
901
- setPostParameters(reqIndex: number) {
902
- this.resolvedRequests[reqIndex].postParameters = manipulationUtils.objToSingletonArray(
903
- this.parameters
904
- ).filter(p => !isEmpty(p)); // cleaning param values that are function or undefined which resolves to empty object {}
905
- }
906
-
907
- async handleExtractions(requestConf: LoadmillRequest, res) {
908
- for (const extractions of (requestConf.extract || [])) {
909
- await this.extractInScope(res, extractions);
910
- }
911
- }
912
-
913
- async extractInScope(res, extractions: Extractions) {
914
- const contextParameters = Object.assign({}, this.parameters);
915
- const extractionCombiner = new ExtractionCombiner(contextParameters, res, res.wsExtractionData);
916
-
917
- for (const [name, extraction] of Object.entries(extractions)) {
918
- await this.extract(name, extraction, extractionCombiner);
919
- }
920
- }
921
-
922
- async extract(
923
- parameterName: string,
924
- extraction: Extraction,
925
- extractionCombiner: ExtractionCombiner
926
- ) {
927
- log.trace('Parameter extraction start: ', { parameterName, extraction });
928
-
929
- const combinedExtractor = await extractionCombiner.combine(extraction);
930
- let result;
931
-
932
- try {
933
- result = await combinedExtractor();
934
- } catch (error) {
935
- const genericMessage = `Failed to extract value for parameter "${parameterName}"`;
936
- log.debug(genericMessage, error);
937
-
938
- const { prettyMessage } = error;
939
- const publicMessage = prettyMessage
940
- ? `${genericMessage} - ${prettyMessage}`
941
- : genericMessage;
942
-
943
- throw new RequestFailuresError(publicMessage);
944
- }
945
-
946
- if (result != null) {
947
- this.parameters[parameterName] = result;
948
- } else if (this.parameters[parameterName] == null) {
949
- // Existing parameter value acts as default. If no such value exists,
950
- // we set the value to be an empty string to differentiate it from undefined parameters.
951
- // Otherwise, this parameter's usages would stay untouched, e.g. "${paramName}".
952
- this.parameters[parameterName] = '';
953
- }
954
-
955
- log.trace('Parameter value:', this.parameters[parameterName]);
956
- }
957
-
958
- resolve(parametrized: string, onError) {
959
- return parameterUtils.resolveExpression(
960
- parametrized,
961
- this.parameters,
962
- onError
963
- );
964
- }
965
-
966
- handlePostScript({ postScript }: LoadmillRequest, { text = '' }) {
967
- if (postScript) {
968
- testRunEventsEmitter.postScript.started();
969
-
970
- try {
971
- const result = this.runPostScript(postScript, text);
972
- Object.assign(this.parameters, result);
973
- } catch (e) {
974
- const message = ['Post Script', e.name + ':', e.message].join(' ');
975
- throw new RequestFailuresError(message);
976
- }
977
-
978
- testRunEventsEmitter.postScript.finished();
979
- }
980
- }
981
-
982
- private runPostScript(postScript: string, text: string) {
983
- const staticContext = this.getStaticContext(text);
984
- return this.postScriptRunner.run({ staticContext }, this.parameters, postScript);
985
- }
986
-
987
- private getStaticContext(text: string) {
988
- let $ = {};
989
- try {
990
- $ = JSON.parse(text);
991
- } catch (e) {
992
- log.debug('res text (body) cannot be JSON parsed', e.name, e.message);
993
- }
994
- return { $, __: parameterFunctionOperations };
995
- }
996
-
997
- handleAssertions(requestConf: LoadmillRequest) {
998
- const asserter = new Asserter(this.parameters);
999
- return requestConf.assert?.map((assertion: Assertion, index) => {
1000
- try {
1001
- return asserter.assert(assertion);
1002
- } catch (e) {
1003
- setParameterErrorHistogram(
1004
- e,
1005
- `Failed to compute assertion #${index + 1} - `
1006
- );
1007
- throw e;
1008
- }
1009
- });
1010
- }
1011
- }
1012
-
1013
- function setParameterErrorHistogram(err, prefix) {
1014
- err.histogram = { [prefix + (err.prettyMessage || 'Parameter error')]: 1 };
1015
- }
1016
-
1017
- function isNonSimpleResponseHeader(headerName: string) {
1018
- const normalized = headerName.toLowerCase();
1019
-
1020
- return (
1021
- normalized !== 'cache-control' &&
1022
- normalized !== 'content-language' &&
1023
- normalized !== 'content-type' &&
1024
- normalized !== 'expires' &&
1025
- normalized !== 'last-modified' &&
1026
- normalized !== 'pragma'
1027
- );
1028
- }
1029
-
1030
- function isSimpleRequest(headers) {
1031
- const contentType = headers['content-type'];
1032
-
1033
- if (contentType) {
1034
- const [type] = contentType.split(';');
1035
-
1036
- if (
1037
- type !== 'text/plain' &&
1038
- type !== 'multipart/form-data' &&
1039
- type !== 'application/x-www-form-urlencoded'
1040
- ) {
1041
- return false;
1042
- }
1043
- }
1044
-
1045
- return !find(
1046
- headers,
1047
- (_, key: string) =>
1048
- key !== 'accept' &&
1049
- key !== 'accept-language' &&
1050
- key !== 'content-language' &&
1051
- key !== 'content-type'
1052
- );
1053
- }
1054
-
1055
- const getLoopIterations = (LoopConf?: LoopConf) => {
1056
- const declared = (LoopConf && LoopConf.iterations) || 1;
1057
- return Math.min(MAX_REQUEST_LOOPS_ITERATIONS, declared);
1058
- };
1059
-
1060
- const extendResponseHeaders = (headers, redirectHeaders) => {
1061
- if (!isEmpty(redirectHeaders) && headers) {
1062
- redirectHeaders.forEach((loc, idx) => {
1063
- headers[`location_${idx + 1}`] = loc;
1064
- });
1065
- headers.location = redirectHeaders[redirectHeaders.length - 1]; // stroe the last one as location
1066
- }
1067
- return headers;
1068
- };
1069
-
1070
- const isExpectedStatus = ({ expectedStatus, url }, status: number) => {
1071
- if (expectedStatus === ALLOWED_RESPONSE_STATUSES.SUCCESS) {
1072
- return (200 <= status && status < 400) || status === 101;
1073
- }
1074
- else if (expectedStatus === ALLOWED_RESPONSE_STATUSES.ERROR) {
1075
- log.debug('user asked to fail this request', url);
1076
- return 400 <= status && status < 600;
1077
- }
1078
- else if (expectedStatus === ALLOWED_RESPONSE_STATUSES.ANY) {
1079
- return true;
1080
- }
1081
- log.warn('Expected status is not valid, expectedStatus');
1082
- return false;
1083
- };
1084
-
1085
- /**
1086
- * Superagent reuse the same request when redirecting and they are overiding the URL
1087
- * So, we are setting the agent after the URL is overrided because there might be a change of protocols
1088
- * Well done Omril
1089
- */
1090
- const setTCPReuse = (request, agent, sslAgent) => {
1091
-
1092
- const _oldRequest = request.request;
1093
- request.request = function (...args) {
1094
- // first intercept the request and do our stuff
1095
- const { url } = this;
1096
-
1097
- if (url.startsWith('https:')) {
1098
- this.agent(sslAgent);
1099
- } else {
1100
- this.agent(agent);
1101
- }
1102
-
1103
- // then continue with superagent logic
1104
- _oldRequest.apply(this, ...args);
1105
- };
1106
-
1107
- };