Skip to content

fix(mock): use operationName for MSW handler names under splitByContentType - #3566

Merged
melloware merged 1 commit into
orval-labs:masterfrom
wadakatu:fix/msw-split-by-content-type
Jun 8, 2026
Merged

fix(mock): use operationName for MSW handler names under splitByContentType#3566
melloware merged 1 commit into
orval-labs:masterfrom
wadakatu:fix/msw-split-by-content-type

Conversation

@wadakatu

@wadakatu wadakatu commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

Summary

When an endpoint's requestBody declares multiple content types (e.g. application/json + multipart/form-data) and splitByContentType: true is set, the client is correctly split into *WithJson / *WithFormData functions (#3201), but the generated MSW mocks emit two handlers with identical names, so the file fails to compile:

error TS2451: Cannot redeclare block-scoped variable 'getCreateEventMockHandler'.
error TS2451: Cannot redeclare block-scoped variable 'getCreateEventResponseMock'.

Closes #3342.

Root cause

splitByContentType keeps a single operationId across the sibling verb options but suffixes each one's operationName (*WithJson / *WithFormData). The client side already names functions from operationName, but generateMSW derived the handler and responseMock name bases from operationId:

const handlerName = `get${pascal(operationId)}MockHandler`;
const getResponseMockFunctionName = `get${pascal(operationId)}ResponseMock`;

So both variants collapsed to the same name. #3201 fixed the client side and missed the MSW side.

Fix

Derive the MSW handler/responseMock name bases from operationName so they match the client split. In the non-split case operationName normalizes to the same pascal-cased string as operationId, so existing output is unchanged.

-const handlerName = `get${pascal(operationId)}MockHandler`;
-const getResponseMockFunctionName = `get${pascal(operationId)}ResponseMock`;
+const handlerName = `get${pascal(operationName)}MockHandler`;
+const getResponseMockFunctionName = `get${pascal(operationName)}ResponseMock`;

generateDefinition's use of operationId (override-mock lookup key) is intentionally left untouched.

Tests

  • Unit (packages/mock/src/msw/index.test.ts): a new case asserts that a suffixed operationName produces get<Name>WithFormDataMockHandler / ...ResponseMock. Existing fixtures gained the (previously omitted) operationName field, which equals their operationId, so their assertions are unchanged.
  • e2e / snapshot (tests/configs/mock.config.tsissue3342): reuses the existing split-by-content-type.yaml spec with react-query + MSW mock + splitByContentType. The generated output is committed and goes through typecheck-generated, which compiles every generated client — so a regression of this collision would fail CI with TS2451 directly.

Verification

  • @orval/mock and @orval/core unit tests pass.
  • typecheck-generated (all clients) and the full snapshot suite (samples + tests) pass.
  • No churn in existing snapshots (only the new issue-3342 fixtures are added).

Note

Two handlers are now registered for the same route (one per content type). Their mocked responses are identical, so MSW uses the first match and the second is effectively redundant but harmless — this matches the reporter's expectation of distinct, split handler names. De-duplicating to a single shared handler is out of scope and could be a follow-up.

Summary by CodeRabbit

  • Bug Fixes

    • Fixed MSW handler naming to prevent duplicate names across content-type variants.
  • Tests

    • Added regression test for MSW split-by-content-type handling.
    • Added test fixtures and configuration for content-type variant scenarios.

…ntType

When splitByContentType splits a multi content-type requestBody into sibling
verb options, those siblings share one operationId but carry distinct suffixed
operationNames (e.g. *WithJson / *WithFormData). The client side already names
functions from operationName, but generateMSW derived the handler and
responseMock names from operationId, so both variants emitted identical names
and the generated mock file failed to compile with TS2451 (redeclared
block-scoped variable).

Derive the MSW handler/responseMock name bases from operationName so they match
the client split. In the non-split case operationName normalizes to the same
pascal-cased string as operationId, so existing output is unchanged.

Closes orval-labs#3342
Copilot AI review requested due to automatic review settings June 8, 2026 01:46
@coderabbitai

coderabbitai Bot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

