Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { url } from "../../src/core";
import { toJson } from "../../src/core/json";
import { withFormUrlEncoded } from "./withFormUrlEncoded";
import { withHeaders } from "./withHeaders";
import { withJson } from "./withJson";
import { withJson, type WithJsonOptions } from "./withJson";

type HttpMethod = "all" | "get" | "post" | "put" | "delete" | "patch" | "options" | "head";

Expand All @@ -26,7 +26,7 @@ interface RequestHeadersStage extends RequestBodyStage, ResponseStage {
}

interface RequestBodyStage extends ResponseStage {
jsonBody(body: unknown): ResponseStage;
jsonBody(body: unknown, options?: WithJsonOptions): ResponseStage;
formUrlEncodedBody(body: unknown): ResponseStage;
}

Expand Down Expand Up @@ -129,11 +129,11 @@ class RequestBuilder implements MethodStage, RequestHeadersStage, RequestBodySta
return this;
}

jsonBody(body: unknown): ResponseStage {
jsonBody(body: unknown, options?: WithJsonOptions): ResponseStage {
if (body === undefined) {
throw new Error("Undefined is not valid JSON. Do not call jsonBody if you want an empty body.");
}
this.predicates.push((resolver) => withJson(body, resolver));
this.predicates.push((resolver) => withJson(body, resolver, options));
return this;
}

Expand Down
21 changes: 19 additions & 2 deletions generators/typescript/asIs/tests/mock-server/withJson.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,26 @@ import { type HttpResponseResolver, passthrough } from "msw";

import { fromJson, toJson } from "../../src/core/json";

export interface WithJsonOptions {
/**
* List of field names to ignore when comparing request bodies.
* This is useful for pagination cursor fields that change between requests.
*/
ignoredFields?: string[];
}

/**
* Creates a request matcher that validates if the request JSON body exactly matches the expected object
* @param expectedBody - The exact body object to match against
* @param resolver - Response resolver to execute if body matches
* @param options - Optional configuration including fields to ignore
*/
export function withJson(expectedBody: unknown, resolver: HttpResponseResolver): HttpResponseResolver {
export function withJson(
expectedBody: unknown,
resolver: HttpResponseResolver,
options?: WithJsonOptions,
): HttpResponseResolver {
const ignoredFields = options?.ignoredFields ?? [];
return async (args) => {
const { request } = args;

Expand All @@ -28,7 +42,10 @@ export function withJson(expectedBody: unknown, resolver: HttpResponseResolver):
}

const mismatches = findMismatches(actualBody, expectedBody);
if (Object.keys(mismatches).filter((key) => !key.startsWith("pagination.")).length > 0) {
const filteredMismatches = Object.keys(mismatches).filter(
(key) => !key.startsWith("pagination.") && !ignoredFields.includes(key),
);
if (filteredMismatches.length > 0) {
console.error("JSON body mismatch:", toJson(mismatches, undefined, 2));
return passthrough();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1299,6 +1299,18 @@ describe("${serviceName}", () => {
hasPagination = false;
}

// Extract pagination cursor field names to ignore in request body matching
// When getNextPage() is called, the SDK sends a different cursor value than the original request
const paginationIgnoredFields: string[] = [];
if (endpoint.pagination !== undefined && endpoint.pagination.type === "cursor") {
// For cursor pagination, the page property contains the cursor field in the request
const pageProperty = endpoint.pagination.page;
if (pageProperty.propertyPath == null || pageProperty.propertyPath.length === 0) {
// Top-level cursor field
paginationIgnoredFields.push(pageProperty.property.name.wireValue);
}
}

const expectedDeclaration = code`const expected = ${expected};`;

const paginationBlock =
Expand Down Expand Up @@ -1353,7 +1365,10 @@ describe("${serviceName}", () => {
`;
})}${
rawRequestBody
? code`.${mockBodyMethod}(rawRequestBody)
? paginationIgnoredFields.length > 0
? code`.${mockBodyMethod}(rawRequestBody, { ignoredFields: ${literalOf(paginationIgnoredFields)} })
`
: code`.${mockBodyMethod}(rawRequestBody)
`
: ""
}.respondWith()
Expand Down
12 changes: 12 additions & 0 deletions generators/typescript/sdk/versions.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,16 @@
# yaml-language-server: $schema=../../../fern-versions-yml.schema.json
- version: 3.43.7
changelogEntry:
- summary: |
Fix wire test mock server to ignore cursor mismatches in pagination tests.
When pagination tests call `getNextPage()`, the SDK correctly sends the cursor from the response,
but the mock server was rejecting the request because the cursor didn't match the original request.
The mock server now filters out top-level "cursor" mismatches, similar to how it already filters
"pagination." prefixed mismatches.
type: fix
createdAt: "2026-01-09"
irVersion: 63

- version: 3.43.6
changelogEntry:
- summary: |
Expand Down
Loading
Loading