• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

andrzej-woof / stuntman / 8908932552

01 May 2024 11:48AM UTC coverage: 81.27%. Remained the same
8908932552

push

github

andrzej-woof
and again

611 of 668 branches covered (91.47%)

Branch coverage included in aggregate %.

2218 of 2813 relevant lines covered (78.85%)

13.32 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

94.29
/packages/client/src/ruleBuilder.ts
1
import { v4 as uuidv4 } from 'uuid';
1✔
2
import type * as Stuntman from '@stuntman/shared';
1✔
3
import { DEFAULT_RULE_PRIORITY, DEFAULT_RULE_TTL_SECONDS, MAX_RULE_TTL_SECONDS, MIN_RULE_TTL_SECONDS } from '@stuntman/shared';
1✔
4

1✔
5
type KeyValueMatcher = string | RegExp | { key: string; value?: string | RegExp };
1✔
6
type ObjectValueMatcher = string | RegExp | number | boolean | null;
1✔
7
type ObjectKeyValueMatcher = { key: string; value?: ObjectValueMatcher };
1✔
8
type GQLRequestMatcher = {
1✔
9
    operationName?: string | RegExp;
1✔
10
    variables?: ObjectKeyValueMatcher[];
1✔
11
    query?: string | RegExp;
1✔
12
    type?: 'query' | 'mutation';
1✔
13
    methodName?: Stuntman.HttpMethod | RegExp;
1✔
14
};
1✔
15

1✔
16
type MatchBuilderVariables = {
1✔
17
    filter?: string | RegExp;
1✔
18
    hostname?: string | RegExp;
1✔
19
    pathname?: string | RegExp;
1✔
20
    port?: number | string | RegExp;
1✔
21
    searchParams?: KeyValueMatcher[];
1✔
22
    headers?: KeyValueMatcher[];
1✔
23
    bodyText?: string | RegExp | null;
1✔
24
    bodyJson?: ObjectKeyValueMatcher[];
1✔
25
    bodyGql?: GQLRequestMatcher;
1✔
26
    jwt?: ObjectKeyValueMatcher[];
1✔
27
};
1✔
28

