diff --git a/openapi_spec_validator/handlers/compat.py b/openapi_spec_validator/handlers/compat.py index 534e746..9451a7d 100644 --- a/openapi_spec_validator/handlers/compat.py +++ b/openapi_spec_validator/handlers/compat.py @@ -4,5 +4,36 @@ except ImportError: from yaml import SafeLoader +from jsonschema.exceptions import ValidationError -__all__ = ['SafeLoader', ] + +class UniqueSchemasLoader(SafeLoader): + """Loader that checks that schemas definitions are unique""" + + POSSIBLE_SCHEMAS_YAML_PATHS = ["definitions", "components.schemas"] + + def construct_mapping(self, node, deep=True): + self._check_for_duplicate_schemas_definitions(node, deep, self.POSSIBLE_SCHEMAS_YAML_PATHS) + return super().construct_mapping(node, deep) + + def _check_for_duplicate_schemas_definitions(self, node, deep, possible_schemas_yaml_paths): + for schemas_yaml_path in possible_schemas_yaml_paths: + keys = [] + for key_node, value_node in node.value: + if schemas_yaml_path: + if key_node.value == schemas_yaml_path.split(".")[0]: + return self._check_for_duplicate_schemas_definitions( + value_node, + deep, + possible_schemas_yaml_paths=[ + ".".join(schemas_yaml_path.split(".")[1:]) + ], + ) + else: + key = self.construct_object(key_node, deep=deep) + if key in keys: + raise ValidationError(f"Duplicate definition for {key} schema.") + keys.append(key) + + +__all__ = ["SafeLoader", "UniqueSchemasLoader"] diff --git a/openapi_spec_validator/handlers/file.py b/openapi_spec_validator/handlers/file.py index 7c3ea65..f46a4d0 100644 --- a/openapi_spec_validator/handlers/file.py +++ b/openapi_spec_validator/handlers/file.py @@ -5,14 +5,14 @@ from yaml import load from openapi_spec_validator.handlers.base import BaseHandler -from openapi_spec_validator.handlers.compat import SafeLoader +from openapi_spec_validator.handlers.compat import UniqueSchemasLoader from openapi_spec_validator.handlers.utils import uri_to_path class FileObjectHandler(BaseHandler): """OpenAPI spec validator file-like object handler.""" - def __init__(self, loader=SafeLoader): + def __init__(self, loader=UniqueSchemasLoader): self.loader = loader def __call__(self, f): diff --git a/tests/integration/data/v2.0/petstore_duplicate_schemas_def.yaml b/tests/integration/data/v2.0/petstore_duplicate_schemas_def.yaml new file mode 100644 index 0000000..4206c54 --- /dev/null +++ b/tests/integration/data/v2.0/petstore_duplicate_schemas_def.yaml @@ -0,0 +1,113 @@ +swagger: "2.0" +info: + version: 1.0.0 + title: Swagger Petstore + license: + name: MIT +host: petstore.swagger.io +basePath: /v1 +schemes: + - http +consumes: + - application/json +produces: + - application/json +paths: + /pets: + get: + summary: List all pets + operationId: listPets + tags: + - pets + parameters: + - name: limit + in: query + description: How many items to return at one time (max 100) + required: false + type: integer + format: int32 + responses: + 200: + description: A paged array of pets + headers: + x-next: + type: string + description: A link to the next page of responses + schema: + $ref: '#/definitions/Pets' + default: + description: unexpected error + schema: + $ref: '#/definitions/Error' + post: + summary: Create a pet + operationId: createPets + tags: + - pets + responses: + '201': + description: Null response + default: + description: unexpected error + schema: + $ref: '#/definitions/Error' + /pets/{petId}: + get: + summary: Info for a specific pet + operationId: showPetById + tags: + - pets + parameters: + - name: petId + in: path + required: true + description: The id of the pet to retrieve + type: string + responses: + '200': + description: Expected response to a valid request + schema: + $ref: '#/definitions/Pets' + default: + description: unexpected error + schema: + $ref: '#/definitions/Error' +definitions: + Pet: + required: + - id + - name + properties: + id: + type: integer + format: int64 + name: + type: string + tag: + type: string + Pet: + required: + - id + - name + properties: + id: + type: integer + format: int64 + name: + type: string + tag: + type: string + Pets: + type: array + items: + $ref: '#/definitions/Pet' + Error: + required: + - code + - message + properties: + code: + type: integer + format: int32 + message: + type: string diff --git a/tests/integration/data/v3.0/petstore_duplicate_schemas_def.yaml b/tests/integration/data/v3.0/petstore_duplicate_schemas_def.yaml new file mode 100644 index 0000000..4452bf3 --- /dev/null +++ b/tests/integration/data/v3.0/petstore_duplicate_schemas_def.yaml @@ -0,0 +1,115 @@ +openapi: "3.0.0" +info: + version: 1.0.0 + title: Swagger Petstore + license: + name: MIT +servers: + - url: http://petstore.swagger.io/v1 +paths: + /pets: + get: + summary: List all pets + operationId: listPets + tags: + - pets + parameters: + - name: limit + in: query + description: How many items to return at one time (max 100) + required: false + schema: + type: integer + format: int32 + responses: + 200: + description: An paged array of pets + headers: + x-next: + description: A link to the next page of responses + schema: + type: string + content: + application/json: + schema: + $ref: "#/components/schemas/Pets" + default: + description: unexpected error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + post: + summary: Create a pet + operationId: createPets + tags: + - pets + responses: + '201': + description: Null response + default: + description: unexpected error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + /pets/{petId}: + get: + summary: Info for a specific pet + operationId: showPetById + tags: + - pets + parameters: + - name: petId + in: path + required: true + description: The id of the pet to retrieve + schema: + type: string + responses: + '200': + description: Expected response to a valid request + content: + application/json: + schema: + $ref: "#/components/schemas/Pets" + default: + description: unexpected error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" +components: + schemas: + Pet: + required: + - id + - name + properties: + id: + type: integer + format: int64 + name: + type: string + tag: + type: string + $ref: + type: string + Pets: + type: array + items: + $ref: "#/components/schemas/Pet" + Pets: + type: array + items: + $ref: "#/components/schemas/Pet" + Error: + required: + - code + - message + properties: + code: + type: integer + format: int32 + message: + type: string diff --git a/tests/integration/data/v3.1/petstore_duplicate_schemas_def.yaml b/tests/integration/data/v3.1/petstore_duplicate_schemas_def.yaml new file mode 100644 index 0000000..2fe2ff8 --- /dev/null +++ b/tests/integration/data/v3.1/petstore_duplicate_schemas_def.yaml @@ -0,0 +1,122 @@ +openapi: "3.1.0" +info: + version: 1.0.0 + title: Swagger Petstore + license: + name: MIT License + identifier: MIT +servers: + - url: http://petstore.swagger.io/v1 +paths: + /pets: + get: + summary: List all pets + operationId: listPets + tags: + - pets + parameters: + - name: limit + in: query + description: How many items to return at one time (max 100) + required: false + schema: + type: integer + format: int32 + responses: + 200: + description: An paged array of pets + headers: + x-next: + description: A link to the next page of responses + schema: + type: string + content: + application/json: + schema: + $ref: "#/components/schemas/Pets" + default: + description: unexpected error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + post: + summary: Create a pet + operationId: createPets + tags: + - pets + responses: + '201': + description: Null response + default: + description: unexpected error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + /pets/{petId}: + get: + summary: Info for a specific pet + operationId: showPetById + tags: + - pets + parameters: + - name: petId + in: path + required: true + description: The id of the pet to retrieve + schema: + type: string + responses: + '200': + description: Expected response to a valid request + content: + application/json: + schema: + $ref: "#/components/schemas/Pets" + default: + description: unexpected error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" +components: + schemas: + Pet: + required: + - id + - name + properties: + id: + type: integer + format: int64 + name: + type: string + tag: + type: string + $ref: + type: string + Pets: + type: array + items: + $ref: "#/components/schemas/Pet" + Error: + required: + - code + - message + properties: + code: + type: integer + format: int32 + message: + type: string + Error: + required: + - code + - message + properties: + code: + type: integer + format: int32 + message: + type: string diff --git a/tests/integration/test_main.py b/tests/integration/test_main.py index a99f4f7..15236e2 100644 --- a/tests/integration/test_main.py +++ b/tests/integration/test_main.py @@ -1,51 +1,45 @@ -import pytest from io import StringIO +from unittest import mock -from openapi_spec_validator.__main__ import main +import pytest -from unittest import mock +from openapi_spec_validator.__main__ import main def test_schema_default(): """Test default schema is 3.1.0""" - testargs = ['./tests/integration/data/v3.1/petstore.yaml'] + testargs = ["./tests/integration/data/v3.1/petstore.yaml"] main(testargs) def test_schema_v31(): """No errors when calling proper v3.1 file.""" - testargs = ['--schema', '3.1.0', - './tests/integration/data/v3.1/petstore.yaml'] + testargs = ["--schema", "3.1.0", "./tests/integration/data/v3.1/petstore.yaml"] main(testargs) def test_schema_v30(): """No errors when calling proper v3.0 file.""" - testargs = ['--schema', '3.0.0', - './tests/integration/data/v3.0/petstore.yaml'] + testargs = ["--schema", "3.0.0", "./tests/integration/data/v3.0/petstore.yaml"] main(testargs) def test_schema_v2(): """No errors when calling with proper v2 file.""" - testargs = ['--schema', '2.0', - './tests/integration/data/v2.0/petstore.yaml'] + testargs = ["--schema", "2.0", "./tests/integration/data/v2.0/petstore.yaml"] main(testargs) def test_errors_on_missing_description_best(capsys): """An error is obviously printed given an empty schema.""" - testargs = [ - './tests/integration/data/v3.0/missing-description.yaml', - '--schema=3.0.0' - ] + testargs = ["./tests/integration/data/v3.0/missing-description.yaml", "--schema=3.0.0"] with pytest.raises(SystemExit): main(testargs) out, err = capsys.readouterr() assert "Failed validating" in out assert "'description' is a required property" in out assert "'$ref' is a required property" not in out - assert '1 more subschemas errors' in out + assert "1 more subschemas errors" in out def test_errors_on_missing_description_full(capsys): @@ -61,51 +55,63 @@ def test_errors_on_missing_description_full(capsys): assert "Failed validating" in out assert "'description' is a required property" in out assert "'$ref' is a required property" in out - assert '1 more subschema error' not in out + assert "1 more subschema error" not in out def test_schema_unknown(): """Errors on running with unknown schema.""" - testargs = ['--schema', 'x.x', - './tests/integration/data/v2.0/petstore.yaml'] + testargs = ["--schema", "x.x", "./tests/integration/data/v2.0/petstore.yaml"] with pytest.raises(SystemExit): main(testargs) def test_validation_error(): """SystemExit on running with ValidationError.""" - testargs = ['--schema', '3.0.0', - './tests/integration/data/v2.0/petstore.yaml'] + testargs = ["--schema", "3.0.0", "./tests/integration/data/v2.0/petstore.yaml"] with pytest.raises(SystemExit): main(testargs) @mock.patch( - 'openapi_spec_validator.__main__.openapi_v30_spec_validator.validate', + "openapi_spec_validator.__main__.openapi_v30_spec_validator.validate", side_effect=Exception, ) def test_unknown_error(m_validate): """SystemExit on running with unknown error.""" - testargs = ['--schema', '3.0.0', - './tests/integration/data/v2.0/petstore.yaml'] + testargs = ["--schema", "3.0.0", "./tests/integration/data/v2.0/petstore.yaml"] with pytest.raises(SystemExit): main(testargs) def test_nonexisting_file(): """Calling with non-existing file should sys.exit.""" - testargs = ['i_dont_exist.yaml'] + testargs = ["i_dont_exist.yaml"] with pytest.raises(SystemExit): main(testargs) def test_schema_stdin(): """Test schema from STDIN""" - spes_path = './tests/integration/data/v3.0/petstore.yaml' - with open(spes_path, 'r') as spec_file: + spes_path = "./tests/integration/data/v3.0/petstore.yaml" + with open(spes_path, "r") as spec_file: spec_lines = spec_file.readlines() spec_io = StringIO("".join(spec_lines)) - testargs = ['--schema', '3.0.0', '-'] - with mock.patch('openapi_spec_validator.__main__.sys.stdin', spec_io): + testargs = ["--schema", "3.0.0", "-"] + with mock.patch("openapi_spec_validator.__main__.sys.stdin", spec_io): + main(testargs) + + +@pytest.mark.parametrize( + "version,spec_path", + [ + ("3.1.0", "./tests/integration/data/v3.1/petstore_duplicate_schemas_def.yaml"), + ("3.0.0", "./tests/integration/data/v3.0/petstore_duplicate_schemas_def.yaml"), + ("2.0", "./tests/integration/data/v2.0/petstore_duplicate_schemas_def.yaml"), + ], +) +def test_duplicate_schemas_definition(version, spec_path): + """Test exception when Swagger has duplicate schema definitions""" + testargs = ["--schema", version, spec_path] + with pytest.raises(SystemExit): main(testargs)
Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.
Alternative Proxies: