From 9ad1c0e8bd32f875dba817aaa71a952a3d363199 Mon Sep 17 00:00:00 2001 From: "DESKTOP-10GD4VQ\\Sufyan" Date: Mon, 24 Feb 2025 15:09:37 +0500 Subject: [PATCH] fixes mypy reported issue in tests package and refactors test with typing support --- .gitignore | 3 +- apimatic_core/response_handler.py | 2 +- apimatic_core/utilities/api_helper.py | 64 +- apimatic_core/utilities/api_helper.py~ | 764 +++++++++++++++ apimatic_core/utilities/comparison_helper.py | 4 +- apimatic_core/utilities/xml_helper.py | 17 +- apimatic_core/utilities/xml_helper.py~ | 419 ++++++++ mypy.ini | 3 + mypy.ini~ | 11 + .../api_call_tests/test_api_call.py | 35 +- .../api_call_tests/test_api_call.py~ | 152 +++ .../api_logger_tests/test_api_logger.py | 250 ++--- .../test_logging_configuration.py | 47 +- .../test_logging_configuration.py~ | 223 +++++ tests/apimatic_core/base.py | 91 +- tests/apimatic_core/base.py~ | 501 ++++++++++ .../mocks/callables/base_uri_callable.py | 4 +- .../mocks/callables/base_uri_callable.py~ | 42 + .../exceptions/nested_model_exception.py | 2 +- tests/apimatic_core/mocks/http/http_client.py | 22 +- .../apimatic_core/mocks/http/http_client.py~ | 52 + .../mocks/http/http_response_catcher.py | 4 +- .../mocks/http/http_response_catcher.py~ | 28 + .../mocks/models/complex_type.py | 4 +- .../mocks/models/complex_type.py~ | 55 ++ .../test_request_builder.py | 251 +++-- .../test_request_builder.py~ | 832 ++++++++++++++++ .../test_response_handler.py | 52 +- .../test_response_handler.py~ | 399 ++++++++ .../union_type_tests/test_any_of.py | 49 +- .../union_type_tests/test_one_of.py | 77 +- .../utility_tests/test_api_helper.py | 399 +++++--- .../utility_tests/test_api_helper.py~ | 922 ++++++++++++++++++ .../utility_tests/test_auth_helper.py | 38 +- .../utility_tests/test_auth_helper.py~ | 61 ++ .../utility_tests/test_comparison_helper.py | 28 +- .../utility_tests/test_datetime_helper.py | 72 +- .../utility_tests/test_datetime_helper.py~ | 104 ++ .../utility_tests/test_file_helper.py | 13 +- .../utility_tests/test_file_helper.py~ | 14 + .../utility_tests/test_xml_helper.py | 134 ++- .../utility_tests/test_xml_helper.py~ | 670 +++++++++++++ 42 files changed, 6339 insertions(+), 575 deletions(-) create mode 100644 apimatic_core/utilities/api_helper.py~ create mode 100644 apimatic_core/utilities/xml_helper.py~ create mode 100644 mypy.ini~ create mode 100644 tests/apimatic_core/api_call_tests/test_api_call.py~ create mode 100644 tests/apimatic_core/api_logger_tests/test_logging_configuration.py~ create mode 100644 tests/apimatic_core/base.py~ create mode 100644 tests/apimatic_core/mocks/callables/base_uri_callable.py~ create mode 100644 tests/apimatic_core/mocks/http/http_client.py~ create mode 100644 tests/apimatic_core/mocks/http/http_response_catcher.py~ create mode 100644 tests/apimatic_core/mocks/models/complex_type.py~ create mode 100644 tests/apimatic_core/request_builder_tests/test_request_builder.py~ create mode 100644 tests/apimatic_core/response_handler_tests/test_response_handler.py~ create mode 100644 tests/apimatic_core/utility_tests/test_api_helper.py~ create mode 100644 tests/apimatic_core/utility_tests/test_auth_helper.py~ create mode 100644 tests/apimatic_core/utility_tests/test_datetime_helper.py~ create mode 100644 tests/apimatic_core/utility_tests/test_file_helper.py~ create mode 100644 tests/apimatic_core/utility_tests/test_xml_helper.py~ diff --git a/.gitignore b/.gitignore index 93f8354..d1ade8b 100644 --- a/.gitignore +++ b/.gitignore @@ -160,4 +160,5 @@ cython_debug/ .idea/ # Visual Studio Code -.vscode/ \ No newline at end of file +.vscode/ +.qodo diff --git a/apimatic_core/response_handler.py b/apimatic_core/response_handler.py index 1511de8..d318748 100644 --- a/apimatic_core/response_handler.py +++ b/apimatic_core/response_handler.py @@ -25,7 +25,7 @@ def __init__(self): self._xml_item_name: Optional[str] = None @validate_call - def deserializer(self, deserializer: Optional[Callable[[Union[str, bytes]], Any]]) -> 'ResponseHandler': + def deserializer(self, deserializer: Any) -> 'ResponseHandler': self._deserializer = deserializer return self diff --git a/apimatic_core/utilities/api_helper.py b/apimatic_core/utilities/api_helper.py index ad6c2f4..05e67b8 100644 --- a/apimatic_core/utilities/api_helper.py +++ b/apimatic_core/utilities/api_helper.py @@ -217,39 +217,6 @@ def json_deserialize( return unboxing_function(decoded) - @staticmethod - @validate_call - def apply_unboxing_function( - value: Any, unboxing_function: Callable[[Any], Any], is_array: bool=False, is_dict: bool=False, - is_array_of_map: bool=False, is_map_of_array: bool=False, dimension_count: int=1 - ) -> Any: - if is_dict: - if is_map_of_array: - return {k: ApiHelper.apply_unboxing_function(v, - unboxing_function, - is_array=True, - dimension_count=dimension_count) - for k, v in value.items()} - else: - return {k: unboxing_function(v) for k, v in value.items()} - elif is_array: - if is_array_of_map: - return [ - ApiHelper.apply_unboxing_function(element, - unboxing_function, - is_dict=True, - dimension_count=dimension_count) - for element in value] - elif dimension_count > 1: - return [ApiHelper.apply_unboxing_function(element, unboxing_function, - is_array=True, - dimension_count=dimension_count - 1) - for element in value] - else: - return [unboxing_function(element) for element in value] - - return unboxing_function(value) - @staticmethod @validate_call def dynamic_deserialize(dynamic_response: Optional[str]) -> Any: @@ -313,19 +280,19 @@ def datetime_deserialize( if DateTimeFormat.HTTP_DATE_TIME == datetime_format: if isinstance(deserialized_response, list): return [element.datetime for element in - ApiHelper.json_deserialize(value, ApiHelper.HttpDateTime.from_value)] + ApiHelper.json_deserialize(str(value), ApiHelper.HttpDateTime.from_value)] else: return ApiHelper.HttpDateTime.from_value(value).datetime elif DateTimeFormat.UNIX_DATE_TIME == datetime_format: if isinstance(deserialized_response, list): return [element.datetime for element in - ApiHelper.json_deserialize(value, ApiHelper.UnixDateTime.from_value)] + ApiHelper.json_deserialize(str(value), ApiHelper.UnixDateTime.from_value)] else: return ApiHelper.UnixDateTime.from_value(value).datetime elif DateTimeFormat.RFC3339_DATE_TIME == datetime_format: if isinstance(deserialized_response, list): return [element.datetime for element in - ApiHelper.json_deserialize(value, ApiHelper.RFC3339DateTime.from_value)] + ApiHelper.json_deserialize(str(value), ApiHelper.RFC3339DateTime.from_value)] else: return ApiHelper.RFC3339DateTime.from_value(value).datetime @@ -744,29 +711,6 @@ def to_lower_case(list_of_string: Optional[List[str]]) -> Optional[List[str]]: return list(map(lambda x: x.lower(), list_of_string)) - @staticmethod - @validate_call - def get_additional_properties( - dictionary: Dict[str, Any], unboxing_function: Callable[[Any], Any] - ) -> Dict[str, Any]: - """Extracts additional properties from the dictionary. - - Args: - dictionary (dict): The dictionary to extract additional properties from. - unboxing_function (callable): The deserializer to apply to each item in the dictionary. - - Returns: - dict: A dictionary containing the additional properties and their values. - """ - additional_properties = {} - for key, value in dictionary.items(): - try: - additional_properties[key] = unboxing_function(value) - except Exception: - pass - - return additional_properties - @staticmethod def sanitize_model(**kwargs: Any) -> Dict[str, Any]: _sanitized_dump: Dict[str, Any] = {} @@ -802,7 +746,7 @@ def sanitize_model(**kwargs: Any) -> Dict[str, Any]: @staticmethod def check_conflicts_with_additional_properties( - model_cls: BaseModel, properties: Dict[str, Any], additional_props_field: str + model_cls: BaseModel, properties: Any, additional_props_field: str ) -> None: """Raise ValueError if properties contain names conflicting with model fields.""" defined_fields = { diff --git a/apimatic_core/utilities/api_helper.py~ b/apimatic_core/utilities/api_helper.py~ new file mode 100644 index 0000000..e74b6f3 --- /dev/null +++ b/apimatic_core/utilities/api_helper.py~ @@ -0,0 +1,764 @@ +from __future__ import annotations +from abc import abstractmethod, ABC +from collections import abc +import re +import datetime +import calendar +import email.utils as eut +from time import mktime +from urllib.parse import urlsplit + +import jsonpickle +import dateutil.parser +from apimatic_core_interfaces.formats.datetime_format import DateTimeFormat +from apimatic_core_interfaces.types.union_type import UnionType +from jsonpointer import JsonPointerException, resolve_pointer +from typing import Optional, Any, Dict, Type, Callable, List, Union, Tuple, Set + +from apimatic_core.types.file_wrapper import FileWrapper +from apimatic_core.types.array_serialization_format import SerializationFormats +from urllib.parse import quote +from pydantic import BaseModel, validate_call +from pydantic.fields import FieldInfo + + +class ApiHelper(object): + """A Helper Class for various functions associated with API Calls. + + This class contains static methods for operations that need to be + performed during API requests. All of the methods inside this class are + static methods, there is no need to ever initialise an instance of this + class. + + """ + + class CustomDate(ABC): + + """ A base class for wrapper classes of datetime. + + This class contains methods which help in + appropriate serialization of datetime objects. + + """ + + def __init__(self, dtime, value=None): + self.datetime = dtime + if not value: + self.value = self.from_datetime(dtime) + else: + self.value = value + + def __repr__(self): + return str(self.value) + + def __getstate__(self): + return self.value + + def __setstate__(self, state): # pragma: no cover + pass + + @classmethod + @abstractmethod + def from_datetime(cls, date_time: datetime.datetime): + pass + + @classmethod + @abstractmethod + def from_value(cls, value: str): + pass + + class HttpDateTime(CustomDate): + + """ A wrapper class for datetime to support HTTP date format.""" + + @classmethod + def from_datetime(cls, date_time): + return eut.formatdate(timeval=mktime(date_time.timetuple()), localtime=False, usegmt=True) + + @classmethod + def from_value(cls, value): + dtime = datetime.datetime.fromtimestamp(eut.mktime_tz(eut.parsedate_tz(value))) + return cls(dtime, value) + + class UnixDateTime(CustomDate): + + """ A wrapper class for datetime to support Unix date format.""" + + @classmethod + def from_datetime(cls, date_time): + return calendar.timegm(date_time.utctimetuple()) + + @classmethod + def from_value(cls, value): + dtime = datetime.datetime.utcfromtimestamp(float(value)) + return cls(dtime, int(value)) + + class RFC3339DateTime(CustomDate): + + """ A wrapper class for datetime to support Rfc 3339 format.""" + + @classmethod + def from_datetime(cls, date_time): + return date_time.isoformat() + + @classmethod + def from_value(cls, value): + dtime = dateutil.parser.parse(value) + return cls(dtime, value) + + SKIP = '#$%^S0K1I2P3))*' + + @staticmethod + @validate_call + def json_serialize_wrapped_params(obj: Optional[Dict[str, Any]]) -> Optional[str]: + """JSON Serialization of a given wrapped object. + + Args: + obj (object): The object to serialize. + + Returns: + str: The JSON serialized string of the object. + + """ + if obj is None: + return None + val = dict() + for k, v in obj.items(): + val[k] = ApiHelper.json_serialize(v, should_encode=False) + + return jsonpickle.encode(val, False) + + @staticmethod + @validate_call + def json_serialize(obj: Any, should_encode: bool=True) -> Optional[str]: + """JSON Serialization of a given object. + + Args: + obj (object): The object to serialize. + should_encode: whether to encode at end or not + + Returns: + str: The JSON serialized string of the object. + + """ + + if obj is None: + return None + + if isinstance(obj, str): + return obj + + # Resolve any Names if it's one of our objects that needs to have this called on + if isinstance(obj, list): + value_list: List[Optional[str]] = list() + for item in obj: + if isinstance(item, dict) or isinstance(item, list): + value_list.append(ApiHelper.json_serialize(item, should_encode=False)) + elif ApiHelper.is_custom_type(item): + value_list.append(ApiHelper.json_serialize(item, should_encode=False)) + else: + value_list.append(item) + obj = value_list + elif isinstance(obj, dict): + value_dict: Dict[str, Optional[str]] = dict() + for key, item in obj.items(): + if isinstance(item, list) or isinstance(item, dict): + value_dict[key] = ApiHelper.json_serialize(item, should_encode=False) + elif ApiHelper.is_custom_type(item): + value_dict[key] = ApiHelper.json_serialize(item, should_encode=False) + else: + value_dict[key] = item + obj = value_dict + elif ApiHelper.is_custom_type(obj): + return obj.model_dump_json() if should_encode else obj.model_dump() + if not should_encode: + return obj + return jsonpickle.encode(obj, False) + + @staticmethod + @validate_call + def is_custom_type(obj_or_class: Any) -> bool: + return (isinstance(obj_or_class, type) and + issubclass(obj_or_class, BaseModel) or + isinstance(obj_or_class, BaseModel)) + + @staticmethod + @validate_call + def json_deserialize( + json: Optional[str], unboxing_function: Optional[Callable[[Any], Any]]=None, as_dict: bool=False + ) -> Any: + """JSON Deserialization of a given string. + + Args: + json (str): The JSON serialized string to deserialize. + unboxing_function (callable): The deserialization funtion to be used. + as_dict (bool): The flag to determine to deserialize json as dictionary type + + Returns: + dict: A dictionary representing the data contained in the + JSON serialized string. + + """ + if json is None or json.strip() == '': + return None + + try: + decoded = jsonpickle.decode(json) + except ValueError: + return json + + if unboxing_function is None: + return decoded + + if as_dict: + return {k: unboxing_function(v) for k, v in decoded.items()} + elif isinstance(decoded, list): + return [unboxing_function(element) for element in decoded] + + return unboxing_function(decoded) + + @staticmethod + @validate_call + def dynamic_deserialize(dynamic_response: Optional[str]) -> Any: + """JSON Deserialization of a given string. + + Args: + dynamic_response (str): The response string to deserialize. + + Returns: + dict: A dictionary representing the data contained in the + JSON serialized string. + + + """ + if dynamic_response is not None or not str(dynamic_response): + return ApiHelper.json_deserialize(dynamic_response) + + @staticmethod + @validate_call + def date_deserialize(json: Optional[str]) -> Union[datetime.date, List[datetime.date]]: + """JSON Deserialization of a given string. + + Args: + json (str): The JSON serialized string to deserialize. + + Returns: + dict: A dictionary representing the data contained in the + JSON serialized string. + + """ + deserialized_response = ApiHelper.json_deserialize(json) + if isinstance(deserialized_response, list): + return [dateutil.parser.parse(element).date() for element in deserialized_response] + + return dateutil.parser.parse(deserialized_response).date() + + @staticmethod + @validate_call + def datetime_deserialize( + value: Optional[Union[str, float]], datetime_format: Optional[DateTimeFormat] + ) -> Union[None, CustomDate, Dict[str, CustomDate], List[CustomDate]]: + """JSON Deserialization of a given string. + + Args: + value: the response to deserialize + datetime_format: The date time format to deserialize into + + Returns: + dict: A dictionary representing the data contained in the + JSON serialized string. + + """ + if value is None: + return None + + if isinstance(value, str): + deserialized_response = ApiHelper.json_deserialize(value) + else: + deserialized_response = value + + if DateTimeFormat.HTTP_DATE_TIME == datetime_format: + if isinstance(deserialized_response, list): + return [element.datetime for element in + ApiHelper.json_deserialize(str(value), ApiHelper.HttpDateTime.from_value)] + else: + return ApiHelper.HttpDateTime.from_value(value).datetime + elif DateTimeFormat.UNIX_DATE_TIME == datetime_format: + if isinstance(deserialized_response, list): + return [element.datetime for element in + ApiHelper.json_deserialize(str(value), ApiHelper.UnixDateTime.from_value)] + else: + return ApiHelper.UnixDateTime.from_value(value).datetime + elif DateTimeFormat.RFC3339_DATE_TIME == datetime_format: + if isinstance(deserialized_response, list): + return [element.datetime for element in + ApiHelper.json_deserialize(str(value), ApiHelper.RFC3339DateTime.from_value)] + else: + return ApiHelper.RFC3339DateTime.from_value(value).datetime + + return deserialized_response + + @staticmethod + @validate_call(config=dict(arbitrary_types_allowed=True)) + def deserialize_union_type( + union_type: UnionType, value: Any, should_deserialize: bool=True + ) -> Any: + if should_deserialize and isinstance(value, str): + value = ApiHelper.json_deserialize(value, as_dict=True) + + union_type_result = union_type.validate(value) + + return union_type_result.deserialize(value) + + @staticmethod + @validate_call + def get_content_type(value: Any) -> Optional[str]: + """Get content type header for oneof. + + Args: + value: The value passed by the user. + + Returns: + dict: A dictionary representing the data contained in the + JSON serialized string. + + """ + if value is None: + return None + primitive = (int, str, bool, float) + + if type(value) in primitive: + return 'text/plain; charset=utf-8' + + else: + return 'application/json; charset=utf-8' + + @staticmethod + @validate_call + def get_schema_path(path: str, file_name: str) -> str: + """Return the Schema's path + + Returns: + string : returns Correct schema path + + """ + pascal_name = file_name.replace("_", " ").title().replace(" ", "") + path = path.replace('\\models', '\\schemas').replace('/models', '/schemas') \ + .replace(".py", ".json").replace(file_name, pascal_name) + return path + + @staticmethod + @validate_call + def serialize_array( + key: str, array: List[Any], formatting: SerializationFormats=SerializationFormats.INDEXED, + is_query: bool=False + ) -> List[Tuple[str, Any]]: + """Converts an array parameter to a list of key value tuples. + + Args: + key (str): The name of the parameter. + array (list): The value of the parameter. + formatting (str): The type of key formatting expected. + is_query (bool): Decides if the parameters are for query or form. + + Returns: + list: A list with key value tuples for the array elements. + + """ + tuples: List[Tuple[str, Any]] = [] + + serializable_types = (str, int, float, bool, datetime.date, ApiHelper.CustomDate) + + if isinstance(array[0], serializable_types): + if formatting == SerializationFormats.UN_INDEXED: + tuples += [("{0}[]".format(key), element) for element in array] + elif formatting == SerializationFormats.INDEXED: + tuples += [("{0}[{1}]".format(key, index), element) for index, element in enumerate(array)] + elif formatting == SerializationFormats.PLAIN: + tuples += [(key, element) for element in array] + elif is_query: + if formatting == SerializationFormats.CSV: + tuples += [(key, ",".join(str(x) for x in array))] + + elif formatting == SerializationFormats.PSV: + tuples += [(key, "|".join(str(x) for x in array))] + + elif formatting == SerializationFormats.TSV: + tuples += [(key, "\t".join(str(x) for x in array))] + else: + raise ValueError("Invalid format provided.") + else: + raise ValueError("Invalid format provided.") + else: + tuples += [("{0}[{1}]".format(key, index), element) for index, element in enumerate(array)] + + return tuples + + @staticmethod + @validate_call + def append_url_with_template_parameters(url: Optional[str], parameters: Optional[Dict[str, Dict[str, Any]]]) -> str: + """Replaces template parameters in the given url. + + Args: + url (str): The query url string to replace the template parameters. + parameters (dict): The parameters to replace in the url. + + Returns: + str: URL with replaced parameters. + + """ + # Parameter validation + if url is None: + raise ValueError("URL is None.") + + if parameters is None: + return url + + # Iterate and replace parameters + for key in parameters: + value = parameters[key]['value'] + encode = parameters[key]['encode'] + replace_value = '' + + # Load parameter value + if value is None: + replace_value = '' + elif isinstance(value, list): + replace_value = "/".join((quote(str(x), safe='') if encode else str(x)) for x in value) + else: + replace_value = quote(str(value), safe='') if encode else str(value) + + url = url.replace('{{{0}}}'.format(key), str(replace_value)) + + return url + + @staticmethod + @validate_call + def append_url_with_query_parameters( + url: Optional[str], parameters: Optional[Dict[str, Any]], + array_serialization: SerializationFormats=SerializationFormats.INDEXED + ) -> str: + """Adds query parameters to a URL. + + Args: + url (str): The URL string. + parameters (dict): The query parameters to add to the URL. + array_serialization (str): The format of array parameter serialization. + + Returns: + str: URL with added query parameters. + + """ + # Parameter validation + if url is None: + raise ValueError("URL is None.") + + if parameters is None: + return url + + query_parameters = ApiHelper.process_complex_types_parameters(parameters, array_serialization) + for value in query_parameters: + key = value[0] + val = value[1] + seperator = '&' if '?' in url else '?' + if value is not None: + url += "{0}{1}={2}".format(seperator, key, quote(str(val), safe='')) + + return url + + @staticmethod + @validate_call + def get_url_without_query(url: str) -> str: + """ + Extracts the protocol, domain, and path from a URL excluding the query parameters. + + Args: + url: The URL string. + + Returns: + A string containing the protocol, domain, and path of the URL without the query string. + + Raises: + ValueError: If the URL is invalid. + """ + try: + parsed_url = urlsplit(url) + if not parsed_url.netloc: # Check if URL has scheme and netloc (valid URL) + raise ValueError("Invalid URL format") + return f"{parsed_url.scheme}://{parsed_url.netloc}{parsed_url.path}" + except ValueError as e: + raise ValueError(f"Error parsing URL: {e}") from e + + @staticmethod + @validate_call + def process_complex_types_parameters( + query_parameters: Dict[str, Any], array_serialization: SerializationFormats + ) -> List[Tuple[str, Any]]: + processed_params = [] + for key, value in query_parameters.items(): + processed_params.extend( + ApiHelper.form_encode(value, key, array_serialization=array_serialization, is_query=True)) + return processed_params + + @staticmethod + @validate_call + def clean_url(url: str) -> str: + """Validates and processes the given query Url to clean empty slashes. + + Args: + url (str): The given query Url to process. + + Returns: + str: Clean Url as string. + + """ + # Ensure that the urls are absolute + regex = "^https?://[^/]+" + match = re.match(regex, url) + if match is None: + raise ValueError('Invalid Url format.') + + protocol = match.group(0) + index = url.find('?') + query_url = url[len(protocol): index if index != -1 else None] + query_url = re.sub("//+", "/", query_url) + parameters = url[index:] if index != -1 else "" + + return protocol + query_url + parameters + + @staticmethod + @validate_call + def form_encode_parameters( + form_parameters: Dict[str, Any], array_serialization: SerializationFormats=SerializationFormats.INDEXED + ) -> List[Tuple[str, Any]]: + """Form encodes a dictionary of form parameters + + Args: + form_parameters (dictionary): The given dictionary which has + atleast one model to form encode. + array_serialization (str): The format of array parameter serialization. + + Returns: + dict: A dictionary of form encoded properties of the model. + + """ + encoded = [] + + for key, value in form_parameters.items(): + encoded += ApiHelper.form_encode(value, key, array_serialization) + + return encoded + + @staticmethod + @validate_call + def form_encode( + obj: Any, instance_name: str, + array_serialization: SerializationFormats=SerializationFormats.INDEXED, is_query: bool=False + ) -> List[Tuple[str, Any]]: + """Encodes a model in a form-encoded manner such as person[Name] + + Args: + obj (object): The given Object to form encode. + instance_name (string): The base name to appear before each entry + for this object. + array_serialization (string): The format of array parameter serialization. + is_query (bool): Decides if the parameters are for query or form. + + Returns: + dict: A dictionary of form encoded properties of the model. + + """ + retval = [] + # If we received an object, resolve its field names. + if ApiHelper.is_custom_type(obj): + obj = ApiHelper.json_serialize(obj, should_encode=False) + + if obj is None: + return [] + elif isinstance(obj, list): + for element in ApiHelper.serialize_array(instance_name, obj, array_serialization, is_query): + retval += ApiHelper.form_encode(element[1], element[0], array_serialization, is_query) + elif isinstance(obj, dict): + for item in obj: + retval += ApiHelper.form_encode(obj[item], instance_name + "[" + item + "]", array_serialization, + is_query) + else: + if isinstance(obj, bool): + obj = str(obj).lower() + retval.append((instance_name, obj)) + + return retval + + @staticmethod + @validate_call + def apply_datetime_converter( + value: Any, + datetime_converter_obj: Callable[[Any], Any]) -> Any: + if isinstance(value, list): + return [ApiHelper.apply_datetime_converter(item, datetime_converter_obj) for item in value] + + if isinstance(value, dict): + return {k: ApiHelper.apply_datetime_converter(v, datetime_converter_obj) for k, v in value.items()} + + if isinstance(value, datetime.datetime): + return ApiHelper.when_defined(datetime_converter_obj, value) + + return value + + @staticmethod + @validate_call + def when_defined(func: Callable[[datetime.datetime], Any], value: datetime.datetime) -> Any: + return func(value) if value else None + + @staticmethod + @validate_call + def is_file_wrapper_instance(param: Any) -> bool: + return isinstance(param, FileWrapper) + + @staticmethod + def is_valid_type( + value: Any, type_callable: Callable[[Any], bool], is_value_nullable: bool=False, is_model_dict: bool=False, + is_inner_model_dict: bool=False + ) -> bool: + if value is None and is_value_nullable: + return True + + if isinstance(value, list): + return all(ApiHelper.is_valid_type(item, type_callable, is_value_nullable, is_model_dict, + is_inner_model_dict) for item in value) + elif isinstance(value, dict) and (not is_model_dict or is_inner_model_dict): + return all(ApiHelper.is_valid_type(item, type_callable, is_value_nullable, is_model_dict) + for item in value.values()) + + return value is not None and type_callable(value) + + @staticmethod + @validate_call + def resolve_template_placeholders_using_json_pointer( + placeholders: Set[str], value: Optional[Dict[str, Any]], template: str + ) -> str: + """Updates all placeholders in the given message template with provided value. + + Args: + placeholders: The placeholders that need to be searched and replaced in the given template value. + value: The dictionary containing the actual values to replace with. + template: The template string containing placeholders. + + Returns: + string: The resolved template value. + """ + for placeholder in placeholders: + extracted_value: Optional[str] = '' + + if '#' in placeholder: + # pick the 2nd chunk then remove the last character (i.e. `}`) of the string value + node_pointer = placeholder.rsplit('#')[1].rstrip('}') + try: + extracted_value = resolve_pointer(value, node_pointer) if node_pointer else '' + extracted_value = ApiHelper.json_serialize(extracted_value) \ + if type(extracted_value) in [list, dict] else str(extracted_value) + except JsonPointerException: + pass + elif value is not None: + extracted_value = ApiHelper.json_serialize(value) + template = template.replace(placeholder, extracted_value or '') + + return template + + @staticmethod + @validate_call + def resolve_template_placeholders(placeholders: Set[str], values: Union[str, Dict[str, Any]], template: str) -> str: + """Updates all placeholders in the given message template with provided value. + + Args: + placeholders: The placeholders that need to be searched and replaced in the given template value. + values: The dictionary|string value which refers to the actual values to replace with. + template: The template string containing placeholders. + + Returns: + string: The resolved template value. + """ + for placeholder in placeholders: + if isinstance(values, abc.Mapping): + # pick the last chunk then strip the last character (i.e. `}`) of the string value + key = placeholder.rsplit('.', maxsplit=1)[-1].rstrip('}') if '.' in placeholder \ + else placeholder.lstrip('{').rstrip('}') + value_to_replace = str(values.get(key)) if values.get(key) else '' + template = template.replace(placeholder, value_to_replace) + else: + values = str(values) if values is not None else '' + template = template.replace(placeholder, values) + + return template + + @staticmethod + @validate_call + def to_lower_case(list_of_string: Optional[List[str]]) -> Optional[List[str]]: + """Converts all strings in a list to lowercase. + + Args: + list_of_string (list): A list of strings to convert to lowercase. + + Returns: + list: A new list containing the lowercase versions of the input strings. + Returns None if the input list is None. + + Raises: + TypeError: If the input is not a list. + """ + if list_of_string is None: + return None + + return list(map(lambda x: x.lower(), list_of_string)) + + @staticmethod + def sanitize_model(**kwargs: Any) -> Dict[str, Any]: + _sanitized_dump: Dict[str, Any] = {} + _nullable_fields: Set[str] = kwargs.get('nullable_fields', set()) + _optional_fields: Set[str] = kwargs.get('optional_fields', set()) + _model_dump: Dict[str, Any] = kwargs.get('model_dump', {}) + _model_fields: Dict[str, FieldInfo] = kwargs.get('model_fields', {}) + _model_fields_set: Set[str] = kwargs.get('model_fields_set', set()) + + for _name, _field_info in _model_fields.items(): + _value = _model_dump.pop(_name, None) + _alias = _field_info.serialization_alias or _name + + if _name not in _nullable_fields and _name not in _optional_fields: + # Always include required properties + _sanitized_dump[_alias] = _value + + elif _name in _nullable_fields and _name in _optional_fields: + # Include optional_nullable properties if explicitly set or not None + if _value is not None or _name in _model_fields_set: + _sanitized_dump[_alias] = _value + + elif _name in _optional_fields: + # Exclude optional properties if None + if _value is not None: + _sanitized_dump[_alias] = _value + + elif _name in _nullable_fields: + # Always include nullable properties, even if None + _sanitized_dump[_alias] = _value + + return _sanitized_dump + + @staticmethod + def check_conflicts_with_additional_properties( + model_cls: BaseModel, properties: Dict[str, Any], additional_props_field: str + ) -> None: + """Raise ValueError if properties contain names conflicting with model fields.""" + defined_fields = { + alias or name + for name, field_info in model_cls.model_fields.items() + if name != additional_props_field # Dynamically exclude additional properties field + for alias in (field_info.serialization_alias, name) + } + + conflicting_keys = defined_fields.intersection(properties) + if conflicting_keys: + raise ValueError( + f"Invalid additional properties: {conflicting_keys}. " + "These names conflict with existing model properties." + ) diff --git a/apimatic_core/utilities/comparison_helper.py b/apimatic_core/utilities/comparison_helper.py index 5c13822..d062bff 100644 --- a/apimatic_core/utilities/comparison_helper.py +++ b/apimatic_core/utilities/comparison_helper.py @@ -10,8 +10,8 @@ class ComparisonHelper: @staticmethod @validate_call def match_headers( - expected_headers: Dict[str, Optional[str]], - received_headers: Dict[str, str], + expected_headers: Dict[str, Any], + received_headers: Dict[str, Any], allow_extra: bool = True ) -> bool: """Static method to compare the received headers with the expected headers. diff --git a/apimatic_core/utilities/xml_helper.py b/apimatic_core/utilities/xml_helper.py index 700dc20..1939795 100644 --- a/apimatic_core/utilities/xml_helper.py +++ b/apimatic_core/utilities/xml_helper.py @@ -24,7 +24,7 @@ class XmlHelper: @staticmethod @validate_call - def serialize_to_xml(value: Any, root_element_name: str) -> str: + def serialize_to_xml(value: Any, root_element_name: str) -> Optional[str]: """Serializes a given value to an XML document. Args: @@ -34,6 +34,9 @@ def serialize_to_xml(value: Any, root_element_name: str) -> str: Returns: str: Serialized XML string. """ + if value is None: + return None + root = ET.Element(root_element_name) if value is not None: @@ -43,7 +46,7 @@ def serialize_to_xml(value: Any, root_element_name: str) -> str: @staticmethod @validate_call - def serialize_list_to_xml(value: List[Any], root_element_name: str, array_item_name: str) -> str: + def serialize_list_to_xml(value: Optional[List[Any]], root_element_name: str, array_item_name: str) -> Optional[str]: """Serializes a given list of values to an XML document. Args: @@ -54,6 +57,9 @@ def serialize_list_to_xml(value: List[Any], root_element_name: str, array_item_n Returns: str: Serialized XML string. """ + if value is None: + return None + root = ET.Element(root_element_name) XmlHelper.add_list_as_subelement(root, value, array_item_name) @@ -62,7 +68,7 @@ def serialize_list_to_xml(value: List[Any], root_element_name: str, array_item_n @staticmethod @validate_call - def serialize_dict_to_xml(value: Dict[str, Any], root_element_name: str) -> str: + def serialize_dict_to_xml(value: Optional[Dict[str, Any]], root_element_name: str) -> Optional[str]: """Serializes a given dictionary of values to an XML document. Args: @@ -72,6 +78,9 @@ def serialize_dict_to_xml(value: Dict[str, Any], root_element_name: str) -> str: Returns: str: Serialized XML string. """ + if value is None: + return None + root = ET.Element(root_element_name) XmlHelper.add_dict_as_subelement(root, value) @@ -126,7 +135,7 @@ def add_as_subelement(root: ET.Element, value: Any, name: str) -> None: @staticmethod @validate_call(config=dict(arbitrary_types_allowed=True)) - def add_list_as_subelement(root: ET.Element, items: List[Any], item_name: str, + def add_list_as_subelement(root: ET.Element, items: Optional[List[Any]], item_name: str, wrapping_element_name: Optional[str] = None) -> None: """Converts the given list to an ET.Element if it is not None and adds it to an existing ET.Element. diff --git a/apimatic_core/utilities/xml_helper.py~ b/apimatic_core/utilities/xml_helper.py~ new file mode 100644 index 0000000..724340d --- /dev/null +++ b/apimatic_core/utilities/xml_helper.py~ @@ -0,0 +1,419 @@ +# -*- coding: utf-8 -*- + +""" +testerxml + +This file was automatically generated by APIMATIC v3.0 ( + https://www.apimatic.io ). +""" + +import xml.etree.ElementTree as ET +import datetime +import dateutil.parser +from typing import Any, List, Dict, Optional, Type, Callable, Union + +from pydantic import validate_call + +from apimatic_core.utilities.api_helper import ApiHelper + + +class XmlHelper: + """This class hold utility methods for xml serialization and + deserialization. + """ + + @staticmethod + @validate_call + def serialize_to_xml(value: Any, root_element_name: str) -> Optional[str]: + """Serializes a given value to an XML document. + + Args: + value (Any): The value to serialize. + root_element_name (str): The name of the document's root element. + + Returns: + str: Serialized XML string. + """ + if value is None: + return None + + root = ET.Element(root_element_name) + + if value is not None: + XmlHelper.add_to_element(root, value) + + return ET.tostring(root).decode() + + @staticmethod + @validate_call + def serialize_list_to_xml(value: Optional[List[Any]], root_element_name: str, array_item_name: str) -> Optional[str]: + """Serializes a given list of values to an XML document. + + Args: + value (List[Any]): The list of values to serialize. + root_element_name (str): The name of the document's root element. + array_item_name (str): The element name to use for each item in 'values'. + + Returns: + str: Serialized XML string. + """ + if value is None: + return None + + root = ET.Element(root_element_name) + + XmlHelper.add_list_as_subelement(root, value, array_item_name) + + return ET.tostring(root).decode() + + @staticmethod + @validate_call + def serialize_dict_to_xml(value: Optional[Dict[str, Any][], root_element_name: str) -> str: + """Serializes a given dictionary of values to an XML document. + + Args: + value (Dict[str, Any]): The dictionary to serialize. + root_element_name (str): The name of the document's root element. + + Returns: + str: Serialized XML string. + """ + if value is None: + return None + + root = ET.Element(root_element_name) + + XmlHelper.add_dict_as_subelement(root, value) + + return ET.tostring(root).decode() + + @staticmethod + @validate_call(config=dict(arbitrary_types_allowed=True)) + def add_to_element(element: ET.Element, value: Any) -> None: + """Converts the given value to XML and adds it to an existing ET.Element. + + Args: + element (ET.Element): The XML tag to add the 'value' to. + value (Any): The value to add to the element. + """ + if value is None: + return + + if isinstance(value, bool): + element.text = str(value).lower() + elif isinstance(value, (int, float, str, datetime.date, ApiHelper.CustomDate)): + element.text = str(value) + else: + value.to_xml_sub_element(element) + + @staticmethod + @validate_call(config=dict(arbitrary_types_allowed=True)) + def add_as_attribute(root: ET.Element, value: Any, name: str) -> None: + """Sets an attribute on an ET.Element instance if the value isn't None. + + Args: + root (ET.Element): The parent of this XML attribute. + value (Any): The value to set to the attribute. + name (str): The name of the attribute being set. + """ + if value is not None: + root.set(name, str(value).lower() if isinstance(value, bool) else str(value)) + + @staticmethod + @validate_call(config=dict(arbitrary_types_allowed=True)) + def add_as_subelement(root: ET.Element, value: Any, name: str) -> None: + """Converts the given value to an ET.Element if it is not None and adds it to an existing ET.Element. + + Args: + root (ET.Element): The parent of this XML element. + value (Any): The value to add to the element. + name (str): The name of the element being added. + """ + if value is not None: + tag = ET.SubElement(root, name) + XmlHelper.add_to_element(tag, value) + + @staticmethod + @validate_call(config=dict(arbitrary_types_allowed=True)) + def add_list_as_subelement(root: ET.Element, items: Optional[List[Any]], item_name: str, + wrapping_element_name: Optional[str] = None) -> None: + """Converts the given list to an ET.Element if it is not None and adds it to an existing ET.Element. + + Args: + root (ET.Element): The parent of this XML element. + items (List[Any]): The list of values to add to the element. + item_name (str): The element name to use for each item in 'items'. + wrapping_element_name (Optional[str]): The element name to use for the wrapping element, if needed. + """ + if items is not None: + parent = ET.SubElement(root, wrapping_element_name) if wrapping_element_name else root + for item in items: + sub_elem = ET.SubElement(parent, item_name) + XmlHelper.add_to_element(sub_elem, item) + + @staticmethod + @validate_call(config=dict(arbitrary_types_allowed=True)) + def add_dict_as_subelement(root: ET.Element, items: Dict[str, Any], dictionary_name: Optional[str] = None) -> None: + """Converts the given dictionary to an ET.Element if it is not None and adds it to an existing ET.Element. + + Args: + root (ET.Element): The parent of this XML element. + items (Dict[str, Any]): The dictionary of values to add to the element. + dictionary_name (Optional[str]): The element name to use for the encapsulating element. + """ + if items is not None: + parent = ET.SubElement(root, dictionary_name) if dictionary_name else root + for key, value in items.items(): + if isinstance(value, list): + XmlHelper.add_list_as_subelement(parent, value, key) + else: + XmlHelper.add_as_subelement(parent, value, key) + + @staticmethod + @validate_call + def deserialize_xml(xml: Optional[str], clazz: Type[Any]) -> Optional[Any]: + """Deserializes an XML document to a Python object of the type given by 'clazz'. + + Args: + xml (Optional[str]): An XML document to deserialize. + clazz (Type[Any]): The class that the deserialized object should belong to. + + Returns: + Optional[Any]: An instance of the specified class, or None if input is empty. + """ + if not xml: + return None + + root = ET.fromstring(xml) + return XmlHelper.value_from_xml_element(root, clazz) + + @staticmethod + @validate_call + def deserialize_xml_to_list(xml: Optional[str], item_name: str, clazz: Type[Any]) -> Optional[List[Any]]: + """Deserializes an XML document to a list of Python objects, each of the type given by 'clazz'. + + Args: + xml (Optional[str]): An XML document to deserialize. + item_name (str): The name of the elements that need to be extracted into a list. + clazz (Type[Any]): The class that the deserialized objects should belong to. + + Returns: + Optional[List[Any]]: A list of instances of the specified class, or None if input is empty. + """ + if not xml: + return None + + root = ET.fromstring(xml) + return XmlHelper.list_from_xml_element(root, item_name, clazz) + + @staticmethod + @validate_call + def deserialize_xml_to_dict(xml: Optional[str], clazz: Type[Any]) -> Optional[Dict[str, Any]]: + """Deserializes an XML document to a dictionary of Python objects, each of the type given by 'clazz'. + + Args: + xml (Optional[str]): An XML document to deserialize. + clazz (Type[Any]): The class that the dictionary values should belong to. + + Returns: + Optional[Dict[str, Any]]: A dictionary with deserialized objects, or None if input is empty. + """ + if not xml: + return None + + root = ET.fromstring(xml) + return XmlHelper.dict_from_xml_element(root, clazz) + + @staticmethod + @validate_call(config=dict(arbitrary_types_allowed=True)) + def value_from_xml_attribute(attribute: Optional[Union[ET.Element, str]], clazz: Type[Any]) -> Optional[Any]: + """Extracts the value from an attribute and converts it to the type + given by 'clazz'. + + Args: + attribute (str): The XML attribute to extract the value from. + clazz (Type[Any]): The class that the deserialized object should + belong to. + """ + if attribute is None: + return None + + conversion_function = XmlHelper.converter(clazz) + + return conversion_function(attribute)\ + if conversion_function is not None and isinstance(attribute, str) else None + + @staticmethod + @validate_call(config=dict(arbitrary_types_allowed=True)) + def value_from_xml_element(element: Optional[ET.Element], clazz: Type[Any]) -> Optional[Any]: + """Extracts the value from an element and converts it to the type given by 'clazz'. + + Args: + element (ET.Element): The XML element to extract the value from. + clazz (Type[Any]): The class that the deserialized object should belong to. + + Returns: + Optional[Any]: An instance of the specified class, or None if the element is None. + """ + if element is None: + return None + + # These classes can be cast directly. + if clazz in [int, float, str, bool, datetime.date] or \ + issubclass(clazz, ApiHelper.CustomDate): + conversion_function = XmlHelper.converter(clazz) + return conversion_function(element.text)\ + if conversion_function is not None and element.text is not None else None + + conversion_function = clazz.from_element if hasattr(clazz, 'from_element') else None + + return conversion_function(element) if conversion_function is not None and element is not None else None + + + + @staticmethod + @validate_call(config=dict(arbitrary_types_allowed=True)) + def list_from_xml_element(root: Optional[ET.Element], item_name: str, clazz: Type[Any], + wrapping_element_name: Optional[str] = None) -> Optional[List[Any]]: + """Deserializes an XML document to a list of Python objects, each of the type given by 'clazz'. + + Args: + root (ET.Element): The root XML element. + item_name (str): The name of the elements that need to be extracted into a list. + clazz (Type[Any]): The class that the deserialized objects should belong to. + wrapping_element_name (Optional[str]): The name of the wrapping element for the XML element array. + + Returns: + Optional[List[Any]]: A list of deserialized objects, or None if no matching elements are found. + """ + if root is None: + return None + + elements = XmlHelper.get_elements(root, wrapping_element_name, item_name) + + if elements is None: + return None + + return [XmlHelper.value_from_xml_element(element, clazz) for element in elements] + + @staticmethod + @validate_call(config=dict(arbitrary_types_allowed=True)) + def dict_from_xml_element(element: Optional[ET.Element], clazz: Type[Any]) -> Optional[Dict[str, Any]]: + """Extracts the values from an element and converts them to a dictionary with values of type 'clazz'. + + Args: + element (ET.Element): The XML element to convert. + clazz (Type[Any]): The class that the dictionary values should belong to. + + Returns: + Optional[Dict[str, Any]]: A dictionary of deserialized values, or None if the element is None. + """ + if element is None: + return None + + entries = list(element) + conversion_function = XmlHelper.converter(clazz) + return { + entry.tag: conversion_function(entry.text) if entry.text is not None else None + for entry in entries if conversion_function + } + + @staticmethod + @validate_call + def converter(clazz: Type[Any]) -> Optional[Callable[[str], Any]]: + """Provides the function to use for converting a string to the type given by 'clazz'. + + Args: + clazz (Type[Any]): The class to find the conversion function for. + + Returns: + Callable[[str], Any]: A conversion function, or None if not applicable. + """ + if clazz in [int, float, str]: + return lambda value: clazz(value) + elif clazz is bool: + return lambda value: value.lower() == 'true' + elif clazz is datetime.date: + return lambda value: dateutil.parser.parse(value).date() + # DateTime classes have their own method to convert from string. + elif issubclass(clazz, ApiHelper.CustomDate): + return clazz.from_value + + return None + + @staticmethod + @validate_call(config=dict(arbitrary_types_allowed=True)) + def value_from_one_of_xml_elements( + root: Optional[ET.Element], mapping_data: Optional[Dict[str, Any]] + ) -> Optional[Any]: + """Extracts the value from an element and converts it to the type given + by 'clazz'. + + Args: + root (ET.Element): The root XML element. + mapping_data (dict): A dictionary mapping possible element names + for a given field to corresponding types. + """ + if not mapping_data or root is None: + return None + + for element_name, tup in mapping_data.items(): + clazz = tup[0] + is_array = tup[1] + wrapping_element_name = tup[2] + if is_array: + elements = XmlHelper.get_elements(root, wrapping_element_name, element_name) + if elements is not None and len(elements) > 0: + return XmlHelper.list_from_xml_element( + root, element_name, clazz, wrapping_element_name) + else: + element = root.find(element_name) + if element is not None: + return XmlHelper.value_from_xml_element(element, clazz) + return None + + @staticmethod + @validate_call(config=dict(arbitrary_types_allowed=True)) + def list_from_multiple_one_of_xml_element(root: ET.Element, mapping_data: Dict[str, Any]) -> Optional[List[Any]]: + """Deserializes an xml document to a list of python objects + where all types of oneof schemas are allowed (when the outer + model is an array) + + Args: + root (ET.Element): An xml document to deserialize. + mapping_data (dict): A dictionary mapping possible element names + for a given field to corresponding types. + + """ + arr = [] + for elem in root.iter(): + if elem.tag in mapping_data: + arr.append(XmlHelper.value_from_xml_element( + elem, mapping_data[elem.tag][0])) + if len(arr) > 0: + return arr + else: + return None + + @staticmethod + @validate_call(config=dict(arbitrary_types_allowed=True)) + def get_elements(root: Optional[ET.Element], wrapping_element_name: Optional[str], item_name: str) -> Optional[List[ET.Element]]: + """Retrieves a list of XML elements based on the specified wrapping element and item name. + + Args: + root (ET.Element): The root XML element. + wrapping_element_name (Optional[str]): The name of the wrapping element. + item_name (str): The name of the desired elements. + + Returns: + Optional[List[ET.Element]]: A list of matching elements, or None if not found. + """ + if root is None: + return None + + if wrapping_element_name is None: + return root.findall(item_name) + wrapping_element = root.find(wrapping_element_name) + if wrapping_element is None: + return None + return wrapping_element.findall(item_name) diff --git a/mypy.ini b/mypy.ini index 68d6768..d2cef49 100644 --- a/mypy.ini +++ b/mypy.ini @@ -5,4 +5,7 @@ check_untyped_defs = True ignore_missing_imports = True [mypy-jsonpointer.*] +ignore_missing_imports = True + +[mypy-testfixtures.*] ignore_missing_imports = True \ No newline at end of file diff --git a/mypy.ini~ b/mypy.ini~ new file mode 100644 index 0000000..7567bb3 --- /dev/null +++ b/mypy.ini~ @@ -0,0 +1,11 @@ +[mypy] +check_untyped_defs = True + +[mypy-jsonpickle.*] +ignore_missing_imports = True + +[mypy-jsonpointer.*] +ignore_missing_imports = True + +[mypy-jsonpointer.*] +ignore_missing_imports = True \ No newline at end of file diff --git a/tests/apimatic_core/api_call_tests/test_api_call.py b/tests/apimatic_core/api_call_tests/test_api_call.py index 95a3e4d..629db3e 100644 --- a/tests/apimatic_core/api_call_tests/test_api_call.py +++ b/tests/apimatic_core/api_call_tests/test_api_call.py @@ -1,6 +1,9 @@ +from typing import Optional + import pytest from apimatic_core_interfaces.configuration.endpoint_configuration import EndpointConfiguration from apimatic_core_interfaces.http.http_client import HttpClient +from pydantic import validate_call from apimatic_core.api_call import ApiCall from apimatic_core.configurations.global_configuration import GlobalConfiguration @@ -12,17 +15,23 @@ from apimatic_core_interfaces.http.http_method_enum import HttpMethodEnum from tests.apimatic_core.base import Base from tests.apimatic_core.mocks.callables.base_uri_callable import Server +from tests.apimatic_core.mocks.http.http_client import MockHttpClient +from tests.apimatic_core.mocks.http.http_response_catcher import HttpResponseCatcher from tests.apimatic_core.mocks.models.person import Employee class TestApiCall(Base): + @validate_call def setup_test(self, global_config: GlobalConfiguration): self.global_config: GlobalConfiguration = global_config - self.http_response_catcher: HttpCallBack = self.global_config.http_client_configuration.http_callback - self.http_client: HttpClient = self.global_config.http_client_configuration.http_client + self.http_response_catcher: HttpCallBack = (self.global_config.http_client_configuration.http_callback or + HttpResponseCatcher()) + self.http_client: HttpClient = (self.global_config.http_client_configuration.http_client or + MockHttpClient()) self.api_call_builder: ApiCall = self.new_api_call_builder(self.global_config) + @validate_call def test_end_to_end_with_uninitialized_http_client(self): self.setup_test(self.default_global_configuration) with pytest.raises(ValueError) as exception: @@ -42,6 +51,7 @@ def test_end_to_end_with_uninitialized_http_client(self): ).execute() assert exception.value.args[0] == 'An HTTP client instance is required to execute an Api call.' + @validate_call def test_end_to_end_with_uninitialized_http_callback(self): self.setup_test(self.global_configuration_without_http_callback) actual_employee_model: Employee = self.api_call_builder.new_builder.request( @@ -59,11 +69,12 @@ def test_end_to_end_with_uninitialized_http_callback(self): .deserialize_into(Employee.model_validate) ).execute() - assert self.http_client.should_retry is False - assert self.http_client.contains_binary_response is False - assert self.http_response_catcher is None + assert self.http_client.should_retry is False # type: ignore[attr-defined] + assert self.http_client.contains_binary_response is False # type: ignore[attr-defined] + assert self.http_response_catcher.response is None # type: ignore[attr-defined] assert ApiHelper.json_serialize(Base.employee_model()) == ApiHelper.json_serialize(actual_employee_model) + @validate_call def test_end_to_end_with_not_implemented_http_callback(self): self.setup_test(self.global_configuration_unimplemented_http_callback) with pytest.raises(NotImplementedError) as not_implemented_exception: @@ -84,6 +95,7 @@ def test_end_to_end_with_not_implemented_http_callback(self): assert not_implemented_exception.value.args[0] == 'This method has not been implemented.' + @validate_call def test_end_to_end_without_endpoint_configurations(self): self.setup_test(self.global_configuration) actual_employee_model: Employee = self.api_call_builder.new_builder.request( @@ -101,9 +113,9 @@ def test_end_to_end_without_endpoint_configurations(self): .deserialize_into(Employee.model_validate) ).execute() - assert self.http_client.should_retry is False - assert self.http_client.contains_binary_response is False - assert self.http_response_catcher.response.status_code == 200 + assert self.http_client.should_retry is False # type: ignore[attr-defined] + assert self.http_client.contains_binary_response is False # type: ignore[attr-defined] + assert self.http_response_catcher.response.status_code == 200 # type: ignore[attr-defined] assert ApiHelper.json_serialize(Base.employee_model()) == ApiHelper.json_serialize(actual_employee_model) @pytest.mark.parametrize('input_to_retry, ' @@ -114,6 +126,7 @@ def test_end_to_end_without_endpoint_configurations(self): (False, False, False, False), (True, True, True, True) ]) + @validate_call def test_end_to_end_with_endpoint_configurations(self, input_to_retry, input_contains_binary_response, expected_to_retry, expected_contains_binary_response): self.setup_test(self.global_configuration) @@ -134,7 +147,7 @@ def test_end_to_end_with_endpoint_configurations(self, input_to_retry, input_con EndpointConfiguration(should_retry=input_to_retry, has_binary_response=input_contains_binary_response) ).execute() - assert self.http_client.should_retry == expected_to_retry - assert self.http_client.contains_binary_response == expected_contains_binary_response - assert self.http_response_catcher.response.status_code == 200 + assert self.http_client.should_retry == expected_to_retry # type: ignore[attr-defined] + assert self.http_client.contains_binary_response == expected_contains_binary_response # type: ignore[attr-defined] + assert self.http_response_catcher.response.status_code == 200 # type: ignore[attr-defined] assert ApiHelper.json_serialize(Base.employee_model()) == ApiHelper.json_serialize(actual_employee_model) diff --git a/tests/apimatic_core/api_call_tests/test_api_call.py~ b/tests/apimatic_core/api_call_tests/test_api_call.py~ new file mode 100644 index 0000000..bbabf32 --- /dev/null +++ b/tests/apimatic_core/api_call_tests/test_api_call.py~ @@ -0,0 +1,152 @@ +from typing import Optional + +import pytest +from apimatic_core_interfaces.configuration.endpoint_configuration import EndpointConfiguration +from apimatic_core_interfaces.http.http_client import HttpClient +from pydantic import validate_call + +from apimatic_core.api_call import ApiCall +from apimatic_core.configurations.global_configuration import GlobalConfiguration +from apimatic_core.http.http_callback import HttpCallBack +from apimatic_core.request_builder import RequestBuilder +from apimatic_core.response_handler import ResponseHandler +from apimatic_core.types.parameter import Parameter +from apimatic_core.utilities.api_helper import ApiHelper +from apimatic_core_interfaces.http.http_method_enum import HttpMethodEnum +from tests.apimatic_core.base import Base +from tests.apimatic_core.mocks.callables.base_uri_callable import Server +from tests.apimatic_core.mocks.http.http_client import MockHttpClient +from tests.apimatic_core.mocks.models.person import Employee + + +class TestApiCall(Base): + + @validate_call + def setup_test(self, global_config: GlobalConfiguration): + self.global_config: GlobalConfiguration = global_config + self.http_response_catcher: HttpCallBack = (self.global_config.http_client_configuration.http_callback or + HttpResponseCatcher()) + self.http_client: HttpClient = (self.global_config.http_client_configuration.http_client or + MockHttpClient()) + self.api_call_builder: ApiCall = self.new_api_call_builder(self.global_config) + + @validate_call + def test_end_to_end_with_uninitialized_http_client(self): + self.setup_test(self.default_global_configuration) + with pytest.raises(ValueError) as exception: + self.api_call_builder.new_builder.request( + RequestBuilder().server(Server.DEFAULT) + .path('/body/model') + .http_method(HttpMethodEnum.POST) + .header_param(Parameter(key='Content-Type', value='application/json')) + .body_param(Parameter(value=Base.employee_model(), is_required=True)) + .header_param(Parameter(key='accept', value='application/json')) + .body_serializer(ApiHelper.json_serialize) + ).response( + ResponseHandler() + .is_nullify404(True) + .deserializer(ApiHelper.json_deserialize) + .deserialize_into(Employee.model_validate) + ).execute() + assert exception.value.args[0] == 'An HTTP client instance is required to execute an Api call.' + + @validate_call + def test_end_to_end_with_uninitialized_http_callback(self): + self.setup_test(self.global_configuration_without_http_callback) + actual_employee_model: Employee = self.api_call_builder.new_builder.request( + RequestBuilder().server(Server.DEFAULT) + .path('/body/model') + .http_method(HttpMethodEnum.POST) + .header_param(Parameter(key='Content-Type', value='application/json')) + .body_param(Parameter(value=Base.employee_model(), is_required=True)) + .header_param(Parameter(key='accept', value='application/json')) + .body_serializer(ApiHelper.json_serialize) + ).response( + ResponseHandler() + .is_nullify404(True) + .deserializer(ApiHelper.json_deserialize) + .deserialize_into(Employee.model_validate) + ).execute() + + assert self.http_client.should_retry is False # type: ignore[attr-defined] + assert self.http_client.contains_binary_response is False # type: ignore[attr-defined] + assert self.http_response_catcher.response is None # type: ignore[attr-defined] + assert ApiHelper.json_serialize(Base.employee_model()) == ApiHelper.json_serialize(actual_employee_model) + + @validate_call + def test_end_to_end_with_not_implemented_http_callback(self): + self.setup_test(self.global_configuration_unimplemented_http_callback) + with pytest.raises(NotImplementedError) as not_implemented_exception: + self.api_call_builder.new_builder.request( + RequestBuilder().server(Server.DEFAULT) + .path('/body/model') + .http_method(HttpMethodEnum.POST) + .header_param(Parameter(key='Content-Type', value='application/json')) + .body_param(Parameter(value=Base.employee_model(), is_required=True)) + .header_param(Parameter(key='accept', value='application/json')) + .body_serializer(ApiHelper.json_serialize) + ).response( + ResponseHandler() + .is_nullify404(True) + .deserializer(ApiHelper.json_deserialize) + .deserialize_into(Employee.model_validate) + ).execute() + + assert not_implemented_exception.value.args[0] == 'This method has not been implemented.' + + @validate_call + def test_end_to_end_without_endpoint_configurations(self): + self.setup_test(self.global_configuration) + actual_employee_model: Employee = self.api_call_builder.new_builder.request( + RequestBuilder().server(Server.DEFAULT) + .path('/body/model') + .http_method(HttpMethodEnum.POST) + .header_param(Parameter(key='Content-Type', value='application/json')) + .body_param(Parameter(value=Base.employee_model(), is_required=True)) + .header_param(Parameter(key='accept', value='application/json')) + .body_serializer(ApiHelper.json_serialize) + ).response( + ResponseHandler() + .is_nullify404(True) + .deserializer(ApiHelper.json_deserialize) + .deserialize_into(Employee.model_validate) + ).execute() + + assert self.http_client.should_retry is False # type: ignore[attr-defined] + assert self.http_client.contains_binary_response is False # type: ignore[attr-defined] + assert self.http_response_catcher.response.status_code == 200 # type: ignore[attr-defined] + assert ApiHelper.json_serialize(Base.employee_model()) == ApiHelper.json_serialize(actual_employee_model) + + @pytest.mark.parametrize('input_to_retry, ' + 'input_contains_binary_response, ' + 'expected_to_retry, expected_contains_binary_response', [ + (True, False, True, False), + (False, True, False, True), + (False, False, False, False), + (True, True, True, True) + ]) + @validate_call + def test_end_to_end_with_endpoint_configurations(self, input_to_retry, input_contains_binary_response, + expected_to_retry, expected_contains_binary_response): + self.setup_test(self.global_configuration) + actual_employee_model: Employee = self.api_call_builder.new_builder.request( + RequestBuilder().server(Server.DEFAULT) + .path('/body/model') + .http_method(HttpMethodEnum.POST) + .header_param(Parameter(key='Content-Type', value='application/json')) + .body_param(Parameter(value=Base.employee_model(), is_required=True)) + .header_param(Parameter(key='accept', value='application/json')) + .body_serializer(ApiHelper.json_serialize) + ).response( + ResponseHandler() + .is_nullify404(True) + .deserializer(ApiHelper.json_deserialize) + .deserialize_into(Employee.model_validate) + ).endpoint_configuration( + EndpointConfiguration(should_retry=input_to_retry, has_binary_response=input_contains_binary_response) + ).execute() + + assert self.http_client.should_retry == expected_to_retry # type: ignore[attr-defined] + assert self.http_client.contains_binary_response == expected_contains_binary_response # type: ignore[attr-defined] + assert self.http_response_catcher.response.status_code == 200 # type: ignore[attr-defined] + assert ApiHelper.json_serialize(Base.employee_model()) == ApiHelper.json_serialize(actual_employee_model) diff --git a/tests/apimatic_core/api_logger_tests/test_api_logger.py b/tests/apimatic_core/api_logger_tests/test_api_logger.py index 0f1257a..1c39ae3 100644 --- a/tests/apimatic_core/api_logger_tests/test_api_logger.py +++ b/tests/apimatic_core/api_logger_tests/test_api_logger.py @@ -1,6 +1,6 @@ import logging from logging import LogRecord -from typing import Any, Iterable, Callable, List +from typing import List, Optional import pytest @@ -9,6 +9,7 @@ from apimatic_core_interfaces.http.http_response import HttpResponse from apimatic_core_interfaces.logger.api_logger import ApiLogger from apimatic_core_interfaces.logger.logger import Logger +from pydantic import validate_call from apimatic_core.logger.configuration.api_logging_configuration import ApiRequestLoggingConfiguration, \ ApiResponseLoggingConfiguration @@ -27,16 +28,18 @@ class TestApiLogger(Base): + @validate_call @pytest.fixture def log_capture(self) -> LogCapture: """Fixture to capture logs during the test.""" with LogCapture() as capture: yield capture + @validate_call(config=dict(arbitrary_types_allowed=True)) def init_sdk_logger( self, logger: Logger, log_level: int=logging.INFO, mask_sensitive_headers: bool=True, - request_logging_configuration: ApiRequestLoggingConfiguration=None, - response_logging_configuration: ApiResponseLoggingConfiguration=None): + request_logging_configuration: Optional[ApiRequestLoggingConfiguration]=None, + response_logging_configuration: Optional[ApiResponseLoggingConfiguration]=None): self._api_request: HttpRequest = self.request( http_method=HttpMethodEnum.POST, query_url='http://localhost:3000/body/model?key=value', @@ -55,57 +58,65 @@ def init_sdk_logger( self._api_logger: ApiLogger = self.get_api_logger( logger, log_level, mask_sensitive_headers, request_logging_configuration, response_logging_configuration) + @validate_call(config=dict(arbitrary_types_allowed=True)) def get_api_logger( self, logger: Logger, log_level: int=logging.INFO, mask_sensitive_headers: bool=True, - request_logging_configuration: ApiRequestLoggingConfiguration=None, - response_logging_configuration: ApiResponseLoggingConfiguration=None + request_logging_configuration: Optional[ApiRequestLoggingConfiguration]=None, + response_logging_configuration: Optional[ApiResponseLoggingConfiguration]=None ) -> ApiLogger: return SdkLogger(self.api_logging_configuration( logger, log_level=log_level, mask_sensitive_headers=mask_sensitive_headers, request_logging_configuration=request_logging_configuration, response_logging_configuration=response_logging_configuration)) + @validate_call(config=dict(arbitrary_types_allowed=True)) def test_custom_logger_for_request(self, log_capture: LogCapture): self.init_sdk_logger(logger=MockedApiLogger()) self._api_logger.log_request(self._api_request) # Assert the captured logs - records = log_capture.records - assert self.any_with_limit( - records, - condition=lambda record: record.msg == 'Request %s %s %s' and - record.args == ('POST', 'http://localhost:3000/body/model', 'application/json') and - record.message == 'Request POST http://localhost:3000/body/model application/json', - max_checks=1) + records: List[LogRecord] = log_capture.records + + assert any( + record.msg == 'Request %s %s %s' and + record.args == ('POST', 'http://localhost:3000/body/model', 'application/json') and + record.message == 'Request POST http://localhost:3000/body/model application/json' + for record in records) + assert all(record.levelname == "INFO" for record in records) + @validate_call(config=dict(arbitrary_types_allowed=True)) def test_custom_logger_for_request_with_log_level(self, log_capture: LogCapture): self.init_sdk_logger(logger=MockedApiLogger(), log_level=logging.WARN) self._api_logger.log_request(self._api_request) # Assert the captured logs - records = log_capture.records - assert self.any_with_limit( - records, - condition=lambda record: record.msg == 'Request %s %s %s' and - record.args == ('POST', 'http://localhost:3000/body/model', 'application/json') and - record.message == 'Request POST http://localhost:3000/body/model application/json', - max_checks=1) + records: List[LogRecord] = log_capture.records + + assert any( + record.msg == 'Request %s %s %s' and + record.args == ('POST', 'http://localhost:3000/body/model', 'application/json') and + record.message == 'Request POST http://localhost:3000/body/model application/json' + for record in records) + assert all(record.levelname == "WARNING" for record in records) + @validate_call(config=dict(arbitrary_types_allowed=True)) def test_custom_logger_for_request_with_include_query_in_path(self, log_capture: LogCapture): self.init_sdk_logger(logger=MockedApiLogger(), request_logging_configuration=self.api_request_logging_configuration( include_query_in_path=True)) self._api_logger.log_request(self._api_request) # Assert the captured logs - records = log_capture.records - assert self.any_with_limit( - records, - condition=lambda record: record.msg == 'Request %s %s %s' and - record.args == ('POST', 'http://localhost:3000/body/model?key=value', 'application/json') and - record.message == 'Request POST http://localhost:3000/body/model?key=value application/json', - max_checks=1) + records: List[LogRecord] = log_capture.records + + assert any( + record.msg == 'Request %s %s %s' and + record.args == ('POST', 'http://localhost:3000/body/model?key=value', 'application/json') and + record.message == 'Request POST http://localhost:3000/body/model?key=value application/json' + for record in records) + assert all(record.levelname == "INFO" for record in records) + @validate_call(config=dict(arbitrary_types_allowed=True)) def test_custom_logger_for_request_with_log_request_header(self, log_capture: LogCapture): self.init_sdk_logger( logger=MockedApiLogger(), @@ -113,68 +124,77 @@ def test_custom_logger_for_request_with_log_request_header(self, log_capture: Lo self._api_logger.log_request(self._api_request) # Assert the captured logs - records = log_capture.records - assert self.any_with_limit( - records, - condition=lambda record: record.msg == 'Request %s %s %s' and - record.args == ('POST', 'http://localhost:3000/body/model', 'application/json') and - record.message == 'Request POST http://localhost:3000/body/model application/json', - max_checks=1) - assert self.any_with_limit( - records, - condition=lambda record: record.msg == 'Request Headers %s' and - record.args == {'Content-Type': 'application/json', 'Accept': 'application/json'} and - record.message == "Request Headers {'Content-Type': 'application/json', 'Accept': 'application/json'}", - max_checks=1) + records: List[LogRecord] = log_capture.records + + assert any( + record.msg == 'Request %s %s %s' and + record.args == ('POST', 'http://localhost:3000/body/model', 'application/json') and + record.message == 'Request POST http://localhost:3000/body/model application/json' + for record in records) + + assert any( + record.msg == 'Request Headers %s' and + record.args == {'Content-Type': 'application/json', 'Accept': 'application/json'} and + record.message == "Request Headers {'Content-Type': 'application/json', 'Accept': 'application/json'}" + for record in records) + assert all(record.levelname == "INFO" for record in records) + @validate_call(config=dict(arbitrary_types_allowed=True)) def test_custom_logger_for_request_with_log_request_body(self, log_capture: LogCapture): self.init_sdk_logger(logger=MockedApiLogger(), request_logging_configuration=self.api_request_logging_configuration(log_body=True)) self._api_logger.log_request(self._api_request) # Assert the captured logs - records = log_capture.records - assert self.any_with_limit( - records, - condition=lambda record: record.msg == 'Request %s %s %s' and - record.args == ('POST', 'http://localhost:3000/body/model', 'application/json') and - record.message == 'Request POST http://localhost:3000/body/model application/json', - max_checks=1) - assert self.any_with_limit( - records, - condition=lambda record: record.msg == 'Request Body %s' and - record.args == ('{"Key": "Value"}',) and - record.message == 'Request Body {"Key": "Value"}', - max_checks=1) + records: List[LogRecord] = log_capture.records + + assert any( + record.msg == 'Request %s %s %s' and + record.args == ('POST', 'http://localhost:3000/body/model', 'application/json') and + record.message == 'Request POST http://localhost:3000/body/model application/json' + for record in records) + + assert any( + record.msg == 'Request Body %s' and + record.args == ('{"Key": "Value"}',) and + record.message == 'Request Body {"Key": "Value"}' + for record in records) + assert all(record.levelname == "INFO" for record in records) + @validate_call(config=dict(arbitrary_types_allowed=True)) def test_custom_logger_for_response(self, log_capture: LogCapture): self.init_sdk_logger(logger=MockedApiLogger()) self._api_logger.log_response(self._api_response) # Assert the captured logs - records = log_capture.records - assert self.any_with_limit( - records, - condition=lambda record: record.msg == 'Response %s %s %s' and - record.args == (200, 'application/json', 50) and - record.message == 'Response 200 application/json 50', - max_checks=1) + records: List[LogRecord] = log_capture.records + + assert any( + record.msg == 'Response %s %s %s' and + record.args == (200, 'application/json', 50) and + record.message == 'Response 200 application/json 50' + for record in records) + assert all(record.levelname == "INFO" for record in records) + @validate_call(config=dict(arbitrary_types_allowed=True)) def test_custom_logger_for_response_with_log_level(self, log_capture: LogCapture): self.init_sdk_logger(logger=MockedApiLogger(), log_level=logging.WARN) self._api_logger.log_response(self._api_response) + # Assert the captured logs - records = log_capture.records - assert self.any_with_limit( - records, - condition=lambda record: record.msg == 'Response %s %s %s' and - record.args == (200, 'application/json', 50) and - record.message == 'Response 200 application/json 50', - max_checks=1) + records: List[LogRecord] = log_capture.records + + assert any( + record.msg == 'Response %s %s %s' and + record.args == (200, 'application/json', 50) and + record.message == 'Response 200 application/json 50' + for record in records) + assert all(record.levelname == "WARNING" for record in records) + @validate_call(config=dict(arbitrary_types_allowed=True)) def test_custom_logger_for_response_with_log_response_header(self, log_capture: LogCapture): self.init_sdk_logger( logger=MockedApiLogger(), @@ -182,61 +202,67 @@ def test_custom_logger_for_response_with_log_response_header(self, log_capture: self._api_logger.log_response(self._api_response) # Assert the captured logs - records = log_capture.records - assert self.any_with_limit( - records, - condition=lambda record: record.msg == 'Response %s %s %s' and - record.args == (200, 'application/json', 50) and - record.message == 'Response 200 application/json 50', - max_checks=1) - assert self.any_with_limit( - records, - condition=lambda record: record.msg == 'Response Headers %s' and - record.args == {'Content-Type': 'application/json', 'Content-Length': 50} and - record.message == "Response Headers {'Content-Type': 'application/json', 'Content-Length': 50}", - max_checks=1) + records: List[LogRecord] = log_capture.records + + assert any( + record.msg == 'Response %s %s %s' and + record.args == (200, 'application/json', 50) and + record.message == 'Response 200 application/json 50' + for record in records) + + assert any( + record.msg == 'Response Headers %s' and + record.args == {'Content-Type': 'application/json', 'Content-Length': 50} and + record.message == "Response Headers {'Content-Type': 'application/json', 'Content-Length': 50}" + for record in records) + assert all(record.levelname == "INFO" for record in records) + @validate_call(config=dict(arbitrary_types_allowed=True)) def test_custom_logger_for_response_with_log_response_body(self, log_capture: LogCapture): self.init_sdk_logger(logger=MockedApiLogger(), response_logging_configuration=self.api_response_logging_configuration(log_body=True)) self._api_logger.log_response(self._api_response) # Assert the captured logs - records = log_capture.records - assert self.any_with_limit( - records, - condition=lambda record: record.msg == 'Response %s %s %s' and - record.args == (200, 'application/json', 50) and - record.message == 'Response 200 application/json 50', - max_checks=1) - assert self.any_with_limit( - records, - condition=lambda record: record.msg == 'Response Body %s' and - record.args == ('{"Key": "Value"}',) and - record.message == 'Response Body {"Key": "Value"}', - max_checks=1) + records: List[LogRecord] = log_capture.records + + assert any( + record.msg == 'Response %s %s %s' and + record.args == (200, 'application/json', 50) and + record.message == 'Response 200 application/json 50' + for record in records) + + assert any( + record.msg == 'Response Body %s' and + record.args == ('{"Key": "Value"}',) and + record.message == 'Response Body {"Key": "Value"}' + for record in records) + assert all(record.levelname == "INFO" for record in records) + @validate_call(config=dict(arbitrary_types_allowed=True)) def test_custom_logger_for_end_to_end_api_call_test(self, log_capture: LogCapture): self.execute_api_call_with_logging(logger=MockedApiLogger()) # Assert the captured logs - records = log_capture.records - assert self.any_with_limit( - records, - condition=lambda record: record.msg == 'Request %s %s %s' and - record.args == ('POST', 'http://localhost:3000/body/model', 'application/json') and - record.message == 'Request POST http://localhost:3000/body/model application/json', - max_checks=1) - assert self.any_with_limit( - records, - condition=lambda record: record.msg == 'Response %s %s %s' and - record.args == (200, 'application/json', None) and - record.message == 'Response 200 application/json None', - max_checks=1) + records: List[LogRecord] = log_capture.records + + assert any( + record.msg == 'Request %s %s %s' and + record.args == ('POST', 'http://localhost:3000/body/model', 'application/json') and + record.message == 'Request POST http://localhost:3000/body/model application/json' + for record in records) + + assert any( + record.msg == 'Response %s %s %s' and + record.args == (200, 'application/json', None) and + record.message == 'Response 200 application/json None' + for record in records) + assert all(record.levelname == "INFO" for record in records) + @validate_call(config=dict(arbitrary_types_allowed=True)) def execute_api_call_with_logging(self, logger: Logger): _api_call_builder = self.new_api_call_builder(self.global_configuration_with_logging(logger)) _api_call_builder.new_builder \ @@ -253,17 +279,3 @@ def execute_api_call_with_logging(self, logger: Logger): .deserializer(ApiHelper.json_deserialize) .deserialize_into(Employee.model_validate)) \ .execute() - - @staticmethod - def any_with_limit(iterable: Iterable, condition: Callable[[List[LogRecord]], bool], max_checks: int): - """Checks if any element in iterable meets the condition, with a limit on checks. - - Args: - iterable: The iterable to check (list, string, etc.) - condition: A function that takes an element and returns True if it meets the criteria. - max_checks: The maximum number of elements to check before returning False (defaults to infinite). - - Returns: - True if any element meets the condition within the check limit, False otherwise. - """ - return sum(1 for element in iterable if condition(element)) == max_checks diff --git a/tests/apimatic_core/api_logger_tests/test_logging_configuration.py b/tests/apimatic_core/api_logger_tests/test_logging_configuration.py index 3e5eac8..5acd5bb 100644 --- a/tests/apimatic_core/api_logger_tests/test_logging_configuration.py +++ b/tests/apimatic_core/api_logger_tests/test_logging_configuration.py @@ -1,11 +1,15 @@ import pytest -from typing import Dict, List, Any +from typing import Dict, List, Any, Optional + +from pydantic import validate_call from apimatic_core.logger.configuration.api_logging_configuration import ApiRequestLoggingConfiguration class TestLogHelper: + @staticmethod + @validate_call def create_sample_headers() -> Dict[str, str]: return { "Accept": "application/json", @@ -15,166 +19,205 @@ def create_sample_headers() -> Dict[str, str]: } @staticmethod + @validate_call def create_sample_request_logging_configuration( - headers_to_include: List[str]=None, headers_to_exclude: List[str]=None, headers_to_unmask: List[str]=None + headers_to_include: Optional[List[str]]=None, + headers_to_exclude: Optional[List[str]]=None, + headers_to_unmask: Optional[List[str]]=None ) -> ApiRequestLoggingConfiguration: return ApiRequestLoggingConfiguration(log_body=False, log_headers=False, include_query_in_path=False, headers_to_include=headers_to_include or [], headers_to_exclude=headers_to_exclude or [], headers_to_unmask=headers_to_unmask or []) + @validate_call def test_get_headers_to_log_include(self): headers: Dict[str, str] = self.create_sample_headers() logging_config: ApiRequestLoggingConfiguration = self.create_sample_request_logging_configuration( headers_to_include=["Authorization"]) result: Dict[str, Any] = logging_config.get_loggable_headers(headers, False) + assert "Authorization" in result.keys() assert "Content-Type" not in result.keys() assert "User-Agent" not in result.keys() assert "Accept" not in result.keys() + @validate_call def test_get_headers_to_log_exclude(self): headers: Dict[str, str] = self.create_sample_headers() logging_config: ApiRequestLoggingConfiguration = self.create_sample_request_logging_configuration( headers_to_exclude=["Authorization"]) result: Dict[str, Any] = logging_config.get_loggable_headers(headers, False) + assert "Authorization" not in result.keys() assert "Content-Type" in result.keys() assert "User-Agent" in result.keys() assert "Accept" in result.keys() + @validate_call def test_get_headers_to_log_include_and_exclude(self): headers: Dict[str, str] = self.create_sample_headers() logging_config: ApiRequestLoggingConfiguration = self.create_sample_request_logging_configuration( headers_to_include=["Authorization"], headers_to_exclude=["Accept"]) result: Dict[str, Any] = logging_config.get_loggable_headers(headers, False) + assert "Authorization" in result.keys() assert "Content-Type" not in result.keys() assert "User-Agent" not in result.keys() assert "Accept" not in result.keys() + @validate_call def test_get_headers_to_log_sensitive_masking_enabled(self): headers: Dict[str, str] = self.create_sample_headers() logging_config: ApiRequestLoggingConfiguration = self.create_sample_request_logging_configuration() result: Dict[str, Any] = logging_config.get_loggable_headers(headers, True) + assert "Authorization" in result.keys() assert "**Redacted**" == result.get("Authorization") + @validate_call def test_get_headers_to_log_sensitive_masking_disabled(self): headers: Dict[str, str] = self.create_sample_headers() logging_config: ApiRequestLoggingConfiguration = self.create_sample_request_logging_configuration() result: Dict[str, Any] = logging_config.get_loggable_headers(headers, False) + assert "Authorization" in result.keys() assert "Bearer token" == result.get("Authorization") + @validate_call def test_get_headers_to_log_sensitive_masking_disabled_with_headers_to_unmask(self): headers: Dict[str, str] = self.create_sample_headers() logging_config: ApiRequestLoggingConfiguration = self.create_sample_request_logging_configuration( headers_to_unmask=["Authorization"]) result: Dict[str, Any] = logging_config.get_loggable_headers(headers, False) + assert "Authorization" in result.keys() assert "Bearer token" == result.get("Authorization") + @validate_call def test_get_headers_to_log_sensitive_masking_enabled_with_headers_to_unmask(self): headers: Dict[str, str] = self.create_sample_headers() logging_config: ApiRequestLoggingConfiguration = self.create_sample_request_logging_configuration( headers_to_unmask=["Authorization"]) result: Dict[str, Any] = logging_config.get_loggable_headers(headers, True) + assert "Authorization" in result.keys() assert "Bearer token" == result.get("Authorization") + @validate_call def test_extract_headers_to_log_include(self): headers: Dict[str, str] = self.create_sample_headers() logging_config: ApiRequestLoggingConfiguration = self.create_sample_request_logging_configuration( headers_to_include=["Authorization"]) result: Dict[str, Any] = logging_config._extract_headers_to_log(headers) + assert "Authorization" in result.keys() assert "Content-Type" not in result.keys() assert "User-Agent" not in result.keys() assert "Accept" not in result.keys() + @validate_call def test_extract_headers_to_log_exclude(self): headers: Dict[str, str] = self.create_sample_headers() logging_config: ApiRequestLoggingConfiguration = self.create_sample_request_logging_configuration( headers_to_exclude=["Authorization"]) result: Dict[str, Any] = logging_config._extract_headers_to_log(headers) + assert "Authorization" not in result.keys() assert "Content-Type" in result.keys() assert "User-Agent" in result.keys() assert "Accept" in result.keys() + @validate_call def test_extract_headers_to_log_no_criteria(self): headers: Dict[str, str] = self.create_sample_headers() logging_config: ApiRequestLoggingConfiguration = self.create_sample_request_logging_configuration() result: Dict[str, Any] = logging_config._extract_headers_to_log(headers) + assert "Authorization" in result.keys() assert "Content-Type" in result.keys() assert "User-Agent" in result.keys() assert "Accept" in result.keys() + @validate_call def test_mask_sensitive_headers_masking_enabled(self): headers: Dict[str, str] = self.create_sample_headers() logging_config: ApiRequestLoggingConfiguration = self.create_sample_request_logging_configuration() result: Dict[str, Any] = logging_config._mask_sensitive_headers(headers, True) + assert "Authorization" in result.keys() assert "**Redacted**" == result.get("Authorization") + @validate_call def test_mask_sensitive_headers_masking_enabled_with_headers_to_unmask(self): headers: Dict[str, str] = self.create_sample_headers() logging_config: ApiRequestLoggingConfiguration = self.create_sample_request_logging_configuration( headers_to_unmask=["Authorization"]) result: Dict[str, Any] = logging_config._mask_sensitive_headers(headers, True) + assert "Authorization" in result.keys() assert "Bearer token" == result.get("Authorization") + @validate_call def test_mask_sensitive_headers_masking_disable(self): headers: Dict[str, str] = self.create_sample_headers() logging_config: ApiRequestLoggingConfiguration = self.create_sample_request_logging_configuration() result: Dict[str, Any] = logging_config._mask_sensitive_headers(headers, False) + assert "Authorization" in result.keys() assert "Bearer token" == result.get("Authorization") + @validate_call def test_mask_sensitive_headers_masking_disabled_with_headers_to_unmask(self): headers: Dict[str, str] = self.create_sample_headers() logging_config: ApiRequestLoggingConfiguration = self.create_sample_request_logging_configuration( headers_to_unmask=["Authorization"]) result: Dict[str, Any] = logging_config._mask_sensitive_headers(headers, False) + assert "Authorization" in result.keys() assert "Bearer token" == result.get("Authorization") + @validate_call def test_filter_included_headers(self): headers: Dict[str, str] = self.create_sample_headers() logging_config: ApiRequestLoggingConfiguration = self.create_sample_request_logging_configuration( headers_to_include=["Authorization"]) result: Dict[str, Any] = logging_config._filter_included_headers(headers) + assert "Authorization" in result.keys() assert "Content-Type" not in result.keys() assert "User-Agent" not in result.keys() assert "Accept" not in result.keys() + @validate_call def test_filter_included_headers_no_inclusion(self): headers: Dict[str, str] = self.create_sample_headers() logging_config: ApiRequestLoggingConfiguration = self.create_sample_request_logging_configuration() result: Dict[str, Any] = logging_config._filter_included_headers(headers) + assert "Authorization" not in result.keys() assert "Content-Type" not in result.keys() assert "User-Agent" not in result.keys() assert "Accept" not in result.keys() + @validate_call def test_filter_excluded_headers(self): headers: Dict[str, str] = self.create_sample_headers() logging_config: ApiRequestLoggingConfiguration = self.create_sample_request_logging_configuration( headers_to_exclude=["Authorization"]) result: Dict[str, Any] = logging_config._filter_excluded_headers(headers) + assert "Authorization" not in result.keys() assert "Content-Type" in result.keys() assert "User-Agent" in result.keys() assert "Accept" in result.keys() + @validate_call def test_filter_excluded_headers_no_exclusion(self): headers: Dict[str, str] = self.create_sample_headers() logging_config: ApiRequestLoggingConfiguration = self.create_sample_request_logging_configuration() result: Dict[str, Any] = logging_config._filter_excluded_headers(headers) + assert "Authorization" in result.keys() assert "Content-Type" in result.keys() assert "User-Agent" in result.keys() diff --git a/tests/apimatic_core/api_logger_tests/test_logging_configuration.py~ b/tests/apimatic_core/api_logger_tests/test_logging_configuration.py~ new file mode 100644 index 0000000..b5f383c --- /dev/null +++ b/tests/apimatic_core/api_logger_tests/test_logging_configuration.py~ @@ -0,0 +1,223 @@ +import pytest +from typing import Dict, List, Any, Optional + +from pydantic import validate_call + +from apimatic_core.logger.configuration.api_logging_configuration import ApiRequestLoggingConfiguration + + +class TestLogHelper: + + @staticmethod + @validate_call + def create_sample_headers() -> Dict[str, str]: + return { + "Accept": "application/json", + "Authorization": "Bearer token", + "Content-Type": "application/json", + "User-Agent": "Mozilla/5.0" + } + + @staticmethod + @validate_call + def create_sample_request_logging_configuration( + headers_to_include: Optional[List[str]]=None, + headers_to_exclude: Optional[List[str]]=None, + headers_to_unmask: Optional[List[str]]=None + ) -> ApiRequestLoggingConfiguration: + return ApiRequestLoggingConfiguration(log_body=False, log_headers=False, include_query_in_path=False, + headers_to_include=headers_to_include or [], + headers_to_exclude=headers_to_exclude or [], + headers_to_unmask=headers_to_unmask or []) + + @validate_call + def test_get_headers_to_log_include(self): + headers: Dict[str, str] = self.create_sample_headers() + logging_config: ApiRequestLoggingConfiguration = self.create_sample_request_logging_configuration( + headers_to_include=["Authorization"]) + result: Dict[str, Any] = logging_config.get_loggable_headers(headers, False) + assert "Authorization" in result.keys() + assert "Content-Type" not in result.keys() + assert "User-Agent" not in result.keys() + assert "Accept" not in result.keys() + + @validate_call + def test_get_headers_to_log_exclude(self): + headers: Dict[str, str] = self.create_sample_headers() + logging_config: ApiRequestLoggingConfiguration = self.create_sample_request_logging_configuration( + headers_to_exclude=["Authorization"]) + result: Dict[str, Any] = logging_config.get_loggable_headers(headers, False) + + assert "Authorization" not in result.keys() + assert "Content-Type" in result.keys() + assert "User-Agent" in result.keys() + assert "Accept" in result.keys() + + @validate_call + def test_get_headers_to_log_include_and_exclude(self): + headers: Dict[str, str] = self.create_sample_headers() + logging_config: ApiRequestLoggingConfiguration = self.create_sample_request_logging_configuration( + headers_to_include=["Authorization"], headers_to_exclude=["Accept"]) + result: Dict[str, Any] = logging_config.get_loggable_headers(headers, False) + + assert "Authorization" in result.keys() + assert "Content-Type" not in result.keys() + assert "User-Agent" not in result.keys() + assert "Accept" not in result.keys() + + @validate_call + def test_get_headers_to_log_sensitive_masking_enabled(self): + headers: Dict[str, str] = self.create_sample_headers() + logging_config: ApiRequestLoggingConfiguration = self.create_sample_request_logging_configuration() + result: Dict[str, Any] = logging_config.get_loggable_headers(headers, True) + + assert "Authorization" in result.keys() + assert "**Redacted**" == result.get("Authorization") + + @validate_call + def test_get_headers_to_log_sensitive_masking_disabled(self): + headers: Dict[str, str] = self.create_sample_headers() + logging_config: ApiRequestLoggingConfiguration = self.create_sample_request_logging_configuration() + result: Dict[str, Any] = logging_config.get_loggable_headers(headers, False) + + assert "Authorization" in result.keys() + assert "Bearer token" == result.get("Authorization") + + @validate_call + def test_get_headers_to_log_sensitive_masking_disabled_with_headers_to_unmask(self): + headers: Dict[str, str] = self.create_sample_headers() + logging_config: ApiRequestLoggingConfiguration = self.create_sample_request_logging_configuration( + headers_to_unmask=["Authorization"]) + result: Dict[str, Any] = logging_config.get_loggable_headers(headers, False) + + assert "Authorization" in result.keys() + assert "Bearer token" == result.get("Authorization") + + @validate_call + def test_get_headers_to_log_sensitive_masking_enabled_with_headers_to_unmask(self): + headers: Dict[str, str] = self.create_sample_headers() + logging_config: ApiRequestLoggingConfiguration = self.create_sample_request_logging_configuration( + headers_to_unmask=["Authorization"]) + result: Dict[str, Any] = logging_config.get_loggable_headers(headers, True) + + assert "Authorization" in result.keys() + assert "Bearer token" == result.get("Authorization") + + @validate_call + def test_extract_headers_to_log_include(self): + headers: Dict[str, str] = self.create_sample_headers() + logging_config: ApiRequestLoggingConfiguration = self.create_sample_request_logging_configuration( + headers_to_include=["Authorization"]) + result: Dict[str, Any] = logging_config._extract_headers_to_log(headers) + + assert "Authorization" in result.keys() + assert "Content-Type" not in result.keys() + assert "User-Agent" not in result.keys() + assert "Accept" not in result.keys() + + @validate_call + def test_extract_headers_to_log_exclude(self): + headers: Dict[str, str] = self.create_sample_headers() + logging_config: ApiRequestLoggingConfiguration = self.create_sample_request_logging_configuration( + headers_to_exclude=["Authorization"]) + result: Dict[str, Any] = logging_config._extract_headers_to_log(headers) + + assert "Authorization" not in result.keys() + assert "Content-Type" in result.keys() + assert "User-Agent" in result.keys() + assert "Accept" in result.keys() + + @validate_call + def test_extract_headers_to_log_no_criteria(self): + headers: Dict[str, str] = self.create_sample_headers() + logging_config: ApiRequestLoggingConfiguration = self.create_sample_request_logging_configuration() + result: Dict[str, Any] = logging_config._extract_headers_to_log(headers) + + assert "Authorization" in result.keys() + assert "Content-Type" in result.keys() + assert "User-Agent" in result.keys() + assert "Accept" in result.keys() + + @validate_call + def test_mask_sensitive_headers_masking_enabled(self): + headers: Dict[str, str] = self.create_sample_headers() + logging_config: ApiRequestLoggingConfiguration = self.create_sample_request_logging_configuration() + result: Dict[str, Any] = logging_config._mask_sensitive_headers(headers, True) + + assert "Authorization" in result.keys() + assert "**Redacted**" == result.get("Authorization") + + @validate_call + def test_mask_sensitive_headers_masking_enabled_with_headers_to_unmask(self): + headers: Dict[str, str] = self.create_sample_headers() + logging_config: ApiRequestLoggingConfiguration = self.create_sample_request_logging_configuration( + headers_to_unmask=["Authorization"]) + result: Dict[str, Any] = logging_config._mask_sensitive_headers(headers, True) + + assert "Authorization" in result.keys() + assert "Bearer token" == result.get("Authorization") + + @validate_call + def test_mask_sensitive_headers_masking_disable(self): + headers: Dict[str, str] = self.create_sample_headers() + logging_config: ApiRequestLoggingConfiguration = self.create_sample_request_logging_configuration() + result: Dict[str, Any] = logging_config._mask_sensitive_headers(headers, False) + + assert "Authorization" in result.keys() + assert "Bearer token" == result.get("Authorization") + + @validate_call + def test_mask_sensitive_headers_masking_disabled_with_headers_to_unmask(self): + headers: Dict[str, str] = self.create_sample_headers() + logging_config: ApiRequestLoggingConfiguration = self.create_sample_request_logging_configuration( + headers_to_unmask=["Authorization"]) + result: Dict[str, Any] = logging_config._mask_sensitive_headers(headers, False) + + assert "Authorization" in result.keys() + assert "Bearer token" == result.get("Authorization") + + @validate_call + def test_filter_included_headers(self): + headers: Dict[str, str] = self.create_sample_headers() + logging_config: ApiRequestLoggingConfiguration = self.create_sample_request_logging_configuration( + headers_to_include=["Authorization"]) + result: Dict[str, Any] = logging_config._filter_included_headers(headers) + + assert "Authorization" in result.keys() + assert "Content-Type" not in result.keys() + assert "User-Agent" not in result.keys() + assert "Accept" not in result.keys() + + @validate_call + def test_filter_included_headers_no_inclusion(self): + headers: Dict[str, str] = self.create_sample_headers() + logging_config: ApiRequestLoggingConfiguration = self.create_sample_request_logging_configuration() + result: Dict[str, Any] = logging_config._filter_included_headers(headers) + + assert "Authorization" not in result.keys() + assert "Content-Type" not in result.keys() + assert "User-Agent" not in result.keys() + assert "Accept" not in result.keys() + + @validate_call + def test_filter_excluded_headers(self): + headers: Dict[str, str] = self.create_sample_headers() + logging_config: ApiRequestLoggingConfiguration = self.create_sample_request_logging_configuration( + headers_to_exclude=["Authorization"]) + result: Dict[str, Any] = logging_config._filter_excluded_headers(headers) + + assert "Authorization" not in result.keys() + assert "Content-Type" in result.keys() + assert "User-Agent" in result.keys() + assert "Accept" in result.keys() + + @validate_call + def test_filter_excluded_headers_no_exclusion(self): + headers: Dict[str, str] = self.create_sample_headers() + logging_config: ApiRequestLoggingConfiguration = self.create_sample_request_logging_configuration() + result: Dict[str, Any] = logging_config._filter_excluded_headers(headers) + + assert "Authorization" in result.keys() + assert "Content-Type" in result.keys() + assert "User-Agent" in result.keys() + assert "Accept" in result.keys() diff --git a/tests/apimatic_core/base.py b/tests/apimatic_core/base.py index dae7bb7..3f23234 100644 --- a/tests/apimatic_core/base.py +++ b/tests/apimatic_core/base.py @@ -7,9 +7,10 @@ from apimatic_core_interfaces.http.http_client import HttpClient from apimatic_core_interfaces.http.http_request import HttpRequest from apimatic_core_interfaces.http.http_response import HttpResponse -from typing import Optional, Union, Dict, IO, Any, List +from typing import Optional, Union, Dict, IO, Any, List, BinaryIO from apimatic_core_interfaces.logger.logger import Logger +from pydantic import validate_call from apimatic_core.api_call import ApiCall from apimatic_core.http.configurations.http_client_configuration import HttpClientConfiguration @@ -48,6 +49,7 @@ class Base: @staticmethod + @validate_call def employee_model() -> Employee: return Employee( name='Bob', uid='1234567', address='street abc', department='IT', @@ -66,6 +68,7 @@ def employee_model() -> Employee: person_type="Empl") @staticmethod + @validate_call def employee_model_str(beautify_with_spaces: bool=True) -> Union[Employee, str]: if beautify_with_spaces: return ('{{"address": "street abc", "age": 27, "birthday": "1994-02-13", "birthtime": "{1}", "name": "Bob",' @@ -85,6 +88,7 @@ def employee_model_str(beautify_with_spaces: bool=True) -> Union[Employee, str]: Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15))) @staticmethod + @validate_call def employee_model_additional_dictionary() -> Employee: return Employee( name='Bob', @@ -124,7 +128,8 @@ def employee_model_additional_dictionary() -> Employee: person_type="Empl") @staticmethod - def get_employee_dictionary() -> Dict[str, Employee]: + @validate_call + def get_employee_dictionary() -> Dict[str, Any]: return { "address": "street abc", "age": 27, @@ -157,20 +162,21 @@ def get_employee_dictionary() -> Dict[str, Employee]: } @staticmethod + @validate_call def get_complex_type() -> ComplexType: inner_complex_type = InnerComplexType( boolean_type=True, long_type=100003, string_type='abc', precision_type=55.44, - string_list_type=['item1', 'item2'], additional_properties={'key0': 'abc', 'key1': 400}) + string_list_type=['item1', 'item2']) return ComplexType( inner_complex_type=inner_complex_type, inner_complex_list_type=[inner_complex_type, inner_complex_type], inner_complex_list_of_map_type=[{'key0': inner_complex_type, 'key1': inner_complex_type}], inner_complex_map_type={'key0': inner_complex_type, 'key1': inner_complex_type}, inner_complex_map_of_list_type={'key0': [inner_complex_type, inner_complex_type], - 'key2': [inner_complex_type, inner_complex_type]}, - additional_properties={'prop1': [1, 2, 3], 'prop2': {'key0': 'abc', 'key1': 'def'}}) + 'key2': [inner_complex_type, inner_complex_type]}) @staticmethod + @validate_call def get_union_type_scalar_model() -> UnionTypeScalarModel: return UnionTypeScalarModel( any_of_required=1.5, @@ -180,41 +186,46 @@ def get_union_type_scalar_model() -> UnionTypeScalarModel: ) @staticmethod + @validate_call def get_serialized_employee() -> str: employee_dictionary = Base.get_employee_dictionary() return json.dumps(employee_dictionary, separators=(',', ':')) @staticmethod + @validate_call def basic_auth() -> BasicAuth: return BasicAuth(BasicAuthCredentials(username='test_username', password='test_password')) @staticmethod + @validate_call def bearer_auth() -> BearerAuth: return BearerAuth(BearerAuthCredentials(access_token='0b79bab50daca910b000d4f1a2b675d604257e42')) @staticmethod + @validate_call def custom_header_auth() -> CustomHeaderAuthentication: return CustomHeaderAuthentication(CustomHeaderAuthenticationCredentials(token='Qaws2W233WedeRe4T56G6Vref2')) @staticmethod + @validate_call def custom_query_auth() -> CustomQueryAuthentication: return CustomQueryAuthentication( CustomQueryAuthenticationCredentials(api_key='W233WedeRe4T56G6Vref2', token='Qaws2W233WedeRe4T56G6Vref2')) @staticmethod + @validate_call def xml_model() -> XMLModel: - model = XMLModel(string_attr='String', number_attr=10000, boolean_attr=False, - string_element='Hey! I am being tested.', number_element=5000, - boolean_element=False, elements=['a', 'b', 'c']) return XMLModel(string_attr='String', number_attr=10000, boolean_attr=False, string_element='Hey! I am being tested.', number_element=5000, boolean_element=False, elements=['a', 'b', 'c']) @staticmethod + @validate_call def one_of_xml_dog_model() -> OneOfXML: return OneOfXML(value=DogModel(barks=True)) @staticmethod + @validate_call def one_of_xml_cat_model() -> OneOfXML: return OneOfXML(value=[ CatModel(meows=True), @@ -222,6 +233,7 @@ def one_of_xml_cat_model() -> OneOfXML: ]) @staticmethod + @validate_call def one_of_xml_wolf_model() -> OneOfXML: return OneOfXML(value=[ WolfModel(howls=True), @@ -229,12 +241,14 @@ def one_of_xml_wolf_model() -> OneOfXML: ]) @staticmethod - def read_file(file_name) -> IO: + @validate_call + def read_file(file_name) -> BinaryIO: real_path = os.path.realpath(os.path.join(os.path.dirname(__file__), '..')) file_path = os.path.join(real_path, 'apimatic_core', 'mocks/files', file_name) return open(file_path, "rb") @staticmethod + @validate_call def global_errors() -> Dict[str, ErrorCase]: return { '400': ErrorCase(message='400 Global', exception_type=GlobalTestException), @@ -244,6 +258,7 @@ def global_errors() -> Dict[str, ErrorCase]: } @staticmethod + @validate_call def global_errors_with_template_message() -> Dict[str, ErrorCase]: return { '400': ErrorCase( @@ -258,10 +273,11 @@ def global_errors_with_template_message() -> Dict[str, ErrorCase]: } @staticmethod + @validate_call def request(http_method: HttpMethodEnum=HttpMethodEnum.GET, query_url: str='http://localhost:3000/test', - headers: Dict[str, Any]=None, - query_parameters: Dict[str, Any]=None, + headers: Optional[Dict[str, Any]]=None, + query_parameters: Optional[Dict[str, Any]]=None, parameters: Any=None, files: Any=None) -> HttpRequest: if headers is None: @@ -277,8 +293,9 @@ def request(http_method: HttpMethodEnum=HttpMethodEnum.GET, files=files) @staticmethod + @validate_call def response( - status_code: int=200, reason_phrase: Optional[str]=None, headers: Dict[str, Any]=None, text: Any=None + status_code: int=200, reason_phrase: Optional[str]=None, headers: Optional[Dict[str, Any]]=None, text: Any=None ) -> HttpResponse: if headers is None: headers = {} @@ -288,14 +305,21 @@ def response( ) @staticmethod + @validate_call def get_http_datetime( datetime_value: datetime, should_return_string: bool=True ) -> Union[ApiHelper.CustomDate, str]: if should_return_string is True: - return ApiHelper.HttpDateTime.from_datetime(datetime_value) + return Base.get_http_datetime_str(datetime_value) return ApiHelper.HttpDateTime(datetime_value) @staticmethod + @validate_call + def get_http_datetime_str(datetime_value: datetime) -> str: + return ApiHelper.HttpDateTime.from_datetime(datetime_value) + + @staticmethod + @validate_call def get_rfc3339_datetime( datetime_value: datetime, should_return_string: bool=True ) -> Union[ApiHelper.CustomDate, str]: @@ -304,14 +328,22 @@ def get_rfc3339_datetime( return ApiHelper.RFC3339DateTime(datetime_value) @staticmethod + @validate_call + def get_rfc3339_datetime_str(datetime_value: datetime) -> str: + return ApiHelper.RFC3339DateTime.from_datetime(datetime_value) + + @staticmethod + @validate_call def new_api_call_builder(global_configuration: GlobalConfiguration) -> ApiCall: return ApiCall(global_configuration) @staticmethod + @validate_call def user_agent() -> str: return 'Python|31.8.0|{engine}|{engine-version}|{os-info}' @staticmethod + @validate_call def user_agent_parameters() -> Dict[str, Dict[str, Any]]: return { 'engine': {'value': platform.python_implementation(), 'encode': False}, @@ -320,6 +352,7 @@ def user_agent_parameters() -> Dict[str, Dict[str, Any]]: } @staticmethod + @validate_call def wrapped_parameters() -> Dict[str, Any]: return { 'bodyScalar': True, @@ -327,10 +360,12 @@ def wrapped_parameters() -> Dict[str, Any]: } @staticmethod + @validate_call def mocked_http_client() -> HttpClient: return MockHttpClient() @staticmethod + @validate_call(config=dict(arbitrary_types_allowed=True)) def http_client_configuration( http_callback: Optional[HttpCallBack]=HttpResponseCatcher(), logging_configuration: Optional[ApiLoggingConfiguration]=None @@ -341,15 +376,18 @@ def http_client_configuration( ) @property + @validate_call def new_request_builder(self) -> RequestBuilder: return RequestBuilder().path('/test') \ .server(Server.DEFAULT) @property + @validate_call def new_response_handler(self) -> ResponseHandler: return ResponseHandler() @property + @validate_call def global_configuration(self) -> GlobalConfiguration: return GlobalConfiguration( http_client_configuration=self.http_client_configuration(), @@ -357,28 +395,33 @@ def global_configuration(self) -> GlobalConfiguration: global_errors=self.global_errors()) @property + @validate_call def global_configuration_without_http_callback(self) -> GlobalConfiguration: return GlobalConfiguration( http_client_configuration=self.http_client_configuration(None), base_uri_executor=BaseUriCallable().get_base_uri) @property + @validate_call def global_configuration_unimplemented_http_callback(self) -> GlobalConfiguration: return GlobalConfiguration( http_client_configuration=self.http_client_configuration(HttpCallBack()), base_uri_executor=BaseUriCallable().get_base_uri) @property + @validate_call def default_global_configuration(self) -> GlobalConfiguration: return GlobalConfiguration() @property + @validate_call def global_configuration_with_useragent(self) -> GlobalConfiguration: global_configuration = self.global_configuration global_configuration.add_user_agent(self.user_agent(), self.user_agent_parameters()) return global_configuration @property + @validate_call def global_configuration_with_auth(self) -> GlobalConfiguration: global_configuration = self.global_configuration global_configuration.auth_managers = { @@ -390,26 +433,29 @@ def global_configuration_with_auth(self) -> GlobalConfiguration: return global_configuration @staticmethod + @validate_call def api_request_logging_configuration( - log_body: bool=False, log_headers: bool=False, headers_to_include: List[str]=None, - headers_to_exclude: List[str]=None, headers_to_unmask: List[str]=None, + log_body: bool=False, log_headers: bool=False, headers_to_include: Optional[List[str]]=None, + headers_to_exclude: Optional[List[str]]=None, headers_to_unmask: Optional[List[str]]=None, include_query_in_path: bool=False ) -> ApiRequestLoggingConfiguration: return ApiRequestLoggingConfiguration( - log_body=log_body, log_headers=log_headers, headers_to_include=headers_to_include, - headers_to_exclude=headers_to_exclude, headers_to_unmask=headers_to_unmask, + log_body=log_body, log_headers=log_headers, headers_to_include=headers_to_include or [], + headers_to_exclude=headers_to_exclude or [], headers_to_unmask=headers_to_unmask or [], include_query_in_path=include_query_in_path) @staticmethod + @validate_call def api_response_logging_configuration( - log_body: bool=False, log_headers: bool=False, headers_to_include: List[str]=None, - headers_to_exclude: List[str]=None, headers_to_unmask: List[str]=None + log_body: bool=False, log_headers: bool=False, headers_to_include: Optional[List[str]]=None, + headers_to_exclude: Optional[List[str]]=None, headers_to_unmask: Optional[List[str]]=None ) -> ApiResponseLoggingConfiguration: return ApiResponseLoggingConfiguration( - log_body=log_body, log_headers=log_headers, headers_to_include=headers_to_include, - headers_to_exclude=headers_to_exclude, headers_to_unmask=headers_to_unmask) + log_body=log_body, log_headers=log_headers, headers_to_include=headers_to_include or [], + headers_to_exclude=headers_to_exclude or [], headers_to_unmask=headers_to_unmask or []) @staticmethod + @validate_call(config=dict(arbitrary_types_allowed=True)) def api_logging_configuration( logger: Logger, log_level: int=logging.INFO, mask_sensitive_headers: bool=True, request_logging_configuration: Optional[ApiRequestLoggingConfiguration]=None, @@ -427,6 +473,7 @@ def api_logging_configuration( response_logging_config=response_logging_configuration) @staticmethod + @validate_call(config=dict(arbitrary_types_allowed=True)) def global_configuration_with_logging(logger: Logger) -> GlobalConfiguration: return GlobalConfiguration( http_client_configuration=Base.http_client_configuration( @@ -434,6 +481,7 @@ def global_configuration_with_logging(logger: Logger) -> GlobalConfiguration: base_uri_executor=BaseUriCallable().get_base_uri) @property + @validate_call def global_configuration_with_uninitialized_auth_params(self) -> GlobalConfiguration: global_configuration = self.global_configuration global_configuration.auth_managers = { @@ -443,6 +491,7 @@ def global_configuration_with_uninitialized_auth_params(self) -> GlobalConfigura return global_configuration @property + @validate_call def global_configuration_with_partially_initialized_auth_params(self) -> GlobalConfiguration: global_configuration = self.global_configuration global_configuration.auth_managers = { diff --git a/tests/apimatic_core/base.py~ b/tests/apimatic_core/base.py~ new file mode 100644 index 0000000..47b8b8d --- /dev/null +++ b/tests/apimatic_core/base.py~ @@ -0,0 +1,501 @@ +import json +import logging +import os +import platform +from datetime import datetime, date + +from apimatic_core_interfaces.http.http_client import HttpClient +from apimatic_core_interfaces.http.http_request import HttpRequest +from apimatic_core_interfaces.http.http_response import HttpResponse +from typing import Optional, Union, Dict, IO, Any, List, BinaryIO + +from apimatic_core_interfaces.logger.logger import Logger +from pydantic import validate_call + +from apimatic_core.api_call import ApiCall +from apimatic_core.http.configurations.http_client_configuration import HttpClientConfiguration +from apimatic_core.http.http_callback import HttpCallBack +from apimatic_core.logger.configuration.api_logging_configuration import ApiLoggingConfiguration, \ + ApiRequestLoggingConfiguration, ApiResponseLoggingConfiguration +from apimatic_core.utilities.api_helper import ApiHelper +from apimatic_core_interfaces.http.http_method_enum import HttpMethodEnum +from apimatic_core.configurations.global_configuration import GlobalConfiguration +from apimatic_core.request_builder import RequestBuilder +from apimatic_core.response_handler import ResponseHandler +from apimatic_core.types.error_case import ErrorCase, MessageType +from tests.apimatic_core.mocks.authentications.basic_auth import BasicAuth, BasicAuthCredentials +from tests.apimatic_core.mocks.authentications.bearer_auth import BearerAuth, BearerAuthCredentials +from tests.apimatic_core.mocks.authentications.custom_header_authentication import CustomHeaderAuthentication, \ + CustomHeaderAuthenticationCredentials +from tests.apimatic_core.mocks.authentications.custom_query_authentication import CustomQueryAuthentication, \ + CustomQueryAuthenticationCredentials +from tests.apimatic_core.mocks.exceptions.global_test_exception import GlobalTestException +from tests.apimatic_core.mocks.exceptions.nested_model_exception import NestedModelException +from tests.apimatic_core.mocks.http.http_response_catcher import HttpResponseCatcher +from tests.apimatic_core.mocks.http.http_client import MockHttpClient +from tests.apimatic_core.mocks.models.cat_model import CatModel +from tests.apimatic_core.mocks.models.complex_type import ComplexType +from tests.apimatic_core.mocks.models.dog_model import DogModel +from tests.apimatic_core.mocks.models.inner_complex_type import InnerComplexType +from tests.apimatic_core.mocks.models.one_of_xml import OneOfXML +from tests.apimatic_core.mocks.models.union_type_scalar_model import UnionTypeScalarModel +from tests.apimatic_core.mocks.models.wolf_model import WolfModel +from tests.apimatic_core.mocks.models.xml_model import XMLModel +from tests.apimatic_core.mocks.models.days import Days +from tests.apimatic_core.mocks.models.person import Employee, Person +from tests.apimatic_core.mocks.callables.base_uri_callable import Server, BaseUriCallable + + +class Base: + + @staticmethod + @validate_call + def employee_model() -> Employee: + return Employee( + name='Bob', uid='1234567', address='street abc', department='IT', + birthday=date(1994, 2, 13), + birthtime=datetime(1994, 2, 13, 5, 30, 15), age=27, + additional_properties={'key1': 'value1', 'key2': 'value2'}, + hired_at=datetime(1994, 2, 13, 5, 30, 15), joining_day=Days.MONDAY, + working_days=[Days.MONDAY, Days.TUESDAY], salary=30000, + dependents=[ + Person(name='John', uid='7654321', address='street abc', birthday=date(1994, 2, 13), + birthtime=datetime(1994, 2, 13, 5, 30, 15), age=12, + additional_properties={ + 'key1': 'value1', + 'key2': 'value2' + }, person_type="Per")], + person_type="Empl") + + @staticmethod + @validate_call + def employee_model_str(beautify_with_spaces: bool=True) -> Union[Employee, str]: + if beautify_with_spaces: + return ('{{"address": "street abc", "age": 27, "birthday": "1994-02-13", "birthtime": "{1}", "name": "Bob",' + ' "uid": "1234567", "personType": "Empl", "department": "IT", "dependents": [{{"address": "street abc",' + ' "age": 12, "birthday": "1994-02-13", "birthtime": "{1}", "name": "John", "uid": "7654321",' + ' "personType": "Per", "key1": "value1", "key2": "value2"}}], "hiredAt": "{0}", "joiningDay": "Monday",' + ' "salary": 30000, "workingDays": ["Monday", "Tuesday"], "key1": "value1", "key2": "value2"}}').format( + Base.get_http_datetime(datetime(1994, 2, 13, 5, 30, 15)), + Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15))) + + return ('{{"address":"street abc","age":27,"birthday":"1994-02-13","birthtime":"{1}","name":"Bob",' + '"uid":"1234567","personType":"Empl","department":"IT","dependents":[{{"address":"street abc",' + '"age":12,"birthday":"1994-02-13","birthtime":"{1}","name":"John","uid":"7654321",' + '"personType":"Per","key1":"value1","key2":"value2"}}],"hiredAt":"{0}","joiningDay":"Monday",' + '"salary":30000,"workingDays":["Monday","Tuesday"],"key1":"value1","key2":"value2"}}').format( + Base.get_http_datetime(datetime(1994, 2, 13, 5, 30, 15)), + Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15))) + + @staticmethod + @validate_call + def employee_model_additional_dictionary() -> Employee: + return Employee( + name='Bob', + uid='1234567', + address='street abc', + department='IT', + birthday=date(1994, 2, 13), + birthtime=datetime(1994, 2, 13, 5, 30, 15), + age=27, + additional_properties={ + 'key1': 'value1', + 'key2': 'value2' + }, + hired_at=datetime(1994, 2, 13, 5, 30, 15), + joining_day=Days.MONDAY, + working_days=[ + Days.MONDAY, + Days.TUESDAY + ], + salary=30000, + dependents=[ + Person( + name='John', + uid='7654321', + address='street abc', + birthday=date(1994, 2, 13), + birthtime=datetime(1994, 2, 13, 5, 30, 15), age=12, + additional_properties={ + 'key1': { + 'inner_key1': 'inner_val1', + 'inner_key2': 'inner_val2' + }, + 'key2': ['value2', 'value3'] + }, + person_type="Per") + ], + person_type="Empl") + + @staticmethod + @validate_call + def get_employee_dictionary() -> Dict[str, Any]: + return { + "address": "street abc", + "age": 27, + "birthday": "1994-02-13", + "birthtime": Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15)), + "name": "Bob", + "uid": '1234567', + "personType": "Empl", + "department": "IT", + "dependents": [ + { + "address": "street abc", + "age": 12, + "birthday": "1994-02-13", + "birthtime": Base.get_rfc3339_datetime( + datetime(1994, 2, 13, 5, 30, 15)), + "name": "John", + "uid": '7654321', + "personType": "Per", + "key1": "value1", + "key2": "value2" + } + ], + "hiredAt": Base.get_http_datetime(datetime(1994, 2, 13, 5, 30, 15)), + "joiningDay": "Monday", + "salary": 30000, + "workingDays": ["Monday", "Tuesday"], + "key1": "value1", + "key2": "value2" + } + + @staticmethod + @validate_call + def get_complex_type() -> ComplexType: + inner_complex_type = InnerComplexType( + boolean_type=True, long_type=100003, string_type='abc', precision_type=55.44, + string_list_type=['item1', 'item2']) + + return ComplexType( + inner_complex_type=inner_complex_type, inner_complex_list_type=[inner_complex_type, inner_complex_type], + inner_complex_list_of_map_type=[{'key0': inner_complex_type, 'key1': inner_complex_type}], + inner_complex_map_type={'key0': inner_complex_type, 'key1': inner_complex_type}, + inner_complex_map_of_list_type={'key0': [inner_complex_type, inner_complex_type], + 'key2': [inner_complex_type, inner_complex_type]}) + + @staticmethod + @validate_call + def get_union_type_scalar_model() -> UnionTypeScalarModel: + return UnionTypeScalarModel( + any_of_required=1.5, + one_of_req_nullable='abc', + one_of_optional=200, + any_of_opt_nullable=True + ) + + @staticmethod + @validate_call + def get_serialized_employee() -> str: + employee_dictionary = Base.get_employee_dictionary() + return json.dumps(employee_dictionary, separators=(',', ':')) + + @staticmethod + @validate_call + def basic_auth() -> BasicAuth: + return BasicAuth(BasicAuthCredentials(username='test_username', password='test_password')) + + @staticmethod + @validate_call + def bearer_auth() -> BearerAuth: + return BearerAuth(BearerAuthCredentials(access_token='0b79bab50daca910b000d4f1a2b675d604257e42')) + + @staticmethod + @validate_call + def custom_header_auth() -> CustomHeaderAuthentication: + return CustomHeaderAuthentication(CustomHeaderAuthenticationCredentials(token='Qaws2W233WedeRe4T56G6Vref2')) + + @staticmethod + @validate_call + def custom_query_auth() -> CustomQueryAuthentication: + return CustomQueryAuthentication( + CustomQueryAuthenticationCredentials(api_key='W233WedeRe4T56G6Vref2', token='Qaws2W233WedeRe4T56G6Vref2')) + + @staticmethod + @validate_call + def xml_model() -> XMLModel: + return XMLModel(string_attr='String', number_attr=10000, boolean_attr=False, + string_element='Hey! I am being tested.', number_element=5000, + boolean_element=False, elements=['a', 'b', 'c']) + + @staticmethod + @validate_call + def one_of_xml_dog_model() -> OneOfXML: + return OneOfXML(value=DogModel(barks=True)) + + @staticmethod + @validate_call + def one_of_xml_cat_model() -> OneOfXML: + return OneOfXML(value=[ + CatModel(meows=True), + CatModel(meows=False) + ]) + + @staticmethod + @validate_call + def one_of_xml_wolf_model() -> OneOfXML: + return OneOfXML(value=[ + WolfModel(howls=True), + WolfModel(howls=False) + ]) + + @staticmethod + @validate_call + def read_file(file_name) -> BinaryIO: + real_path = os.path.realpath(os.path.join(os.path.dirname(__file__), '..')) + file_path = os.path.join(real_path, 'apimatic_core', 'mocks/files', file_name) + return open(file_path, "rb") + + @staticmethod + @validate_call + def global_errors() -> Dict[str, ErrorCase]: + return { + '400': ErrorCase(message='400 Global', exception_type=GlobalTestException), + '412': ErrorCase(message='Precondition Failed', exception_type=NestedModelException), + '3XX': ErrorCase(message='3XX Global', exception_type=GlobalTestException), + 'default': ErrorCase(message='Invalid response', exception_type=GlobalTestException), + } + + @staticmethod + @validate_call + def global_errors_with_template_message() -> Dict[str, ErrorCase]: + return { + '400': ErrorCase( + message='error_code => {$statusCode}, header => {$response.header.accept}, ' + 'body => {$response.body#/ServerCode} - {$response.body#/ServerMessage}', + message_type=MessageType.TEMPLATE, exception_type=GlobalTestException), + '412': ErrorCase( + message='global error message -> error_code => {$statusCode}, header => ' + '{$response.header.accept}, body => {$response.body#/ServerCode} - ' + '{$response.body#/ServerMessage} - {$response.body#/model/name}', + message_type=MessageType.TEMPLATE, exception_type=NestedModelException) + } + + @staticmethod + @validate_call + def request(http_method: HttpMethodEnum=HttpMethodEnum.GET, + query_url: str='http://localhost:3000/test', + headers: Dict[str, Any]=None, + query_parameters: Optional[Dict[str, Any]]=None, + parameters: Any=None, + files: Any=None) -> HttpRequest: + if headers is None: + headers = {} + if query_parameters is None: + query_parameters = {} + + return HttpRequest(http_method=http_method, + query_url=query_url, + headers=headers, + query_parameters=query_parameters, + parameters=parameters, + files=files) + + @staticmethod + @validate_call + def response( + status_code: int=200, reason_phrase: Optional[str]=None, headers: Optional[Dict[str, Any]]=None, text: Any=None + ) -> HttpResponse: + if headers is None: + headers = {} + return HttpResponse( + status_code=status_code, reason_phrase=reason_phrase, + headers=headers, text=text, request=Base.request() + ) + + @staticmethod + @validate_call + def get_http_datetime( + datetime_value: datetime, should_return_string: bool=True + ) -> Union[ApiHelper.CustomDate, str]: + if should_return_string is True: + return Base.get_http_datetime_str(datetime_value) + return ApiHelper.HttpDateTime(datetime_value) + + @staticmethod + @validate_call + def get_http_datetime_str(datetime_value: datetime) -> str: + return ApiHelper.HttpDateTime.from_datetime(datetime_value) + + @staticmethod + @validate_call + def get_rfc3339_datetime( + datetime_value: datetime, should_return_string: bool=True + ) -> Union[ApiHelper.CustomDate, str]: + if should_return_string is True: + return ApiHelper.RFC3339DateTime.from_datetime(datetime_value) + return ApiHelper.RFC3339DateTime(datetime_value) + + @staticmethod + @validate_call + def get_rfc3339_datetime_str(datetime_value: datetime) -> str: + return ApiHelper.RFC3339DateTime.from_datetime(datetime_value) + + @staticmethod + @validate_call + def new_api_call_builder(global_configuration: GlobalConfiguration) -> ApiCall: + return ApiCall(global_configuration) + + @staticmethod + @validate_call + def user_agent() -> str: + return 'Python|31.8.0|{engine}|{engine-version}|{os-info}' + + @staticmethod + @validate_call + def user_agent_parameters() -> Dict[str, Dict[str, Any]]: + return { + 'engine': {'value': platform.python_implementation(), 'encode': False}, + 'engine-version': {'value': "", 'encode': False}, + 'os-info': {'value': platform.system(), 'encode': False}, + } + + @staticmethod + @validate_call + def wrapped_parameters() -> Dict[str, Any]: + return { + 'bodyScalar': True, + 'bodyNonScalar': Base.employee_model(), + } + + @staticmethod + @validate_call + def mocked_http_client() -> HttpClient: + return MockHttpClient() + + @staticmethod + @validate_call(config=dict(arbitrary_types_allowed=True)) + def http_client_configuration( + http_callback: Optional[HttpCallBack]=HttpResponseCatcher(), + logging_configuration: Optional[ApiLoggingConfiguration]=None + ) -> HttpClientConfiguration: + return HttpClientConfiguration( + http_callback=http_callback, logging_configuration=logging_configuration, + http_client=Base.mocked_http_client() + ) + + @property + @validate_call + def new_request_builder(self) -> RequestBuilder: + return RequestBuilder().path('/test') \ + .server(Server.DEFAULT) + + @property + @validate_call + def new_response_handler(self) -> ResponseHandler: + return ResponseHandler() + + @property + @validate_call + def global_configuration(self) -> GlobalConfiguration: + return GlobalConfiguration( + http_client_configuration=self.http_client_configuration(), + base_uri_executor=BaseUriCallable().get_base_uri, + global_errors=self.global_errors()) + + @property + @validate_call + def global_configuration_without_http_callback(self) -> GlobalConfiguration: + return GlobalConfiguration( + http_client_configuration=self.http_client_configuration(None), + base_uri_executor=BaseUriCallable().get_base_uri) + + @property + @validate_call + def global_configuration_unimplemented_http_callback(self) -> GlobalConfiguration: + return GlobalConfiguration( + http_client_configuration=self.http_client_configuration(HttpCallBack()), + base_uri_executor=BaseUriCallable().get_base_uri) + + @property + @validate_call + def default_global_configuration(self) -> GlobalConfiguration: + return GlobalConfiguration() + + @property + @validate_call + def global_configuration_with_useragent(self) -> GlobalConfiguration: + global_configuration = self.global_configuration + global_configuration.add_user_agent(self.user_agent(), self.user_agent_parameters()) + return global_configuration + + @property + @validate_call + def global_configuration_with_auth(self) -> GlobalConfiguration: + global_configuration = self.global_configuration + global_configuration.auth_managers = { + 'basic_auth': self.basic_auth(), + 'bearer_auth': self.bearer_auth(), + 'custom_header_auth': self.custom_header_auth(), + 'custom_query_auth': self.custom_query_auth() + } + return global_configuration + + @staticmethod + @validate_call + def api_request_logging_configuration( + log_body: bool=False, log_headers: bool=False, headers_to_include: Optional[List[str]]=None, + headers_to_exclude: Optional[List[str]]=None, headers_to_unmask: Optional[List[str]]=None, + include_query_in_path: bool=False + ) -> ApiRequestLoggingConfiguration: + return ApiRequestLoggingConfiguration( + log_body=log_body, log_headers=log_headers, headers_to_include=headers_to_include or [], + headers_to_exclude=headers_to_exclude or [], headers_to_unmask=headers_to_unmask or [], + include_query_in_path=include_query_in_path) + + @staticmethod + @validate_call + def api_response_logging_configuration( + log_body: bool=False, log_headers: bool=False, headers_to_include: Optional[List[str]]=None, + headers_to_exclude: Optional[List[str]]=None, headers_to_unmask: Optional[List[str]]=None + ) -> ApiResponseLoggingConfiguration: + return ApiResponseLoggingConfiguration( + log_body=log_body, log_headers=log_headers, headers_to_include=headers_to_include or [], + headers_to_exclude=headers_to_exclude or [], headers_to_unmask=headers_to_unmask or []) + + @staticmethod + @validate_call(config=dict(arbitrary_types_allowed=True)) + def api_logging_configuration( + logger: Logger, log_level: int=logging.INFO, mask_sensitive_headers: bool=True, + request_logging_configuration: Optional[ApiRequestLoggingConfiguration]=None, + response_logging_configuration: Optional[ApiResponseLoggingConfiguration]=None + ) -> ApiLoggingConfiguration: + if request_logging_configuration is None: + request_logging_configuration = Base.api_request_logging_configuration() + + if response_logging_configuration is None: + response_logging_configuration = Base.api_response_logging_configuration() + + return ApiLoggingConfiguration( + logger=logger, log_level=log_level, mask_sensitive_headers=mask_sensitive_headers, + request_logging_config=request_logging_configuration, + response_logging_config=response_logging_configuration) + + @staticmethod + @validate_call(config=dict(arbitrary_types_allowed=True)) + def global_configuration_with_logging(logger: Logger) -> GlobalConfiguration: + return GlobalConfiguration( + http_client_configuration=Base.http_client_configuration( + logging_configuration=Base.api_logging_configuration(logger)), + base_uri_executor=BaseUriCallable().get_base_uri) + + @property + @validate_call + def global_configuration_with_uninitialized_auth_params(self) -> GlobalConfiguration: + global_configuration = self.global_configuration + global_configuration.auth_managers = { + 'basic_auth': BasicAuth(None), 'bearer_auth': BearerAuth(None), + 'custom_header_auth': CustomHeaderAuthentication(None) + } + return global_configuration + + @property + @validate_call + def global_configuration_with_partially_initialized_auth_params(self) -> GlobalConfiguration: + global_configuration = self.global_configuration + global_configuration.auth_managers = { + 'basic_auth': BasicAuth(None), + 'custom_header_auth': self.custom_header_auth() + } + return global_configuration diff --git a/tests/apimatic_core/mocks/callables/base_uri_callable.py b/tests/apimatic_core/mocks/callables/base_uri_callable.py index f46b61d..5c17e41 100644 --- a/tests/apimatic_core/mocks/callables/base_uri_callable.py +++ b/tests/apimatic_core/mocks/callables/base_uri_callable.py @@ -1,6 +1,6 @@ from enum import Enum -from typing import Dict +from typing import Dict, Any from apimatic_core.utilities.api_helper import ApiHelper @@ -35,7 +35,7 @@ def get_base_uri(self, server: Server=Server.DEFAULT): String: The base URI. """ - parameters = {} + parameters: Dict[str, Dict[str, Any]] = {} return ApiHelper.append_url_with_template_parameters( self.environments[Environment.TESTING][server], parameters diff --git a/tests/apimatic_core/mocks/callables/base_uri_callable.py~ b/tests/apimatic_core/mocks/callables/base_uri_callable.py~ new file mode 100644 index 0000000..0cef368 --- /dev/null +++ b/tests/apimatic_core/mocks/callables/base_uri_callable.py~ @@ -0,0 +1,42 @@ +from enum import Enum + +from typing import Dict + +from apimatic_core.utilities.api_helper import ApiHelper + + +class Environment(int, Enum): + TESTING = 1 + + +class Server(int, Enum): + """An enum for API servers""" + DEFAULT = 0 + AUTH_SERVER = 1 + + +class BaseUriCallable: + environments: Dict[Environment, Dict[Server, str]] = { + Environment.TESTING: { + Server.DEFAULT: 'http://localhost:3000', + Server.AUTH_SERVER: 'http://authserver:5000' + } + } + + def get_base_uri(self, server: Server=Server.DEFAULT): + """Generates the appropriate base URI for the environment and the + server. + + Args: + server (Configuration.Server): The server enum for which the base + URI is required. + + Returns: + String: The base URI. + + """ + parameters: Optional[Dict[str, Dict[str, Any]]] = {} + + return ApiHelper.append_url_with_template_parameters( + self.environments[Environment.TESTING][server], parameters + ) diff --git a/tests/apimatic_core/mocks/exceptions/nested_model_exception.py b/tests/apimatic_core/mocks/exceptions/nested_model_exception.py index c1a05d9..37bec36 100644 --- a/tests/apimatic_core/mocks/exceptions/nested_model_exception.py +++ b/tests/apimatic_core/mocks/exceptions/nested_model_exception.py @@ -29,7 +29,7 @@ class NestedModelExceptionValidator(BaseModel): class NestedModelException(APIException): server_message: Optional[str] = None server_code: Optional[int] = None - model: Optional[str] = None + model: Optional[Validate] = None def __init__(self, reason: str, response: HttpResponse): """Constructor for the NestedModelException class diff --git a/tests/apimatic_core/mocks/http/http_client.py b/tests/apimatic_core/mocks/http/http_client.py index 0280780..ea740a2 100644 --- a/tests/apimatic_core/mocks/http/http_client.py +++ b/tests/apimatic_core/mocks/http/http_client.py @@ -1,3 +1,5 @@ +from typing import Any + from apimatic_core_interfaces.configuration.endpoint_configuration import EndpointConfiguration from apimatic_core_interfaces.http.http_client import HttpClient from apimatic_core_interfaces.http.http_request import HttpRequest @@ -11,7 +13,7 @@ class MockHttpClient(HttpClient): contains_binary_response: bool = False @validate_call - def execute(self, request: HttpRequest, endpoint_configuration: EndpointConfiguration = None) -> HttpResponse: + def execute(self, request: HttpRequest, endpoint_configuration: EndpointConfiguration) -> HttpResponse: """Execute a given CoreHttpRequest to get a string response back Args: @@ -24,16 +26,11 @@ def execute(self, request: HttpRequest, endpoint_configuration: EndpointConfigur """ self.should_retry = endpoint_configuration.should_retry self.contains_binary_response = endpoint_configuration.has_binary_response - return HttpResponse( - status_code=200, - reason_phrase=None, - headers=request.headers, - text=str(request.parameters), - request=request) + return self.convert_response(None, self.contains_binary_response, request) @validate_call def convert_response( - self, response: HttpResponse, contains_binary_response: bool, http_request: HttpRequest + self, response: Any, contains_binary_response: bool, request: HttpRequest ) -> HttpResponse: """Converts the Response object of the CoreHttpClient into an CoreHttpResponse object. @@ -41,10 +38,15 @@ def convert_response( Args: response (dynamic): The original response object. contains_binary_response (bool): The flag to check if the response is of binary type. - http_request (HttpRequest): The original HttpRequest object. + request (HttpRequest): The original HttpRequest object. Returns: CoreHttpResponse: The converted CoreHttpResponse object. """ - pass + return HttpResponse( + status_code=200, + reason_phrase=None, + headers=request.headers, + text=str(request.parameters), + request=request) diff --git a/tests/apimatic_core/mocks/http/http_client.py~ b/tests/apimatic_core/mocks/http/http_client.py~ new file mode 100644 index 0000000..3044b6c --- /dev/null +++ b/tests/apimatic_core/mocks/http/http_client.py~ @@ -0,0 +1,52 @@ +from typing import Any + +from apimatic_core_interfaces.configuration.endpoint_configuration import EndpointConfiguration +from apimatic_core_interfaces.http.http_client import HttpClient +from apimatic_core_interfaces.http.http_request import HttpRequest +from apimatic_core_interfaces.http.http_response import HttpResponse +from pydantic import validate_call + + +class MockHttpClient(HttpClient): + + should_retry: bool = False + contains_binary_response: bool = False + + @validate_call + def execute(self, request: HttpRequest, endpoint_configuration: EndpointConfiguration = None) -> HttpResponse: + """Execute a given CoreHttpRequest to get a string response back + + Args: + request (HttpRequest): The given HttpRequest to execute. + endpoint_configuration (EndpointConfiguration): The endpoint configurations to use. + + Returns: + HttpResponse: The response of the CoreHttpRequest. + + """ + self.should_retry = endpoint_configuration.should_retry + self.contains_binary_response = endpoint_configuration.has_binary_response + return self.convert_response(None, self.contains_binary_response, request) + + @validate_call + def convert_response( + self, response: Any, contains_binary_response: bool, request: HttpRequest + ) -> HttpResponse: + """Converts the Response object of the CoreHttpClient into an + CoreHttpResponse object. + + Args: + response (dynamic): The original response object. + contains_binary_response (bool): The flag to check if the response is of binary type. + request (HttpRequest): The original HttpRequest object. + + Returns: + CoreHttpResponse: The converted CoreHttpResponse object. + + """ + return HttpResponse( + status_code=200, + reason_phrase=None, + headers=request.headers, + text=str(request.parameters), + request=request) diff --git a/tests/apimatic_core/mocks/http/http_response_catcher.py b/tests/apimatic_core/mocks/http/http_response_catcher.py index 6d94b6a..f4b3a20 100644 --- a/tests/apimatic_core/mocks/http/http_response_catcher.py +++ b/tests/apimatic_core/mocks/http/http_response_catcher.py @@ -1,3 +1,5 @@ +from typing import Optional + from apimatic_core_interfaces.http.http_request import HttpRequest from apimatic_core_interfaces.http.http_response import HttpResponse from pydantic import validate_call @@ -14,7 +16,7 @@ class HttpResponseCatcher(HttpCallBack): after a request is executed. """ - response: HttpResponse = None + response: Optional[HttpResponse] = None @validate_call def on_before_request(self, request: HttpRequest): diff --git a/tests/apimatic_core/mocks/http/http_response_catcher.py~ b/tests/apimatic_core/mocks/http/http_response_catcher.py~ new file mode 100644 index 0000000..97d2585 --- /dev/null +++ b/tests/apimatic_core/mocks/http/http_response_catcher.py~ @@ -0,0 +1,28 @@ +from apimatic_core_interfaces.http.http_request import HttpRequest +from apimatic_core_interfaces.http.http_response import HttpResponse +from pydantic import validate_call + +from apimatic_core.http.http_callback import HttpCallBack + + +class HttpResponseCatcher(HttpCallBack): + + """A class used for catching the HttpResponse object from controllers. + + This class inherits HttpCallBack and implements the on_after_response + method to catch the HttpResponse object as returned by the HttpClient + after a request is executed. + + """ + response: Optional[HttpResponse] = None + + @validate_call + def on_before_request(self, request: HttpRequest): + pass + + @validate_call + def on_after_response(self, response: HttpResponse): + self.response = response + + + diff --git a/tests/apimatic_core/mocks/models/complex_type.py b/tests/apimatic_core/mocks/models/complex_type.py index a459083..43ca6bd 100644 --- a/tests/apimatic_core/mocks/models/complex_type.py +++ b/tests/apimatic_core/mocks/models/complex_type.py @@ -34,12 +34,12 @@ class ComplexType(BaseModel): serialization_alias="innerComplexMapType") ] = None inner_complex_list_of_map_type: Annotated[ - Optional[List[InnerComplexType]], + Optional[List[Dict[str, InnerComplexType]]], Field(validation_alias=AliasChoices("inner_complex_list_of_map_type", "innerComplexListOfMapType"), serialization_alias="innerComplexListOfMapType") ] = None inner_complex_map_of_list_type: Annotated[ - Optional[List[InnerComplexType]], + Optional[Dict[str, List[InnerComplexType]]], Field(validation_alias=AliasChoices("inner_complex_map_of_list_type", "innerComplexMapOfListType"), serialization_alias="innerComplexMapOfListType") ] = None diff --git a/tests/apimatic_core/mocks/models/complex_type.py~ b/tests/apimatic_core/mocks/models/complex_type.py~ new file mode 100644 index 0000000..5242cf0 --- /dev/null +++ b/tests/apimatic_core/mocks/models/complex_type.py~ @@ -0,0 +1,55 @@ +from typing import Optional, List, Dict +from pydantic import BaseModel, Field, model_serializer, AliasChoices +from pydantic_core.core_schema import SerializerFunctionWrapHandler +from apimatic_core.utilities.api_helper import ApiHelper +from typing_extensions import Annotated + +from tests.apimatic_core.mocks.models.inner_complex_type import InnerComplexType + + +class ComplexType(BaseModel): + """Implementation of the 'ComplexType' model using Pydantic. + + Attributes: + inner_complex_type (InnerComplexType): TODO: type description here. + inner_complex_list_type (list[InnerComplexType]): TODO: type description here. + inner_complex_map_type (Optional[dict]): TODO: type description here. + inner_complex_list_of_map_type (Optional[list[InnerComplexType]]): TODO: type description here. + inner_complex_map_of_list_type (Optional[list[InnerComplexType]]): TODO: type description here. + """ + + inner_complex_type: Annotated[ + InnerComplexType, + Field(validation_alias=AliasChoices("inner_complex_type", "innerComplexType"), + serialization_alias="innerComplexType") + ] + inner_complex_list_type: Annotated[ + List[InnerComplexType], + Field(validation_alias=AliasChoices("inner_complex_list_type", "innerComplexListType"), + serialization_alias="innerComplexListType") + ] + inner_complex_map_type: Annotated[ + Optional[Dict], + Field(validation_alias=AliasChoices("inner_complex_map_type", "innerComplexMapType"), + serialization_alias="innerComplexMapType") + ] = None + inner_complex_list_of_map_type: Annotated[ + Optional[List[Dict[str, InnerComplexType]]], + Field(validation_alias=AliasChoices("inner_complex_list_of_map_type", "innerComplexListOfMapType"), + serialization_alias="innerComplexListOfMapType") + ] = None + inner_complex_map_of_list_type: Annotated[ + Optional[List[InnerComplexType]], + Field(validation_alias=AliasChoices("inner_complex_map_of_list_type", "innerComplexMapOfListType"), + serialization_alias="innerComplexMapOfListType") + ] = None + + @model_serializer(mode="wrap") + def serialize_model(self, nxt: SerializerFunctionWrapHandler): + _optional_fields = { + "inner_complex_map_type", + "inner_complex_list_of_map_type", + "inner_complex_map_of_list_type" + } + return ApiHelper.sanitize_model(optional_fields=_optional_fields, model_dump=nxt(self), + model_fields=self.model_fields, model_fields_set=self.model_fields_set) diff --git a/tests/apimatic_core/request_builder_tests/test_request_builder.py b/tests/apimatic_core/request_builder_tests/test_request_builder.py index 3fc8c24..6a77b4d 100644 --- a/tests/apimatic_core/request_builder_tests/test_request_builder.py +++ b/tests/apimatic_core/request_builder_tests/test_request_builder.py @@ -1,14 +1,19 @@ from datetime import datetime, date +from typing import Any, Dict, Optional, List, Tuple + import pytest import sys +from apimatic_core_interfaces.authentication.authentication import Authentication +from pydantic import validate_call + from apimatic_core.exceptions.auth_validation_exception import AuthValidationException from apimatic_core_interfaces.http.http_method_enum import HttpMethodEnum from apimatic_core.authentication.multiple.and_auth_group import And from apimatic_core.authentication.multiple.or_auth_group import Or from apimatic_core.authentication.multiple.single_auth import Single from apimatic_core.types.array_serialization_format import SerializationFormats -from apimatic_core.types.file_wrapper import FileWrapper +from apimatic_core.types.file_wrapper import FileWrapper, FileType from apimatic_core.types.parameter import Parameter from apimatic_core.types.xml_attributes import XmlAttributes from apimatic_core.utilities.api_helper import ApiHelper @@ -16,7 +21,10 @@ from apimatic_core.utilities.xml_helper import XmlHelper from tests.apimatic_core.base import Base from tests.apimatic_core.mocks.callables.base_uri_callable import Server -from requests.utils import quote +from urllib.parse import quote + +from tests.apimatic_core.mocks.models.person import Employee +from tests.apimatic_core.mocks.models.xml_model import XMLModel from tests.apimatic_core.mocks.union_type_lookup import UnionTypeLookUp @@ -26,30 +34,38 @@ class TestRequestBuilder(Base): (Server.DEFAULT, 'http://localhost:3000/'), (Server.AUTH_SERVER, 'http://authserver:5000/') ]) - def test_base_uri(self, input_server, expected_base_uri): + @validate_call + def test_base_uri(self, input_server: Server, expected_base_uri: str): http_request = (self.new_request_builder .server(input_server) .http_method(HttpMethodEnum.POST) .path('/') .build(self.global_configuration)) + assert http_request.query_url == expected_base_uri + @validate_call def test_path(self): http_request = self.new_request_builder.http_method(HttpMethodEnum.POST).build(self.global_configuration) + assert http_request.query_url == 'http://localhost:3000/test' + @validate_call def test_required_param(self): with pytest.raises(ValueError) as validation_error: self.new_request_builder \ .query_param(Parameter(key='query_param', value=None, is_required=True)) \ .build(self.global_configuration) + assert validation_error.value.args[0] == 'Required parameter query_param cannot be None.' + @validate_call def test_optional_param(self): http_request = self.new_request_builder \ .http_method(HttpMethodEnum.POST) \ .query_param(Parameter(key='query_param', value=None, is_required=False)) \ .build(self.global_configuration) + assert http_request.query_url == 'http://localhost:3000/test' @pytest.mark.parametrize('input_http_method, expected_http_method', [ @@ -59,10 +75,12 @@ def test_optional_param(self): (HttpMethodEnum.DELETE, HttpMethodEnum.DELETE), (HttpMethodEnum.GET, HttpMethodEnum.GET), ]) - def test_http_method(self, input_http_method, expected_http_method): + @validate_call + def test_http_method(self, input_http_method: HttpMethodEnum, expected_http_method: HttpMethodEnum): http_request = self.new_request_builder \ .http_method(input_http_method) \ .build(self.global_configuration) + assert http_request.http_method == expected_http_method @pytest.mark.parametrize('input_template_param_value, expected_template_param_value, should_encode', [ @@ -81,14 +99,17 @@ def test_http_method(self, input_http_method, expected_http_method): ('Basic%Test', 'Basic%Test', False), ('Basic|Test', 'Basic|Test', False), ]) - def test_template_params_with_encoding(self, input_template_param_value, expected_template_param_value, - should_encode): + @validate_call + def test_template_params_with_encoding( + self, input_template_param_value: str, expected_template_param_value: str, should_encode: bool + ): http_request = self.new_request_builder \ .http_method(HttpMethodEnum.GET) \ .path('/{template_param}') \ .template_param(Parameter(key='template_param', value=input_template_param_value, should_encode=should_encode)) \ .build(self.global_configuration) + assert http_request.query_url == 'http://localhost:3000/{}'.format(expected_template_param_value) @pytest.mark.parametrize('input_query_param_value, expected_query_param_value, array_serialization_format', [ @@ -99,10 +120,10 @@ def test_template_params_with_encoding(self, input_template_param_value, expecte (ApiHelper.UnixDateTime.from_datetime(datetime(1994, 2, 13, 5, 30, 15)), 'query_param=761117415', SerializationFormats.INDEXED), (Base.get_http_datetime(datetime(1994, 2, 13, 5, 30, 15)), - 'query_param={}'.format(quote(Base.get_http_datetime(datetime(1994, 2, 13, 5, 30, 15)), safe='')), + 'query_param={}'.format(quote(Base.get_http_datetime_str(datetime(1994, 2, 13, 5, 30, 15)), safe='')), SerializationFormats.INDEXED), (Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15)), - 'query_param={}'.format(quote(Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15)), safe='')), + 'query_param={}'.format(quote(Base.get_rfc3339_datetime_str(datetime(1994, 2, 13, 5, 30, 15)), safe='')), SerializationFormats.INDEXED), ([1, 2, 3, 4], 'query_param[0]=1&query_param[1]=2&query_param[2]=3&query_param[3]=4', SerializationFormats.INDEXED), @@ -133,7 +154,7 @@ def test_template_params_with_encoding(self, input_template_param_value, expecte '&query_param[age]=27' '&query_param[birthday]=1994-02-13' '&query_param[birthtime]={}'.format(quote( - Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15)), safe='')) + + Base.get_rfc3339_datetime_str(datetime(1994, 2, 13, 5, 30, 15)), safe='')) + '&query_param[name]=Bob' '&query_param[uid]=1234567' '&query_param[personType]=Empl' @@ -142,14 +163,14 @@ def test_template_params_with_encoding(self, input_template_param_value, expecte '&query_param[dependents][0][age]=12' '&query_param[dependents][0][birthday]=1994-02-13' '&query_param[dependents][0][birthtime]={}'.format(quote( - Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15)), safe='')) + + Base.get_rfc3339_datetime_str(datetime(1994, 2, 13, 5, 30, 15)), safe='')) + '&query_param[dependents][0][name]=John' '&query_param[dependents][0][uid]=7654321' '&query_param[dependents][0][personType]=Per' '&query_param[dependents][0][key1]=value1' '&query_param[dependents][0][key2]=value2' '&query_param[hiredAt]={}'.format(quote( - Base.get_http_datetime(datetime(1994, 2, 13, 5, 30, 15)), safe='')) + + Base.get_http_datetime_str(datetime(1994, 2, 13, 5, 30, 15)), safe='')) + '&query_param[joiningDay]=Monday' '&query_param[salary]=30000' '&query_param[workingDays][0]=Monday' @@ -157,22 +178,31 @@ def test_template_params_with_encoding(self, input_template_param_value, expecte '&query_param[key1]=value1' '&query_param[key2]=value2', SerializationFormats.INDEXED) ]) - def test_query_params(self, input_query_param_value, expected_query_param_value, array_serialization_format): + @validate_call + def test_query_params( + self, input_query_param_value: Any, expected_query_param_value: str, + array_serialization_format: SerializationFormats + ): http_request = self.new_request_builder \ .http_method(HttpMethodEnum.GET) \ .query_param(Parameter(key='query_param', value=input_query_param_value)) \ .array_serialization_format(array_serialization_format) \ .build(self.global_configuration) + assert http_request.query_url == 'http://localhost:3000/test?{}'.format(expected_query_param_value) @pytest.mark.parametrize('input_additional_query_params_value, expected_additional_query_params_value', [ ({'key1': 'value1', 'key2': 'value2'}, 'key1=value1&key2=value2') ]) - def test_additional_query_params(self, input_additional_query_params_value, expected_additional_query_params_value): + @validate_call + def test_additional_query_params( + self, input_additional_query_params_value: Dict[str, str], expected_additional_query_params_value: str + ): http_request = self.new_request_builder \ .http_method(HttpMethodEnum.GET) \ .additional_query_params(input_additional_query_params_value) \ .build(self.global_configuration) + assert http_request.query_url == 'http://localhost:3000/test?{}'.format(expected_additional_query_params_value) @pytest.mark.parametrize('input_local_header_param_value, expected_local_header_param_value', [ @@ -188,11 +218,15 @@ def test_additional_query_params(self, input_additional_query_params_value, expe {'header_param': '{}'.format(Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15)))}), ([1, 2, 3, 4], {'header_param': [1, 2, 3, 4]}) ]) - def test_local_headers(self, input_local_header_param_value, expected_local_header_param_value): + @validate_call + def test_local_headers( + self, input_local_header_param_value: Any, expected_local_header_param_value: Dict[str, Any] + ): http_request = self.new_request_builder \ .http_method(HttpMethodEnum.GET) \ .header_param(Parameter(key='header_param', value=input_local_header_param_value)) \ .build(self.global_configuration) + assert http_request.headers == expected_local_header_param_value @pytest.mark.parametrize('input_global_header_param_value, expected_global_header_param_value', [ @@ -208,12 +242,16 @@ def test_local_headers(self, input_local_header_param_value, expected_local_head {'header_param': '{}'.format(Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15)))}), ([100, 200, 300, 400], {'header_param': [100, 200, 300, 400]}) ]) - def test_global_headers(self, input_global_header_param_value, expected_global_header_param_value): + @validate_call + def test_global_headers( + self, input_global_header_param_value: Any, expected_global_header_param_value: Dict[str, Any] + ): global_configuration = self.global_configuration global_configuration.global_headers = {'header_param': input_global_header_param_value} http_request = self.new_request_builder \ .http_method(HttpMethodEnum.GET) \ .build(global_configuration) + assert http_request.headers == expected_global_header_param_value @pytest.mark.parametrize('input_additional_header_param_value, expected_additional_header_param_value', [ @@ -229,12 +267,16 @@ def test_global_headers(self, input_global_header_param_value, expected_global_h {'header_param': '{}'.format(Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15)))}), ([100, 200, 300, 400], {'header_param': [100, 200, 300, 400]}) ]) - def test_additional_headers(self, input_additional_header_param_value, expected_additional_header_param_value): + @validate_call + def test_additional_headers( + self, input_additional_header_param_value: Any, expected_additional_header_param_value: Dict[str, Any] + ): global_configuration = self.global_configuration global_configuration.additional_headers = {'header_param': input_additional_header_param_value} http_request = self.new_request_builder \ .http_method(HttpMethodEnum.GET) \ .build(global_configuration) + assert http_request.headers == expected_additional_header_param_value @pytest.mark.parametrize('input_global_header_param_value,' @@ -243,14 +285,18 @@ def test_additional_headers(self, input_additional_header_param_value, expected_ ('global_string', None, {'header_param': None}), ('global_string', 'local_string', {'header_param': 'local_string'}) ]) - def test_local_and_global_headers_precedence(self, input_global_header_param_value, input_local_header_param_value, - expected_header_param_value): + @validate_call + def test_local_and_global_headers_precedence( + self, input_global_header_param_value: str, input_local_header_param_value: Optional[str], + expected_header_param_value: Dict[str, Any] + ): global_configuration = self.global_configuration global_configuration.global_headers = {'header_param': input_global_header_param_value} http_request = self.new_request_builder \ .http_method(HttpMethodEnum.GET) \ .header_param(Parameter(key='header_param', value=input_local_header_param_value)) \ .build(global_configuration) + assert http_request.headers == expected_header_param_value @pytest.mark.parametrize('input_global_header_param_value,' @@ -262,8 +308,11 @@ def test_local_and_global_headers_precedence(self, input_global_header_param_val ('global_string', 'local_string', None, {'header_param': None}) ]) - def test_all_headers_precedence(self, input_global_header_param_value, input_local_header_param_value, - input_additional_header_param_value, expected_header_param_value): + @validate_call + def test_all_headers_precedence( + self, input_global_header_param_value: str, input_local_header_param_value: str, + input_additional_header_param_value: Optional[str], expected_header_param_value: Dict[str, Any] + ): global_configuration = self.global_configuration global_configuration.global_headers = {'header_param': input_global_header_param_value} global_configuration.additional_headers = {'header_param': input_additional_header_param_value} @@ -271,18 +320,20 @@ def test_all_headers_precedence(self, input_global_header_param_value, input_loc .http_method(HttpMethodEnum.GET) \ .header_param(Parameter(key='header_param', value=input_local_header_param_value)) \ .build(global_configuration) + assert http_request.headers == expected_header_param_value + @validate_call def test_useragent_header(self): - engines = ['CPython', 'Jython', 'JPython', 'IronPython', 'PyPy', 'RubyPython', 'AnacondaPython'] - operating_systems = ['Linux', 'Windows', 'Darwin', 'FreeBSD', 'OpenBSD', 'macOS'] + engines: List[str] = ['CPython', 'Jython', 'JPython', 'IronPython', 'PyPy', 'RubyPython', 'AnacondaPython'] + operating_systems: List[str] = ['Linux', 'Windows', 'Darwin', 'FreeBSD', 'OpenBSD', 'macOS'] http_request = self.new_request_builder \ .http_method(HttpMethodEnum.GET) \ .build(self.global_configuration_with_useragent) [lang, version, engine, _, osInfo] = http_request.headers['user-agent'].split('|') - assert lang == 'Python' and version == '31.8.0' \ - and engine in engines and osInfo in operating_systems + + assert lang == 'Python' and version == '31.8.0' and engine in engines and osInfo in operating_systems @pytest.mark.parametrize('input_form_param_value, expected_form_param_value, array_serialization_format', [ ('string', [('form_param', 'string')], SerializationFormats.INDEXED), @@ -327,7 +378,11 @@ def test_useragent_header(self): ('form_param[workingDays][1]', 'Tuesday'), ('form_param[key1]', 'value1'), ('form_param[key2]', 'value2')], SerializationFormats.INDEXED) ]) - def test_form_params(self, input_form_param_value, expected_form_param_value, array_serialization_format): + @validate_call + def test_form_params( + self, input_form_param_value: Any, expected_form_param_value: List[Tuple[str, Any]], + array_serialization_format: SerializationFormats + ): http_request = self.new_request_builder \ .http_method(HttpMethodEnum.POST) \ .form_param(Parameter(key='form_param', value=input_form_param_value)) \ @@ -339,11 +394,16 @@ def test_form_params(self, input_form_param_value, expected_form_param_value, ar @pytest.mark.parametrize('input_additional_form_param_value, expected_additional_form_param_value', [ ({'key1': 'value1', 'key2': 'value2'}, [('key1', 'value1'), ('key2', 'value2')]) ]) - def test_addition_form_params(self, input_additional_form_param_value, expected_additional_form_param_value): + @validate_call + def test_addition_form_params( + self, input_additional_form_param_value: Dict[str, str], + expected_additional_form_param_value: List[Tuple[str, str]] + ): http_request = self.new_request_builder \ .http_method(HttpMethodEnum.POST) \ .additional_form_params(input_additional_form_param_value) \ .build(self.global_configuration) + assert http_request.parameters == expected_additional_form_param_value @pytest.mark.parametrize('input_form_param_value,' @@ -355,13 +415,17 @@ def test_addition_form_params(self, input_additional_form_param_value, expected_ ('additional_key1', 'additional_value1'), ('additional_key2', 'additional_value2')]) ]) - def test_form_params_with_additional_form_params(self, input_form_param_value, input_additional_form_param_value, - expected_form_param_value): + @validate_call + def test_form_params_with_additional_form_params( + self, input_form_param_value: Dict[str, str], input_additional_form_param_value: Dict[str, str], + expected_form_param_value: List[Tuple[str, str]] + ): http_request = self.new_request_builder \ .http_method(HttpMethodEnum.POST) \ .form_param(Parameter(key='form_param', value=input_form_param_value)) \ .additional_form_params(input_additional_form_param_value) \ .build(self.global_configuration) + assert http_request.parameters == expected_form_param_value @pytest.mark.parametrize('input_body_param_value, expected_body_param_value', [ @@ -375,11 +439,13 @@ def test_form_params_with_additional_form_params(self, input_form_param_value, i (Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15)), Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15))) ]) - def test_json_body_params_without_serializer(self, input_body_param_value, expected_body_param_value): + @validate_call + def test_json_body_params_without_serializer(self, input_body_param_value: Any, expected_body_param_value: Any): http_request = self.new_request_builder \ .http_method(HttpMethodEnum.POST) \ .body_param(Parameter(value=input_body_param_value)) \ .build(self.global_configuration) + assert http_request.parameters == expected_body_param_value @pytest.mark.parametrize('input_body_param_value1, input_body_param_value2, expected_body_param_value', [ @@ -387,14 +453,16 @@ def test_json_body_params_without_serializer(self, input_body_param_value, expec (100, 200, '{"param1": 100, "param2": 200}'), (100.12, 200.12, '{"param1": 100.12, "param2": 200.12}') ]) - def test_multiple_json_body_params_with_serializer(self, input_body_param_value1, input_body_param_value2, - expected_body_param_value): + @validate_call + def test_multiple_json_body_params_with_serializer(self, input_body_param_value1: Any, input_body_param_value2: Any, + expected_body_param_value: str): http_request = self.new_request_builder \ .http_method(HttpMethodEnum.POST) \ .body_param(Parameter(key='param1', value=input_body_param_value1)) \ .body_param(Parameter(key='param2', value=input_body_param_value2)) \ .body_serializer(ApiHelper.json_serialize) \ .build(self.global_configuration) + assert http_request.parameters == expected_body_param_value @pytest.mark.parametrize('input_body_param_value, expected_body_param_value', [ @@ -405,19 +473,22 @@ def test_multiple_json_body_params_with_serializer(self, input_body_param_value1 '{"key1": "value1", "key2": [1, 2, 3, {"key1": "value1", "key2": "value2"}]}'), (Base.employee_model(), Base.get_serialized_employee()) ]) - def test_json_body_params_with_serializer(self, input_body_param_value, expected_body_param_value): + @validate_call + def test_json_body_params_with_serializer(self, input_body_param_value: Any, expected_body_param_value: str): http_request = self.new_request_builder \ .http_method(HttpMethodEnum.POST) \ .body_param(Parameter(value=input_body_param_value)) \ .body_serializer(ApiHelper.json_serialize) \ .build(self.global_configuration) + assert http_request.parameters == expected_body_param_value @pytest.mark.parametrize('input_value, expected_value', [ (100.0, '100.0'), (True, 'true') ]) - def test_type_combinator_validation_in_request(self, input_value, expected_value): + @validate_call + def test_type_combinator_validation_in_request(self, input_value: Any, expected_value: str): http_request = self.new_request_builder \ .http_method(HttpMethodEnum.POST) \ .body_param(Parameter( @@ -425,6 +496,7 @@ def test_type_combinator_validation_in_request(self, input_value, expected_value value=input_value)) \ .body_serializer(ApiHelper.json_serialize) \ .build(self.global_configuration) + assert http_request.parameters == expected_value @pytest.mark.parametrize('input_body_param_value, expected_body_param_value', [ @@ -439,7 +511,8 @@ def test_type_combinator_validation_in_request(self, input_value, expected_value '' '') ]) - def test_xml_body_param_with_serializer(self, input_body_param_value, expected_body_param_value): + @validate_call + def test_xml_body_param_with_serializer(self, input_body_param_value: XMLModel, expected_body_param_value: str): if sys.version_info[1] == 7: expected_body_param_value = expected_body_param_value.replace( 'string="String" number="10000" boolean="false">', @@ -449,6 +522,7 @@ def test_xml_body_param_with_serializer(self, input_body_param_value, expected_b .xml_attributes(XmlAttributes(value=input_body_param_value, root_element_name='AttributesAndElements')) \ .body_serializer(XmlHelper.serialize_to_xml) \ .build(self.global_configuration) + assert http_request.parameters == expected_body_param_value @pytest.mark.parametrize('input_body_param_value, expected_body_param_value', [ @@ -476,7 +550,10 @@ def test_xml_body_param_with_serializer(self, input_body_param_value, expected_b '' '') ]) - def test_xml_array_body_param_with_serializer(self, input_body_param_value, expected_body_param_value): + @validate_call + def test_xml_array_body_param_with_serializer( + self, input_body_param_value: List[XMLModel], expected_body_param_value: str + ): if sys.version_info[1] == 7: expected_body_param_value = expected_body_param_value.replace( 'string="String" number="10000" boolean="false">', @@ -488,12 +565,16 @@ def test_xml_array_body_param_with_serializer(self, input_body_param_value, expe array_item_name='item')) \ .body_serializer(XmlHelper.serialize_list_to_xml) \ .build(self.global_configuration) + assert http_request.parameters == expected_body_param_value @pytest.mark.parametrize("input_body_param_value, expected_input_file, expected_content_type", [ (FileWrapper(Base.read_file('apimatic.png'), 'image/png'), Base.read_file('apimatic.png'), 'image/png')]) - def test_file_as_body_param(self, input_body_param_value, expected_input_file, expected_content_type): + @validate_call(config=dict(arbitrary_types_allowed=True)) + def test_file_as_body_param( + self, input_body_param_value: FileWrapper, expected_input_file: FileType, expected_content_type: str + ): http_request = self.new_request_builder \ .http_method(HttpMethodEnum.POST) \ .header_param(Parameter(key='content-type', value='application/xml')) \ @@ -506,23 +587,24 @@ def test_file_as_body_param(self, input_body_param_value, expected_input_file, e assert input_file.read() == expected_input_file.read() \ and http_request.headers['content-type'] == expected_content_type finally: - input_file.close() - expected_input_file.close() + self.close_file(input_file) + self.close_file(expected_input_file) @pytest.mark.parametrize( - "input_multipart_param_value1, input_default_content_type1,input_multipart_param_value2, input_default_content_type2,expected_file, expected_file_content_type, expected_model, expected_model_content_type", [ - (Base.read_file('apimatic.png'), 'image/png', Base.employee_model(), - 'application/json', Base.read_file('apimatic.png'), 'image/png', - Base.get_serialized_employee(), 'application/json') - ]) - def test_multipart_request_without_file_wrapper(self, input_multipart_param_value1, - input_default_content_type1, - input_multipart_param_value2, - input_default_content_type2, - expected_file, - expected_file_content_type, - expected_model, - expected_model_content_type): + "input_multipart_param_value1, input_default_content_type1,input_multipart_param_value2, input_default_content_type2,expected_file, expected_file_content_type, expected_model, expected_model_content_type", + [ + (Base.read_file('apimatic.png'), 'image/png', + Base.employee_model(), 'application/json', + Base.read_file('apimatic.png'), 'image/png', + Base.get_serialized_employee(), 'application/json') + ]) + @validate_call(config=dict(arbitrary_types_allowed=True)) + def test_multipart_request_without_file_wrapper( + self, input_multipart_param_value1: FileType, input_default_content_type1: str, + input_multipart_param_value2: Employee, input_default_content_type2: str, + expected_file: FileType, expected_file_content_type: str, + expected_model: str, expected_model_content_type: str + ): http_request = self.new_request_builder \ .http_method(HttpMethodEnum.POST) \ .multipart_param(Parameter(key='file_wrapper', value=input_multipart_param_value1, @@ -542,22 +624,24 @@ def test_multipart_request_without_file_wrapper(self, input_multipart_param_valu and actual_model == expected_model \ and actual_model_content_type == expected_model_content_type finally: - actual_file.close() - expected_file.close() + self.close_file(actual_file) + self.close_file(expected_file) @pytest.mark.parametrize( - "input_multipart_param_value1, input_multipart_param_value2, input_default_content_type2,expected_file, expected_file_content_type,expected_model, expected_model_content_type", [ - (FileWrapper(Base.read_file('apimatic.png'), 'image/png'), Base.employee_model(), - 'application/json', Base.read_file('apimatic.png'), 'image/png', - Base.get_serialized_employee(), 'application/json') - ]) - def test_multipart_request_with_file_wrapper(self, input_multipart_param_value1, - input_multipart_param_value2, - input_default_content_type2, - expected_file, - expected_file_content_type, - expected_model, - expected_model_content_type): + "input_multipart_param_value1, input_multipart_param_value2, input_default_content_type2, expected_file, expected_file_content_type,expected_model, expected_model_content_type", + [ + (FileWrapper(Base.read_file('apimatic.png'), 'image/png'), + Base.employee_model(), 'application/json', + Base.read_file('apimatic.png'), 'image/png', + Base.get_serialized_employee(), 'application/json') + ]) + @validate_call(config=dict(arbitrary_types_allowed=True)) + def test_multipart_request_with_file_wrapper( + self, input_multipart_param_value1: FileWrapper, + input_multipart_param_value2: Employee, input_default_content_type2: str, + expected_file: FileType, expected_file_content_type: str, + expected_model: str, expected_model_content_type: str + ): http_request = self.new_request_builder \ .http_method(HttpMethodEnum.POST) \ @@ -578,8 +662,8 @@ def test_multipart_request_with_file_wrapper(self, input_multipart_param_value1, and actual_model == expected_model \ and actual_model_content_type == expected_model_content_type finally: - actual_file.close() - expected_file.close() + self.close_file(actual_file) + self.close_file(expected_file) @pytest.mark.parametrize('input_auth_scheme, expected_auth_header_key, expected_auth_header_value', [ (Single('basic_auth'), 'Basic-Authorization', 'Basic {}'.format( @@ -587,7 +671,10 @@ def test_multipart_request_with_file_wrapper(self, input_multipart_param_value1, (Single('bearer_auth'), 'Bearer-Authorization', 'Bearer 0b79bab50daca910b000d4f1a2b675d604257e42'), (Single('custom_header_auth'), 'token', 'Qaws2W233WedeRe4T56G6Vref2') ]) - def test_header_authentication(self, input_auth_scheme, expected_auth_header_key, expected_auth_header_value): + @validate_call(config=dict(arbitrary_types_allowed=True)) + def test_header_authentication( + self, input_auth_scheme: Authentication, expected_auth_header_key: str, expected_auth_header_value: str + ): http_request = self.new_request_builder \ .http_method(HttpMethodEnum.POST) \ .auth(input_auth_scheme) \ @@ -595,6 +682,7 @@ def test_header_authentication(self, input_auth_scheme, expected_auth_header_key assert http_request.headers[expected_auth_header_key] == expected_auth_header_value + @validate_call def test_query_authentication(self): http_request = self.new_request_builder \ .http_method(HttpMethodEnum.POST) \ @@ -608,12 +696,14 @@ def test_query_authentication(self): (Or('invalid_1', 'invalid_2')), (And('invalid_1', 'invalid_2')) ]) - def test_invalid_key_authentication(self, input_invalid_auth_scheme): + @validate_call(config=dict(arbitrary_types_allowed=True)) + def test_invalid_key_authentication(self, input_invalid_auth_scheme: Authentication): with pytest.raises(ValueError) as validation_error: self.new_request_builder \ .http_method(HttpMethodEnum.POST) \ .auth(input_invalid_auth_scheme) \ .build(self.global_configuration_with_auth) + assert validation_error.value.args[0] == 'Auth key is invalid.' @pytest.mark.parametrize('input_auth_scheme, expected_request_headers', [ @@ -663,11 +753,15 @@ def test_invalid_key_authentication(self, input_invalid_auth_scheme): 'token': None }), ]) - def test_success_case_of_multiple_authentications(self, input_auth_scheme, expected_request_headers): + @validate_call(config=dict(arbitrary_types_allowed=True)) + def test_success_case_of_multiple_authentications( + self, input_auth_scheme: Authentication, expected_request_headers: Dict[str, Optional[str]] + ): http_request = self.new_request_builder \ .http_method(HttpMethodEnum.POST) \ .auth(input_auth_scheme) \ .build(self.global_configuration_with_auth) + for key in expected_request_headers.keys(): assert http_request.headers.get(key) == expected_request_headers.get(key) @@ -701,12 +795,16 @@ def test_success_case_of_multiple_authentications(self, input_auth_scheme, expec (And(), ''), (And( 'basic_auth'), '[BasicAuth: username or password is undefined.]') ]) - def test_failed_case_of_multiple_authentications(self, input_auth_scheme, expected_error_message): + @validate_call(config=dict(arbitrary_types_allowed=True)) + def test_failed_case_of_multiple_authentications( + self, input_auth_scheme: Authentication, expected_error_message: str + ): with pytest.raises(AuthValidationException) as errors: self.new_request_builder \ .http_method(HttpMethodEnum.POST) \ .auth(input_auth_scheme) \ .build(self.global_configuration_with_uninitialized_auth_params) + assert errors.value.args[0] == expected_error_message @pytest.mark.parametrize('input_auth_scheme, expected_auth_header_key,' @@ -716,8 +814,10 @@ def test_failed_case_of_multiple_authentications(self, input_auth_scheme, expect (Or('custom_header_auth', And('basic_auth', 'custom_header_auth')), 'token', 'Qaws2W233WedeRe4T56G6Vref2') ]) - def test_case_of_multiple_authentications(self, input_auth_scheme, expected_auth_header_key, - expected_auth_header_value): + @validate_call(config=dict(arbitrary_types_allowed=True)) + def test_case_of_multiple_authentications( + self, input_auth_scheme: Authentication, expected_auth_header_key: str, expected_auth_header_value: str + ): http_request = self.new_request_builder \ .http_method(HttpMethodEnum.POST) \ .auth(input_auth_scheme) \ @@ -725,3 +825,8 @@ def test_case_of_multiple_authentications(self, input_auth_scheme, expected_auth assert http_request.headers[expected_auth_header_key] == expected_auth_header_value assert http_request.headers.get('Authorization') is None + + @validate_call(config=dict(arbitrary_types_allowed=True)) + def close_file(self, file: Optional[FileType]) -> None: + if file is not None: + file.close() \ No newline at end of file diff --git a/tests/apimatic_core/request_builder_tests/test_request_builder.py~ b/tests/apimatic_core/request_builder_tests/test_request_builder.py~ new file mode 100644 index 0000000..cc0fc70 --- /dev/null +++ b/tests/apimatic_core/request_builder_tests/test_request_builder.py~ @@ -0,0 +1,832 @@ +from datetime import datetime, date +from typing import Any, Dict, Optional, List, Tuple + +import pytest +import sys + +from apimatic_core_interfaces.authentication.authentication import Authentication +from pydantic import validate_call + +from apimatic_core.exceptions.auth_validation_exception import AuthValidationException +from apimatic_core_interfaces.http.http_method_enum import HttpMethodEnum +from apimatic_core.authentication.multiple.and_auth_group import And +from apimatic_core.authentication.multiple.or_auth_group import Or +from apimatic_core.authentication.multiple.single_auth import Single +from apimatic_core.types.array_serialization_format import SerializationFormats +from apimatic_core.types.file_wrapper import FileWrapper, FileType +from apimatic_core.types.parameter import Parameter +from apimatic_core.types.xml_attributes import XmlAttributes +from apimatic_core.utilities.api_helper import ApiHelper +from apimatic_core.utilities.auth_helper import AuthHelper +from apimatic_core.utilities.xml_helper import XmlHelper +from tests.apimatic_core.base import Base +from tests.apimatic_core.mocks.callables.base_uri_callable import Server +from urllib.parse import quote + +from tests.apimatic_core.mocks.models.person import Employee +from tests.apimatic_core.mocks.models.xml_model import XMLModel +from tests.apimatic_core.mocks.union_type_lookup import UnionTypeLookUp + + +class TestRequestBuilder(Base): + + @pytest.mark.parametrize('input_server, expected_base_uri', [ + (Server.DEFAULT, 'http://localhost:3000/'), + (Server.AUTH_SERVER, 'http://authserver:5000/') + ]) + @validate_call + def test_base_uri(self, input_server: Server, expected_base_uri: str): + http_request = (self.new_request_builder + .server(input_server) + .http_method(HttpMethodEnum.POST) + .path('/') + .build(self.global_configuration)) + + assert http_request.query_url == expected_base_uri + + @validate_call + def test_path(self): + http_request = self.new_request_builder.http_method(HttpMethodEnum.POST).build(self.global_configuration) + + assert http_request.query_url == 'http://localhost:3000/test' + + @validate_call + def test_required_param(self): + with pytest.raises(ValueError) as validation_error: + self.new_request_builder \ + .query_param(Parameter(key='query_param', value=None, is_required=True)) \ + .build(self.global_configuration) + + assert validation_error.value.args[0] == 'Required parameter query_param cannot be None.' + + @validate_call + def test_optional_param(self): + http_request = self.new_request_builder \ + .http_method(HttpMethodEnum.POST) \ + .query_param(Parameter(key='query_param', value=None, is_required=False)) \ + .build(self.global_configuration) + + assert http_request.query_url == 'http://localhost:3000/test' + + @pytest.mark.parametrize('input_http_method, expected_http_method', [ + (HttpMethodEnum.POST, HttpMethodEnum.POST), + (HttpMethodEnum.PUT, HttpMethodEnum.PUT), + (HttpMethodEnum.PATCH, HttpMethodEnum.PATCH), + (HttpMethodEnum.DELETE, HttpMethodEnum.DELETE), + (HttpMethodEnum.GET, HttpMethodEnum.GET), + ]) + @validate_call + def test_http_method(self, input_http_method: HttpMethodEnum, expected_http_method: HttpMethodEnum): + http_request = self.new_request_builder \ + .http_method(input_http_method) \ + .build(self.global_configuration) + + assert http_request.http_method == expected_http_method + + @pytest.mark.parametrize('input_template_param_value, expected_template_param_value, should_encode', [ + ('Basic Test', 'Basic%20Test', True), + ('Basic"Test', 'Basic%22Test', True), + ('BasicTest', 'Basic%3ETest', True), + ('Basic#Test', 'Basic%23Test', True), + ('Basic%Test', 'Basic%25Test', True), + ('Basic|Test', 'Basic%7CTest', True), + ('Basic Test', 'Basic Test', False), + ('Basic"Test', 'Basic"Test', False), + ('BasicTest', 'Basic>Test', False), + ('Basic#Test', 'Basic#Test', False), + ('Basic%Test', 'Basic%Test', False), + ('Basic|Test', 'Basic|Test', False), + ]) + @validate_call + def test_template_params_with_encoding( + self, input_template_param_value: str, expected_template_param_value: str, should_encode: bool + ): + http_request = self.new_request_builder \ + .http_method(HttpMethodEnum.GET) \ + .path('/{template_param}') \ + .template_param(Parameter(key='template_param', value=input_template_param_value, + should_encode=should_encode)) \ + .build(self.global_configuration) + + assert http_request.query_url == 'http://localhost:3000/{}'.format(expected_template_param_value) + + @pytest.mark.parametrize('input_query_param_value, expected_query_param_value, array_serialization_format', [ + ('string', 'query_param=string', SerializationFormats.INDEXED), + (500, 'query_param=500', SerializationFormats.INDEXED), + (500.12, 'query_param=500.12', SerializationFormats.INDEXED), + (date(1994, 2, 13), 'query_param=1994-02-13', SerializationFormats.INDEXED), + (ApiHelper.UnixDateTime.from_datetime(datetime(1994, 2, 13, 5, 30, 15)), + 'query_param=761117415', SerializationFormats.INDEXED), + (Base.get_http_datetime(datetime(1994, 2, 13, 5, 30, 15)), + 'query_param={}'.format(quote(Base.get_http_datetime_str(datetime(1994, 2, 13, 5, 30, 15)), safe='')), + SerializationFormats.INDEXED), + (Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15)), + 'query_param={}'.format(quote(Base.get_rfc3339_datetime_str(datetime(1994, 2, 13, 5, 30, 15)), safe='')), + SerializationFormats.INDEXED), + ([1, 2, 3, 4], 'query_param[0]=1&query_param[1]=2&query_param[2]=3&query_param[3]=4', + SerializationFormats.INDEXED), + ([1, 2, 3, 4], 'query_param[]=1&query_param[]=2&query_param[]=3&query_param[]=4', + SerializationFormats.UN_INDEXED), + ([1, 2, 3, 4], 'query_param=1&query_param=2&query_param=3&query_param=4', + SerializationFormats.PLAIN), + ([1, 2, 3, 4], 'query_param=1%2C2%2C3%2C4', SerializationFormats.CSV), + ([1, 2, 3, 4], 'query_param=1%7C2%7C3%7C4', SerializationFormats.PSV), + ([1, 2, 3, 4], 'query_param=1%092%093%094', SerializationFormats.TSV), + ({'key1': 'value1', 'key2': 'value2'}, 'query_param[key1]=value1&query_param[key2]=value2', + SerializationFormats.INDEXED), + ({'key1': 'value1', 'key2': [1, 2, 3, 4]}, + 'query_param[key1]=value1' + '&query_param[key2][0]=1' + '&query_param[key2][1]=2' + '&query_param[key2][2]=3' + '&query_param[key2][3]=4', SerializationFormats.INDEXED), + ({'key1': 'value1', 'key2': [1, 2, 3, {'key1': 'value1', 'key2': 'value2'}]}, + 'query_param[key1]=value1' + '&query_param[key2][0]=1' + '&query_param[key2][1]=2' + '&query_param[key2][2]=3' + '&query_param[key2][3][key1]=value1' + '&query_param[key2][3][key2]=value2', SerializationFormats.INDEXED), + (Base.employee_model(), + 'query_param[address]=street%20abc' + '&query_param[age]=27' + '&query_param[birthday]=1994-02-13' + '&query_param[birthtime]={}'.format(quote( + Base.get_rfc3339_datetime_str(datetime(1994, 2, 13, 5, 30, 15)), safe='')) + + '&query_param[name]=Bob' + '&query_param[uid]=1234567' + '&query_param[personType]=Empl' + '&query_param[department]=IT' + '&query_param[dependents][0][address]=street%20abc' + '&query_param[dependents][0][age]=12' + '&query_param[dependents][0][birthday]=1994-02-13' + '&query_param[dependents][0][birthtime]={}'.format(quote( + Base.get_rfc3339_datetime_str(datetime(1994, 2, 13, 5, 30, 15)), safe='')) + + '&query_param[dependents][0][name]=John' + '&query_param[dependents][0][uid]=7654321' + '&query_param[dependents][0][personType]=Per' + '&query_param[dependents][0][key1]=value1' + '&query_param[dependents][0][key2]=value2' + '&query_param[hiredAt]={}'.format(quote( + Base.get_http_datetime(datetime(1994, 2, 13, 5, 30, 15)), safe='')) + + '&query_param[joiningDay]=Monday' + '&query_param[salary]=30000' + '&query_param[workingDays][0]=Monday' + '&query_param[workingDays][1]=Tuesday' + '&query_param[key1]=value1' + '&query_param[key2]=value2', SerializationFormats.INDEXED) + ]) + @validate_call + def test_query_params( + self, input_query_param_value: Any, expected_query_param_value: str, + array_serialization_format: SerializationFormats + ): + http_request = self.new_request_builder \ + .http_method(HttpMethodEnum.GET) \ + .query_param(Parameter(key='query_param', value=input_query_param_value)) \ + .array_serialization_format(array_serialization_format) \ + .build(self.global_configuration) + + assert http_request.query_url == 'http://localhost:3000/test?{}'.format(expected_query_param_value) + + @pytest.mark.parametrize('input_additional_query_params_value, expected_additional_query_params_value', [ + ({'key1': 'value1', 'key2': 'value2'}, 'key1=value1&key2=value2') + ]) + @validate_call + def test_additional_query_params( + self, input_additional_query_params_value: Dict[str, str], expected_additional_query_params_value: str + ): + http_request = self.new_request_builder \ + .http_method(HttpMethodEnum.GET) \ + .additional_query_params(input_additional_query_params_value) \ + .build(self.global_configuration) + + assert http_request.query_url == 'http://localhost:3000/test?{}'.format(expected_additional_query_params_value) + + @pytest.mark.parametrize('input_local_header_param_value, expected_local_header_param_value', [ + ('string', {'header_param': 'string'}), + (500, {'header_param': 500}), + (500.12, {'header_param': 500.12}), + (str(date(1994, 2, 13)), {'header_param': '1994-02-13'}), + (ApiHelper.UnixDateTime.from_datetime(datetime(1994, 2, 13, 5, 30, 15)), + {'header_param': 761117415}), + (Base.get_http_datetime(datetime(1994, 2, 13, 5, 30, 15)), + {'header_param': '{}'.format(Base.get_http_datetime(datetime(1994, 2, 13, 5, 30, 15)))}), + (Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15)), + {'header_param': '{}'.format(Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15)))}), + ([1, 2, 3, 4], {'header_param': [1, 2, 3, 4]}) + ]) + @validate_call + def test_local_headers( + self, input_local_header_param_value: Any, expected_local_header_param_value: Dict[str, Any] + ): + http_request = self.new_request_builder \ + .http_method(HttpMethodEnum.GET) \ + .header_param(Parameter(key='header_param', value=input_local_header_param_value)) \ + .build(self.global_configuration) + + assert http_request.headers == expected_local_header_param_value + + @pytest.mark.parametrize('input_global_header_param_value, expected_global_header_param_value', [ + ('my-string', {'header_param': 'my-string'}), + (5000, {'header_param': 5000}), + (5000.12, {'header_param': 5000.12}), + (str(date(1998, 2, 13)), {'header_param': '1998-02-13'}), + (ApiHelper.UnixDateTime.from_datetime(datetime(1994, 2, 13, 5, 30, 15)), + {'header_param': 761117415}), + (Base.get_http_datetime(datetime(1994, 2, 13, 5, 30, 15)), + {'header_param': '{}'.format(Base.get_http_datetime(datetime(1994, 2, 13, 5, 30, 15)))}), + (Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15)), + {'header_param': '{}'.format(Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15)))}), + ([100, 200, 300, 400], {'header_param': [100, 200, 300, 400]}) + ]) + @validate_call + def test_global_headers( + self, input_global_header_param_value: Any, expected_global_header_param_value: Dict[str, Any] + ): + global_configuration = self.global_configuration + global_configuration.global_headers = {'header_param': input_global_header_param_value} + http_request = self.new_request_builder \ + .http_method(HttpMethodEnum.GET) \ + .build(global_configuration) + + assert http_request.headers == expected_global_header_param_value + + @pytest.mark.parametrize('input_additional_header_param_value, expected_additional_header_param_value', [ + ('my-string', {'header_param': 'my-string'}), + (5000, {'header_param': 5000}), + (5000.12, {'header_param': 5000.12}), + (str(date(1998, 2, 13)), {'header_param': '1998-02-13'}), + (ApiHelper.UnixDateTime.from_datetime(datetime(1994, 2, 13, 5, 30, 15)), + {'header_param': 761117415}), + (Base.get_http_datetime(datetime(1994, 2, 13, 5, 30, 15)), + {'header_param': '{}'.format(Base.get_http_datetime(datetime(1994, 2, 13, 5, 30, 15)))}), + (Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15)), + {'header_param': '{}'.format(Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15)))}), + ([100, 200, 300, 400], {'header_param': [100, 200, 300, 400]}) + ]) + @validate_call + def test_additional_headers( + self, input_additional_header_param_value: Any, expected_additional_header_param_value: Dict[str, Any] + ): + global_configuration = self.global_configuration + global_configuration.additional_headers = {'header_param': input_additional_header_param_value} + http_request = self.new_request_builder \ + .http_method(HttpMethodEnum.GET) \ + .build(global_configuration) + + assert http_request.headers == expected_additional_header_param_value + + @pytest.mark.parametrize('input_global_header_param_value,' + 'input_local_header_param_value,' + 'expected_header_param_value', [ + ('global_string', None, {'header_param': None}), + ('global_string', 'local_string', {'header_param': 'local_string'}) + ]) + @validate_call + def test_local_and_global_headers_precedence( + self, input_global_header_param_value: str, input_local_header_param_value: Optional[str], + expected_header_param_value: Dict[str, Any] + ): + global_configuration = self.global_configuration + global_configuration.global_headers = {'header_param': input_global_header_param_value} + http_request = self.new_request_builder \ + .http_method(HttpMethodEnum.GET) \ + .header_param(Parameter(key='header_param', value=input_local_header_param_value)) \ + .build(global_configuration) + + assert http_request.headers == expected_header_param_value + + @pytest.mark.parametrize('input_global_header_param_value,' + 'input_local_header_param_value,' + 'input_additional_header_param_value,' + 'expected_header_param_value', [ + ('global_string', 'local_string', 'additional_string', + {'header_param': 'additional_string'}), + ('global_string', 'local_string', None, + {'header_param': None}) + ]) + @validate_call + def test_all_headers_precedence( + self, input_global_header_param_value: str, input_local_header_param_value: str, + input_additional_header_param_value: Optional[str], expected_header_param_value: Dict[str, Any] + ): + global_configuration = self.global_configuration + global_configuration.global_headers = {'header_param': input_global_header_param_value} + global_configuration.additional_headers = {'header_param': input_additional_header_param_value} + http_request = self.new_request_builder \ + .http_method(HttpMethodEnum.GET) \ + .header_param(Parameter(key='header_param', value=input_local_header_param_value)) \ + .build(global_configuration) + + assert http_request.headers == expected_header_param_value + + @validate_call + def test_useragent_header(self): + engines: List[str] = ['CPython', 'Jython', 'JPython', 'IronPython', 'PyPy', 'RubyPython', 'AnacondaPython'] + operating_systems: List[str] = ['Linux', 'Windows', 'Darwin', 'FreeBSD', 'OpenBSD', 'macOS'] + + http_request = self.new_request_builder \ + .http_method(HttpMethodEnum.GET) \ + .build(self.global_configuration_with_useragent) + [lang, version, engine, _, osInfo] = http_request.headers['user-agent'].split('|') + + assert lang == 'Python' and version == '31.8.0' and engine in engines and osInfo in operating_systems + + @pytest.mark.parametrize('input_form_param_value, expected_form_param_value, array_serialization_format', [ + ('string', [('form_param', 'string')], SerializationFormats.INDEXED), + (500, [('form_param', 500)], SerializationFormats.INDEXED), + (500.12, [('form_param', 500.12)], SerializationFormats.INDEXED), + (str(date(1994, 2, 13)), [('form_param', '1994-02-13')], SerializationFormats.INDEXED), + (ApiHelper.UnixDateTime.from_datetime(datetime(1994, 2, 13, 5, 30, 15)), + [('form_param', 761117415)], SerializationFormats.INDEXED), + (Base.get_http_datetime(datetime(1994, 2, 13, 5, 30, 15)), + [('form_param', Base.get_http_datetime(datetime(1994, 2, 13, 5, 30, 15)))], SerializationFormats.INDEXED), + (Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15)), + [('form_param', Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15)))], SerializationFormats.INDEXED), + ([1, 2, 3, 4], [('form_param[0]', 1), ('form_param[1]', 2), ('form_param[2]', 3), ('form_param[3]', 4)], + SerializationFormats.INDEXED), + ([1, 2, 3, 4], [('form_param[]', 1), ('form_param[]', 2), ('form_param[]', 3), ('form_param[]', 4)], + SerializationFormats.UN_INDEXED), + ([1, 2, 3, 4], [('form_param', 1), ('form_param', 2), ('form_param', 3), ('form_param', 4)], + SerializationFormats.PLAIN), + ({'key1': 'value1', 'key2': 'value2'}, [('form_param[key1]', 'value1'), ('form_param[key2]', 'value2')], + SerializationFormats.INDEXED), + ({'key1': 'value1', 'key2': [1, 2, 3, 4]}, + [('form_param[key1]', 'value1'), ('form_param[key2][0]', 1), ('form_param[key2][1]', 2), + ('form_param[key2][2]', 3), ('form_param[key2][3]', 4)], SerializationFormats.INDEXED), + ({'key1': 'value1', 'key2': [1, 2, 3, {'key1': 'value1', 'key2': 'value2'}]}, + [('form_param[key1]', 'value1'), ('form_param[key2][0]', 1), ('form_param[key2][1]', 2), + ('form_param[key2][2]', 3), ('form_param[key2][3][key1]', 'value1'), ('form_param[key2][3][key2]', 'value2')], + SerializationFormats.INDEXED), + (Base.employee_model(), + [('form_param[address]', 'street abc'), ('form_param[age]', 27), ('form_param[birthday]', '1994-02-13'), + ('form_param[birthtime]', Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15), True)), + ('form_param[name]', 'Bob'), + ('form_param[uid]', '1234567'), + ('form_param[personType]', 'Empl'), + ('form_param[department]', 'IT'), ('form_param[dependents][0][address]', 'street abc'), + ('form_param[dependents][0][age]', 12), ('form_param[dependents][0][birthday]', '1994-02-13'), + ('form_param[dependents][0][birthtime]', Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15), True)), + ('form_param[dependents][0][name]', 'John'), ('form_param[dependents][0][uid]', '7654321'), + ('form_param[dependents][0][personType]', 'Per'), ('form_param[dependents][0][key1]', 'value1'), + ('form_param[dependents][0][key2]', 'value2'), + ('form_param[hiredAt]', Base.get_http_datetime(datetime(1994, 2, 13, 5, 30, 15), True)), + ('form_param[joiningDay]', 'Monday'), ('form_param[salary]', 30000), ('form_param[workingDays][0]', 'Monday'), + ('form_param[workingDays][1]', 'Tuesday'), ('form_param[key1]', 'value1'), + ('form_param[key2]', 'value2')], SerializationFormats.INDEXED) + ]) + @validate_call + def test_form_params( + self, input_form_param_value: Any, expected_form_param_value: List[Tuple[str, Any]], + array_serialization_format: SerializationFormats + ): + http_request = self.new_request_builder \ + .http_method(HttpMethodEnum.POST) \ + .form_param(Parameter(key='form_param', value=input_form_param_value)) \ + .array_serialization_format(array_serialization_format) \ + .build(self.global_configuration) + + assert http_request.parameters == expected_form_param_value + + @pytest.mark.parametrize('input_additional_form_param_value, expected_additional_form_param_value', [ + ({'key1': 'value1', 'key2': 'value2'}, [('key1', 'value1'), ('key2', 'value2')]) + ]) + @validate_call + def test_addition_form_params( + self, input_additional_form_param_value: Dict[str, str], + expected_additional_form_param_value: List[Tuple[str, str]] + ): + http_request = self.new_request_builder \ + .http_method(HttpMethodEnum.POST) \ + .additional_form_params(input_additional_form_param_value) \ + .build(self.global_configuration) + + assert http_request.parameters == expected_additional_form_param_value + + @pytest.mark.parametrize('input_form_param_value,' + 'input_additional_form_param_value,' + 'expected_form_param_value', [ + ({'key1': 'value1', 'key2': 'value2'}, + {'additional_key1': 'additional_value1', 'additional_key2': 'additional_value2'}, + [('form_param[key1]', 'value1'), ('form_param[key2]', 'value2'), + ('additional_key1', 'additional_value1'), + ('additional_key2', 'additional_value2')]) + ]) + @validate_call + def test_form_params_with_additional_form_params( + self, input_form_param_value: Dict[str, str], input_additional_form_param_value: Dict[str, str], + expected_form_param_value: List[Tuple[str, str]] + ): + http_request = self.new_request_builder \ + .http_method(HttpMethodEnum.POST) \ + .form_param(Parameter(key='form_param', value=input_form_param_value)) \ + .additional_form_params(input_additional_form_param_value) \ + .build(self.global_configuration) + + assert http_request.parameters == expected_form_param_value + + @pytest.mark.parametrize('input_body_param_value, expected_body_param_value', [ + ('string', 'string'), + (500, 500), + (500.12, 500.12), + (str(date(1994, 2, 13)), '1994-02-13'), + (ApiHelper.UnixDateTime.from_datetime(datetime(1994, 2, 13, 5, 30, 15)), 761117415), + (Base.get_http_datetime(datetime(1994, 2, 13, 5, 30, 15)), + Base.get_http_datetime(datetime(1994, 2, 13, 5, 30, 15))), + (Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15)), + Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15))) + ]) + @validate_call + def test_json_body_params_without_serializer(self, input_body_param_value: Any, expected_body_param_value: Any): + http_request = self.new_request_builder \ + .http_method(HttpMethodEnum.POST) \ + .body_param(Parameter(value=input_body_param_value)) \ + .build(self.global_configuration) + + assert http_request.parameters == expected_body_param_value + + @pytest.mark.parametrize('input_body_param_value1, input_body_param_value2, expected_body_param_value', [ + ('string1', 'string2', '{"param1": "string1", "param2": "string2"}'), + (100, 200, '{"param1": 100, "param2": 200}'), + (100.12, 200.12, '{"param1": 100.12, "param2": 200.12}') + ]) + @validate_call + def test_multiple_json_body_params_with_serializer(self, input_body_param_value1: Any, input_body_param_value2: Any, + expected_body_param_value: str): + http_request = self.new_request_builder \ + .http_method(HttpMethodEnum.POST) \ + .body_param(Parameter(key='param1', value=input_body_param_value1)) \ + .body_param(Parameter(key='param2', value=input_body_param_value2)) \ + .body_serializer(ApiHelper.json_serialize) \ + .build(self.global_configuration) + + assert http_request.parameters == expected_body_param_value + + @pytest.mark.parametrize('input_body_param_value, expected_body_param_value', [ + ([1, 2, 3, 4], '[1, 2, 3, 4]'), + ({'key1': 'value1', 'key2': 'value2'}, '{"key1": "value1", "key2": "value2"}'), + ({'key1': 'value1', 'key2': [1, 2, 3, 4]}, '{"key1": "value1", "key2": [1, 2, 3, 4]}'), + ({'key1': 'value1', 'key2': [1, 2, 3, {'key1': 'value1', 'key2': 'value2'}]}, + '{"key1": "value1", "key2": [1, 2, 3, {"key1": "value1", "key2": "value2"}]}'), + (Base.employee_model(), Base.get_serialized_employee()) + ]) + @validate_call + def test_json_body_params_with_serializer(self, input_body_param_value: Any, expected_body_param_value: str): + http_request = self.new_request_builder \ + .http_method(HttpMethodEnum.POST) \ + .body_param(Parameter(value=input_body_param_value)) \ + .body_serializer(ApiHelper.json_serialize) \ + .build(self.global_configuration) + + assert http_request.parameters == expected_body_param_value + + @pytest.mark.parametrize('input_value, expected_value', [ + (100.0, '100.0'), + (True, 'true') + ]) + @validate_call + def test_type_combinator_validation_in_request(self, input_value: Any, expected_value: str): + http_request = self.new_request_builder \ + .http_method(HttpMethodEnum.POST) \ + .body_param(Parameter( + validator=lambda value: UnionTypeLookUp.get('ScalarTypes').validate(value=value).is_valid, + value=input_value)) \ + .body_serializer(ApiHelper.json_serialize) \ + .build(self.global_configuration) + + assert http_request.parameters == expected_value + + @pytest.mark.parametrize('input_body_param_value, expected_body_param_value', [ + (Base.xml_model(), '' + 'Hey! I am being tested.' + '5000' + 'false' + '' + 'a' + 'b' + 'c' + '' + '') + ]) + @validate_call + def test_xml_body_param_with_serializer(self, input_body_param_value: XMLModel, expected_body_param_value: str): + if sys.version_info[1] == 7: + expected_body_param_value = expected_body_param_value.replace( + 'string="String" number="10000" boolean="false">', + 'boolean="false" number="10000" string="String">') + http_request = self.new_request_builder \ + .http_method(HttpMethodEnum.POST) \ + .xml_attributes(XmlAttributes(value=input_body_param_value, root_element_name='AttributesAndElements')) \ + .body_serializer(XmlHelper.serialize_to_xml) \ + .build(self.global_configuration) + + assert http_request.parameters == expected_body_param_value + + @pytest.mark.parametrize('input_body_param_value, expected_body_param_value', [ + ([Base.xml_model(), Base.xml_model()], + '' + '' + 'Hey! I am being tested.' + '5000' + 'false' + '' + 'a' + 'b' + 'c' + '' + '' + '' + 'Hey! I am being tested.' + '5000' + 'false' + '' + 'a' + 'b' + 'c' + '' + '' + '') + ]) + @validate_call + def test_xml_array_body_param_with_serializer( + self, input_body_param_value: List[XMLModel], expected_body_param_value: str + ): + if sys.version_info[1] == 7: + expected_body_param_value = expected_body_param_value.replace( + 'string="String" number="10000" boolean="false">', + 'boolean="false" number="10000" string="String">') + http_request = self.new_request_builder \ + .http_method(HttpMethodEnum.POST) \ + .xml_attributes(XmlAttributes(value=input_body_param_value, + root_element_name='arrayOfModels', + array_item_name='item')) \ + .body_serializer(XmlHelper.serialize_list_to_xml) \ + .build(self.global_configuration) + + assert http_request.parameters == expected_body_param_value + + @pytest.mark.parametrize("input_body_param_value, expected_input_file, expected_content_type", [ + (FileWrapper(Base.read_file('apimatic.png'), 'image/png'), + Base.read_file('apimatic.png'), 'image/png')]) + @validate_call(config=dict(arbitrary_types_allowed=True)) + def test_file_as_body_param( + self, input_body_param_value: FileWrapper, expected_input_file: FileType, expected_content_type: str + ): + http_request = self.new_request_builder \ + .http_method(HttpMethodEnum.POST) \ + .header_param(Parameter(key='content-type', value='application/xml')) \ + .body_param(Parameter(value=input_body_param_value)) \ + .build(self.global_configuration) + input_file = None + try: + input_file = http_request.parameters + + assert input_file.read() == expected_input_file.read() \ + and http_request.headers['content-type'] == expected_content_type + finally: + self.close_file(input_file) + self.close_file(expected_input_file) + + @pytest.mark.parametrize( + "input_multipart_param_value1, input_default_content_type1,input_multipart_param_value2, input_default_content_type2,expected_file, expected_file_content_type, expected_model, expected_model_content_type", + [ + (Base.read_file('apimatic.png'), 'image/png', + Base.employee_model(), 'application/json', + Base.read_file('apimatic.png'), 'image/png', + Base.get_serialized_employee(), 'application/json') + ]) + @validate_call(config=dict(arbitrary_types_allowed=True)) + def test_multipart_request_without_file_wrapper( + self, input_multipart_param_value1: FileType, input_default_content_type1: str, + input_multipart_param_value2: Employee, input_default_content_type2: str, + expected_file: FileType, expected_file_content_type: str, + expected_model: str, expected_model_content_type: str + ): + http_request = self.new_request_builder \ + .http_method(HttpMethodEnum.POST) \ + .multipart_param(Parameter(key='file_wrapper', value=input_multipart_param_value1, + default_content_type=input_default_content_type1)) \ + .multipart_param(Parameter(key='model', value=ApiHelper.json_serialize(input_multipart_param_value2), + default_content_type=input_default_content_type2)) \ + .build(self.global_configuration) + actual_file = None + try: + actual_file = http_request.files['file_wrapper'][1] + actual_file_content_type = http_request.files['file_wrapper'][2] + actual_model = http_request.files['model'][1] + actual_model_content_type = http_request.files['model'][2] + + assert actual_file.read() == expected_file.read() \ + and actual_file_content_type == expected_file_content_type \ + and actual_model == expected_model \ + and actual_model_content_type == expected_model_content_type + finally: + self.close_file(actual_file) + self.close_file(expected_file) + + @pytest.mark.parametrize( + "input_multipart_param_value1, input_multipart_param_value2, input_default_content_type2, expected_file, expected_file_content_type,expected_model, expected_model_content_type", + [ + (FileWrapper(Base.read_file('apimatic.png'), 'image/png'), + Base.employee_model(), 'application/json', + Base.read_file('apimatic.png'), 'image/png', + Base.get_serialized_employee(), 'application/json') + ]) + @validate_call(config=dict(arbitrary_types_allowed=True)) + def test_multipart_request_with_file_wrapper( + self, input_multipart_param_value1: FileWrapper, + input_multipart_param_value2: Employee, input_default_content_type2: str, + expected_file: FileType, expected_file_content_type: str, + expected_model: str, expected_model_content_type: str + ): + + http_request = self.new_request_builder \ + .http_method(HttpMethodEnum.POST) \ + .multipart_param(Parameter(key='file', value=input_multipart_param_value1)) \ + .multipart_param(Parameter(key='model', value=ApiHelper.json_serialize(input_multipart_param_value2), + default_content_type=input_default_content_type2)) \ + .build(self.global_configuration) + + actual_file = None + try: + actual_file = http_request.files['file'][1] + actual_file_content_type = http_request.files['file'][2] + actual_model = http_request.files['model'][1] + actual_model_content_type = http_request.files['model'][2] + + assert actual_file.read() == expected_file.read() \ + and actual_file_content_type == expected_file_content_type \ + and actual_model == expected_model \ + and actual_model_content_type == expected_model_content_type + finally: + self.close_file(actual_file) + self.close_file(expected_file) + + @pytest.mark.parametrize('input_auth_scheme, expected_auth_header_key, expected_auth_header_value', [ + (Single('basic_auth'), 'Basic-Authorization', 'Basic {}'.format( + AuthHelper.get_base64_encoded_value('test_username', 'test_password'))), + (Single('bearer_auth'), 'Bearer-Authorization', 'Bearer 0b79bab50daca910b000d4f1a2b675d604257e42'), + (Single('custom_header_auth'), 'token', 'Qaws2W233WedeRe4T56G6Vref2') + ]) + @validate_call(config=dict(arbitrary_types_allowed=True)) + def test_header_authentication( + self, input_auth_scheme: Authentication, expected_auth_header_key: str, expected_auth_header_value: str + ): + http_request = self.new_request_builder \ + .http_method(HttpMethodEnum.POST) \ + .auth(input_auth_scheme) \ + .build(self.global_configuration_with_auth) + + assert http_request.headers[expected_auth_header_key] == expected_auth_header_value + + @validate_call + def test_query_authentication(self): + http_request = self.new_request_builder \ + .http_method(HttpMethodEnum.POST) \ + .auth(Single('custom_query_auth')) \ + .build(self.global_configuration_with_auth) + + assert http_request.query_url == 'http://localhost:3000/test?token=Qaws2W233WedeRe4T56G6Vref2&api-key=W233WedeRe4T56G6Vref2' + + @pytest.mark.parametrize('input_invalid_auth_scheme', [ + (Single('invalid')), + (Or('invalid_1', 'invalid_2')), + (And('invalid_1', 'invalid_2')) + ]) + @validate_call(config=dict(arbitrary_types_allowed=True)) + def test_invalid_key_authentication(self, input_invalid_auth_scheme: Authentication): + with pytest.raises(ValueError) as validation_error: + self.new_request_builder \ + .http_method(HttpMethodEnum.POST) \ + .auth(input_invalid_auth_scheme) \ + .build(self.global_configuration_with_auth) + + assert validation_error.value.args[0] == 'Auth key is invalid.' + + @pytest.mark.parametrize('input_auth_scheme, expected_request_headers', [ + (Or('basic_auth', 'custom_header_auth'), + { + 'Basic-Authorization': 'Basic dGVzdF91c2VybmFtZTp0ZXN0X3Bhc3N3b3Jk', + 'token': None + }), + (And('basic_auth', 'bearer_auth', 'custom_header_auth'), + { + 'Basic-Authorization': 'Basic dGVzdF91c2VybmFtZTp0ZXN0X3Bhc3N3b3Jk', + 'Bearer-Authorization': 'Bearer 0b79bab50daca910b000d4f1a2b675d604257e42', + 'token': 'Qaws2W233WedeRe4T56G6Vref2' + }), + (Or('basic_auth', And('bearer_auth', 'custom_header_auth')), + { + 'Basic-Authorization': 'Basic dGVzdF91c2VybmFtZTp0ZXN0X3Bhc3N3b3Jk', + 'Bearer-Authorization': None, + 'token': None + }), + (And('basic_auth', Or('bearer_auth', 'custom_header_auth')), + { + 'Basic-Authorization': 'Basic dGVzdF91c2VybmFtZTp0ZXN0X3Bhc3N3b3Jk', + 'Bearer-Authorization': 'Bearer 0b79bab50daca910b000d4f1a2b675d604257e42', + 'token': None + }), + (And('basic_auth', And('bearer_auth', 'custom_header_auth')), + { + 'Basic-Authorization': 'Basic dGVzdF91c2VybmFtZTp0ZXN0X3Bhc3N3b3Jk', + 'Bearer-Authorization': 'Bearer 0b79bab50daca910b000d4f1a2b675d604257e42', + 'token': 'Qaws2W233WedeRe4T56G6Vref2' + }), + (Or('basic_auth', Or('bearer_auth', 'custom_header_auth')), + { + 'Basic-Authorization': 'Basic dGVzdF91c2VybmFtZTp0ZXN0X3Bhc3N3b3Jk', + 'Bearer-Authorization': None, + 'token': None + }), + (Or('basic_auth', Or('custom_header_auth')), + { + 'Basic-Authorization': 'Basic dGVzdF91c2VybmFtZTp0ZXN0X3Bhc3N3b3Jk', + 'token': None + }), + (Or('basic_auth', And('custom_header_auth')), + { + 'Basic-Authorization': 'Basic dGVzdF91c2VybmFtZTp0ZXN0X3Bhc3N3b3Jk', + 'token': None + }), + ]) + @validate_call(config=dict(arbitrary_types_allowed=True)) + def test_success_case_of_multiple_authentications( + self, input_auth_scheme: Authentication, expected_request_headers: Dict[str, Optional[str]] + ): + http_request = self.new_request_builder \ + .http_method(HttpMethodEnum.POST) \ + .auth(input_auth_scheme) \ + .build(self.global_configuration_with_auth) + + for key in expected_request_headers.keys(): + assert http_request.headers.get(key) == expected_request_headers.get(key) + + @pytest.mark.parametrize('input_auth_scheme, expected_error_message', [ + (Or('basic_auth', 'bearer_auth', 'custom_header_auth'), + '[BasicAuth: username or password is undefined.] or ' + '[BearerAuth: access_token is undefined.] or ' + '[CustomHeaderAuthentication: token is undefined.]'), + (And('basic_auth', 'bearer_auth', 'custom_header_auth'), + '[BasicAuth: username or password is undefined.] and ' + '[BearerAuth: access_token is undefined.] and ' + '[CustomHeaderAuthentication: token is undefined.]'), + (Or('basic_auth', And('bearer_auth', 'custom_header_auth')), + '[BasicAuth: username or password is undefined.] or ' + '[BearerAuth: access_token is undefined.] and ' + '[CustomHeaderAuthentication: token is undefined.]'), + (And('basic_auth', Or('bearer_auth', 'custom_header_auth')), + '[BasicAuth: username or password is undefined.] and ' + '[BearerAuth: access_token is undefined.] or ' + '[CustomHeaderAuthentication: token is undefined.]'), + (And('basic_auth', And('bearer_auth', 'custom_header_auth')), + '[BasicAuth: username or password is undefined.] and ' + '[BearerAuth: access_token is undefined.] and ' + '[CustomHeaderAuthentication: token is undefined.]'), + (Or('basic_auth', Or('bearer_auth', 'custom_header_auth')), + '[BasicAuth: username or password is undefined.] or ' + '[BearerAuth: access_token is undefined.] or ' + '[CustomHeaderAuthentication: token is undefined.]'), + (Or(), ''), + (Or('basic_auth'), '[BasicAuth: username or password is undefined.]'), + (And(), ''), + (And( 'basic_auth'), '[BasicAuth: username or password is undefined.]') + ]) + @validate_call(config=dict(arbitrary_types_allowed=True)) + def test_failed_case_of_multiple_authentications( + self, input_auth_scheme: Authentication, expected_error_message: str + ): + with pytest.raises(AuthValidationException) as errors: + self.new_request_builder \ + .http_method(HttpMethodEnum.POST) \ + .auth(input_auth_scheme) \ + .build(self.global_configuration_with_uninitialized_auth_params) + + assert errors.value.args[0] == expected_error_message + + @pytest.mark.parametrize('input_auth_scheme, expected_auth_header_key,' + 'expected_auth_header_value', [ + (Or('basic_auth', 'custom_header_auth'), 'token', + 'Qaws2W233WedeRe4T56G6Vref2'), + (Or('custom_header_auth', And('basic_auth', 'custom_header_auth')), 'token', + 'Qaws2W233WedeRe4T56G6Vref2') + ]) + @validate_call(config=dict(arbitrary_types_allowed=True)) + def test_case_of_multiple_authentications( + self, input_auth_scheme: Authentication, expected_auth_header_key: str, expected_auth_header_value: str + ): + http_request = self.new_request_builder \ + .http_method(HttpMethodEnum.POST) \ + .auth(input_auth_scheme) \ + .build(self.global_configuration_with_partially_initialized_auth_params) + + assert http_request.headers[expected_auth_header_key] == expected_auth_header_value + assert http_request.headers.get('Authorization') is None + + @validate_call(config=dict(arbitrary_types_allowed=True)) + def close_file(self, file: Optional[FileType]) -> None: + if file is not None: + file.close() \ No newline at end of file diff --git a/tests/apimatic_core/response_handler_tests/test_response_handler.py b/tests/apimatic_core/response_handler_tests/test_response_handler.py index c1109dc..e1ff96a 100644 --- a/tests/apimatic_core/response_handler_tests/test_response_handler.py +++ b/tests/apimatic_core/response_handler_tests/test_response_handler.py @@ -2,10 +2,12 @@ import pytest import sys +from apimatic_core_interfaces.formats.datetime_format import DateTimeFormat from apimatic_core_interfaces.http.http_response import HttpResponse -from typing import Type, Any, TypeVar, Optional, Callable, List +from typing import Type, Any, TypeVar, Optional, Callable, List, Union + +from pydantic import validate_call -from apimatic_core.types.datetime_format import DateTimeFormat from apimatic_core.utilities.api_helper import ApiHelper from apimatic_core.utilities.xml_helper import XmlHelper from tests.apimatic_core.base import Base @@ -21,6 +23,7 @@ class TestResponseHandler(Base): E = TypeVar("E", bound=BaseException) + @validate_call def test_nullify_404(self): http_response = (self.new_response_handler .is_nullify404(True) @@ -34,14 +37,16 @@ def test_nullify_404(self): (Base.response(status_code=429), GlobalTestException, 'Invalid response'), (Base.response(status_code=399), GlobalTestException, '3XX Global') ]) + @validate_call def test_global_error( - self, http_response: HttpResponse, expected_exception_type: Type[E], expected_error_message: str + self, http_response: HttpResponse, expected_exception_type: Type[APIException], expected_error_message: str ): with pytest.raises(expected_exception_type) as error: self.new_response_handler.handle(http_response, self.global_errors()) assert error.value.reason == expected_error_message + @validate_call def test_local_error(self): with pytest.raises(LocalTestException) as error: self.new_response_handler.local_error(404, 'Not Found', LocalTestException) \ @@ -49,6 +54,7 @@ def test_local_error(self): assert error.value.reason == 'Not Found' + @validate_call def test_default_local_error(self): with pytest.raises(LocalTestException) as error: self.new_response_handler.local_error(404, 'Not Found', LocalTestException) \ @@ -62,8 +68,9 @@ def test_default_local_error(self): (Base.response(status_code=443), LocalTestException, '4XX local'), (Base.response(status_code=522), LocalTestException, '522 local') ]) + @validate_call def test_default_range_local_error( - self, http_response: HttpResponse, expected_exception_type: Type[E], expected_error_message: str): + self, http_response: HttpResponse, expected_exception_type: Type[APIException], expected_error_message: str): with pytest.raises(expected_exception_type) as error: self.new_response_handler.local_error(522, '522 local', LocalTestException) \ .local_error('5XX', '5XX local', APIException) \ @@ -72,6 +79,7 @@ def test_default_range_local_error( assert error.value.reason == expected_error_message + @validate_call def test_local_error_with_body(self): with pytest.raises(LocalTestException) as error: self.new_response_handler.local_error(404, 'Not Found', LocalTestException) \ @@ -85,6 +93,7 @@ def test_local_error_with_body(self): and error.value.server_message == 'Test message from server' \ and error.value.secret_message_for_endpoint == 'This is test error message' + @validate_call def test_local_error_template_message(self): with pytest.raises(LocalTestException) as error: self.new_response_handler.local_error_template(404, 'error_code => {$statusCode}, ' @@ -103,6 +112,7 @@ def test_local_error_template_message(self): 'header => application/json, ' \ 'body => 5001 - Test message from server - This is test error message' + @validate_call def test_global_error_with_body(self): with pytest.raises(NestedModelException) as error: self.new_response_handler.local_error(404, 'Not Found', LocalTestException) \ @@ -119,10 +129,12 @@ def test_global_error_with_body(self): assert error.value.server_code == 5001 \ and error.value.server_message == 'Test message from server' \ + and error.value.model is not None \ and error.value.model.field == 'Test field' \ and error.value.model.name == 'Test name' \ and error.value.model.address == 'Test address' + @validate_call def test_global_error_template_message(self): with pytest.raises(NestedModelException) as error: self.new_response_handler.local_error(404, 'Not Found', LocalTestException) \ @@ -134,6 +146,7 @@ def test_global_error_template_message(self): assert error.value.reason == 'global error message -> error_code => 412, header => ,' \ ' body => 5001 - Test message from server - Test name' + @validate_call def test_local_error_precedence(self): with pytest.raises(LocalTestException) as error: self.new_response_handler.local_error(400, '400 Local', LocalTestException) \ @@ -141,6 +154,7 @@ def test_local_error_precedence(self): assert error.value.reason == '400 Local' + @validate_call def test_global_error_precedence(self): with pytest.raises(GlobalTestException) as error: self.new_response_handler.local_error(404, 'Not Found', LocalTestException) \ @@ -153,7 +167,10 @@ def test_global_error_precedence(self): (Base.response(text=500), 500), (Base.response(text=500.124), 500.124) ]) - def test_simple_response_body(self, input_http_response: HttpResponse, expected_response_body: str): + @validate_call + def test_simple_response_body( + self, input_http_response: HttpResponse, expected_response_body: Union[int, float, str] + ): http_response = self.new_response_handler.handle(input_http_response, self.global_errors()) assert http_response == expected_response_body @@ -163,6 +180,7 @@ def test_simple_response_body(self, input_http_response: HttpResponse, expected_ (Base.response(text='500'), int, int, 500), (Base.response(text=500), str, str, '500') ]) + @validate_call def test_simple_response_body_with_convertor( self, input_http_response: HttpResponse, input_response_convertor: Optional[Callable[[Any], Any]], expected_response_body_type: Type, expected_response_body: Any): @@ -179,11 +197,13 @@ def test_simple_response_body_with_convertor( (Base.response(text=''), None), (Base.response(text=' '), None) ]) + @validate_call def test_custom_type_response_body(self, input_http_response: HttpResponse, expected_response_body: Optional[str]): http_response = self.new_response_handler \ .deserializer(ApiHelper.json_deserialize) \ .deserialize_into(Employee.model_validate) \ .handle(input_http_response, self.global_errors()) + assert ApiHelper.json_serialize(http_response) == expected_response_body @pytest.mark.parametrize('input_http_response, expected_response_body', [ @@ -195,10 +215,12 @@ def test_custom_type_response_body(self, input_http_response: HttpResponse, expe (Base.response(text=''), None), (Base.response(text=' '), None) ]) + @validate_call def test_json_response_body(self, input_http_response: HttpResponse, expected_response_body: Optional[str]): http_response = self.new_response_handler \ .deserializer(ApiHelper.json_deserialize) \ .handle(input_http_response, self.global_errors()) + assert ApiHelper.json_serialize(http_response) == expected_response_body @pytest.mark.parametrize('input_http_response, expected_response_body', [ @@ -227,6 +249,7 @@ def test_json_response_body(self, input_http_response: HttpResponse, expected_re '' ''), ]) + @validate_call def test_xml_response_body_with_item_name(self, input_http_response: HttpResponse, expected_response_body: str): if sys.version_info[1] == 7: expected_response_body = expected_response_body.replace( @@ -238,6 +261,7 @@ def test_xml_response_body_with_item_name(self, input_http_response: HttpRespons .deserialize_into(XMLModel) \ .xml_item_name('item') \ .handle(input_http_response, self.global_errors()) + assert XmlHelper.serialize_list_to_xml( http_response, 'arrayOfModels', 'item') == expected_response_body @@ -254,6 +278,7 @@ def test_xml_response_body_with_item_name(self, input_http_response: HttpRespons '' ''), ]) + @validate_call def test_xml_response_body_without_item_name(self, input_http_response: HttpResponse, expected_response_body: str): if sys.version_info[1] == 7: expected_response_body = expected_response_body.replace( @@ -264,6 +289,7 @@ def test_xml_response_body_without_item_name(self, input_http_response: HttpResp .deserializer(XmlHelper.deserialize_xml) \ .deserialize_into(XMLModel) \ .handle(input_http_response, self.global_errors()) + assert XmlHelper.serialize_to_xml(http_response, 'AttributesAndElements') == expected_response_body @pytest.mark.parametrize('input_http_response, expected_response_body', [ @@ -272,11 +298,13 @@ def test_xml_response_body_without_item_name(self, input_http_response: HttpResp (Base.response(text='{"key1": "value1", "key2": [1, 2, 3, {"key1": "value1", "key2": "value2"}]}'), '{"key1": "value1", "key2": [1, 2, 3, {"key1": "value1", "key2": "value2"}]}') ]) + @validate_call def test_api_response(self, input_http_response: HttpResponse, expected_response_body: str): api_response = self.new_response_handler \ .deserializer(ApiHelper.json_deserialize) \ .is_api_response(True) \ .handle(input_http_response, self.global_errors()) + assert ApiHelper.json_serialize(api_response.body) == expected_response_body assert api_response.is_success() is True assert api_response.is_error() is False @@ -289,6 +317,7 @@ def test_api_response(self, input_http_response: HttpResponse, expected_response (Base.response(text='{"key1": "value1", "key2": "value2", "errors": ["e1", "e2"], "cursor": "Test cursor"}'), '{"key1": "value1", "key2": "value2", "errors": ["e1", "e2"], "cursor": "Test cursor"}', ['e1', 'e2']) ]) + @validate_call def test_api_response_convertor( self, input_http_response: HttpResponse, expected_response_body: str, expected_error_list: List[str]): api_response = self.new_response_handler \ @@ -296,6 +325,7 @@ def test_api_response_convertor( .is_api_response(True) \ .convertor(ApiResponse.create) \ .handle(input_http_response, self.global_errors()) + assert isinstance(api_response, ApiResponse) and \ ApiHelper.json_serialize(api_response.body) == expected_response_body \ and api_response.errors == expected_error_list @@ -306,6 +336,7 @@ def test_api_response_convertor( (Base.response(text=''), None, None), (Base.response(text=' '), None, None) ]) + @validate_call def test_api_response_with_errors( self, input_http_response: HttpResponse, expected_response_body: Optional[str], expected_error_list: Optional[List[str]]): @@ -313,6 +344,7 @@ def test_api_response_with_errors( .deserializer(ApiHelper.json_deserialize) \ .is_api_response(True) \ .handle(input_http_response, self.global_errors()) + assert ApiHelper.json_serialize(api_response.body) == expected_response_body \ and api_response.errors == expected_error_list @@ -340,13 +372,15 @@ def test_api_response_with_errors( DateTimeFormat.RFC3339_DATE_TIME, [datetime(1996, 2, 13, 5, 30, 15), datetime(1996, 2, 13, 5, 30, 15)]) ]) + @validate_call def test_date_time_response_body( self, input_http_response: HttpResponse, input_date_time_format: DateTimeFormat, - expected_response_body: datetime): + expected_response_body: Union[datetime, List[datetime]]): http_response = self.new_response_handler \ .deserializer(ApiHelper.datetime_deserialize) \ .datetime_format(input_date_time_format) \ .handle(input_http_response, self.global_errors()) + assert http_response == expected_response_body @pytest.mark.parametrize('input_http_response, expected_response_body', [ @@ -354,8 +388,12 @@ def test_date_time_response_body( (Base.response(text=ApiHelper.json_serialize([str(date(1994, 2, 13)), str(date(1994, 2, 13))])), [date(1994, 2, 13), date(1994, 2, 13)]) ]) - def test_date_response_body(self, input_http_response: HttpResponse, expected_response_body: date): + @validate_call + def test_date_response_body( + self, input_http_response: HttpResponse, expected_response_body: Union[date, List[date]] + ): http_response = self.new_response_handler \ .deserializer(ApiHelper.date_deserialize) \ .handle(input_http_response, self.global_errors()) + assert http_response == expected_response_body diff --git a/tests/apimatic_core/response_handler_tests/test_response_handler.py~ b/tests/apimatic_core/response_handler_tests/test_response_handler.py~ new file mode 100644 index 0000000..455f905 --- /dev/null +++ b/tests/apimatic_core/response_handler_tests/test_response_handler.py~ @@ -0,0 +1,399 @@ +from datetime import datetime, date +import pytest +import sys + +from apimatic_core_interfaces.formats.datetime_format import DateTimeFormat +from apimatic_core_interfaces.http.http_response import HttpResponse +from typing import Type, Any, TypeVar, Optional, Callable, List, Union + +from pydantic import validate_call + +from apimatic_core.utilities.api_helper import ApiHelper +from apimatic_core.utilities.xml_helper import XmlHelper +from tests.apimatic_core.base import Base +from tests.apimatic_core.mocks.exceptions.api_exception import APIException +from tests.apimatic_core.mocks.exceptions.global_test_exception import GlobalTestException +from tests.apimatic_core.mocks.exceptions.local_test_exception import LocalTestException +from tests.apimatic_core.mocks.exceptions.nested_model_exception import NestedModelException +from tests.apimatic_core.mocks.models.api_response import ApiResponse +from tests.apimatic_core.mocks.models.person import Employee +from tests.apimatic_core.mocks.models.xml_model import XMLModel + + +class TestResponseHandler(Base): + E = TypeVar("E", bound=BaseException) + + @validate_call + def test_nullify_404(self): + http_response = (self.new_response_handler + .is_nullify404(True) + .handle(self.response(status_code=404), self.global_errors())) + + assert http_response is None + + @pytest.mark.parametrize('http_response, expected_exception_type, expected_error_message', [ + (Base.response(status_code=400), GlobalTestException, '400 Global'), + (Base.response(status_code=412), NestedModelException, 'Precondition Failed'), + (Base.response(status_code=429), GlobalTestException, 'Invalid response'), + (Base.response(status_code=399), GlobalTestException, '3XX Global') + ]) + @validate_call + def test_global_error( + self, http_response: HttpResponse, expected_exception_type: Type[E], expected_error_message: str + ): + with pytest.raises(expected_exception_type) as error: + self.new_response_handler.handle(http_response, self.global_errors()) + + assert error.value.reason == expected_error_message + + @validate_call + def test_local_error(self): + with pytest.raises(LocalTestException) as error: + self.new_response_handler.local_error(404, 'Not Found', LocalTestException) \ + .handle(self.response(status_code=404), self.global_errors()) + + assert error.value.reason == 'Not Found' + + @validate_call + def test_default_local_error(self): + with pytest.raises(LocalTestException) as error: + self.new_response_handler.local_error(404, 'Not Found', LocalTestException) \ + .local_error('default', 'Response Not OK', LocalTestException) \ + .handle(self.response(status_code=412), self.global_errors()) + + assert error.value.reason == 'Response Not OK' + + @pytest.mark.parametrize('http_response, expected_exception_type, expected_error_message', [ + (Base.response(status_code=501), APIException, '5XX local'), + (Base.response(status_code=443), LocalTestException, '4XX local'), + (Base.response(status_code=522), LocalTestException, '522 local') + ]) + @validate_call + def test_default_range_local_error( + self, http_response: HttpResponse, expected_exception_type: Type[APIException], expected_error_message: str): + with pytest.raises(expected_exception_type) as error: + self.new_response_handler.local_error(522, '522 local', LocalTestException) \ + .local_error('5XX', '5XX local', APIException) \ + .local_error('4XX', '4XX local', LocalTestException) \ + .handle(http_response, self.global_errors()) + + assert error.value.reason == expected_error_message + + @validate_call + def test_local_error_with_body(self): + with pytest.raises(LocalTestException) as error: + self.new_response_handler.local_error(404, 'Not Found', LocalTestException) \ + .handle(self.response(status_code=404, + text='{"ServerCode": 5001, ' + '"ServerMessage": "Test message from server", ' + '"SecretMessageForEndpoint": "This is test error message"}'), + self.global_errors()) + + assert error.value.server_code == 5001 \ + and error.value.server_message == 'Test message from server' \ + and error.value.secret_message_for_endpoint == 'This is test error message' + + @validate_call + def test_local_error_template_message(self): + with pytest.raises(LocalTestException) as error: + self.new_response_handler.local_error_template(404, 'error_code => {$statusCode}, ' + 'header => {$response.header.accept}, ' + 'body => {$response.body#/ServerCode} - ' + '{$response.body#/ServerMessage} - ' + '{$response.body#/SecretMessageForEndpoint}', + LocalTestException) \ + .handle(self.response(status_code=404, text='{"ServerCode": 5001, "ServerMessage": ' + '"Test message from server", "SecretMessageForEndpoint": ' + '"This is test error message"}', + headers={'accept': 'application/json'}), + self.global_errors_with_template_message()) + + assert error.value.reason == 'error_code => 404, ' \ + 'header => application/json, ' \ + 'body => 5001 - Test message from server - This is test error message' + + @validate_call + def test_global_error_with_body(self): + with pytest.raises(NestedModelException) as error: + self.new_response_handler.local_error(404, 'Not Found', LocalTestException) \ + .handle(self.response(status_code=412, + text='{"ServerCode": 5001, ' + '"ServerMessage": "Test message from server", ' + '"model": { ' + '"field": "Test field", ' + '"name": "Test name", ' + '"address": "Test address"' + '}' + '}'), + self.global_errors()) + + assert error.value.server_code == 5001 \ + and error.value.server_message == 'Test message from server' \ + and error.value.model is not None \ + and error.value.model.field == 'Test field' \ + and error.value.model.name == 'Test name' \ + and error.value.model.address == 'Test address' + + @validate_call + def test_global_error_template_message(self): + with pytest.raises(NestedModelException) as error: + self.new_response_handler.local_error(404, 'Not Found', LocalTestException) \ + .handle(self.response(status_code=412, + text='{"ServerCode": 5001, "ServerMessage": "Test message from server", "model": ' + '{ "field": "Test field", "name": "Test name", "address": "Test address"}}'), + self.global_errors_with_template_message()) + + assert error.value.reason == 'global error message -> error_code => 412, header => ,' \ + ' body => 5001 - Test message from server - Test name' + + @validate_call + def test_local_error_precedence(self): + with pytest.raises(LocalTestException) as error: + self.new_response_handler.local_error(400, '400 Local', LocalTestException) \ + .handle(self.response(status_code=400), self.global_errors()) + + assert error.value.reason == '400 Local' + + @validate_call + def test_global_error_precedence(self): + with pytest.raises(GlobalTestException) as error: + self.new_response_handler.local_error(404, 'Not Found', LocalTestException) \ + .handle(self.response(status_code=400), self.global_errors()) + + assert error.value.reason == '400 Global' + + @pytest.mark.parametrize('input_http_response, expected_response_body', [ + (Base.response(text='This is a string response'), 'This is a string response'), + (Base.response(text=500), 500), + (Base.response(text=500.124), 500.124) + ]) + @validate_call + def test_simple_response_body( + self, input_http_response: HttpResponse, expected_response_body: Union[int, float, str] + ): + http_response = self.new_response_handler.handle(input_http_response, self.global_errors()) + + assert http_response == expected_response_body + + @pytest.mark.parametrize('input_http_response, input_response_convertor, expected_response_body_type, ' + 'expected_response_body', [ + (Base.response(text='500'), int, int, 500), + (Base.response(text=500), str, str, '500') + ]) + @validate_call + def test_simple_response_body_with_convertor( + self, input_http_response: HttpResponse, input_response_convertor: Optional[Callable[[Any], Any]], + expected_response_body_type: Type, expected_response_body: Any): + http_response = (self.new_response_handler + .convertor(input_response_convertor) + .handle(input_http_response, self.global_errors())) + + assert type(http_response) == expected_response_body_type and http_response == expected_response_body + + @pytest.mark.parametrize('input_http_response, expected_response_body', [ + (Base.response(text=ApiHelper.json_serialize(Base.employee_model())), + ApiHelper.json_serialize(Base.employee_model())), + (Base.response(), None), + (Base.response(text=''), None), + (Base.response(text=' '), None) + ]) + @validate_call + def test_custom_type_response_body(self, input_http_response: HttpResponse, expected_response_body: Optional[str]): + http_response = self.new_response_handler \ + .deserializer(ApiHelper.json_deserialize) \ + .deserialize_into(Employee.model_validate) \ + .handle(input_http_response, self.global_errors()) + + assert ApiHelper.json_serialize(http_response) == expected_response_body + + @pytest.mark.parametrize('input_http_response, expected_response_body', [ + (Base.response(text='[1, 2, 3, 4]'), '[1, 2, 3, 4]'), + (Base.response(text='{"key1": "value1", "key2": "value2"}'), '{"key1": "value1", "key2": "value2"}'), + (Base.response(text='{"key1": "value1", "key2": [1, 2, 3, {"key1": "value1", "key2": "value2"}]}'), + '{"key1": "value1", "key2": [1, 2, 3, {"key1": "value1", "key2": "value2"}]}'), + (Base.response(), None), + (Base.response(text=''), None), + (Base.response(text=' '), None) + ]) + @validate_call + def test_json_response_body(self, input_http_response: HttpResponse, expected_response_body: Optional[str]): + http_response = self.new_response_handler \ + .deserializer(ApiHelper.json_deserialize) \ + .handle(input_http_response, self.global_errors()) + + assert ApiHelper.json_serialize(http_response) == expected_response_body + + @pytest.mark.parametrize('input_http_response, expected_response_body', [ + (Base.response(text=XmlHelper.serialize_list_to_xml( + [Base.xml_model(), Base.xml_model()], 'arrayOfModels', 'item')), + '' + '' + 'Hey! I am being tested.' + '5000' + 'false' + '' + 'a' + 'b' + 'c' + '' + '' + '' + 'Hey! I am being tested.' + '5000' + 'false' + '' + 'a' + 'b' + 'c' + '' + '' + ''), + ]) + @validate_call + def test_xml_response_body_with_item_name(self, input_http_response: HttpResponse, expected_response_body: str): + if sys.version_info[1] == 7: + expected_response_body = expected_response_body.replace( + 'string="String" number="10000" boolean="false">', + 'boolean="false" number="10000" string="String">') + http_response = self.new_response_handler \ + .is_xml_response(True) \ + .deserializer(XmlHelper.deserialize_xml_to_list) \ + .deserialize_into(XMLModel) \ + .xml_item_name('item') \ + .handle(input_http_response, self.global_errors()) + + assert XmlHelper.serialize_list_to_xml( + http_response, 'arrayOfModels', 'item') == expected_response_body + + @pytest.mark.parametrize('input_http_response, expected_response_body', [ + (Base.response(text=XmlHelper.serialize_to_xml(Base.xml_model(), 'AttributesAndElements')), + '' + 'Hey! I am being tested.' + '5000' + 'false' + '' + 'a' + 'b' + 'c' + '' + ''), + ]) + @validate_call + def test_xml_response_body_without_item_name(self, input_http_response: HttpResponse, expected_response_body: str): + if sys.version_info[1] == 7: + expected_response_body = expected_response_body.replace( + 'string="String" number="10000" boolean="false">', + 'boolean="false" number="10000" string="String">') + http_response = self.new_response_handler \ + .is_xml_response(True) \ + .deserializer(XmlHelper.deserialize_xml) \ + .deserialize_into(XMLModel) \ + .handle(input_http_response, self.global_errors()) + + assert XmlHelper.serialize_to_xml(http_response, 'AttributesAndElements') == expected_response_body + + @pytest.mark.parametrize('input_http_response, expected_response_body', [ + (Base.response(text='[1, 2, 3, 4]'), '[1, 2, 3, 4]'), + (Base.response(text='{"key1": "value1", "key2": "value2"}'), '{"key1": "value1", "key2": "value2"}'), + (Base.response(text='{"key1": "value1", "key2": [1, 2, 3, {"key1": "value1", "key2": "value2"}]}'), + '{"key1": "value1", "key2": [1, 2, 3, {"key1": "value1", "key2": "value2"}]}') + ]) + @validate_call + def test_api_response(self, input_http_response: HttpResponse, expected_response_body: str): + api_response = self.new_response_handler \ + .deserializer(ApiHelper.json_deserialize) \ + .is_api_response(True) \ + .handle(input_http_response, self.global_errors()) + + assert ApiHelper.json_serialize(api_response.body) == expected_response_body + assert api_response.is_success() is True + assert api_response.is_error() is False + assert api_response.status_code == 200 + assert api_response.text == expected_response_body + assert api_response.reason_phrase is None + assert api_response.headers == {} + + @pytest.mark.parametrize('input_http_response, expected_response_body, expected_error_list', [ + (Base.response(text='{"key1": "value1", "key2": "value2", "errors": ["e1", "e2"], "cursor": "Test cursor"}'), + '{"key1": "value1", "key2": "value2", "errors": ["e1", "e2"], "cursor": "Test cursor"}', ['e1', 'e2']) + ]) + @validate_call + def test_api_response_convertor( + self, input_http_response: HttpResponse, expected_response_body: str, expected_error_list: List[str]): + api_response = self.new_response_handler \ + .deserializer(ApiHelper.json_deserialize) \ + .is_api_response(True) \ + .convertor(ApiResponse.create) \ + .handle(input_http_response, self.global_errors()) + + assert isinstance(api_response, ApiResponse) and \ + ApiHelper.json_serialize(api_response.body) == expected_response_body \ + and api_response.errors == expected_error_list + + @pytest.mark.parametrize('input_http_response, expected_response_body, expected_error_list', [ + (Base.response(text='{"key1": "value1", "key2": "value2", "errors": ["e1", "e2"]}'), + '{"key1": "value1", "key2": "value2", "errors": ["e1", "e2"]}', ['e1', 'e2']), + (Base.response(text=''), None, None), + (Base.response(text=' '), None, None) + ]) + @validate_call + def test_api_response_with_errors( + self, input_http_response: HttpResponse, expected_response_body: Optional[str], + expected_error_list: Optional[List[str]]): + api_response = self.new_response_handler \ + .deserializer(ApiHelper.json_deserialize) \ + .is_api_response(True) \ + .handle(input_http_response, self.global_errors()) + + assert ApiHelper.json_serialize(api_response.body) == expected_response_body \ + and api_response.errors == expected_error_list + + @pytest.mark.parametrize('input_http_response, input_date_time_format, expected_response_body', [ + (Base.response(text=ApiHelper.UnixDateTime.from_datetime(datetime(1994, 2, 13, 5, 30, 15))), + DateTimeFormat.UNIX_DATE_TIME, datetime(1994, 2, 13, 5, 30, 15)), + (Base.response(text=ApiHelper.HttpDateTime.from_datetime(datetime(1994, 2, 13, 5, 30, 15))), + DateTimeFormat.HTTP_DATE_TIME, datetime(1994, 2, 13, 5, 30, 15)), + (Base.response(text=ApiHelper.RFC3339DateTime.from_datetime(datetime(1994, 2, 13, 5, 30, 15))), + DateTimeFormat.RFC3339_DATE_TIME, datetime(1994, 2, 13, 5, 30, 15)), + + (Base.response( + text=ApiHelper.json_serialize([ApiHelper.UnixDateTime.from_datetime(datetime(1994, 2, 13, 5, 30, 15)), + ApiHelper.UnixDateTime.from_datetime(datetime(1994, 2, 13, 5, 30, 15))])), + DateTimeFormat.UNIX_DATE_TIME, [datetime(1994, 2, 13, 5, 30, 15), datetime(1994, 2, 13, 5, 30, 15)]), + + (Base.response( + text=ApiHelper.json_serialize([ApiHelper.HttpDateTime.from_datetime(datetime(1995, 2, 13, 5, 30, 15)), + ApiHelper.HttpDateTime.from_datetime(datetime(1995, 2, 13, 5, 30, 15))])), + DateTimeFormat.HTTP_DATE_TIME, [datetime(1995, 2, 13, 5, 30, 15), datetime(1995, 2, 13, 5, 30, 15)]), + + (Base.response( + text=ApiHelper.json_serialize([ApiHelper.RFC3339DateTime.from_datetime(datetime(1996, 2, 13, 5, 30, 15)), + ApiHelper.RFC3339DateTime.from_datetime(datetime(1996, 2, 13, 5, 30, 15))])), + DateTimeFormat.RFC3339_DATE_TIME, [datetime(1996, 2, 13, 5, 30, 15), datetime(1996, 2, 13, 5, 30, 15)]) + + ]) + @validate_call + def test_date_time_response_body( + self, input_http_response: HttpResponse, input_date_time_format: DateTimeFormat, + expected_response_body: Union[datetime, List[datetime]]): + http_response = self.new_response_handler \ + .deserializer(ApiHelper.datetime_deserialize) \ + .datetime_format(input_date_time_format) \ + .handle(input_http_response, self.global_errors()) + + assert http_response == expected_response_body + + @pytest.mark.parametrize('input_http_response, expected_response_body', [ + (Base.response(text=str(date(1994, 2, 13))), date(1994, 2, 13)), + (Base.response(text=ApiHelper.json_serialize([str(date(1994, 2, 13)), str(date(1994, 2, 13))])), + [date(1994, 2, 13), date(1994, 2, 13)]) + ]) + @validate_call + def test_date_response_body( + self, input_http_response: HttpResponse, expected_response_body: Union[date, List[date]] + ): + http_response = self.new_response_handler \ + .deserializer(ApiHelper.date_deserialize) \ + .handle(input_http_response, self.global_errors()) + + assert http_response == expected_response_body diff --git a/tests/apimatic_core/union_type_tests/test_any_of.py b/tests/apimatic_core/union_type_tests/test_any_of.py index ed11e89..c6a493d 100644 --- a/tests/apimatic_core/union_type_tests/test_any_of.py +++ b/tests/apimatic_core/union_type_tests/test_any_of.py @@ -1,6 +1,10 @@ from datetime import datetime, date +from typing import Any, List, Union + import pytest +from apimatic_core_interfaces.types.union_type import UnionType from apimatic_core_interfaces.types.union_type_context import UnionTypeContext +from pydantic import validate_call from apimatic_core.exceptions.anyof_validation_exception import AnyOfValidationException from apimatic_core.types.datetime_format import DateTimeFormat @@ -285,8 +289,11 @@ class TestAnyOf: UnionTypeContext(is_array=True)), LeafType(int, UnionTypeContext(is_array=True))], UnionTypeContext(is_array=True), False, None), ]) - def test_any_of_primitive_type(self, input_value, input_types, input_context, expected_validity, - expected_deserialized_value): + @validate_call(config=dict(arbitrary_types_allowed=True)) + def test_any_of_primitive_type( + self, input_value: Any, input_types: List[UnionType], input_context: UnionTypeContext, + expected_validity: bool, expected_deserialized_value: Any + ): try: union_type_result = AnyOf(input_types, input_context).validate(input_value) actual_is_valid = union_type_result.is_valid @@ -317,8 +324,13 @@ def test_any_of_primitive_type(self, input_value, input_types, input_context, ex ('1994-11-06', '1994-11-06', [LeafType(date), LeafType(datetime, UnionTypeContext(date_time_format=DateTimeFormat.RFC3339_DATE_TIME))], UnionTypeContext(), True, date(1994, 11, 6)) ]) - def test_any_of_date_and_datetime(self, input_value, input_date, input_types, input_context, expected_validity, expected_value): + @validate_call(config=dict(arbitrary_types_allowed=True)) + def test_any_of_date_and_datetime( + self, input_value: Any, input_date: Union[str, float, int], input_types: List[UnionType], + input_context: UnionTypeContext, expected_validity: bool, expected_value: Union[date, datetime] + ): union_type_result = AnyOf(input_types, input_context).validate(input_value) + assert union_type_result.is_valid == expected_validity actual_deserialized_value = union_type_result.deserialize(input_date) assert actual_deserialized_value == expected_value @@ -338,8 +350,12 @@ def test_any_of_date_and_datetime(self, input_value, input_date, input_types, in ({'key0': 1, 'key3': 2}, [LeafType(int, UnionTypeContext(is_nullable=True)), LeafType(str)], UnionTypeContext(is_dict=True), True, {'key0': 1, 'key3': 2}) ]) - def test_any_of_optional_nullable(self, input_value, input_types, input_context, expected_validity, expected_value): + @validate_call(config=dict(arbitrary_types_allowed=True)) + def test_any_of_optional_nullable( + self, input_value: Any, input_types: List[UnionType], input_context: UnionTypeContext, + expected_validity: bool, expected_value: Any): union_type_result = AnyOf(input_types, input_context).validate(input_value) + assert union_type_result.is_valid == expected_validity actual_deserialized_value = union_type_result.deserialize(input_value) assert actual_deserialized_value == expected_value @@ -643,8 +659,11 @@ def test_any_of_optional_nullable(self, input_value, input_types, input_context, UnionTypeContext(is_dict=True, is_array=True), True, {'key0': [Orbit(orbit_number_of_electrons=10), Atom(atom_number_of_electrons=2, atom_number_of_protons=10)], 'key1': [Orbit(orbit_number_of_electrons=12), Atom(atom_number_of_electrons=2, atom_number_of_protons=12)]}), ]) - def test_any_of_custom_type(self, input_value, input_types, input_context, expected_is_valid_output, - expected_deserialized_value_output): + @validate_call(config=dict(arbitrary_types_allowed=True)) + def test_any_of_custom_type( + self, input_value: Any, input_types: List[UnionType], input_context: UnionTypeContext, + expected_is_valid_output: bool, expected_deserialized_value_output: Any + ): try: union_type_result = AnyOf(input_types, input_context).validate(input_value) actual_is_valid = union_type_result.is_valid @@ -710,7 +729,11 @@ def test_any_of_custom_type(self, input_value, input_types, input_context, expec ('{"name": "sam", "weight": 5, "type": "deer", "kind": "hunter"}', [LeafType(dict), LeafType(dict)], UnionTypeContext(), True), ]) - def test_any_of_with_discriminator_custom_type(self, input_value, input_types, input_context, expected_output): + @validate_call(config=dict(arbitrary_types_allowed=True)) + def test_any_of_with_discriminator_custom_type( + self, input_value: Any, input_types: List[UnionType], input_context: UnionTypeContext, + expected_output: Any + ): try: deserialized_dict_input = ApiHelper.json_deserialize(input_value, as_dict=True) union_type_result = AnyOf(input_types, input_context).validate(deserialized_dict_input) @@ -785,7 +808,11 @@ def test_any_of_with_discriminator_custom_type(self, input_value, input_types, i LeafType(Days, UnionTypeContext(is_array=True))], UnionTypeContext(is_array=True), True, [[1, 2], ['Monday', 'Tuesday']]), ]) - def test_any_of_enum_type(self, input_value, input_types, input_context, expected_is_valid_output, expected_deserialized_value_output): + @validate_call(config=dict(arbitrary_types_allowed=True)) + def test_any_of_enum_type( + self, input_value: Any, input_types: List[UnionType], input_context: UnionTypeContext, + expected_is_valid_output: bool, expected_deserialized_value_output: Any + ): try: union_type_result = AnyOf(input_types, input_context).validate(input_value) actual_is_valid = union_type_result.is_valid @@ -806,7 +833,11 @@ def test_any_of_enum_type(self, input_value, input_types, input_context, expecte '{} \nActual Value: 100.5\nExpected Type: Any Of int, bool, str.'.format( UnionTypeHelper.NONE_MATCHED_ERROR_MESSAGE)) ]) - def test_any_of_validation_errors(self, input_value, input_types, input_context, expected_validation_message): + @validate_call(config=dict(arbitrary_types_allowed=True)) + def test_any_of_validation_errors( + self, input_value: Any, input_types: List[UnionType], input_context: UnionTypeContext, + expected_validation_message: str + ): with pytest.raises(AnyOfValidationException) as validation_error: AnyOf(input_types, input_context).validate(input_value) assert validation_error.value.message == expected_validation_message diff --git a/tests/apimatic_core/union_type_tests/test_one_of.py b/tests/apimatic_core/union_type_tests/test_one_of.py index 668271d..564a5f9 100644 --- a/tests/apimatic_core/union_type_tests/test_one_of.py +++ b/tests/apimatic_core/union_type_tests/test_one_of.py @@ -1,6 +1,10 @@ from datetime import datetime, date, timezone +from typing import Any, List, Union + import pytest +from apimatic_core_interfaces.types.union_type import UnionType from apimatic_core_interfaces.types.union_type_context import UnionTypeContext +from pydantic import validate_call from apimatic_core.exceptions.oneof_validation_exception import OneOfValidationException from apimatic_core.types.datetime_format import DateTimeFormat @@ -288,9 +292,11 @@ class TestOneOf: UnionTypeContext(is_array=True)), LeafType(int, UnionTypeContext(is_array=True))], UnionTypeContext(is_array=True), False, None), ]) - - def test_one_of_primitive_type(self, input_value, input_types, input_context, expected_is_valid_output, - expected_deserialized_value_output): + @validate_call(config=dict(arbitrary_types_allowed=True)) + def test_one_of_primitive_type( + self, input_value: Any, input_types: List[UnionType], input_context: UnionTypeContext, + expected_is_valid_output: bool, expected_deserialized_value_output: Any + ): try: union_type_result = OneOf(input_types, input_context).validate(input_value) actual_is_valid = union_type_result.is_valid @@ -303,27 +309,36 @@ def test_one_of_primitive_type(self, input_value, input_types, input_context, ex assert actual_deserialized_value == expected_deserialized_value_output @pytest.mark.parametrize( - 'input_value, input_types, input_context, expected_validity, expected_value', [ - (Base.get_rfc3339_datetime(datetime(1994, 11, 6, 8, 49, 37)), + 'input_value, input_date, input_types, input_context, expected_validity, expected_value', [ + (Base.get_rfc3339_datetime(datetime(1994, 11, 6, 8, 49, 37), False), + Base.get_rfc3339_datetime(datetime(1994, 11, 6, 8, 49, 37)), [LeafType(datetime, UnionTypeContext(date_time_format=DateTimeFormat.RFC3339_DATE_TIME)), LeafType(date)], UnionTypeContext(), True, datetime(1994, 11, 6, 8, 49, 37)), - (Base.get_http_datetime(datetime(1994, 11, 6, 8, 49, 37)), + (Base.get_http_datetime(datetime(1994, 11, 6, 8, 49, 37), False), + Base.get_http_datetime(datetime(1994, 11, 6, 8, 49, 37)), [LeafType(datetime, UnionTypeContext(date_time_format=DateTimeFormat.HTTP_DATE_TIME)), LeafType(date)], UnionTypeContext(), True, datetime(1994, 11, 6, 8, 49, 37)), - (1480809600, + (ApiHelper.UnixDateTime(datetime(1994, 11, 6, 8, 49, 37)), 1480809600, [LeafType(datetime, UnionTypeContext(date_time_format=DateTimeFormat.UNIX_DATE_TIME)), LeafType(date)], UnionTypeContext(), True, datetime.utcfromtimestamp(1480809600)), - (Base.get_rfc3339_datetime(datetime(1994, 11, 6, 8, 49, 37)), - [LeafType(datetime, UnionTypeContext(date_time_converter=ApiHelper.RFC3339DateTime, date_time_format=DateTimeFormat.RFC3339_DATE_TIME)), LeafType(date)], + (datetime(1994, 11, 6, 8, 49, 37), Base.get_rfc3339_datetime(datetime(1994, 11, 6, 8, 49, 37)), + [LeafType(datetime, UnionTypeContext(date_time_converter=ApiHelper.RFC3339DateTime, + date_time_format=DateTimeFormat.RFC3339_DATE_TIME)), LeafType(date)], UnionTypeContext(), True, datetime(1994, 11, 6, 8, 49, 37)), - ('1994-11-06', [LeafType(date), LeafType(datetime, UnionTypeContext(date_time_format=DateTimeFormat.RFC3339_DATE_TIME))], UnionTypeContext(), + ('1994-11-06', '1994-11-06', + [LeafType(date), LeafType(datetime, UnionTypeContext(date_time_format=DateTimeFormat.RFC3339_DATE_TIME))], + UnionTypeContext(), True, date(1994, 11, 6)) ]) - def test_one_of_date_and_datetime(self, input_value, input_types, input_context, expected_validity, expected_value): - union_type = OneOf(input_types, input_context) - union_type_result = union_type.validate(input_value) + @validate_call(config=dict(arbitrary_types_allowed=True)) + def test_any_of_date_and_datetime( + self, input_value: Any, input_date: Union[str, float, int], input_types: List[UnionType], + input_context: UnionTypeContext, expected_validity: bool, expected_value: Union[date, datetime] + ): + union_type_result = OneOf(input_types, input_context).validate(input_value) + assert union_type_result.is_valid == expected_validity - actual_deserialized_value = union_type_result.deserialize(input_value) + actual_deserialized_value = union_type_result.deserialize(input_date) assert actual_deserialized_value == expected_value @pytest.mark.parametrize( @@ -341,8 +356,13 @@ def test_one_of_date_and_datetime(self, input_value, input_types, input_context, ({'key0': 1, 'key3': 2}, [LeafType(int, UnionTypeContext(is_nullable=True)), LeafType(str)], UnionTypeContext(is_dict=True), True, {'key0': 1, 'key3': 2}) ]) - def test_one_of_optional_nullable(self, input_value, input_types, input_context, expected_validity, expected_value): + @validate_call(config=dict(arbitrary_types_allowed=True)) + def test_one_of_optional_nullable( + self, input_value: Any, input_types: List[UnionType], input_context: UnionTypeContext, + expected_validity: bool, expected_value: Any + ): union_type_result = OneOf(input_types, input_context).validate(input_value) + assert union_type_result.is_valid == expected_validity actual_deserialized_value = union_type_result.deserialize(input_value) assert actual_deserialized_value == expected_value @@ -646,8 +666,11 @@ def test_one_of_optional_nullable(self, input_value, input_types, input_context, UnionTypeContext(is_dict=True, is_array=True), True, {'key0': [Orbit(orbit_number_of_electrons=10), Atom(atom_number_of_electrons=2, atom_number_of_protons=10)], 'key1': [Orbit(orbit_number_of_electrons=12), Atom(atom_number_of_electrons=2, atom_number_of_protons=12)]}), ]) - def test_one_of_custom_type(self, input_value, input_types, input_context, expected_is_valid_output, - expected_deserialized_value_output): + @validate_call(config=dict(arbitrary_types_allowed=True)) + def test_one_of_custom_type( + self, input_value: Any, input_types: List[UnionType], input_context: UnionTypeContext, + expected_is_valid_output: bool, expected_deserialized_value_output: Any + ): try: union_type_result = OneOf(input_types, input_context).validate(input_value) actual_is_valid = union_type_result.is_valid @@ -713,7 +736,11 @@ def test_one_of_custom_type(self, input_value, input_types, input_context, expec ('{"name": "sam", "weight": 5, "type": "deer", "kind": "hunter"}', [LeafType(dict), LeafType(dict)], UnionTypeContext(), False), ]) - def test_one_of_with_discriminator_custom_type(self, input_value, input_types, input_context, expected_output): + @validate_call(config=dict(arbitrary_types_allowed=True)) + def test_one_of_with_discriminator_custom_type( + self, input_value: Any, input_types: List[UnionType], input_context: UnionTypeContext, + expected_output: Any + ): try: deserialized_dict_input = ApiHelper.json_deserialize(input_value, as_dict=True) union_type_result = OneOf(input_types, input_context).validate(deserialized_dict_input) @@ -788,8 +815,11 @@ def test_one_of_with_discriminator_custom_type(self, input_value, input_types, i LeafType(Days, UnionTypeContext(is_array=True))], UnionTypeContext(is_array=True), True, [[1, 2], ['Monday', 'Tuesday']]), ]) - def test_one_of_enum_type(self, input_value, input_types, input_context, expected_is_valid_output, - expected_deserialized_value_output): + @validate_call(config=dict(arbitrary_types_allowed=True)) + def test_one_of_enum_type( + self, input_value: Any, input_types: List[UnionType], input_context: UnionTypeContext, + expected_is_valid_output: bool, expected_deserialized_value_output: Any + ): try: union_type_result = OneOf(input_types, input_context).validate(input_value) actual_is_valid = union_type_result.is_valid @@ -813,7 +843,12 @@ def test_one_of_enum_type(self, input_value, input_types, input_context, expecte '{} \nActual Value: 100.5\nExpected Type: One Of int, bool, str.'.format( UnionTypeHelper.NONE_MATCHED_ERROR_MESSAGE)) ]) - def test_one_of_validation_errors(self, input_value, input_types, input_context, expected_validation_message): + @validate_call(config=dict(arbitrary_types_allowed=True)) + def test_one_of_validation_errors( + self, input_value: Any, input_types: List[UnionType], input_context: UnionTypeContext, + expected_validation_message: str + ): with pytest.raises(OneOfValidationException) as validation_error: OneOf(input_types, input_context).validate(input_value) + assert validation_error.value.message == expected_validation_message diff --git a/tests/apimatic_core/utility_tests/test_api_helper.py b/tests/apimatic_core/utility_tests/test_api_helper.py index 1154082..c88fe0a 100644 --- a/tests/apimatic_core/utility_tests/test_api_helper.py +++ b/tests/apimatic_core/utility_tests/test_api_helper.py @@ -1,8 +1,11 @@ from datetime import datetime, date +from typing import Optional, Any, Dict, Callable, Tuple, List, Union, Set import pytest +from apimatic_core_interfaces.formats.datetime_format import DateTimeFormat +from apimatic_core_interfaces.types.union_type import UnionType from apimatic_core_interfaces.types.union_type_context import UnionTypeContext -from pydantic import ValidationError +from pydantic import ValidationError, validate_call from apimatic_core.types.array_serialization_format import SerializationFormats from tests.apimatic_core.mocks.models.lion import Lion @@ -12,7 +15,6 @@ from apimatic_core.types.union_types.one_of import OneOf from dateutil.tz import tzutc -from apimatic_core.types.datetime_format import DateTimeFormat from apimatic_core.types.file_wrapper import FileWrapper from apimatic_core.utilities.api_helper import ApiHelper from tests.apimatic_core.base import Base @@ -22,7 +24,7 @@ ModelWithAdditionalPropertiesOfModelType, ModelWithAdditionalPropertiesOfModelArrayType, \ ModelWithAdditionalPropertiesOfTypeCombinatorPrimitive, ModelWithAdditionalPropertiesOfModelDictType from tests.apimatic_core.mocks.models.person import Employee -from requests.utils import quote +from urllib.parse import quote class TestApiHelper(Base): @@ -41,8 +43,10 @@ class TestApiHelper(Base): Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15)), Base.get_http_datetime(datetime(1994, 2, 13, 5, 30, 15)))), ]) - def test_json_serialize_wrapped_params(self, input_value, expected_value): + @validate_call + def test_json_serialize_wrapped_params(self, input_value: Optional[Dict[str, Any]], expected_value: Optional[str]): request_param = ApiHelper.json_serialize_wrapped_params(input_value) + assert request_param == expected_value @pytest.mark.parametrize('input_value, expected_value', [ @@ -63,8 +67,10 @@ def test_json_serialize_wrapped_params(self, input_value, expected_value): (1, '1'), ('1', '1') ]) - def test_json_serialize(self, input_value, expected_value): + @validate_call + def test_json_serialize(self, input_value: Any, expected_value: Optional[str]): serialized_value = ApiHelper.json_serialize(input_value) + assert serialized_value == expected_value @pytest.mark.parametrize('input_json_value, unboxing_function, as_dict, expected_value', [ @@ -113,26 +119,27 @@ def test_json_serialize(self, input_value, expected_value): False, '{"email":"test","prop":100.65}') ]) - def test_json_deserialize(self, input_json_value, unboxing_function, as_dict, expected_value): + @validate_call + def test_json_deserialize( + self, input_json_value: Optional[str], unboxing_function: Optional[Callable[[Any], Any]], + as_dict: bool, expected_value: Any + ): deserialized_value = ApiHelper.json_deserialize(input_json_value, unboxing_function, as_dict) assert ApiHelper.json_serialize(deserialized_value) == expected_value - @pytest.mark.parametrize('input_json_value, unboxing_function, expected_value', [ + @pytest.mark.parametrize('input_json_value, unboxing_function', [ ('{"email": "test", "prop1": 1, "prop2": 2, "prop3": "invalid type"}', - ModelWithAdditionalPropertiesOfPrimitiveType.model_validate, - '{"email": "test", "prop1": 1, "prop2": 2}'), + ModelWithAdditionalPropertiesOfPrimitiveType.model_validate), ('{"email": "test", "prop1": [1, 2, 3], "prop2": [1, 2, 3], "prop3": "invalid type"}', - ModelWithAdditionalPropertiesOfPrimitiveArrayType.model_validate, - '{"email": "test", "prop1": [1, 2, 3], "prop2": [1, 2, 3]}'), + ModelWithAdditionalPropertiesOfPrimitiveArrayType.model_validate), ( '{"email": "test", "prop1": {"inner_prop1": 1, "inner_prop2": 2}, "prop2": {"inner_prop1": 1, "inner_prop2": 2}, "prop3": "invalid type"}', - ModelWithAdditionalPropertiesOfPrimitiveDictType.model_validate, - '{"email": "test", "prop1": {"inner_prop1": 1, "inner_prop2": 2}, "prop2": {"inner_prop1": 1, "inner_prop2": 2}}'), + ModelWithAdditionalPropertiesOfPrimitiveDictType.model_validate), ('{"email": "test", "prop1": {"id": 1, "weight": 50, "type": "Lion"}, "prop3": "invalid type"}', - ModelWithAdditionalPropertiesOfModelType.model_validate, - '{"email": "test", "prop1": {"id": 1, "weight": 50, "type": "Lion"}}') + ModelWithAdditionalPropertiesOfModelType.model_validate) ]) - def test_json_deserialize(self, input_json_value, unboxing_function, expected_value): + @validate_call + def test_invalid_model_json_deserialize(self, input_json_value: Any, unboxing_function: Optional[Callable[[Any], Any]]): with pytest.raises(ValidationError): ApiHelper.json_deserialize(input_json_value, unboxing_function) @@ -140,7 +147,7 @@ def test_json_deserialize(self, input_json_value, unboxing_function, expected_va ('C:\\PYTHON_GENERIC_LIB\\Tester\\models\\test_file.py', "test_file", 'C:\\PYTHON_GENERIC_LIB\\Tester\\schemas\\TestFile.json'), ]) - def test_get_schema_path(self, input_url, input_file_value, expected_value): + def test_get_schema_path(self, input_url: str, input_file_value: str, expected_value: str): assert expected_value == ApiHelper.get_schema_path(input_url, input_file_value) @pytest.mark.parametrize('input_template_params, expected_template_params', [ @@ -161,7 +168,10 @@ def test_get_schema_path(self, input_url, input_file_value, expected_value): ({'template_param': {'value': 'Basic|Test', 'encode': False}}, 'Basic|Test'), ({'template_param': {'value': None, 'encode': False}}, ''), ]) - def test_append_url_with_template_parameters(self, input_template_params, expected_template_params): + @validate_call + def test_append_url_with_template_parameters( + self, input_template_params: Optional[Dict[str, Dict[str, Any]]], expected_template_params: str + ): assert ApiHelper.append_url_with_template_parameters('http://localhost:3000/{template_param}', input_template_params) \ == 'http://localhost:3000/{}'.format(expected_template_params) @@ -172,15 +182,21 @@ def test_append_url_with_template_parameters(self, input_template_params, expect 'template_param3': {'value': 'Basic Test', 'encode': True}, 'template_param4': {'value': 'Basic Test', 'encode': False}, })]) - def test_append_url_with_template_parameters_multiple_values(self, input_template_params): + @validate_call + def test_append_url_with_template_parameters_multiple_values( + self, input_template_params: Optional[Dict[str, Dict[str, Any]]] + ): url = 'http://localhost:3000/{template_param1}/{template_param2}/{template_param3}/{template_param4}' + assert ApiHelper.append_url_with_template_parameters(url, input_template_params) \ == 'http://localhost:3000/Basic%20Test/Basic%22Test/Basic Test/Basic"Test/Basic%20Test/Basic Test' - + @validate_call @pytest.mark.parametrize('input_url, input_template_param_value', [ (None, {'template_param1': {'value': ['Basic Test', 'Basic"Test'], 'encode': True}}) ]) - def test_append_url_with_template_parameters_value_error(self, input_url, input_template_param_value): + def test_append_url_with_template_parameters_value_error( + self, input_url: Optional[str], input_template_param_value: Optional[Dict[str, Dict[str, Any]]] + ): with pytest.raises(ValueError, match="URL is None."): ApiHelper.append_url_with_template_parameters(input_url, input_template_param_value) @@ -193,10 +209,10 @@ def test_append_url_with_template_parameters_value_error(self, input_url, input_ ({'query_param': ApiHelper.UnixDateTime.from_datetime(datetime(1994, 2, 13, 5, 30, 15))}, 'query_param=761117415', SerializationFormats.INDEXED), ({'query_param': Base.get_http_datetime(datetime(1994, 2, 13, 5, 30, 15))}, - 'query_param={}'.format(quote(Base.get_http_datetime(datetime(1994, 2, 13, 5, 30, 15)), safe='')), + 'query_param={}'.format(quote(Base.get_http_datetime_str(datetime(1994, 2, 13, 5, 30, 15)), safe='')), SerializationFormats.INDEXED), ({'query_param': Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15))}, - 'query_param={}'.format(quote(Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15)), safe='')), + 'query_param={}'.format(quote(Base.get_rfc3339_datetime_str(datetime(1994, 2, 13, 5, 30, 15)), safe='')), SerializationFormats.INDEXED), ({'query_param': [1, 2, 3, 4]}, 'query_param[0]=1&query_param[1]=2&query_param[2]=3&query_param[3]=4', SerializationFormats.INDEXED), @@ -229,7 +245,7 @@ def test_append_url_with_template_parameters_value_error(self, input_url, input_ '&query_param[age]=27' '&query_param[birthday]=1994-02-13' '&query_param[birthtime]={}'.format(quote( - Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15)), safe='')) + + Base.get_rfc3339_datetime_str(datetime(1994, 2, 13, 5, 30, 15)), safe='')) + '&query_param[name]=Bob' '&query_param[uid]=1234567' '&query_param[personType]=Empl' @@ -238,14 +254,14 @@ def test_append_url_with_template_parameters_value_error(self, input_url, input_ '&query_param[dependents][0][age]=12' '&query_param[dependents][0][birthday]=1994-02-13' '&query_param[dependents][0][birthtime]={}'.format(quote( - Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15)), safe='')) + + Base.get_rfc3339_datetime_str(datetime(1994, 2, 13, 5, 30, 15)), safe='')) + '&query_param[dependents][0][name]=John' '&query_param[dependents][0][uid]=7654321' '&query_param[dependents][0][personType]=Per' '&query_param[dependents][0][key1]=value1' '&query_param[dependents][0][key2]=value2' '&query_param[hiredAt]={}'.format(quote( - Base.get_http_datetime(datetime(1994, 2, 13, 5, 30, 15)), safe='')) + + Base.get_http_datetime_str(datetime(1994, 2, 13, 5, 30, 15)), safe='')) + '&query_param[joiningDay]=Monday' '&query_param[salary]=30000' '&query_param[workingDays][0]=Monday' @@ -253,8 +269,11 @@ def test_append_url_with_template_parameters_value_error(self, input_url, input_ '&query_param[key1]=value1' '&query_param[key2]=value2', SerializationFormats.INDEXED) ]) - def test_append_url_with_query_parameters(self, input_query_param_value, expected_query_param_value, - array_serialization_format): + @validate_call + def test_append_url_with_query_parameters( + self, input_query_param_value: Optional[Dict[str, Any]], expected_query_param_value: str, + array_serialization_format: SerializationFormats + ): if input_query_param_value is None: assert ApiHelper.append_url_with_query_parameters('http://localhost:3000/test', input_query_param_value, array_serialization_format) == 'http://localhost:3000/test' @@ -267,7 +286,9 @@ def test_append_url_with_query_parameters(self, input_query_param_value, expecte @pytest.mark.parametrize('input_url, input_query_param_value', [ (None, {'query_param': 'string'}) ]) - def test_append_url_with_query_parameters_value_error(self, input_url, input_query_param_value): + def test_append_url_with_query_parameters_value_error( + self, input_url: Optional[str], input_query_param_value: Optional[Dict[str, Any]] + ): with pytest.raises(ValueError, match="URL is None."): ApiHelper.append_url_with_query_parameters(input_url, input_query_param_value) @@ -278,15 +299,19 @@ def test_append_url_with_query_parameters_value_error(self, input_url, input_que }, 'query_param=string&query_param2=true&query_param3[0]=1&query_param3[1]=2&query_param3[2]=3', SerializationFormats.INDEXED) ]) - def test_process_complex_query_parameters(self, input_query_params, expected_query_param_value, - array_serialization_format): + @validate_call + def test_process_complex_query_parameters( + self, input_query_params: Optional[Dict[str, Any]], expected_query_param_value: str, + array_serialization_format: SerializationFormats + ): assert ApiHelper.append_url_with_query_parameters('http://localhost:3000/test', input_query_params, array_serialization_format) == 'http://localhost:3000/test?{}'.format( expected_query_param_value) @pytest.mark.parametrize('input_url', [ 'This is not a url']) - def test_clean_url_value_error(self, input_url): + @validate_call + def test_clean_url_value_error(self, input_url: str): with pytest.raises(ValueError, match="Invalid Url format."): ApiHelper.clean_url(input_url) @@ -298,7 +323,8 @@ def test_clean_url_value_error(self, input_url): 'query_param=string&query_param2=True&query_param3[0]=1&query_param3[1]=2query_param3[2]=3',) ]) - def test_clean_url(self, input_url, expected_url): + @validate_call + def test_clean_url(self, input_url: str, expected_url: str): assert ApiHelper.clean_url(input_url) == expected_url @pytest.mark.parametrize('input_form_param_value, expected_form_param_value, array_serialization_format', [ @@ -381,7 +407,11 @@ def test_clean_url(self, input_url, expected_url): [('form_param[email]', 'test@gmail.com'), ('form_param[prop]', 10.55)], SerializationFormats.INDEXED) ]) - def test_form_params(self, input_form_param_value, expected_form_param_value, array_serialization_format): + @validate_call + def test_form_params( + self, input_form_param_value: Any, expected_form_param_value: List[Tuple[str, Any]], + array_serialization_format: SerializationFormats + ): key = 'form_param' form_encoded_params = ApiHelper.form_encode(input_form_param_value, key, array_serialization_format) for index, item in enumerate(form_encoded_params): @@ -393,6 +423,7 @@ def test_form_params(self, input_form_param_value, expected_form_param_value, ar else: assert item == expected_form_param_value[index] + @validate_call def test_conflicting_additional_property(self): with pytest.raises(ValueError) as conflictingPropertyError: ModelWithAdditionalPropertiesOfTypeCombinatorPrimitive( @@ -411,21 +442,33 @@ def test_conflicting_additional_property(self): ('form_param2[1]', 'true'), ('form_param3[key]', 'string_val')], SerializationFormats.INDEXED) ]) - def test_form_encode_parameters(self, input_form_param_value, expected_form_param_value, - array_serialization_format): + @validate_call + def test_form_encode_parameters( + self, input_form_param_value: Dict[str, Any], expected_form_param_value: List[Tuple[str, Any]], + array_serialization_format: SerializationFormats + ): assert ApiHelper.form_encode_parameters(input_form_param_value, array_serialization_format) == \ expected_form_param_value @pytest.mark.parametrize('input_value, input_converter, expected_obj', [ - (datetime(1994, 2, 13, 5, 30, 15), ApiHelper.RFC3339DateTime, + (datetime(1994, 2, 13, 5, 30, 15), + ApiHelper.RFC3339DateTime, ApiHelper.RFC3339DateTime), - (datetime(1994, 2, 13, 5, 30, 15), ApiHelper.HttpDateTime, ApiHelper.HttpDateTime), - (datetime(1994, 2, 13, 5, 30, 15), ApiHelper.UnixDateTime, ApiHelper.UnixDateTime), + (datetime(1994, 2, 13, 5, 30, 15), + ApiHelper.HttpDateTime, + ApiHelper.HttpDateTime), + (datetime(1994, 2, 13, 5, 30, 15), + ApiHelper.UnixDateTime, + ApiHelper.UnixDateTime), (500, ApiHelper.UnixDateTime, int), ('500', ApiHelper.UnixDateTime, str), (None, ApiHelper.RFC3339DateTime, None) ]) - def test_apply_date_time_converter(self, input_value, input_converter, expected_obj): + @validate_call(config=dict(arbitrary_types_allowed=True)) + def test_apply_date_time_converter( + self, input_value: Optional[Union[datetime, int, str]], input_converter: Callable[[Any], Any], + expected_obj: Any + ): if input_value is None: assert ApiHelper.apply_datetime_converter(input_value, input_converter) == expected_obj else: @@ -435,22 +478,36 @@ def test_apply_date_time_converter(self, input_value, input_converter, expected_ (ApiHelper.RFC3339DateTime, ApiHelper.RFC3339DateTime.from_value('1994-02-13T14:01:54.9571247Z').datetime, '1994-02-13T14:01:54.957124+00:00') ]) - def test_when_defined(self, input_function, input_body, expected_value): + @validate_call(config=dict(arbitrary_types_allowed=True)) + def test_when_defined(self, input_function: Callable[[datetime], Any], input_body: datetime, expected_value: str): assert str(ApiHelper.when_defined(input_function, input_body)) == expected_value @pytest.mark.parametrize('input_value, input_converter, expected_obj', [ - ([datetime(1994, 2, 13, 5, 30, 15), datetime(1994, 2, 13, 5, 30, 15)], ApiHelper.RFC3339DateTime, - [ApiHelper.RFC3339DateTime, ApiHelper.RFC3339DateTime]), - ([datetime(1994, 2, 13, 5, 30, 15), datetime(1994, 2, 13, 5, 30, 15)], ApiHelper.HttpDateTime, - [ApiHelper.HttpDateTime, ApiHelper.HttpDateTime]), - ([datetime(1994, 2, 13, 5, 30, 15), datetime(1994, 2, 13, 5, 30, 15)], ApiHelper.UnixDateTime, - [ApiHelper.UnixDateTime, ApiHelper.UnixDateTime]), + ([ + datetime(1994, 2, 13, 5, 30, 15), + datetime(1994, 2, 13, 5, 30, 15) + ], ApiHelper.RFC3339DateTime, [ApiHelper.RFC3339DateTime, ApiHelper.RFC3339DateTime]), + ([ + datetime(1994, 2, 13, 5, 30, 15), + datetime(1994, 2, 13, 5, 30, 15) + ], ApiHelper.HttpDateTime, [ApiHelper.HttpDateTime, ApiHelper.HttpDateTime]), + ([ + datetime(1994, 2, 13, 5, 30, 15), + datetime(1994, 2, 13, 5, 30, 15) + ], ApiHelper.UnixDateTime, [ApiHelper.UnixDateTime, ApiHelper.UnixDateTime]), ([500, 1000], ApiHelper.UnixDateTime, [int, int]), (['500', '1000'], ApiHelper.UnixDateTime, [str, str]), - (['500', datetime(1994, 2, 13, 5, 30, 15)], ApiHelper.UnixDateTime, [str, ApiHelper.UnixDateTime]), + ([ + '500', + datetime(1994, 2, 13, 5, 30, 15) + ], ApiHelper.UnixDateTime, [str, ApiHelper.UnixDateTime]), (None, ApiHelper.RFC3339DateTime, None) ]) - def test_apply_date_time_converter_to_list(self, input_value, input_converter, expected_obj): + @validate_call + def test_apply_date_time_converter_to_list( + self, input_value: Optional[List[Union[datetime, int, str]]], input_converter: Callable[[Any], Any], + expected_obj: Any + ): if input_value is None: assert ApiHelper.apply_datetime_converter(input_value, input_converter) == expected_obj else: @@ -459,18 +516,43 @@ def test_apply_date_time_converter_to_list(self, input_value, input_converter, e assert isinstance(actual_value, expected_obj[index]) @pytest.mark.parametrize('input_value, input_converter, expected_obj', [ - ([[datetime(1994, 2, 13, 5, 30, 15), datetime(1994, 2, 13, 5, 30, 15)]], ApiHelper.RFC3339DateTime, - [[ApiHelper.RFC3339DateTime, ApiHelper.RFC3339DateTime]]), - ([[datetime(1994, 2, 13, 5, 30, 15), datetime(1994, 2, 13, 5, 30, 15)]], ApiHelper.HttpDateTime, - [[ApiHelper.HttpDateTime, ApiHelper.HttpDateTime]]), - ([[datetime(1994, 2, 13, 5, 30, 15), datetime(1994, 2, 13, 5, 30, 15)]], ApiHelper.UnixDateTime, - [[ApiHelper.UnixDateTime, ApiHelper.UnixDateTime]]), + ([[ + datetime(1994, 2, 13, 5, 30, 15), + datetime(1994, 2, 13, 5, 30, 15) + ]], ApiHelper.RFC3339DateTime, + [[ + ApiHelper.RFC3339DateTime, + ApiHelper.RFC3339DateTime + ]]), + ([[ + datetime(1994, 2, 13, 5, 30, 15), + datetime(1994, 2, 13, 5, 30, 15) + ]], ApiHelper.HttpDateTime, + [[ + ApiHelper.HttpDateTime, + ApiHelper.HttpDateTime + ]]), + ([[ + datetime(1994, 2, 13, 5, 30, 15), + datetime(1994, 2, 13, 5, 30, 15) + ]], ApiHelper.UnixDateTime, + [[ + ApiHelper.UnixDateTime, + ApiHelper.UnixDateTime + ]]), ([[500, 1000]], ApiHelper.UnixDateTime, [[int, int]]), ([['500', '1000']], ApiHelper.UnixDateTime, [[str, str]]), - ([['500', datetime(1994, 2, 13, 5, 30, 15)]], ApiHelper.UnixDateTime, [[str, ApiHelper.UnixDateTime]]), + ([[ + '500', + datetime(1994, 2, 13, 5, 30, 15) + ]], ApiHelper.UnixDateTime, [[str, ApiHelper.UnixDateTime]]), (None, ApiHelper.RFC3339DateTime, None) ]) - def test_apply_date_time_converter_to_list_of_list(self, input_value, input_converter, expected_obj): + @validate_call + def test_apply_date_time_converter_to_list_of_list( + self, input_value: Optional[List[List[Union[datetime, int, str]]]], input_converter: Callable[[Any], Any], + expected_obj: Any + ): if input_value is None: assert ApiHelper.apply_datetime_converter(input_value, input_converter) == expected_obj else: @@ -480,19 +562,35 @@ def test_apply_date_time_converter_to_list_of_list(self, input_value, input_conv assert isinstance(actual_value, expected_obj[outer_index][index]) @pytest.mark.parametrize('input_value, input_converter, expected_obj', [ - ({'key0': datetime(1994, 2, 13, 5, 30, 15), 'key1': datetime(1994, 2, 13, 5, 30, 15)}, ApiHelper.RFC3339DateTime, + ({ + 'key0': datetime(1994, 2, 13, 5, 30, 15), + 'key1': datetime(1994, 2, 13, 5, 30, 15) + }, ApiHelper.RFC3339DateTime, [ApiHelper.RFC3339DateTime, ApiHelper.RFC3339DateTime]), - ({'key0': datetime(1994, 2, 13, 5, 30, 15), 'key1': datetime(1994, 2, 13, 5, 30, 15)}, ApiHelper.HttpDateTime, + ({ + 'key0': datetime(1994, 2, 13, 5, 30, 15), + 'key1': datetime(1994, 2, 13, 5, 30, 15) + }, ApiHelper.HttpDateTime, [ApiHelper.HttpDateTime, ApiHelper.HttpDateTime]), - ({'key0': datetime(1994, 2, 13, 5, 30, 15), 'key1': datetime(1994, 2, 13, 5, 30, 15)}, ApiHelper.UnixDateTime, + ({ + 'key0': datetime(1994, 2, 13, 5, 30, 15), + 'key1': datetime(1994, 2, 13, 5, 30, 15) + }, ApiHelper.UnixDateTime, [ApiHelper.UnixDateTime, ApiHelper.UnixDateTime]), - ({'key0': '5000', 'key1': datetime(1994, 2, 13, 5, 30, 15)}, ApiHelper.UnixDateTime, + ({ + 'key0': '5000', + 'key1': datetime(1994, 2, 13, 5, 30, 15) + }, ApiHelper.UnixDateTime, [str, ApiHelper.UnixDateTime]), ({'key0': 5000, 'key1': 10000}, ApiHelper.UnixDateTime, [int, int]), ({'key0': '5000', 'key1': '10000'}, ApiHelper.UnixDateTime, [str, str]), (None, ApiHelper.RFC3339DateTime, None) ]) - def test_apply_date_time_converter_to_dict(self, input_value, input_converter, expected_obj): + @validate_call + def test_apply_date_time_converter_to_dict( + self, input_value: Optional[Dict[str, Union[datetime, int, str]]], input_converter: Callable[[Any], Any], + expected_obj: Any + ): if input_value is None: assert ApiHelper.apply_datetime_converter(input_value, input_converter) == expected_obj else: @@ -501,19 +599,38 @@ def test_apply_date_time_converter_to_dict(self, input_value, input_converter, e assert isinstance(actual_value, expected_obj[index]) @pytest.mark.parametrize('input_value, input_converter, expected_obj', [ - ({'key': {'key0': datetime(1994, 2, 13, 5, 30, 15), 'key1': datetime(1994, 2, 13, 5, 30, 15)}}, - ApiHelper.RFC3339DateTime, {'key': [ApiHelper.RFC3339DateTime, ApiHelper.RFC3339DateTime]}), - ({'key': {'key0': datetime(1994, 2, 13, 5, 30, 15), 'key1': datetime(1994, 2, 13, 5, 30, 15)}}, ApiHelper.HttpDateTime, - {'key': [ApiHelper.HttpDateTime, ApiHelper.HttpDateTime]}), - ({'key': {'key0': datetime(1994, 2, 13, 5, 30, 15), 'key1': datetime(1994, 2, 13, 5, 30, 15)}}, ApiHelper.UnixDateTime, - {'key': [ApiHelper.UnixDateTime, ApiHelper.UnixDateTime]}), - ({'key': {'key0': '5000', 'key1': datetime(1994, 2, 13, 5, 30, 15)}}, ApiHelper.UnixDateTime, - {'key': [str, ApiHelper.UnixDateTime]}), + ({ + 'key': { + 'key0': datetime(1994, 2, 13, 5, 30, 15), + 'key1': datetime(1994, 2, 13, 5, 30, 15) + } + }, ApiHelper.RFC3339DateTime, {'key': [ApiHelper.RFC3339DateTime, ApiHelper.RFC3339DateTime]}), + ({ + 'key': { + 'key0': datetime(1994, 2, 13, 5, 30, 15), + 'key1': datetime(1994, 2, 13, 5, 30, 15) + } + }, ApiHelper.HttpDateTime, {'key': [ApiHelper.HttpDateTime, ApiHelper.HttpDateTime]}), + ({ + 'key': { + 'key0': datetime(1994, 2, 13, 5, 30, 15), + 'key1': datetime(1994, 2, 13, 5, 30, 15) + } + }, ApiHelper.UnixDateTime, {'key': [ApiHelper.UnixDateTime, ApiHelper.UnixDateTime]}), + ({ + 'key': { + 'key0': '5000', + 'key1': datetime(1994, 2, 13, 5, 30, 15) + } + }, ApiHelper.UnixDateTime, {'key': [str, ApiHelper.UnixDateTime]}), ({'key': {'key0': 5000, 'key1': 10000}}, ApiHelper.UnixDateTime, {'key': [int, int]}), ({'key': {'key0': '5000', 'key1': '10000'}}, ApiHelper.UnixDateTime, {'key': [str, str]}), (None, ApiHelper.RFC3339DateTime, None) ]) - def test_apply_date_time_converter_to_dict_of_dict(self, input_value, input_converter, expected_obj): + @validate_call + def test_apply_date_time_converter_to_dict_of_dict( + self, input_value: Optional[Dict[str, Any]], input_converter: Callable[[Any], Any], expected_obj: Any + ): if input_value is None: assert ApiHelper.apply_datetime_converter(input_value, input_converter) == expected_obj else: @@ -563,7 +680,10 @@ def test_apply_date_time_converter_to_dict_of_dict(self, input_value, input_conv [ApiHelper.json_serialize(Base.employee_model()), ApiHelper.json_serialize(Base.employee_model())]), ]) - def test_serialize_array(self, input_array, formatting, is_query, expected_array): + @validate_call + def test_serialize_array( + self, input_array: List[Any], formatting: SerializationFormats, is_query: bool, expected_array: List[Any] + ): serialized_array = ApiHelper.serialize_array('test_array', input_array, formatting, is_query) if ApiHelper.is_custom_type(input_array[0]): assert serialized_array[0][0] == 'test_array[0]' and serialized_array[1][0] == 'test_array[1]' and \ @@ -576,7 +696,10 @@ def test_serialize_array(self, input_array, formatting, is_query, expected_array ([1, True, 'string', 2.36, Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15)), str(date(1994, 2, 13))], SerializationFormats.TSV, False) ]) - def test_serialize_array_value_error(self, input_array, formatting, is_query): + @validate_call + def test_serialize_array_value_error( + self, input_array: List[Any], formatting: SerializationFormats, is_query: bool + ): with pytest.raises(ValueError) as exception: ApiHelper.serialize_array('key', input_array, formatting, is_query) @@ -587,11 +710,12 @@ def test_serialize_array_value_error(self, input_array, formatting, is_query): (ApiHelper.json_serialize([str(date(1994, 2, 13)), str(date(1994, 2, 13))]), [date(1994, 2, 13), date(1994, 2, 13)]) ]) - def test_date_deserialize(self, input_date, expected_date): + @validate_call + def test_date_deserialize(self, input_date: str, expected_date: Union[date, List[date]]): assert ApiHelper.date_deserialize(input_date) == expected_date @pytest.mark.parametrize('input_http_response, input_date_time_format, expected_response_body', [ - (None, None, None), + (None, DateTimeFormat.UNIX_DATE_TIME, None), (ApiHelper.UnixDateTime.from_datetime(datetime(1994, 2, 13, 5, 30, 15)), DateTimeFormat.UNIX_DATE_TIME, datetime(1994, 2, 13, 5, 30, 15)), (ApiHelper.HttpDateTime.from_datetime(datetime(1994, 2, 13, 5, 30, 15)), @@ -609,50 +733,61 @@ def test_date_deserialize(self, input_date, expected_date): DateTimeFormat.RFC3339_DATE_TIME, [datetime(1996, 2, 13, 5, 30, 15), datetime(1996, 2, 13, 5, 30, 15)]) ]) - def test_date_time_response_body(self, input_http_response, input_date_time_format, expected_response_body): + @validate_call + def test_date_time_response_body( + self, input_http_response: Optional[Union[str, int]], input_date_time_format: DateTimeFormat, + expected_response_body: Optional[Union[datetime, List[datetime]]] + ): assert ApiHelper.datetime_deserialize(input_http_response, input_date_time_format) == expected_response_body @pytest.mark.parametrize('input_file_wrapper, is_file_instance', [ (FileWrapper(Base.read_file('apimatic.png'), 'image/png'), True), ('I am not a file', False) ]) - def test_is_file_wrapper_instance(self, input_file_wrapper, is_file_instance): + @validate_call + def test_is_file_wrapper_instance(self, input_file_wrapper: Union[FileWrapper, str], is_file_instance: bool): assert ApiHelper.is_file_wrapper_instance(input_file_wrapper) == is_file_instance @pytest.mark.parametrize(' input_date, output_date', [ (datetime(1994, 2, 13, 5, 30, 15), Base.get_http_datetime(datetime(1994, 2, 13, 5, 30, 15))), ]) - def test_http_datetime_from_datetime(self, input_date, output_date): + @validate_call + def test_http_datetime_from_datetime(self, input_date: datetime, output_date: str): assert ApiHelper.HttpDateTime.from_datetime(input_date) == output_date @pytest.mark.parametrize('input_date, output_date', [ (datetime(1994, 2, 13, 5, 30, 15), Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15))), ]) - def test_rfc3339_datetime_from_datetime(self, input_date, output_date): + @validate_call + def test_rfc3339_datetime_from_datetime(self, input_date: datetime, output_date: str): assert ApiHelper.RFC3339DateTime.from_datetime(input_date) == output_date @pytest.mark.parametrize('input_date, output_date', [ (datetime(1994, 2, 13, 5, 30, 15), 761117415), ]) - def test_unix_datetime_from_datetime(self, input_date, output_date): + @validate_call + def test_unix_datetime_from_datetime(self, input_date: datetime, output_date: int): assert ApiHelper.UnixDateTime.from_datetime(input_date) == output_date @pytest.mark.parametrize('input_date, output_date', [ (Base.get_http_datetime(datetime(1994, 2, 13, 5, 30, 15)), datetime(1994, 2, 13, 5, 30, 15)), ]) - def test_http_datetime_from_value(self, input_date, output_date): + @validate_call + def test_http_datetime_from_value(self, input_date: str, output_date: datetime): assert ApiHelper.HttpDateTime.from_value(input_date).datetime == output_date @pytest.mark.parametrize('input_date, output_date', [ ('1994-02-13T14:01:54.9571247Z', datetime(1994, 2, 13, 14, 1, 54, 957124, tzinfo=tzutc())), ]) - def test_rfc3339_from_value(self, input_date, output_date): + @validate_call + def test_rfc3339_from_value(self, input_date: str, output_date: datetime): assert ApiHelper.RFC3339DateTime.from_value(input_date).datetime == output_date @pytest.mark.parametrize('input_date, output_date', [ (1484719381, datetime(2017, 1, 18, 6, 3, 1)), ]) - def test_unix_datetime_from_value(self, input_date, output_date): + @validate_call + def test_unix_datetime_from_value(self, input_date: int, output_date: datetime): assert ApiHelper.UnixDateTime.from_value(input_date).datetime == output_date @pytest.mark.parametrize('input_value, output_value', [ @@ -663,7 +798,8 @@ def test_unix_datetime_from_value(self, input_date, output_date): ('string', 'text/plain; charset=utf-8'), (Base.employee_model(), 'application/json; charset=utf-8'), ]) - def test_get_content_type(self, input_value, output_value): + @validate_call + def test_get_content_type(self, input_value: Any, output_value: Optional[str]): assert ApiHelper.get_content_type(input_value) == output_value @pytest.mark.parametrize('input_value, output_value', [ @@ -673,7 +809,8 @@ def test_get_content_type(self, input_value, output_value): ('', None), (' ', None) ]) - def test_dynamic_deserialize(self, input_value, output_value): + @validate_call + def test_dynamic_deserialize(self, input_value: Optional[str], output_value: Any): assert ApiHelper.dynamic_deserialize(input_value) == output_value @pytest.mark.parametrize('input_placeholders, input_values, input_template, expected_message', [ @@ -686,7 +823,11 @@ def test_dynamic_deserialize(self, input_value, output_value): ({'{accept}'}, {'accept': 'application/json'}, 'Test template -- {accept}', 'Test template -- application/json') ]) - def test_resolve_template_placeholders(self, input_placeholders, input_values, input_template, expected_message): + @validate_call + def test_resolve_template_placeholders( + self, input_placeholders: Set[str], input_values: Union[str, Dict[str, Any]], + input_template: str, expected_message: str + ): actual_message = ApiHelper.resolve_template_placeholders(input_placeholders, input_values, input_template) assert actual_message == expected_message @@ -728,8 +869,11 @@ def test_resolve_template_placeholders(self, input_placeholders, input_values, i 'Test template -- {$response.body}', 'Test template -- ') ]) - def test_resolve_template_placeholders_using_json_pointer(self, input_placeholders, input_value, input_template, - expected_message): + @validate_call + def test_resolve_template_placeholders_using_json_pointer( + self, input_placeholders: Set[str], input_value: Optional[Dict[str, Any]], input_template: str, + expected_message: str + ): actual_message = ApiHelper.resolve_template_placeholders_using_json_pointer( input_placeholders, input_value, input_template) assert actual_message == expected_message @@ -739,7 +883,10 @@ def test_resolve_template_placeholders_using_json_pointer(self, input_placeholde (OneOf([LeafType(int), LeafType(str)], UnionTypeContext(is_array=True)), '[100, "200"]', True, [100, '200']), ]) - def test_union_type_deserialize(self, input_union_type, input_value, input_should_deserialize, expected_value): + @validate_call(config=dict(arbitrary_types_allowed=True)) + def test_union_type_deserialize( + self, input_union_type: UnionType, input_value: Any, input_should_deserialize: bool, expected_value: Any + ): actual_value = ApiHelper.deserialize_union_type(input_union_type, input_value, input_should_deserialize) assert actual_value == expected_value @@ -747,14 +894,16 @@ def test_union_type_deserialize(self, input_union_type, input_value, input_shoul ("https://www.example.com/path/to/resource?param1=value1¶m2=value2", "https://www.example.com/path/to/resource"), ("https://www.example.com/path/to/resource", "https://www.example.com/path/to/resource") ]) - def test_get_url_without_query(self, input_url, expected_url): + @validate_call + def test_get_url_without_query(self, input_url: str, expected_url: str): assert ApiHelper.get_url_without_query(input_url) == expected_url @pytest.mark.parametrize('input_url, expected_error_message', [ ("", "Error parsing URL: Invalid URL format"), ("invalid_url", "Error parsing URL: Invalid URL format") ]) - def test_get_url_without_query_with_invalid_url(self, input_url, expected_error_message): + @validate_call + def test_get_url_without_query_with_invalid_url(self, input_url: str, expected_error_message: str): with pytest.raises(ValueError) as excinfo: ApiHelper.get_url_without_query(input_url) assert str(excinfo.value) == expected_error_message @@ -765,69 +914,9 @@ def test_get_url_without_query_with_invalid_url(self, input_url, expected_error_ (["HELLO"], ["hello"]), (["hElLo", "worLd"], ["hello", "world"]) ]) - def test_to_lower_case(self, input_list, expected_output): + @validate_call + def test_to_lower_case(self, input_list: Optional[List[str]], expected_output: Optional[List[str]]): """Tests if an empty list returns an empty list.""" actual_output = ApiHelper.to_lower_case(input_list) - assert actual_output == expected_output - - @pytest.mark.parametrize( - "dictionary, expected_result, unboxing_func, is_array, is_dict", - [ - ({}, {}, lambda x: int(x), False, False), - ({"a": 1, "b": 2}, {"a": 1, "b": 2}, lambda x: int(x), False, False), - ({"a": "1", "b": "2"}, {"a": "1", "b": "2"}, lambda x: str(x), False, False), - ({"a": "Test 1", "b": "Test 2"}, {}, lambda x: int(x), False, False), - ({"a": [1, 2], "b": [3, 4]}, {"a": [1, 2], "b": [3, 4]}, lambda x: int(x), True, False), - ({"a": {"x": 1, "y": 2}, "b": {"x": 3, "y": 4}}, {"a": {"x": 1, "y": 2}, "b": {"x": 3, "y": 4}}, lambda x: int(x), False, True), - ], - ) - def test_get_additional_properties_success(self, dictionary, expected_result, unboxing_func, is_array, is_dict): - result = ApiHelper.get_additional_properties( - dictionary, lambda x: ApiHelper.apply_unboxing_function( - x, unboxing_func, is_array, is_dict)) - assert result == expected_result - - @pytest.mark.parametrize( - "dictionary", - [ - ({"a": None}), - ({"a": lambda x: x}), - ], - ) - def test_get_additional_properties_exception(self, dictionary): - result = ApiHelper.get_additional_properties(dictionary, ApiHelper.apply_unboxing_function) - assert result == {} # expected result when exception occurs - - @pytest.mark.parametrize( - "value, unboxing_function, is_array, is_dict, is_array_of_map, is_map_of_array, dimension_count, expected", - [ - # Test case 1: Simple object - (5, lambda x: x * 2, False, False, False, False, 0, 10), - # Test case 2: Array - ([1, 2, 3], lambda x: x * 2, True, False, False, False, 0, [2, 4, 6]), - # Test case 3: Dictionary - ({"a": 1, "b": 2}, lambda x: x * 2, False, True, False, False, 0, {"a": 2, "b": 4}), - # Test case 4: Array of maps - ([{"a": 1}, {"b": 2}], lambda x: x * 2, True, False, True, False, 0, [{"a": 2}, {"b": 4}]), - # Test case 5: Map of arrays - ({"a": [1, 2], "b": [3, 4]}, lambda x: x * 2, False, True, False, True, 0, {"a": [2, 4], "b": [6, 8]}), - # Test case 6: Multi-dimensional array - ([[1], [2, 3], [4]], lambda x: x * 2, True, False, False, False, 2, [[2], [4, 6], [8]]), - # Test case 7: Array of arrays - ([[1, 2], [3, 4]], lambda x: x * 2, True, False, False, False, 2, [[2, 4], [6, 8]]), - # Test case 8: Array of arrays of arrays - ([[[1, 2], [3, 4]], [[5, 6], [7, 8]]], lambda x: x * 2, True, False, False, False, 3, - [[[2, 4], [6, 8]], [[10, 12], [14, 16]]]), - ], - ) - def test_apply_unboxing_function(self, value, unboxing_function, is_array, is_dict, - is_array_of_map, is_map_of_array, dimension_count, expected): - result = ApiHelper.apply_unboxing_function( - value, - unboxing_function, - is_array, - is_dict, - is_array_of_map, - is_map_of_array, - dimension_count) - assert result == expected \ No newline at end of file + + assert actual_output == expected_output \ No newline at end of file diff --git a/tests/apimatic_core/utility_tests/test_api_helper.py~ b/tests/apimatic_core/utility_tests/test_api_helper.py~ new file mode 100644 index 0000000..cba74c2 --- /dev/null +++ b/tests/apimatic_core/utility_tests/test_api_helper.py~ @@ -0,0 +1,922 @@ +from datetime import datetime, date +from typing import Optional, Any, Dict, Callable, Tuple, List, Union, Set + +import pytest +from apimatic_core_interfaces.formats.datetime_format import DateTimeFormat +from apimatic_core_interfaces.types.union_type import UnionType +from apimatic_core_interfaces.types.union_type_context import UnionTypeContext +from pydantic import ValidationError, validate_call + +from apimatic_core.types.array_serialization_format import SerializationFormats +from tests.apimatic_core.mocks.models.lion import Lion + + +from apimatic_core.types.union_types.leaf_type import LeafType +from apimatic_core.types.union_types.one_of import OneOf +from dateutil.tz import tzutc + +from apimatic_core.types.file_wrapper import FileWrapper +from apimatic_core.utilities.api_helper import ApiHelper +from tests.apimatic_core.base import Base +from tests.apimatic_core.mocks.models.model_with_additional_properties import \ + ModelWithAdditionalPropertiesOfPrimitiveType, \ + ModelWithAdditionalPropertiesOfPrimitiveArrayType, ModelWithAdditionalPropertiesOfPrimitiveDictType, \ + ModelWithAdditionalPropertiesOfModelType, ModelWithAdditionalPropertiesOfModelArrayType, \ + ModelWithAdditionalPropertiesOfTypeCombinatorPrimitive, ModelWithAdditionalPropertiesOfModelDictType +from tests.apimatic_core.mocks.models.person import Employee +from urllib.parse import quote + + +class TestApiHelper(Base): + + @pytest.mark.parametrize('input_value, expected_value', [ + (None, None), + (Base.wrapped_parameters(), '{{"bodyScalar": true, "bodyNonScalar": {{"address": "street abc", "age": 27, ' + '"birthday": "1994-02-13", "birthtime": "{0}", "name": "Bob", "uid": "1234567", ' + '"personType": "Empl", "department": "IT", "dependents": ' + '[{{"address": "street abc", "age": 12, "birthday": ' + '"1994-02-13", "birthtime": "{0}", "name": "John", "uid": ' + '"7654321", "personType": "Per", "key1": "value1", "key2": "value2"}}], ' + '"hiredAt": "{1}", "joiningDay": "Monday", ' + '"salary": 30000, "workingDays": ["Monday", ' + '"Tuesday"], "key1": "value1", "key2": "value2"}}}}'.format( + Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15)), + Base.get_http_datetime(datetime(1994, 2, 13, 5, 30, 15)))), + ]) + @validate_call + def test_json_serialize_wrapped_params(self, input_value: Optional[Dict[str, Any]], expected_value: Optional[str]): + request_param = ApiHelper.json_serialize_wrapped_params(input_value) + + assert request_param == expected_value + + @pytest.mark.parametrize('input_value, expected_value', [ + (None, None), + ([Base.employee_model(), Base.employee_model()], + '[{0}, {0}]'.format(Base.employee_model_str())), + ([[Base.employee_model(), Base.employee_model()], [Base.employee_model(), Base.employee_model()]], + '[[{0}, {0}], [{0}, {0}]]'.format(Base.employee_model_str())), + ({'key0': [Base.employee_model(), Base.employee_model()], + 'key1': [Base.employee_model(), Base.employee_model()]}, + '{{"key0": [{0}, {0}], "key1": [{0}, {0}]}}'.format(Base.employee_model_str())), + ([1, 2, 3], '[1, 2, 3]'), + ({'key0': 1, 'key1': 'abc'}, '{"key0": 1, "key1": "abc"}'), + ([[1, 2, 3], ['abc', 'def']], '[[1, 2, 3], ["abc", "def"]]'), + ([{'key0': [1, 2, 3]}, {'key1': ['abc', 'def']}], '[{"key0": [1, 2, 3]}, {"key1": ["abc", "def"]}]'), + ({'key0': [1, 2, 3], 'key1': ['abc', 'def']}, '{"key0": [1, 2, 3], "key1": ["abc", "def"]}'), + (Base.employee_model(), Base.employee_model_str(beautify_with_spaces=False)), + (1, '1'), + ('1', '1') + ]) + @validate_call + def test_json_serialize(self, input_value: Any, expected_value: Optional[str]): + serialized_value = ApiHelper.json_serialize(input_value) + + assert serialized_value == expected_value + + @pytest.mark.parametrize('input_json_value, unboxing_function, as_dict, expected_value', [ + (None, None, False, None), + ('true', None, False, 'true'), + ('', None, False, None), + (' ', None, False, None), + (ApiHelper.json_serialize(Base.employee_model()), Employee.model_validate, False, + ApiHelper.json_serialize(Base.employee_model())), + (ApiHelper.json_serialize([Base.employee_model(), Base.employee_model()]), + Employee.model_validate, False, + ApiHelper.json_serialize([Base.employee_model(), Base.employee_model()])), + (ApiHelper.json_serialize({'key1': Base.employee_model(), 'key2': Base.employee_model()}), + Employee.model_validate, True, '{{"key1": {0}, "key2": {0}}}'.format(Base.employee_model_str())), + ('{"email": "test", "prop1": 1, "prop2": 2}', + ModelWithAdditionalPropertiesOfPrimitiveType.model_validate, False, + '{"email":"test","prop1":1,"prop2":2}'), + ('{"email": "test", "prop1": [1, 2, 3], "prop2": [1, 2, 3]}', + ModelWithAdditionalPropertiesOfPrimitiveArrayType.model_validate, False, + '{"email":"test","prop1":[1,2,3],"prop2":[1,2,3]}'), + ('{"email": "test", "prop1": {"inner_prop1": 1, "inner_prop2": 2}, "prop2": {"inner_prop1": 1, "inner_prop2": 2}}', + ModelWithAdditionalPropertiesOfPrimitiveDictType.model_validate, False, + '{"email":"test","prop1":{"inner_prop1":1,"inner_prop2":2},"prop2":{"inner_prop1":1,"inner_prop2":2}}'), + ('{"email": "test", "prop1": {"id": "1", "weight": 50, "type": "Lion"}}', + ModelWithAdditionalPropertiesOfModelType.model_validate, + False, + '{"email":"test","prop1":{"id":"1","weight":50,"type":"Lion"}}'), + ('{"email": "test", "prop": [{"id": "1", "weight": 50, "type": "Lion"}, {"id": "2", "weight": 100, "type": "Lion"}]}', + ModelWithAdditionalPropertiesOfModelArrayType.model_validate, + False, + '{"email":"test","prop":[{"id":"1","weight":50,"type":"Lion"},{"id":"2","weight":100,"type":"Lion"}]}'), + ('{"email": "test", "prop": {"inner prop 1": {"id": "1", "weight": 50, "type": "Lion"}, "inner prop 2": {"id": "2", "weight": 100, "type": "Lion"}}}', + ModelWithAdditionalPropertiesOfModelDictType.model_validate, + False, + '{"email":"test","prop":{"inner prop 1":{"id":"1","weight":50,"type":"Lion"},"inner prop 2":{"id":"2","weight":100,"type":"Lion"}}}'), + ('{"email": "test", "prop": true}', + ModelWithAdditionalPropertiesOfTypeCombinatorPrimitive.model_validate, + False, + '{"email":"test","prop":true}'), + ('{"email": "test", "prop": 100.65}', + ModelWithAdditionalPropertiesOfTypeCombinatorPrimitive.model_validate, + False, + '{"email":"test","prop":100.65}'), + ('{"email": "test", "prop": "100.65"}', + ModelWithAdditionalPropertiesOfTypeCombinatorPrimitive.model_validate, + False, + '{"email":"test","prop":100.65}') + ]) + @validate_call + def test_json_deserialize( + self, input_json_value: Optional[str], unboxing_function: Optional[Callable[[Any], Any]], + as_dict: bool, expected_value: Any + ): + deserialized_value = ApiHelper.json_deserialize(input_json_value, unboxing_function, as_dict) + assert ApiHelper.json_serialize(deserialized_value) == expected_value + + @pytest.mark.parametrize('input_json_value, unboxing_function', [ + ('{"email": "test", "prop1": 1, "prop2": 2, "prop3": "invalid type"}', + ModelWithAdditionalPropertiesOfPrimitiveType.model_validate), + ('{"email": "test", "prop1": [1, 2, 3], "prop2": [1, 2, 3], "prop3": "invalid type"}', + ModelWithAdditionalPropertiesOfPrimitiveArrayType.model_validate), + ( + '{"email": "test", "prop1": {"inner_prop1": 1, "inner_prop2": 2}, "prop2": {"inner_prop1": 1, "inner_prop2": 2}, "prop3": "invalid type"}', + ModelWithAdditionalPropertiesOfPrimitiveDictType.model_validate), + ('{"email": "test", "prop1": {"id": 1, "weight": 50, "type": "Lion"}, "prop3": "invalid type"}', + ModelWithAdditionalPropertiesOfModelType.model_validate) + ]) + @validate_call + def test_invalid_model_json_deserialize(self, input_json_value: Any, unboxing_function: Optional[Callable[[Any], Any]]): + with pytest.raises(ValidationError): + ApiHelper.json_deserialize(input_json_value, unboxing_function) + + @pytest.mark.parametrize('input_url, input_file_value, expected_value', [ + ('C:\\PYTHON_GENERIC_LIB\\Tester\\models\\test_file.py', "test_file", + 'C:\\PYTHON_GENERIC_LIB\\Tester\\schemas\\TestFile.json'), + ]) + def test_get_schema_path(self, input_url: str, input_file_value: str, expected_value: str): + assert expected_value == ApiHelper.get_schema_path(input_url, input_file_value) + + @pytest.mark.parametrize('input_template_params, expected_template_params', [ + (None, '{template_param}'), + ({'template_param': {'value': 'Basic Test', 'encode': True}}, 'Basic%20Test'), + ({'template_param': {'value': 'Basic"Test', 'encode': True}}, 'Basic%22Test'), + ({'template_param': {'value': 'BasicTest', 'encode': True}}, 'Basic%3ETest'), + ({'template_param': {'value': 'Basic#Test', 'encode': True}}, 'Basic%23Test'), + ({'template_param': {'value': 'Basic%Test', 'encode': True}}, 'Basic%25Test'), + ({'template_param': {'value': 'Basic|Test', 'encode': True}}, 'Basic%7CTest'), + ({'template_param': {'value': 'Basic Test', 'encode': False}}, 'Basic Test'), + ({'template_param': {'value': 'Basic"Test', 'encode': False}}, 'Basic"Test'), + ({'template_param': {'value': 'BasicTest', 'encode': False}}, 'Basic>Test'), + ({'template_param': {'value': 'Basic#Test', 'encode': False}}, 'Basic#Test'), + ({'template_param': {'value': 'Basic%Test', 'encode': False}}, 'Basic%Test'), + ({'template_param': {'value': 'Basic|Test', 'encode': False}}, 'Basic|Test'), + ({'template_param': {'value': None, 'encode': False}}, ''), + ]) + @validate_call + def test_append_url_with_template_parameters( + self, input_template_params: Optional[Dict[str, Dict[str, Any]]], expected_template_params: str + ): + assert ApiHelper.append_url_with_template_parameters('http://localhost:3000/{template_param}', + input_template_params) \ + == 'http://localhost:3000/{}'.format(expected_template_params) + + @pytest.mark.parametrize('input_template_params', [ + ({'template_param1': {'value': ['Basic Test', 'Basic"Test'], 'encode': True}, + 'template_param2': {'value': ['Basic Test', 'Basic"Test'], 'encode': False}, + 'template_param3': {'value': 'Basic Test', 'encode': True}, + 'template_param4': {'value': 'Basic Test', 'encode': False}, + })]) + @validate_call + def test_append_url_with_template_parameters_multiple_values( + self, input_template_params: Optional[Dict[str, Dict[str, Any]]] + ): + url = 'http://localhost:3000/{template_param1}/{template_param2}/{template_param3}/{template_param4}' + + assert ApiHelper.append_url_with_template_parameters(url, input_template_params) \ + == 'http://localhost:3000/Basic%20Test/Basic%22Test/Basic Test/Basic"Test/Basic%20Test/Basic Test' + @validate_call + @pytest.mark.parametrize('input_url, input_template_param_value', [ + (None, {'template_param1': {'value': ['Basic Test', 'Basic"Test'], 'encode': True}}) + ]) + def test_append_url_with_template_parameters_value_error( + self, input_url: Optional[str], input_template_param_value: Optional[Dict[str, Dict[str, Any]]] + ): + with pytest.raises(ValueError, match="URL is None."): + ApiHelper.append_url_with_template_parameters(input_url, input_template_param_value) + + @pytest.mark.parametrize('input_query_param_value, expected_query_param_value, array_serialization_format', [ + (None, '', SerializationFormats.INDEXED), + ({'query_param': "string"}, 'query_param=string', SerializationFormats.INDEXED), + ({'query_param': 500}, 'query_param=500', SerializationFormats.INDEXED), + ({'query_param': 500.12}, 'query_param=500.12', SerializationFormats.INDEXED), + ({'query_param': date(1994, 2, 13)}, 'query_param=1994-02-13', SerializationFormats.INDEXED), + ({'query_param': ApiHelper.UnixDateTime.from_datetime(datetime(1994, 2, 13, 5, 30, 15))}, + 'query_param=761117415', SerializationFormats.INDEXED), + ({'query_param': Base.get_http_datetime(datetime(1994, 2, 13, 5, 30, 15))}, + 'query_param={}'.format(quote(Base.get_http_datetime_str(datetime(1994, 2, 13, 5, 30, 15)), safe='')), + SerializationFormats.INDEXED), + ({'query_param': Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15))}, + 'query_param={}'.format(quote(Base.get_rfc3339_datetime_str(datetime(1994, 2, 13, 5, 30, 15)), safe='')), + SerializationFormats.INDEXED), + ({'query_param': [1, 2, 3, 4]}, 'query_param[0]=1&query_param[1]=2&query_param[2]=3&query_param[3]=4', + SerializationFormats.INDEXED), + ({'query_param': [1, 2, 3, 4]}, 'query_param[]=1&query_param[]=2&query_param[]=3&query_param[]=4', + SerializationFormats.UN_INDEXED), + ({'query_param': [1, 2, 3, 4]}, 'query_param=1&query_param=2&query_param=3&query_param=4', + SerializationFormats.PLAIN), + ({'query_param': [1, 2, 3, 4]}, 'query_param=1%2C2%2C3%2C4', SerializationFormats.CSV), + ({'query_param': [1, 2, 3, 4]}, 'query_param=1%7C2%7C3%7C4', SerializationFormats.PSV), + ({'query_param': [1, 2, 3, 4]}, 'query_param=1%092%093%094', SerializationFormats.TSV), + ({'query_param': {'key1': 'value1', 'key2': 'value2'}}, 'query_param[key1]=value1&query_param[key2]=value2', + SerializationFormats.INDEXED), + ({'query_param': 1, 'query_param_none': None, 'query_param2': 2}, 'query_param=1&query_param2=2', + SerializationFormats.INDEXED), + ({'query_param': {'key1': 'value1', 'key2': [1, 2, 3, 4]}}, + 'query_param[key1]=value1' + '&query_param[key2][0]=1' + '&query_param[key2][1]=2' + '&query_param[key2][2]=3' + '&query_param[key2][3]=4', SerializationFormats.INDEXED), + ({'query_param': {'key1': 'value1', 'key2': [1, 2, 3, {'key1': 'value1', 'key2': 'value2'}]}}, + 'query_param[key1]=value1' + '&query_param[key2][0]=1' + '&query_param[key2][1]=2' + '&query_param[key2][2]=3' + '&query_param[key2][3][key1]=value1' + '&query_param[key2][3][key2]=value2', SerializationFormats.INDEXED), + ({'query_param': Base.employee_model()}, + 'query_param[address]=street%20abc' + '&query_param[age]=27' + '&query_param[birthday]=1994-02-13' + '&query_param[birthtime]={}'.format(quote( + Base.get_rfc3339_datetime_str(datetime(1994, 2, 13, 5, 30, 15)), safe='')) + + '&query_param[name]=Bob' + '&query_param[uid]=1234567' + '&query_param[personType]=Empl' + '&query_param[department]=IT' + '&query_param[dependents][0][address]=street%20abc' + '&query_param[dependents][0][age]=12' + '&query_param[dependents][0][birthday]=1994-02-13' + '&query_param[dependents][0][birthtime]={}'.format(quote( + Base.get_rfc3339_datetime_str(datetime(1994, 2, 13, 5, 30, 15)), safe='')) + + '&query_param[dependents][0][name]=John' + '&query_param[dependents][0][uid]=7654321' + '&query_param[dependents][0][personType]=Per' + '&query_param[dependents][0][key1]=value1' + '&query_param[dependents][0][key2]=value2' + '&query_param[hiredAt]={}'.format(quote( + Base.get_http_datetime(datetime(1994, 2, 13, 5, 30, 15)), safe='')) + + '&query_param[joiningDay]=Monday' + '&query_param[salary]=30000' + '&query_param[workingDays][0]=Monday' + '&query_param[workingDays][1]=Tuesday' + '&query_param[key1]=value1' + '&query_param[key2]=value2', SerializationFormats.INDEXED) + ]) + @validate_call + def test_append_url_with_query_parameters( + self, input_query_param_value: Optional[Dict[str, Any]], expected_query_param_value: str, + array_serialization_format: SerializationFormats + ): + if input_query_param_value is None: + assert ApiHelper.append_url_with_query_parameters('http://localhost:3000/test', input_query_param_value, + array_serialization_format) == 'http://localhost:3000/test' + else: + + assert ApiHelper.append_url_with_query_parameters('http://localhost:3000/test', input_query_param_value, + array_serialization_format) == \ + 'http://localhost:3000/test?{}'.format(expected_query_param_value) + + @pytest.mark.parametrize('input_url, input_query_param_value', [ + (None, {'query_param': 'string'}) + ]) + def test_append_url_with_query_parameters_value_error( + self, input_url: Optional[str], input_query_param_value: Optional[Dict[str, Any]] + ): + with pytest.raises(ValueError, match="URL is None."): + ApiHelper.append_url_with_query_parameters(input_url, input_query_param_value) + + @pytest.mark.parametrize('input_query_params, expected_query_param_value, array_serialization_format', [ + ({'query_param': "string", + 'query_param2': True, + 'query_param3': [1, 2, 3] + }, 'query_param=string&query_param2=true&query_param3[0]=1&query_param3[1]=2&query_param3[2]=3', + SerializationFormats.INDEXED) + ]) + @validate_call + def test_process_complex_query_parameters( + self, input_query_params: Optional[Dict[str, Any]], expected_query_param_value: str, + array_serialization_format: SerializationFormats + ): + assert ApiHelper.append_url_with_query_parameters('http://localhost:3000/test', input_query_params, + array_serialization_format) == 'http://localhost:3000/test?{}'.format( + expected_query_param_value) + + @pytest.mark.parametrize('input_url', [ + 'This is not a url']) + @validate_call + def test_clean_url_value_error(self, input_url: str): + with pytest.raises(ValueError, match="Invalid Url format."): + ApiHelper.clean_url(input_url) + + @pytest.mark.parametrize('input_url, expected_url', [ + ('http://localhost:3000//test', 'http://localhost:3000/test'), + ('http://localhost:3000//test?' + 'query_param=string&query_param2=True&query_param3[0]=1&query_param3[1]=2query_param3[2]=3', + 'http://localhost:3000/test?' + 'query_param=string&query_param2=True&query_param3[0]=1&query_param3[1]=2query_param3[2]=3',) + + ]) + @validate_call + def test_clean_url(self, input_url: str, expected_url: str): + assert ApiHelper.clean_url(input_url) == expected_url + + @pytest.mark.parametrize('input_form_param_value, expected_form_param_value, array_serialization_format', [ + (None, [], SerializationFormats.INDEXED), + ('string', [('form_param', 'string')], SerializationFormats.INDEXED), + (500, [('form_param', 500)], SerializationFormats.INDEXED), + (500.12, [('form_param', 500.12)], SerializationFormats.INDEXED), + (str(date(1994, 2, 13)), [('form_param', '1994-02-13')], SerializationFormats.INDEXED), + (ApiHelper.UnixDateTime.from_datetime(datetime(1994, 2, 13, 5, 30, 15)), + [('form_param', 761117415)], SerializationFormats.INDEXED), + (Base.get_http_datetime(datetime(1994, 2, 13, 5, 30, 15)), + [('form_param', Base.get_http_datetime(datetime(1994, 2, 13, 5, 30, 15)))], SerializationFormats.INDEXED), + (Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15)), + [('form_param', Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15)))], SerializationFormats.INDEXED), + ([1, 2, 3, 4], [('form_param[0]', 1), ('form_param[1]', 2), ('form_param[2]', 3), ('form_param[3]', 4)], + SerializationFormats.INDEXED), + ([1, 2, 3, 4], [('form_param[]', 1), ('form_param[]', 2), ('form_param[]', 3), ('form_param[]', 4)], + SerializationFormats.UN_INDEXED), + ([1, 2, 3, 4], [('form_param', 1), ('form_param', 2), ('form_param', 3), ('form_param', 4)], + SerializationFormats.PLAIN), + ({'key1': 'value1', 'key2': 'value2'}, [('form_param[key1]', 'value1'), ('form_param[key2]', 'value2')], + SerializationFormats.INDEXED), + ({'key1': 'value1', 'key2': [1, 2, 3, 4]}, + [('form_param[key1]', 'value1'), ('form_param[key2][0]', 1), ('form_param[key2][1]', 2), + ('form_param[key2][2]', 3), ('form_param[key2][3]', 4)], SerializationFormats.INDEXED), + ({'key1': 'value1', 'key2': [1, 2, 3, {'key1': 'value1', 'key2': 'value2'}]}, + [('form_param[key1]', 'value1'), ('form_param[key2][0]', 1), ('form_param[key2][1]', 2), + ('form_param[key2][2]', 3), ('form_param[key2][3][key1]', 'value1'), ('form_param[key2][3][key2]', 'value2')], + SerializationFormats.INDEXED), + (Base.employee_model(), + [('form_param[address]', 'street abc'), ('form_param[age]', 27), ('form_param[birthday]', '1994-02-13'), + ('form_param[birthtime]', Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15))), + ('form_param[name]', 'Bob'), ('form_param[uid]', '1234567'), ('form_param[personType]', 'Empl'), + ('form_param[department]', 'IT'), ('form_param[dependents][0][address]', 'street abc'), + ('form_param[dependents][0][age]', 12), ('form_param[dependents][0][birthday]', '1994-02-13'), + ('form_param[dependents][0][birthtime]', Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15))), + ('form_param[dependents][0][name]', 'John'), ('form_param[dependents][0][uid]', '7654321'), + ('form_param[dependents][0][personType]', 'Per'), ('form_param[dependents][0][key1]', 'value1'), + ('form_param[dependents][0][key2]', 'value2'), + ('form_param[hiredAt]', Base.get_http_datetime(datetime(1994, 2, 13, 5, 30, 15))), + ('form_param[joiningDay]', 'Monday'), ('form_param[salary]', 30000), ('form_param[workingDays][0]', 'Monday'), + ('form_param[workingDays][1]', 'Tuesday'), ('form_param[key1]', 'value1'), + ('form_param[key2]', 'value2'),], SerializationFormats.INDEXED), + (ModelWithAdditionalPropertiesOfPrimitiveType(email='test@gmail.com', additional_properties={'prop': 20}), + [('form_param[email]', 'test@gmail.com'), ('form_param[prop]', 20)], SerializationFormats.INDEXED), + (ModelWithAdditionalPropertiesOfPrimitiveArrayType( + email='test@gmail.com', additional_properties={'prop': [20, 30]}), + [('form_param[email]', 'test@gmail.com'), ('form_param[prop][0]', 20), ('form_param[prop][1]', 30)], SerializationFormats.INDEXED), + (ModelWithAdditionalPropertiesOfPrimitiveDictType( + email='test@gmail.com', additional_properties={'prop': {'inner prop 1': 20, 'inner prop 2': 30}}), + [('form_param[email]', 'test@gmail.com'), ('form_param[prop][inner prop 1]', 20), ('form_param[prop][inner prop 2]', 30)], + SerializationFormats.INDEXED), + (ModelWithAdditionalPropertiesOfModelType( + email='test@gmail.com', additional_properties={'prop': Lion(id='leo', weight=5, mtype='Lion')}), + [('form_param[email]', 'test@gmail.com'), ('form_param[prop][id]', 'leo'), ('form_param[prop][weight]', 5), ('form_param[prop][type]', 'Lion')], + SerializationFormats.INDEXED), + (ModelWithAdditionalPropertiesOfModelArrayType( + email='test@gmail.com', + additional_properties={'prop': [Lion(id='leo 1', weight=5, mtype='Lion'), Lion(id='leo 2', weight=10, mtype='Lion')]}), + [('form_param[email]', 'test@gmail.com'), ('form_param[prop][0][id]', 'leo 1'), ('form_param[prop][0][weight]', 5), + ('form_param[prop][0][type]', 'Lion'), ('form_param[prop][1][id]', 'leo 2'), ('form_param[prop][1][weight]', 10), + ('form_param[prop][1][type]', 'Lion')], + SerializationFormats.INDEXED), + (ModelWithAdditionalPropertiesOfModelDictType( + email='test@gmail.com', + additional_properties={ + 'prop': { + 'leo 1': Lion(id='leo 1', weight=5, mtype='Lion'), + 'leo 2': Lion(id='leo 2', weight=10, mtype='Lion') + } + }), + [('form_param[email]', 'test@gmail.com'), ('form_param[prop][leo 1][id]', 'leo 1'), + ('form_param[prop][leo 1][weight]', 5), + ('form_param[prop][leo 1][type]', 'Lion'), ('form_param[prop][leo 2][id]', 'leo 2'), + ('form_param[prop][leo 2][weight]', 10), + ('form_param[prop][leo 2][type]', 'Lion')], + SerializationFormats.INDEXED), + (ModelWithAdditionalPropertiesOfTypeCombinatorPrimitive( + email='test@gmail.com', additional_properties={'prop': 10.55}), + [('form_param[email]', 'test@gmail.com'), ('form_param[prop]', 10.55)], + SerializationFormats.INDEXED) + ]) + @validate_call + def test_form_params( + self, input_form_param_value: Any, expected_form_param_value: List[Tuple[str, Any]], + array_serialization_format: SerializationFormats + ): + key = 'form_param' + form_encoded_params = ApiHelper.form_encode(input_form_param_value, key, array_serialization_format) + for index, item in enumerate(form_encoded_params): + # form encoding stores the datetime object so converting datetime to string for assertions as assertions + # do not work for objects + if isinstance(item[1], ApiHelper.CustomDate): + assert item[0] == expected_form_param_value[index][0] \ + and item[1].value == expected_form_param_value[index][1].value + else: + assert item == expected_form_param_value[index] + + @validate_call + def test_conflicting_additional_property(self): + with pytest.raises(ValueError) as conflictingPropertyError: + ModelWithAdditionalPropertiesOfTypeCombinatorPrimitive( + email='test@gmail.com', additional_properties={'email': 10.55}) + + assert ("Invalid additional properties: {'email'}. These names conflict with existing model properties." + in str(conflictingPropertyError.value)) + + + @pytest.mark.parametrize('input_form_param_value, expected_form_param_value, array_serialization_format', [ + ({'form_param1': 'string', + 'form_param2': ['string', True], + 'form_param3': {'key': 'string_val'}, + }, [('form_param1', 'string'), + ('form_param2[0]', 'string'), + ('form_param2[1]', 'true'), + ('form_param3[key]', 'string_val')], SerializationFormats.INDEXED) + ]) + @validate_call + def test_form_encode_parameters( + self, input_form_param_value: Dict[str, Any], expected_form_param_value: List[Tuple[str, Any]], + array_serialization_format: SerializationFormats + ): + assert ApiHelper.form_encode_parameters(input_form_param_value, array_serialization_format) == \ + expected_form_param_value + + @pytest.mark.parametrize('input_value, input_converter, expected_obj', [ + (datetime(1994, 2, 13, 5, 30, 15), + ApiHelper.RFC3339DateTime, + ApiHelper.RFC3339DateTime), + (datetime(1994, 2, 13, 5, 30, 15), + ApiHelper.HttpDateTime, + ApiHelper.HttpDateTime), + (datetime(1994, 2, 13, 5, 30, 15), + ApiHelper.UnixDateTime, + ApiHelper.UnixDateTime), + (500, ApiHelper.UnixDateTime, int), + ('500', ApiHelper.UnixDateTime, str), + (None, ApiHelper.RFC3339DateTime, None) + ]) + @validate_call(config=dict(arbitrary_types_allowed=True)) + def test_apply_date_time_converter( + self, input_value: Optional[Union[datetime, int, str]], input_converter: Callable[[Any], Any], + expected_obj: Any + ): + if input_value is None: + assert ApiHelper.apply_datetime_converter(input_value, input_converter) == expected_obj + else: + assert isinstance(ApiHelper.apply_datetime_converter(input_value, input_converter), expected_obj) + + @pytest.mark.parametrize('input_function, input_body, expected_value', [ + (ApiHelper.RFC3339DateTime, ApiHelper.RFC3339DateTime.from_value('1994-02-13T14:01:54.9571247Z').datetime, + '1994-02-13T14:01:54.957124+00:00') + ]) + @validate_call(config=dict(arbitrary_types_allowed=True)) + def test_when_defined(self, input_function: Callable[[datetime], Any], input_body: datetime, expected_value: str): + assert str(ApiHelper.when_defined(input_function, input_body)) == expected_value + + @pytest.mark.parametrize('input_value, input_converter, expected_obj', [ + ([ + datetime(1994, 2, 13, 5, 30, 15), + datetime(1994, 2, 13, 5, 30, 15) + ], ApiHelper.RFC3339DateTime, [ApiHelper.RFC3339DateTime, ApiHelper.RFC3339DateTime]), + ([ + datetime(1994, 2, 13, 5, 30, 15), + datetime(1994, 2, 13, 5, 30, 15) + ], ApiHelper.HttpDateTime, [ApiHelper.HttpDateTime, ApiHelper.HttpDateTime]), + ([ + datetime(1994, 2, 13, 5, 30, 15), + datetime(1994, 2, 13, 5, 30, 15) + ], ApiHelper.UnixDateTime, [ApiHelper.UnixDateTime, ApiHelper.UnixDateTime]), + ([500, 1000], ApiHelper.UnixDateTime, [int, int]), + (['500', '1000'], ApiHelper.UnixDateTime, [str, str]), + ([ + '500', + datetime(1994, 2, 13, 5, 30, 15) + ], ApiHelper.UnixDateTime, [str, ApiHelper.UnixDateTime]), + (None, ApiHelper.RFC3339DateTime, None) + ]) + @validate_call + def test_apply_date_time_converter_to_list( + self, input_value: Optional[List[Union[datetime, int, str]]], input_converter: Callable[[Any], Any], + expected_obj: Any + ): + if input_value is None: + assert ApiHelper.apply_datetime_converter(input_value, input_converter) == expected_obj + else: + actual_converted_value = ApiHelper.apply_datetime_converter(input_value, input_converter) + for index, actual_value in enumerate(actual_converted_value): + assert isinstance(actual_value, expected_obj[index]) + + @pytest.mark.parametrize('input_value, input_converter, expected_obj', [ + ([[ + datetime(1994, 2, 13, 5, 30, 15), + datetime(1994, 2, 13, 5, 30, 15) + ]], ApiHelper.RFC3339DateTime, + [[ + ApiHelper.RFC3339DateTime, + ApiHelper.RFC3339DateTime + ]]), + ([[ + datetime(1994, 2, 13, 5, 30, 15), + datetime(1994, 2, 13, 5, 30, 15) + ]], ApiHelper.HttpDateTime, + [[ + ApiHelper.HttpDateTime, + ApiHelper.HttpDateTime + ]]), + ([[ + datetime(1994, 2, 13, 5, 30, 15), + datetime(1994, 2, 13, 5, 30, 15) + ]], ApiHelper.UnixDateTime, + [[ + ApiHelper.UnixDateTime, + ApiHelper.UnixDateTime + ]]), + ([[500, 1000]], ApiHelper.UnixDateTime, [[int, int]]), + ([['500', '1000']], ApiHelper.UnixDateTime, [[str, str]]), + ([[ + '500', + datetime(1994, 2, 13, 5, 30, 15) + ]], ApiHelper.UnixDateTime, [[str, ApiHelper.UnixDateTime]]), + (None, ApiHelper.RFC3339DateTime, None) + ]) + @validate_call + def test_apply_date_time_converter_to_list_of_list( + self, input_value: Optional[List[List[Union[datetime, int, str]]]], input_converter: Callable[[Any], Any], + expected_obj: Any + ): + if input_value is None: + assert ApiHelper.apply_datetime_converter(input_value, input_converter) == expected_obj + else: + actual_converted_value = ApiHelper.apply_datetime_converter(input_value, input_converter) + for outer_index, actual_outer_value in enumerate(actual_converted_value): + for index, actual_value in enumerate(actual_outer_value): + assert isinstance(actual_value, expected_obj[outer_index][index]) + + @pytest.mark.parametrize('input_value, input_converter, expected_obj', [ + ({ + 'key0': datetime(1994, 2, 13, 5, 30, 15), + 'key1': datetime(1994, 2, 13, 5, 30, 15) + }, ApiHelper.RFC3339DateTime, + [ApiHelper.RFC3339DateTime, ApiHelper.RFC3339DateTime]), + ({ + 'key0': datetime(1994, 2, 13, 5, 30, 15), + 'key1': datetime(1994, 2, 13, 5, 30, 15) + }, ApiHelper.HttpDateTime, + [ApiHelper.HttpDateTime, ApiHelper.HttpDateTime]), + ({ + 'key0': datetime(1994, 2, 13, 5, 30, 15), + 'key1': datetime(1994, 2, 13, 5, 30, 15) + }, ApiHelper.UnixDateTime, + [ApiHelper.UnixDateTime, ApiHelper.UnixDateTime]), + ({ + 'key0': '5000', + 'key1': datetime(1994, 2, 13, 5, 30, 15) + }, ApiHelper.UnixDateTime, + [str, ApiHelper.UnixDateTime]), + ({'key0': 5000, 'key1': 10000}, ApiHelper.UnixDateTime, [int, int]), + ({'key0': '5000', 'key1': '10000'}, ApiHelper.UnixDateTime, [str, str]), + (None, ApiHelper.RFC3339DateTime, None) + ]) + @validate_call + def test_apply_date_time_converter_to_dict( + self, input_value: Optional[Dict[str, Union[datetime, int, str]]], input_converter: Callable[[Any], Any], + expected_obj: Any + ): + if input_value is None: + assert ApiHelper.apply_datetime_converter(input_value, input_converter) == expected_obj + else: + actual_converted_value = ApiHelper.apply_datetime_converter(input_value, input_converter) + for index, actual_value in enumerate(actual_converted_value.values()): + assert isinstance(actual_value, expected_obj[index]) + + @pytest.mark.parametrize('input_value, input_converter, expected_obj', [ + ({ + 'key': { + 'key0': datetime(1994, 2, 13, 5, 30, 15), + 'key1': datetime(1994, 2, 13, 5, 30, 15) + } + }, ApiHelper.RFC3339DateTime, {'key': [ApiHelper.RFC3339DateTime, ApiHelper.RFC3339DateTime]}), + ({ + 'key': { + 'key0': datetime(1994, 2, 13, 5, 30, 15), + 'key1': datetime(1994, 2, 13, 5, 30, 15) + } + }, ApiHelper.HttpDateTime, {'key': [ApiHelper.HttpDateTime, ApiHelper.HttpDateTime]}), + ({ + 'key': { + 'key0': datetime(1994, 2, 13, 5, 30, 15), + 'key1': datetime(1994, 2, 13, 5, 30, 15) + } + }, ApiHelper.UnixDateTime, {'key': [ApiHelper.UnixDateTime, ApiHelper.UnixDateTime]}), + ({ + 'key': { + 'key0': '5000', + 'key1': datetime(1994, 2, 13, 5, 30, 15) + } + }, ApiHelper.UnixDateTime, {'key': [str, ApiHelper.UnixDateTime]}), + ({'key': {'key0': 5000, 'key1': 10000}}, ApiHelper.UnixDateTime, {'key': [int, int]}), + ({'key': {'key0': '5000', 'key1': '10000'}}, ApiHelper.UnixDateTime, {'key': [str, str]}), + (None, ApiHelper.RFC3339DateTime, None) + ]) + @validate_call + def test_apply_date_time_converter_to_dict_of_dict( + self, input_value: Optional[Dict[str, Any]], input_converter: Callable[[Any], Any], expected_obj: Any + ): + if input_value is None: + assert ApiHelper.apply_datetime_converter(input_value, input_converter) == expected_obj + else: + actual_converted_value = ApiHelper.apply_datetime_converter(input_value, input_converter) + for outer_key, actual_outer_value in actual_converted_value.items(): + for index, actual_value in enumerate(actual_outer_value.values()): + assert isinstance(actual_value, expected_obj[outer_key][index]) + + @pytest.mark.parametrize('input_array, formatting, is_query, expected_array', [ + ([1, True, 'string', 2.36, Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15)), + str(date(1994, 2, 13))], SerializationFormats.INDEXED, False, [('test_array[0]', 1), + ('test_array[1]', True), + ('test_array[2]', 'string'), + ('test_array[3]', 2.36), + ('test_array[4]', Base.get_rfc3339_datetime( + datetime(1994, 2, 13, 5, 30, 15))), + ('test_array[5]', '1994-02-13')]), + ([1, True, 'string', 2.36, Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15)), + str(date(1994, 2, 13))], SerializationFormats.UN_INDEXED, False, [('test_array[]', 1), + ('test_array[]', True), + ('test_array[]', 'string'), + ('test_array[]', 2.36), + ('test_array[]', Base.get_rfc3339_datetime( + datetime(1994, 2, 13, 5, 30, 15))), + ('test_array[]', '1994-02-13')]), + ([1, True, 'string', 2.36, Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15)), + str(date(1994, 2, 13))], SerializationFormats.PLAIN, False, [('test_array', 1), + ('test_array', True), + ('test_array', 'string'), + ('test_array', 2.36), + ('test_array', Base.get_rfc3339_datetime( + datetime(1994, 2, 13, 5, 30, 15))), + ('test_array', '1994-02-13')]), + ([1, True, 'string', 2.36, Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15)), + str(date(1994, 2, 13))], SerializationFormats.CSV, True, + [('test_array', + '1,True,string,2.36,{0},1994-02-13'.format(Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15))))]), + ([1, True, 'string', 2.36, Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15)), + str(date(1994, 2, 13))], SerializationFormats.PSV, True, + [('test_array', + '1|True|string|2.36|{0}|1994-02-13'.format(Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15))))]), + ([1, True, 'string', 2.36, Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15)), + str(date(1994, 2, 13))], SerializationFormats.TSV, True, + [('test_array', '1\tTrue\tstring\t2.36\t{0}\t1994-02-13'.format( + Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15))))]), + ([Base.employee_model(), Base.employee_model()], SerializationFormats.INDEXED, False, + [ApiHelper.json_serialize(Base.employee_model()), ApiHelper.json_serialize(Base.employee_model())]), + + ]) + @validate_call + def test_serialize_array( + self, input_array: List[Any], formatting: SerializationFormats, is_query: bool, expected_array: List[Any] + ): + serialized_array = ApiHelper.serialize_array('test_array', input_array, formatting, is_query) + if ApiHelper.is_custom_type(input_array[0]): + assert serialized_array[0][0] == 'test_array[0]' and serialized_array[1][0] == 'test_array[1]' and \ + ApiHelper.json_serialize(serialized_array[0][1]) == expected_array[0] \ + and ApiHelper.json_serialize(serialized_array[1][1]) == expected_array[1] + else: + assert ApiHelper.serialize_array('test_array', input_array, formatting, is_query) == expected_array + + @pytest.mark.parametrize('input_array, formatting, is_query', [ + ([1, True, 'string', 2.36, Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15)), + str(date(1994, 2, 13))], SerializationFormats.TSV, False) + ]) + @validate_call + def test_serialize_array_value_error( + self, input_array: List[Any], formatting: SerializationFormats, is_query: bool + ): + with pytest.raises(ValueError) as exception: + ApiHelper.serialize_array('key', input_array, formatting, is_query) + + assert 'Invalid format provided.' in str(exception.value) + + @pytest.mark.parametrize('input_date, expected_date', [ + (str(date(1994, 2, 13)), date(1994, 2, 13)), + (ApiHelper.json_serialize([str(date(1994, 2, 13)), str(date(1994, 2, 13))]), + [date(1994, 2, 13), date(1994, 2, 13)]) + ]) + @validate_call + def test_date_deserialize(self, input_date: str, expected_date: Union[date, List[date]]): + assert ApiHelper.date_deserialize(input_date) == expected_date + + @pytest.mark.parametrize('input_http_response, input_date_time_format, expected_response_body', [ + (None, DateTimeFormat.UNIX_DATE_TIME, None), + (ApiHelper.UnixDateTime.from_datetime(datetime(1994, 2, 13, 5, 30, 15)), + DateTimeFormat.UNIX_DATE_TIME, datetime(1994, 2, 13, 5, 30, 15)), + (ApiHelper.HttpDateTime.from_datetime(datetime(1994, 2, 13, 5, 30, 15)), + DateTimeFormat.HTTP_DATE_TIME, datetime(1994, 2, 13, 5, 30, 15)), + (ApiHelper.RFC3339DateTime.from_datetime(datetime(1994, 2, 13, 5, 30, 15)), + DateTimeFormat.RFC3339_DATE_TIME, datetime(1994, 2, 13, 5, 30, 15)), + (ApiHelper.json_serialize([ApiHelper.UnixDateTime.from_datetime(datetime(1994, 2, 13, 5, 30, 15)), + ApiHelper.UnixDateTime.from_datetime(datetime(1994, 2, 13, 5, 30, 15))]), + DateTimeFormat.UNIX_DATE_TIME, [datetime(1994, 2, 13, 5, 30, 15), datetime(1994, 2, 13, 5, 30, 15)]), + (ApiHelper.json_serialize([ApiHelper.HttpDateTime.from_datetime(datetime(1995, 2, 13, 5, 30, 15)), + ApiHelper.HttpDateTime.from_datetime(datetime(1995, 2, 13, 5, 30, 15))]), + DateTimeFormat.HTTP_DATE_TIME, [datetime(1995, 2, 13, 5, 30, 15), datetime(1995, 2, 13, 5, 30, 15)]), + (ApiHelper.json_serialize([ApiHelper.RFC3339DateTime.from_datetime(datetime(1996, 2, 13, 5, 30, 15)), + ApiHelper.RFC3339DateTime.from_datetime(datetime(1996, 2, 13, 5, 30, 15))]), + DateTimeFormat.RFC3339_DATE_TIME, [datetime(1996, 2, 13, 5, 30, 15), datetime(1996, 2, 13, 5, 30, 15)]) + + ]) + @validate_call + def test_date_time_response_body( + self, input_http_response: Optional[Union[str, int]], input_date_time_format: DateTimeFormat, + expected_response_body: Optional[Union[datetime, List[datetime]]] + ): + assert ApiHelper.datetime_deserialize(input_http_response, input_date_time_format) == expected_response_body + + @pytest.mark.parametrize('input_file_wrapper, is_file_instance', [ + (FileWrapper(Base.read_file('apimatic.png'), 'image/png'), True), + ('I am not a file', False) + ]) + @validate_call + def test_is_file_wrapper_instance(self, input_file_wrapper: Union[FileWrapper, str], is_file_instance: bool): + assert ApiHelper.is_file_wrapper_instance(input_file_wrapper) == is_file_instance + + @pytest.mark.parametrize(' input_date, output_date', [ + (datetime(1994, 2, 13, 5, 30, 15), Base.get_http_datetime(datetime(1994, 2, 13, 5, 30, 15))), + ]) + @validate_call + def test_http_datetime_from_datetime(self, input_date: datetime, output_date: str): + assert ApiHelper.HttpDateTime.from_datetime(input_date) == output_date + + @pytest.mark.parametrize('input_date, output_date', [ + (datetime(1994, 2, 13, 5, 30, 15), Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15))), + ]) + @validate_call + def test_rfc3339_datetime_from_datetime(self, input_date: datetime, output_date: str): + assert ApiHelper.RFC3339DateTime.from_datetime(input_date) == output_date + + @pytest.mark.parametrize('input_date, output_date', [ + (datetime(1994, 2, 13, 5, 30, 15), 761117415), + ]) + @validate_call + def test_unix_datetime_from_datetime(self, input_date: datetime, output_date: int): + assert ApiHelper.UnixDateTime.from_datetime(input_date) == output_date + + @pytest.mark.parametrize('input_date, output_date', [ + (Base.get_http_datetime(datetime(1994, 2, 13, 5, 30, 15)), datetime(1994, 2, 13, 5, 30, 15)), + ]) + @validate_call + def test_http_datetime_from_value(self, input_date: str, output_date: datetime): + assert ApiHelper.HttpDateTime.from_value(input_date).datetime == output_date + + @pytest.mark.parametrize('input_date, output_date', [ + ('1994-02-13T14:01:54.9571247Z', datetime(1994, 2, 13, 14, 1, 54, 957124, tzinfo=tzutc())), + ]) + @validate_call + def test_rfc3339_from_value(self, input_date: str, output_date: datetime): + assert ApiHelper.RFC3339DateTime.from_value(input_date).datetime == output_date + + @pytest.mark.parametrize('input_date, output_date', [ + (1484719381, datetime(2017, 1, 18, 6, 3, 1)), + ]) + @validate_call + def test_unix_datetime_from_value(self, input_date: int, output_date: datetime): + assert ApiHelper.UnixDateTime.from_value(input_date).datetime == output_date + + @pytest.mark.parametrize('input_value, output_value', [ + (None, None), + (1, 'text/plain; charset=utf-8'), + (1.4, 'text/plain; charset=utf-8'), + (True, 'text/plain; charset=utf-8'), + ('string', 'text/plain; charset=utf-8'), + (Base.employee_model(), 'application/json; charset=utf-8'), + ]) + @validate_call + def test_get_content_type(self, input_value: Any, output_value: Optional[str]): + assert ApiHelper.get_content_type(input_value) == output_value + + @pytest.mark.parametrize('input_value, output_value', [ + ('{"method": "GET", "body": {}, "uploadCount": 0}', {'body': {}, 'method': 'GET', 'uploadCount': 0}), + ('I am a string', 'I am a string'), + (None, None), + ('', None), + (' ', None) + ]) + @validate_call + def test_dynamic_deserialize(self, input_value: Optional[str], output_value: Any): + assert ApiHelper.dynamic_deserialize(input_value) == output_value + + @pytest.mark.parametrize('input_placeholders, input_values, input_template, expected_message', [ + (set(), '400', 'Test template -- {$statusCode}', 'Test template -- {$statusCode}'), + ({'{$statusCode}'}, '400', 'Test template -- {$statusCode}', 'Test template -- 400'), + ({'{$response.header.accept}'}, {'accept': 'application/json'}, + 'Test template -- {$response.header.accept}', 'Test template -- application/json'), + ({'{$response.header.accept}'}, {'retry-after': 60}, + 'Test template -- {$response.header.accept}', 'Test template -- '), + ({'{accept}'}, {'accept': 'application/json'}, + 'Test template -- {accept}', 'Test template -- application/json') + ]) + @validate_call + def test_resolve_template_placeholders( + self, input_placeholders: Set[str], input_values: Union[str, Dict[str, Any]], + input_template: str, expected_message: str + ): + actual_message = ApiHelper.resolve_template_placeholders(input_placeholders, input_values, input_template) + assert actual_message == expected_message + + @pytest.mark.parametrize('input_placeholders, input_value, input_template, expected_message', [ + (set(), + {"scalar": 123.2, + "object": {"keyA": {"keyC": True, "keyD": 34}, "keyB": "some string", "arrayScalar": ["value1", "value2"], + "arrayObjects":[{"key1": 123, "key2": False}, {"key3": 1234, "key4": None}]}}, + 'Test template -- {$response.body#/scalar}, {$response.body#/object/arrayObjects/0/key2}', + 'Test template -- {$response.body#/scalar}, {$response.body#/object/arrayObjects/0/key2}'), + ({'{$response.body#/scalar}', '{$response.body#/object/arrayObjects/0/key2}'}, + {"scalar": 123.2, + "object": {"keyA": {"keyC": True, "keyD": 34}, "keyB": "some string", "arrayScalar": ["value1", "value2"], + "arrayObjects":[{"key1": 123, "key2": False}, {"key3": 1234, "key4": None}]}}, + 'Test template -- {$response.body#/scalar}, {$response.body#/object/arrayObjects/0/key2}', + 'Test template -- 123.2, False'), + ({'{$response.body#/scalar}', '{$response.body#/object/arrayObjects/0/key2}'}, + {"object": {"keyA": {"keyC": True, "keyD": 34}, "keyB": "some string", "arrayScalar": ["value1", "value2"], + "arrayObjects": [{"key1": 123, "key2": False}, {"key3": 1234, "key4": None}]}}, + 'Test template -- {$response.body#/scalar}, {$response.body#/object/arrayObjects/0/key2}', + 'Test template -- , False'), + ({'{$response.body}'}, + {"scalar": 123.2, + "object": {"keyA": {"keyC": True, "keyD": 34}, "keyB": "some string", "arrayScalar": ["value1", "value2"], + "arrayObjects": [{"key1": 123, "key2": False}, {"key3": 1234, "key4": None}]}}, + 'Test template -- {$response.body}', + 'Test template -- {"scalar": 123.2, "object": {"keyA": {"keyC": true, "keyD": 34}, "keyB": "some string", ' + '"arrayScalar": ["value1", "value2"], ' + '"arrayObjects": [{"key1": 123, "key2": false}, {"key3": 1234, "key4": null}]}}'), + ({'{$response.body#/object}'}, + {"scalar": 123.2, + "object": {"keyA": {"keyC": True, "keyD": 34}, "keyB": "some string", "arrayScalar": ["value1", "value2"], + "arrayObjects": [{"key1": 123, "key2": False}, {"key3": 1234, "key4": None}]}}, + 'Test template -- {$response.body#/object}', + 'Test template -- {"keyA": {"keyC": true, "keyD": 34}, "keyB": "some string", "arrayScalar": ' + '["value1", "value2"], "arrayObjects": [{"key1": 123, "key2": false}, {"key3": 1234, "key4": null}]}'), + ({'{$response.body}'}, + None, + 'Test template -- {$response.body}', + 'Test template -- ') + ]) + @validate_call + def test_resolve_template_placeholders_using_json_pointer( + self, input_placeholders: Set[str], input_value: Optional[Dict[str, Any]], input_template: str, + expected_message: str + ): + actual_message = ApiHelper.resolve_template_placeholders_using_json_pointer( + input_placeholders, input_value, input_template) + assert actual_message == expected_message + + @pytest.mark.parametrize('input_union_type, input_value, input_should_deserialize, expected_value', [ + (OneOf([LeafType(int), LeafType(str)]), 100, False, 100), + (OneOf([LeafType(int), LeafType(str)], UnionTypeContext(is_array=True)), '[100, "200"]', True, + [100, '200']), + ]) + @validate_call(config=dict(arbitrary_types_allowed=True)) + def test_union_type_deserialize( + self, input_union_type: UnionType, input_value: Any, input_should_deserialize: bool, expected_value: Any + ): + actual_value = ApiHelper.deserialize_union_type(input_union_type, input_value, input_should_deserialize) + assert actual_value == expected_value + + @pytest.mark.parametrize('input_url, expected_url', [ + ("https://www.example.com/path/to/resource?param1=value1¶m2=value2", "https://www.example.com/path/to/resource"), + ("https://www.example.com/path/to/resource", "https://www.example.com/path/to/resource") + ]) + @validate_call + def test_get_url_without_query(self, input_url: str, expected_url: str): + assert ApiHelper.get_url_without_query(input_url) == expected_url + + @pytest.mark.parametrize('input_url, expected_error_message', [ + ("", "Error parsing URL: Invalid URL format"), + ("invalid_url", "Error parsing URL: Invalid URL format") + ]) + @validate_call + def test_get_url_without_query_with_invalid_url(self, input_url: str, expected_error_message: str): + with pytest.raises(ValueError) as excinfo: + ApiHelper.get_url_without_query(input_url) + assert str(excinfo.value) == expected_error_message + + @pytest.mark.parametrize('input_list, expected_output', [ + (None, None), + ([], []), + (["HELLO"], ["hello"]), + (["hElLo", "worLd"], ["hello", "world"]) + ]) + @validate_call + def test_to_lower_case(self, input_list: Optional[List[str]], expected_output: Optional[List[str]]): + """Tests if an empty list returns an empty list.""" + actual_output = ApiHelper.to_lower_case(input_list) + + assert actual_output == expected_output \ No newline at end of file diff --git a/tests/apimatic_core/utility_tests/test_auth_helper.py b/tests/apimatic_core/utility_tests/test_auth_helper.py index fc28c87..784ecfc 100644 --- a/tests/apimatic_core/utility_tests/test_auth_helper.py +++ b/tests/apimatic_core/utility_tests/test_auth_helper.py @@ -1,52 +1,62 @@ +from pydantic import validate_call + from apimatic_core.utilities.auth_helper import AuthHelper class TestAuthHelper: + @validate_call def test_base64_encoded_none_value(self): actual_base64_encoded_value = AuthHelper.get_base64_encoded_value() - expected_base64_encoded_value = None - assert actual_base64_encoded_value == expected_base64_encoded_value + assert actual_base64_encoded_value is None + + @validate_call def test_base64_encoded_value(self): actual_base64_encoded_value = AuthHelper.get_base64_encoded_value('test_username', 'test_password') - expected_base64_encoded_value = 'dGVzdF91c2VybmFtZTp0ZXN0X3Bhc3N3b3Jk' - assert actual_base64_encoded_value == expected_base64_encoded_value + assert actual_base64_encoded_value == 'dGVzdF91c2VybmFtZTp0ZXN0X3Bhc3N3b3Jk' + + @validate_call def test_token_expiry(self): current_utc_timestamp = AuthHelper.get_current_utc_timestamp() actual_token_expiry_value = AuthHelper.get_token_expiry(current_utc_timestamp, 5) expected_token_expiry_value = current_utc_timestamp + int(5) + assert actual_token_expiry_value == expected_token_expiry_value + @validate_call def test_token_is_expired(self): past_timestamp = AuthHelper.get_current_utc_timestamp() - 5 actual_token_expired = AuthHelper.is_token_expired(past_timestamp) - expected_token_expired = True - assert actual_token_expired == expected_token_expired + assert actual_token_expired is True + + @validate_call def test_token_is_expired_with_clock_skew(self): past_timestamp = AuthHelper.get_current_utc_timestamp() + 5 actual_token_expired = AuthHelper.is_token_expired(past_timestamp, 10) - expected_token_expired = True - assert actual_token_expired == expected_token_expired + assert actual_token_expired is True + + @validate_call def test_token_is_not_expired_with_clock_skew(self): past_timestamp = AuthHelper.get_current_utc_timestamp() + 5 actual_token_expired = AuthHelper.is_token_expired(past_timestamp, 3) - expected_token_expired = False - assert actual_token_expired == expected_token_expired + + assert actual_token_expired is False past_timestamp = AuthHelper.get_current_utc_timestamp() + 5 actual_token_expired = AuthHelper.is_token_expired(past_timestamp, 5) - expected_token_expired = False - assert actual_token_expired == expected_token_expired + assert actual_token_expired is False + + @validate_call def test_token_is_not_expired(self): past_timestamp = AuthHelper.get_current_utc_timestamp() + 5 actual_token_expired = AuthHelper.is_token_expired(past_timestamp) - expected_token_expired = False - assert actual_token_expired == expected_token_expired + + assert actual_token_expired is False diff --git a/tests/apimatic_core/utility_tests/test_auth_helper.py~ b/tests/apimatic_core/utility_tests/test_auth_helper.py~ new file mode 100644 index 0000000..2a44894 --- /dev/null +++ b/tests/apimatic_core/utility_tests/test_auth_helper.py~ @@ -0,0 +1,61 @@ +from pydantic import validate_call + +from apimatic_core.utilities.auth_helper import AuthHelper + + +class TestAuthHelper: + + @validate_call + def test_base64_encoded_none_value(self): + actual_base64_encoded_value = AuthHelper.get_base64_encoded_value() + + assert actual_base64_encoded_value is None + + @validate_call + def test_base64_encoded_value(self): + actual_base64_encoded_value = AuthHelper.get_base64_encoded_value('test_username', 'test_password') + + assert actual_base64_encoded_value == 'dGVzdF91c2VybmFtZTp0ZXN0X3Bhc3N3b3Jk' + + @validate_call + def test_token_expiry(self): + current_utc_timestamp = AuthHelper.get_current_utc_timestamp() + actual_token_expiry_value = AuthHelper.get_token_expiry(current_utc_timestamp, 5) + expected_token_expiry_value = current_utc_timestamp + int(5) + + assert actual_token_expiry_value == expected_token_expiry_value + + @validate_call + def test_token_is_expired(self): + past_timestamp = AuthHelper.get_current_utc_timestamp() - 5 + actual_token_expired = AuthHelper.is_token_expired(past_timestamp) + + assert actual_token_expired is True + + @validate_call + def test_token_is_expired_with_clock_skew(self): + past_timestamp = AuthHelper.get_current_utc_timestamp() + 5 + actual_token_expired = AuthHelper.is_token_expired(past_timestamp, 10) + + assert actual_token_expired is True + + @validate_call + def test_token_is_not_expired_with_clock_skew(self): + past_timestamp = AuthHelper.get_current_utc_timestamp() + 5 + actual_token_expired = AuthHelper.is_token_expired(past_timestamp, 3) + + assert actual_token_expired is False + + past_timestamp = AuthHelper.get_current_utc_timestamp() + 5 + actual_token_expired = AuthHelper.is_token_expired(past_timestamp, 5) + + assert actual_token_expired is False + + def test_token_is_not_expired(self): + past_timestamp = AuthHelper.get_current_utc_timestamp() + 5 + actual_token_expired = AuthHelper.is_token_expired(past_timestamp) + + assert actual_token_expired is False + + + diff --git a/tests/apimatic_core/utility_tests/test_comparison_helper.py b/tests/apimatic_core/utility_tests/test_comparison_helper.py index b9456b7..ffc6768 100644 --- a/tests/apimatic_core/utility_tests/test_comparison_helper.py +++ b/tests/apimatic_core/utility_tests/test_comparison_helper.py @@ -1,9 +1,13 @@ import pytest +from typing import Dict, Any, Union, List, TypeVar + +from pydantic import validate_call from apimatic_core.utilities.comparison_helper import ComparisonHelper class TestComparisonHelper: + T = TypeVar("T") @pytest.mark.parametrize('input_expected_headers, input_received_headers, ' 'input_should_allow_extra, expected_output', @@ -26,10 +30,14 @@ class TestComparisonHelper: ({'content-type': 'application/json'}, {'content-type': 'application/json', 'accept': 'application/json'}, True, True) ]) - def test_match_headers(self, input_expected_headers, input_received_headers, - input_should_allow_extra, expected_output): - actual_output = ComparisonHelper.match_headers(input_expected_headers, input_received_headers - , input_should_allow_extra) + @validate_call + def test_match_headers( + self, input_expected_headers: Dict[str, Any], input_received_headers: Dict[str, Any], + input_should_allow_extra: bool, expected_output: bool + ): + actual_output = ComparisonHelper.match_headers( + input_expected_headers, input_received_headers, input_should_allow_extra) + assert actual_output is expected_output @pytest.mark.parametrize('input_expected_body, input_received_body, input_check_values, ' @@ -2118,8 +2126,12 @@ def test_match_headers(self, input_expected_headers, input_received_headers, } }, False, False, False, True) # 97 ]) - def test_match_body(self, input_expected_body, input_received_body, input_check_values, input_check_order, - input_check_count, expected_output): - actual_output = ComparisonHelper.match_body(input_expected_body, input_received_body, input_check_values, - input_check_order, input_check_count) + @validate_call + def test_match_body( + self, input_expected_body: Union[Dict[str, Any], List[Any], T], + input_received_body: Union[Dict[str, Any], List[Any], T], input_check_values: bool, + input_check_order: bool, input_check_count: bool, expected_output: bool): + actual_output = ComparisonHelper.match_body( + input_expected_body, input_received_body, input_check_values, input_check_order, input_check_count) + assert actual_output is expected_output diff --git a/tests/apimatic_core/utility_tests/test_datetime_helper.py b/tests/apimatic_core/utility_tests/test_datetime_helper.py index 51edd33..bda99b8 100644 --- a/tests/apimatic_core/utility_tests/test_datetime_helper.py +++ b/tests/apimatic_core/utility_tests/test_datetime_helper.py @@ -1,7 +1,10 @@ -from datetime import datetime, date +from datetime import date +from typing import Union, Optional + import pytest -from apimatic_core.types.datetime_format import DateTimeFormat -from apimatic_core.utilities.api_helper import ApiHelper +from apimatic_core_interfaces.formats.datetime_format import DateTimeFormat +from pydantic import validate_call + from apimatic_core.utilities.datetime_helper import DateTimeHelper @@ -20,8 +23,13 @@ class TestDateTimeHelper: ('Sun, 06 Nov 1994 03:49:37 GMT', DateTimeFormat.UNIX_DATE_TIME, False), ('Sun, 06 Nov 1994 03:49:37 GMT', None, False) ]) - def test_is_valid_datetime(self, input_dt, input_datetime_format, expected_output): + @validate_call + def test_is_valid_datetime( + self, input_dt: Union[str, int, float], input_datetime_format: Optional[DateTimeFormat], + expected_output: bool + ): actual_output = DateTimeHelper.validate_datetime(input_dt, input_datetime_format) + assert actual_output == expected_output @pytest.mark.parametrize('input_date, expected_output', [ @@ -34,10 +42,64 @@ def test_is_valid_datetime(self, input_dt, input_datetime_format, expected_outpu ('1941106', False), ('1994=11=06', False) ]) - def test_is_valid_date(self, input_date, expected_output): + @validate_call + def test_is_valid_date(self, input_date: Union[date, str], expected_output: bool): actual_output = DateTimeHelper.validate_date(input_date) assert actual_output == expected_output + # Valid RFC 1123 datetime string returns True + @validate_call + def test_valid_rfc1123_datetime_returns_true(self): + datetime_str = "Sun, 06 Nov 1994 08:49:37 GMT" + + # Assert that valid RFC 1123 string returns True + assert DateTimeHelper.is_rfc_1123(datetime_str) is True + + # Empty string input returns False + @validate_call + def test_empty_rfc1123_string_returns_false(self): + datetime_str = "" + + # Assert that empty string returns False + assert DateTimeHelper.is_rfc_1123(datetime_str) is False + + # Valid RFC 3339 datetime string without milliseconds returns true + @validate_call + def test_valid_rfc3339_datetime_without_ms_returns_true(self): + datetime_str = "2023-12-25T10:30:00" + result = DateTimeHelper.is_rfc_3339(datetime_str) + + # Assert + assert result is True + + # Empty string input returns false + @validate_call + def test_empty_rfc3339_string_returns_false(self): + datetime_str = "" + result = DateTimeHelper.is_rfc_3339(datetime_str) + + assert result is False + + # Valid integer Unix timestamp returns True + @validate_call + def test_valid_integer_timestamp_returns_true(self): + # Current timestamp + timestamp = 1672531200 + result = DateTimeHelper.is_unix_timestamp(timestamp) + assert result is True + + # Zero timestamp returns True + @validate_call + def test_zero_timestamp_returns_true(self): + timestamp = 0 + result = DateTimeHelper.is_unix_timestamp(timestamp) + assert result is True + # Timestamp string with whitespace returns False + @validate_call + def test_timestamp_string_with_whitespace_returns_false(self): + timestamp = "" + result = DateTimeHelper.is_unix_timestamp(timestamp) + assert result is False diff --git a/tests/apimatic_core/utility_tests/test_datetime_helper.py~ b/tests/apimatic_core/utility_tests/test_datetime_helper.py~ new file mode 100644 index 0000000..b13dd85 --- /dev/null +++ b/tests/apimatic_core/utility_tests/test_datetime_helper.py~ @@ -0,0 +1,104 @@ +from datetime import date +from typing import Union, Optional + +import pytest +from pydantic import validate_call + +from apimatic_core.utilities.datetime_helper import DateTimeHelper + + +class TestDateTimeHelper: + + @pytest.mark.parametrize('input_dt, input_datetime_format, expected_output', [ + ('1994-11-06T08:49:37', DateTimeFormat.RFC3339_DATE_TIME, True), + ('1994-02-13T14:01:54.656647Z', DateTimeFormat.RFC3339_DATE_TIME, True), + ('Sun, 06 Nov 1994 03:49:37 GMT', DateTimeFormat.HTTP_DATE_TIME, True), + (1480809600, DateTimeFormat.UNIX_DATE_TIME, True), + ('1994-11-06T08:49:37', DateTimeFormat.HTTP_DATE_TIME, False), + (1480809600, DateTimeFormat.HTTP_DATE_TIME, False), + ('Sun, 06 Nov 1994 03:49:37 GMT', DateTimeFormat.RFC3339_DATE_TIME, False), + (1480809600, DateTimeFormat.RFC3339_DATE_TIME, False), + ('1994-11-06T08:49:37', DateTimeFormat.UNIX_DATE_TIME, False), + ('Sun, 06 Nov 1994 03:49:37 GMT', DateTimeFormat.UNIX_DATE_TIME, False), + ('Sun, 06 Nov 1994 03:49:37 GMT', None, False) + ]) + @validate_call + def test_is_valid_datetime( + self, input_dt: Union[str, int, float], input_datetime_format: Optional[DateTimeFormat], + expected_output: bool + ): + actual_output = DateTimeHelper.validate_datetime(input_dt, input_datetime_format) + + assert actual_output == expected_output + + @pytest.mark.parametrize('input_date, expected_output', [ + ('1994-11-06', True), + (date(1994, 11, 6), True), + (date(94, 11, 6), True), + ('1994/11/06', False), + ('19941106', False), + ('941106', False), + ('1941106', False), + ('1994=11=06', False) + ]) + @validate_call + def test_is_valid_date(self, input_date: Union[date, str], expected_output: bool): + actual_output = DateTimeHelper.validate_date(input_date) + assert actual_output == expected_output + + # Valid RFC 1123 datetime string returns True + @validate_call + def test_valid_rfc1123_datetime_returns_true(self): + datetime_str = "Sun, 06 Nov 1994 08:49:37 GMT" + + # Assert that valid RFC 1123 string returns True + assert DateTimeHelper.is_rfc_1123(datetime_str) is True + + # Empty string input returns False + @validate_call + def test_empty_rfc1123_string_returns_false(self): + datetime_str = "" + + # Assert that empty string returns False + assert DateTimeHelper.is_rfc_1123(datetime_str) is False + + # Valid RFC 3339 datetime string without milliseconds returns true + @validate_call + def test_valid_rfc3339_datetime_without_ms_returns_true(self): + datetime_str = "2023-12-25T10:30:00" + result = DateTimeHelper.is_rfc_3339(datetime_str) + + # Assert + assert result is True + + # Empty string input returns false + @validate_call + def test_empty_rfc3339_string_returns_false(self): + datetime_str = "" + result = DateTimeHelper.is_rfc_3339(datetime_str) + + assert result is False + + # Valid integer Unix timestamp returns True + @validate_call + def test_valid_integer_timestamp_returns_true(self): + # Current timestamp + timestamp = 1672531200 + result = DateTimeHelper.is_unix_timestamp(timestamp) + assert result is True + + # Zero timestamp returns True + @validate_call + def test_zero_timestamp_returns_true(self): + timestamp = 0 + result = DateTimeHelper.is_unix_timestamp(timestamp) + assert result is True + + # Timestamp string with whitespace returns False + @validate_call + def test_timestamp_string_with_whitespace_returns_false(self): + timestamp = "" + result = DateTimeHelper.is_unix_timestamp(timestamp) + assert result is False + + diff --git a/tests/apimatic_core/utility_tests/test_file_helper.py b/tests/apimatic_core/utility_tests/test_file_helper.py index fce8a2e..2945c24 100644 --- a/tests/apimatic_core/utility_tests/test_file_helper.py +++ b/tests/apimatic_core/utility_tests/test_file_helper.py @@ -1,12 +1,17 @@ +from typing import IO + +from pydantic import validate_call + from apimatic_core.utilities.file_helper import FileHelper class TestFileHelper: + @validate_call def test_get_file(self): - file_url = 'https://gist.githubusercontent.com/asadali214/' \ + file_url: str = 'https://gist.githubusercontent.com/asadali214/' \ '0a64efec5353d351818475f928c50767/raw/8ad3533799ecb4e01a753aaf04d248e6702d4947/testFile.txt' - actualFile = FileHelper.get_file(file_url) - assert actualFile is not None \ - and actualFile.read() == 'This test file is created to test CoreFileWrapper ' \ + actual_file: IO[bytes] = FileHelper.get_file(file_url) + assert actual_file is not None \ + and actual_file.read() == 'This test file is created to test CoreFileWrapper ' \ 'functionality'.encode('ascii') diff --git a/tests/apimatic_core/utility_tests/test_file_helper.py~ b/tests/apimatic_core/utility_tests/test_file_helper.py~ new file mode 100644 index 0000000..4f01549 --- /dev/null +++ b/tests/apimatic_core/utility_tests/test_file_helper.py~ @@ -0,0 +1,14 @@ +from typing import IO + +from apimatic_core.utilities.file_helper import FileHelper + + +class TestFileHelper: + + def test_get_file(self): + file_url: str = 'https://gist.githubusercontent.com/asadali214/' \ + '0a64efec5353d351818475f928c50767/raw/8ad3533799ecb4e01a753aaf04d248e6702d4947/testFile.txt' + actual_file: IO[bytes] = FileHelper.get_file(file_url) + assert actual_file is not None \ + and actual_file.read() == 'This test file is created to test CoreFileWrapper ' \ + 'functionality'.encode('ascii') diff --git a/tests/apimatic_core/utility_tests/test_xml_helper.py b/tests/apimatic_core/utility_tests/test_xml_helper.py index 8f6704f..dfec0e2 100644 --- a/tests/apimatic_core/utility_tests/test_xml_helper.py +++ b/tests/apimatic_core/utility_tests/test_xml_helper.py @@ -1,6 +1,11 @@ from datetime import date, datetime +from typing import Any, List, Dict, Type, Optional, Tuple, Union + import pytest import sys + +from pydantic import validate_call + from apimatic_core.utilities.api_helper import ApiHelper import xml.etree.ElementTree as ET from apimatic_core.utilities.xml_helper import XmlHelper @@ -40,12 +45,14 @@ class TestXMLHelper: '' ''), ]) - def test_serialize_to_xml(self, input_value, root_element_name, expected_value): + @validate_call + def test_serialize_to_xml(self, input_value: Any, root_element_name: str, expected_value: str): if sys.version_info[1] == 7: expected_value = expected_value.replace('string="String" number="10000" boolean="false">', 'boolean="false" number="10000" string="String">') actual_value = XmlHelper.serialize_to_xml(input_value, root_element_name) + assert actual_value == expected_value @pytest.mark.parametrize('input_value, root_element_name, array_element_name, expected_value', [ @@ -121,11 +128,14 @@ def test_serialize_to_xml(self, input_value, root_element_name, expected_value): '' '') ]) - def test_serialize_list_to_xml(self, input_value, root_element_name, array_element_name, expected_value): + @validate_call + def test_serialize_list_to_xml( + self, input_value: List[Any], root_element_name: str, array_element_name: str, expected_value: str): if sys.version_info[1] == 7: expected_value = expected_value.replace('string="String" number="10000" boolean="false">', 'boolean="false" number="10000" string="String">') actual_value = XmlHelper.serialize_list_to_xml(input_value, root_element_name, array_element_name) + assert actual_value == expected_value @pytest.mark.parametrize('input_value, root_element_name, expected_value', [ @@ -202,11 +212,14 @@ def test_serialize_list_to_xml(self, input_value, root_element_name, array_eleme '' ''), ]) - def test_serialize_dictionary_to_xml(self, input_value, root_element_name, expected_value): + @validate_call + def test_serialize_dictionary_to_xml( + self, input_value: Dict[str, Any], root_element_name: str, expected_value: str): if sys.version_info[1] == 7: expected_value = expected_value.replace('string="String" number="10000" boolean="false">', 'boolean="false" number="10000" string="String">') actual_value = XmlHelper.serialize_dict_to_xml(input_value, root_element_name) + assert actual_value == expected_value @pytest.mark.parametrize('input_value, root_element_name, expected_value', [ @@ -215,10 +228,12 @@ def test_serialize_dictionary_to_xml(self, input_value, root_element_name, expec (True, 'root', 'true'), ('True', 'root', 'True'), ]) - def test_add_to_element(self, input_value, root_element_name, expected_value): + @validate_call + def test_add_to_element(self, input_value: Any, root_element_name: str, expected_value: str): root_element = ET.Element(root_element_name) XmlHelper.add_to_element(root_element, input_value) actual_value = ET.tostring(root_element).decode() + assert actual_value == expected_value @pytest.mark.parametrize('input_value, root_element_name, attribute_name, expected_value', [ @@ -227,10 +242,12 @@ def test_add_to_element(self, input_value, root_element_name, expected_value): (True, 'root', 'attribute', ''), ('True', 'root', 'attribute', ''), ]) - def test_add_as_attribute(self, input_value, root_element_name, attribute_name, expected_value): + @validate_call + def test_add_as_attribute(self, input_value: Any, root_element_name: str, attribute_name: str, expected_value: str): root_element = ET.Element(root_element_name) XmlHelper.add_as_attribute(root_element, input_value, attribute_name) actual_value = ET.tostring(root_element).decode() + assert actual_value == expected_value @pytest.mark.parametrize('input_value, root_element_name, element_name, expected_value', [ @@ -239,10 +256,12 @@ def test_add_as_attribute(self, input_value, root_element_name, attribute_name, (True, 'root', 'element', 'true'), ('True', 'root', 'element', 'True') ]) - def test_add_as_sub_element(self, input_value, root_element_name, element_name, expected_value): + @validate_call + def test_add_as_sub_element(self, input_value: Any, root_element_name: str, element_name: str, expected_value: str): root_element = ET.Element(root_element_name) XmlHelper.add_as_subelement(root_element, input_value, element_name) actual_value = ET.tostring(root_element).decode() + assert actual_value == expected_value @pytest.mark.parametrize('input_value, root_element_name, item_name, wrapping_element_name, expected_value', [ @@ -257,11 +276,14 @@ def test_add_as_sub_element(self, input_value, root_element_name, element_name, ([True, False, True], 'root', 'Item', None, 'truefalsetrue') ]) - def test_add_list_as_sub_element(self, input_value, root_element_name, item_name, wrapping_element_name, - expected_value): + @validate_call + def test_add_list_as_sub_element( + self, input_value: List[Any], root_element_name: str, item_name: str, wrapping_element_name: Optional[str], + expected_value: str): root_element = ET.Element(root_element_name) XmlHelper.add_list_as_subelement(root_element, input_value, item_name, wrapping_element_name) actual_value = ET.tostring(root_element).decode() + assert actual_value == expected_value @pytest.mark.parametrize('input_value, root_element_name, dictionary_name, expected_value', [ @@ -276,10 +298,13 @@ def test_add_list_as_sub_element(self, input_value, root_element_name, item_name ({'Item1': True, 'Item2': False, 'Item3': True}, 'root', 'Dictionary', 'truefalsetrue') ]) - def test_add_dict_as_sub_element(self, input_value, root_element_name, dictionary_name, expected_value): + @validate_call + def test_add_dict_as_sub_element( + self, input_value: Dict[str, Any], root_element_name: str, dictionary_name: str, expected_value: str): root_element = ET.Element(root_element_name) XmlHelper.add_dict_as_subelement(root_element, input_value, dictionary_name) actual_value = ET.tostring(root_element).decode() + assert actual_value == expected_value @pytest.mark.parametrize('input_value, clazz, root_element_name, expected_value', [ @@ -332,10 +357,15 @@ def test_add_dict_as_sub_element(self, input_value, root_element_name, dictionar '' '', OneOfXML, 'Root', XmlHelper.serialize_to_xml(Base.one_of_xml_wolf_model(), 'Root')), - (None, int, None, None) + (None, int, '', None) ]) - def test_deserialize_xml(self, input_value, clazz, root_element_name, expected_value): + @validate_call + def test_deserialize_xml( + self, input_value: Optional[str], clazz: Type[Any], root_element_name: str, + expected_value: Optional[str] + ): actual_value = XmlHelper.deserialize_xml(input_value, clazz) + if expected_value: assert XmlHelper.serialize_to_xml(actual_value, root_element_name) == expected_value else: @@ -425,12 +455,15 @@ def test_deserialize_xml(self, input_value, clazz, root_element_name, expected_v 'Models', 'Item')), (None, 'Item', int, 'Items', None) ]) - def test_deserialize_xml_to_list(self, input_value, item_name, clazz, root_element_name, expected_value): - actual_value = XmlHelper.deserialize_xml_to_list(input_value, item_name, clazz) - if expected_value: - assert XmlHelper.serialize_list_to_xml(actual_value, root_element_name, item_name) == expected_value - else: - assert actual_value == expected_value + @validate_call + def test_deserialize_xml_to_list( + self, input_value: Optional[str], item_name: str, clazz: Type[Any], root_element_name: str, + expected_value: Optional[str] + ): + deserialized_value = XmlHelper.deserialize_xml_to_list(input_value, item_name, clazz) + actual_value = XmlHelper.serialize_list_to_xml(deserialized_value, root_element_name, item_name) + + assert actual_value == expected_value @pytest.mark.parametrize('input_value, clazz, root_element_name, expected_value', [ ('506070', int, 'Items', @@ -486,14 +519,15 @@ def test_deserialize_xml_to_list(self, input_value, item_name, clazz, root_eleme datetime(1994, 2, 13, 5, 30, 15)) }, 'Items')), - (None, int, None, None) + (None, int, 'Items', None) ]) - def test_deserialize_xml_to_dict(self, input_value, clazz, root_element_name, expected_value): - actual_value = XmlHelper.deserialize_xml_to_dict(input_value, clazz) - if expected_value: - assert XmlHelper.serialize_dict_to_xml(actual_value, root_element_name) == expected_value - else: - assert actual_value == expected_value + @validate_call + def test_deserialize_xml_to_dict( + self, input_value: Optional[str], clazz: Type[Any], root_element_name: str, expected_value: Optional[str]): + deserialized_value = XmlHelper.deserialize_xml_to_dict(input_value, clazz) + actual_value = XmlHelper.serialize_dict_to_xml(deserialized_value, root_element_name) + + assert actual_value == expected_value @pytest.mark.parametrize('input_value, clazz, expected_value', [ ('True', str, 'True'), @@ -502,8 +536,11 @@ def test_deserialize_xml_to_dict(self, input_value, clazz, root_element_name, ex ('70.56443', float, 70.56443), (None, int, None), ]) - def test_value_from_xml_attribute(self, input_value, clazz, expected_value): + @validate_call + def test_value_from_xml_attribute( + self, input_value: Optional[str], clazz: Type[Any], expected_value: Optional[Union[str, int, float, bool]]): actual_value = XmlHelper.value_from_xml_attribute(input_value, clazz) + assert actual_value == expected_value @pytest.mark.parametrize('input_value, clazz, root_element_name, expected_value', [ @@ -511,9 +548,11 @@ def test_value_from_xml_attribute(self, input_value, clazz, expected_value): ('True', bool, 'root', True), ('70', int, 'root', 70), ('70.56443', float, 'root', 70.56443), - (None, int, None, None), + (None, int, '', None), ]) - def test_value_from_xml_element(self, input_value, root_element_name, clazz, expected_value): + @validate_call + def test_value_from_xml_element( + self, input_value: Optional[str], root_element_name: str, clazz: Type[Any], expected_value: Optional[Any]): root_element = None if input_value: root_element = ET.Element(root_element_name) @@ -528,15 +567,17 @@ def test_value_from_xml_element(self, input_value, root_element_name, clazz, exp ([50.58, 60.58, 70.58], 'root', 'item', 'items', float, [50.58, 60.58, 70.58]), ([True, False, True], 'root', 'item', 'items', bool, [True, False, True]), ([True, False, True], 'root', 'item', 'items', bool, [True, False, True]), - (None, None, 'item', 'items', int, None), + (None, 'root', 'item', 'items', int, None), ]) - def test_list_from_xml_element(self, input_value, root_element_name, item_name, - wrapping_element_name, clazz, expected_value): + @validate_call + def test_list_from_xml_element(self, input_value: Optional[List[Any]], root_element_name: str, item_name: str, + wrapping_element_name: str, clazz: Type[Any], expected_value: Optional[List[Any]]): root_element = None if input_value: root_element = ET.Element(root_element_name) XmlHelper.add_list_as_subelement(root_element, input_value, item_name, wrapping_element_name) actual_value = XmlHelper.list_from_xml_element(root_element, item_name, clazz, wrapping_element_name) + assert actual_value == expected_value @pytest.mark.parametrize('input_value, root_element_name, item_name, ' @@ -544,16 +585,18 @@ def test_list_from_xml_element(self, input_value, root_element_name, item_name, 'clazz, expected_value', [ ([50, 60, 70], 'root', 'item', 'items', 'invalid_items', int, None), ]) - def test_list_from_xml_element_with_unset_wrapper(self, input_value, root_element_name, item_name, - serialization_wrapping_element_name, - deserialization_wrapping_element_name, - clazz, expected_value): + @validate_call + def test_list_from_xml_element_with_unset_wrapper( + self, input_value: List[Any], root_element_name: str, item_name: str, + serialization_wrapping_element_name: str, deserialization_wrapping_element_name: str, clazz: Type[Any], + expected_value: Optional[List[Any]]): root_element = None if input_value: root_element = ET.Element(root_element_name) XmlHelper.add_list_as_subelement(root_element, input_value, item_name, serialization_wrapping_element_name) - actual_value = XmlHelper.list_from_xml_element(root_element, item_name, clazz, - deserialization_wrapping_element_name) + + actual_value = XmlHelper.list_from_xml_element( + root_element, item_name, clazz, deserialization_wrapping_element_name) assert actual_value == expected_value @pytest.mark.parametrize('input_value, wrapping_element_name, clazz, expected_value', [ @@ -563,15 +606,17 @@ def test_list_from_xml_element_with_unset_wrapper(self, input_value, root_elemen {'Item1': 50.58, 'Item2': 60.58, 'Item3': 70.58}), ({'Item1': True, 'Item2': False, 'Item3': True}, 'items', bool, {'Item1': True, 'Item2': False, 'Item3': True}), ({'Item1': True, 'Item2': False, 'Item3': True}, 'items', bool, {'Item1': True, 'Item2': False, 'Item3': True}), - (None, None, int, None), + (None, 'root', int, None), ]) - def test_dict_from_xml_element(self, input_value, wrapping_element_name, - clazz, expected_value): + @validate_call + def test_dict_from_xml_element(self, input_value: Optional[Dict[str, Any]], wrapping_element_name: str, + clazz: Type[Any], expected_value: Optional[Dict[str, Any]]): root_element = None if input_value: root_element = ET.Element(wrapping_element_name) XmlHelper.add_dict_as_subelement(root_element, input_value) actual_value = XmlHelper.dict_from_xml_element(root_element, clazz) + assert actual_value == expected_value @pytest.mark.parametrize('input_value, root_element_name, item_name, mapping_data, expected_value', [ @@ -600,12 +645,15 @@ def test_dict_from_xml_element(self, input_value, wrapping_element_name, ''), (None, 'root', 'Wolf', {}, None) ]) - def test_list_from_multiple_one_of_xml_element(self, input_value, root_element_name, item_name, - mapping_data, expected_value): + @validate_call + def test_list_from_multiple_one_of_xml_element( + self, input_value: Any, root_element_name: str, item_name: str, + mapping_data: Dict[str, Any], expected_value: Optional[str]): root_element = ET.Element(root_element_name) if input_value: XmlHelper.add_to_element(root_element, input_value) actual_value = XmlHelper.list_from_multiple_one_of_xml_element(root_element, mapping_data) + if actual_value: assert XmlHelper.serialize_list_to_xml(actual_value, root_element_name, item_name) == expected_value else: @@ -615,5 +663,7 @@ def test_list_from_multiple_one_of_xml_element(self, input_value, root_element_n ({}, None), (None, None) ]) - def test_value_from_one_of_xml_elements(self, input_mapping_data, expected_output): + @validate_call + def test_value_from_one_of_xml_elements( + self, input_mapping_data: Optional[Dict[str, Any]], expected_output: Optional[Any]): assert XmlHelper.value_from_one_of_xml_elements(None, input_mapping_data) == expected_output diff --git a/tests/apimatic_core/utility_tests/test_xml_helper.py~ b/tests/apimatic_core/utility_tests/test_xml_helper.py~ new file mode 100644 index 0000000..4b75ba1 --- /dev/null +++ b/tests/apimatic_core/utility_tests/test_xml_helper.py~ @@ -0,0 +1,670 @@ +from datetime import date, datetime +from typing import Any, List, Dict, Type, Optional, Tuple, Union + +import pytest +import sys + +from pydantic import validate_call + +from apimatic_core.utilities.api_helper import ApiHelper +import xml.etree.ElementTree as ET +from apimatic_core.utilities.xml_helper import XmlHelper +from tests.apimatic_core.base import Base +from tests.apimatic_core.mocks.models.cat_model import CatModel +from tests.apimatic_core.mocks.models.dog_model import DogModel +from tests.apimatic_core.mocks.models.one_of_xml import OneOfXML +from tests.apimatic_core.mocks.models.wolf_model import WolfModel +from tests.apimatic_core.mocks.models.xml_model import XMLModel + + +class TestXMLHelper: + + @pytest.mark.parametrize('input_value, root_element_name, expected_value', [ + (50, 'Number', '50'), + ('50', 'String', '50'), + (50.58, 'Decimal', '50.58'), + (True, 'Boolean', 'true'), + (date(1994, 2, 13), 'Date', '1994-02-13'), + (ApiHelper.UnixDateTime.from_datetime(datetime(1994, 2, 13, 5, 30, 15)), + 'UnixDateTime', '761117415'), + (ApiHelper.HttpDateTime.from_datetime(datetime(1994, 2, 13, 5, 30, 15)), + 'HttpDateTime', '{}' + .format(Base.get_http_datetime(datetime(1994, 2, 13, 5, 30, 15)))), + (ApiHelper.RFC3339DateTime.from_datetime(datetime(1994, 2, 13, 5, 30, 15)), + 'RFC3339DateTime', '{}' + .format(Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15)))), + (Base.xml_model(), 'Model', + '' + 'Hey! I am being tested.' + '5000' + 'false' + '' + 'a' + 'b' + 'c' + '' + ''), + ]) + @validate_call + def test_serialize_to_xml(self, input_value: Any, root_element_name: str, expected_value: str): + if sys.version_info[1] == 7: + expected_value = expected_value.replace('string="String" number="10000" boolean="false">', + 'boolean="false" number="10000" string="String">') + + actual_value = XmlHelper.serialize_to_xml(input_value, root_element_name) + + assert actual_value == expected_value + + @pytest.mark.parametrize('input_value, root_element_name, array_element_name, expected_value', [ + ([50, 60, 70], 'Numbers', 'Item', '506070'), + (['50', '60', '70'], 'Strings', 'Item', + '506070'), + ([50.58, 60.58, 70.58], 'Decimals', 'Item', + '50.5860.5870.58'), + ([True, False, True], 'Booleans', 'Item', + 'truefalsetrue'), + ([date(1994, 2, 13), date(1994, 2, 14), date(1994, 2, 15)], + 'Dates', 'Item', '' + '1994-02-13' + '1994-02-14' + '1994-02-15' + ''), + ([ApiHelper.UnixDateTime.from_datetime(datetime(1994, 2, 13, 5, 30, 15)), + ApiHelper.UnixDateTime.from_datetime(datetime(1994, 2, 14, 5, 30, 15)), + ApiHelper.UnixDateTime.from_datetime(datetime(1994, 2, 15, 5, 30, 15))], + 'DateTimes', 'Item', '' + '761117415' + '761203815' + '761290215' + ''), + ([ApiHelper.HttpDateTime.from_datetime(datetime(1994, 2, 13, 5, 30, 15)), + ApiHelper.HttpDateTime.from_datetime(datetime(1994, 2, 13, 5, 30, 15)), + ApiHelper.HttpDateTime.from_datetime(datetime(1994, 2, 13, 5, 30, 15))], + 'DateTimes', 'Item', "" + f"{Base.get_http_datetime(datetime(1994, 2, 13, 5, 30, 15))}" + f"{Base.get_http_datetime(datetime(1994, 2, 13, 5, 30, 15))}" + f"{Base.get_http_datetime(datetime(1994, 2, 13, 5, 30, 15))}" + ""), + ([ApiHelper.RFC3339DateTime.from_datetime(datetime(1994, 2, 13, 5, 30, 15)), + ApiHelper.RFC3339DateTime.from_datetime(datetime(1994, 2, 13, 5, 30, 15)), + ApiHelper.RFC3339DateTime.from_datetime(datetime(1994, 2, 13, 5, 30, 15))], + 'DateTimes', 'Item', "" + f"{Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15))}" + f"{Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15))}" + f"{Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15))}" + ""), + ([Base.xml_model(), Base.xml_model(), + Base.xml_model()], 'Models', 'Item', + '' + '' + 'Hey! I am being tested.' + '5000' + 'false' + '' + 'a' + 'b' + 'c' + '' + '' + '' + 'Hey! I am being tested.' + '5000' + 'false' + '' + 'a' + 'b' + 'c' + '' + '' + '' + 'Hey! I am being tested.' + '5000' + 'false' + '' + 'a' + 'b' + 'c' + '' + '' + '') + ]) + @validate_call + def test_serialize_list_to_xml( + self, input_value: List[Any], root_element_name: str, array_element_name: str, expected_value: str): + if sys.version_info[1] == 7: + expected_value = expected_value.replace('string="String" number="10000" boolean="false">', + 'boolean="false" number="10000" string="String">') + actual_value = XmlHelper.serialize_list_to_xml(input_value, root_element_name, array_element_name) + + assert actual_value == expected_value + + @pytest.mark.parametrize('input_value, root_element_name, expected_value', [ + ({'Item1': 50, 'Item2': 60, 'Item3': 70}, 'Dictionary', + '506070'), + ({'Item1': '50', 'Item2': '60', 'Item3': '70'}, 'Dictionary', + '506070'), + ({'Item1': 50.58, 'Item2': 60.58, 'Item3': 70.58}, 'Dictionary', + '50.5860.5870.58'), + ({'Item1': True, 'Item2': False, 'Item3': True}, 'Dictionary', + 'truefalsetrue'), + ({'Item1': date(1994, 2, 13), 'Item2': date(1994, 2, 14), 'Item3': date(1994, 2, 15)}, + 'Dictionary', '' + '1994-02-13' + '1994-02-14' + '1994-02-15' + ''), + ({'Item1': ApiHelper.UnixDateTime.from_datetime(datetime(1994, 2, 13, 5, 30, 15)), + 'Item2': ApiHelper.UnixDateTime.from_datetime(datetime(1994, 2, 14, 5, 30, 15)), + 'Item3': ApiHelper.UnixDateTime.from_datetime(datetime(1994, 2, 15, 5, 30, 15))}, + 'Dictionary', '' + '761117415' + '761203815' + '761290215' + ''), + ({'Item1': ApiHelper.HttpDateTime.from_datetime(datetime(1994, 2, 13, 5, 30, 15)), + 'Item2': ApiHelper.HttpDateTime.from_datetime(datetime(1994, 2, 13, 5, 30, 15)), + 'Item3': ApiHelper.HttpDateTime.from_datetime(datetime(1994, 2, 13, 5, 30, 15))}, + 'Dictionary', '' + f"{Base.get_http_datetime(datetime(1994, 2, 13, 5, 30, 15))}" + f"{Base.get_http_datetime(datetime(1994, 2, 13, 5, 30, 15))}" + f"{Base.get_http_datetime(datetime(1994, 2, 13, 5, 30, 15))}" + ''), + ({'Item1': ApiHelper.RFC3339DateTime.from_datetime(datetime(1994, 2, 13, 5, 30, 15)), + 'Item2': ApiHelper.RFC3339DateTime.from_datetime(datetime(1994, 2, 13, 5, 30, 15)), + 'Item3': ApiHelper.RFC3339DateTime.from_datetime(datetime(1994, 2, 13, 5, 30, 15))}, + 'Dictionary', '' + f"{Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15))}" + f"{Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15))}" + f"{Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15))}" + ''), + ({'Item1': Base.xml_model(), 'Item2': Base.xml_model(), + 'Item3': Base.xml_model()}, 'Dictionary', + '' + '' + 'Hey! I am being tested.' + '5000' + 'false' + '' + 'a' + 'b' + 'c' + '' + '' + '' + 'Hey! I am being tested.' + '5000' + 'false' + '' + 'a' + 'b' + 'c' + '' + '' + '' + 'Hey! I am being tested.' + '5000' + 'false' + '' + 'a' + 'b' + 'c' + '' + '' + ''), + ]) + @validate_call + def test_serialize_dictionary_to_xml( + self, input_value: Dict[str, Any], root_element_name: str, expected_value: str): + if sys.version_info[1] == 7: + expected_value = expected_value.replace('string="String" number="10000" boolean="false">', + 'boolean="false" number="10000" string="String">') + actual_value = XmlHelper.serialize_dict_to_xml(input_value, root_element_name) + + assert actual_value == expected_value + + @pytest.mark.parametrize('input_value, root_element_name, expected_value', [ + (70, 'root', '70'), + (70.887, 'root', '70.887'), + (True, 'root', 'true'), + ('True', 'root', 'True'), + ]) + @validate_call + def test_add_to_element(self, input_value: Any, root_element_name: str, expected_value: str): + root_element = ET.Element(root_element_name) + XmlHelper.add_to_element(root_element, input_value) + actual_value = ET.tostring(root_element).decode() + + assert actual_value == expected_value + + @pytest.mark.parametrize('input_value, root_element_name, attribute_name, expected_value', [ + (70, 'root', 'attribute', ''), + (70.887, 'root', 'attribute', ''), + (True, 'root', 'attribute', ''), + ('True', 'root', 'attribute', ''), + ]) + @validate_call + def test_add_as_attribute(self, input_value: Any, root_element_name: str, attribute_name: str, expected_value: str): + root_element = ET.Element(root_element_name) + XmlHelper.add_as_attribute(root_element, input_value, attribute_name) + actual_value = ET.tostring(root_element).decode() + + assert actual_value == expected_value + + @pytest.mark.parametrize('input_value, root_element_name, element_name, expected_value', [ + (70, 'root', 'element', '70'), + (70.887, 'root', 'element', '70.887'), + (True, 'root', 'element', 'true'), + ('True', 'root', 'element', 'True') + ]) + @validate_call + def test_add_as_sub_element(self, input_value: Any, root_element_name: str, element_name: str, expected_value: str): + root_element = ET.Element(root_element_name) + XmlHelper.add_as_subelement(root_element, input_value, element_name) + actual_value = ET.tostring(root_element).decode() + + assert actual_value == expected_value + + @pytest.mark.parametrize('input_value, root_element_name, item_name, wrapping_element_name, expected_value', [ + ([50, 60, 70], 'root', 'Item', 'Numbers', + '506070'), + (['50', '60', '70'], 'root', 'Item', 'Strings', + '506070'), + ([50.58, 60.58, 70.58], 'root', 'Item', 'Decimals', + '50.5860.5870.58'), + ([True, False, True], 'root', 'Item', 'Booleans', + 'truefalsetrue'), + ([True, False, True], 'root', 'Item', None, + 'truefalsetrue') + ]) + @validate_call + def test_add_list_as_sub_element( + self, input_value: List[Any], root_element_name: str, item_name: str, wrapping_element_name: Optional[str], + expected_value: str): + root_element = ET.Element(root_element_name) + XmlHelper.add_list_as_subelement(root_element, input_value, item_name, wrapping_element_name) + actual_value = ET.tostring(root_element).decode() + + assert actual_value == expected_value + + @pytest.mark.parametrize('input_value, root_element_name, dictionary_name, expected_value', [ + ({'Item1': 50, 'Item2': 60, 'Item3': 70}, 'root', 'Dictionary', + '506070'), + ({'Item': [50, 60, 70]}, 'root', 'Dictionary', + '506070'), + ({'Item1': '50', 'Item2': '60', 'Item3': '70'}, 'root', 'Dictionary', + '506070'), + ({'Item1': 50.58, 'Item2': 60.58, 'Item3': 70.58}, 'root', 'Dictionary', + '50.5860.5870.58'), + ({'Item1': True, 'Item2': False, 'Item3': True}, 'root', 'Dictionary', + 'truefalsetrue') + ]) + @validate_call + def test_add_dict_as_sub_element( + self, input_value: Dict[str, Any], root_element_name: str, dictionary_name: str, expected_value: str): + root_element = ET.Element(root_element_name) + XmlHelper.add_dict_as_subelement(root_element, input_value, dictionary_name) + actual_value = ET.tostring(root_element).decode() + + assert actual_value == expected_value + + @pytest.mark.parametrize('input_value, clazz, root_element_name, expected_value', [ + ('50', int, 'Number', XmlHelper.serialize_to_xml(50, 'Number')), + ('50', str, 'String', XmlHelper.serialize_to_xml('50', 'String')), + ('50.58', float, 'Decimal', XmlHelper.serialize_to_xml(50.58, 'Decimal')), + ('true', bool, 'Boolean', XmlHelper.serialize_to_xml(True, 'Boolean')), + ('1994-02-13', date, 'Date', XmlHelper.serialize_to_xml(date(1994, 2, 13), 'Date')), + ('761117415', + ApiHelper.UnixDateTime, 'UnixDateTime', + XmlHelper.serialize_to_xml(ApiHelper.UnixDateTime.from_datetime(datetime(1994, 2, 13, 5, 30, 15)), + 'UnixDateTime')), + ('{}'.format(Base.get_http_datetime(datetime(1994, 2, 13, 5, 30, 15))), + ApiHelper.HttpDateTime, 'HttpDateTime', + XmlHelper.serialize_to_xml(ApiHelper.HttpDateTime.from_datetime(datetime(1994, 2, 13, 5, 30, 15)), + 'HttpDateTime')), + ('{}'.format(Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15))), + ApiHelper.RFC3339DateTime, 'RFC3339DateTime', + XmlHelper.serialize_to_xml(ApiHelper.RFC3339DateTime.from_datetime(datetime(1994, 2, 13, 5, 30, 15)), + 'RFC3339DateTime')), + ('' + 'Hey! I am being tested.' + '5000' + 'false' + '' + 'a' + 'b' + 'c' + '' + '', XMLModel, 'Model', + XmlHelper.serialize_to_xml(Base.xml_model(), 'Model')), + ('' + '' + 'true' + '' + '' + 'false' + '', OneOfXML, 'Root', + XmlHelper.serialize_to_xml(Base.one_of_xml_cat_model(), 'Root')), + ('' + '' + 'true' + '' + '', OneOfXML, 'Root', + XmlHelper.serialize_to_xml(Base.one_of_xml_dog_model(), 'Root')), + ('' + '' + 'true' + 'false' + '' + '', OneOfXML, 'Root', + XmlHelper.serialize_to_xml(Base.one_of_xml_wolf_model(), 'Root')), + (None, int, '', None) + ]) + @validate_call + def test_deserialize_xml( + self, input_value: Optional[str], clazz: Type[Any], root_element_name: str, + expected_value: Optional[str] + ): + actual_value = XmlHelper.deserialize_xml(input_value, clazz) + + if expected_value: + assert XmlHelper.serialize_to_xml(actual_value, root_element_name) == expected_value + else: + assert actual_value == expected_value + + @pytest.mark.parametrize('input_value, item_name, clazz, root_element_name, expected_value', [ + ('506070', 'Item', int, 'Items', + XmlHelper.serialize_list_to_xml([50, 60, 70], 'Items', 'Item')), + ('506070', 'Item', str, 'Items', + XmlHelper.serialize_list_to_xml(['50', '60', '70'], 'Items', 'Item')), + ('50.5860.5870.58', 'Item', float, 'Items', + XmlHelper.serialize_list_to_xml([50.58, 60.58, 70.58], 'Items', 'Item')), + ('truefalsetrue', 'Item', bool, 'Items', + XmlHelper.serialize_list_to_xml([True, False, True], 'Items', 'Item')), + ('' + '1994-02-13' + '1994-02-14' + '1994-02-15' + '', 'Item', date, 'Items', + XmlHelper.serialize_list_to_xml([date(1994, 2, 13), date(1994, 2, 14), date(1994, 2, 15)], 'Items', 'Item')), + ('' + '761117415' + '761203815' + '761290215' + '', 'Item', ApiHelper.UnixDateTime, + 'Items', + XmlHelper.serialize_list_to_xml([ApiHelper.UnixDateTime.from_datetime(datetime(1994, 2, 13, 5, 30, 15)), + ApiHelper.UnixDateTime.from_datetime(datetime(1994, 2, 14, 5, 30, 15)), + ApiHelper.UnixDateTime.from_datetime(datetime(1994, 2, 15, 5, 30, 15))], + 'Items', 'Item')), + ('' + f"{Base.get_http_datetime(datetime(1994, 2, 13, 5, 30, 15))}" + f"{Base.get_http_datetime(datetime(1994, 2, 13, 5, 30, 15))}" + f"{Base.get_http_datetime(datetime(1994, 2, 13, 5, 30, 15))}" + '', 'Item', ApiHelper.HttpDateTime, + 'Items', + XmlHelper.serialize_list_to_xml([ApiHelper.HttpDateTime.from_datetime(datetime(1994, 2, 13, 5, 30, 15)), + ApiHelper.HttpDateTime.from_datetime(datetime(1994, 2, 13, 5, 30, 15)), + ApiHelper.HttpDateTime.from_datetime(datetime(1994, 2, 13, 5, 30, 15))], + 'Items', 'Item')), + ('' + f"{Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15))}" + f"{Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15))}" + f"{Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15))}" + '', 'Item', ApiHelper.RFC3339DateTime, + 'Items', + XmlHelper.serialize_list_to_xml([ApiHelper.RFC3339DateTime.from_datetime(datetime(1994, 2, 13, 5, 30, 15)), + ApiHelper.RFC3339DateTime.from_datetime(datetime(1994, 2, 13, 5, 30, 15)), + ApiHelper.RFC3339DateTime.from_datetime(datetime(1994, 2, 13, 5, 30, 15))], + 'Items', 'Item')), + ('' + '' + 'Hey! I am being tested.' + '5000' + 'false' + '' + 'a' + 'b' + 'c' + '' + '' + '' + 'Hey! I am being tested.' + '5000' + 'false' + '' + 'a' + 'b' + 'c' + '' + '' + '' + 'Hey! I am being tested.' + '5000' + 'false' + '' + 'a' + 'b' + 'c' + '' + '' + '', 'Item', XMLModel, + 'Models', + XmlHelper.serialize_list_to_xml([Base.xml_model(), + Base.xml_model(), + Base.xml_model()], + 'Models', 'Item')), + (None, 'Item', int, 'Items', None) + ]) + @validate_call + def test_deserialize_xml_to_list( + self, input_value: Optional[str], item_name: str, clazz: Type[Any], root_element_name: str, + expected_value: Optional[str] + ): + deserialized_value = XmlHelper.deserialize_xml_to_list(input_value, item_name, clazz) + + actual_value = XmlHelper.serialize_list_to_xml(deserialized_value, root_element_name, item_name) + + assert actual_value == expected_value + + @pytest.mark.parametrize('input_value, clazz, root_element_name, expected_value', [ + ('506070', int, 'Items', + XmlHelper.serialize_dict_to_xml({'Item1': 50, 'Item2': 60, 'Item3': 70}, 'Items')), + ('506070', str, 'Items', + XmlHelper.serialize_dict_to_xml({'Item1': '50', 'Item2': '60', 'Item3': '70'}, 'Items')), + ('50.5860.5870.58', float, 'Items', + XmlHelper.serialize_dict_to_xml({'Item1': 50.58, 'Item2': 60.58, 'Item3': 70.58}, 'Items')), + ('truefalsetrue', bool, 'Items', + XmlHelper.serialize_dict_to_xml({'Item1': True, 'Item2': False, 'Item3': True}, 'Items')), + ('' + '1994-02-13' + '1994-02-14' + '1994-02-15' + '', date, 'Items', + XmlHelper.serialize_dict_to_xml( + {'Item1': date(1994, 2, 13), 'Item2': date(1994, 2, 14), 'Item3': date(1994, 2, 15)}, 'Items')), + ('' + '761117415' + '761203815' + '761290215' + '', ApiHelper.UnixDateTime, + 'Items', + XmlHelper.serialize_dict_to_xml( + {'Item1': ApiHelper.UnixDateTime.from_datetime(datetime(1994, 2, 13, 5, 30, 15)), + 'Item2': ApiHelper.UnixDateTime.from_datetime(datetime(1994, 2, 14, 5, 30, 15)), + 'Item3': ApiHelper.UnixDateTime.from_datetime(datetime(1994, 2, 15, 5, 30, 15))}, + 'Items')), + ('' + f"{Base.get_http_datetime(datetime(1994, 2, 13, 5, 30, 15))}" + f"{Base.get_http_datetime(datetime(1994, 2, 13, 5, 30, 15))}" + f"{Base.get_http_datetime(datetime(1994, 2, 13, 5, 30, 15))}" + '', ApiHelper.HttpDateTime, + 'Items', + XmlHelper.serialize_dict_to_xml( + {'Item1': ApiHelper.HttpDateTime.from_datetime(datetime(1994, 2, 13, 5, 30, 15)), + 'Item2': ApiHelper.HttpDateTime.from_datetime(datetime(1994, 2, 13, 5, 30, 15)), + 'Item3': ApiHelper.HttpDateTime.from_datetime(datetime(1994, 2, 13, 5, 30, 15))}, + 'Items')), + ('' + f"{Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15))}" + f"{Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15))}" + f"{Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15))}" + '', ApiHelper.RFC3339DateTime, + 'Items', + XmlHelper.serialize_dict_to_xml( + { + 'Item1': ApiHelper.RFC3339DateTime.from_datetime( + datetime(1994, 2, 13, 5, 30, 15)), + 'Item2': ApiHelper.RFC3339DateTime.from_datetime( + datetime(1994, 2, 13, 5, 30, 15)), + 'Item3': ApiHelper.RFC3339DateTime.from_datetime( + datetime(1994, 2, 13, 5, 30, 15)) + }, + 'Items')), + (None, int, 'Items', None) + ]) + @validate_call + def test_deserialize_xml_to_dict( + self, input_value: Optional[str], clazz: Type[Any], root_element_name: str, expected_value: Optional[str]): + deserialized_value = XmlHelper.deserialize_xml_to_dict(input_value, clazz) + actual_value = XmlHelper.serialize_dict_to_xml(deserialized_value, root_element_name) + + assert actual_value == expected_value + + @pytest.mark.parametrize('input_value, clazz, expected_value', [ + ('True', str, 'True'), + ('True', bool, True), + ('70', int, 70), + ('70.56443', float, 70.56443), + (None, int, None), + ]) + @validate_call + def test_value_from_xml_attribute( + self, input_value: Optional[str], clazz: Type[Any], expected_value: Optional[Union[str, int, float, bool]]): + actual_value = XmlHelper.value_from_xml_attribute(input_value, clazz) + + assert actual_value == expected_value + + @pytest.mark.parametrize('input_value, clazz, root_element_name, expected_value', [ + ('True', str, 'root', 'True'), + ('True', bool, 'root', True), + ('70', int, 'root', 70), + ('70.56443', float, 'root', 70.56443), + (None, int, '', None), + ]) + @validate_call + def test_value_from_xml_element( + self, input_value: Optional[str], root_element_name: str, clazz: Type[Any], expected_value: Optional[Any]): + root_element = None + if input_value: + root_element = ET.Element(root_element_name) + XmlHelper.add_to_element(root_element, input_value) + actual_value = XmlHelper.value_from_xml_element(root_element, clazz) + assert actual_value == expected_value + + @pytest.mark.parametrize('input_value, root_element_name, item_name, ' + 'wrapping_element_name, clazz, expected_value', [ + ([50, 60, 70], 'root', 'item', 'items', int, [50, 60, 70]), + (['50', '60', '70'], 'root', 'item', 'items', str, ['50', '60', '70']), + ([50.58, 60.58, 70.58], 'root', 'item', 'items', float, [50.58, 60.58, 70.58]), + ([True, False, True], 'root', 'item', 'items', bool, [True, False, True]), + ([True, False, True], 'root', 'item', 'items', bool, [True, False, True]), + (None, 'root', 'item', 'items', int, None), + ]) + @validate_call + def test_list_from_xml_element(self, input_value: Optional[List[Any]], root_element_name: str, item_name: str, + wrapping_element_name: str, clazz: Type[Any], expected_value: Optional[List[Any]]): + root_element = None + if input_value: + root_element = ET.Element(root_element_name) + XmlHelper.add_list_as_subelement(root_element, input_value, item_name, wrapping_element_name) + actual_value = XmlHelper.list_from_xml_element(root_element, item_name, clazz, wrapping_element_name) + + assert actual_value == expected_value + + @pytest.mark.parametrize('input_value, root_element_name, item_name, ' + 'serialization_wrapping_element_name, deserialization_wrapping_element_name,' + 'clazz, expected_value', [ + ([50, 60, 70], 'root', 'item', 'items', 'invalid_items', int, None), + ]) + @validate_call + def test_list_from_xml_element_with_unset_wrapper( + self, input_value: List[Any], root_element_name: str, item_name: str, + serialization_wrapping_element_name: str, deserialization_wrapping_element_name: str, clazz: Type[Any], + expected_value: Optional[List[Any]]): + root_element = None + if input_value: + root_element = ET.Element(root_element_name) + XmlHelper.add_list_as_subelement(root_element, input_value, item_name, serialization_wrapping_element_name) + + actual_value = XmlHelper.list_from_xml_element( + root_element, item_name, clazz, deserialization_wrapping_element_name) + assert actual_value == expected_value + + @pytest.mark.parametrize('input_value, wrapping_element_name, clazz, expected_value', [ + ({'Item1': 50, 'Item2': 60, 'Item3': 70}, 'items', int, {'Item1': 50, 'Item2': 60, 'Item3': 70}), + ({'Item1': '50', 'Item2': '60', 'Item3': '70'}, 'items', str, {'Item1': '50', 'Item2': '60', 'Item3': '70'}), + ({'Item1': 50.58, 'Item2': 60.58, 'Item3': 70.58}, 'items', float, + {'Item1': 50.58, 'Item2': 60.58, 'Item3': 70.58}), + ({'Item1': True, 'Item2': False, 'Item3': True}, 'items', bool, {'Item1': True, 'Item2': False, 'Item3': True}), + ({'Item1': True, 'Item2': False, 'Item3': True}, 'items', bool, {'Item1': True, 'Item2': False, 'Item3': True}), + (None, 'root', int, None), + ]) + @validate_call + def test_dict_from_xml_element(self, input_value: Optional[Dict[str, Any]], wrapping_element_name: str, + clazz: Type[Any], expected_value: Optional[Dict[str, Any]]): + root_element = None + if input_value: + root_element = ET.Element(wrapping_element_name) + XmlHelper.add_dict_as_subelement(root_element, input_value) + actual_value = XmlHelper.dict_from_xml_element(root_element, clazz) + + assert actual_value == expected_value + + @pytest.mark.parametrize('input_value, root_element_name, item_name, mapping_data, expected_value', [ + (Base.one_of_xml_dog_model(), 'root', 'Dog', { + 'Cat': (CatModel, True, None), + 'Dog': (DogModel, False, None), + 'Wolf': (WolfModel, True, 'Items'), + }, '' + 'true' + ''), + (Base.one_of_xml_cat_model(), 'root', 'Cat', { + 'Cat': (CatModel, True, None), + 'Dog': (DogModel, False, None), + 'Wolf': (WolfModel, True, 'Items'), + }, '' + 'true' + 'false' + ''), + (Base.one_of_xml_wolf_model(), 'root', 'Wolf', { + 'Cat': (CatModel, True, None), + 'Dog': (DogModel, False, None), + 'Wolf': (WolfModel, True, 'Items'), + }, '' + 'true' + 'false' + ''), + (None, 'root', 'Wolf', {}, None) + ]) + @validate_call + def test_list_from_multiple_one_of_xml_element( + self, input_value: Any, root_element_name: str, item_name: str, + mapping_data: Dict[str, Any], expected_value: Optional[str]): + root_element = ET.Element(root_element_name) + if input_value: + XmlHelper.add_to_element(root_element, input_value) + actual_value = XmlHelper.list_from_multiple_one_of_xml_element(root_element, mapping_data) + + if actual_value: + assert XmlHelper.serialize_list_to_xml(actual_value, root_element_name, item_name) == expected_value + else: + assert input_value == expected_value + + @pytest.mark.parametrize('input_mapping_data, expected_output', [ + ({}, None), + (None, None) + ]) + @validate_call + def test_value_from_one_of_xml_elements( + self, input_mapping_data: Optional[Dict[str, Any]], expected_output: Optional[Any]): + assert XmlHelper.value_from_one_of_xml_elements(None, input_mapping_data) == expected_output