Skip to content

Commit e00dc6d

Browse files
timovvsadasantHarshaNalluru
authored
Migration guide for Unified Recorder (Azure#19210)
Co-authored-by: Daniel Rodríguez <sadasant@users.noreply.github.com> Co-authored-by: Harsha Nalluru <sanallur@microsoft.com>
1 parent b200156 commit e00dc6d

File tree

1 file changed

+302
-0
lines changed

1 file changed

+302
-0
lines changed
Lines changed: 302 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,302 @@
1+
# Migration Guide
2+
3+
This document outlines key differences between the legacy recorder and the new Unified Recorder client. The Unified Recorder replaces the existing `nock/nise`-based recorder with a solution that uses the language-agnostic [test proxy server].
4+
5+
## Prerequisites
6+
7+
- [Docker] is required, as the [test proxy server] is run in a container during testing. When running the tests, ensure the Docker daemon is running and you have permission to use it. For WSL 2, running `sudo service docker start` and `sudo usermod -aG docker $USER` should be sufficient.
8+
9+
## Upgrading to the Unified Recorder
10+
11+
The new recorder is version 2.0.0 of the `@azure-tools/test-recorder` package. Update the test-recorder dependency in your package.json file as follows:
12+
13+
```json
14+
{
15+
// ...
16+
"devDependencies": {
17+
// ...
18+
"@azure-tools/test-recorder": "^2.0.0",
19+
}
20+
}
21+
```
22+
23+
Once you've updated the dependency version, run `rush update` and you are ready to start using the new recorder. The new recorder's API is similar to the legacy recorder. Differences will be discussed below.
24+
25+
## Changes to NPM scripts
26+
27+
For the unified recorder client library to work, the [test proxy server] must be active while you are running your tests. Helpers have been added to the `dev-tool` package which manage starting and stopping the test proxy server before and after your tests are run.
28+
29+
Update your test scripts based on the following examples:
30+
31+
| Script | Before migration | After migration |
32+
| :------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------- |
33+
| `unit-test:browser` | `karma start --single-run` | `dev-tool run test:browser` |
34+
| `unit-test:node` | `mocha -r esm -r ts-node/register --reporter ../../../common/tools/mocha-multi-reporter.js --timeout 1200000 --full-trace --exclude \"test/**/browser/*.spec.ts\" \"test/**/*.spec.ts\"` | `dev-tool run test:node-ts-input -- --timeout 1200000 --exclude 'test/**/browser/*.spec.ts' 'test/**/*.spec.ts'` |
35+
| `integration-test:browser` | `karma start --single-run` | `dev-tool run test:browser` |
36+
| `integration-test:node` | `nyc mocha -r esm --require source-map-support/register --reporter ../../../common/tools/mocha-multi-reporter.js --timeout 5000000 \"dist-esm/test/{,!(browser)/**/}*.spec.js\"` | `dev-tool run test:node-js-input -- --timeout 5000000 'dist-esm/test/**/*.spec.js'` |
37+
38+
Note the difference between the dev-tool `node-ts-input` and `node-js-input` commands:
39+
40+
- `node-ts-input` runs the tests using `ts-node`, without code coverage.
41+
- `node-js-input` runs the tests using the built JavaScript output, and generates coverage reporting using `nyc`.
42+
43+
## Initializing the recorder
44+
45+
The approach taken to initialize the recorder depends on whether the SDK being tested uses Core v1 ([`core-http`]) or Core v2 ([`core-rest-pipeline`]). If your SDK is on Core v2, read on. If you're still on Core v1, [jump to the section on Core v1 below](#for-core-v1-sdks).
46+
47+
### For Core v2 SDKs
48+
49+
The recorder is implemented as a custom policy which should be attached to your client's pipeline. Firstly, initialize the recorder:
50+
51+
```ts
52+
let recorder: Recorder;
53+
54+
/*
55+
* Note the use of function() instead of the arrow syntax. We need access to `this` so we
56+
* can pass test information from Mocha to the recorder.
57+
*/
58+
beforeEach(function (this: Context) {
59+
recorder = new Recorder(this.currentTest);
60+
});
61+
```
62+
63+
To enable the recorder, you should then initialize your SDK client as normal and use the recorder's `configureClient` method. This method will attach the necessary policies to the client for recording to be enabled. Note that for this method to work, the `pipeline` object must be exposed as a property on the client.
64+
65+
```ts
66+
const client = /* ... initialize your client as normal ... */;
67+
// recorderHttpPolicy is provided as an export from the test-recorder-new package.
68+
recorder.configureClient(client);
69+
```
70+
71+
### For Core v1 SDKs
72+
73+
The recorder library provides a custom `HttpClient` that is then passed to the SDK. This client needs to be initialized as follows:
74+
75+
```ts
76+
let recorder: Recorder;
77+
78+
/*
79+
* Note the use of function() instead of the arrow syntax. We need access to `this` so we
80+
* can pass test information from Mocha to the recorder.
81+
*/
82+
beforeEach(function (this: Context) {
83+
recorder = new Recorder(this.currentTest);
84+
});
85+
```
86+
87+
When initialising your client in your test, you should pass in the recorder as follows:
88+
89+
```ts
90+
const client = new MyServiceClient(
91+
/* ... insert options here ... */,
92+
recorder.configureClientOptionsCoreV1({ /* any additional options to pass through */ }),
93+
);
94+
```
95+
96+
This will allow requests to be intercepted and redirected to the proxy tool.
97+
98+
## Starting and stopping the recorder
99+
100+
The way that the recorder is started and stopped has changed slightly. At the beginning of your test (or in a `beforeEach` block), start the recorder as follows:
101+
102+
```ts
103+
await recorder.start({
104+
envSetupForPlayback: {
105+
// Your environment variables (equivalent to the old recorder's replaceableVariables option). See the section on environment variables below for detail
106+
},
107+
// Other options, e.g. sanitizers (which replace the customizationsOnRecordings option)
108+
});
109+
```
110+
111+
And at the end of your test (or in an `afterEach` block), stop the recorder:
112+
113+
```ts
114+
await recorder.stop();
115+
```
116+
117+
It is important that `recorder.stop()` is called, or otherwise the next test will throw an error when trying to start the already started recorder. Additionally, it is important that both the `start` and `stop` calls are awaited, for similar reasons.
118+
119+
## Environment variables
120+
121+
In the legacy recorder, the `replaceableVariables` option could be used to specify environment variables that would be replaced in the recording and set during playback. This could be used to ensure that secrets and user-specific options do not appear in the recording body.
122+
123+
The Unified Recorder client provides this functionality through the use of the `envSetupForPlayback` option, which is passed when `recorder.start` is called. Like the legacy recorder, it takes in an object mapping environment variables to what they should be replaced with in the recording. For example:
124+
125+
```ts
126+
await recorder.start({
127+
envSetupForPlayback: {
128+
TABLES_SAS_CONNECTION_STRING: "fakeConnectionString",
129+
},
130+
});
131+
```
132+
133+
Under the hood, this is powered by the Unified Recorder's sanitizer functionality.
134+
135+
**⚠️Important:** To access environment variables, you must use the `env` export made available from the **new** recorder. This ensures that environment variables are sourced from the correct location (using `process.env` and `dotenv` in Node, and using `window.__env__` via karma in the browser), and also means that the environment variables set in `envSetupForPlayback` are used in playback mode.
136+
137+
## Recorder variables
138+
139+
If you want to compute a value at record time and re-use it during playback, the Unified Recorder's variable functionality is for you. This API lets you declare variables which are stored with the recording at record time. During playback, instead of computing the variable afresh, the value will be retrieved from the recording. A use case of this might be to set a value randomly during record time that needs to be the same during playback.
140+
141+
Here is an example:
142+
143+
```ts
144+
const queueName = recorder.variable("queueName", "queue-${Math.floor(Math.random * 1000)}");
145+
// Assume that we have a client that has a createQueue method.
146+
await client.createQueue(queueName);
147+
```
148+
149+
In this example, the name of the queue used in the recording is randomized. However, in playback, instead of using the value passed into `recorder.variable`, the value will be retrieved from the recording file. This means that the name of the queue will be consistent between record and playback modes.
150+
151+
## Customizations on recordings
152+
153+
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.
154+
155+
### GeneralRegexSanitizer
156+
157+
For a simple find/replace, a `GeneralRegexSanitizer` can be used. For example:
158+
159+
```ts
160+
await recorder.addSanitizers({
161+
generalRegexSanitizers: [
162+
{
163+
regex: "find", // This should be a .NET regular expression as it is passed to the .NET proxy tool
164+
value: "replace",
165+
},
166+
// add additional sanitizers here as required
167+
],
168+
});
169+
```
170+
171+
This example would replace all instances of `find` in the recording with `replace`.
172+
173+
### ConnectionStringSanitizer
174+
175+
A `ConnectionStringSanitizer` can be used to strip all occurrences of a connection string from a recording. Its usage is very similar to the `GeneralRegexSanitizer`. For example:
176+
177+
```ts
178+
recorder.addSanitizers({
179+
connectionStringSanitizers: [
180+
{
181+
actualConnString: /* the actual connection string to be replaced, usually passed in as an environment variable */,
182+
fakeConnString: /* a mock connection string to replace actualConnString with */,
183+
},
184+
],
185+
});
186+
```
187+
188+
### RemoveHeaderSanitizer
189+
190+
`RemoveHeaderSanitizer` can be used to remove specific headers from the recordings as follows:
191+
192+
```ts
193+
recorder.addSanitizers({
194+
removeHeaderSanitizer: {
195+
headersForRemoval: ["Header1", "Header2" /* ... */],
196+
},
197+
});
198+
```
199+
200+
Other sanitizers for more complex use cases are also available.
201+
202+
## AAD and the new `NoOpCredential`
203+
204+
The new recorder does not record AAD traffic at present. As such, tests with clients using AAD should make use of the new `@azure-tools/test-credential` package, installed as follows:
205+
206+
```bash
207+
$ rush add --dev --caret -p @azure-tools/test-credential
208+
```
209+
210+
This package provides a `NoOpCredential` implementation of `TokenCredential` which makes no network requests, and should be used in playback mode. The provided `createTestCredential` helper will handle switching between NoOpCredential in playback and ClientSecretCredential when recording for you:
211+
212+
```ts
213+
const credential = createTestCredential();
214+
215+
// Create your client using the test credential.
216+
new MyServiceClient(<endpoint>, credential);
217+
```
218+
219+
Since AAD traffic is not recorded by the new recorder, there is no longer a need to remove AAD credentials from the recording using a sanitizer.
220+
221+
## Browser tests and modifications to Karma configuration
222+
223+
When running browser tests, the recorder relies on an environment variable to determine where to save the recordings. Add this snippet to your `karma.conf.js`:
224+
225+
```ts
226+
const { relativeRecordingsPath } = require("@azure-tools/test-recorder-new");
227+
228+
process.env.RECORDINGS_RELATIVE_PATH = relativeRecordingsPath();
229+
```
230+
231+
And then, again in `karma.conf.js`, add the variable to the list of environment variables:
232+
233+
```ts
234+
module.exports = function (config) {
235+
config.set({
236+
/* ... */
237+
238+
envPreprocessor: [
239+
,
240+
/* ... */ "RECORDINGS_RELATIVE_PATH", // Add this!
241+
],
242+
243+
/* ... */
244+
});
245+
};
246+
```
247+
248+
The following configuration options in `karma.config.js` should be **removed**:
249+
250+
```ts
251+
browserConsoleLogOptions: {
252+
terminal: !isRecordMode(),
253+
}
254+
255+
/* ... */
256+
257+
jsonToFileReporter: {
258+
filter: jsonRecordingFilterFunction, outputPath: ".",
259+
}
260+
```
261+
262+
## Changes to `ci.yml`
263+
264+
You must set the `TestProxy` parameter to `true` to enable the test proxy server in your SDK's `ci.yml` file.
265+
266+
```yaml
267+
# irrelevant sections of ci.yml omitted
268+
269+
extends:
270+
template: ../../eng/pipelines/templates/stages/archetype-sdk-client.yml
271+
parameters:
272+
TestProxy: true # Add me!
273+
```
274+
275+
## Migrating your recordings
276+
277+
Once you have made the necessary code changes, it is time to re-record your tests using the Unified Recorder to complete the migration. To do this, first **delete** the directory containing the old recordings (the `recordings` folder). Then, run your tests with the `TEST_MODE` environment variable to `record`.
278+
279+
If everything succeeds, the new recordings will be made available in the `recordings` directory. Inspect them to make sure everything looks OK (no secrets present, etc.), and then run the tests in playback mode to ensure everything is passing. If you're running into issues, check out the [Troubleshooting section](#troubleshooting).
280+
281+
## Troubleshooting
282+
283+
If you run into issues while migrating your package, some of the following troubleshooting steps may help:
284+
285+
### Viewing test proxy log output
286+
287+
`dev-tool` by default outputs logs from the test proxy to `test-proxy-output.log` in your package's root directory. These logs can be inspected to see what requests were made to the proxy tool.
288+
289+
### Viewing more detailed logs by running the proxy tool manually
290+
291+
If you desire, you can run the proxy tool docker image manually before running your tests. This allows you to specify a different log level (debug in the below example), allowing for more detailed logs to be viewed. Do this by running:
292+
293+
```bash
294+
docker run -v <your azure-sdk-for-js repository root>:/srv/testproxy -p 5001:5001 -p 5000:5000 -e Logging__LogLevel__Microsoft=Debug azsdkengsys.azurecr.io/engsys/testproxy-lin:latest
295+
```
296+
297+
Once you've done this, you can run your tests in a separate terminal. `dev-tool` will detect that a test proxy container is already running and will point requests to the Docker container you started.
298+
299+
[docker]: https://docker.com/
300+
[`core-rest-pipeline`]: https://github.com/Azure/azure-sdk-for-js/tree/main/sdk/core/core-rest-pipeline
301+
[`core-http`]: https://github.com/Azure/azure-sdk-for-js/tree/main/sdk/core/core-http
302+
[test proxy server]: https://github.com/Azure/azure-sdk-tools/tree/main/tools/test-proxy

0 commit comments

Comments
 (0)