Skip to content
Merged
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
70 changes: 70 additions & 0 deletions packages/mock/src/faker/getters/combine.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -436,6 +436,76 @@ describe('combineSchemasMock', () => {
expect(result.value).toBe('undefined');
});

// Regression test for #2155: when a `oneOf` schema declares a discriminator
// with a mapping AND the discriminator property is also listed in the
// parent's `properties`, the parent's free-choice enum must NOT be emitted
// alongside the picked variant. Each variant already encodes a constrained
// value for that property via `resolveDiscriminators`, so a trailing parent
// assignment would statically override it and guarantee a mismatch.
it('should not emit the discriminator property when oneOf has a discriminator mapping (#2155)', () => {
const item: MockSchemaObject = {
name: 'DiscriminatorTest',
type: 'object',
required: ['type'],
properties: {
type: { type: 'string', enum: ['item1', 'item2', 'item3'] },
},
discriminator: {
propertyName: 'type',
mapping: {
item1: '#/components/schemas/Item1',
item2: '#/components/schemas/Item2',
item3: '#/components/schemas/Item3',
},
},
oneOf: [
{
type: 'object',
properties: {
type: { type: 'string', enum: ['item1'] },
property1: { type: 'string' },
},
required: ['type'],
},
{
type: 'object',
properties: {
type: { type: 'string', enum: ['item2'] },
property2: { type: 'string' },
},
required: ['type'],
},
{
type: 'object',
properties: {
type: { type: 'string', enum: ['item3'] },
property3: { type: 'string' },
},
required: ['type'],
},
],
};

const result = combineSchemasMock({
item,
separator: 'oneOf',
operationId: 'testOp',
tags: ['test'],
context: createMockContext(),
imports: [],
existingReferencedProperties: [],
splitMockImplementations: [],
});

expect(result).toBeDefined();
// The picked variant carries the constrained discriminator value
// (e.g. `type: 'item1'`); appending the parent's full enum after the
// spread would overwrite it. Ensure the trailing override is gone.
expect(result.value).not.toMatch(
/faker\.helpers\.arrayElement\(\[\s*'item1'\s*,\s*'item2'\s*,\s*'item3'\s*\]/,
);
});

// Regression test for #2081: a oneOf variant that points back to an
// already-visited schema must be skipped, otherwise polymorphic recursion
// (Base → Parent.oneOf → Derived → allOf [Base]) overflows the stack.
Expand Down
69 changes: 65 additions & 4 deletions packages/mock/src/faker/getters/combine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,12 +54,73 @@ export function combineSchemasMock({
const isRefAndNotExisting =
isReference(item) && !existingReferencedProperties.includes(item.name);

// When a oneOf schema declares a discriminator with a mapping AND the
// discriminator property is also declared on the parent's `properties`,
// skip that property here. Each variant already encodes a constrained value
// for it via `resolveDiscriminators`; emitting the parent's free-choice enum
// alongside the picked variant would override the constrained value and
// guarantee a discriminator mismatch (#2155).
const discriminator = item.discriminator as
| { propertyName?: string; mapping?: Record<string, string> }
| undefined;
const itemProperties = item.properties as Record<string, unknown> | undefined;
const discriminatorPropertyName =
separator === 'oneOf' &&
discriminator?.mapping &&
discriminator.propertyName &&
itemProperties &&
discriminator.propertyName in itemProperties
? discriminator.propertyName
: undefined;
Comment on lines +63 to +74

const itemEntriesForResolve = Object.entries(item).filter(
([key]) => key !== separator,
);
if (discriminatorPropertyName && itemProperties) {
const propertiesIdx = itemEntriesForResolve.findIndex(
([key]) => key === 'properties',
);
if (propertiesIdx !== -1) {
const filteredProperties = Object.fromEntries(
Object.entries(itemProperties).filter(
([key]) => key !== discriminatorPropertyName,
),
);
if (Object.keys(filteredProperties).length === 0) {
itemEntriesForResolve.splice(propertiesIdx, 1);
} else {
itemEntriesForResolve[propertiesIdx] = [
'properties',
filteredProperties,
];
}
}
// Keep `required` in sync with the filtered properties — leaving the
// discriminator key in `required` would describe a schema whose required
// field is missing from `properties`.
const requiredIdx = itemEntriesForResolve.findIndex(
([key]) => key === 'required',
);
if (requiredIdx !== -1 && Array.isArray(itemRequired)) {
const filteredRequired = itemRequired.filter(
(key) => key !== discriminatorPropertyName,
);
if (filteredRequired.length === 0) {
itemEntriesForResolve.splice(requiredIdx, 1);
} else {
itemEntriesForResolve[requiredIdx] = ['required', filteredRequired];
}
}
}

const hasResolvableProperties = itemEntriesForResolve.some(
([key]) => key === 'properties',
);

const itemResolvedValue =
isRefAndNotExisting || item.properties
isRefAndNotExisting || hasResolvableProperties
? resolveMockValue({
schema: Object.fromEntries(
Object.entries(item).filter(([key]) => key !== separator),
) as MockSchemaObject,
schema: Object.fromEntries(itemEntriesForResolve) as MockSchemaObject,
combine: {
separator: 'allOf',
includedProperties: [],
Expand Down
98 changes: 98 additions & 0 deletions tests/__snapshots__/mock/discriminator-oneof-union/endpoints.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/**
* Generated by orval v8.12.3 🍺
* Do not edit manually.
* Discriminator with oneOf union
* OpenAPI spec version: 1.0
*/
import axios from 'axios';
import type { AxiosRequestConfig, AxiosResponse } from 'axios';

import type { DiscriminatorTest } from './model';

import { faker } from '@faker-js/faker';

import { HttpResponse, http } from 'msw';
import type { RequestHandlerOptions } from 'msw';

import type { Item1, Item2, Item3 } from './model';

export const getTest = (
options?: AxiosRequestConfig,
): Promise<AxiosResponse<DiscriminatorTest>> => {
return axios.get(`/test`, options);
};

export type GetTestResult = AxiosResponse<DiscriminatorTest>;

export const getGetTestResponseItem1Mock = (
overrideResponse: Partial<Item1> = {},
): Item1 => ({
...{
type: faker.helpers.arrayElement(['item1'] as const),
property1: faker.helpers.arrayElement([
faker.string.alpha({ length: { min: 10, max: 20 } }),
undefined,
]),
},
...overrideResponse,
});

export const getGetTestResponseItem2Mock = (
overrideResponse: Partial<Item2> = {},
): Item2 => ({
...{
type: faker.helpers.arrayElement(['item2'] as const),
property2: faker.helpers.arrayElement([
faker.string.alpha({ length: { min: 10, max: 20 } }),
undefined,
]),
},
...overrideResponse,
});

export const getGetTestResponseItem3Mock = (
overrideResponse: Partial<Item3> = {},
): Item3 => ({
...{
type: faker.helpers.arrayElement(['item3'] as const),
property3: faker.helpers.arrayElement([
faker.string.alpha({ length: { min: 10, max: 20 } }),
undefined,
]),
},
...overrideResponse,
});

export const getGetTestResponseMock = (): DiscriminatorTest =>
faker.helpers.arrayElement([
{ ...getGetTestResponseItem1Mock() },
{ ...getGetTestResponseItem2Mock() },
{ ...getGetTestResponseItem3Mock() },
]);

export const getGetTestMockHandler = (
overrideResponse?:
| DiscriminatorTest
| ((
info: Parameters<Parameters<typeof http.get>[1]>[0],
) => Promise<DiscriminatorTest> | DiscriminatorTest),
options?: RequestHandlerOptions,
) => {
return http.get(
'*/test',
async (info: Parameters<Parameters<typeof http.get>[1]>[0]) => {
return HttpResponse.json(
overrideResponse !== undefined
? typeof overrideResponse === 'function'
? await overrideResponse(info)
: overrideResponse
: getGetTestResponseMock(),
{ status: 200 },
);
},
options,
);
};
export const getDiscriminatorWithOneOfUnionMock = () => [
getGetTestMockHandler(),
];
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/**
* Generated by orval v8.12.3 🍺
* Do not edit manually.
* Discriminator with oneOf union
* OpenAPI spec version: 1.0
*/
import type { DiscriminatorTestType } from './discriminatorTestType';
import type { Item1 } from './item1';
import type { Item2 } from './item2';
import type { Item3 } from './item3';

export type DiscriminatorTest =
| (Item1 & {
type: DiscriminatorTestType;
})
| (Item2 & {
type: DiscriminatorTestType;
})
| (Item3 & {
type: DiscriminatorTestType;
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/**
* Generated by orval v8.12.3 🍺
* Do not edit manually.
* Discriminator with oneOf union
* OpenAPI spec version: 1.0
*/

export type DiscriminatorTestType =
(typeof DiscriminatorTestType)[keyof typeof DiscriminatorTestType];

export const DiscriminatorTestType = {
item1: 'item1',
item2: 'item2',
item3: 'item3',
} as const;
15 changes: 15 additions & 0 deletions tests/__snapshots__/mock/discriminator-oneof-union/model/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/**
* Generated by orval v8.12.3 🍺
* Do not edit manually.
* Discriminator with oneOf union
* OpenAPI spec version: 1.0
*/

export * from './discriminatorTest';
export * from './discriminatorTestType';
export * from './item1';
export * from './item1Type';
export * from './item2';
export * from './item2Type';
export * from './item3';
export * from './item3Type';
12 changes: 12 additions & 0 deletions tests/__snapshots__/mock/discriminator-oneof-union/model/item1.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/**
* Generated by orval v8.12.3 🍺
* Do not edit manually.
* Discriminator with oneOf union
* OpenAPI spec version: 1.0
*/
import type { Item1Type } from './item1Type';

export interface Item1 {
type: Item1Type;
property1?: string;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/**
* Generated by orval v8.12.3 🍺
* Do not edit manually.
* Discriminator with oneOf union
* OpenAPI spec version: 1.0
*/

export type Item1Type = (typeof Item1Type)[keyof typeof Item1Type];

export const Item1Type = {
item1: 'item1',
} as const;
12 changes: 12 additions & 0 deletions tests/__snapshots__/mock/discriminator-oneof-union/model/item2.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/**
* Generated by orval v8.12.3 🍺
* Do not edit manually.
* Discriminator with oneOf union
* OpenAPI spec version: 1.0
*/
import type { Item2Type } from './item2Type';

export interface Item2 {
type: Item2Type;
property2?: string;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/**
* Generated by orval v8.12.3 🍺
* Do not edit manually.
* Discriminator with oneOf union
* OpenAPI spec version: 1.0
*/

export type Item2Type = (typeof Item2Type)[keyof typeof Item2Type];

export const Item2Type = {
item2: 'item2',
} as const;
12 changes: 12 additions & 0 deletions tests/__snapshots__/mock/discriminator-oneof-union/model/item3.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/**
* Generated by orval v8.12.3 🍺
* Do not edit manually.
* Discriminator with oneOf union
* OpenAPI spec version: 1.0
*/
import type { Item3Type } from './item3Type';

export interface Item3 {
type: Item3Type;
property3?: string;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/**
* Generated by orval v8.12.3 🍺
* Do not edit manually.
* Discriminator with oneOf union
* OpenAPI spec version: 1.0
*/

export type Item3Type = (typeof Item3Type)[keyof typeof Item3Type];

export const Item3Type = {
item3: 'item3',
} as const;
Loading
Loading