Skip to content

Commit 77d5fd1

Browse files
authored
[Unified recorder] String sanitizer support + sanitizer refactor (Azure#19954)
- Fixes Azure#19809 - Part of work towards Azure#18223 The main motivation of this PR was to add support for the new string sanitizers introduced in Azure/azure-sdk-tools#2530. As part of this, I've also tackled some refactoring that will be required for session-level sanitizer support (Azure#18223) where we will be wanting to enable adding sanitizers without access to an instance of the `Recorder` class. While implementing the new sanitizer logic, I refactored the `addSanitizers` method into smaller chunks to make adding additional sanitizers easier. To summarize the changes: * Removed the `Sanitizer` class, instead making the `addSanitizers` function in `sanitizer.ts` take in a `HttpClient` and recording ID as parameter. * Refactored the `addSanitizers` function to call smaller functions for each sanitizer (some of which are a bit FP-style) instead of using if statements + special cases. Hopefully this will make things a bit easier to maintain. * Some other minor refactors (e.g. extracting duplicated `createRecordingRequest` function into a utility). * Add support for the string sanitizers in what I think is the most logical way, but there is a **breaking change**: * When calling `addSanitizers`, instead of specifying `generalRegexSanitizers: [...]` etc., you now specify `generalSanitizers: [...]`. Both regex sanitizers and string sanitizers can be used in this way, for example: ```ts recorder.addSanitizers({ generalSanitizers: [ { regex: true, // Regex matching is enabled by setting the 'regex' option to true. target: ".*regex", value: "sanitized", }, { // Note that `regex` defaults to false and doesn't need to be specified when working with bare strings. // In my experience, this is the most common scenario anyway. target: "Not a regex", value: "sanitized", } ], }); ```
1 parent 4d16087 commit 77d5fd1

File tree

13 files changed

+539
-388
lines changed

13 files changed

+539
-388
lines changed

sdk/test-utils/recorder/CHANGELOG.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,36 @@
22

33
## 2.0.0 (Unreleased)
44

5+
## 2022-01-27
6+
7+
Add support for the new string sanitizers, including **breaking changes**:
8+
9+
- Removed the `Sanitizer` class, instead making the `addSanitizers` function in `sanitizer.ts` take in a `HttpClient` and recording ID as parameter.
10+
- Refactored the `addSanitizers` function to call smaller functions for each sanitizer (some of which are a bit FP-style) instead of using if statements + special cases. Hopefully this will make things a bit easier to maintain.
11+
- Some other minor refactors (e.g. extracting duplicated `createRecordingRequest` function into a utility).
12+
- Add support for the string sanitizers in what I think is the most logical way, but there is a **breaking change**:
13+
- When calling `addSanitizers`, instead of specifying `generalRegexSanitizers: [...]` etc., you now specify `generalSanitizers: [...]`. Both regex sanitizers and string sanitizers can be used in this way, for example:
14+
15+
```ts
16+
recorder.addSanitizers({
17+
generalSanitizers: [
18+
{
19+
regex: true, // Regex matching is enabled by setting the 'regex' option to true.
20+
target: ".*regex",
21+
value: "sanitized",
22+
},
23+
{
24+
// Note that `regex` defaults to false and doesn't need to be specified when working with bare strings.
25+
// In my experience, this is the most common scenario anyway.
26+
target: "Not a regex",
27+
value: "sanitized",
28+
},
29+
],
30+
});
31+
```
32+
33+
[#19954](https://github.com/Azure/azure-sdk-for-js/pull/19954)
34+
535
## 2022-01-06
636

737
- Renaming the package `@azure-tools/[email protected]` as `@azure-tools/[email protected]`.

sdk/test-utils/recorder/MIGRATION.md

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ The new recorder is version 2.0.0 of the `@azure-tools/test-recorder` package. U
1515
// ...
1616
"devDependencies": {
1717
// ...
18-
"@azure-tools/test-recorder": "^2.0.0",
18+
"@azure-tools/test-recorder": "^2.0.0"
1919
}
2020
}
2121
```
@@ -152,23 +152,30 @@ In this example, the name of the queue used in the recording is randomized. Howe
152152

153153
A powerful feature of the legacy recorder was its `customizationsOnRecordings` option, which allowed for arbitrary replacements to be made to recordings. The new recorder's analog to this is the sanitizer functionality.
154154

155-
### GeneralRegexSanitizer
155+
### General sanitizers
156156

157-
For a simple find/replace, a `GeneralRegexSanitizer` can be used. For example:
157+
For a simple find/replace, `generalSanitizers` can be used. For example:
158158

159159
```ts
160160
await recorder.addSanitizers({
161-
generalRegexSanitizers: [
161+
generalSanitizers: [
162162
{
163-
regex: "find", // This should be a .NET regular expression as it is passed to the .NET proxy tool
163+
target: "find", // With `regex` unspecified, this matches a plaintext string
164+
value: "replace",
165+
},
166+
{
167+
regex: true, // Enable regex matching
168+
target: "[Rr]egex", // This is a .NET regular expression that will be compiled by the proxy tool.
164169
value: "replace",
165170
},
166171
// add additional sanitizers here as required
167172
],
168173
});
169174
```
170175

171-
This example would replace all instances of `find` in the recording with `replace`.
176+
This example has two sanitizers:
177+
- The first sanitizer replaces all instances of "find" in the recording with "replace".
178+
- The second example demonstrates the use of a regular expression for replacement, where anything matching the .NET regular expression `[Rr]egex` (i.e. "Regex" and "regex") would be replaced with "replace".
172179

173180
### ConnectionStringSanitizer
174181

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

Lines changed: 26 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,7 @@
33

44
import {
55
createDefaultHttpClient,
6-
createPipelineRequest,
76
HttpClient,
8-
HttpMethods,
97
Pipeline,
108
PipelinePolicy,
119
PipelineRequest,
@@ -27,7 +25,7 @@ import { Test } from "mocha";
2725
import { sessionFilePath } from "./utils/sessionFilePath";
2826
import { SanitizerOptions } from "./utils/utils";
2927
import { paths } from "./utils/paths";
30-
import { Sanitizer } from "./sanitizer";
28+
import { addSanitizers, transformsInfo } from "./sanitizer";
3129
import { handleEnvSetup } from "./utils/envSetupForPlayback";
3230
import { Matcher, setMatcher } from "./matcher";
3331
import {
@@ -37,6 +35,7 @@ import {
3735
WebResource,
3836
WebResourceLike,
3937
} from "@azure/core-http";
38+
import { createRecordingRequest } from "./utils/createRecordingRequest";
4039

4140
/**
4241
* This client manages the recorder life cycle and interacts with the proxy-tool to do the recording,
@@ -55,7 +54,6 @@ export class Recorder {
5554
private stateManager = new RecordingStateManager();
5655
private httpClient?: HttpClient;
5756
private sessionFile?: string;
58-
private sanitizer?: Sanitizer;
5957
private variables: Record<string, string>;
6058

6159
constructor(private testContext?: Test | undefined) {
@@ -68,7 +66,6 @@ export class Recorder {
6866
"Unable to determine the recording file path, testContext provided is not defined."
6967
);
7068
}
71-
this.sanitizer = new Sanitizer(this.url, this.httpClient);
7269
}
7370
this.variables = {};
7471
}
@@ -113,8 +110,12 @@ export class Recorder {
113110
*/
114111
async addSanitizers(options: SanitizerOptions): Promise<void> {
115112
// If check needed because we only sanitize when the recording is being generated, and we need a recording to apply the sanitizers on.
116-
if (isRecordMode() && ensureExistence(this.sanitizer, "this.sanitizer")) {
117-
return this.sanitizer.addSanitizers(options);
113+
if (
114+
isRecordMode() &&
115+
ensureExistence(this.httpClient, "this.httpClient") &&
116+
ensureExistence(this.recordingId, "this.recordingId")
117+
) {
118+
return addSanitizers(this.httpClient, this.url, this.recordingId, options);
118119
}
119120
}
120121

@@ -135,7 +136,7 @@ export class Recorder {
135136
const startUri = `${this.url}${isPlaybackMode() ? paths.playback : paths.record}${
136137
paths.start
137138
}`;
138-
const req = this._createRecordingRequest(startUri);
139+
const req = createRecordingRequest(startUri, this.sessionFile, this.recordingId);
139140

140141
if (ensureExistence(this.httpClient, "TestProxyHttpClient.httpClient")) {
141142
const rsp = await this.httpClient.sendRequest({
@@ -153,12 +154,14 @@ export class Recorder {
153154
if (isPlaybackMode()) {
154155
this.variables = rsp.bodyAsText ? JSON.parse(rsp.bodyAsText) : {};
155156
}
156-
if (ensureExistence(this.sanitizer, "TestProxyHttpClient.sanitizer")) {
157-
// Setting the recordingId in the sanitizer,
158-
// the sanitizers added will take the recording id and only be part of the current test
159-
this.sanitizer.setRecordingId(this.recordingId);
160-
await handleEnvSetup(options.envSetupForPlayback, this.sanitizer);
161-
}
157+
158+
await handleEnvSetup(
159+
this.httpClient,
160+
this.url,
161+
this.recordingId,
162+
options.envSetupForPlayback
163+
);
164+
162165
// Sanitizers to be added only in record mode
163166
if (isRecordMode() && options.sanitizerOptions) {
164167
// Makes a call to the proxy-tool to add the sanitizers for the current recording id
@@ -177,7 +180,7 @@ export class Recorder {
177180
this.stateManager.state = "stopped";
178181
if (this.recordingId !== undefined) {
179182
const stopUri = `${this.url}${isPlaybackMode() ? paths.playback : paths.record}${paths.stop}`;
180-
const req = this._createRecordingRequest(stopUri);
183+
const req = createRecordingRequest(stopUri, undefined, this.recordingId);
181184
req.headers.set("x-recording-save", "true");
182185

183186
if (isRecordMode()) {
@@ -211,19 +214,16 @@ export class Recorder {
211214
}
212215
}
213216

214-
/**
215-
* Adds the recording file and the recording id headers to the requests that are sent to the proxy tool.
216-
* These are required to appropriately save the recordings in the record mode and picking them up in playback.
217-
*/
218-
private _createRecordingRequest(url: string, method: HttpMethods = "POST") {
219-
const req = createPipelineRequest({ url, method });
220-
if (ensureExistence(this.sessionFile, "sessionFile")) {
221-
req.body = JSON.stringify({ "x-recording-file": this.sessionFile });
217+
async transformsInfo(): Promise<string | null | undefined> {
218+
if (isLiveMode()) {
219+
throw new RecorderError("Cannot call transformsInfo in live mode");
222220
}
223-
if (this.recordingId !== undefined) {
224-
req.headers.set("x-recording-id", this.recordingId);
221+
222+
if (ensureExistence(this.httpClient, "this.httpClient")) {
223+
return await transformsInfo(this.httpClient, this.url, this.recordingId!);
225224
}
226-
return req;
225+
226+
throw new RecorderError("Expected httpClient to be defined");
227227
}
228228

229229
/**

0 commit comments

Comments
 (0)