diff --git a/sdk/test-utils/recorder-new/CHANGELOG.md b/sdk/test-utils/recorder-new/CHANGELOG.md index a80b97cb2bde..2bb8b67fce26 100644 --- a/sdk/test-utils/recorder-new/CHANGELOG.md +++ b/sdk/test-utils/recorder-new/CHANGELOG.md @@ -2,6 +2,15 @@ ## 1.0.0 (Unreleased) +## 2021-12-27 + +- Allows passing `undefined` as keys in the sanitizer options so that devs don't have to add additional checks if a certain env variable exists in playback. +- Exports `delay` + - waits for expected time in record/live modes + - no-op in playback + +[#19561](https://github.com/Azure/azure-sdk-for-js/pull/19561) + ## 2021-12-17 - Refactoring the test proxy http clients for better clarity for the end users [#19446](https://github.com/Azure/azure-sdk-for-js/pull/19446) diff --git a/sdk/test-utils/recorder-new/src/index.ts b/sdk/test-utils/recorder-new/src/index.ts index c9cf1a9cc8dd..3563779f203e 100644 --- a/sdk/test-utils/recorder-new/src/index.ts +++ b/sdk/test-utils/recorder-new/src/index.ts @@ -11,3 +11,4 @@ export { isRecordMode } from "./utils/utils"; export { env } from "./utils/env"; +export { delay } from "./utils/delay"; diff --git a/sdk/test-utils/recorder-new/src/sanitizer.ts b/sdk/test-utils/recorder-new/src/sanitizer.ts index 429f507228e0..089a2bf0fe2c 100644 --- a/sdk/test-utils/recorder-new/src/sanitizer.ts +++ b/sdk/test-utils/recorder-new/src/sanitizer.ts @@ -4,8 +4,10 @@ import { getRealAndFakePairs } from "./utils/connectionStringHelpers"; import { paths } from "./utils/paths"; import { getTestMode, + isRecordMode, ProxyToolSanitizers, RecorderError, + RegexSanitizer, sanitizerKeywordMapping, SanitizerOptions } from "./utils/utils"; @@ -74,12 +76,27 @@ export class Sanitizer { const replacers = options[prop]; if (replacers) { return Promise.all( - replacers.map((replacer: unknown) => - this.addSanitizer({ + replacers.map((replacer: RegexSanitizer) => { + if ( + // sanitizers where the "regex" is a required attribute + [ + "bodyKeySanitizers", + "bodyRegexSanitizers", + "generalRegexSanitizers", + "uriRegexSanitizers" + ].includes(prop) && + !replacer.regex + ) { + if (!isRecordMode()) return; + throw new RecorderError( + `Attempted to add an invalid sanitizer - ${JSON.stringify(replacer)}` + ); + } + return this.addSanitizer({ sanitizer: sanitizerKeywordMapping[prop], body: JSON.stringify(replacer) - }) - ) + }); + }) ); } else return; }) @@ -140,9 +157,18 @@ export class Sanitizer { * - generalRegexSanitizer is applied for each of the parts with the real and fake values that are parsed */ async addConnectionStringSanitizer( - actualConnString: string, + actualConnString: string | undefined, fakeConnString: string ): Promise { + if (!actualConnString) { + if (!isRecordMode()) return; + throw new RecorderError( + `Attempted to add an invalid sanitizer - ${JSON.stringify({ + actualConnString: actualConnString, + fakeConnString: fakeConnString + })}` + ); + } // extract connection string parts and match call const pairsMatched = getRealAndFakePairs(actualConnString, fakeConnString); await this.addSanitizers({ diff --git a/sdk/test-utils/recorder-new/src/utils/delay.ts b/sdk/test-utils/recorder-new/src/utils/delay.ts new file mode 100644 index 000000000000..306ca65443bf --- /dev/null +++ b/sdk/test-utils/recorder-new/src/utils/delay.ts @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { isPlaybackMode } from "./utils"; + +/** + * Usage - `await delay()` + * This `delay` has no effect if the `TEST_MODE` is `"playback"`. + * If the `TEST_MODE` is not `"playback"`, `delay` is a wrapper for setTimeout that resolves a promise after t milliseconds. + * + * @param {number} milliseconds The number of milliseconds to be delayed. + */ +export function delay(milliseconds: number): Promise | void { + if (isPlaybackMode()) { + return; + } + return new Promise((resolve) => setTimeout(resolve, milliseconds)); +} diff --git a/sdk/test-utils/recorder-new/src/utils/utils.ts b/sdk/test-utils/recorder-new/src/utils/utils.ts index d4efb3f30e55..5077dade3a54 100644 --- a/sdk/test-utils/recorder-new/src/utils/utils.ts +++ b/sdk/test-utils/recorder-new/src/utils/utils.ts @@ -95,9 +95,9 @@ export interface RegexSanitizer { */ value: string; /** - * A regex. Can be defined as a simple regex replace OR if groupForReplace is set, a subsitution operation. + * A regex. Can be defined as a simple regex replace OR if groupForReplace is set, a substitution operation. */ - regex: string; + regex?: string; /** * The capture group that needs to be operated upon. Do not set if you're invoking a simple replacement operation. */ @@ -129,15 +129,11 @@ interface BodyKeySanitizer extends RegexSanitizer { * 2) To do a simple regex replace operation, define arguments "key", "value", and "regex" * 3) To do a targeted substitution of a specific group, define all arguments "key", "value", and "regex" */ -interface HeaderRegexSanitizer extends Omit { +interface HeaderRegexSanitizer extends RegexSanitizer { /** * The name of the header we're operating against. */ key: string; - /** - * A regex. Can be defined as a simple regex replace OR if groupForReplace is set, a subsitution operation. - */ - regex?: string; } /** * Internally, @@ -149,7 +145,7 @@ interface ConnectionStringSanitizer { /** * Real connection string with all the secrets */ - actualConnString: string; + actualConnString?: string; /** * Fake connection string - with all the parts of the connection string mapped to fake values */ @@ -181,7 +177,17 @@ export interface SanitizerOptions { * Regardless, there are examples present in `recorder-new/test/testProxyTests.spec.ts`. */ bodyRegexSanitizers?: RegexSanitizer[]; - + /** + * This sanitizer offers regex update of a specific JTokenPath. + * + * EG: "TableName" within a json response body having its value replaced by whatever substitution is offered. + * This simply means that if you are attempting to replace a specific key wholesale, this sanitizer will be simpler + * than configuring a BodyRegexSanitizer that has to match against the full "KeyName": "Value" that is part of the json structure. + * + * Further reading is available [here](https://www.newtonsoft.com/json/help/html/SelectToken.htm#SelectTokenJSONPath). + * + * If the body is NOT a JSON object, this sanitizer will NOT be applied. + */ bodyKeySanitizers?: BodyKeySanitizer[]; /** * TODO diff --git a/sdk/test-utils/recorder-new/test/sanitizers.spec.ts b/sdk/test-utils/recorder-new/test/sanitizers.spec.ts new file mode 100644 index 000000000000..55a5c6c8dc4e --- /dev/null +++ b/sdk/test-utils/recorder-new/test/sanitizers.spec.ts @@ -0,0 +1,370 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { ServiceClient } from "@azure/core-client"; +import { expect } from "chai"; +import { env, isPlaybackMode, Recorder } from "../src"; +import { isRecordMode, RecorderError, TestMode } from "../src/utils/utils"; +import { getTestServerUrl, makeRequestAndVerifyResponse, setTestMode } from "./utils/utils"; + +// These tests require the following to be running in parallel +// - utils/server.ts (to serve requests to act as a service) +// - proxy-tool (to save/mock the responses) +(["record", "playback", "live"] as TestMode[]).forEach((mode) => { + describe(`proxy tool - sanitizers`, () => { + let recorder: Recorder; + let client: ServiceClient; + + before(() => { + setTestMode(mode); + }); + + beforeEach(async function() { + recorder = new Recorder(this.currentTest); + client = new ServiceClient({ baseUri: getTestServerUrl() }); + recorder.configureClient(client); + }); + + afterEach(async () => { + await recorder.stop(); + }); + + describe("Sanitizers - functionalities", () => { + it("GeneralRegexSanitizer", async () => { + env.SECRET_INFO = "abcdef"; + const fakeSecretInfo = "fake_secret_info"; + await recorder.start({ + envSetupForPlayback: { + SECRET_INFO: fakeSecretInfo + } + }); // Adds generalRegexSanitizers by default based on envSetupForPlayback + await makeRequestAndVerifyResponse( + client, + { + path: `/sample_response/${env.SECRET_INFO}`, + method: "GET" + }, + { val: "I am the answer!" } + ); + }); + + it("RemoveHeaderSanitizer", async () => { + await recorder.start({ + envSetupForPlayback: {}, + sanitizerOptions: { + removeHeaderSanitizer: { + headersForRemoval: ["ETag", "Date"] + } + } + }); + await makeRequestAndVerifyResponse( + client, + { path: `/sample_response`, method: "GET" }, + { val: "abc" } + ); + }); + + it("BodyKeySanitizer", async () => { + const secretValue = "ab12cd34ef"; + const fakeSecretValue = "fake_secret_info"; + await recorder.start({ + envSetupForPlayback: {}, + sanitizerOptions: { + bodyKeySanitizers: [ + { + jsonPath: "$.secret_info", // Handles the request body + regex: secretValue, + value: fakeSecretValue + }, + { + jsonPath: "$.bodyProvided.secret_info", // Handles the response body + regex: secretValue, + value: fakeSecretValue + } + ] + } + }); + const reqBody = { + secret_info: isPlaybackMode() ? fakeSecretValue : secretValue + }; + await makeRequestAndVerifyResponse( + client, + { + path: `/api/sample_request_body`, + body: JSON.stringify(reqBody), + method: "POST", + headers: [{ headerName: "Content-Type", value: "application/json" }] + }, + { bodyProvided: reqBody } + ); + }); + + it("BodyRegexSanitizer", async () => { + const secretValue = "ab12cd34ef"; + const fakeSecretValue = "fake_secret_info"; + await recorder.start({ + envSetupForPlayback: {}, + sanitizerOptions: { + bodyRegexSanitizers: [ + { + regex: "(.*)&SECRET=(?[^&]*)&(.*)", + value: fakeSecretValue, + groupForReplace: "secret_content" + } + ] + } + }); + const reqBody = `non_secret=i'm_no_secret&SECRET=${ + isPlaybackMode() ? fakeSecretValue : secretValue + }&random=random`; + await makeRequestAndVerifyResponse( + client, + { + path: `/api/sample_request_body`, + body: reqBody, + method: "POST", + headers: [{ headerName: "Content-Type", value: "text/plain" }] + }, + { bodyProvided: reqBody } + ); + }); + + it("UriRegexSanitizer", async () => { + const secretEndpoint = "host.docker.internal"; + const fakeEndpoint = "fake_endpoint"; + await recorder.start({ + envSetupForPlayback: {}, + sanitizerOptions: { + uriRegexSanitizers: [ + { + regex: secretEndpoint, + value: fakeEndpoint + } + ] + } + }); + const pathToHit = `/api/sample_request_body`; + await makeRequestAndVerifyResponse( + client, + { + url: isPlaybackMode() + ? getTestServerUrl().replace(secretEndpoint, fakeEndpoint) + pathToHit + : undefined, + path: pathToHit, + method: "POST" + }, + { bodyProvided: {} } + ); + }); + + it("UriSubscriptionIdSanitizer", async () => { + const id = "73c83158-bd73-4cda-aa11-a0c2a34e2544"; + const fakeId = "00000000-0000-0000-0000-000000000000"; + await recorder.start({ + envSetupForPlayback: {}, + sanitizerOptions: { + uriSubscriptionIdSanitizer: { + value: fakeId + } + } + }); + await makeRequestAndVerifyResponse( + client, + { + path: `/subscriptions/${isPlaybackMode() ? fakeId : id}`, + method: "GET" + }, + { val: "I am the answer!" } + ); + }); + + it.skip("ContinuationSanitizer", async () => { + // Skipping since the test is failing in the browser + await recorder.start({ + envSetupForPlayback: {}, + sanitizerOptions: { + continuationSanitizers: [ + { + key: "your_uuid", + method: "guid", // What is this method exactly? + resetAfterFirst: false + } + ] + } + }); + // What if the id is part of the response body and not response headers? + + const firstResponse = await makeRequestAndVerifyResponse( + client, + { + path: `/api/sample_uuid_in_header`, + method: "GET" + }, + undefined + ); + + await makeRequestAndVerifyResponse( + client, + { + path: `/sample_response`, + method: "GET", + headers: [ + { + headerName: "your_uuid", + value: firstResponse.headers.get("your_uuid") || "" + } + ] + }, + { val: "abc" } + ); + }); + + it("HeaderRegexSanitizer", async () => { + const sanitizedValue = "Sanitized"; + await recorder.start({ + envSetupForPlayback: {}, + sanitizerOptions: { + headerRegexSanitizers: [ + { + key: "your_uuid", + value: sanitizedValue + } + ] + } + }); + + await makeRequestAndVerifyResponse( + client, + { + path: `/api/sample_uuid_in_header`, + method: "GET" + }, + undefined + ); + // TODO: Add more tests to cover groupForReplace + }); + + // it("OAuthResponseSanitizer", async () => { + // await recorder.start({}); + // await recorder.addSanitizers({ + // oAuthResponseSanitizer: true + // }); + + // await makeRequestAndVerifyResponse(client, + // { + // path: `/api/sample_uuid_in_header`, + // method: "GET" + // }, + // undefined + // ); + // // TODO: Add more tests to cover groupForReplace + // }); + + it.skip("ResetSanitizer (uses BodyRegexSanitizer as example)", async () => { + const secretValue = "ab12cd34ef"; + const fakeSecretValue = "fake_secret_info"; + await recorder.start({ + envSetupForPlayback: {}, + sanitizerOptions: { + bodyRegexSanitizers: [ + { + regex: "(.*)&SECRET=(?[^&]*)&(.*)", + value: fakeSecretValue, + groupForReplace: "secret_content" + } + ] + } + }); + const reqBody = `non_secret=i'm_no_secret&SECRET=${ + isPlaybackMode() ? fakeSecretValue : secretValue + }&random=random`; + await makeRequestAndVerifyResponse( + client, + { + path: `/api/sample_request_body`, + body: reqBody, + method: "POST", + headers: [{ headerName: "Content-Type", value: "text/plain" }] + }, + { bodyProvided: reqBody } + ); + + await recorder.addSanitizers({ + resetSanitizer: true + }); + + const reqBodyAfterReset = `non_secret=i'm_no_secret&SECRET=${secretValue}&random=random`; + // TODO: BUG OBSERVED - The following request should not be sanitized, but is sanitized + await makeRequestAndVerifyResponse( + client, + { + path: `/api/sample_request_body`, + body: reqBodyAfterReset, + method: "POST", + headers: [{ headerName: "Content-Type", value: "text/plain" }] + }, + { bodyProvided: reqBodyAfterReset } + ); + }); + }); + + describe("Sanitizers - handling undefined", () => { + beforeEach(async () => { + await recorder.start({ envSetupForPlayback: {} }); + }); + + const cases = [ + { + options: { + connectionStringSanitizers: [ + { actualConnString: undefined, fakeConnString: "a=b;c=d" } + ], + generalRegexSanitizers: [{ regex: undefined, value: "fake-value" }] + }, + title: "all sanitizers are undefined", + type: "negative" + }, + { + options: { + connectionStringSanitizers: [ + { actualConnString: undefined, fakeConnString: "a=b;c=d" }, + { actualConnString: "1=2,3=4", fakeConnString: "a=b;c=d" } + ], + generalRegexSanitizers: [{ regex: undefined, value: "fake-value" }] + }, + title: "partial sanitizers are undefined", + type: "negative" + }, + { + options: { + connectionStringSanitizers: [ + { actualConnString: "1=2,3=4", fakeConnString: "a=b;c=d" } + ], + generalRegexSanitizers: [{ regex: "value", value: "fake-value" }] + }, + title: "all sanitizers are defined", + type: "positive" + } + ]; + + cases.forEach((testCase) => { + it(`case - ${testCase.title}`, async () => { + try { + await recorder.addSanitizers(testCase.options); + throw new Error("error was not thrown from addSanitizers call"); + } catch (error) { + if (isRecordMode() && testCase.type === "negative") { + expect((error as RecorderError).message).includes( + `Attempted to add an invalid sanitizer` + ); + } else { + expect((error as RecorderError).message).includes( + `error was not thrown from addSanitizers call` + ); + } + } + }); + }); + }); + }); +}); diff --git a/sdk/test-utils/recorder-new/test/testProxyClient.spec.ts b/sdk/test-utils/recorder-new/test/testProxyClient.spec.ts index c367dc773d97..8fabcdf60932 100644 --- a/sdk/test-utils/recorder-new/test/testProxyClient.spec.ts +++ b/sdk/test-utils/recorder-new/test/testProxyClient.spec.ts @@ -316,5 +316,3 @@ describe("State Manager", function() { } }); }); - -// TODO: Can potentially add more tests that use the proxy-tool once we figure out the start/setup scripts for proxy-tool diff --git a/sdk/test-utils/recorder-new/test/testProxyTests.spec.ts b/sdk/test-utils/recorder-new/test/testProxyTests.spec.ts index 50883edf8c6a..36e265ef0db1 100644 --- a/sdk/test-utils/recorder-new/test/testProxyTests.spec.ts +++ b/sdk/test-utils/recorder-new/test/testProxyTests.spec.ts @@ -1,40 +1,10 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -import { - createPipelineRequest, - HttpMethods, - PipelineRequestOptions -} from "@azure/core-rest-pipeline"; import { ServiceClient } from "@azure/core-client"; -import { env, isLiveMode, isPlaybackMode, Recorder } from "../src"; -import { expect } from "chai"; +import { isLiveMode, isPlaybackMode, Recorder } from "../src"; import { TestMode } from "../src/utils/utils"; - -const setTestMode = (mode: TestMode): TestMode => { - env.TEST_MODE = mode; - console.log(`==== setting TEST_MODE = ${mode} ====`); - return mode; -}; - -/** - * Returns the test server url - * Acts as the endpoint [ Works as a substitute to the actual Azure Services ] - */ -function getTestServerUrl() { - // utils/server.ts creates a localhost server at port 8080 - // - In "live" mode, we are hitting directly the localhost endpoint - // - In "record" and "playback" modes, we need to hit the localhost of the host network - // from the proxy tool running in the docker container. - // `host.docker.internal` alias can be used in the docker container to access host's network(localhost) - // - // if PROXY_MANUAL_START=true, we start the proxy tool using the dotnet tool instead of the `docker run` command - // - in this case, we don't need to hit the localhost using the alias - // - needed for the CI since we have difficulties with the mac machines - return !isLiveMode() && !(env.PROXY_MANUAL_START === "true") - ? `http://host.docker.internal:8080` // Accessing host's network(localhost) through docker container - : `http://127.0.0.1:8080`; -} +import { getTestServerUrl, makeRequestAndVerifyResponse, setTestMode } from "./utils/utils"; // These tests require the following to be running in parallel // - utils/server.ts (to serve requests to act as a service) @@ -44,9 +14,6 @@ function getTestServerUrl() { let recorder: Recorder; let client: ServiceClient; - const basePipelineReqOptions: Partial = - mode === "live" ? { allowInsecureConnection: true } : {}; - before(() => { setTestMode(mode); }); @@ -61,40 +28,10 @@ function getTestServerUrl() { await recorder.stop(); }); - async function makeRequestAndVerifyResponse( - request: { - url?: string; - path: string; - body?: string; - headers?: { headerName: string; value: string }[]; - method: HttpMethods; - }, - expectedResponse: { [key: string]: unknown } | undefined - ) { - const req = createPipelineRequest({ - url: request.url ?? getTestServerUrl() + request.path, - body: request.body, - method: request.method, - ...basePipelineReqOptions - }); - request.headers?.forEach(({ headerName, value }) => { - req.headers.set(headerName, value); - }); - const response = await client.sendRequest(req); - if (expectedResponse) { - if (!response.bodyAsText) { - throw new Error("Expected response.bodyAsText to be defined"); - } - - expect(JSON.parse(response.bodyAsText)).to.deep.equal(expectedResponse); - } - // Add code to also check expected headers - return response; - } - it("sample_response", async () => { await recorder.start({ envSetupForPlayback: {} }); await makeRequestAndVerifyResponse( + client, { path: `/sample_response`, method: "GET" }, { val: "abc" } ); @@ -104,6 +41,7 @@ function getTestServerUrl() { await recorder.start({ envSetupForPlayback: {} }); await makeRequestAndVerifyResponse( + client, { path: `/sample_response/${recorder.variable( "random-1", @@ -114,6 +52,7 @@ function getTestServerUrl() { { val: "I am the answer!" } ); await makeRequestAndVerifyResponse( + client, { path: `/sample_response/${recorder.variable("random-2", "known-string")}`, method: "GET" @@ -122,275 +61,7 @@ function getTestServerUrl() { ); }); - describe("Sanitizers", () => { - it("GeneralRegexSanitizer", async () => { - env.SECRET_INFO = "abcdef"; - const fakeSecretInfo = "fake_secret_info"; - await recorder.start({ - envSetupForPlayback: { - SECRET_INFO: fakeSecretInfo - } - }); // Adds generalRegexSanitizers by default based on envSetupForPlayback - await makeRequestAndVerifyResponse( - { - path: `/sample_response/${env.SECRET_INFO}`, - method: "GET" - }, - { val: "I am the answer!" } - ); - }); - - it("RemoveHeaderSanitizer", async () => { - await recorder.start({ - envSetupForPlayback: {}, - sanitizerOptions: { - removeHeaderSanitizer: { - headersForRemoval: ["ETag", "Date"] - } - } - }); - await makeRequestAndVerifyResponse( - { path: `/sample_response`, method: "GET" }, - { val: "abc" } - ); - }); - - it("BodyKeySanitizer", async () => { - const secretValue = "ab12cd34ef"; - const fakeSecretValue = "fake_secret_info"; - await recorder.start({ - envSetupForPlayback: {}, - sanitizerOptions: { - bodyKeySanitizers: [ - { - jsonPath: "$.secret_info", // Handles the request body - regex: secretValue, - value: fakeSecretValue - }, - { - jsonPath: "$.bodyProvided.secret_info", // Handles the response body - regex: secretValue, - value: fakeSecretValue - } - ] - } - }); - const reqBody = { - secret_info: isPlaybackMode() ? fakeSecretValue : secretValue - }; - await makeRequestAndVerifyResponse( - { - path: `/api/sample_request_body`, - body: JSON.stringify(reqBody), - method: "POST", - headers: [{ headerName: "Content-Type", value: "application/json" }] - }, - { bodyProvided: reqBody } - ); - }); - - it("BodyRegexSanitizer", async () => { - const secretValue = "ab12cd34ef"; - const fakeSecretValue = "fake_secret_info"; - await recorder.start({ - envSetupForPlayback: {}, - sanitizerOptions: { - bodyRegexSanitizers: [ - { - regex: "(.*)&SECRET=(?[^&]*)&(.*)", - value: fakeSecretValue, - groupForReplace: "secret_content" - } - ] - } - }); - const reqBody = `non_secret=i'm_no_secret&SECRET=${ - isPlaybackMode() ? fakeSecretValue : secretValue - }&random=random`; - await makeRequestAndVerifyResponse( - { - path: `/api/sample_request_body`, - body: reqBody, - method: "POST", - headers: [{ headerName: "Content-Type", value: "text/plain" }] - }, - { bodyProvided: reqBody } - ); - }); - - it("UriRegexSanitizer", async () => { - const secretEndpoint = "host.docker.internal"; - const fakeEndpoint = "fake_endpoint"; - await recorder.start({ - envSetupForPlayback: {}, - sanitizerOptions: { - uriRegexSanitizers: [ - { - regex: secretEndpoint, - value: fakeEndpoint - } - ] - } - }); - const pathToHit = `/api/sample_request_body`; - await makeRequestAndVerifyResponse( - { - url: isPlaybackMode() - ? getTestServerUrl().replace(secretEndpoint, fakeEndpoint) + pathToHit - : undefined, - path: pathToHit, - method: "POST" - }, - { bodyProvided: {} } - ); - }); - - it("UriSubscriptionIdSanitizer", async () => { - const id = "73c83158-bd73-4cda-aa11-a0c2a34e2544"; - const fakeId = "00000000-0000-0000-0000-000000000000"; - await recorder.start({ - envSetupForPlayback: {}, - sanitizerOptions: { - uriSubscriptionIdSanitizer: { - value: fakeId - } - } - }); - await makeRequestAndVerifyResponse( - { - path: `/subscriptions/${isPlaybackMode() ? fakeId : id}`, - method: "GET" - }, - { val: "I am the answer!" } - ); - }); - - it("ContinuationSanitizer", async () => { - await recorder.start({ - envSetupForPlayback: {}, - sanitizerOptions: { - continuationSanitizers: [ - { - key: "your_uuid", - method: "guid", // What is this method exactly? - resetAfterFirst: false - } - ] - } - }); - // What if the id is part of the response body and not response headers? - - const firstResponse = await makeRequestAndVerifyResponse( - { - path: `/api/sample_uuid_in_header`, - method: "GET" - }, - undefined - ); - - await makeRequestAndVerifyResponse( - { - path: `/sample_response`, - method: "GET", - headers: [ - { - headerName: "your_uuid", - value: firstResponse.headers.get("your_uuid") || "" - } - ] - }, - { val: "abc" } - ); - }); - - it("HeaderRegexSanitizer", async () => { - const sanitizedValue = "Sanitized"; - await recorder.start({ - envSetupForPlayback: {}, - sanitizerOptions: { - headerRegexSanitizers: [ - { - key: "your_uuid", - value: sanitizedValue - } - ] - } - }); - - await makeRequestAndVerifyResponse( - { - path: `/api/sample_uuid_in_header`, - method: "GET" - }, - undefined - ); - // TODO: Add more tests to cover groupForReplace - }); - - // it("OAuthResponseSanitizer", async () => { - // await recorder.start({}); - // await recorder.addSanitizers({ - // oAuthResponseSanitizer: true - // }); - - // await makeRequestAndVerifyResponse( - // { - // path: `/api/sample_uuid_in_header`, - // method: "GET" - // }, - // undefined - // ); - // // TODO: Add more tests to cover groupForReplace - // }); - - it.skip("ResetSanitizer (uses BodyRegexSanitizer as example)", async () => { - const secretValue = "ab12cd34ef"; - const fakeSecretValue = "fake_secret_info"; - await recorder.start({ - envSetupForPlayback: {}, - sanitizerOptions: { - bodyRegexSanitizers: [ - { - regex: "(.*)&SECRET=(?[^&]*)&(.*)", - value: fakeSecretValue, - groupForReplace: "secret_content" - } - ] - } - }); - const reqBody = `non_secret=i'm_no_secret&SECRET=${ - isPlaybackMode() ? fakeSecretValue : secretValue - }&random=random`; - await makeRequestAndVerifyResponse( - { - path: `/api/sample_request_body`, - body: reqBody, - method: "POST", - headers: [{ headerName: "Content-Type", value: "text/plain" }] - }, - { bodyProvided: reqBody } - ); - - await recorder.addSanitizers({ - resetSanitizer: true - }); - - const reqBodyAfterReset = `non_secret=i'm_no_secret&SECRET=${secretValue}&random=random`; - // TODO: BUG OBSERVED - The following request should not be sanitized, but is sanitized - await makeRequestAndVerifyResponse( - { - path: `/api/sample_request_body`, - body: reqBodyAfterReset, - method: "POST", - headers: [{ headerName: "Content-Type", value: "text/plain" }] - }, - { bodyProvided: reqBodyAfterReset } - ); - }); - }); - // Matchers - describe("Matchers", () => { it("BodilessMatcher", async () => { await recorder.start({ envSetupForPlayback: {} }); @@ -401,6 +72,7 @@ function getTestServerUrl() { const body = isPlaybackMode() ? "playback" : "record"; await makeRequestAndVerifyResponse( + client, { path: `/sample_response`, body, @@ -421,6 +93,7 @@ function getTestServerUrl() { }; await makeRequestAndVerifyResponse( + client, { path: `/sample_response`, body: "body", diff --git a/sdk/test-utils/recorder-new/test/utils/utils.ts b/sdk/test-utils/recorder-new/test/utils/utils.ts new file mode 100644 index 000000000000..c8557cfb6012 --- /dev/null +++ b/sdk/test-utils/recorder-new/test/utils/utils.ts @@ -0,0 +1,62 @@ +import { createPipelineRequest, HttpMethods } from "@azure/core-rest-pipeline"; +import { expect } from "chai"; +import { env } from "../../src"; +import { isLiveMode, TestMode } from "../../src/utils/utils"; +import { ServiceClient } from "@azure/core-client"; + +export const setTestMode = (mode: TestMode): TestMode => { + env.TEST_MODE = mode; + console.log(`==== setting TEST_MODE = ${mode} ====`); + return mode; +}; + +/** + * Returns the test server url + * Acts as the endpoint [ Works as a substitute to the actual Azure Services ] + */ +export function getTestServerUrl() { + // utils/server.ts creates a localhost server at port 8080 + // - In "live" mode, we are hitting directly the localhost endpoint + // - In "record" and "playback" modes, we need to hit the localhost of the host network + // from the proxy tool running in the docker container. + // `host.docker.internal` alias can be used in the docker container to access host's network(localhost) + // + // if PROXY_MANUAL_START=true, we start the proxy tool using the dotnet tool instead of the `docker run` command + // - in this case, we don't need to hit the localhost using the alias + // - needed for the CI since we have difficulties with the mac machines + return !isLiveMode() && !(env.PROXY_MANUAL_START === "true") + ? `http://host.docker.internal:8080` // Accessing host's network(localhost) through docker container + : `http://127.0.0.1:8080`; +} + +export async function makeRequestAndVerifyResponse( + client: ServiceClient, + request: { + url?: string; + path: string; + body?: string; + headers?: { headerName: string; value: string }[]; + method: HttpMethods; + }, + expectedResponse: { [key: string]: unknown } | undefined +) { + const req = createPipelineRequest({ + url: request.url ?? getTestServerUrl() + request.path, + body: request.body, + method: request.method, + allowInsecureConnection: isLiveMode() + }); + request.headers?.forEach(({ headerName, value }) => { + req.headers.set(headerName, value); + }); + const response = await client.sendRequest(req); + if (expectedResponse) { + if (!response.bodyAsText) { + throw new Error("Expected response.bodyAsText to be defined"); + } + + expect(JSON.parse(response.bodyAsText)).to.deep.equal(expectedResponse); + } + // Add code to also check expected headers + return response; +} diff --git a/sdk/test-utils/testing-recorder-new/test/core-v2-test.spec.ts b/sdk/test-utils/testing-recorder-new/test/core-v2-test.spec.ts index 7b9287ba9bc1..26cb21b2e5e2 100644 --- a/sdk/test-utils/testing-recorder-new/test/core-v2-test.spec.ts +++ b/sdk/test-utils/testing-recorder-new/test/core-v2-test.spec.ts @@ -11,7 +11,7 @@ const fakeConnString = const sanitizerOptions: SanitizerOptions = { connectionStringSanitizers: [ { - actualConnString: env.TABLES_SAS_CONNECTION_STRING || "undefined", + actualConnString: env.TABLES_SAS_CONNECTION_STRING, fakeConnString } ], diff --git a/sdk/test-utils/testing-recorder-new/test/noOpCredentialTest.spec.ts b/sdk/test-utils/testing-recorder-new/test/noOpCredentialTest.spec.ts index c16fc30c6aed..d2b49ab5c6b0 100644 --- a/sdk/test-utils/testing-recorder-new/test/noOpCredentialTest.spec.ts +++ b/sdk/test-utils/testing-recorder-new/test/noOpCredentialTest.spec.ts @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -import { RecorderStartOptions, Recorder, isRecordMode } from "@azure-tools/test-recorder-new"; +import { RecorderStartOptions, Recorder, env } from "@azure-tools/test-recorder-new"; import { createTestCredential } from "@azure-tools/test-credential"; import { TokenCredential } from "@azure/core-auth"; import { TableServiceClient } from "@azure/data-tables"; @@ -15,16 +15,14 @@ const getRecorderStartOptions = (): RecorderStartOptions => { AZURE_CLIENT_SECRET: "azure_client_secret", AZURE_TENANT_ID: "azuretenantid" }, - sanitizerOptions: isRecordMode() - ? { - bodyRegexSanitizers: [ - { - regex: encodeURIComponent(assertEnvironmentVariable("TABLES_URL")), - value: encodeURIComponent(`https://fakeaccount.table.core.windows.net`) - } - ] + sanitizerOptions: { + bodyRegexSanitizers: [ + { + regex: env.TABLES_URL ? encodeURIComponent(env.TABLES_URL) : undefined, + value: encodeURIComponent(`https://fakeaccount.table.core.windows.net`) } - : {} + ] + } }; };