From 13fba508f92643a64b90c10d7773f2448e6d5112 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 19 Nov 2025 20:00:26 +0000 Subject: [PATCH 1/5] Initial plan From bf4a22d7792a4327cdd87f4db763a1ab2aa00e73 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 19 Nov 2025 20:10:04 +0000 Subject: [PATCH 2/5] Fix numeric string comparison in header and query param validation Co-authored-by: JoshLove-msft <54595583+JoshLove-msft@users.noreply.github.com> --- packages/spec-api/src/request-validations.ts | 48 +++++++++++++- packages/spec-api/test/expectation.test.ts | 68 ++++++++++++++++++++ 2 files changed, 114 insertions(+), 2 deletions(-) diff --git a/packages/spec-api/src/request-validations.ts b/packages/spec-api/src/request-validations.ts index 223b439ce2a..79eef9be5ac 100644 --- a/packages/spec-api/src/request-validations.ts +++ b/packages/spec-api/src/request-validations.ts @@ -122,12 +122,49 @@ const isBodyEmpty = (body: string | Buffer | undefined | null) => { return body == null || body === "" || body.length === 0; }; +/** + * Check if a string represents a numeric value. + * @param value String to check + * @returns true if the string is a valid number representation + */ +const isNumericString = (value: string): boolean => { + return value !== "" && !isNaN(Number(value)); +}; + +/** + * Compare two values, treating numeric strings as numbers. + * This allows "150" and "150.0" to be considered equal. + * @param actual Actual value (can be undefined or various types from Express) + * @param expected Expected value + * @returns true if values are equal (including numeric string comparison) + */ +const areValuesEqual = (actual: any, expected: string): boolean => { + // Handle undefined or null + if (actual == null) { + return false; + } + + // Convert actual to string for comparison + const actualStr = String(actual); + + if (actualStr === expected) { + return true; + } + + // If both values are numeric strings, compare them as numbers + if (isNumericString(actualStr) && isNumericString(expected)) { + return Number(actualStr) === Number(expected); + } + + return false; +}; + /** * Check whether the request header contains the given name/value pair */ export const validateHeader = (request: RequestExt, headerName: string, expected: string): void => { const actual = request.headers[headerName]; - if (actual !== expected) { + if (!areValuesEqual(actual, expected)) { throw new ValidationError(`Expected ${expected} but got ${actual}`, expected, actual); } }; @@ -165,7 +202,14 @@ export const validateQueryParam = ( actual, ); } - } else if (actual !== expected) { + } else if (typeof expected === "string" && !areValuesEqual(actual, expected)) { + throw new ValidationError( + `Expected query param ${paramName}=${expected} but got ${actual}`, + expected, + actual, + ); + } else if (typeof expected !== "string") { + // If expected is an array but no collectionFormat was provided, treat as error throw new ValidationError( `Expected query param ${paramName}=${expected} but got ${actual}`, expected, diff --git a/packages/spec-api/test/expectation.test.ts b/packages/spec-api/test/expectation.test.ts index 77600bfc7d9..2c381fb96dc 100644 --- a/packages/spec-api/test/expectation.test.ts +++ b/packages/spec-api/test/expectation.test.ts @@ -50,4 +50,72 @@ describe("containsQueryParam()", () => { const requestExpectation = new RequestExpectation(requestExt); expect(requestExpectation.containsQueryParam("letter", "[a, b, c]")).toBe(undefined); }); + + it("should validate successfully when numeric strings match exactly", () => { + const requestExt = { query: { value: "150.0" } } as unknown as RequestExt; + const requestExpectation = new RequestExpectation(requestExt); + expect(requestExpectation.containsQueryParam("value", "150.0")).toBe(undefined); + }); + + it("should validate successfully when numeric strings are numerically equal", () => { + const requestExt = { query: { value: "150" } } as unknown as RequestExt; + const requestExpectation = new RequestExpectation(requestExt); + expect(requestExpectation.containsQueryParam("value", "150.0")).toBe(undefined); + }); + + it("should validate successfully when numeric strings with trailing zero match", () => { + const requestExt = { query: { value: "210000.0" } } as unknown as RequestExt; + const requestExpectation = new RequestExpectation(requestExt); + expect(requestExpectation.containsQueryParam("value", "210000")).toBe(undefined); + }); + + it("should throw validation error when numeric values are different", () => { + const requestExt = { query: { value: "150" } } as unknown as RequestExt; + const requestExpectation = new RequestExpectation(requestExt); + expect(() => requestExpectation.containsQueryParam("value", "151")).toThrow(); + }); + + it("should throw validation error when non-numeric strings don't match", () => { + const requestExt = { query: { value: "hello" } } as unknown as RequestExt; + const requestExpectation = new RequestExpectation(requestExt); + expect(() => requestExpectation.containsQueryParam("value", "world")).toThrow(); + }); +}); + +describe("containsHeader()", () => { + it("should validate successfully when header matches exactly", () => { + const requestExt = { headers: { duration: "P40D" } } as unknown as RequestExt; + const requestExpectation = new RequestExpectation(requestExt); + expect(requestExpectation.containsHeader("duration", "P40D")).toBe(undefined); + }); + + it("should validate successfully when numeric header strings match exactly", () => { + const requestExt = { headers: { duration: "150.0" } } as unknown as RequestExt; + const requestExpectation = new RequestExpectation(requestExt); + expect(requestExpectation.containsHeader("duration", "150.0")).toBe(undefined); + }); + + it("should validate successfully when numeric header strings are numerically equal", () => { + const requestExt = { headers: { duration: "150" } } as unknown as RequestExt; + const requestExpectation = new RequestExpectation(requestExt); + expect(requestExpectation.containsHeader("duration", "150.0")).toBe(undefined); + }); + + it("should validate successfully when numeric header strings with trailing zero match", () => { + const requestExt = { headers: { duration: "210000.0" } } as unknown as RequestExt; + const requestExpectation = new RequestExpectation(requestExt); + expect(requestExpectation.containsHeader("duration", "210000")).toBe(undefined); + }); + + it("should throw validation error when numeric header values are different", () => { + const requestExt = { headers: { duration: "150" } } as unknown as RequestExt; + const requestExpectation = new RequestExpectation(requestExt); + expect(() => requestExpectation.containsHeader("duration", "151")).toThrow(); + }); + + it("should throw validation error when non-numeric header strings don't match", () => { + const requestExt = { headers: { duration: "P40D" } } as unknown as RequestExt; + const requestExpectation = new RequestExpectation(requestExt); + expect(() => requestExpectation.containsHeader("duration", "P50D")).toThrow(); + }); }); From c93998c2be8523df18a1edc467afa113367eebdb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 19 Nov 2025 20:55:44 +0000 Subject: [PATCH 3/5] Fix EncodeDuration tests by removing trailing .0 from expected values in mockapi Co-authored-by: JoshLove-msft <54595583+JoshLove-msft@users.noreply.github.com> --- .../specs/encode/duration/mockapi.ts | 12 ++-- packages/spec-api/src/request-validations.ts | 48 +------------ packages/spec-api/test/expectation.test.ts | 68 ------------------- 3 files changed, 8 insertions(+), 120 deletions(-) diff --git a/packages/http-specs/specs/encode/duration/mockapi.ts b/packages/http-specs/specs/encode/duration/mockapi.ts index 157858bc68c..ebbe35c126d 100644 --- a/packages/http-specs/specs/encode/duration/mockapi.ts +++ b/packages/http-specs/specs/encode/duration/mockapi.ts @@ -231,7 +231,7 @@ Scenarios.Encode_Duration_Query_floatSecondsLargerUnit = createQueryServerTests( { input: 150.0, }, - "150.0", + "150", ); Scenarios.Encode_Duration_Query_int32MillisecondsLargerUnit = createQueryServerTests( "/encode/duration/query/int32-milliseconds-larger-unit", @@ -245,7 +245,7 @@ Scenarios.Encode_Duration_Query_floatMillisecondsLargerUnit = createQueryServerT { input: 210000.0, }, - "210000.0", + "210000", ); function createHeaderServerTests(uri: string, headersData: any, value: any) { @@ -343,9 +343,9 @@ Scenarios.Encode_Duration_Header_int32SecondsLargerUnit = createHeaderServerTest Scenarios.Encode_Duration_Header_floatSecondsLargerUnit = createHeaderServerTests( "/encode/duration/header/float-seconds-larger-unit", { - duration: "150.0", + duration: "150", }, - "150.0", + "150", ); Scenarios.Encode_Duration_Header_int32MillisecondsLargerUnit = createHeaderServerTests( "/encode/duration/header/int32-milliseconds-larger-unit", @@ -357,7 +357,7 @@ Scenarios.Encode_Duration_Header_int32MillisecondsLargerUnit = createHeaderServe Scenarios.Encode_Duration_Header_floatMillisecondsLargerUnit = createHeaderServerTests( "/encode/duration/header/float-milliseconds-larger-unit", { - duration: "210000.0", + duration: "210000", }, - "210000.0", + "210000", ); diff --git a/packages/spec-api/src/request-validations.ts b/packages/spec-api/src/request-validations.ts index 79eef9be5ac..223b439ce2a 100644 --- a/packages/spec-api/src/request-validations.ts +++ b/packages/spec-api/src/request-validations.ts @@ -122,49 +122,12 @@ const isBodyEmpty = (body: string | Buffer | undefined | null) => { return body == null || body === "" || body.length === 0; }; -/** - * Check if a string represents a numeric value. - * @param value String to check - * @returns true if the string is a valid number representation - */ -const isNumericString = (value: string): boolean => { - return value !== "" && !isNaN(Number(value)); -}; - -/** - * Compare two values, treating numeric strings as numbers. - * This allows "150" and "150.0" to be considered equal. - * @param actual Actual value (can be undefined or various types from Express) - * @param expected Expected value - * @returns true if values are equal (including numeric string comparison) - */ -const areValuesEqual = (actual: any, expected: string): boolean => { - // Handle undefined or null - if (actual == null) { - return false; - } - - // Convert actual to string for comparison - const actualStr = String(actual); - - if (actualStr === expected) { - return true; - } - - // If both values are numeric strings, compare them as numbers - if (isNumericString(actualStr) && isNumericString(expected)) { - return Number(actualStr) === Number(expected); - } - - return false; -}; - /** * Check whether the request header contains the given name/value pair */ export const validateHeader = (request: RequestExt, headerName: string, expected: string): void => { const actual = request.headers[headerName]; - if (!areValuesEqual(actual, expected)) { + if (actual !== expected) { throw new ValidationError(`Expected ${expected} but got ${actual}`, expected, actual); } }; @@ -202,14 +165,7 @@ export const validateQueryParam = ( actual, ); } - } else if (typeof expected === "string" && !areValuesEqual(actual, expected)) { - throw new ValidationError( - `Expected query param ${paramName}=${expected} but got ${actual}`, - expected, - actual, - ); - } else if (typeof expected !== "string") { - // If expected is an array but no collectionFormat was provided, treat as error + } else if (actual !== expected) { throw new ValidationError( `Expected query param ${paramName}=${expected} but got ${actual}`, expected, diff --git a/packages/spec-api/test/expectation.test.ts b/packages/spec-api/test/expectation.test.ts index 2c381fb96dc..77600bfc7d9 100644 --- a/packages/spec-api/test/expectation.test.ts +++ b/packages/spec-api/test/expectation.test.ts @@ -50,72 +50,4 @@ describe("containsQueryParam()", () => { const requestExpectation = new RequestExpectation(requestExt); expect(requestExpectation.containsQueryParam("letter", "[a, b, c]")).toBe(undefined); }); - - it("should validate successfully when numeric strings match exactly", () => { - const requestExt = { query: { value: "150.0" } } as unknown as RequestExt; - const requestExpectation = new RequestExpectation(requestExt); - expect(requestExpectation.containsQueryParam("value", "150.0")).toBe(undefined); - }); - - it("should validate successfully when numeric strings are numerically equal", () => { - const requestExt = { query: { value: "150" } } as unknown as RequestExt; - const requestExpectation = new RequestExpectation(requestExt); - expect(requestExpectation.containsQueryParam("value", "150.0")).toBe(undefined); - }); - - it("should validate successfully when numeric strings with trailing zero match", () => { - const requestExt = { query: { value: "210000.0" } } as unknown as RequestExt; - const requestExpectation = new RequestExpectation(requestExt); - expect(requestExpectation.containsQueryParam("value", "210000")).toBe(undefined); - }); - - it("should throw validation error when numeric values are different", () => { - const requestExt = { query: { value: "150" } } as unknown as RequestExt; - const requestExpectation = new RequestExpectation(requestExt); - expect(() => requestExpectation.containsQueryParam("value", "151")).toThrow(); - }); - - it("should throw validation error when non-numeric strings don't match", () => { - const requestExt = { query: { value: "hello" } } as unknown as RequestExt; - const requestExpectation = new RequestExpectation(requestExt); - expect(() => requestExpectation.containsQueryParam("value", "world")).toThrow(); - }); -}); - -describe("containsHeader()", () => { - it("should validate successfully when header matches exactly", () => { - const requestExt = { headers: { duration: "P40D" } } as unknown as RequestExt; - const requestExpectation = new RequestExpectation(requestExt); - expect(requestExpectation.containsHeader("duration", "P40D")).toBe(undefined); - }); - - it("should validate successfully when numeric header strings match exactly", () => { - const requestExt = { headers: { duration: "150.0" } } as unknown as RequestExt; - const requestExpectation = new RequestExpectation(requestExt); - expect(requestExpectation.containsHeader("duration", "150.0")).toBe(undefined); - }); - - it("should validate successfully when numeric header strings are numerically equal", () => { - const requestExt = { headers: { duration: "150" } } as unknown as RequestExt; - const requestExpectation = new RequestExpectation(requestExt); - expect(requestExpectation.containsHeader("duration", "150.0")).toBe(undefined); - }); - - it("should validate successfully when numeric header strings with trailing zero match", () => { - const requestExt = { headers: { duration: "210000.0" } } as unknown as RequestExt; - const requestExpectation = new RequestExpectation(requestExt); - expect(requestExpectation.containsHeader("duration", "210000")).toBe(undefined); - }); - - it("should throw validation error when numeric header values are different", () => { - const requestExt = { headers: { duration: "150" } } as unknown as RequestExt; - const requestExpectation = new RequestExpectation(requestExt); - expect(() => requestExpectation.containsHeader("duration", "151")).toThrow(); - }); - - it("should throw validation error when non-numeric header strings don't match", () => { - const requestExt = { headers: { duration: "P40D" } } as unknown as RequestExt; - const requestExpectation = new RequestExpectation(requestExt); - expect(() => requestExpectation.containsHeader("duration", "P50D")).toThrow(); - }); }); From e5b67685b353e0f6102c2ab8576523853ddf4cef Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 19 Nov 2025 21:54:36 +0000 Subject: [PATCH 4/5] Make query param expectations match input format as numbers Co-authored-by: JoshLove-msft <54595583+JoshLove-msft@users.noreply.github.com> --- packages/http-specs/specs/encode/duration/mockapi.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/http-specs/specs/encode/duration/mockapi.ts b/packages/http-specs/specs/encode/duration/mockapi.ts index ebbe35c126d..e77b3f4ab90 100644 --- a/packages/http-specs/specs/encode/duration/mockapi.ts +++ b/packages/http-specs/specs/encode/duration/mockapi.ts @@ -229,9 +229,9 @@ Scenarios.Encode_Duration_Query_int32SecondsLargerUnit = createQueryServerTests( Scenarios.Encode_Duration_Query_floatSecondsLargerUnit = createQueryServerTests( "/encode/duration/query/float-seconds-larger-unit", { - input: 150.0, + input: 150, }, - "150", + 150, ); Scenarios.Encode_Duration_Query_int32MillisecondsLargerUnit = createQueryServerTests( "/encode/duration/query/int32-milliseconds-larger-unit", @@ -243,9 +243,9 @@ Scenarios.Encode_Duration_Query_int32MillisecondsLargerUnit = createQueryServerT Scenarios.Encode_Duration_Query_floatMillisecondsLargerUnit = createQueryServerTests( "/encode/duration/query/float-milliseconds-larger-unit", { - input: 210000.0, + input: 210000, }, - "210000", + 210000, ); function createHeaderServerTests(uri: string, headersData: any, value: any) { From 258442a4141fc22e9cfec63f7899a083add3a19d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 25 Nov 2025 04:19:36 +0000 Subject: [PATCH 5/5] Add chronus change documentation for http-specs fix Co-authored-by: JoshLove-msft <54595583+JoshLove-msft@users.noreply.github.com> --- .../fix-encode-duration-tests-2025-11-25-04-18-59.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .chronus/changes/fix-encode-duration-tests-2025-11-25-04-18-59.md diff --git a/.chronus/changes/fix-encode-duration-tests-2025-11-25-04-18-59.md b/.chronus/changes/fix-encode-duration-tests-2025-11-25-04-18-59.md new file mode 100644 index 00000000000..16a7b59f680 --- /dev/null +++ b/.chronus/changes/fix-encode-duration-tests-2025-11-25-04-18-59.md @@ -0,0 +1,7 @@ +--- +changeKind: fix +packages: + - "@typespec/http-specs" +--- + +Fix EncodeDuration tests with larger unit durations being too strict by making query parameter expectations match input types as numbers instead of strings