@stuntman/client 0.1.2 → 0.1.3

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 CHANGED
@@ -30,4 +30,4 @@ const rule = ruleBuilder()
30
30
  client.addRule(rule).then((x) => console.log(x));
31
31
  ```
32
32
 
33
- Check [example app](https://github.com/andrzej-woof/stuntman/tree/master/packages/example#readme) for more samples
33
+ Check [example app](https://github.com/andrzej-woof/stuntman/tree/master/apps/example#readme) for more samples
@@ -4,6 +4,7 @@ type ClientOptions = {
4
4
  host?: string;
5
5
  port?: number;
6
6
  timeout?: number;
7
+ apiKey?: string;
7
8
  };
8
9
  export declare class Client {
9
10
  private options;
package/dist/apiClient.js CHANGED
@@ -107,7 +107,14 @@ class Client {
107
107
  controller.abort();
108
108
  }, this.options.timeout);
109
109
  try {
110
- const response = await fetch(url, { ...init, signal: (_a = init === null || init === void 0 ? void 0 : init.signal) !== null && _a !== void 0 ? _a : controller.signal });
110
+ const response = await fetch(url, {
111
+ ...init,
112
+ headers: {
113
+ ...(this.options.apiKey && { 'x-api-key': this.options.apiKey }),
114
+ ...init === null || init === void 0 ? void 0 : init.headers,
115
+ },
116
+ signal: (_a = init === null || init === void 0 ? void 0 : init.signal) !== null && _a !== void 0 ? _a : controller.signal,
117
+ });
111
118
  if (!response.ok) {
112
119
  const text = await response.text();
113
120
  let json;
@@ -45,6 +45,7 @@ declare class RuleBuilder extends RuleBuilderBase {
45
45
  onRequestTo(filter: string | RegExp): RuleBuilderInitialized;
46
46
  onRequestToHostname(hostname: string | RegExp): RuleBuilderInitialized;
47
47
  onRequestToPathname(pathname: string | RegExp): RuleBuilderInitialized;
48
+ onRequestToPort(port: string | number | RegExp): RuleBuilderInitialized;
48
49
  onAnyRequest(): RuleBuilderInitialized;
49
50
  }
50
51
  declare class RuleBuilderInitialized extends RuleBuilderBase {
@@ -56,7 +57,7 @@ declare class RuleBuilderInitialized extends RuleBuilderBase {
56
57
  withSearchParams(params: KeyValueMatcher[]): RuleBuilderInitialized;
57
58
  withHeader(key: string | RegExp): RuleBuilderInitialized;
58
59
  withHeader(key: string, value?: string | RegExp): RuleBuilderInitialized;
59
- withHeaders(headers: KeyValueMatcher[]): RuleBuilderInitialized;
60
+ withHeaders(...headers: KeyValueMatcher[]): RuleBuilderInitialized;
60
61
  withBodyText(includes: string): RuleBuilderInitialized;
61
62
  withBodyText(matches: RegExp): RuleBuilderInitialized;
62
63
  withoutBody(): RuleBuilderInitialized;
@@ -67,11 +68,12 @@ declare class RuleBuilderInitialized extends RuleBuilderBase {
67
68
  proxyPass(): Stuntman.SerializableRule;
68
69
  mockResponse(staticResponse: Stuntman.Response): Stuntman.SerializableRule;
69
70
  mockResponse(generationFunction: Stuntman.RemotableFunction<Stuntman.ResponseGenerationFn>): Stuntman.SerializableRule;
70
- modifyRequest(modifyFunction: Stuntman.RemotableFunction<Stuntman.RequestManipulationFn>): RuleBuilderRequestInitialized;
71
- modifyResponse(modifyFunction: Stuntman.RemotableFunction<Stuntman.ResponseManipulationFn>): Stuntman.SerializableRule;
71
+ mockResponse(localFn: Stuntman.ResponseGenerationFn, localVariables?: Stuntman.LocalVariables): Stuntman.SerializableRule;
72
+ modifyRequest(modifyFunction: Stuntman.RequestManipulationFn | Stuntman.RemotableFunction<Stuntman.RequestManipulationFn>, localVariables?: Stuntman.LocalVariables): RuleBuilderRequestInitialized;
73
+ modifyResponse(modifyFunction: Stuntman.ResponseManipulationFn | Stuntman.RemotableFunction<Stuntman.ResponseManipulationFn>, localVariables?: Stuntman.LocalVariables): Stuntman.SerializableRule;
72
74
  }
73
75
  declare class RuleBuilderRequestInitialized extends RuleBuilderBase {
74
- modifyResponse(modifyFunction: Stuntman.RemotableFunction<Stuntman.ResponseManipulationFn>): Stuntman.SerializableRule;
76
+ modifyResponse(modifyFunction: Stuntman.ResponseManipulationFn | Stuntman.RemotableFunction<Stuntman.ResponseManipulationFn>, localVariables?: Stuntman.LocalVariables): Stuntman.SerializableRule;
75
77
  }
76
78
  export declare const ruleBuilder: () => RuleBuilder;
77
79
  export {};
@@ -13,19 +13,21 @@ class RuleBuilderBaseBase {
13
13
  priority: shared_1.DEFAULT_RULE_PRIORITY,
14
14
  matches: {
15
15
  localFn: (req) => {
16
- var _a, _b, _c;
16
+ var _a, _b, _c, _d;
17
17
  const ___url = new URL(req.url);
18
18
  const ___headers = req.rawHeaders;
19
19
  const arrayIndexerRegex = /\[(?<arrayIndex>[0-9]*)\]/i;
20
20
  const matchObject = (obj, path, value, parentPath) => {
21
- var _a, _b;
21
+ var _a, _b, _c, _d;
22
22
  if (!obj) {
23
23
  return { result: false, description: `${parentPath} is falsey` };
24
24
  }
25
25
  const [rawKey, ...rest] = path.split('.');
26
26
  const key = rawKey.replace(arrayIndexerRegex, '');
27
27
  const shouldBeArray = arrayIndexerRegex.test(rawKey);
28
- const arrayIndex = Number((_b = (_a = arrayIndexerRegex.exec(rawKey)) === null || _a === void 0 ? void 0 : _a.groups) === null || _b === void 0 ? void 0 : _b.arrayIndex);
28
+ const arrayIndex = (((_b = (_a = arrayIndexerRegex.exec(rawKey)) === null || _a === void 0 ? void 0 : _a.groups) === null || _b === void 0 ? void 0 : _b.arrayIndex) || '').length > 0
29
+ ? Number((_d = (_c = arrayIndexerRegex.exec(rawKey)) === null || _c === void 0 ? void 0 : _c.groups) === null || _d === void 0 ? void 0 : _d.arrayIndex)
30
+ : Number.NaN;
29
31
  const actualValue = key ? obj[key] : obj;
30
32
  const currentPath = `${parentPath ? `${parentPath}.` : ''}${rawKey}`;
31
33
  if (value === undefined && actualValue === undefined) {
@@ -34,7 +36,7 @@ class RuleBuilderBaseBase {
34
36
  if (rest.length === 0) {
35
37
  if (shouldBeArray &&
36
38
  (!Array.isArray(actualValue) ||
37
- !(!Number.isInteger(arrayIndex) && actualValue.length > Number(arrayIndex)))) {
39
+ (Number.isInteger(arrayIndex) && actualValue.length <= Number(arrayIndex)))) {
38
40
  return { result: false, description: `${currentPath} empty array` };
39
41
  }
40
42
  if (value === undefined) {
@@ -52,7 +54,7 @@ class RuleBuilderBaseBase {
52
54
  if (Number.isInteger(arrayIndex)) {
53
55
  return matchObject(actualValue[Number(arrayIndex)], rest.join('.'), value, currentPath);
54
56
  }
55
- const hasArrayMatch = actualValue.some((arrayValue) => matchObject(arrayValue, rest.join('.'), value, currentPath));
57
+ const hasArrayMatch = actualValue.some((arrayValue) => matchObject(arrayValue, rest.join('.'), value, currentPath).result);
56
58
  return { result: hasArrayMatch, description: `array match ${currentPath}` };
57
59
  }
58
60
  if (typeof actualValue !== 'object') {
@@ -96,6 +98,17 @@ class RuleBuilderBaseBase {
96
98
  description: `pathname ${___url.pathname} doesn't match ${(_c = matchBuilderVariables.pathname) === null || _c === void 0 ? void 0 : _c.toString()}`,
