Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 46 additions & 2 deletions packages/spec-api/src/request-validations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
};
Expand Down Expand Up @@ -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,
Expand Down
68 changes: 68 additions & 0 deletions packages/spec-api/test/expectation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
});