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

[Recorder] Coupling RecorderEnvironmentSetup with the record call #7083

Merged
Merged
10 changes: 9 additions & 1 deletion sdk/test-utils/recorder/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
## 2020-01-27
## 2020-01-29

- Fixed a bug where the replacements provided aren't being replaced in the query-param parts of recordings, which is an expectation for playback. This caused a few test failures during playback for new recordings. More info [#7108](https://github.com/Azure/azure-sdk-for-js/issues/7108)

## 2020-01-28

- [BREAKING] `setReplaceableVariables`, `setReplacements`, `skipQueryParams` methods of the recorder are no longer exposed.

- Equivalent `RecorderEnvironmentSetup` type is being exported instead. An object of this type is expected to be passed in the `record()` call.
- `record(this)` changes to `record(this, recorderEnvSetup)`, where `recorderEnvSetup` is of type `RecorderEnvironmentSetup`.
- [PR #7083](https://github.com/Azure/azure-sdk-for-js/pull/7083)
121 changes: 53 additions & 68 deletions sdk/test-utils/recorder/GUIDELINES.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,33 +21,21 @@ Add `@azure/test-utils-recorder` as a devDependency of your sdk.

- `record` from `@azure/test-utils-recorder` package should be imported in the test files.

- `recorder = record(this);` initiates recording the HTTP requests and when `recorder.stop();` is called, the recording stops
### Recording the tests

- `recorder = record(this, recorderEnvSetup);` initiates recording the HTTP requests and when `recorder.stop();` is called, the recording stops
and all the HTTP requests recorded in between the two calls are saved as part of the recording in the `"record"` mode.
In the same way, existing recordings are leveraged and played back in the `"playback"` mode when `recorder = record(this);` is invoked.
In the same way, existing recordings are leveraged and played back in the `"playback"` mode when `recorder = record(this, recorderEnvSetup);` is invoked.
[Has no effect if the `TEST_MODE` is `"live"`, tests hit the live-service, we don't record the requests/responses]

- Follow the below template for adding a new test. `before` and `after` sections are optional, `beforeEach` and `afterEach` sections are compulsory.
- Follow the below template for adding a new test. `beforeEach` and `afterEach` sections are compulsory.

```typescript
import { record } from "@azure/test-utils-recorder";

describe("<describe-block-title>", () => {
// before section is optional
before(async function() {
recorder = record(this);
/*Place your code here*/
recorder.stop();
});

// after section is optional
after(async function() {
recorder = record(this);
/*Place your code here*/
recorder.stop();
});

beforeEach(async function() {
recorder = record(this);
recorder = record(this, recorderEnvSetup);
/*Place your code here*/
});

Expand All @@ -64,17 +52,56 @@ Add `@azure/test-utils-recorder` as a devDependency of your sdk.

- Consider the `beforeEach`, `afterEach` and `it` blocks in the above test-suite.

- `recorder = record(this);` is invoked in the `beforeEach` section and `recorder.stop();` in the `afterEach` section.
- `recorder = record(this, recorderEnvSetup);` is invoked in the `beforeEach` section and `recorder.stop();` in the `afterEach` section.
- All the HTTP requests recorded in between the two calls are saved as part of the test(`it` block) recording in the `"record"` mode.
- Existing test recording is played back when invoked `recorder = record(this);` in the `"playback"` mode.
- Existing test recording is played back when invoked `recorder = record(this, recorderEnvSetup);` in the `"playback"` mode.=
- Any function call that affects http requests and is not in the `it`-block and belongs to a `describe`-block must go in one of the `beforeEach` or `afterEach` sections.

### ENV Setup - `recorderEnvSetup`

- Required `recorderEnvSetup` has to be defined before invoking the recording

```typescript
interface RecorderEnvironmentSetup {
replaceableVariables: { [ENV_VAR: string]: string };
replaceInRecordings: Array<(recording: string) => string>;
queryParametersToSkip: Array<string>;
}
```

- `RecorderEnvironmentSetup` type is being exposed from the `@azure/test-utils-recorder` package.
Environment variables for the tests in the sdk can be managed with the setup on a per-test basis.

- `replaceableVariables: { [ENV_VAR: string]: string };`

- Used in both record and playback modes.
- `replaceableVariables` is a dictionary with key-value pairs.
- The key-value pairs will be used as the environment variables in playback mode.
- If the env variables are present in the recordings as plain strings, they will be replaced with the provided values in record mode.

- `replaceInRecordings: Array<(recording: string) => string>;`

- Used only in record mode.
- Array of callback functions provided to customize the generated recordings in record mode
- Example with one callback function -
```typescript
// `sig` param of SAS Token is being filtered here from the recordings..
(recording: string): string =>
recording.replace(new RegExp(env.ACCOUNT_SAS.match("(.*)&sig=(.*)")[2], "g"), "aaaaa");
```

- Recordings corresponding to `beforeEach` or `afterEach` sections are saved along with the test recordings(`recordings/{node|browsers}/<describe-block-title>/recording_<test-title>.{js|json}`).
- `queryParametersToSkip: Array<string>;`

- Recordings corresponding to `before` or `after` sections are saved separately under `recordings/{node|browsers}/<describe-block-title>/recording_before_all_hook.{js|json}`.
- Used in record and playback modes
- Array of query parameters provided will be filtered from the requests

- `Mocha.Context` is being leveraged to obtain the test title and other required information to save and replay the recordings.
- With the above attributes, declare the `recorderEnvSetup` to use it below.

- Any function call that affects http requests and is not in the `it`-block and belongs to a `describe`-block must go in one of the `beforeEach`, `afterEach`, `before` or `after` sections.
### Saving the recordings

- Any potential plain secrets in the recordings are replaced with dummy values provided at `replaceableVariables` and `replaceInRecordings`.

- `Mocha.Context` is being leveraged to obtain the test title and other required information to save and replay the recordings. Recordings corresponding to `beforeEach`, `it` and `afterEach` sections are saved as a single test recording(`recordings/{node|browsers}/<describe-block-title>/recording_<test-title>.{js|json}`).

### Member functions getUniqueName() and newDate() of the `Recorder`

Expand All @@ -83,7 +110,7 @@ Add `@azure/test-utils-recorder` as a devDependency of your sdk.
- `Recorder` returned by the `record()` method has `getUniqueName()` and `newDate()` member functions along with the `stop()`.

```typescript
recorder = record(this);
recorder = record(this, recorderEnvSetup);

const randomName = recorder.getUniqueName("random");
const tmr = recorder.newDate("tmr");
Expand Down Expand Up @@ -158,7 +185,7 @@ Add `@azure/test-utils-recorder` as a devDependency of your sdk.
```typescript
describe("Some Random Test Suite", () => {
beforeEach(async function() {
recorder = record(this);
recorder = record(this, recorderEnvSetup);
/*Place your code here*/
});

Expand All @@ -179,50 +206,8 @@ Add `@azure/test-utils-recorder` as a devDependency of your sdk.

[ Following this rule - `./recordings/node/<describe-block-title>/recording_<test-title>.js` ]

- Just like the test recordings, we save the requests and responses from the `before` and `after` sections of the describe block in - `recordings/{node|browsers}/<describe-block-title>/recording_before_all_hook.{js|json}`.
- In node recordings(Nock), the query parameters are not being stored and the SAS Token query parameters are not being stored in browser recordings(Nise).

### ENV Variables

- Any potential plain secrets in the recordings are replaced with dummy values.
- `setReplaceableVariables` and `setReplacements` methods are being exposed from the recorder package so that the environment variables can be managed from the tests in the sdk. These two methods can be imported from `@azure/test-utils-recorder`.
- Taking tests in the Keyvault sdk as an example -

```typescript
setReplaceableVariables({
AZURE_CLIENT_ID: "azure_client_id",
AZURE_CLIENT_SECRET: "azure_client_secret",
AZURE_TENANT_ID: "azure_tenant_id",
KEYVAULT_NAME: "keyvault_name"
});
```

Calling the `setReplaceableVariables` method would mean that

- In playback mode, the environment variables will be overriden by the provided values.
- In record mode, occurences of the environment variables in the recordings are replaced with the provided values.
- This has no effect in the `live` test mode.

```typescript
setReplacements([
(recording: any): any =>
recording.replace(/"access_token":"[^"]*"/g, `"access_token":"access_token"`),
(recording: any): any =>
keySuffix === "" ? recording : recording.replace(new RegExp(keySuffix, "g"), "")
]);
```

Calling the `setReplacements` method would mean that

- In `record` mode, occurences of any strings in the recordings that match the regular expressions are replaced with the provided values.
- This has no effect in the `live` test mode or `playback` mode.

- The above methods can be called from the `before` section so that the tests can leverage the environment variables.

The same dummy values are used as the environment variables during the playback mode.

---

## Skipping a test

- Currently, some tests (in storage packages) are being skipped because record and playback does not work as expected while executing them, the reasons are listed below.
Expand Down
6 changes: 5 additions & 1 deletion sdk/test-utils/recorder/src/baseRecorder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
isPlaybackMode,
isRecordMode,
findRecordingsFolderPath,
RecorderEnvironmentSetup,
ReplacementFunctions,
ReplacementMap,
filterSecretsFromStrings,
Expand Down Expand Up @@ -47,14 +48,17 @@ export function skipQueryParams(params: string[]): void {
queryParameters = params;
}

export function setEnvironmentOnLoad() {
export function setEnvironmentOnLoad(environmentSetup: RecorderEnvironmentSetup) {
if (!isBrowser() && (isRecordMode() || isPlaybackMode())) {
nock = require("nock");
}

if (isBrowser() && isRecordMode()) {
customConsoleLog();
}
setReplaceableVariables(environmentSetup.replaceableVariables);
setReplacements(environmentSetup.replaceInRecordings);
skipQueryParams(environmentSetup.queryParametersToSkip);
}

export abstract class BaseRecorder {
Expand Down
13 changes: 7 additions & 6 deletions sdk/test-utils/recorder/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@
// Licensed under the MIT License. See License.txt in the project root for license information.

export { record, Recorder } from "./recorder";
export { env, delay, isPlaybackMode, isRecordMode, isLiveMode } from "./utils";
export {
setReplaceableVariables,
setReplacements,
setEnvironmentOnLoad,
skipQueryParams
} from "./baseRecorder";
env,
delay,
isPlaybackMode,
isRecordMode,
isLiveMode,
RecorderEnvironmentSetup
} from "./utils";
export { jsonRecordingFilterFunction } from "./basekarma.conf";
15 changes: 12 additions & 3 deletions sdk/test-utils/recorder/src/recorder.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.

import { getUniqueName, isBrowser, isRecordMode, isPlaybackMode } from "./utils";
import {
getUniqueName,
isBrowser,
isRecordMode,
isPlaybackMode,
RecorderEnvironmentSetup
} from "./utils";
import { NiseRecorder, NockRecorder, BaseRecorder, setEnvironmentOnLoad } from "./baseRecorder";

/**
Expand Down Expand Up @@ -52,7 +58,10 @@ export interface Recorder {
* @param {Mocha.Context} [testContext]
* @returns {Recorder}
*/
export function record(testContext: Mocha.Context): Recorder {
export function record(
testContext: Mocha.Context,
environmentSetup: RecorderEnvironmentSetup
): Recorder {
let recorder: BaseRecorder;
let testHierarchy: string;
let testTitle: string;
Expand All @@ -72,7 +81,7 @@ export function record(testContext: Mocha.Context): Recorder {
testTitle = testContext.test!.title;
}

setEnvironmentOnLoad();
setEnvironmentOnLoad(environmentSetup);

if (isBrowser()) {
recorder = new NiseRecorder(testHierarchy, testTitle);
Expand Down
37 changes: 37 additions & 0 deletions sdk/test-utils/recorder/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,43 @@ export interface TestInfo {
newDate: { [x: string]: string };
}

/**
* Interface to setup environment necessary for the test run.
*
* @export
* @interface RecorderEnvironmentSetup
*/
export interface RecorderEnvironmentSetup {
/**
* Used in record and playback modes
* 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
*
* @type {{ [ENV_VAR: string]: string }}
* @memberof RecorderEnvironmentSetup
*/
replaceableVariables: { [ENV_VAR: string]: string };
/**
* Used in record mode
* Array of callback functions provided to customize the generated recordings in record mode
*
* Example with one callback function -
* `sig` param of SAS Token is being filtered here from the recordings..
* [ (recording: string): string => recording.replace(new RegExp(env.ACCOUNT_SAS.match("(.*)&sig=(.*)")[2], "g"), "aaaaa") ]
*
* @memberof RecorderEnvironmentSetup
*/
replaceInRecordings: Array<(recording: string) => string>;
/**
* Used in record and playback modes
* Array of query parameters provided will be filtered from the requests
*
* @type {Array<string>}
* @memberof RecorderEnvironmentSetup
*/
queryParametersToSkip: Array<string>;
}

export const env = isBrowser() ? (window as any).__env__ : process.env;

export function isRecordMode() {
Expand Down