Skip to content

Commit 6053015

Browse files
authored
test(orval): cover double-linked cross-file $ref chain (#1935) (#3409)
* test(orval): cover double-linked cross-file $ref chain (#1935) * test(orval): align path key in #1935 fixture with userId path param Make the standalone projects.yaml a valid OpenAPI document by changing its path key from `/projects` to `/users/{userId}/projects` so the declared `userId` path parameter matches the path template. Update the root $ref accordingly to use the JSON Pointer / percent-encoded escape, mirroring the existing issue-3380 fixture. No change to bundled output.
1 parent cc74503 commit 6053015

12 files changed

Lines changed: 252 additions & 0 deletions

File tree

packages/orval/src/import-specs.test.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -908,6 +908,78 @@ describe('dereferenceExternalRefs', () => {
908908
expect(result).not.toHaveProperty('x-ext');
909909
});
910910

911+
// Regression test for https://github.com/orval-labs/orval/issues/1935
912+
it('should resolve a barrel $ref that crosses into a second external doc', () => {
913+
// The middle external doc ("barrel") only re-exports a schema as a $ref
914+
// into a third external doc ("concrete"). The cross-doc rewrite in
915+
// `replaceXExtRefs` must collapse the chain so the merged barrel schema
916+
// ends up pointing at the resolved schema in the main spec, not at the
917+
// raw `#/x-ext/concrete/...` ref it was bundled with.
918+
const input = {
919+
openapi: '3.0.3',
920+
info: { title: 'Demo', version: '0.0.0' },
921+
paths: {},
922+
components: {
923+
schemas: {
924+
UserProjectDTO: {
925+
$ref: '#/x-ext/barrel/components/schemas/UserProjectDTO',
926+
},
927+
},
928+
},
929+
'x-ext': {
930+
barrel: {
931+
openapi: '3.0.3',
932+
info: { title: 'Barrel', version: '0.0.0' },
933+
components: {
934+
schemas: {
935+
UserProjectDTO: {
936+
$ref: '#/x-ext/concrete/components/schemas/UserProject',
937+
},
938+
},
939+
},
940+
},
941+
concrete: {
942+
openapi: '3.0.3',
943+
info: { title: 'Concrete', version: '0.0.0' },
944+
components: {
945+
schemas: {
946+
UserProject: {
947+
type: 'object',
948+
required: ['id', 'title'],
949+
properties: {
950+
id: { type: 'string' },
951+
title: { type: 'string' },
952+
},
953+
},
954+
},
955+
},
956+
},
957+
},
958+
};
959+
960+
const result = dereferenceExternalRef(input) as OpenApiDocument;
961+
const schemas = result.components?.schemas;
962+
963+
// Both ends of the chain are merged into the main spec.
964+
expect(schemas?.UserProject).toEqual({
965+
type: 'object',
966+
required: ['id', 'title'],
967+
properties: {
968+
id: { type: 'string' },
969+
title: { type: 'string' },
970+
},
971+
});
972+
973+
// The barrel entry collapses to an internal $ref pointing at the
974+
// resolved concrete schema — the x-ext hop must be rewritten, otherwise
975+
// downstream resolveRef() throws "Ref not found".
976+
expect(schemas?.UserProjectDTO).toEqual({
977+
$ref: '#/components/schemas/UserProject',
978+
});
979+
980+
expect(result).not.toHaveProperty('x-ext');
981+
});
982+
911983
// Regression test for https://github.com/orval-labs/orval/issues/394
912984
it('should resolve internal $ref inside an external parameter against the external doc', () => {
913985
const input = {
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/**
2+
* Generated by orval v8.11.0 🍺
3+
* Do not edit manually.
4+
* Issue 1935 - double-linked $ref
5+
* OpenAPI spec version: 1.0.0
6+
*/
7+
import axios from 'axios';
8+
import type { AxiosRequestConfig, AxiosResponse } from 'axios';
9+
10+
import type { UserProject } from './model';
11+
12+
/**
13+
* @summary Retrieve all related projects for the userId
14+
*/
15+
export const getUserProjects = (
16+
userId: string,
17+
options?: AxiosRequestConfig,
18+
): Promise<AxiosResponse<UserProject[]>> => {
19+
return axios.get(`/users/${userId}/projects`, options);
20+
};
21+
22+
export type GetUserProjectsResult = AxiosResponse<UserProject[]>;
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
/**
2+
* Generated by orval v8.11.0 🍺
3+
* Do not edit manually.
4+
* Issue 1935 - double-linked $ref
5+
* OpenAPI spec version: 1.0.0
6+
*/
7+
8+
export * from './userProject';
9+
export * from './userProjectDTO';
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
/**
2+
* Generated by orval v8.11.0 🍺
3+
* Do not edit manually.
4+
* Issue 1935 - double-linked $ref
5+
* OpenAPI spec version: 1.0.0
6+
*/
7+
8+
export interface UserProject {
9+
id: string;
10+
title: string;
11+
description?: string;
12+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
/**
2+
* Generated by orval v8.11.0 🍺
3+
* Do not edit manually.
4+
* Issue 1935 - double-linked $ref
5+
* OpenAPI spec version: 1.0.0
6+
*/
7+
import type { UserProject } from './userProject';
8+
9+
export type UserProjectDTO = UserProject;

tests/api-generation.spec.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,47 @@ test('default issue-3380 resolves external path-item $refs with escaped pointers
297297
expect(content).toContain('`/pets/${petId}`');
298298
});
299299

300+
test('default issue-1935 resolves a $ref chain across three external files', async () => {
301+
// Regression for #1935: an operation defined in one file (path-item $ref)
302+
// whose response schema $refs into a second file that itself only
303+
// re-exports the schema via a $ref to a third concrete-schema file used
304+
// to abort with `Ref not found: ../schemas/index.yaml#/components/schemas/
305+
// UserProjectDTO`. The bundler must follow the chain through both hops so
306+
// the operation resolves to the third file's concrete object schema. Keep
307+
// this focused assertion alongside the snapshot so #1935 fails with a
308+
// targeted message instead of a full-file snapshot diff.
309+
const root = (file: string) =>
310+
readFile(
311+
generated('default', 'issue-1935-double-linked-ref', file),
312+
'utf8',
313+
);
314+
const model = (file: string) =>
315+
readFile(
316+
generated('default', 'issue-1935-double-linked-ref', 'model', file),
317+
'utf8',
318+
);
319+
320+
// The operation imports the concrete third-file schema (not the barrel
321+
// alias) and uses it as the response item type.
322+
const endpoints = await root('endpoints.ts');
323+
expect(endpoints).toContain("import type { UserProject } from './model';");
324+
expect(endpoints).toContain('Promise<AxiosResponse<UserProject[]>>');
325+
326+
// The concrete schema from the third file is emitted as an interface.
327+
const userProject = await model('userProject.ts');
328+
expect(userProject).toContain('export interface UserProject {');
329+
expect(userProject).toContain('id: string;');
330+
expect(userProject).toContain('title: string;');
331+
332+
// The barrel re-export collapses to a type alias pointing at the resolved
333+
// schema — proving the second hop of the chain was followed.
334+
const userProjectDTO = await model('userProjectDTO.ts');
335+
expect(userProjectDTO).toContain(
336+
"import type { UserProject } from './userProject';",
337+
);
338+
expect(userProjectDTO).toContain('export type UserProjectDTO = UserProject;');
339+
});
340+
300341
test('react-query issue-1522 passes the enabled option into the queryOptions mutator', async () => {
301342
// Regression for #1522: when `allParamsOptional` and a custom `queryOptions`
302343
// mutator are combined, the auto-generated `enabled` guard (which disables

tests/configs/default.config.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -705,6 +705,17 @@ export default defineConfig({
705705
target: '../specifications/issue-3380/issue-3380.yaml',
706706
},
707707
},
708+
'issue-1935-double-linked-ref': {
709+
output: {
710+
target: '../generated/default/issue-1935-double-linked-ref/endpoints.ts',
711+
schemas: '../generated/default/issue-1935-double-linked-ref/model',
712+
clean: true,
713+
formatter: 'prettier',
714+
},
715+
input: {
716+
target: '../specifications/issue-1935/issue-1935.yaml',
717+
},
718+
},
708719
'boolean-discriminator': {
709720
output: {
710721
target: '../generated/default/boolean-discriminator/endpoints.ts',
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
openapi: 3.1.0
2+
info:
3+
title: Issue 1935 - double-linked $ref
4+
version: 1.0.0
5+
6+
# Regression for #1935: a $ref chain that crosses three external files.
7+
# - paths.<...> → external path-item file (path/projects.yaml)
8+
# - that file's response schema → external schemas barrel (schemas/index.yaml)
9+
# - the barrel re-exports → external concrete schema file (schemas/UserProjectDTO.yaml)
10+
paths:
11+
'/users/{userId}/projects':
12+
$ref: './path/projects.yaml#/paths/~1users~1%7BuserId%7D~1projects'
13+
components: {}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
openapi: 3.1.0
2+
info:
3+
title: userId path parameter
4+
version: 1.0.0
5+
paths: {}
6+
components:
7+
parameters:
8+
userId:
9+
name: userId
10+
in: path
11+
required: true
12+
schema:
13+
type: string
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
openapi: 3.1.0
2+
info:
3+
title: User Projects
4+
version: 1.0.0
5+
paths:
6+
/users/{userId}/projects:
7+
get:
8+
operationId: getUserProjects
9+
summary: Retrieve all related projects for the userId
10+
parameters:
11+
- $ref: '../parameters/userId.yaml#/components/parameters/userId'
12+
responses:
13+
'200':
14+
description: OK
15+
content:
16+
application/json:
17+
schema:
18+
type: array
19+
items:
20+
$ref: '../schemas/index.yaml#/components/schemas/UserProjectDTO'
21+
'500':
22+
description: Error

0 commit comments

Comments
 (0)