1✔
29
// TODO add fluent match on multipart from data
1✔
30
function matchFunction(req: Stuntman.Request): Stuntman.RuleMatchResult {
102✔
31
    // eslint-disable-next-line @typescript-eslint/ban-ts-comment
102✔
32
    // @ts-ignore
102✔
33
    const localMatchBuilderVariables: MatchBuilderVariables = this?.matchBuilderVariables ?? matchBuilderVariables;
102!
34
    const ___url = new URL(req.url);
102✔
35
    const ___headers = req.rawHeaders;
102✔
36
    const arrayIndexerRegex = /\[(?<arrayIndex>[0-9]*)\]/i;
102✔
37
    const matchObject = (
102✔
38
        obj: any,
297✔
39
        path: string,
297✔
40
        value?: string | RegExp | number | boolean | null,
297✔
41
        parentPath?: string
297✔
42
    ): Exclude<Stuntman.RuleMatchResult, boolean> => {
297✔
43
        if (!obj) {
297✔
44
            return { result: false, description: `${parentPath} is falsey` };
1✔
45
        }
1✔
46
        const [rawKey, ...rest] = path.split('.');
296✔
47
        const key = (rawKey ?? '').replace(arrayIndexerRegex, '');
297!
48
        const shouldBeArray = rawKey ? arrayIndexerRegex.test(rawKey) : false;
297✔
49
        const arrayIndex =
297✔
50
            rawKey && (arrayIndexerRegex.exec(rawKey)?.groups?.arrayIndex || '').length > 0
297✔
51
                ? Number(arrayIndexerRegex.exec(rawKey)?.groups?.arrayIndex)
297✔
52
                : Number.NaN;
297✔
53
        const actualValue = key ? obj[key] : obj;
297✔
54
        const currentPath = `${parentPath ? `${parentPath}.` : ''}${rawKey}`;
297✔
55
        if (value === undefined && actualValue === undefined) {
297✔
56
            return { result: false, description: `${currentPath}=undefined` };
4✔
57
        }
4✔
58
        if (rest.length === 0) {
297✔
59
            if (
122✔
60
                shouldBeArray &&
122✔
61
                (!Array.isArray(actualValue) || (Number.isInteger(arrayIndex) && actualValue.length <= Number(arrayIndex)))
26✔
62
            ) {
122✔
63
                return { result: false, description: `${currentPath} empty array` };
2✔
64
            }
2✔
65
            if (value === undefined) {
122✔
66
                const result = shouldBeArray
15✔
67
                    ? !Number.isInteger(arrayIndex) || actualValue.length >= Number(arrayIndex)
15!
68
                    : actualValue !== undefined;
15✔
69
                return { result, description: `${currentPath} === undefined` };
15✔
70
            }
15✔
71
            if (!shouldBeArray) {
122✔
72
                const result = value instanceof RegExp ? value.test(actualValue) : value === actualValue;
81✔
73
                return { result, description: `${currentPath} === "${actualValue}"` };
81✔
74
            }
81✔
75
        }
122✔
76
        if (shouldBeArray) {
297✔
77
            if (Number.isInteger(arrayIndex)) {
24✔
78
                return matchObject(actualValue[Number(arrayIndex)], rest.join('.'), value, currentPath);
9✔
79
            }
9✔
80
            const hasArrayMatch = (actualValue as Array<any>).some(
15✔
81
                (arrayValue) => matchObject(arrayValue, rest.join('.'), value, currentPath).result
15✔
82
            );
15✔
83
            return { result: hasArrayMatch, description: `array match ${currentPath}` };
15✔
84
        }
15✔
85
        if (typeof actualValue !== 'object') {
297✔
86
            return { result: false, description: `${currentPath} not an object` };
1✔
87
        }
1✔
88
        return matchObject(actualValue, rest.join('.'), value, currentPath);
169✔
89
    };
102✔
90

102✔
91
    const ___matchesValue = (matcher: number | string | RegExp | undefined, value?: string | number): boolean => {
102✔
92
        if (matcher === undefined) {
361✔
93
            return true;
306✔
94
        }
306✔
95
        if (typeof matcher !== 'string' && !(matcher instanceof RegExp) && typeof matcher !== 'number') {
361✔
96
            throw new Error('invalid matcher');
1✔
97
        }
1✔
98
        if (typeof matcher === 'string' && matcher !== value) {
361✔
99
            return false;
14✔
100
        }
14✔
101
        if (matcher instanceof RegExp && (typeof value !== 'string' || !matcher.test(value))) {
361✔
102
            return false;
9✔
103
        }
9✔
104
        if (typeof matcher === 'number' && (typeof value !== 'number' || matcher !== value)) {
361!
105
            return false;
1✔
106
        }
1✔
107
        return true;
30✔
108
    };
102✔
109
    if (!___matchesValue(localMatchBuilderVariables.filter, req.url)) {
102✔
110
        return {
3✔
111
            result: false,
3✔
112
            description: `url ${req.url} doesn't match ${localMatchBuilderVariables.filter?.toString()}`,
3✔
113
        };
3✔
114
    }
3✔
115
    if (!___matchesValue(localMatchBuilderVariables.hostname, ___url.hostname)) {
102✔
116
        return {
5✔
117
            result: false,
5✔
118
            description: `hostname ${___url.hostname} doesn't match ${localMatchBuilderVariables.hostname?.toString()}`,
5✔
119
        };
5✔
120
    }
5✔
121
    if (!___matchesValue(localMatchBuilderVariables.pathname, ___url.pathname)) {
102✔
122
        return {
2✔
123
            result: false,
2✔
124
            description: `pathname ${___url.pathname} doesn't match ${localMatchBuilderVariables.pathname?.toString()}`,
2✔
125
        };
2✔
126
    }
2✔
127
    if (localMatchBuilderVariables.port) {
102✔
128
        const port = ___url.port && ___url.port !== '' ? ___url.port : ___url.protocol === 'https:' ? '443' : '80';
9✔
129
        if (
9✔
130
            !___matchesValue(
9✔
131
                localMatchBuilderVariables.port instanceof RegExp
9✔
132
                    ? localMatchBuilderVariables.port
9✔
133
                    : `${localMatchBuilderVariables.port}`,
9✔
134
                port
9✔
135
            )
9✔
136
        ) {
9✔
137
            return {
4✔
138
                result: false,
4✔
139
                description: `port ${port} doesn't match ${localMatchBuilderVariables.port?.toString()}`,
4✔
140
            };
4✔
141
        }
4✔
142
    }
9✔
143
    if (localMatchBuilderVariables.searchParams) {
102✔
144
        for (const searchParamMatcher of localMatchBuilderVariables.searchParams) {
9✔
145
            if (typeof searchParamMatcher === 'string') {
13✔
146
                const result = ___url.searchParams.has(searchParamMatcher);
4✔
147
                if (!result) {
4✔
148
                    return { result, description: `searchParams.has("${searchParamMatcher}")` };
1✔
149
                }
1✔
150
                continue;
3✔
151
            }
3✔
152
            if (searchParamMatcher instanceof RegExp) {
13✔
153
                const result = Array.from(___url.searchParams.keys()).some((key) => searchParamMatcher.test(key));
3✔
154
                if (!result) {
3✔
155
                    return { result, description: `searchParams.keys() matches ${searchParamMatcher.toString()}` };
1✔
156
                }
1✔
157
                continue;
2✔
158
            }
2✔
159
            if (!___url.searchParams.has(searchParamMatcher.key)) {
13✔
160
                return { result: false, description: `searchParams.has("${searchParamMatcher.key}")` };
1✔
161
            }
1✔
162
            if (searchParamMatcher.value) {
5✔
163
                const value = ___url.searchParams.get(searchParamMatcher.key);
5✔
164
                if (!___matchesValue(searchParamMatcher.value, value as string)) {
5✔
165
                    return {
1✔
166
                        result: false,
1✔
167
                        description: `searchParams.get("${searchParamMatcher.key}") = "${searchParamMatcher.value}"`,
1✔
168
                    };
1✔
169
                }
1✔
170
            }
5✔
171
        }
13✔
172
    }
5✔
173
    if (localMatchBuilderVariables.headers) {
102✔
174
        for (const headerMatcher of localMatchBuilderVariables.headers) {
11✔
175
            if (typeof headerMatcher === 'string') {
17✔
176
                const result = ___headers.has(headerMatcher);
4✔
177
                if (result) {
4✔
178
                    continue;
3✔
179
                }
3✔
180
                return { result: false, description: `headers.has("${headerMatcher}")` };
1✔
181
            }
1✔
182
            if (headerMatcher instanceof RegExp) {
17✔
183
                const result = ___headers.toHeaderPairs().some(([key]) => headerMatcher.test(key));
4✔
184
                if (result) {
4✔
185
                    continue;
3✔
186
                }
3✔
187
                return { result: false, description: `headers.keys matches ${headerMatcher.toString()}` };
1✔
188
            }
1✔
189
            if (!___headers.has(headerMatcher.key)) {
17✔
190
                return { result: false, description: `headers.has("${headerMatcher.key}")` };
1✔
191
            }
1✔
192
            if (headerMatcher.value) {
8✔
193
                const value = ___headers.get(headerMatcher.key);
8✔
194
                if (!___matchesValue(headerMatcher.value, value)) {
8✔
195
                    return {
3✔
196
                        result: false,
3✔
197
                        description: `headerMatcher.get("${headerMatcher.key}") = "${headerMatcher.value}"`,
3✔
198
                    };
3✔
199
                }
3✔
200
            }
8✔
201
        }
17✔
202
    }
5✔
203
    if (localMatchBuilderVariables.jwt) {
102!
204
        if (!req.jwt) {
×
205
            return { result: false, description: `no jwt found on request` };
×
206
        }
×
207
        for (const jsonMatcher of Array.isArray(localMatchBuilderVariables.jwt)
×
208
            ? localMatchBuilderVariables.jwt
×
209
            : [localMatchBuilderVariables.jwt]) {
×
210
            const matchObjectResult = matchObject(req.jwt, jsonMatcher.key, jsonMatcher.value);
×
211
            if (!matchObjectResult.result) {
×
212
                return { result: false, description: `jwt $.${jsonMatcher.key} != "${jsonMatcher.value}"` };
×
213
            }
×
214
        }
×
215
    }
×
216
    if (localMatchBuilderVariables.bodyText === null && !!req.body) {
102✔
217
        return { result: false, description: `empty body` };
2✔
218
    }
2✔
219
    if (localMatchBuilderVariables.bodyText) {
102✔
220
        if (!req.body) {
10✔
221
            return { result: false, description: `empty body` };
4✔
222
        }
4✔
223
        if (localMatchBuilderVariables.bodyText instanceof RegExp) {
10✔
224
            if (!___matchesValue(localMatchBuilderVariables.bodyText, req.body)) {
3✔
225
                return {
2✔
226
                    result: false,
2✔
227
                    description: `body text doesn't match ${localMatchBuilderVariables.bodyText.toString()}`,
2✔
228
                };
2✔
229
            }
2✔
230
        } else if (!req.body.includes(localMatchBuilderVariables.bodyText)) {
3✔
231
            return {
2✔
232
                result: false,
2✔
233
                description: `body text doesn't include "${localMatchBuilderVariables.bodyText}"`,
2✔
234
            };
2✔
235
        }
2✔
236
    }
10✔
237
    if (localMatchBuilderVariables.bodyJson) {
102✔
238
        let json: any;
21✔
239
        try {
21✔
240
            json = JSON.parse(req.body);
21✔
241
        } catch (kiss) {
21✔
242
            return { result: false, description: `unparseable json` };
2✔
243
        }
2✔
244
        if (!json) {
21✔
245
            return { result: false, description: `empty json object` };
1✔
246
        }
1✔
247
        for (const jsonMatcher of Array.isArray(localMatchBuilderVariables.bodyJson)
18✔
248
            ? localMatchBuilderVariables.bodyJson
18✔
249
            : [localMatchBuilderVariables.bodyJson]) {
21!
250
            const matchObjectResult = matchObject(json, jsonMatcher.key, jsonMatcher.value);
82✔
251
            if (!matchObjectResult.result) {
82✔
252
                return { result: false, description: `$.${jsonMatcher.key} != "${jsonMatcher.value}"` };
16✔
253
            }
16✔
254
        }
82✔
255
    }
2✔
256
    if (localMatchBuilderVariables.bodyGql) {
102✔
257
        if (!req.gqlBody) {
13✔
258
            return { result: false, description: `not a gql body` };
1✔
259
        }
1✔
260
        if (!___matchesValue(localMatchBuilderVariables.bodyGql.methodName, req.gqlBody.methodName)) {
13!
261
            return {
×
262
                result: false,
×
263
                description: `methodName "${localMatchBuilderVariables.bodyGql.methodName}" !== "${req.gqlBody.methodName}"`,
×
264
            };
×
265
        }
×
266
        if (!___matchesValue(localMatchBuilderVariables.bodyGql.operationName, req.gqlBody.operationName)) {
13✔
267
            return {
2✔
268
                result: false,
2✔
269
                description: `operationName "${localMatchBuilderVariables.bodyGql.operationName}" !== "${req.gqlBody.operationName}"`,
2✔
270
            };
2✔
271
        }
2✔
272
        if (!___matchesValue(localMatchBuilderVariables.bodyGql.query, req.gqlBody.query)) {
13✔
273
            return {
1✔
274
                result: false,
1✔
275
                description: `query "${localMatchBuilderVariables.bodyGql.query}" !== "${req.gqlBody.query}"`,
1✔
276
            };
1✔
277
        }
1✔
278
        if (!___matchesValue(localMatchBuilderVariables.bodyGql.type, req.gqlBody.type)) {
13✔
279
            return {
1✔
280
                result: false,
1✔
281
                description: `type "${localMatchBuilderVariables.bodyGql.type}" !== "${req.gqlBody.type}"`,
1✔
282
            };
1✔
283
        }
1✔
284
        if (localMatchBuilderVariables.bodyGql.variables) {
13✔
285
            for (const jsonMatcher of Array.isArray(localMatchBuilderVariables.bodyGql.variables)
6✔
286
                ? localMatchBuilderVariables.bodyGql.variables
6✔
287
                : [localMatchBuilderVariables.bodyGql.variables]) {
6!
288
                const matchObjectResult = matchObject(req.gqlBody.variables, jsonMatcher.key, jsonMatcher.value);
6✔
289
                if (!matchObjectResult.result) {
6✔
290
                    return {
3✔
291
                        result: false,
3✔
292
                        description: `GQL variable ${jsonMatcher.key} != "${jsonMatcher.value}". Detail: ${matchObjectResult.description}`,
3✔
293
                    };
3✔
294
                }
3✔
295
            }
6✔
296
        }
3✔
297
    }
13✔
298
    return { result: true, description: 'match' };
40✔
299
}
40✔
300

