def __init__(self, response_schema: dict, response_data: Any, **kwargs) -> None: """ Iterates through both a response schema and an actual API response to check that they match. :param response_schema: Response OpenAPI schema section :param response_data: API response data raises: django_swagger_tester.exceptions.SwaggerDocumentationError or ImproperlyConfigured """ if '$ref' in str(response_schema): # `$ref`s should be replaced inplace before the schema is passed to this class raise ImproperlyConfigured( 'Received invalid schema. You must replace $ref sections before passing a schema for validation.' ) if read_type(response_schema) == 'object': logger.debug('init --> dict') self.test_dict(schema=response_schema, data=response_data, reference='init', **kwargs) elif read_type(response_schema) == 'array': logger.debug('init --> list') self.test_list(schema=response_schema, data=response_data, reference='init', **kwargs) # this should always be third, as list_types also contains `array` and `object` elif read_type(response_schema) in list_types(): logger.debug('init --> item') self.test_item(schema=response_schema, data=response_data, reference='init', **kwargs)
def test_list(self, array: dict) -> None: """ Iterates through a schema array to pass appropriate nested items for further test_checks. Only object keys need case checking, so that's what we're looking for. """ item = read_items(array) if read_type(item) == 'object': logger.debug('list -> dict') self.test_dict(obj=item) elif read_type(item) == 'array': logger.debug('list -> list') self.test_list(array=item)
def test_read_type(): """ Ensure this helper function works as it's designed to. """ e = 'Schema item has an invalid `type` attribute. The type should be a single string' with pytest.raises(OpenAPISchemaError, match=e): read_type('test') e = 'Schema item has an invalid `type` attribute. The type `bad type` is not supported.' with pytest.raises(OpenAPISchemaError, match=e): read_type({'type': 'bad type'}) assert read_type({'type': 'string'}) == 'string'
def _iterate_schema_list(l: dict) -> list: x = [] i = read_items(l) if read_type(i) == 'object': x.append(_iterate_schema_dict(i)) elif read_type(i) == 'array': x.append(_iterate_schema_list(i)) # type: ignore elif 'example' in i: x.append(i['example']) else: raise ImproperlyConfigured( f'This schema item does not seem to have example value. Item: {i}') return x
def test_dict(self, obj: dict) -> None: """ Iterates through a schema object to check keys' case, and to pass nested values for further test_checks. """ properties = read_properties(obj) for key, value in properties.items(): conditional_check(key, self.case_check, self.ignored_keys) if read_type(value) == 'object': logger.debug('dict -> dict') self.test_dict(obj=value) elif read_type(value) == 'array': logger.debug('dict -> list') self.test_list(array=value)
def _iterate_schema_dict(d: dict) -> dict: x = {} for key, value in d['properties'].items(): if read_type(value) == 'object': x[key] = _iterate_schema_dict(value) elif read_type(value) == 'array': x[key] = _iterate_schema_list(value) # type: ignore elif 'example' in value: x[key] = value['example'] else: raise ImproperlyConfigured( f'This schema item does not seem to have example value. Item: {value}' ) return x
def serialize_schema(schema: dict) -> Union[list, dict, str, int, bool]: """ Converts an OpenAPI schema representation of a dict to dict. """ if read_type(schema) == 'array': logger.debug('--> list') return _iterate_schema_list(schema) elif read_type(schema) == 'object': logger.debug('--> dict') return _iterate_schema_dict(schema) elif 'example' in schema: return schema['example'] else: raise ImproperlyConfigured( f'This schema item does not seem to have example value. Item: {schema}' )
def __init__(self, schema: dict, **kwargs) -> None: """ Finds the appropriate case check function and calls the appropriate function base on the schema item type. :param schema: openapi schema item """ self.case_check = case_check(settings.CASE) self.ignored_keys = set_ignored_keys(**kwargs) if read_type(schema) == 'object': logger.debug('root -> dict') self.test_dict(schema) elif read_type(schema) == 'array': logger.debug('root -> list') self.test_list(schema) else: logger.debug('Skipping case check')
def test_list(self, schema: dict, data: Union[list, dict], reference: str, **kwargs) -> None: """ Verifies that a schema array matches a response list. :param schema: OpenAPI schema :param data: Response data :param reference: string reference pointing to function caller :raises: django_swagger_tester.exceptions.SwaggerDocumentationError """ if not isinstance(data, list): hint = '' if isinstance(data, dict): hint = 'You might need to wrap your response item in a list, or remove the excess list layer from your documented response.' raise format_error( error_message= f"Mismatched types. Expected response to be <class 'list'> but found {type(data)}.", data=data, schema=schema, reference=reference, hint=hint, **kwargs, ) item = read_items(schema) for index in range(len(data)): if read_type(item) == 'object': logger.debug('test_list --> test_dict') self.test_dict(schema=item, data=data[index], reference=f'{reference}.list', **kwargs) elif read_type(item) == 'array': logger.debug('test_list --> test_dict') self.test_list(schema=item, data=data[index], reference=f'{reference}.list', **kwargs) elif read_type(item) in list_types(): logger.debug('test_list --> test_item') self.test_item(schema=item, data=data[index], reference=f'{reference}.list', **kwargs)
def test_dict(self, schema: dict, data: Union[list, dict], reference: str, **kwargs) -> None: """ Verifies that a schema dict matches a response dict. :param schema: OpenAPI schema :param data: Response data :param reference: string reference pointing to function caller :raises: django_swagger_tester.exceptions.SwaggerDocumentationError """ if not isinstance(data, dict): hint = '' if isinstance(data, list): hint = 'The expected item should be a dict, or your schema should be a list.' raise format_error( error_message= f"Mismatched types. Expected response to be <class 'dict'> but found {type(data)}.", data=data, schema=schema, reference=reference, hint=hint, **kwargs, ) properties = read_properties(schema) schema_keys = properties.keys() response_keys = data.keys() check_keys_match(schema_keys, response_keys, schema, data, reference) for schema_key, response_key in zip(schema_keys, response_keys): # Check that each element in the schema exists in the response, and vice versa if schema_key not in response_keys: raise format_error( error_message= f'Schema key `{schema_key}` was not found in the API response.', data=data, schema=schema, reference=reference, hint= 'You need to add the missing schema key to the response, or remove it from the documented response.', **kwargs, ) elif response_key not in schema_keys: raise format_error( error_message= f'Response key `{response_key}` not found in the OpenAPI schema.', data=data, schema=schema, reference=reference, hint= 'You need to add the missing schema key to your documented response, or stop returning it in your API.', **kwargs, ) # Pass nested elements to the appropriate function schema_value = properties[schema_key] response_value = data[schema_key] if read_type(schema_value) == 'object': logger.debug( 'test_dict --> test_dict. Response: %s, Schema: %s', response_value, schema_value) self.test_dict(schema=schema_value, data=response_value, reference=f'{reference}.dict:key:{schema_key}', **kwargs) elif read_type(schema_value) == 'array': logger.debug( 'test_dict --> test_list. Response: %s, Schema: %s', response_value, schema_value) self.test_list(schema=schema_value, data=response_value, reference=f'{reference}.dict:key:{schema_key}', **kwargs) elif read_type(schema_value) in list_types( ): # This needs to come after array and object test_checks logger.debug( 'test_dict --> test_item. Response: %s, Schema: %s', response_value, schema_value) self.test_item(schema=schema_value, data=response_value, reference=f'{reference}.dict:key:{schema_key}', **kwargs)