Skip to content

Commit

Permalink
[Unified recorder] String sanitizer support + sanitizer refactor (#19954
Browse files Browse the repository at this point in the history
)

- Fixes #19809
- Part of work towards #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 (#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",
    }
  ],
});
```
  • Loading branch information
timovv authored Jan 27, 2022
1 parent 4d16087 commit 77d5fd1
Show file tree
Hide file tree
Showing 13 changed files with 539 additions and 388 deletions.
30 changes: 30 additions & 0 deletions sdk/test-utils/recorder/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,36 @@

## 2.0.0 (Unreleased)

## 2022-01-27

Add support for the new string sanitizers, including **breaking 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",
},
],
});
```

[#19954](https://github.com/Azure/azure-sdk-for-js/pull/19954)

## 2022-01-06

- Renaming the package `@azure-tools/test-recorder-new@1.0.0` as `@azure-tools/test-recorder@2.0.0`.
Expand Down
19 changes: 13 additions & 6 deletions sdk/test-utils/recorder/MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ The new recorder is version 2.0.0 of the `@azure-tools/test-recorder` package. U
// ...
"devDependencies": {
// ...
"@azure-tools/test-recorder": "^2.0.0",
"@azure-tools/test-recorder": "^2.0.0"
}
}
```
Expand Down Expand Up @@ -152,23 +152,30 @@ In this example, the name of the queue used in the recording is randomized. Howe

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.

### GeneralRegexSanitizer
### General sanitizers

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

```ts
await recorder.addSanitizers({
generalRegexSanitizers: [
generalSanitizers: [
{
regex: "find", // This should be a .NET regular expression as it is passed to the .NET proxy tool
target: "find", // With `regex` unspecified, this matches a plaintext string
value: "replace",
},
{
regex: true, // Enable regex matching
target: "[Rr]egex", // This is a .NET regular expression that will be compiled by the proxy tool.
value: "replace",
},
// add additional sanitizers here as required
],
});
```

This example would replace all instances of `find` in the recording with `replace`.
This example has two sanitizers:
- The first sanitizer replaces all instances of "find" in the recording with "replace".
- 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".

### ConnectionStringSanitizer

Expand Down
52 changes: 26 additions & 26 deletions sdk/test-utils/recorder/src/recorder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,7 @@

import {
createDefaultHttpClient,
createPipelineRequest,
HttpClient,
HttpMethods,
Pipeline,
PipelinePolicy,
PipelineRequest,
Expand All @@ -27,7 +25,7 @@ import { Test } from "mocha";
import { sessionFilePath } from "./utils/sessionFilePath";
import { SanitizerOptions } from "./utils/utils";
import { paths } from "./utils/paths";
import { Sanitizer } from "./sanitizer";
import { addSanitizers, transformsInfo } from "./sanitizer";
import { handleEnvSetup } from "./utils/envSetupForPlayback";
import { Matcher, setMatcher } from "./matcher";
import {
Expand All @@ -37,6 +35,7 @@ import {
WebResource,
WebResourceLike,
} from "@azure/core-http";
import { createRecordingRequest } from "./utils/createRecordingRequest";

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

constructor(private testContext?: Test | undefined) {
Expand All @@ -68,7 +66,6 @@ export class Recorder {
"Unable to determine the recording file path, testContext provided is not defined."
);
}
this.sanitizer = new Sanitizer(this.url, this.httpClient);
}
this.variables = {};
}
Expand Down Expand Up @@ -113,8 +110,12 @@ export class Recorder {
*/
async addSanitizers(options: SanitizerOptions): Promise<void> {
// If check needed because we only sanitize when the recording is being generated, and we need a recording to apply the sanitizers on.
if (isRecordMode() && ensureExistence(this.sanitizer, "this.sanitizer")) {
return this.sanitizer.addSanitizers(options);
if (
isRecordMode() &&
ensureExistence(this.httpClient, "this.httpClient") &&
ensureExistence(this.recordingId, "this.recordingId")
) {
return addSanitizers(this.httpClient, this.url, this.recordingId, options);
}
}

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

if (ensureExistence(this.httpClient, "TestProxyHttpClient.httpClient")) {
const rsp = await this.httpClient.sendRequest({
Expand All @@ -153,12 +154,14 @@ export class Recorder {
if (isPlaybackMode()) {
this.variables = rsp.bodyAsText ? JSON.parse(rsp.bodyAsText) : {};
}
if (ensureExistence(this.sanitizer, "TestProxyHttpClient.sanitizer")) {
// Setting the recordingId in the sanitizer,
// the sanitizers added will take the recording id and only be part of the current test
this.sanitizer.setRecordingId(this.recordingId);
await handleEnvSetup(options.envSetupForPlayback, this.sanitizer);
}

await handleEnvSetup(
this.httpClient,
this.url,
this.recordingId,
options.envSetupForPlayback
);

// Sanitizers to be added only in record mode
if (isRecordMode() && options.sanitizerOptions) {
// Makes a call to the proxy-tool to add the sanitizers for the current recording id
Expand All @@ -177,7 +180,7 @@ export class Recorder {
this.stateManager.state = "stopped";
if (this.recordingId !== undefined) {
const stopUri = `${this.url}${isPlaybackMode() ? paths.playback : paths.record}${paths.stop}`;
const req = this._createRecordingRequest(stopUri);
const req = createRecordingRequest(stopUri, undefined, this.recordingId);
req.headers.set("x-recording-save", "true");

if (isRecordMode()) {
Expand Down Expand Up @@ -211,19 +214,16 @@ export class Recorder {
}
}

/**
* Adds the recording file and the recording id headers to the requests that are sent to the proxy tool.
* These are required to appropriately save the recordings in the record mode and picking them up in playback.
*/
private _createRecordingRequest(url: string, method: HttpMethods = "POST") {
const req = createPipelineRequest({ url, method });
if (ensureExistence(this.sessionFile, "sessionFile")) {
req.body = JSON.stringify({ "x-recording-file": this.sessionFile });
async transformsInfo(): Promise<string | null | undefined> {
if (isLiveMode()) {
throw new RecorderError("Cannot call transformsInfo in live mode");
}
if (this.recordingId !== undefined) {
req.headers.set("x-recording-id", this.recordingId);

if (ensureExistence(this.httpClient, "this.httpClient")) {
return await transformsInfo(this.httpClient, this.url, this.recordingId!);
}
return req;

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

/**
Expand Down
Loading

0 comments on commit 77d5fd1

Please sign in to comment.