This PR fixes duplicate MSW handler exports when using splitByContentType: true by changing generateMSW to derive handler names from operationName instead of operationId. The core change enables correct naming for content-type variants (e.g., getPetWithFormData). Tests are added to validate the behavior, and comprehensive generated fixtures demonstrate the fix with a split-by-content-type test case.

Changes

MSW Handler Naming Fix for splitByContentType

Layer / File(s) Summary
Core MSW naming fix
packages/mock/src/msw/index.ts
generateMSW derives handler and response mock function names from operationName instead of operationId, enabling correct naming for splitByContentType variants. A comment documents the reason for avoiding duplicate handler names across content-type variants.
Test assertions for operationName naming
packages/mock/src/msw/index.test.ts
Tests are updated to provide operationName (getUser, getPet, getRoot), and a new regression test verifies that splitByContentType variants generate suffixed mock/handler names (e.g., getPetWithFormData) without reusing base operation names.
Test configuration for issue-3342
tests/configs/mock.config.ts
New Orval configuration entry issue3342 enables splitByContentType: true and targets generated endpoint/model files in the issue-3342 snapshot directory.
Generated model type definitions
tests/__snapshots__/mock/issue-3342/model/*
TypeScript interfaces for the test case: Avatar (id, url), AvatarUpload (file, description), Error (code, message), Profile (id, name, email, avatarUrl), UpdateProfileBody (name, email), exported through barrel module.
Generated endpoint implementations and MSW handlers
tests/__snapshots__/mock/issue-3342/endpoints.ts
React Query endpoints and MSW handlers for profile/avatar operations with multiple content-type variants: JSON update, FormData update, FormData avatar upload, Blob avatar upload, and profile list. Includes fetch implementations, mutation/query options, React Query hooks, faker-based mock generators, and MSW handler factories supporting overrideResponse values.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • orval-labs/orval#3398: Modifies generateMSW output naming and handler generation logic in tandem with this PR's changes.
  • orval-labs/orval#3529: Adds strict mock type logic for the "strict mock types (#3525)" feature alongside this PR's operationName-based naming changes.

Suggested labels

msw, mock

Suggested reviewers

  • melloware

Poem

🐰 A rabbit's ode to clarity

When handlers split by content's way,
And names must differ, day by day,
OperationName leads the fight—
No duplicates, just names done right! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: using operationName instead of operationId for MSW handler names when splitByContentType is enabled, which is the core fix documented in the PR objectives.
Linked Issues check ✅ Passed The PR directly addresses #3342 by implementing operationName-based naming in MSW generation to eliminate duplicate handler exports, includes a regression test and snapshot test to verify the fix compiles correctly.
Out of Scope Changes check ✅ Passed All changes align with the stated objective: MSW code uses operationName, generateDefinition unchanged, config and model files support the test scenario, and no unrelated modifications are present.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Adds a regression test fixture to ensure MSW mock generation produces distinct handler/response-mock names when splitByContentType is enabled, preventing duplicate declarations during TypeScript compilation.

Changes:

  • Update MSW name derivation to use operationName (content-type-suffixed) instead of operationId.
  • Add a new test config entry and snapshot outputs for issue #3342.
  • Extend MSW unit tests to assert naming behavior for splitByContentType variants.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
tests/configs/mock.config.ts Adds a dedicated config case to generate mocks for the split-by-content-type regression.
tests/snapshots/mock/issue-3342/endpoints.ts New snapshot proving generated handlers/mocks are uniquely named per content type variant.
tests/snapshots/mock/issue-3342/model/*.ts New snapshot models for the issue #3342 generated output.
packages/mock/src/msw/index.ts Switches MSW handler/response-mock naming from operationId to operationName.
packages/mock/src/msw/index.test.ts Adds coverage to ensure naming derives from operationName for split-by-content-type.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

): ClientMockGeneratorBuilder {
const { pathRoute, override, mock } = generatorOptions;
const { operationId, response } = generatorVerbOptions;
const { operationName, response } = generatorVerbOptions;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

operationName is a required field on GeneratorVerbOptions (packages/core/src/types.ts), not optional, and buildVerbOption always populates it in the generation pipeline. Both production call sites — packages/mock/src/index.ts and packages/mock/src/faker/index.ts — pass pipeline-built verb options, so it's never undefined here. The client generators already derive their function names from operationName; this change only aligns the MSW side with them. A ?? operationId fallback would guard a state the type system already prevents.

Minor: pascal(undefined) returns '' via its default parameter, so a missing name would yield getMockHandler, not getUndefinedMockHandler.

Comment on lines +447 to +452
// Derive names from operationName (not operationId): splitByContentType keeps
// one operationId across variants but suffixes operationName (e.g. *WithJson /
// *WithFormData), and the client side already names functions from it. Using
// operationId here would emit duplicate handler names and break tsc. See #3342.
const handlerName = `get${pascal(operationName)}MockHandler`;
const getResponseMockFunctionName = `get${pascal(operationName)}ResponseMock`;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as the line 438 thread: operationName is a required field on GeneratorVerbOptions and is always populated by buildVerbOption in the pipeline, so no fallback is needed here. This only mirrors how the client generators already name their functions.

Comment on lines +8 to +11
export interface Error {
code: number;
message: string;
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Error schema comes from the existing shared test spec tests/specifications/split-by-content-type.yaml, which is also consumed by the split-by-content-type config in tests/configs/default.config.ts (the original #3201 test) and already emits this Error type there. Renaming the schema would churn an unrelated committed snapshot and is out of scope for this fix — it's a generated test fixture, not shipped code.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
packages/mock/src/msw/index.test.ts (1)

1500-1526: 💤 Low value

Consider moving this test to its own describe block.

This test verifies the fix for issue #3342 (splitByContentType naming), but it's nested inside describe('strict mock types (#3525)'). For better discoverability, consider moving it to a separate describe('splitByContentType handler naming (#3342)') block or into the main describe('generateMSW') suite.

The test implementation itself is well-structured and correctly verifies that handler and response mock names are derived from operationName rather than operationId.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/mock/src/msw/index.test.ts` around lines 1500 - 1526, Move the
specific it('derives responseMock and handler names from operationName
(splitByContentType)') test out of the nested describe('strict mock types
(`#3525`)') block into its own describe block (e.g., describe('splitByContentType
handler naming (`#3342`)')) or into the top-level describe('generateMSW') suite so
it’s discoverable; locate the test that calls generateMSW with operationId
'getPet' and operationName 'getPetWithFormData' and wrap that it(...) in the new
describe, preserving the assertions that reference
result.implementation.handlerName and result.implementation.function/handler to
ensure the same checks remain intact.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@packages/mock/src/msw/index.test.ts`:
- Around line 1500-1526: Move the specific it('derives responseMock and handler
names from operationName (splitByContentType)') test out of the nested
describe('strict mock types (`#3525`)') block into its own describe block (e.g.,
describe('splitByContentType handler naming (`#3342`)')) or into the top-level
describe('generateMSW') suite so it’s discoverable; locate the test that calls
generateMSW with operationId 'getPet' and operationName 'getPetWithFormData' and
wrap that it(...) in the new describe, preserving the assertions that reference
result.implementation.handlerName and result.implementation.function/handler to
ensure the same checks remain intact.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: bce3002b-ae1a-4559-b57a-e45b39f84080

📥 Commits

Reviewing files that changed from the base of the PR and between 77000de and dfd3678.

📒 Files selected for processing (10)
  • packages/mock/src/msw/index.test.ts
  • packages/mock/src/msw/index.ts
  • tests/__snapshots__/mock/issue-3342/endpoints.ts
  • tests/__snapshots__/mock/issue-3342/model/avatar.ts
  • tests/__snapshots__/mock/issue-3342/model/avatarUpload.ts
  • tests/__snapshots__/mock/issue-3342/model/error.ts
  • tests/__snapshots__/mock/issue-3342/model/index.ts
  • tests/__snapshots__/mock/issue-3342/model/profile.ts
  • tests/__snapshots__/mock/issue-3342/model/updateProfileBody.ts
  • tests/configs/mock.config.ts

@melloware
melloware merged commit 62af7f0 into orval-labs:master Jun 8, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

splitByContentType: true generates duplicate MSW handler exports

3 participants