CVE-2020-15142 (Python, published 2020-08-14T17:15Z)
Description: value: In openapi-python-client before version 0.5.3, clients generated with a maliciously crafted OpenAPI Document can generate arbitrary Python code. Subsequent execution of this malicious client is arbitrary code execution.
Severity: MEDIUM (CVSS v3 base score: 9)
Weakness: CWE-94 \u2014 Improper Control of Generation of Code ('Code Injection')
The product constructs all or part of a code segment using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the syntax or behavior of the intended code segment.
Repository: https://github.com/triaxtec/openapi-python-client
Files changed:
CHANGELOG.md
end_to_end_tests/fastapi_app/__init__.py
end_to_end_tests/fastapi_app/openapi.json
end_to_end_tests/golden-master/my_test_api_client/api/tests.py
end_to_end_tests/golden-master/my_test_api_client/async_api/tests.py
end_to_end_tests/golden-master/my_test_api_client/models/a_model.py
openapi_python_client/__init__.py
openapi_python_client/parser/errors.py
openapi_python_client/parser/openapi.py
openapi_python_client/parser/properties.py
openapi_python_client/templates/async_endpoint_module.pyi
openapi_python_client/templates/property_templates/date_property.pyi
openapi_python_client/templates/property_templates/datetime_property.pyi
openapi_python_client/templates/property_templates/dict_property.pyi
openapi_python_client/templates/property_templates/union_property.pyi
openapi_python_client/utils.py
tests/test_openapi_parser/test_openapi.py
tests/test_openapi_parser/test_properties.py
tests/test_utils.py
Commit message: Merge pull request from GHSA-9x4c-63pf-525f

* All strings used as file/directory names are now sanitized to address the path traversal vulnerabilities

* Switched calls to utils.spinal_case to utils.kebab_case

* Quotation marks are now escaped in all names/descriptions to address arbitrary code execution issues

* Sanitized identifiers are now also checked for keyword collision

* Added validation for default property values where possible

Address arbitrary code execution vulnerability

* Enum keys are now also sanitized

* Update end_to_end_tests/fastapi_app/__init__.py

Co-authored-by: Dylan Anthony <43723790+dbanty@users.noreply.github.com>

* Update openapi_python_client/parser/properties.py

Co-authored-by: Dylan Anthony <43723790+dbanty@users.noreply.github.com>

* Changed NotImplementedErrors to ValidationErrors

* Updated changelog

* Fixed regex

* Update openapi_python_client/utils.py

Co-authored-by: Dylan Anthony <43723790+dbanty@users.noreply.github.com>

* Fixed rendering of union, datetime, and date properties. Added e2e test for all defaults

* Fixed rendering of date/datetime default values when timezone is included

* Simplified datetime rendering

* Fixed typo

* Readded support for DictProperty defaults and fixed a bug with json body template rendering

* Fixed dict json body compatibility

Co-authored-by: Ethan Mann <emann@triaxtec.com>
Co-authored-by: Dylan Anthony <43723790+dbanty@users.noreply.github.com>

Vulnerable code:
## 0.5.3 - Unrelease
from fastapi import APIRouter, FastAPI, File, Header, Query, UploadFile
    some_dict: Dict[str, str] = {}
def get_list(an_enum_value: List[AnEnum] = Query(...), some_date: Union[date, datetime] = Query(...)):
                        },
                        "default": {}
from dataclasses import asdict
from datetime import date, datetime
    *, client: Client, an_enum_value: List[AnEnum], some_date: Union[date, datetime],
    if isinstance(some_date, date):
from dataclasses import asdict
from datetime import date, datetime
    *, client: Client, an_enum_value: List[AnEnum], some_date: Union[date, datetime],
    if isinstance(some_date, date):
from datetime import date, datetime
    a_camel_date_time: Union[datetime, date]
    a_date: date
    some_dict: Optional[Dict[Any, Any]] = field(default_factory=lambda: cast(Optional[Dict[Any, Any]], {}))
        if isinstance(self.a_camel_date_time, datetime):
        some_dict = self.some_dict

            "some_dict": some_dict,
        def _parse_a_camel_date_time(data: Dict[str, Any]) -> Union[datetime, date]:
            a_camel_date_time: Union[datetime, date]
                a_camel_date_time = datetime.fromisoformat(d["aCamelDateTime"])
            a_camel_date_time = date.fromisoformat(d["aCamelDateTime"])
        a_date = date.fromisoformat(d["a_date"])
        some_dict = d.get("some_dict")

            some_dict=some_dict,
        self.package_description: str = f"A client library for accessing {self.openapi.title}"
            description=data.description,
