Skip to content

Commit f95377d

Browse files
authored
fix(mock): emit boolean enum literals instead of random datatype.boolean() (#3428)
The MSW mock generator's boolean branch in scalar.ts ignored `item.enum` and unconditionally emitted `faker.datatype.boolean()`. For schemas like `oneOf [{ success: enum [true] }, { success: enum [false], failReason }]` this broke the discriminator: the randomly-picked union variant set `success` to a random boolean, so the mock no longer matched the TypeScript literal type its branch enforced (`success: true` / `success: false`). Route boolean through the same `getEnum` helper as number/string so `enum: [true]` emits `faker.helpers.arrayElement([true] as const)` and `enum: [false]` likewise, keeping the mock value pinned to the chosen branch's literal. The type-generation half of #1775 was already addressed by #3159's boolean-enum branch in `packages/core/src/getters/scalar.ts`; this change closes the mock-side gap and adds a focused regression covering the exact #1775 shape. Closes #1775
1 parent 8f4ae4a commit f95377d

8 files changed

Lines changed: 198 additions & 7 deletions

File tree

packages/mock/src/faker/getters/scalar.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -240,12 +240,22 @@ export function getMockScalar({
240240

241241
case 'boolean': {
242242
let value = 'faker.datatype.boolean()';
243-
if ('const' in item) {
243+
const booleanImports: GeneratorImport[] = [];
244+
if (item.enum) {
245+
value = getEnum(
246+
item,
247+
booleanImports,
248+
context,
249+
existingReferencedProperties,
250+
'boolean',
251+
);
252+
} else if ('const' in item) {
244253
value = JSON.stringify(item.const);
245254
}
246255
return {
247256
value,
248-
imports: [],
257+
enums: item.enum,
258+
imports: booleanImports,
249259
name: item.name,
250260
};
251261
}
@@ -463,7 +473,7 @@ function getEnum(
463473
imports: GeneratorImport[],
464474
context: ContextSpec,
465475
existingReferencedProperties: string[],
466-
type?: 'string' | 'number',
476+
type?: 'string' | 'number' | 'boolean',
467477
) {
468478
if (!item.enum) return '';
469479
const joinedEnumValues = item.enum
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/**
2+
* Generated by orval v8.12.3 🍺
3+
* Do not edit manually.
4+
* issue-1775
5+
* OpenAPI spec version: 1.0.0
6+
*/
7+
import axios from 'axios';
8+
import type { AxiosRequestConfig, AxiosResponse } from 'axios';
9+
10+
import type { PutApiOrderLimit200Item } from './model';
11+
12+
import { faker } from '@faker-js/faker';
13+
14+
import { HttpResponse, http } from 'msw';
15+
import type { RequestHandlerOptions } from 'msw';
16+
17+
export const putApiOrderLimit = (
18+
options?: AxiosRequestConfig,
19+
): Promise<AxiosResponse<PutApiOrderLimit200Item[]>> => {
20+
return axios.put(`/api/order/limit`, undefined, options);
21+
};
22+
23+
export type PutApiOrderLimitResult = AxiosResponse<PutApiOrderLimit200Item[]>;
24+
25+
export const getPutApiOrderLimitResponseMock = (): PutApiOrderLimit200Item[] =>
26+
Array.from(
27+
{ length: faker.number.int({ min: 1, max: 10 }) },
28+
(_, i) => i + 1,
29+
).map(() => ({
30+
...{ orderId: faker.string.alpha({ length: { min: 10, max: 20 } }) },
31+
...faker.helpers.arrayElement([
32+
{ success: faker.helpers.arrayElement([true] as const) },
33+
{
34+
success: faker.helpers.arrayElement([false] as const),
35+
failReason: faker.string.alpha({ length: { min: 10, max: 20 } }),
36+
},
37+
]),
38+
}));
39+
40+
export const getPutApiOrderLimitMockHandler = (
41+
overrideResponse?:
42+
| PutApiOrderLimit200Item[]
43+
| ((
44+
info: Parameters<Parameters<typeof http.put>[1]>[0],
45+
) => Promise<PutApiOrderLimit200Item[]> | PutApiOrderLimit200Item[]),
46+
options?: RequestHandlerOptions,
47+
) => {
48+
return http.put(
49+
'*/api/order/limit',
50+
async (info: Parameters<Parameters<typeof http.put>[1]>[0]) => {
51+
return HttpResponse.json(
52+
overrideResponse !== undefined
53+
? typeof overrideResponse === 'function'
54+
? await overrideResponse(info)
55+
: overrideResponse
56+
: getPutApiOrderLimitResponseMock(),
57+
{ status: 200 },
58+
);
59+
},
60+
options,
61+
);
62+
};
63+
export const getIssue1775Mock = () => [getPutApiOrderLimitMockHandler()];
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
/**
2+
* Generated by orval v8.12.3 🍺
3+
* Do not edit manually.
4+
* issue-1775
5+
* OpenAPI spec version: 1.0.0
6+
*/
7+
8+
export * from './putApiOrderLimit200Item';
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/**
2+
* Generated by orval v8.12.3 🍺
3+
* Do not edit manually.
4+
* issue-1775
5+
* OpenAPI spec version: 1.0.0
6+
*/
7+
8+
export type PutApiOrderLimit200Item = (
9+
| {
10+
success: true;
11+
}
12+
| {
13+
success: false;
14+
failReason: string;
15+
}
16+
) & {
17+
orderId: string;
18+
};

tests/__snapshots__/default/nullable-oneof-enums/endpoints.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ export const getGetItemsWithMultiplePropsResponseMock = (): Item3[] =>
9090
world: faker.helpers.arrayElement([
9191
faker.helpers.arrayElement([
9292
faker.helpers.arrayElement([1, 2, 3] as const),
93-
faker.datatype.boolean(),
93+
faker.helpers.arrayElement([true, false] as const),
9494
]),
9595
null,
9696
]),
@@ -121,7 +121,7 @@ export const getGetNestedItemsResponseMock = (): NestedItem[] =>
121121
world: faker.helpers.arrayElement([
122122
faker.helpers.arrayElement([
123123
faker.helpers.arrayElement([1, 2, 3] as const),
124-
faker.datatype.boolean(),
124+
faker.helpers.arrayElement([true, false] as const),
125125
]),
126126
null,
127127
]),
@@ -173,8 +173,8 @@ export const getGetMixedTypeEnumsResponseMock = (): MixedTypeEnums[] =>
173173
]),
174174
booleanEnum: faker.helpers.arrayElement([
175175
faker.helpers.arrayElement([
176-
faker.datatype.boolean(),
177-
faker.datatype.boolean(),
176+
faker.helpers.arrayElement([true, false] as const),
177+
faker.helpers.arrayElement([true] as const),
178178
]),
179179
null,
180180
]),

tests/api-generation.spec.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -476,3 +476,42 @@ test('fetch issue-1879 inlines header schema when $ref targets another path para
476476
);
477477
expect(indexContent).not.toMatch(/\bn0\b/);
478478
});
479+
480+
test('default issue-1775 preserves boolean enum literals across allOf+oneOf', async () => {
481+
// Regression for #1775: an `allOf: [{orderId}, oneOf: [{success: enum [true]},
482+
// {success: enum [false], failReason}]]` schema returned as an array.
483+
//
484+
// The type-generation half (boolean-literal preservation) was already fixed
485+
// by #3159's enum branch in `packages/core/src/getters/scalar.ts`. The mock
486+
// half is what this regression locks down: the boolean branch in
487+
// `packages/mock/src/faker/getters/scalar.ts` previously ignored `item.enum`
488+
// and unconditionally emitted `faker.datatype.boolean()`, so each `oneOf`
489+
// variant's `success` randomly flipped instead of matching its literal type.
490+
// The fix routes boolean through the same `getEnum` helper as number/string,
491+
// emitting `faker.helpers.arrayElement([true] as const)` /
492+
// `arrayElement([false] as const)` so the mock's discriminator stays in sync
493+
// with the union branch it picked.
494+
const model = await readFile(
495+
generated('default', 'issue-1775', 'model', 'putApiOrderLimit200Item.ts'),
496+
'utf8',
497+
);
498+
499+
// The two oneOf branches keep their literal types, and `orderId` is shared
500+
// through the `& { orderId: string }` half of the allOf intersection.
501+
expect(model).toContain('success: true;');
502+
expect(model).toContain('success: false;');
503+
expect(model).toContain('failReason: string;');
504+
expect(model).toContain('orderId: string;');
505+
506+
const endpoints = await readFile(
507+
generated('default', 'issue-1775', 'endpoints.ts'),
508+
'utf8',
509+
);
510+
511+
// Each oneOf branch's mock must pin `success` to the branch's literal so a
512+
// random selection still produces a valid `PutApiOrderLimit200Item`. The
513+
// pre-fix output emitted `faker.datatype.boolean()` for both branches.
514+
expect(endpoints).toContain('arrayElement([true] as const)');
515+
expect(endpoints).toContain('arrayElement([false] as const)');
516+
expect(endpoints).not.toMatch(/success: faker\.datatype\.boolean\(\)/);
517+
});

tests/configs/default.config.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,16 @@ export default defineConfig({
129129
formatter: 'prettier',
130130
},
131131
},
132+
'issue-1775': {
133+
input: '../specifications/issue-1775.yaml',
134+
output: {
135+
schemas: '../generated/default/issue-1775/model',
136+
target: '../generated/default/issue-1775/endpoints.ts',
137+
mock: true,
138+
clean: true,
139+
formatter: 'prettier',
140+
},
141+
},
132142
'all-of-without-type': {
133143
input: '../specifications/all-of-without-type.yaml',
134144
output: {
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
openapi: 3.0.1
2+
info:
3+
version: 1.0.0
4+
title: issue-1775
5+
paths:
6+
/api/order/limit:
7+
put:
8+
operationId: putApiOrderLimit
9+
responses:
10+
'200':
11+
description: Batch result
12+
content:
13+
application/json:
14+
schema:
15+
type: array
16+
items:
17+
allOf:
18+
- type: object
19+
required:
20+
- orderId
21+
properties:
22+
orderId:
23+
type: string
24+
- oneOf:
25+
- type: object
26+
required:
27+
- success
28+
properties:
29+
success:
30+
type: boolean
31+
enum:
32+
- true
33+
- type: object
34+
required:
35+
- success
36+
- failReason
37+
properties:
38+
success:
39+
type: boolean
40+
enum:
41+
- false
42+
failReason:
43+
type: string

0 commit comments

Comments
 (0)