Skip to content

Commit 11c2645

Browse files
authored
[Recorder] Add support for session-level sanitizers (Azure#21533)
The test proxy has support for adding sanitizers at two levels: at the per-recording level and at the session level. Before this PR, the JS recorder client only allowed for sanitizers to be added at the per-recording level (using the `addSanitizers` instance method on the Recorder class). This PR adds a static `Recorder.addSessionSanitizers(...)` method which allows for sanitizers to be set at the session level. These sanitizers will be applied to every recording following the call.
1 parent 93d2c21 commit 11c2645

File tree

4 files changed

+150
-58
lines changed

4 files changed

+150
-58
lines changed

sdk/test-utils/recorder/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
### Features Added
66

7+
- Add support for session-level sanitization using the `Recorder.addSessionSanitizers` static method. [#21533](https://github.com/Azure/azure-sdk-for-js/pull/21533)
8+
79
### Breaking Changes
810

911
### Bugs Fixed

sdk/test-utils/recorder/src/recorder.ts

Lines changed: 38 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ import { AdditionalPolicyConfig } from "@azure/core-client";
5050
* Other than configuring your clients, use `start`, `stop`, `addSanitizers` methods to use the recorder.
5151
*/
5252
export class Recorder {
53-
private url = "http://localhost:5000";
53+
private static url = "http://localhost:5000";
5454
public recordingId?: string;
5555
private stateManager = new RecordingStateManager();
5656
private httpClient?: HttpClient;
@@ -81,7 +81,7 @@ export class Recorder {
8181
private redirectRequest(request: WebResource | PipelineRequest): void {
8282
const upstreamUrl = new URL(request.url);
8383
const redirectedUrl = new URL(request.url);
84-
const testProxyUrl = new URL(this.url);
84+
const testProxyUrl = new URL(Recorder.url);
8585

8686
// Sometimes, due to the service returning a redirect or due to the retry policy, redirectRequest
8787
// may be called multiple times. We only want to update the request the second time if the request's
@@ -104,6 +104,7 @@ export class Recorder {
104104
redirectedUrl.host = testProxyUrl.host;
105105
redirectedUrl.port = testProxyUrl.port;
106106
redirectedUrl.protocol = testProxyUrl.protocol;
107+
107108
request.headers.set("x-recording-upstream-base-uri", upstreamUrl.toString());
108109
request.url = redirectedUrl.toString();
109110

@@ -134,7 +135,33 @@ export class Recorder {
134135
ensureExistence(this.httpClient, "this.httpClient") &&
135136
ensureExistence(this.recordingId, "this.recordingId")
136137
) {
137-
return addSanitizers(this.httpClient, this.url, this.recordingId, options);
138+
return addSanitizers(this.httpClient, Recorder.url, this.recordingId, options);
139+
}
140+
}
141+
142+
/**
143+
* addSessionSanitizers adds the sanitizers for all the following recordings which will be applied on it before being saved.
144+
* This lets you call addSessionSanitizers once (e.g. in a global before() in your tests). The sanitizers will be applied
145+
* to every subsequent test.
146+
*
147+
* Takes SanitizerOptions as the input, passes on to the proxy-tool.
148+
*
149+
* By default, it applies only to record mode.
150+
*
151+
* If you want this to be applied in a specific mode or in a combination of modes, use the "mode" argument.
152+
*/
153+
static async addSessionSanitizers(
154+
options: SanitizerOptions,
155+
mode: ("record" | "playback")[] = ["record"]
156+
): Promise<void> {
157+
if (isLiveMode()) {
158+
return;
159+
}
160+
161+
const actualTestMode = getTestMode() as "record" | "playback";
162+
if (mode.includes(actualTestMode)) {
163+
const httpClient = createDefaultHttpClient();
164+
return addSanitizers(httpClient, Recorder.url, undefined, options);
138165
}
139166
}
140167

@@ -144,7 +171,7 @@ export class Recorder {
144171
ensureExistence(this.httpClient, "this.httpClient") &&
145172
ensureExistence(this.recordingId, "this.recordingId")
146173
) {
147-
await addTransform(this.url, this.httpClient, transform, this.recordingId);
174+
await addTransform(Recorder.url, this.httpClient, transform, this.recordingId);
148175
}
149176
}
150177

@@ -162,7 +189,7 @@ export class Recorder {
162189
if (isLiveMode()) return;
163190
this.stateManager.state = "started";
164191
if (this.recordingId === undefined) {
165-
const startUri = `${this.url}${isPlaybackMode() ? paths.playback : paths.record}${
192+
const startUri = `${Recorder.url}${isPlaybackMode() ? paths.playback : paths.record}${
166193
paths.start
167194
}`;
168195
const req = createRecordingRequest(startUri, this.sessionFile, this.recordingId);
@@ -186,7 +213,7 @@ export class Recorder {
186213

187214
await handleEnvSetup(
188215
this.httpClient,
189-
this.url,
216+
Recorder.url,
190217
this.recordingId,
191218
options.envSetupForPlayback
192219
);
@@ -208,7 +235,9 @@ export class Recorder {
208235
if (isLiveMode()) return;
209236
this.stateManager.state = "stopped";
210237
if (this.recordingId !== undefined) {
211-
const stopUri = `${this.url}${isPlaybackMode() ? paths.playback : paths.record}${paths.stop}`;
238+
const stopUri = `${Recorder.url}${isPlaybackMode() ? paths.playback : paths.record}${
239+
paths.stop
240+
}`;
212241
const req = createRecordingRequest(stopUri, undefined, this.recordingId);
213242
req.headers.set("x-recording-save", "true");
214243

@@ -247,7 +276,7 @@ export class Recorder {
247276
throw new RecorderError("httpClient should be defined in playback mode");
248277
}
249278

250-
await setMatcher(this.url, this.httpClient, matcher, this.recordingId, options);
279+
await setMatcher(Recorder.url, this.httpClient, matcher, this.recordingId, options);
251280
}
252281
}
253282

@@ -257,7 +286,7 @@ export class Recorder {
257286
}
258287

259288
if (ensureExistence(this.httpClient, "this.httpClient")) {
260-
return await transformsInfo(this.httpClient, this.url, this.recordingId!);
289+
return await transformsInfo(this.httpClient, Recorder.url, this.recordingId!);
261290
}
262291

263292
throw new RecorderError("Expected httpClient to be defined");

sdk/test-utils/recorder/src/sanitizer.ts

Lines changed: 35 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import {
2222
type AddSanitizer<T> = (
2323
httpClient: HttpClient,
2424
url: string,
25-
recordingId: string,
25+
recordingId: string | undefined,
2626
sanitizer: T
2727
) => Promise<void>;
2828

@@ -32,7 +32,7 @@ type AddSanitizer<T> = (
3232
*/
3333
const pluralize =
3434
<T>(singular: AddSanitizer<T>): AddSanitizer<T[]> =>
35-
async (httpClient: HttpClient, url: string, recordingId: string, sanitizers: T[]) => {
35+
async (httpClient, url, recordingId, sanitizers) => {
3636
await Promise.all(
3737
sanitizers.map((sanitizer) => singular(httpClient, url, recordingId, sanitizer))
3838
);
@@ -43,12 +43,7 @@ const pluralize =
4343
*/
4444
const makeAddSanitizer =
4545
(sanitizerName: ProxyToolSanitizers): AddSanitizer<Record<string, unknown>> =>
46-
async (
47-
httpClient: HttpClient,
48-
url: string,
49-
recordingId: string,
50-
sanitizer: Record<string, unknown>
51-
) => {
46+
async (httpClient, url, recordingId, sanitizer) => {
5247
await addSanitizer(httpClient, url, recordingId, {
5348
sanitizer: sanitizerName,
5449
body: sanitizer,
@@ -61,7 +56,7 @@ const makeAddSanitizer =
6156
*/
6257
const makeAddBodilessSanitizer =
6358
(sanitizerName: ProxyToolSanitizers): AddSanitizer<boolean> =>
64-
async (httpClient: HttpClient, url: string, recordingId: string, enable: boolean) => {
59+
async (httpClient, url, recordingId, enable) => {
6560
if (enable) {
6661
await addSanitizer(httpClient, url, recordingId, {
6762
sanitizer: sanitizerName,
@@ -80,12 +75,7 @@ const makeAddFindReplaceSanitizer =
8075
regexSanitizerName: ProxyToolSanitizers,
8176
stringSanitizerName: ProxyToolSanitizers
8277
): AddSanitizer<FindReplaceSanitizer> =>
83-
async (
84-
httpClient: HttpClient,
85-
url: string,
86-
recordingId: string,
87-
sanitizer: FindReplaceSanitizer
88-
): Promise<void> => {
78+
async (httpClient, url, recordingId, sanitizer): Promise<void> => {
8979
if (isStringSanitizer(sanitizer)) {
9080
await addSanitizer(httpClient, url, recordingId, {
9181
sanitizer: stringSanitizerName,
@@ -112,12 +102,12 @@ const makeAddFindReplaceSanitizer =
112102
* - each part of the connection string is mapped with its corresponding fake value
113103
* - GeneralStringSanitizer is applied for each of the parts with the real and fake values that are parsed
114104
*/
115-
async function addConnectionStringSanitizer(
116-
httpClient: HttpClient,
117-
url: string,
118-
recordingId: string,
119-
{ actualConnString, fakeConnString }: ConnectionStringSanitizer
120-
): Promise<void> {
105+
const addConnectionStringSanitizer: AddSanitizer<ConnectionStringSanitizer> = async (
106+
httpClient,
107+
url,
108+
recordingId,
109+
{ actualConnString, fakeConnString }
110+
) => {
121111
if (!actualConnString) {
122112
if (!isRecordMode()) return;
123113
throw new RecorderError(
@@ -134,42 +124,42 @@ async function addConnectionStringSanitizer(
134124
return { value, target: key };
135125
}),
136126
});
137-
}
127+
};
138128

139129
/**
140130
* Adds a ContinuationSanitizer with the given options.
141131
*/
142-
async function addContinuationSanitizer(
143-
httpClient: HttpClient,
144-
url: string,
145-
recordingId: string,
146-
sanitizer: ContinuationSanitizer
147-
) {
132+
const addContinuationSanitizer: AddSanitizer<ContinuationSanitizer> = async (
133+
httpClient,
134+
url,
135+
recordingId,
136+
sanitizer
137+
) => {
148138
await addSanitizer(httpClient, url, recordingId, {
149139
sanitizer: "ContinuationSanitizer",
150140
body: {
151141
...sanitizer,
152142
resetAfterFirst: sanitizer.resetAfterFirst.toString(),
153143
},
154144
});
155-
}
145+
};
156146

157147
/**
158148
* Adds a RemoveHeaderSanitizer with the given options.
159149
*/
160-
async function addRemoveHeaderSanitizer(
161-
httpClient: HttpClient,
162-
url: string,
163-
recordingId: string,
164-
sanitizer: RemoveHeaderSanitizer
165-
) {
150+
const addRemoveHeaderSanitizer: AddSanitizer<RemoveHeaderSanitizer> = async (
151+
httpClient,
152+
url,
153+
recordingId,
154+
sanitizer
155+
) => {
166156
await addSanitizer(httpClient, url, recordingId, {
167157
sanitizer: "RemoveHeaderSanitizer",
168158
body: {
169159
headersForRemoval: sanitizer.headersForRemoval.toString(),
170160
},
171161
});
172-
}
162+
};
173163

174164
/**
175165
* Adds a HeaderRegexSanitizer or HeaderStringSanitizer.
@@ -178,12 +168,12 @@ async function addRemoveHeaderSanitizer(
178168
* Additionally, the 'target' option is not required. If target is unspecified, the header's value will always
179169
* be replaced.
180170
*/
181-
async function addHeaderSanitizer(
182-
httpClient: HttpClient,
183-
url: string,
184-
recordingId: string,
185-
sanitizer: HeaderSanitizer
186-
) {
171+
const addHeaderSanitizer: AddSanitizer<HeaderSanitizer> = async (
172+
httpClient,
173+
url,
174+
recordingId,
175+
sanitizer
176+
) => {
187177
if (sanitizer.regex || !sanitizer.target) {
188178
await addSanitizer(httpClient, url, recordingId, {
189179
sanitizer: "HeaderRegexSanitizer",
@@ -204,7 +194,7 @@ async function addHeaderSanitizer(
204194
},
205195
});
206196
}
207-
}
197+
};
208198

209199
const addSanitizersActions: {
210200
[K in keyof SanitizerOptions]: AddSanitizer<Exclude<SanitizerOptions[K], undefined>>;
@@ -229,7 +219,7 @@ const addSanitizersActions: {
229219
export async function addSanitizers(
230220
httpClient: HttpClient,
231221
url: string,
232-
recordingId: string,
222+
recordingId: string | undefined,
233223
options: SanitizerOptions
234224
): Promise<void> {
235225
await Promise.all(
@@ -250,7 +240,7 @@ export async function addSanitizers(
250240
async function addSanitizer(
251241
httpClient: HttpClient,
252242
url: string,
253-
recordingId: string,
243+
recordingId: string | undefined,
254244
options: {
255245
sanitizer: ProxyToolSanitizers;
256246
body: Record<string, unknown> | undefined;

0 commit comments

Comments
 (0)