from .errors import PropertyError
    def get_type_string(self) -> str:
        if self.required:
    def __post_init__(self) -> None:
        super().__post_init__()
        if self.default is not None:
            self.default = f'"{self.default}"'
    _type_string: ClassVar[str] = "datetime"
        imports.update({"from datetime import datetime", "from typing import cast"})
    _type_string: ClassVar[str] = "date"
        imports.update({"from datetime import date", "from typing import cast"})
    def __post_init__(self) -> None:
        super().__post_init__()
        if self.default is not None:
            self.default = f"field(default_factory=lambda: cast({self.get_type_string()}, {self.default}))"

    def get_type_string(self) -> str:
        if self.required:
    def get_type_string(self) -> str:
        if self.required:
        super().__post_init__()
        inverse_values = {v: k for k, v in self.values.items()}
        if self.default is not None:
            self.default = f"{self.reference.class_name}.{inverse_values[self.default]}"
    def get_type_string(self) -> str:
        if self.required:
            output[key] = value
    def get_type_string(self) -> str:
        if self.required:

    def __post_init__(self) -> None:
        super().__post_init__()
        if self.default is not None:
            self.default = f"field(default_factory=lambda: cast({self.get_type_string()}, {self.default}))"
def property_from_data(
            json={{ "json_" +  endpoint.json_body.python_name }},
{{ property.python_name }} = date.fromisoformat({{ source }})
    {{ property.python_name }} = date.fromisoformat(cast(str, {{ source }}))
{{ property.python_name }} = datetime.fromisoformat({{ source }})
    {{ property.python_name }} = datetime.fromisoformat(cast(str, {{ source }}))
    {{ destination }} = None
if isinstance({{ source }}, {{ inner_property.get_type_string() }}):
elif isinstance({{ source }}, {{ inner_property.get_type_string() }}):
def _sanitize(value: str) -> str:
    return re.sub(r"[^\w _-]+", "", value)
    value = re.sub(r"([A-Z]{2,})([A-Z][a-z]|[ -_]|$)", lambda m: m.group(1).title() + m.group(2), value.strip())
    return stringcase.snakecase(group_title(_sanitize(value)))
    return stringcase.pascalcase(_sanitize(value))
    return stringcase.spinalcase(group_title(_sanitize(value)))
from openapi_python_client import GeneratorError
from openapi_python_client.parser.errors import PropertyError
class TestStringProperty:
    def test___post_init__(self):
        from openapi_python_client.parser.properties import StringProperty
        sp = StringProperty(name="test", required=True, default="A Default Value",)
        assert sp.default == '"A Default Value"'
            "from datetime import datetime",
            "from datetime import datetime",
            "from datetime import date",
            "from datetime import date",
class TestDictProperty:
    def test___post_init__(self):
        from openapi_python_client.parser.properties import DictProperty
        p = DictProperty(name="blah", required=True, default={})
        assert p.default == "field(default_factory=lambda: cast(Dict[Any, Any], {}))"
        FloatProperty.assert_called_once_with(name=f"{name}_item", required=True, default="0.0")

Fixed code:
## 0.5.3 - Unreleased
### Security
- All values that become file/directory names are sanitized to address path traversal vulnerabilities (CVE-2020-15141)
- All values that get placed into python files (everything from enum names, to endpoint descriptions, to default values) are validated and/or saniziatied to address arbitrary code execution vulnerabilities (CVE-2020-15142)

### Changes
- Due to security concerns/implementation complexities, default values are temporarily unsupported for any `RefProperty` that doesn't refer to an enum.
- Defaults for properties must now be valid values for their respective type (e.g. "example string" is an invalid default for an `integer` type property, and the function for an endpoint using it would fail to generate and be skipped). 

from fastapi import APIRouter, Body, FastAPI, File, Header, Query, UploadFile
    some_dict: Dict[str, str]
def get_list(
    an_enum_value: List[AnEnum] = Query(...), some_date: Union[date, datetime] = Query(...),
):
@test_router.post("/test_defaults")
def test_defaults(
    string_prop: str = Query(default="the default string"),
    datetime_prop: datetime = Query(default=datetime(1010, 10, 10)),
    date_prop: date = Query(default=date(1010, 10, 10)),
    float_prop: float = Query(default=3.14),
    int_prop: int = Query(default=7),
    boolean_prop: bool = Query(default=False),
    list_prop: List[AnEnum] = Query(default=[AnEnum.FIRST_VALUE, AnEnum.SECOND_VALUE]),
    union_prop: Union[float, str] = Query(default="not a float"),
    enum_prop: AnEnum = Query(default=AnEnum.FIRST_VALUE),
    dict_prop: Dict[str, str] = Body(default={"key": "val"}),
):
    return


        },
        "/tests/test_defaults": {
            "post": {
                "tags": [
                    "tests"
                ],
                "summary": "Test Defaults",
                "operationId": "test_defaults_tests_test_defaults_post",
                "parameters": [
                    {
                        "required": false,
                        "schema": {
                            "title": "String Prop",
                            "type": "string",
                            "default": "the default string"
                        },
                        "name": "string_prop",
                        "in": "query"
                    },
                    {
                        "required": false,
                        "schema": {
                            "title": "Datetime Prop",
                            "type": "string",
                            "format": "date-time",
                            "default": "1010-10-10T00:00:00"
                        },
                        "name": "datetime_prop",
                        "in": "query"
                    },
                    {
                        "required": false,
                        "schema": {
                            "title": "Date Prop",
                            "type": "string",
                            "format": "date",
                            "default": "1010-10-10"
                        },
                        "name": "date_prop",
                        "in": "query"
                    },
                    {
                        "required": false,
                        "schema": {
                            "title": "Float Prop",
                            "type": "number",
                            "default": 3.14
                        },
                        "name": "float_prop",
                        "in": "query"
                    },
                    {
                        "required": false,
                        "schema": {
                            "title": "Int Prop",
                            "type": "integer",
                            "default": 7
                        },
                        "name": "int_prop",
                        "in": "query"
                    },
                    {
                        "required": false,
                        "schema": {
                            "title": "Boolean Prop",
                            "type": "boolean",
                            "default": false
                        },
                        "name": "boolean_prop",
                        "in": "query"
                    },
                    {
                        "required": false,
                        "schema": {
                            "title": "List Prop",
                            "type": "array",
                            "items": {
                                "$ref": "#/components/schemas/AnEnum"
                            },
                            "default": [
                                "FIRST_VALUE",
                                "SECOND_VALUE"
                            ]
                        },
                        "name": "list_prop",
                        "in": "query"
                    },
                    {
                        "required": false,
                        "schema": {
                            "title": "Union Prop",
                            "anyOf": [
                                {
                                    "type": "number"
                                },
                                {
                                    "type": "string"
                                }
                            ],
                            "default": "not a float"
                        },
                        "name": "union_prop",
                        "in": "query"
                    },
                    {
                        "required": false,
                        "schema": {
                            "$ref": "#/components/schemas/AnEnum"
                        },
                        "name": "enum_prop",
                        "in": "query"
                    }
                ],
                "requestBody": {
                    "content": {
                        "application/json": {
                            "schema": {
                                "title": "Dict Prop",
                                "type": "object",
                                "additionalProperties": {
                                    "type": "string"
                                },
                                "default": {
                                    "key": "val"
                                }
                            }
                        }
                    }
                },
                "responses": {
                    "200": {
                        "description": "Successful Response",
                        "content": {
                            "application/json": {
                                "schema": {}
                            }
                        }
                    },
                    "422": {
                        "description": "Validation Error",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/HTTPValidationError"
                                }
                            }
                        }
                    }
                }
            }
                    "some_dict",
                        }
