@@ -257,6 +257,122 @@ def test_operation_tags_without_root_declaration_are_valid(self):
257257
258258 assert not errors
259259
260+ def test_tag_hierarchy_is_valid (self ):
261+ spec = {
262+ "openapi" : "3.2.0" ,
263+ "info" : {
264+ "title" : "Tag API" ,
265+ "version" : "1.0.0" ,
266+ },
267+ "tags" : [
268+ {
269+ "name" : "external" ,
270+ "kind" : "audience" ,
271+ },
272+ {
273+ "name" : "partner" ,
274+ "parent" : "external" ,
275+ "kind" : "audience" ,
276+ },
277+ {
278+ "name" : "partner-updates" ,
279+ "parent" : "partner" ,
280+ "kind" : "nav" ,
281+ },
282+ ],
283+ "paths" : {
284+ "/pets" : {
285+ "get" : {
286+ "responses" : {
287+ "200" : {
288+ "description" : "ok" ,
289+ },
290+ },
291+ },
292+ },
293+ },
294+ }
295+
296+ errors = list (OpenAPIV32SpecValidator (spec ).iter_errors ())
297+
298+ assert not errors
299+
300+ def test_tag_hierarchy_fails_for_unknown_parent (self ):
301+ spec = {
302+ "openapi" : "3.2.0" ,
303+ "info" : {
304+ "title" : "Tag API" ,
305+ "version" : "1.0.0" ,
306+ },
307+ "tags" : [
308+ {
309+ "name" : "partner" ,
310+ "parent" : "external" ,
311+ },
312+ ],
313+ "paths" : {
314+ "/pets" : {
315+ "get" : {
316+ "responses" : {
317+ "200" : {
318+ "description" : "ok" ,
319+ },
320+ },
321+ },
322+ },
323+ },
324+ }
325+
326+ errors = list (OpenAPIV32SpecValidator (spec ).iter_errors ())
327+
328+ assert len (errors ) == 1
329+ assert (
330+ errors [0 ].message
331+ == "Tag 'partner' references unknown parent tag 'external'"
332+ )
333+
334+ def test_tag_hierarchy_fails_for_circular_reference (self ):
335+ spec = {
336+ "openapi" : "3.2.0" ,
337+ "info" : {
338+ "title" : "Tag API" ,
339+ "version" : "1.0.0" ,
340+ },
341+ "tags" : [
342+ {
343+ "name" : "a" ,
344+ "parent" : "b" ,
345+ },
346+ {
347+ "name" : "b" ,
348+ "parent" : "c" ,
349+ },
350+ {
351+ "name" : "c" ,
352+ "parent" : "a" ,
353+ },
354+ ],
355+ "paths" : {
356+ "/pets" : {
357+ "get" : {
358+ "responses" : {
359+ "200" : {
360+ "description" : "ok" ,
361+ },
362+ },
363+ },
364+ },
365+ },
366+ }
367+
368+ errors = list (OpenAPIV32SpecValidator (spec ).iter_errors ())
369+
370+ assert errors
371+ assert any (
372+ err .message == "Circular tag hierarchy detected: a -> b -> c -> a"
373+ for err in errors
374+ )
375+
260376
261377def test_oas31_query_operation_is_not_semantically_traversed ():
262378 spec = {
@@ -312,6 +428,106 @@ def test_oas31_additional_operations_are_not_semantically_traversed():
312428 assert all ("Path parameter 'item_id'" not in err .message for err in errors )
313429
314430
431+ @pytest .mark .parametrize (
432+ "spec,validator_cls" ,
433+ [
434+ (
435+ {
436+ "swagger" : "2.0" ,
437+ "info" : {
438+ "title" : "Tag API" ,
439+ "version" : "1.0.0" ,
440+ },
441+ "tags" : [
442+ {
443+ "name" : "pets" ,
444+ },
445+ {
446+ "name" : "pets" ,
447+ "description" : "duplicate by name" ,
448+ },
449+ ],
450+ "paths" : {
451+ "/pets" : {
452+ "get" : {
453+ "responses" : {
454+ "200" : {
455+ "description" : "ok" ,
456+ },
457+ },
458+ },
459+ },
460+ },
461+ },
462+ OpenAPIV2SpecValidator ,
463+ ),
464+ (
465+ {
466+ "openapi" : "3.0.3" ,
467+ "info" : {
468+ "title" : "Tag API" ,
469+ "version" : "1.0.0" ,
470+ },
471+ "tags" : [
472+ {
473+ "name" : "pets" ,
474+ },
475+ {
476+ "name" : "pets" ,
477+ },
478+ ],
479+ "paths" : {
480+ "/pets" : {
481+ "get" : {
482+ "responses" : {
483+ "200" : {
484+ "description" : "ok" ,
485+ },
486+ },
487+ },
488+ },
489+ },
490+ },
491+ OpenAPIV30SpecValidator ,
492+ ),
493+ (
494+ {
495+ "openapi" : "3.1.0" ,
496+ "info" : {
497+ "title" : "Tag API" ,
498+ "version" : "1.0.0" ,
499+ },
500+ "tags" : [
501+ {
502+ "name" : "pets" ,
503+ },
504+ {
505+ "name" : "pets" ,
506+ },
507+ ],
508+ "paths" : {
509+ "/pets" : {
510+ "get" : {
511+ "responses" : {
512+ "200" : {
513+ "description" : "ok" ,
514+ },
515+ },
516+ },
517+ },
518+ },
519+ },
520+ OpenAPIV31SpecValidator ,
521+ ),
522+ ],
523+ )
524+ def test_oas2_oas3_duplicate_top_level_tags_are_invalid (spec , validator_cls ):
525+ errors = list (validator_cls (spec ).iter_errors ())
526+
527+ assert errors
528+ assert any (err .message == "Duplicate tag name 'pets'" for err in errors )
529+
530+
315531@pytest .mark .network
316532class TestRemoteOpenAPIv30Validator :
317533 REMOTE_SOURCE_URL = (
0 commit comments