1✔
301
class RuleBuilderBaseBase {
1✔
302
    protected rule: Stuntman.SerializableRule;
141✔
303
    protected _matchBuilderVariables: MatchBuilderVariables;
141✔
304

141✔
305
    constructor(rule?: Stuntman.SerializableRule, _matchBuilderVariables?: MatchBuilderVariables) {
141✔
306
        this._matchBuilderVariables = _matchBuilderVariables || {};
141✔
307
        this.rule = rule || {
141✔
308
            id: uuidv4(),
75✔
309
            ttlSeconds: DEFAULT_RULE_TTL_SECONDS,
75✔
310
            priority: DEFAULT_RULE_PRIORITY,
75✔
311
            actions: {
75✔
312
                mockResponse: { status: 200 },
75✔
313
            },
75✔
314
            matches: {
75✔
315
                localFn: matchFunction,
75✔
316
                localVariables: { matchBuilderVariables: this._matchBuilderVariables },
75✔
317
            },
75✔
318
        };
75✔
319
    }
141✔
320
}
141✔
321

1✔
322
class RuleBuilderBase extends RuleBuilderBaseBase {
1✔
323
    limitedUse(hitCount: number) {
1✔
324
        if (this.rule.removeAfterUse) {
6✔
325
            throw new Error(`limit already set at ${this.rule.removeAfterUse}`);
2✔
326
        }
2✔
327
        if (Number.isNaN(hitCount) || !Number.isFinite(hitCount) || !Number.isInteger(hitCount) || hitCount <= 0) {
6✔
328
            throw new Error('Invalid hitCount');
2✔
329
        }
2✔
330
        this.rule.removeAfterUse = hitCount;
2✔
331
        return this;
2✔
332
    }
2✔
333

1✔
334
    singleUse() {
1✔
335
        return this.limitedUse(1);
2✔
336
    }
2✔
337

1✔
338
    storeTraffic() {
1✔
339
        this.rule.storeTraffic = true;
1✔
340
        return this;
1✔
341
    }
1✔
342

1✔
343
    disabled() {
1✔
344
        this.rule.isEnabled = false;
1✔
345
    }
1✔
346
}
1✔
347

1✔
348
class RuleBuilder extends RuleBuilderBase {
1✔
349
    raisePriority(by?: number) {
1✔
350
        if (this.rule.priority !== DEFAULT_RULE_PRIORITY) {
4✔
351
            throw new Error('you should not alter rule priority more than once');
1✔
352
        }
1✔
353
        const subtract = by ?? 1;
4✔
354
        if (subtract >= DEFAULT_RULE_PRIORITY) {
4✔
355
            throw new Error(`Unable to raise priority over the default ${DEFAULT_RULE_PRIORITY}`);
1✔
356
        }
1✔
357
        this.rule.priority = DEFAULT_RULE_PRIORITY - subtract;
2✔
358
        return this;
2✔
359
    }
2✔
360

1✔
361
    decreasePriority(by?: number) {
1✔
362
        if (this.rule.priority !== DEFAULT_RULE_PRIORITY) {
3✔
363
            throw new Error('you should not alter rule priority more than once');
1✔
364
        }
1✔
365
        const add = by ?? 1;
3✔
366
        this.rule.priority = DEFAULT_RULE_PRIORITY + add;
3✔
367
        return this;
3✔
368
    }
3✔
369

1✔
370
    customTtl(ttlSeconds: number) {
1✔
371
        if (Number.isNaN(ttlSeconds) || !Number.isInteger(ttlSeconds) || !Number.isFinite(ttlSeconds) || ttlSeconds < 0) {
4✔
372
            throw new Error('Invalid ttl');
1✔
373
        }
1✔
374
        if (ttlSeconds < MIN_RULE_TTL_SECONDS || ttlSeconds > MAX_RULE_TTL_SECONDS) {
4✔
375
            throw new Error(
2✔
376
                `ttl of ${ttlSeconds} seconds is outside range min: ${MIN_RULE_TTL_SECONDS}, max:${MAX_RULE_TTL_SECONDS}`
2✔
377
            );
2✔
378
        }
2✔
379
        this.rule.ttlSeconds = ttlSeconds;
1✔
380
        return this;
1✔
381
    }
1✔
382

1✔
383
    customId(id: string) {
1✔
384
        this.rule.id = id;
1✔
385
        return this;
1✔
386
    }
1✔
387

1✔
388
    onRequestTo(filter: string | RegExp): RuleBuilderInitialized {
1✔
389
        this._matchBuilderVariables.filter = filter;
2✔
390
        return new RuleBuilderInitialized(this.rule, this._matchBuilderVariables);
2✔
391
    }
2✔
392

1✔
393
    onRequestToHostname(hostname: string | RegExp): RuleBuilderInitialized {
1✔
394
        this._matchBuilderVariables.hostname = hostname;
3✔
395
        return new RuleBuilderInitialized(this.rule, this._matchBuilderVariables);
3✔
396
    }
3✔
397

1✔
398
    onRequestToPathname(pathname: string | RegExp): RuleBuilderInitialized {
1✔
399
        this._matchBuilderVariables.pathname = pathname;
3✔
400
        return new RuleBuilderInitialized(this.rule, this._matchBuilderVariables);
3✔
401
    }
3✔
402

1✔
403
    onRequestToPort(port: string | number | RegExp): RuleBuilderInitialized {
1✔
404
        this._matchBuilderVariables.port = port;
4✔
405
        return new RuleBuilderInitialized(this.rule, this._matchBuilderVariables);
4✔
406
    }
4✔
407

1✔
408
    onAnyRequest(): RuleBuilderInitialized {
1✔
409
        return new RuleBuilderInitialized(this.rule, this._matchBuilderVariables);
51✔
410
    }
51✔
411
}
1✔
412

