diff --git a/.changeset/loud-mails-hang.md b/.changeset/loud-mails-hang.md new file mode 100644 index 0000000000..7b71f89260 --- /dev/null +++ b/.changeset/loud-mails-hang.md @@ -0,0 +1,6 @@ +--- +"@redocly/openapi-core": patch +"@redocly/cli": patch +--- + +Fixed an issue where the content of `$ref`s inside example values was erroneously resolved during bundling and linting. diff --git a/docs/@v2/configuration/reference/resolve.md b/docs/@v2/configuration/reference/resolve.md index ca8971a057..619098e14f 100644 --- a/docs/@v2/configuration/reference/resolve.md +++ b/docs/@v2/configuration/reference/resolve.md @@ -21,7 +21,7 @@ One HTTP header is supported for each URL resolved. - doNotResolveExamples - boolean -- When running `lint`, set this option to `true` to avoid resolving `$ref` fields in examples. Resolving `$ref`s in other parts of the API is unaffected. +- Set this option to `true` to prevent resolving `$ref` fields in singular `example` properties. This affects both `lint` and `bundle` commands. Resolving `$ref`s in other parts of the API description is unaffected. --- diff --git a/docs/@v2/configuration/resolve.yaml b/docs/@v2/configuration/resolve.yaml index c0d41aeb57..af61c4f60b 100644 --- a/docs/@v2/configuration/resolve.yaml +++ b/docs/@v2/configuration/resolve.yaml @@ -12,7 +12,8 @@ properties: doNotResolveExamples: type: boolean description: >- - You can stop `lint` from resolving `$refs` in examples by setting this configuration option to `true`. + Set this option to `true` to prevent resolving `$ref` fields in singular `example` properties. + This affects both `lint` and `bundle` commands. This does not affect `$ref` resolution in other parts of the API description. default: false http: diff --git a/packages/core/src/__tests__/bundle-examples-ref-resolution.test.ts b/packages/core/src/__tests__/bundle-examples-ref-resolution.test.ts new file mode 100644 index 0000000000..841b222612 --- /dev/null +++ b/packages/core/src/__tests__/bundle-examples-ref-resolution.test.ts @@ -0,0 +1,229 @@ +import outdent from 'outdent'; +import * as path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { bundleDocument } from '../bundle/bundle-document.js'; +import { parseYamlToDocument, yamlSerializer } from '../../__tests__/utils.js'; +import { createConfig } from '../config/index.js'; +import { BaseResolver } from '../resolve.js'; +import { AsyncApi3Types, Oas3Types } from '../index.js'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +describe('Bundle Examples $ref Resolution', () => { + expect.addSnapshotSerializer(yamlSerializer); + + describe('OpenAPI 3.0/3.1 Examples', () => { + it('should resolve $ref in Example field', async () => { + const testDocument = parseYamlToDocument( + outdent` + openapi: 3.0.0 + info: + title: Test API + version: 1.0.0 + paths: + /test: + get: + responses: + '200': + content: + application/json: + schema: + type: object + properties: + id: + type: integer + name: + type: string + example: + $ref: ${path.join(__dirname, 'fixtures/example-data.json')} + `, + '' + ); + + const { bundle: result, problems } = await bundleDocument({ + document: testDocument, + externalRefResolver: new BaseResolver(), + config: await createConfig({}), + types: Oas3Types, + }); + + expect(problems).toHaveLength(0); + expect(result.parsed).toMatchInlineSnapshot(` + openapi: 3.0.0 + info: + title: Test API + version: 1.0.0 + paths: + /test: + get: + responses: + '200': + content: + application/json: + schema: + type: object + properties: + id: + type: integer + name: + type: string + example: + id: 123 + name: Test User + components: {} + `); + }); + + it('should NOT resolve $ref in Example.value field', async () => { + const testDocument = parseYamlToDocument( + outdent` + openapi: 3.1.0 + info: + title: Test API + version: 1.0.0 + paths: + /test: + get: + responses: + '200': + content: + application/json: + examples: + test-example: + value: + $ref: ./example-data.json + `, + '' + ); + + const { bundle: result, problems } = await bundleDocument({ + document: testDocument, + externalRefResolver: new BaseResolver(), + config: await createConfig({}), + types: Oas3Types, + }); + + expect(problems).toHaveLength(0); + expect(result.parsed).toMatchInlineSnapshot(` + openapi: 3.1.0 + info: + title: Test API + version: 1.0.0 + paths: + /test: + get: + responses: + '200': + content: + application/json: + examples: + test-example: + value: + $ref: ./example-data.json + components: {} + `); + }); + }); + + describe('OpenAPI 3.2 Examples', () => { + it('should NOT resolve $ref in Example.dataValue field', async () => { + const testDocument = parseYamlToDocument( + outdent` + openapi: 3.2.0 + info: + title: Test API + version: 1.0.0 + paths: + /test: + get: + responses: + '200': + content: + application/json: + examples: + test-example: + dataValue: + $ref: ./example-data.json + `, + '' + ); + + const { bundle: result, problems } = await bundleDocument({ + document: testDocument, + externalRefResolver: new BaseResolver(), + config: await createConfig({}), + types: Oas3Types, + }); + + expect(problems).toHaveLength(0); + expect(result.parsed).toMatchInlineSnapshot(` + openapi: 3.2.0 + info: + title: Test API + version: 1.0.0 + paths: + /test: + get: + responses: + '200': + content: + application/json: + examples: + test-example: + dataValue: + $ref: ./example-data.json + components: {} + `); + }); + }); + + describe('AsyncAPI 3.0 Examples', () => { + it('should NOT resolve $ref in Example.value field', async () => { + const testDocument = parseYamlToDocument( + outdent` + asyncapi: 3.0.0 + info: + title: Test AsyncAPI + version: 1.0.0 + operations: + sendUserSignedup: + action: send + messages: + - payload: + type: object + examples: + test-example: + value: + $ref: ./example-data.json + `, + '' + ); + + const { bundle: result, problems } = await bundleDocument({ + document: testDocument, + externalRefResolver: new BaseResolver(), + config: await createConfig({}), + types: AsyncApi3Types, + }); + + expect(problems).toHaveLength(0); + expect(result.parsed).toMatchInlineSnapshot(` + asyncapi: 3.0.0 + info: + title: Test AsyncAPI + version: 1.0.0 + operations: + sendUserSignedup: + action: send + messages: + - payload: + type: object + examples: + test-example: + value: + $ref: ./example-data.json + components: {} + `); + }); + }); +}); diff --git a/packages/core/src/__tests__/fixtures/example-data.json b/packages/core/src/__tests__/fixtures/example-data.json new file mode 100644 index 0000000000..a14f51e285 --- /dev/null +++ b/packages/core/src/__tests__/fixtures/example-data.json @@ -0,0 +1,4 @@ +{ + "id": 123, + "name": "Test User" +} diff --git a/packages/core/src/rules/oas3/__tests__/struct/referenceableScalars.test.ts b/packages/core/src/rules/oas3/__tests__/struct/referenceableScalars.test.ts index 8656d45d32..2f1cd82fff 100644 --- a/packages/core/src/rules/oas3/__tests__/struct/referenceableScalars.test.ts +++ b/packages/core/src/rules/oas3/__tests__/struct/referenceableScalars.test.ts @@ -69,4 +69,38 @@ describe('Referenceable scalars', () => { }); expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`[]`); }); + + it('should not report example value with $ref', async () => { + const document = parseYamlToDocument( + outdent` + openapi: 3.2.0 + info: + title: Test + version: '1.0' + paths: + '/test': + get: + parameters: + - name: test + schema: + type: object + examples: + test: + value: + $ref: not $ref, example + `, + __dirname + '/foobar.yaml' + ); + + const results = await lintDocument({ + externalRefResolver: new BaseResolver(), + document, + config: await createConfig({ + rules: { + 'no-unresolved-refs': 'error', + }, + }), + }); + expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`[]`); + }); }); diff --git a/packages/core/src/types/index.ts b/packages/core/src/types/index.ts index b80dd12dc1..3b9d025baa 100755 --- a/packages/core/src/types/index.ts +++ b/packages/core/src/types/index.ts @@ -4,6 +4,7 @@ export type ScalarSchema = { items?: ScalarSchema; enum?: string[]; isExample?: boolean; + resolvable?: boolean; directResolveAs?: string; minimum?: number; }; diff --git a/packages/core/src/types/oas3.ts b/packages/core/src/types/oas3.ts index 5d9cb8b702..38eb7cca73 100755 --- a/packages/core/src/types/oas3.ts +++ b/packages/core/src/types/oas3.ts @@ -233,7 +233,7 @@ const MediaType: NodeType = { const Example: NodeType = { properties: { - value: { isExample: true }, + value: { resolvable: false }, summary: { type: 'string' }, description: { type: 'string' }, externalValue: { type: 'string' }, diff --git a/packages/core/src/types/oas3_2.ts b/packages/core/src/types/oas3_2.ts index 9470b06533..1cca1eb584 100755 --- a/packages/core/src/types/oas3_2.ts +++ b/packages/core/src/types/oas3_2.ts @@ -194,7 +194,7 @@ const Example: NodeType = { ...Oas3_1Types.Example, properties: { ...Oas3_1Types.Example.properties, - dataValue: { isExample: true }, + dataValue: { resolvable: false }, serializedValue: { type: 'string' }, }, }; diff --git a/tests/e2e/bundle/bundle-example-value-with-ref/openapi.yaml b/tests/e2e/bundle/bundle-example-value-with-ref/openapi.yaml new file mode 100644 index 0000000000..348649a741 --- /dev/null +++ b/tests/e2e/bundle/bundle-example-value-with-ref/openapi.yaml @@ -0,0 +1,28 @@ +openapi: 3.2.0 +info: + title: Test API + version: 1.0.0 +paths: + /test: + get: + responses: + '200': + content: + application/json: + schema: + type: object + properties: + $ref: + type: string + example: + $ref: example.json # should NOT be resolved + examples: + Test: + summary: Example with $ref in value (should NOT resolve) + value: + $ref: example.json # should NOT be resolved + + TestDataValue: + summary: Example with $ref in dataValue (should NOT resolve) + dataValue: + $ref: example.json # should NOT be resolved diff --git a/tests/e2e/bundle/bundle-example-value-with-ref/redocly.yaml b/tests/e2e/bundle/bundle-example-value-with-ref/redocly.yaml new file mode 100644 index 0000000000..9e587a9f8e --- /dev/null +++ b/tests/e2e/bundle/bundle-example-value-with-ref/redocly.yaml @@ -0,0 +1,6 @@ +apis: + main: + root: openapi.yaml + +resolve: + doNotResolveExamples: true diff --git a/tests/e2e/bundle/bundle-example-value-with-ref/snapshot.txt b/tests/e2e/bundle/bundle-example-value-with-ref/snapshot.txt new file mode 100644 index 0000000000..205cd72132 --- /dev/null +++ b/tests/e2e/bundle/bundle-example-value-with-ref/snapshot.txt @@ -0,0 +1,31 @@ +openapi: 3.2.0 +info: + title: Test API + version: 1.0.0 +paths: + /test: + get: + responses: + '200': + content: + application/json: + schema: + type: object + properties: + $ref: + type: string + example: + $ref: example.json + examples: + Test: + summary: Example with $ref in value (should NOT resolve) + value: + $ref: example.json + TestDataValue: + summary: Example with $ref in dataValue (should NOT resolve) + dataValue: + $ref: example.json +components: {} + +bundling openapi.yaml using configuration for api 'main'... +📦 Created a bundle for openapi.yaml at stdout ms.