import datetime
from dataclasses import asdict, field
    *, client: Client, an_enum_value: List[AnEnum], some_date: Union[datetime.date, datetime.datetime],
    if isinstance(some_date, datetime.date):


def test_defaults_tests_test_defaults_post(
    *,
    client: Client,
    json_body: Dict[Any, Any],
    string_prop: Optional[str] = "the default string",
    datetime_prop: Optional[datetime.datetime] = datetime.datetime(1010, 10, 10, 0, 0),
    date_prop: Optional[datetime.date] = datetime.date(1010, 10, 10),
    float_prop: Optional[float] = 3.14,
    int_prop: Optional[int] = 7,
    boolean_prop: Optional[bool] = False,
    list_prop: Optional[List[AnEnum]] = field(
        default_factory=lambda: cast(Optional[List[AnEnum]], [AnEnum.FIRST_VALUE, AnEnum.SECOND_VALUE])
    ),
    union_prop: Optional[Union[Optional[float], Optional[str]]] = "not a float",
    enum_prop: Optional[AnEnum] = None,
) -> Union[
    None, HTTPValidationError,
]:

    """  """
    url = "{}/tests/test_defaults".format(client.base_url)

    headers: Dict[str, Any] = client.get_headers()

    json_datetime_prop = datetime_prop.isoformat() if datetime_prop else None

    json_date_prop = date_prop.isoformat() if date_prop else None

    if list_prop is None:
        json_list_prop = None
    else:
        json_list_prop = []
        for list_prop_item_data in list_prop:
            list_prop_item = list_prop_item_data.value

            json_list_prop.append(list_prop_item)

    if union_prop is None:
        json_union_prop: Optional[Union[Optional[float], Optional[str]]] = None
    elif isinstance(union_prop, float):
        json_union_prop = union_prop
    else:
        json_union_prop = union_prop

    json_enum_prop = enum_prop.value if enum_prop else None

    params: Dict[str, Any] = {}
    if string_prop is not None:
        params["string_prop"] = string_prop
    if datetime_prop is not None:
        params["datetime_prop"] = json_datetime_prop
    if date_prop is not None:
        params["date_prop"] = json_date_prop
    if float_prop is not None:
        params["float_prop"] = float_prop
    if int_prop is not None:
        params["int_prop"] = int_prop
    if boolean_prop is not None:
        params["boolean_prop"] = boolean_prop
    if list_prop is not None:
        params["list_prop"] = json_list_prop
    if union_prop is not None:
        params["union_prop"] = json_union_prop
    if enum_prop is not None:
        params["enum_prop"] = json_enum_prop

    json_json_body = json_body

    response = httpx.post(url=url, headers=headers, json=json_json_body, params=params,)

    if response.status_code == 200:
        return None
    if response.status_code == 422:
        return HTTPValidationError.from_dict(cast(Dict[str, Any], response.json()))
    else:
        raise ApiResponseError(response=response)
import datetime
from dataclasses import asdict, field
    *, client: Client, an_enum_value: List[AnEnum], some_date: Union[datetime.date, datetime.datetime],
    if isinstance(some_date, datetime.date):


async def test_defaults_tests_test_defaults_post(
    *,
    client: Client,
    json_body: Dict[Any, Any],
    string_prop: Optional[str] = "the default string",
    datetime_prop: Optional[datetime.datetime] = datetime.datetime(1010, 10, 10, 0, 0),
    date_prop: Optional[datetime.date] = datetime.date(1010, 10, 10),
    float_prop: Optional[float] = 3.14,
    int_prop: Optional[int] = 7,
    boolean_prop: Optional[bool] = False,
    list_prop: Optional[List[AnEnum]] = field(
        default_factory=lambda: cast(Optional[List[AnEnum]], [AnEnum.FIRST_VALUE, AnEnum.SECOND_VALUE])
    ),
    union_prop: Optional[Union[Optional[float], Optional[str]]] = "not a float",
    enum_prop: Optional[AnEnum] = None,
) -> Union[
    None, HTTPValidationError,
]:

    """  """
    url = "{}/tests/test_defaults".format(client.base_url,)

    headers: Dict[str, Any] = client.get_headers()

    json_datetime_prop = datetime_prop.isoformat() if datetime_prop else None

    json_date_prop = date_prop.isoformat() if date_prop else None

    if list_prop is None:
        json_list_prop = None
    else:
        json_list_prop = []
        for list_prop_item_data in list_prop:
            list_prop_item = list_prop_item_data.value

            json_list_prop.append(list_prop_item)

    if union_prop is None:
        json_union_prop: Optional[Union[Optional[float], Optional[str]]] = None
    elif isinstance(union_prop, float):
        json_union_prop = union_prop
    else:
        json_union_prop = union_prop

    json_enum_prop = enum_prop.value if enum_prop else None

    params: Dict[str, Any] = {}
    if string_prop is not None:
        params["string_prop"] = string_prop
    if datetime_prop is not None:
        params["datetime_prop"] = json_datetime_prop
    if date_prop is not None:
        params["date_prop"] = json_date_prop
    if float_prop is not None:
        params["float_prop"] = float_prop
    if int_prop is not None:
        params["int_prop"] = int_prop
    if boolean_prop is not None:
        params["boolean_prop"] = boolean_prop
    if list_prop is not None:
        params["list_prop"] = json_list_prop
    if union_prop is not None:
        params["union_prop"] = json_union_prop
    if enum_prop is not None:
        params["enum_prop"] = json_enum_prop

    json_json_body = json_body

    async with httpx.AsyncClient() as _client:
        response = await _client.post(url=url, headers=headers, json=json_json_body, params=params,)

    if response.status_code == 200:
        return None
    if response.status_code == 422:
        return HTTPValidationError.from_dict(cast(Dict[str, Any], response.json()))
    else:
        raise ApiResponseError(response=response)
import datetime
    some_dict: Dict[Any, Any]
    a_camel_date_time: Union[datetime.datetime, datetime.date]
    a_date: datetime.date
        some_dict = self.some_dict

        if isinstance(self.a_camel_date_time, datetime.datetime):
            "some_dict": some_dict,
        some_dict = d["some_dict"]

        def _parse_a_camel_date_time(data: Dict[str, Any]) -> Union[datetime.datetime, datetime.date]:
            a_camel_date_time: Union[datetime.datetime, datetime.date]
                a_camel_date_time = datetime.datetime.fromisoformat(d["aCamelDateTime"])
            a_camel_date_time = datetime.date.fromisoformat(d["aCamelDateTime"])
        a_date = datetime.date.fromisoformat(d["a_date"])
            some_dict=some_dict,
        self.package_description: str = utils.remove_string_escapes(
            f"A client library for accessing {self.openapi.title}"
        )


class ValidationError(Exception):
    pass
from .. import utils
            description=utils.remove_string_escapes(data.description) if data.description else "",
from datetime import date, datetime
from .errors import PropertyError, ValidationError

    Raises:
        ValidationError: Raised when the default value fails to be converted to the expected type
        if self.default is not None:
            self.default = self._validate_default(default=self.default)

    def _validate_default(self, default: Any) -> Any:
        """ Check that the default value is valid for the property's type + perform any necessary sanitization """
        raise ValidationError
    def get_type_string(self, no_optional: bool = False) -> str:
        if self.required or no_optional:
    def _validate_default(self, default: Any) -> str:
        return f'"{utils.remove_string_escapes(default)}"'
    _type_string: ClassVar[str] = "datetime.datetime"
        imports.update({"import datetime", "from typing import cast"})
    def _validate_default(self, default: Any) -> str:
        for format_string in ("%Y-%m-%dT%H:%M:%S", "%Y-%m-%dT%H:%M:%S%z"):
            try:
                return repr(datetime.strptime(default, format_string))
            except (TypeError, ValueError):
                continue
        raise ValidationError

    _type_string: ClassVar[str] = "datetime.date"
        imports.update({"import datetime", "from typing import cast"})
    def _validate_default(self, default: Any) -> str:
        try:
            return repr(date.fromisoformat(default))
        except (TypeError, ValueError) as e:
            raise ValidationError() from e

    def _validate_default(self, default: Any) -> float:
        try:
            return float(default)
        except (TypeError, ValueError) as e:
            raise ValidationError() from e

    def _validate_default(self, default: Any) -> int:
        try:
            return int(default)
        except (TypeError, ValueError) as e:
            raise ValidationError() from e

    def _validate_default(self, default: Any) -> bool:
        # no try/except needed as anything that comes from the initial load from json/yaml will be boolable
        return bool(default)

    def get_type_string(self, no_optional: bool = False) -> str:
        if self.required or no_optional:
    def _validate_default(self, default: Any) -> str:
        if not isinstance(default, list):
            raise ValidationError()

        default = list(map(self.inner_property._validate_default, default))
        if isinstance(self.inner_property, RefProperty):  # Fix enums to use the actual value
            default = str(default).replace("'", "")

        return f"field(default_factory=lambda: cast({self.get_type_string()}, {default}))"

    def get_type_string(self, no_optional: bool = False) -> str:
        if self.required or no_optional:
    def _validate_default(self, default: Any) -> Any:
        for property in self.inner_properties:
            try:
                val = property._validate_default(default)
                return val
            except ValidationError:
                continue
        raise ValidationError()

        super().__post_init__()
    def get_type_string(self, no_optional: bool = False) -> str:
        if self.required or no_optional:
            sanitized_key = utils.fix_keywords(utils.sanitize(key))
            output[sanitized_key] = utils.remove_string_escapes(value)
    def _validate_default(self, default: Any) -> str:
        inverse_values = {v: k for k, v in self.values.items()}
        try:
            return f"{self.reference.class_name}.{inverse_values[default]}"
        except KeyError as e:
            raise ValidationError() from e

    def get_type_string(self, no_optional: bool = False) -> str:
        if self.required or no_optional:
    def _validate_default(self, default: Any) -> Any:
        enum = EnumProperty.get_enum(self.reference.class_name)
        if enum:
            return enum._validate_default(default)
        else:
            raise ValidationError

    template: ClassVar[str] = "dict_property.pyi"
    def _validate_default(self, default: Any) -> str:
        if isinstance(default, dict):
            return repr(default)
        raise ValidationError

def _property_from_data(
    name = utils.remove_string_escapes(name)


def property_from_data(
    name: str, required: bool, data: Union[oai.Reference, oai.Schema]
) -> Union[Property, PropertyError]:
    try:
        return _property_from_data(name=name, required=required, data=data)
    except ValidationError:
        return PropertyError(detail="Failed to validate default value", data=data)
            json={{ "json_" + endpoint.json_body.python_name }},
{{ property.python_name }} = datetime.date.fromisoformat({{ source }})
    {{ property.python_name }} = datetime.date.fromisoformat(cast(str, {{ source }}))
{{ property.python_name }} = datetime.datetime.fromisoformat({{ source }})
    {{ property.python_name }} = datetime.datetime.fromisoformat(cast(str, {{ source }}))
{% macro construct(property, source) %}
{% if property.required %}
{{ property.python_name }} = {{ source }}
{% else %}
{{ property.python_name }} = None
if {{ source }} is not None:
    {{ property.python_name }} = {{ source }}
{% endif %}
{% endmacro %}

{% macro transform(property, source, destination) %}
{% if property.required %}
{{ destination }} = {{ source }}
{% else %}
{{ destination }} = {{ source }} if {{ source }} else None
{% endif %}
{% endmacro %}
    {{ destination }}: {{ property.get_type_string() }} = None
if isinstance({{ source }}, {{ inner_property.get_type_string(no_optional=True) }}):
elif isinstance({{ source }}, {{ inner_property.get_type_string(no_optional=True) }}):
from keyword import iskeyword
def sanitize(value: str) -> str:
    return re.sub(r"[^\w _\-]+", "", value)


def fix_keywords(value: str) -> str:
    if iskeyword(value):
        return f"{value}_"
    return value
    value = re.sub(r"([A-Z]{2,})([A-Z][a-z]|[ \-_]|$)", lambda m: m.group(1).title() + m.group(2), value.strip())
    return fix_keywords(stringcase.snakecase(group_title(sanitize(value))))
    return fix_keywords(stringcase.pascalcase(sanitize(value)))
    return fix_keywords(stringcase.spinalcase(group_title(sanitize(value))))


def remove_string_escapes(value: str) -> str:
    return value.replace('"', r"\"")
from openapi_python_client import GeneratorError, utils
        mocker.patch("openapi_python_client.utils.remove_string_escapes", return_value=data.description)

from datetime import date, datetime

from openapi_python_client.parser.errors import PropertyError, ValidationError
from openapi_python_client.parser.reference import Reference
    def test___post_init(self, mocker):
        from openapi_python_client.parser.properties import Property

        validate_default = mocker.patch(f"{MODULE_NAME}.Property._validate_default")

        Property(name="a name", required=True, default=None)
        validate_default.assert_not_called()

        Property(name="a name", required=True, default="the default value")
        validate_default.assert_called_with(default="the default value")

    def test__validate_default(self):
        from openapi_python_client.parser.properties import Property
        # should be okay if default isn't specified
        p = Property(name="a name", required=True, default=None)
        with pytest.raises(ValidationError):
            p._validate_default("a default value")
        with pytest.raises(ValidationError):
            p = Property(name="a name", required=True, default="")

class TestStringProperty:
    def test__validate_default(self):
        from openapi_python_client.parser.properties import StringProperty

        p = StringProperty(name="a name", required=True, default="the default value")
        assert p.default == '"the default value"'

            "import datetime",
            "import datetime",
    def test__validate_default(self):
        from openapi_python_client.parser.properties import DateTimeProperty

        with pytest.raises(ValidationError):
            p = DateTimeProperty(name="a name", required=True, default="not a datetime")

        p = DateTimeProperty(name="a name", required=True, default="2017-07-21T17:32:28Z")
        assert p.default == "datetime.datetime(2017, 7, 21, 17, 32, 28, tzinfo=datetime.timezone.utc)"

            "import datetime",
            "import datetime",
    def test__validate_default(self):
        from openapi_python_client.parser.properties import DateProperty

        with pytest.raises(ValidationError):
            p = DateProperty(name="a name", required=True, default="not a date")

        p = DateProperty(name="a name", required=True, default="1010-10-10")
        assert p.default == "datetime.date(1010, 10, 10)"

    def test__validate_default(self):
        from openapi_python_client.parser.properties import FileProperty

        # should be okay if default isn't specified
        p = FileProperty(name="a name", required=True, default=None)

        with pytest.raises(ValidationError):
            p = FileProperty(name="a name", required=True, default="")


class TestFloatProperty:
    def test__validate_default(self):
        from openapi_python_client.parser.properties import FloatProperty

        # should be okay if default isn't specified
        p = FloatProperty(name="a name", required=True, default=None)

        p = FloatProperty(name="a name", required=True, default="123.123")
        assert p.default == 123.123

        with pytest.raises(ValidationError):
            p = FloatProperty(name="a name", required=True, default="not a float")


class TestIntProperty:
    def test__validate_default(self):
        from openapi_python_client.parser.properties import IntProperty

        # should be okay if default isn't specified
        p = IntProperty(name="a name", required=True, default=None)

        p = IntProperty(name="a name", required=True, default="123")
        assert p.default == 123

        with pytest.raises(ValidationError):
            p = IntProperty(name="a name", required=True, default="not an int")


class TestBooleanProperty:
    def test__validate_default(self):
        from openapi_python_client.parser.properties import BooleanProperty

        # should be okay if default isn't specified
        p = BooleanProperty(name="a name", required=True, default=None)

        p = BooleanProperty(name="a name", required=True, default="Literally anything will work")
        assert p.default == True

    def test__validate_default(self, mocker):
        from openapi_python_client.parser.properties import ListProperty

        inner_property = mocker.MagicMock()
        inner_type_string = mocker.MagicMock()
        inner_property.get_type_string.return_value = inner_type_string
        inner_property._validate_default.return_value = "y"

        p = ListProperty(name="a name", required=True, default=["x"], inner_property=inner_property)
        assert p.default == f"field(default_factory=lambda: cast(List[{inner_type_string}], ['y']))"

        with pytest.raises(ValidationError):
            p = ListProperty(name="a name", required=True, default="x", inner_property=inner_property)

    def test__validate_default_enum_items(self, mocker):
        from openapi_python_client.parser.properties import ListProperty, RefProperty

        inner_enum_property = mocker.MagicMock(spec=RefProperty)
        inner_enum_property.get_type_string.return_value = "AnEnum"
        inner_enum_property._validate_default.return_value = "AnEnum.val1"

        p = ListProperty(name="a name", required=True, default=["val1"], inner_property=inner_enum_property)
        assert p.default == f"field(default_factory=lambda: cast(List[AnEnum], [AnEnum.val1]))"

    def test__validate_default(self, mocker):
        from openapi_python_client.parser.properties import UnionProperty

        inner_property_1 = mocker.MagicMock()
        inner_property_1.get_type_string.return_value = "inner_type_string_1"
        inner_property_1._validate_default.side_effect = ValidationError()
        inner_property_2 = mocker.MagicMock()
        inner_property_2.get_type_string.return_value = "inner_type_string_2"
        inner_property_2._validate_default.return_value = "the default value"
        p = UnionProperty(
            name="test", required=True, default="a value", inner_properties=[inner_property_1, inner_property_2]
        )

        assert p.default == "the default value"

        inner_property_2._validate_default.side_effect = ValidationError()

        with pytest.raises(ValidationError):
            p = UnionProperty(
                name="test", required=True, default="a value", inner_properties=[inner_property_1, inner_property_2]
            )

    def test__validate_default(self, mocker):
        fake_reference = mocker.MagicMock(class_name="MyTestEnum", module_name="my_test_enum")
        mocker.patch(f"{MODULE_NAME}.Reference.from_ref", return_value=fake_reference)

        from openapi_python_client.parser import properties

        enum_property = properties.EnumProperty(
            name="test", required=True, default="test", values={"TEST": "test"}, title="a_title"
        )
        assert enum_property.default == "MyTestEnum.TEST"

        with pytest.raises(ValidationError):
            enum_property = properties.EnumProperty(
                name="test", required=True, default="bad_val", values={"TEST": "test"}, title="a_title"
            )

        properties._existing_enums = {}

    def test__validate_default(self, mocker):
        from openapi_python_client.parser.properties import RefProperty

        with pytest.raises(ValidationError):
            p = RefProperty(name="a name", required=True, default="", reference=mocker.MagicMock())
        enum_property = mocker.MagicMock()
        enum_property._validate_default.return_value = "val1"
        mocker.patch(f"{MODULE_NAME}.EnumProperty.get_enum", return_value=enum_property)
        p = RefProperty(name="a name", required=True, default="", reference=mocker.MagicMock())
        assert p.default == "val1"
class TestDictProperty:
    def test__validate_default(self):
        from openapi_python_client.parser.properties import DictProperty

        p = DictProperty(name="a name", required=True, default={"key": "value"})

        with pytest.raises(ValidationError):
            p = DictProperty(name="a name", required=True, default="not a dict")

        mocker.patch("openapi_python_client.utils.remove_string_escapes", return_value=name)
        mocker.patch("openapi_python_client.utils.remove_string_escapes", return_value=name)
        mocker.patch("openapi_python_client.utils.remove_string_escapes", return_value=name)

        mocker.patch("openapi_python_client.utils.remove_string_escapes", return_value=name)
        mocker.patch("openapi_python_client.utils.remove_string_escapes", return_value=name)
        FloatProperty.assert_called_once_with(name=name, required=True, default="0.0")
        mocker.patch("openapi_python_client.utils.remove_string_escapes", return_value=name)
        mocker.patch("openapi_python_client.utils.remove_string_escapes", return_value=name)
        mocker.patch("openapi_python_client.utils.remove_string_escapes", return_value=name)
    def test_property_from_data_validation_error(self, mocker):
        from openapi_python_client.parser.errors import PropertyError
        from openapi_python_client.parser.properties import property_from_data

        mocker.patch(f"{MODULE_NAME}._property_from_data").side_effect = ValidationError()

        data = oai.Schema()
        assert property_from_data(name="blah", required=True, data=data) == PropertyError(
            detail="Failed to validate default value", data=data
        )



def test__sanitize():
    assert utils.sanitize("something*~with lots_- of weird things}=") == "somethingwith lots_- of weird things"


def test_no_string_escapes():
    assert utils.remove_string_escapes('an "evil" string') == 'an \\"evil\\" string'


def test__fix_keywords():
    assert utils.fix_keywords("None") == "None_"