Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Unified recorder] String sanitizer support + sanitizer refactor #19954

Merged
merged 14 commits into from
Jan 27, 2022
17 changes: 11 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,28 @@ 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: [
HarshaNalluru marked this conversation as resolved.
Show resolved Hide resolved
{
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".
timovv marked this conversation as resolved.
Show resolved Hide resolved

### ConnectionStringSanitizer

Expand Down
54 changes: 28 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,16 @@ 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);
}

// Setting the recordingId in the sanitizer,
// the sanitizers added will take the recording id and only be part of the current test
HarshaNalluru marked this conversation as resolved.
Show resolved Hide resolved
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 +182,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, this.sessionFile, this.recordingId);
HarshaNalluru marked this conversation as resolved.
Show resolved Hide resolved
req.headers.set("x-recording-save", "true");

if (isRecordMode()) {
Expand Down Expand Up @@ -211,19 +216,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");
timovv marked this conversation as resolved.
Show resolved Hide resolved
}

/**
Expand Down
Loading