diff --git a/openapi_spec_validator/exceptions.py b/openapi_spec_validator/exceptions.py index 85d8c8a..bb44ce0 100644 --- a/openapi_spec_validator/exceptions.py +++ b/openapi_spec_validator/exceptions.py @@ -15,3 +15,7 @@ class ParameterDuplicateError(OpenAPIValidationError): class UnresolvableParameterError(OpenAPIValidationError): pass + + +class DuplicateOperationIDError(OpenAPIValidationError): + pass diff --git a/openapi_spec_validator/validators.py b/openapi_spec_validator/validators.py index 219c595..ef03b55 100644 --- a/openapi_spec_validator/validators.py +++ b/openapi_spec_validator/validators.py @@ -6,7 +6,7 @@ from openapi_spec_validator.exceptions import ( ParameterDuplicateError, ExtraParametersError, UnresolvableParameterError, - OpenAPIValidationError + OpenAPIValidationError, DuplicateOperationIDError, ) from openapi_spec_validator.decorators import ValidationErrorWrapper from openapi_spec_validator.factories import Draft4ExtendedValidatorFactory @@ -157,8 +157,10 @@ def _iter_value_errors(self, schema, value): class PathsValidator(object): - def __init__(self, dereferencer): + def __init__(self, dereferencer, operation_ids_registry=None): self.dereferencer = dereferencer + self.operation_ids_registry = [] if operation_ids_registry is None \ + else operation_ids_registry @wraps_errors def iter_errors(self, paths): @@ -168,13 +170,17 @@ def iter_errors(self, paths): yield err def _iter_path_errors(self, url, path_item): - return PathValidator(self.dereferencer).iter_errors(url, path_item) + return PathValidator( + self.dereferencer, self.operation_ids_registry).iter_errors( + url, path_item) class PathValidator(object): - def __init__(self, dereferencer): + def __init__(self, dereferencer, operation_ids_registry=None): self.dereferencer = dereferencer + self.operation_ids_registry = [] if operation_ids_registry is None \ + else operation_ids_registry @wraps_errors def iter_errors(self, url, path_item): @@ -184,7 +190,9 @@ def iter_errors(self, url, path_item): yield err def _iter_path_item_errors(self, url, path_item): - return PathItemValidator(self.dereferencer).iter_errors(url, path_item) + return PathItemValidator( + self.dereferencer, self.operation_ids_registry).iter_errors( + url, path_item) class PathItemValidator(object): @@ -193,8 +201,10 @@ class PathItemValidator(object): 'get', 'put', 'post', 'delete', 'options', 'head', 'patch', 'trace', ] - def __init__(self, dereferencer): + def __init__(self, dereferencer, operation_ids_registry=None): self.dereferencer = dereferencer + self.operation_ids_registry = [] if operation_ids_registry is None \ + else operation_ids_registry @wraps_errors def iter_errors(self, url, path_item): @@ -213,8 +223,9 @@ def iter_errors(self, url, path_item): yield err def _iter_operation_errors(self, url, name, operation, path_parameters): - return OperationValidator(self.dereferencer).iter_errors( - url, name, operation, path_parameters) + return OperationValidator( + self.dereferencer, self.operation_ids_registry).iter_errors( + url, name, operation, path_parameters) def _iter_parameters_errors(self, parameters): return ParametersValidator(self.dereferencer).iter_errors(parameters) @@ -222,14 +233,23 @@ def _iter_parameters_errors(self, parameters): class OperationValidator(object): - def __init__(self, dereferencer): + def __init__(self, dereferencer, seen_ids=None): self.dereferencer = dereferencer + self.seen_ids = [] if seen_ids is None else seen_ids @wraps_errors def iter_errors(self, url, name, operation, path_parameters=None): path_parameters = path_parameters or [] operation_deref = self.dereferencer.dereference(operation) + operation_id = operation_deref.get('operationId') + if operation_id is not None and operation_id in self.seen_ids: + yield DuplicateOperationIDError( + "Operation ID '{0}' for '{1}' in '{2}' is not unique".format( + operation_id, name, url) + ) + self.seen_ids.append(operation_id) + parameters = operation_deref.get('parameters', []) for err in self._iter_parameters_errors(parameters): yield err diff --git a/tests/integration/test_validators.py b/tests/integration/test_validators.py index db4a273..1a712fe 100644 --- a/tests/integration/test_validators.py +++ b/tests/integration/test_validators.py @@ -1,5 +1,6 @@ from openapi_spec_validator.exceptions import ( ExtraParametersError, UnresolvableParameterError, OpenAPIValidationError, + DuplicateOperationIDError, ) @@ -80,6 +81,40 @@ def test_same_parameters_names(self, validator): errors_list = list(errors) assert errors_list == [] + def test_same_operation_ids(self, validator): + spec = { + 'openapi': '3.0.0', + 'info': { + 'title': 'Test Api', + 'version': '0.0.1', + }, + 'paths': { + '/test': { + 'get': { + 'operationId': 'operation1', + 'responses': {}, + }, + 'post': { + 'operationId': 'operation1', + 'responses': {}, + }, + }, + '/test2': { + 'get': { + 'operationId': 'operation1', + 'responses': {}, + }, + }, + }, + } + + errors = validator.iter_errors(spec) + + errors_list = list(errors) + assert len(errors_list) == 2 + assert errors_list[0].__class__ == DuplicateOperationIDError + assert errors_list[1].__class__ == DuplicateOperationIDError + def test_allow_allof_required_no_properties(self, validator): spec = { 'openapi': '3.0.0',