Skip to content

Commit 694ac67

Browse files
[Recorder] Add custom matcher support (#20404)
* CustomDefaultMatcher * changelog * Maor's improvements
1 parent c0b30fe commit 694ac67

5 files changed

Lines changed: 185 additions & 7 deletions

File tree

sdk/test-utils/recorder/CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,13 @@
22

33
## 2.0.0 (Unreleased)
44

5+
## 2022-02-15
6+
7+
- Bug Fix - Fixed the bug where the `recordingId` was being ignored in the add-sanitizer requests which led the test level sanitizers to be treated as session level sanitizers.
8+
[#20393](https://github.com/Azure/azure-sdk-for-js/pull/20393)
9+
- `CustomDefaultMatcher`- exposes the default matcher in a customizable way. Currently, this includes enabling/disabling body match, adding additional excluded headers, and enable/disable matching the order of query params in the requests.
10+
[#20404](https://github.com/Azure/azure-sdk-for-js/pull/20404)
11+
512
## 2022-02-04
613

714
- [#19920](https://github.com/Azure/azure-sdk-for-js/pull/19920) Added support for adding polices as part of the client options with the new "additionalPolicies" array.

sdk/test-utils/recorder/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,4 @@ export {
1313
} from "./utils/utils";
1414
export { env } from "./utils/env";
1515
export { delay } from "./utils/delay";
16+
export { CustomMatcherOptions } from "./matcher";

sdk/test-utils/recorder/src/matcher.ts

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,49 @@ import { createPipelineRequest, HttpClient } from "@azure/core-rest-pipeline";
55
import { paths } from "./utils/paths";
66
import { RecorderError } from "./utils/utils";
77

8-
export type Matcher = "HeaderlessMatcher" | "BodilessMatcher";
8+
export type Matcher = "HeaderlessMatcher" | "BodilessMatcher" | "CustomDefaultMatcher";
9+
10+
/**
11+
* Body the customer matcher expects.
12+
*/
13+
export interface CustomMatcherOptions {
14+
/**
15+
* Should the body value be compared during lookup operations?
16+
*/
17+
compareBodies?: boolean;
18+
/**
19+
* Array of additional headers that should be excluded during matching.
20+
*/
21+
excludedHeaders?: string[];
22+
/**
23+
* By default, the test-proxy does not sort query params before matching. Setting true will sort query params alphabetically before comparing URI.
24+
*/
25+
ignoreQueryOrdering?: boolean;
26+
}
27+
28+
/**
29+
* Body the customer matcher expects.
30+
*
31+
* // Ignored Headers option is not exposed to the users as of now.
32+
* // If needed, this can be moved into CustomMatcherOptions.
33+
*/
34+
interface InternalCustomMatcherOptions extends CustomMatcherOptions {
35+
/**
36+
* Array of additional headers that should be ignored during matching.
37+
* Any headers that are "ignored" will not do value comparison when matching.
38+
* This means that if the recording has a header that isn't in the request, a test mismatch exception will be thrown noting the lack of header in the request.
39+
*
40+
* This also applies if the header is present in the request but not recording.
41+
*/
42+
ignoredHeaders?: string[];
43+
}
944

1045
export async function setMatcher(
1146
recorderUrl: string,
1247
httpClient: HttpClient,
1348
matcher: Matcher,
14-
recordingId?: string
49+
recordingId?: string,
50+
matcherBody: InternalCustomMatcherOptions = { compareBodies: true, ignoreQueryOrdering: false }
1551
): Promise<void> {
1652
const url = `${recorderUrl}${paths.admin}${paths.setMatcher}`;
1753

@@ -20,7 +56,14 @@ export async function setMatcher(
2056
if (recordingId) {
2157
request.headers.set("x-recording-id", recordingId);
2258
}
23-
59+
if (matcherBody) {
60+
request.body = JSON.stringify({
61+
compareBodies: matcherBody.compareBodies,
62+
excludedHeaders: matcherBody.excludedHeaders?.toString(),
63+
ignoredHeaders: matcherBody.ignoredHeaders?.toString(),
64+
ignoreQueryOrdering: matcherBody.ignoreQueryOrdering,
65+
});
66+
}
2467
const { status, bodyAsText } = await httpClient.sendRequest(request);
2568

2669
if (status < 200 || status > 299) {

sdk/test-utils/recorder/src/recorder.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import { SanitizerOptions } from "./utils/utils";
2626
import { paths } from "./utils/paths";
2727
import { addSanitizers, transformsInfo } from "./sanitizer";
2828
import { handleEnvSetup } from "./utils/envSetupForPlayback";
29-
import { Matcher, setMatcher } from "./matcher";
29+
import { CustomMatcherOptions, Matcher, setMatcher } from "./matcher";
3030
import {
3131
DefaultHttpClient,
3232
HttpClient as HttpClientCoreV1,
@@ -204,13 +204,21 @@ export class Recorder {
204204
/**
205205
* Sets the matcher for the current recording to the matcher specified.
206206
*/
207-
async setMatcher(matcher: Matcher): Promise<void> {
207+
async setMatcher(matcher: "HeaderlessMatcher" | "BodilessMatcher"): Promise<void>;
208+
/**
209+
* Sets the matcher for the current recording to the matcher specified.
210+
*/
211+
async setMatcher(matcher: "CustomDefaultMatcher", options?: CustomMatcherOptions): Promise<void>;
212+
/**
213+
* Sets the matcher for the current recording to the matcher specified.
214+
*/
215+
async setMatcher(matcher: Matcher, options?: CustomMatcherOptions): Promise<void> {
208216
if (isPlaybackMode()) {
209217
if (!this.httpClient) {
210218
throw new RecorderError("httpClient should be defined in playback mode");
211219
}
212220

213-
await setMatcher(this.url, this.httpClient, matcher, this.recordingId);
221+
await setMatcher(this.url, this.httpClient, matcher, this.recordingId, options);
214222
}
215223
}
216224

sdk/test-utils/recorder/test/testProxyTests.spec.ts

Lines changed: 120 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// Licensed under the MIT license.
33

44
import { ServiceClient } from "@azure/core-client";
5-
import { isPlaybackMode, Recorder } from "../src";
5+
import { CustomMatcherOptions, isPlaybackMode, Recorder } from "../src";
66
import { isLiveMode, TestMode } from "../src/utils/utils";
77
import { getTestServerUrl, makeRequestAndVerifyResponse, setTestMode } from "./utils/utils";
88

@@ -102,6 +102,125 @@ import { getTestServerUrl, makeRequestAndVerifyResponse, setTestMode } from "./u
102102
{ val: "abc" }
103103
);
104104
});
105+
106+
describe("CustomDefaultMatcher", () => {
107+
it("excludedHeaders - header value is different", async () => {
108+
const headerName = `X-Test-Dynamic-Header`;
109+
await recorder.start({ envSetupForPlayback: {} });
110+
await recorder.setMatcher("CustomDefaultMatcher", {
111+
excludedHeaders: [headerName],
112+
});
113+
114+
const testHeader = {
115+
headerName, // dynamic header
116+
value: isPlaybackMode() ? "playback" : "record",
117+
};
118+
119+
await makeRequestAndVerifyResponse(
120+
client,
121+
{
122+
path: `/sample_response`,
123+
body: "body",
124+
method: "POST",
125+
headers: [{ headerName: "Content-Type", value: "text/plain" }, testHeader],
126+
},
127+
{ val: "abc" }
128+
);
129+
});
130+
131+
it("excludedHeaders - header is non-existent", async () => {
132+
const headerName = `X-Test-Dynamic-Header`;
133+
await recorder.start({ envSetupForPlayback: {} });
134+
await recorder.setMatcher("CustomDefaultMatcher", {
135+
excludedHeaders: [headerName],
136+
});
137+
138+
const testHeader = {
139+
headerName, // dynamic header
140+
value: "record",
141+
};
142+
143+
await makeRequestAndVerifyResponse(
144+
client,
145+
{
146+
path: `/sample_response`,
147+
body: "body",
148+
method: "POST",
149+
headers: [{ headerName: "Content-Type", value: "text/plain" }].concat(
150+
!isPlaybackMode() ? [testHeader] : []
151+
),
152+
},
153+
{ val: "abc" }
154+
);
155+
});
156+
157+
it("ignoredHeaders", async () => {
158+
const headerName = `X-Test-Dynamic-Header`;
159+
await recorder.start({ envSetupForPlayback: {} });
160+
await recorder.setMatcher("CustomDefaultMatcher", {
161+
ignoredHeaders: [headerName],
162+
} as CustomMatcherOptions);
163+
164+
const testHeader = {
165+
headerName, // dynamic header
166+
value: isPlaybackMode() ? "playback" : "record",
167+
};
168+
169+
await makeRequestAndVerifyResponse(
170+
client,
171+
{
172+
path: `/sample_response`,
173+
body: "body",
174+
method: "POST",
175+
headers: [{ headerName: "Content-Type", value: "text/plain" }, testHeader],
176+
},
177+
{ val: "abc" }
178+
);
179+
});
180+
181+
it("compareBodies", async () => {
182+
await recorder.start({ envSetupForPlayback: {} });
183+
await recorder.setMatcher("CustomDefaultMatcher", {
184+
compareBodies: false,
185+
ignoredHeaders: ["Content-Length"], // adding this header since the body sizes are different
186+
} as CustomMatcherOptions);
187+
188+
// The body shouldn't matter for the match; verify this by using a
189+
// different body in playback vs record mode.
190+
const body = isPlaybackMode() ? "playback" : "record";
191+
192+
await makeRequestAndVerifyResponse(
193+
client,
194+
{
195+
path: `/sample_response`,
196+
body,
197+
method: "POST",
198+
headers: [{ headerName: "Content-Type", value: "text/plain" }],
199+
},
200+
{ val: "abc" }
201+
);
202+
});
203+
204+
it("ignoreQueryOrdering", async () => {
205+
await recorder.start({ envSetupForPlayback: {} });
206+
await recorder.setMatcher("CustomDefaultMatcher", {
207+
ignoreQueryOrdering: true,
208+
});
209+
210+
await makeRequestAndVerifyResponse(
211+
client,
212+
{
213+
path: `/sample_response${
214+
isPlaybackMode() ? "?first=abc&second=def" : "?second=def&first=abc"
215+
}`,
216+
body: undefined,
217+
method: "POST",
218+
headers: [{ headerName: "Content-Type", value: "text/plain" }],
219+
},
220+
{ val: "abc" }
221+
);
222+
});
223+
});
105224
});
106225

107226
// Transforms

0 commit comments

Comments
 (0)