1✔
413
class RuleBuilderRequestInitialized extends RuleBuilderBase {
1✔
414
    modifyResponse(
1✔
415
        modifyFunction: Stuntman.ResponseManipulationFn | Stuntman.RemotableFunction<Stuntman.ResponseManipulationFn>,
4✔
416
        localVariables?: Stuntman.LocalVariables
4✔
417
    ): Stuntman.SerializableRule {
4✔
418
        if (!this.rule.actions) {
4!
419
            throw new Error('rule.actions not defined - builder implementation error');
×
420
        }
×
421
        if (typeof modifyFunction === 'function') {
4✔
422
            this.rule.actions.modifyResponse = { localFn: modifyFunction, localVariables: localVariables ?? {} };
2✔
423
            return this.rule;
2✔
424
        }
2✔
425
        if (localVariables) {
4✔
426
            throw new Error('invalid call - localVariables cannot be used together with Response or RemotableFunction');
1✔
427
        }
1✔
428
        this.rule.actions.modifyResponse = modifyFunction;
1✔
429
        return this.rule;
1✔
430
    }
1✔
431
}
1✔
432

1✔
433
class RuleBuilderInitialized extends RuleBuilderRequestInitialized {
1✔
434
    withHostname(hostname: string | RegExp) {
1✔
435
        if (this._matchBuilderVariables.hostname) {
2✔
436
            throw new Error('hostname already set');
1✔
437
        }
1✔
438
        this._matchBuilderVariables.hostname = hostname;
1✔
439
        return this;
1✔
440
    }
1✔
441

1✔
442
    withPathname(pathname: string | RegExp) {
1✔
443
        if (this._matchBuilderVariables.pathname) {
2✔
444
            throw new Error('pathname already set');
1✔
445
        }
1✔
446
        this._matchBuilderVariables.pathname = pathname;
1✔
447
        return this;
1✔
448
    }
1✔
449

1✔
450
    withPort(port: number | string | RegExp) {
1✔
451
        if (this._matchBuilderVariables.port) {
2✔
452
            throw new Error('port already set');
1✔
453
        }
1✔
454
        this._matchBuilderVariables.port = port;
1✔
455
        return this;
1✔
456
    }
1✔
457

1✔
458
    withSearchParam(key: string | RegExp): RuleBuilderInitialized;
1✔
459
    withSearchParam(key: string, value?: string | RegExp): RuleBuilderInitialized;
1✔
460
    withSearchParam(key: string | RegExp, value?: string | RegExp): RuleBuilderInitialized {
1✔
461
        if (!this._matchBuilderVariables.searchParams) {
15✔
462
            this._matchBuilderVariables.searchParams = [];
9✔
463
        }
9✔
464
        if (!key) {
15✔
465
            throw new Error('key cannot be empty');
1✔
466
        }
1✔
467
        if (!value) {
15✔
468
            this._matchBuilderVariables.searchParams.push(key);
7✔
469
            return this;
7✔
470
        }
7✔
471
        if (key instanceof RegExp) {
15✔
472
            throw new Error('Unsupported regex param key with value');
1✔
473
        }
1✔
474
        this._matchBuilderVariables.searchParams.push({ key, value });
6✔
475
        return this;
6✔
476
    }
6✔
477

1✔
478
    withSearchParams(params: KeyValueMatcher[]): RuleBuilderInitialized {
1✔
479
        if (!this._matchBuilderVariables.searchParams) {
1✔
480
            this._matchBuilderVariables.searchParams = [];
1✔
481
        }
1✔
482
        for (const param of params) {
1✔
483
            if (typeof param === 'string' || param instanceof RegExp) {
5✔
484
                this.withSearchParam(param);
2✔
485
            } else {
5✔
486
                this.withSearchParam(param.key, param.value);
3✔
487
            }
3✔
488
        }
5✔
489
        return this;
1✔
490
    }
1✔
491

1✔
492
    withHeader(key: string | RegExp): RuleBuilderInitialized;
1✔
493
    withHeader(key: string, value?: string | RegExp): RuleBuilderInitialized;
1✔
494
    withHeader(key: string | RegExp, value?: string | RegExp): RuleBuilderInitialized {
1✔
495
        if (!this._matchBuilderVariables.headers) {
16✔
496
            this._matchBuilderVariables.headers = [];
6✔
497
        }
6✔
498
        if (!key) {
16✔
499
            throw new Error('key cannot be empty');
1✔
500
        }
1✔
501
        if (!value) {
16✔
502
            this._matchBuilderVariables.headers.push(key);
7✔
503
            return this;
7✔
504
        }
7✔
505
        if (key instanceof RegExp) {
16✔
506
            throw new Error('Unsupported regex param key with value');
1✔
507
        }
1✔
508
        this._matchBuilderVariables.headers.push({ key, value });
7✔
509
        return this;
7✔
510
    }
7✔
511

1✔
512
    withHeaders(...headers: KeyValueMatcher[]): RuleBuilderInitialized {
1✔
513
        if (!this._matchBuilderVariables.headers) {
1✔
514
            this._matchBuilderVariables.headers = [];
1✔
515
        }
1✔
516
        for (const header of headers) {
1✔
517
            if (typeof header === 'string' || header instanceof RegExp) {
5✔
518
                this.withHeader(header);
2✔
519
            } else {
5✔
520
                this.withHeader(header.key, header.value);
3✔
521
            }
3✔
522
        }
5✔
523
        return this;
1✔
524
    }
1✔
525

1✔
526
    withBodyText(includes: string): RuleBuilderInitialized;
1✔
527
    withBodyText(matches: RegExp): RuleBuilderInitialized;
1✔
528
    withBodyText(includesOrMatches: string | RegExp): RuleBuilderInitialized {
1✔
529
        if (this._matchBuilderVariables.bodyText) {
6✔
530
            throw new Error('bodyText already set');
1✔
531
        }
1✔
532
        if (this._matchBuilderVariables.bodyText === null) {
6✔
533
            throw new Error('cannot use both withBodyText and withoutBody');
1✔
534
        }
1✔
535
        this._matchBuilderVariables.bodyText = includesOrMatches;
4✔
536
        return this;
4✔
537
    }
4✔
538

1✔
539
    withoutBody(): RuleBuilderInitialized {
1✔
540
        if (this._matchBuilderVariables.bodyText) {
3✔
541
            throw new Error('cannot use both withBodyText and withoutBody');
1✔
542
        }
1✔
543
        this._matchBuilderVariables.bodyText = null;
2✔
544
        return this;
2✔
545
    }
2✔
546

1✔
547
    withJwt(hasKey: string): RuleBuilderInitialized;
1✔
548
    withJwt(hasKey: string, withValue: ObjectValueMatcher): RuleBuilderInitialized;
1✔
549
    withJwt(matches: ObjectKeyValueMatcher): RuleBuilderInitialized;
1✔
550
    withJwt(keyOrMatcher: string | ObjectKeyValueMatcher, withValue?: ObjectValueMatcher): RuleBuilderInitialized {
1✔
551
        const keyRegex = /^(?:(?:[a-z0-9_-]+)|(?:\[[0-9]*\]))(?:\.(?:(?:[a-z0-9_-]+)|(?:\[[0-9]*\])))*$/i;
×
552
        if (!this._matchBuilderVariables.jwt) {
×
553
            this._matchBuilderVariables.jwt = [];
×
554
        }
×
555
        if (typeof keyOrMatcher === 'string') {
×
556
            if (!keyRegex.test(keyOrMatcher)) {
×
557
                throw new Error(`invalid key "${keyOrMatcher}"`);
×
558
            }
×
559
            if (withValue === undefined) {
×
560
                this._matchBuilderVariables.jwt.push({ key: keyOrMatcher });
×
561
            } else {
×
562
                this._matchBuilderVariables.jwt.push({ key: keyOrMatcher, value: withValue });
×
563
            }
×
564
            return this;
×
565
        }
×
566
        if (withValue !== undefined) {
×
567
            throw new Error('invalid usage');
×
568
        }
×
569
        if (!keyRegex.test(keyOrMatcher.key)) {
×
570
            throw new Error(`invalid key "${keyOrMatcher}"`);
×
571
        }
×
572
        this._matchBuilderVariables.jwt.push(keyOrMatcher);
×
573
        return this;
×
574
    }
×
575

1✔
576
    withBodyJson(hasKey: string): RuleBuilderInitialized;
1✔
577
    withBodyJson(hasKey: string, withValue: ObjectValueMatcher): RuleBuilderInitialized;
1✔
578
    withBodyJson(matches: ObjectKeyValueMatcher): RuleBuilderInitialized;
1✔
579
    withBodyJson(keyOrMatcher: string | ObjectKeyValueMatcher, withValue?: ObjectValueMatcher): RuleBuilderInitialized {
1✔
580
        const keyRegex = /^(?:(?:[a-z0-9_-]+)|(?:\[[0-9]*\]))(?:\.(?:(?:[a-z0-9_-]+)|(?:\[[0-9]*\])))*$/i;
13✔
581
        if (!this._matchBuilderVariables.bodyJson) {
13✔
582
            this._matchBuilderVariables.bodyJson = [];
1✔
583
        }
1✔
584
        if (typeof keyOrMatcher === 'string') {
13✔
585
            if (!keyRegex.test(keyOrMatcher)) {
10✔
586
                throw new Error(`invalid key "${keyOrMatcher}"`);
1✔
587
            }
1✔
588
            if (withValue === undefined) {
10✔
589
                this._matchBuilderVariables.bodyJson.push({ key: keyOrMatcher });
1✔
590
            } else {
10✔
591
                this._matchBuilderVariables.bodyJson.push({ key: keyOrMatcher, value: withValue });
8✔
592
            }
8✔
593
            return this;
9✔
594
        }
9✔
595
        if (withValue !== undefined) {
13✔
596
            throw new Error('invalid usage');
1✔
597
        }
1✔
598
        if (!keyRegex.test(keyOrMatcher.key)) {
13✔
599
            throw new Error(`invalid key "${keyOrMatcher}"`);
1✔
600
        }
1✔
601
        this._matchBuilderVariables.bodyJson.push(keyOrMatcher);
1✔
602
        return this;
1✔
603
    }
1✔
604

1✔
605
    withBodyGql(gqlMatcher: GQLRequestMatcher): RuleBuilderInitialized {
1✔
606
        if (this._matchBuilderVariables.bodyGql) {
14✔
607
            throw new Error('gqlMatcher already set');
1✔
608
        }
1✔
609
        this._matchBuilderVariables.bodyGql = gqlMatcher;
13✔
610
        return this;
13✔
611
    }
13✔
612

1✔
613
    proxyPass(): Stuntman.SerializableRule {
1✔
614
        this.rule.actions = { proxyPass: true };
1✔
615
        return this.rule;
1✔
616
    }
1✔
617

1✔
618
    mockResponse(staticResponse: Stuntman.Response): Stuntman.SerializableRule;
1✔
619
    mockResponse(generationFunction: Stuntman.RemotableFunction<Stuntman.ResponseGenerationFn>): Stuntman.SerializableRule;
1✔
620
    mockResponse(localFn: Stuntman.ResponseGenerationFn, localVariables?: Stuntman.LocalVariables): Stuntman.SerializableRule;
1✔
621
    mockResponse(
1✔
622
        response: Stuntman.Response | Stuntman.RemotableFunction<Stuntman.ResponseGenerationFn> | Stuntman.ResponseGenerationFn,
3✔
623
        localVariables?: Stuntman.LocalVariables
3✔
624
    ): Stuntman.SerializableRule {
3✔
625
        if (typeof response === 'function') {
3✔
626
            this.rule.actions = { mockResponse: { localFn: response, localVariables: localVariables ?? {} } };
1✔
627
            return this.rule;
1✔
628
        }
1✔
629
        if (localVariables) {
3✔
630
            throw new Error('invalid call - localVariables cannot be used together with Response or RemotableFunction');
1✔
631
        }
1✔
632
        this.rule.actions = { mockResponse: response };
1✔
633
        return this.rule;
1✔
634
    }
1✔
635

1✔
636
    modifyRequest(
1✔
637
        modifyFunction: Stuntman.RequestManipulationFn | Stuntman.RemotableFunction<Stuntman.RequestManipulationFn>,
4✔
638
        localVariables?: Stuntman.LocalVariables
4✔
639
    ): RuleBuilderRequestInitialized {
4✔
640
        if (typeof modifyFunction === 'function') {
4✔
641
            this.rule.actions = { modifyRequest: { localFn: modifyFunction, localVariables: localVariables ?? {} } };
2✔
642
            return new RuleBuilderRequestInitialized(this.rule, this._matchBuilderVariables);
2✔
643
        }
2✔
644
        if (localVariables) {
4✔
645
            throw new Error('invalid call - localVariables cannot be used together with Response or RemotableFunction');
1✔
646
        }
1✔
647
        this.rule.actions = { modifyRequest: modifyFunction };
1✔
648
        return new RuleBuilderRequestInitialized(this.rule, this._matchBuilderVariables);
1✔
649
    }
1✔
650

1✔
651
    override modifyResponse(
1✔
652
        modifyFunction: Stuntman.ResponseManipulationFn | Stuntman.RemotableFunction<Stuntman.ResponseManipulationFn>,
4✔
653
        localVariables?: Stuntman.LocalVariables
4✔
654
    ): Stuntman.SerializableRule {
4✔
655
        if (!this.rule.actions) {
4✔
656
            this.rule.actions = { proxyPass: true };
1✔
657
        }
1✔
658
        return super.modifyResponse(modifyFunction, localVariables);
4✔
659
    }
4✔
660
}
1✔
661

1✔
662
export const ruleBuilder = () => new RuleBuilder();
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc