-
Notifications
You must be signed in to change notification settings - Fork 1.2k
/
utils.ts
362 lines (321 loc) · 11.3 KB
/
utils.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
import { env } from "./env";
/**
* A custom error type for failed pipeline requests.
*/
export class RecorderError extends Error {
constructor(message: string, public statusCode?: number) {
super(message);
this.name = "RecorderError";
this.statusCode = statusCode;
}
}
export type RecordingState = "started" | "stopped";
/**
* Helper class to manage the recording state to make sure the proxy-tool is not flooded with unintended requests.
*/
export class RecordingStateManager {
private currentState: RecordingState = "stopped";
/**
* validateState
*/
private validateState(nextState: RecordingState) {
if (nextState === "started") {
if (this.state === "started") {
throw new RecorderError("Already started, should not have called start again.");
}
}
if (nextState === "stopped") {
if (this.state === "stopped") {
throw new RecorderError("Already stopped, should not have called stop again.");
}
}
}
public get state(): RecordingState {
return this.currentState;
}
public set state(nextState: RecordingState) {
// Validate state transition
this.validateState(nextState);
this.currentState = nextState;
}
}
/**
* Keywords that should be passed as part of the headers(except for "Reset") to the proxy-tool to be able to leverage the sanitizer.
*
* "x-abstraction-identifier" - header
*/
export type ProxyToolSanitizers =
| "GeneralRegexSanitizer"
| "GeneralStringSanitizer"
| "RemoveHeaderSanitizer"
| "BodyKeySanitizer"
| "BodyRegexSanitizer"
| "BodyStringSanitizer"
| "ContinuationSanitizer"
| "HeaderRegexSanitizer"
| "HeaderStringSanitizer"
| "OAuthResponseSanitizer"
| "UriRegexSanitizer"
| "UriStringSanitizer"
| "UriSubscriptionIdSanitizer"
| "Reset";
/**
* This sanitizer offers a general regex replace across request/response Body, Headers, and URI. For the body, this means regex applying to the raw JSON.
*/
export interface RegexSanitizer {
/**
* Set to true to show that regex replacement is to be used.
*/
regex: true;
/**
* The substitution value.
*/
value: string;
/**
* A regex. Can be defined as a simple regex replace OR if groupForReplace is set, a substitution operation.
*/
target: string;
/**
* The capture group that needs to be operated upon. Do not set if you're invoking a simple replacement operation.
*/
groupForReplace?: string;
}
/**
* A sanitizer that performs a simple find/replace based on a plain string.
*/
export interface StringSanitizer {
/**
* If regex is set to false or is not specified, plain-text matching will be performed.
*/
regex?: false;
/**
* The string to be replaced.
*/
target: string;
/**
* The value that the string should be replaced with.
*/
value: string;
}
export type FindReplaceSanitizer = RegexSanitizer | StringSanitizer;
export function isStringSanitizer(sanitizer: FindReplaceSanitizer): sanitizer is StringSanitizer {
return !sanitizer.regex;
}
/**
* This sanitizer offers regex update of a specific JTokenPath.
*
* EG: "TableName" within a json response body having its value replaced by whatever substitution is offered.
* This simply means that if you are attempting to replace a specific key wholesale, this sanitizer will be simpler
* than configuring a BodyRegexSanitizer that has to match against the full "KeyName": "Value" that is part of the json structure.
*
* Further reading is available [here](https://www.newtonsoft.com/json/help/html/SelectToken.htm#SelectTokenJSONPath).
*
* If the body is NOT a JSON object, this sanitizer will NOT be applied.
*/
type BodyKeySanitizer = {
regex?: string;
value?: string;
groupForReplace?: string;
/**
* The SelectToken path (which could possibly match multiple entries) that will be used to select JTokens for value replacement.
*/
jsonPath: string;
};
/**
* Can be used for multiple purposes:
*
* 1) To replace a key with a specific value, do not set "regex" value.
* 2) To do a simple regex replace operation, define arguments "key", "value", and "regex"
* 3) To do a targeted substitution of a specific group, define all arguments "key", "value", and "regex"
*/
export interface HeaderSanitizer {
key: string;
regex?: boolean;
target?: string;
value?: string;
groupForReplace?: string;
}
/**
* Internally,
* - connection strings are parsed and
* - each part of the connection string is mapped with its corresponding fake value
* - `generalRegexSanitizer` is applied for each of the parts with the real and fake values that are parsed
*/
export interface ConnectionStringSanitizer {
/**
* Real connection string with all the secrets
*/
actualConnString?: string;
/**
* Fake connection string - with all the parts of the connection string mapped to fake values
*/
fakeConnString: string;
}
export interface ContinuationSanitizer {
key: string;
method?: string;
resetAfterFirst: boolean;
}
export interface RemoveHeaderSanitizer {
headersForRemoval: string[];
}
/**
* Test-proxy tool supports "extensions" or "customizations" to the recording experience.
* This means that non-default sanitizations such as the generalized regex find/replace on different parts of the recordings in various ways are possible.
*/
export interface SanitizerOptions {
/**
* This sanitizer offers a general regex replace across request/response Body, Headers, and URI. For the body, this means regex applying to the raw JSON.
*/
generalSanitizers?: FindReplaceSanitizer[];
/**
* This sanitizer offers regex replace within a returned body.
*
* Specifically, this means regex applying to the raw JSON.
* If you are attempting to simply replace a specific key, the BodyKeySanitizer is probably the way to go.
*
* Regardless, there are examples present in `recorder-new/test/testProxyTests.spec.ts`.
*/
bodySanitizers?: FindReplaceSanitizer[];
/**
* Can be used for multiple purposes:
*
* 1) To replace a key with a specific value, do not set "regex" value.
* 2) To do a simple regex replace operation, define arguments "key", "value", and "regex"
* 3) To do a targeted substitution of a specific group, define all arguments "key", "value", and "regex"
*/
headerSanitizers?: HeaderSanitizer[];
/**
* General use sanitizer for cleaning URIs via regex. Runs a regex replace on the member of your choice.
*/
uriSanitizers?: FindReplaceSanitizer[];
/**
* Internally,
* - connection strings are parsed and
* - each part of the connection string is mapped with its corresponding fake value
* - `generalRegexSanitizer` is applied for each of the parts with the real and fake values that are parsed
*/
connectionStringSanitizers?: ConnectionStringSanitizer[];
/**
* This sanitizer offers regex update of a specific JTokenPath.
*
* EG: "TableName" within a json response body having its value replaced by whatever substitution is offered.
* This simply means that if you are attempting to replace a specific key wholesale, this sanitizer will be simpler
* than configuring a BodyRegexSanitizer that has to match against the full "KeyName": "Value" that is part of the json structure.
*
* Further reading is available [here](https://www.newtonsoft.com/json/help/html/SelectToken.htm#SelectTokenJSONPath).
*
* If the body is NOT a JSON object, this sanitizer will NOT be applied.
*/
bodyKeySanitizers?: BodyKeySanitizer[];
/**
* TODO
* Has a bug, not implemented fully.
*/
continuationSanitizers?: ContinuationSanitizer[];
/**
* A simple sanitizer that should be used to clean out one or multiple headers by their key.
* Removes headers from before saving a recording.
*/
removeHeaderSanitizer?: RemoveHeaderSanitizer;
/**
* TODO: To be tested with scenarios, not to be used yet.
*/
oAuthResponseSanitizer?: boolean;
/**
* This sanitizer relies on UriRegexSanitizer to replace real subscriptionIds within a URI w/ a default or configured fake value.
* This sanitizer is targeted using the regex "/subscriptions/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})". This is not a setting that can be changed for this sanitizer. For full regex support, take a look at UriRegexSanitizer. You CAN modify the value that the subscriptionId is replaced WITH however.
*/
uriSubscriptionIdSanitizer?: {
/**
* The fake subscriptionId that will be placed where the real one is in the real request. The default replacement value is "00000000-0000-0000-0000-000000000000".
*/
value: string;
};
/**
* This clears the sanitizers that are added.
*/
resetSanitizer?: boolean;
}
/**
* Used in record and playback modes. No effect in live mode.
*
* Options to be provided as part of the `recorder.start()` call.
*/
export interface RecorderStartOptions {
/**
* Used in record and playback modes. No effect in live mode.
*
* 1. The key-value pairs will be used as the environment variables in playback mode.
* 2. If the env variables are present in the recordings as plain strings, they will be replaced with the provided values in record mode
*/
envSetupForPlayback: Record<string, string>;
/**
* Used in record mode. No effect in playback and live modes.
*
* Generated recordings are updated by the "proxy-tool" based on the sanitizer options provided.
*/
sanitizerOptions?: SanitizerOptions;
}
/**
* Throws error message when the `label` is not defined when it should have been defined in the given mode.
*
* Returns true if the param exists.
*/
export function ensureExistence<T>(thing: T | undefined, label: string): thing is T {
if (!thing) {
throw new RecorderError(
`Something went wrong, ${label} should not have been undefined in "${getTestMode()}" mode.`
);
}
return true; // Since we would throw error if undefined
}
export type TestMode = "record" | "playback" | "live";
/**
* Returns the test mode.
*
* If TEST_MODE is not defined, defaults to playback.
*/
export function getTestMode(): TestMode {
if (isPlaybackMode()) {
return "playback";
}
return env.TEST_MODE as "record" | "live";
}
/** Make a lazy value that can be deferred and only computed once. */
export const once = <T>(make: () => T): (() => T) => {
let value: T;
return () => (value = value ?? make());
};
export function isRecordMode() {
return env.TEST_MODE === "record";
}
export function isLiveMode() {
return env.TEST_MODE === "live";
}
export function isPlaybackMode() {
return !isRecordMode() && !isLiveMode();
}
/**
* Loads the environment variables in both node and browser modes corresponding to the key-value pairs provided.
*
* Example-
*
* Suppose `variables` is { ACCOUNT_NAME: "my_account_name", ACCOUNT_KEY: "fake_secret" },
* `setEnvironmentVariables` loads the ACCOUNT_NAME and ACCOUNT_KEY in the environment accordingly.
*/
export function setEnvironmentVariables(variables: { [key: string]: string }) {
for (const [key, value] of Object.entries(variables)) {
env[key] = value;
}
}
/**
* Returns the environment variable. Throws error if not defined.
*/
export function assertEnvironmentVariable(variable: string): string {
const value = env[variable];
if (!value) throw new Error(`${variable} is not defined`);
return value;
}