Skip to content

Commit 61f09f7

Browse files
fix: path-parameters-defined rule false positive (#2442)
1 parent 0483640 commit 61f09f7

3 files changed

Lines changed: 383 additions & 46 deletions

File tree

.changeset/rotten-news-battle.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@redocly/openapi-core": patch
3+
---
4+
5+
Fixed false positive reports for path parameters in callback operations reported by the `path-parameters-defined` rule.

packages/core/src/rules/common/__tests__/path-params-defined.test.ts

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,4 +200,206 @@ describe('Oas3 path-params-defined', () => {
200200

201201
expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`[]`);
202202
});
203+
204+
it('should not report on undefined params in callback for next operation in same path item', async () => {
205+
const document = parseYamlToDocument(
206+
outdent`
207+
openapi: 3.1.1
208+
paths:
209+
/projects/{projectId}:
210+
post:
211+
operationId: createProject
212+
parameters:
213+
- name: projectId
214+
in: path
215+
required: true
216+
schema:
217+
type: string
218+
callbacks:
219+
onEvent:
220+
'{$request.body#/callbackUrl}':
221+
post:
222+
summary: Callback endpoint
223+
responses:
224+
'200':
225+
description: OK
226+
requestBody:
227+
content:
228+
application/json:
229+
schema:
230+
type: object
231+
properties:
232+
callbackUrl:
233+
type: string
234+
responses:
235+
'201':
236+
description: Created
237+
238+
patch:
239+
operationId: updateProject
240+
parameters:
241+
- name: projectId
242+
in: path
243+
required: true
244+
schema:
245+
type: string
246+
responses:
247+
'200':
248+
description: OK
249+
`,
250+
'foobar.yaml'
251+
);
252+
253+
const results = await lintDocument({
254+
externalRefResolver: new BaseResolver(),
255+
document,
256+
config: await createConfig({ rules: { 'path-params-defined': 'error' } }),
257+
});
258+
259+
expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`[]`);
260+
});
261+
262+
it('should fail on undefined or missing params in callback', async () => {
263+
const document = parseYamlToDocument(
264+
outdent`
265+
openapi: 3.1.1
266+
paths:
267+
/projects/{projectId}:
268+
post:
269+
operationId: createProject
270+
parameters:
271+
- name: projectId
272+
in: path
273+
required: true
274+
schema:
275+
type: string
276+
callbacks:
277+
onEvent:
278+
'{$request.body#/callbackUrl/{missingId}}':
279+
post:
280+
parameters:
281+
- name: notDefinedId
282+
in: path
283+
required: true
284+
schema:
285+
type: string
286+
responses:
287+
'200':
288+
description: OK
289+
requestBody:
290+
content:
291+
application/json:
292+
schema:
293+
type: object
294+
properties:
295+
callbackUrl:
296+
type: string
297+
responses:
298+
'201':
299+
description: Created
300+
301+
patch:
302+
operationId: updateProject
303+
parameters:
304+
- name: projectId
305+
in: path
306+
required: true
307+
schema:
308+
type: string
309+
responses:
310+
'200':
311+
description: OK
312+
`,
313+
'foobar.yaml'
314+
);
315+
316+
const results = await lintDocument({
317+
externalRefResolver: new BaseResolver(),
318+
document,
319+
config: await createConfig({ rules: { 'path-params-defined': 'error' } }),
320+
});
321+
322+
expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`
323+
[
324+
{
325+
"location": [
326+
{
327+
"pointer": "#/paths/~1projects~1{projectId}/post/callbacks/onEvent/{$request.body#~1callbackUrl~1{missingId}}/post/parameters/0/name",
328+
"reportOnKey": false,
329+
"source": "foobar.yaml",
330+
},
331+
],
332+
"message": "Path parameter \`notDefinedId\` is not used in the path \`{$request.body#/callbackUrl/{missingId}}\`.",
333+
"ruleId": "path-params-defined",
334+
"severity": "error",
335+
"suggest": [],
336+
},
337+
{
338+
"location": [
339+
{
340+
"pointer": "#/paths/~1projects~1{projectId}/post/callbacks/onEvent/{$request.body#~1callbackUrl~1{missingId}}/post/parameters",
341+
"reportOnKey": true,
342+
"source": "foobar.yaml",
343+
},
344+
],
345+
"message": "The operation does not define the path parameter \`{missingId}\` expected by path \`{$request.body#/callbackUrl/{missingId}}\`.",
346+
"ruleId": "path-params-defined",
347+
"severity": "error",
348+
"suggest": [],
349+
},
350+
]
351+
`);
352+
});
353+
354+
it('should fail on too deep callback nesting with 2 levels', async () => {
355+
const document = parseYamlToDocument(
356+
outdent`
357+
openapi: 3.1.1
358+
paths:
359+
/projects/{projectId}:
360+
post:
361+
operationId: createProject
362+
parameters:
363+
- name: projectId
364+
in: path
365+
required: true
366+
schema:
367+
type: string
368+
callbacks:
369+
onEvent:
370+
'{$request.body#/callbackUrl}':
371+
post:
372+
summary: Callback endpoint
373+
callbacks:
374+
onEvent:
375+
'{$request.body#/callbackUrl}':
376+
post:
377+
summary: Callback endpoint
378+
`,
379+
'foobar.yaml'
380+
);
381+
382+
const results = await lintDocument({
383+
externalRefResolver: new BaseResolver(),
384+
document,
385+
config: await createConfig({ rules: { 'path-params-defined': 'error' } }),
386+
});
387+
expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`
388+
[
389+
{
390+
"location": [
391+
{
392+
"pointer": "#/paths/~1projects~1{projectId}/post/callbacks/onEvent/{$request.body#~1callbackUrl}/post/callbacks/onEvent/{$request.body#~1callbackUrl}/post",
393+
"reportOnKey": false,
394+
"source": "foobar.yaml",
395+
},
396+
],
397+
"message": "Maximum callback nesting depth (2) reached. Path parameter validation is limited beyond this depth to prevent infinite recursion.",
398+
"ruleId": "path-params-defined",
399+
"severity": "error",
400+
"suggest": [],
401+
},
402+
]
403+
`);
404+
});
203405
});

0 commit comments

Comments
 (0)