|
| 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