97
99
  };
98
100
  }
101
+ if (matchBuilderVariables.port) {
102
+ const port = ___url.port && ___url.port !== '' ? ___url.port : ___url.protocol === 'https:' ? '443' : '80';
103
+ if (!___matchesValue(matchBuilderVariables.port instanceof RegExp
104
+ ? matchBuilderVariables.port
105
+ : `${matchBuilderVariables.port}`, port)) {
106
+ return {
107
+ result: false,
108
+ description: `port ${port} doesn't match ${(_d = matchBuilderVariables.port) === null || _d === void 0 ? void 0 : _d.toString()}`,
109
+ };
110
+ }
111
+ }
99
112
  if (matchBuilderVariables.searchParams) {
100
113
  for (const searchParamMatcher of matchBuilderVariables.searchParams) {
101
114
  if (typeof searchParamMatcher === 'string') {
@@ -130,11 +143,17 @@ class RuleBuilderBaseBase {
130
143
  for (const headerMatcher of matchBuilderVariables.headers) {
131
144
  if (typeof headerMatcher === 'string') {
132
145
  const result = ___headers.has(headerMatcher);
133
- return { result, description: `headers.has("${headerMatcher}")` };
146
+ if (result) {
147
+ continue;
148
+ }
149
+ return { result: false, description: `headers.has("${headerMatcher}")` };
134
150
  }
135
151
  if (headerMatcher instanceof RegExp) {
136
152
  const result = ___headers.toHeaderPairs().some(([key]) => headerMatcher.test(key));
137
- return { result, description: `headers.keys matches ${headerMatcher.toString()}` };
153
+ if (result) {
154
+ continue;
155
+ }
156
+ return { result: false, description: `headers.keys matches ${headerMatcher.toString()}` };
138
157
  }
139
158
  if (!___headers.has(headerMatcher.key)) {
140
159
  return { result: false, description: `headers.has("${headerMatcher.key}")` };
@@ -189,7 +208,8 @@ class RuleBuilderBaseBase {
189
208
  for (const jsonMatcher of Array.isArray(matchBuilderVariables.bodyJson)
190
209
  ? matchBuilderVariables.bodyJson
191
210
  : [matchBuilderVariables.bodyJson]) {
192
- if (!matchObject(json, jsonMatcher.key, jsonMatcher.value)) {
211
+ const matchObjectResult = matchObject(json, jsonMatcher.key, jsonMatcher.value);
212
+ if (!matchObjectResult.result) {
193
213
  return { result: false, description: `$.${jsonMatcher.key} != "${jsonMatcher.value}"` };
194
214
  }
195
215
  }
@@ -246,6 +266,9 @@ class RuleBuilderBaseBase {
246
266
  }
247
267
  class RuleBuilderBase extends RuleBuilderBaseBase {
248
268
  limitedUse(hitCount) {
269
+ if (this.rule.removeAfterUse) {
270
+ throw new Error(`limit already set at ${this.rule.removeAfterUse}`);
271
+ }
249
272
  if (Number.isNaN(hitCount) || !Number.isFinite(hitCount) || !Number.isInteger(hitCount) || hitCount <= 0) {
250
273
  throw new Error('Invalid hitCount');
251
274
  }
@@ -265,6 +288,9 @@ class RuleBuilderBase extends RuleBuilderBaseBase {
265
288
  }
266
289
  class RuleBuilder extends RuleBuilderBase {
267
290
  raisePriority(by) {
291
+ if (this.rule.priority !== shared_1.DEFAULT_RULE_PRIORITY) {
292
+ throw new Error('you should not alter rule priority more than once');
293
+ }
268
294
  const subtract = by !== null && by !== void 0 ? by : 1;
269
295
  if (subtract >= shared_1.DEFAULT_RULE_PRIORITY) {
270
296
  throw new Error(`Unable to raise priority over the default ${shared_1.DEFAULT_RULE_PRIORITY}`);
@@ -273,6 +299,9 @@ class RuleBuilder extends RuleBuilderBase {
273
299
  return this;
274
300
  }
275
301
  decreasePriority(by) {
302
+ if (this.rule.priority !== shared_1.DEFAULT_RULE_PRIORITY) {
303
+ throw new Error('you should not alter rule priority more than once');
304
+ }
276
305
  const add = by !== null && by !== void 0 ? by : 1;
277
306
  this.rule.priority = shared_1.DEFAULT_RULE_PRIORITY + add;
278
307
  return this;
@@ -303,20 +332,33 @@ class RuleBuilder extends RuleBuilderBase {
303
332
  this._matchBuilderVariables.pathname = pathname;
304
333
  return new RuleBuilderInitialized(this.rule, this._matchBuilderVariables);
305
334
  }
335
+ onRequestToPort(port) {
336
+ this._matchBuilderVariables.port = port;
337
+ return new RuleBuilderInitialized(this.rule, this._matchBuilderVariables);
338
+ }
306
339
  onAnyRequest() {
307
- return this.onRequestTo(/.*/);
340
+ return new RuleBuilderInitialized(this.rule, this._matchBuilderVariables);
308
341
  }
309
342
  }
310
343
  class RuleBuilderInitialized extends RuleBuilderBase {
311
344
  withHostname(hostname) {
345
+ if (this._matchBuilderVariables.hostname) {
346
+ throw new Error('hostname already set');
347
+ }
312
348
  this._matchBuilderVariables.hostname = hostname;
313
349
  return this;
314
350
  }
315
351
  withPathname(pathname) {
352
+ if (this._matchBuilderVariables.pathname) {
353
+ throw new Error('pathname already set');
354
+ }
316
355
  this._matchBuilderVariables.pathname = pathname;
317
356
  return this;
318
357
  }
319
358
  withPort(port) {
359
+ if (this._matchBuilderVariables.port) {
360
+ throw new Error('port already set');
361
+ }
320
362
  this._matchBuilderVariables.port = port;
321
363
  return this;
322
364
  }
@@ -368,7 +410,7 @@ class RuleBuilderInitialized extends RuleBuilderBase {
368
410
  this._matchBuilderVariables.headers.push({ key, value });
369
411
  return this;
370
412
  }
371
- withHeaders(headers) {
413
+ withHeaders(...headers) {
372
414
  if (!this._matchBuilderVariables.headers) {
373
415
  this._matchBuilderVariables.headers = [];
374
416
  }
@@ -383,10 +425,19 @@ class RuleBuilderInitialized extends RuleBuilderBase {
383
425
  return this;
384
426
  }
385
427
  withBodyText(includesOrMatches) {
428
+ if (this._matchBuilderVariables.bodyText) {
429
+ throw new Error('bodyText already set');
430
+ }
431
+ if (this._matchBuilderVariables.bodyText === null) {
432
+ throw new Error('cannot use both withBodyText and withoutBody');
433
+ }
386
434
  this._matchBuilderVariables.bodyText = includesOrMatches;
387
435
  return this;
388
436
  }
389
437
  withoutBody() {
438
+ if (this._matchBuilderVariables.bodyText) {
439
+ throw new Error('cannot use both withBodyText and withoutBody');
440
+ }
390
441
  this._matchBuilderVariables.bodyText = null;
391
442
  return this;
392
443
  }
@@ -397,7 +448,7 @@ class RuleBuilderInitialized extends RuleBuilderBase {
397
448
  }
398
449
  if (typeof keyOrMatcher === 'string') {
399
450
  if (!keyRegex.test(keyOrMatcher)) {
400
- throw new Error('invalid key');
451
+ throw new Error(`invalid key "${keyOrMatcher}"`);
401
452
  }
402
453
  this._matchBuilderVariables.bodyJson.push({ key: keyOrMatcher, value: withValue });
403
454
  return this;
@@ -406,7 +457,7 @@ class RuleBuilderInitialized extends RuleBuilderBase {
406
457
  throw new Error('invalid usage');
407
458
  }
408
459
  if (!keyRegex.test(keyOrMatcher.key)) {
409
- throw new Error('invalid key');
460
+ throw new Error(`invalid key "${keyOrMatcher}"`);
410
461
  }
411
462
  this._matchBuilderVariables.bodyJson.push(keyOrMatcher);
412
463
  return this;
@@ -418,24 +469,52 @@ class RuleBuilderInitialized extends RuleBuilderBase {
418
469
  proxyPass() {
419
470
  return this.rule;
420
471
  }
421
- mockResponse(response) {
472
+ mockResponse(response, localVariables) {
473
+ if (typeof response === 'function') {
474
+ this.rule.actions = { mockResponse: { localFn: response, localVariables: localVariables !== null && localVariables !== void 0 ? localVariables : {} } };
475
+ return this.rule;
476
+ }
477
+ if (localVariables) {
478
+ throw new Error('invalid call - localVariables cannot be used together with Response or RemotableFunction');
479
+ }
422
480
  this.rule.actions = { mockResponse: response };
423
481
  return this.rule;
424
482
  }
425
- modifyRequest(modifyFunction) {
483
+ modifyRequest(modifyFunction, localVariables) {
484
+ if (typeof modifyFunction === 'function') {
485
+ this.rule.actions = { modifyRequest: { localFn: modifyFunction, localVariables: localVariables !== null && localVariables !== void 0 ? localVariables : {} } };
486
+ return new RuleBuilderRequestInitialized(this.rule, this._matchBuilderVariables);
487
+ }
488
+ if (localVariables) {
489
+ throw new Error('invalid call - localVariables cannot be used together with Response or RemotableFunction');
490
+ }
426
491
  this.rule.actions = { modifyRequest: modifyFunction };
427
492
  return new RuleBuilderRequestInitialized(this.rule, this._matchBuilderVariables);
428
493
  }
429
- modifyResponse(modifyFunction) {
494
+ modifyResponse(modifyFunction, localVariables) {
495
+ if (typeof modifyFunction === 'function') {
496
+ this.rule.actions = { modifyResponse: { localFn: modifyFunction, localVariables: localVariables !== null && localVariables !== void 0 ? localVariables : {} } };
497
+ return this.rule;
498
+ }
499
+ if (localVariables) {
500
+ throw new Error('invalid call - localVariables cannot be used together with Response or RemotableFunction');
501
+ }
430
502
  this.rule.actions = { modifyResponse: modifyFunction };
431
503
  return this.rule;
432
504
  }
433
505
  }
434
506
  class RuleBuilderRequestInitialized extends RuleBuilderBase {
435
- modifyResponse(modifyFunction) {
507
+ modifyResponse(modifyFunction, localVariables) {
436
508
  if (!this.rule.actions) {
437
509
  throw new Error('rule.actions not defined - builder implementation error');
438
510
  }
511
+ if (typeof modifyFunction === 'function') {
512
+ this.rule.actions = { modifyResponse: { localFn: modifyFunction, localVariables: localVariables !== null && localVariables !== void 0 ? localVariables : {} } };
513
+ return this.rule;
514
+ }
515
+ if (localVariables) {
516
+ throw new Error('invalid call - localVariables cannot be used together with Response or RemotableFunction');
517
+ }
439
518
  this.rule.actions.modifyResponse = modifyFunction;
440
519
  return this.rule;
441
520
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stuntman/client",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Stuntman - HTTP proxy / mock API client",
5
5
  "main": "dist/index.js",
6
6
  "repository": {
@@ -37,8 +37,12 @@
37
37
  "uuid": "9.0.0"
38
38
  },
39
39
  "devDependencies": {
40
+ "@jest/globals": "29.4.3",
40
41
  "@types/serialize-javascript": "5.0.2",
41
- "@types/uuid": "9.0.0"
42
+ "@types/uuid": "9.0.0",
43
+ "jest": "29.4.3",
44
+ "ts-jest": "29.0.5",
45
+ "typescript": "4.9.5"
42
46
  },
43
47
  "files": [
44
48
  "dist/",
@@ -47,10 +51,10 @@
47
51
  "CHANGELOG.md"
48
52
  ],
49
53
  "scripts": {
50
- "test": "echo \"Error: no test specified\" && exit 1",
54
+ "test": "SUPPRESS_NO_CONFIG_WARNING=1 jest",
51
55
  "clean": "rm -fr dist",
52
56
  "build": "tsc",
53
57
  "lint": "prettier --check . && eslint . --ext ts",
54
- "lint:fix": "prettier --write ./src && eslint ./src --ext ts --fix"
58
+ "lint:fix": "prettier --write ./{src,test} && eslint ./{src,test} --ext ts --fix"
55
59
  }
56
60
  }