Skip to content

Commit

Permalink
fix: TypeError during negative testing on Open API schemas with par…
Browse files Browse the repository at this point in the history
…ameters that have non-default `style` value

Ref: #1208
  • Loading branch information
Stranger6667 committed Jul 9, 2021
1 parent 424dfa2 commit 9395a74
Show file tree
Hide file tree
Showing 3 changed files with 78 additions and 10 deletions.
2 changes: 2 additions & 0 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ Changelog
**Fixed**

- ``KeyError`` when the ``response_schema_conformance`` check is executed against responses without schema definition. `#1220`_
- ``TypeError`` during negative testing on Open API schemas with parameters that have non-default ``style`` value. `#1208`_

`3.9.3`_ - 2021-06-22
---------------------
Expand Down Expand Up @@ -2076,6 +2077,7 @@ Deprecated
.. _0.2.0: https://github.com/schemathesis/schemathesis/compare/v0.1.0...v0.2.0

.. _#1220: https://github.com/schemathesis/schemathesis/issues/1220
.. _#1208: https://github.com/schemathesis/schemathesis/issues/1208
.. _#1202: https://github.com/schemathesis/schemathesis/issues/1202
.. _#1194: https://github.com/schemathesis/schemathesis/issues/1194
.. _#1190: https://github.com/schemathesis/schemathesis/issues/1190
Expand Down
40 changes: 30 additions & 10 deletions src/schemathesis/specs/openapi/serialization.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import json
from typing import Any, Callable, Dict, Generator, List, Optional
from typing import Any, Callable, Dict, Generator, List, Optional, Tuple, Union

from ...utils import compose

Expand Down Expand Up @@ -167,7 +167,27 @@ def _map(item: Generated) -> Generated:


def make_delimited(data: Optional[Dict[str, Any]], delimiter: str = ",") -> str:
return delimiter.join(f"{key}={value}" for key, value in (data or {}).items())
return delimiter.join(f"{key}={value}" for key, value in force_dict(data or {}).items())


def force_iterable(value: Any) -> Union[List, Tuple]:
"""Converts the value to a list or a tuple.
Only relevant for negative test scenarios where the original types might be changed.
"""
if isinstance(value, (tuple, list)):
return value
return [value]


def force_dict(value: Any) -> Dict:
"""Converts the value to a dictionary.
Only relevant for negative test scenarios where the original types might be changed.
"""
if isinstance(value, dict):
return value
return {"": value}


@conversion
Expand All @@ -178,7 +198,7 @@ def to_json(item: Generated, name: str) -> None:

@conversion
def delimited(item: Generated, name: str, delimiter: str) -> None:
item[name] = delimiter.join(map(str, item[name] or ()))
item[name] = delimiter.join(map(str, force_iterable(item[name] or ())))


@conversion
Expand All @@ -189,14 +209,14 @@ def deep_object(item: Generated, name: str) -> None:
"""
generated = item.pop(name)
if generated:
item.update({f"{name}[{key}]": value for key, value in generated.items()})
item.update({f"{name}[{key}]": value for key, value in force_dict(generated).items()})
else:
item[name] = ""


@conversion
def comma_delimited_object(item: Generated, name: str) -> None:
item[name] = ",".join(map(str, sum((item[name] or {}).items(), ())))
item[name] = ",".join(map(str, sum((force_dict(item[name] or {})).items(), ())))


@conversion
Expand Down Expand Up @@ -243,7 +263,7 @@ def label_array(item: Generated, name: str, explode: Optional[bool]) -> None:
delimiter = "."
else:
delimiter = ","
new = delimiter.join(map(str, item[name] or ()))
new = delimiter.join(map(str, force_iterable(item[name] or ())))
if new:
item[name] = f".{new}"
else:
Expand All @@ -265,7 +285,7 @@ def label_object(item: Generated, name: str, explode: Optional[bool]) -> None:
if explode:
new = make_delimited(item[name], ".")
else:
object_items = map(str, sum((item[name] or {}).items(), ()))
object_items = map(str, sum(force_dict(item[name] or {}).items(), ()))
new = ",".join(object_items)
if new:
item[name] = f".{new}"
Expand Down Expand Up @@ -299,9 +319,9 @@ def matrix_array(item: Generated, name: str, explode: Optional[bool]) -> None:
id=[3, 4, 5] => ";id=3,4,5"
"""
if explode:
new = ";".join(f"{name}={value}" for value in item[name] or ())
new = ";".join(f"{name}={value}" for value in force_iterable(item[name] or ()))
else:
new = ",".join(map(str, item[name] or ()))
new = ",".join(map(str, force_iterable(item[name] or ())))
if new:
item[name] = f";{new}"
else:
Expand All @@ -323,7 +343,7 @@ def matrix_object(item: Generated, name: str, explode: Optional[bool]) -> None:
if explode:
new = make_delimited(item[name], ";")
else:
object_items = map(str, sum((item[name] or {}).items(), ()))
object_items = map(str, sum(force_dict(item[name] or {}).items(), ()))
new = ",".join(object_items)
if new:
item[name] = f";{new}"
Expand Down
46 changes: 46 additions & 0 deletions test/specs/openapi/test_negative.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from copy import deepcopy
from test.utils import assert_requests_call

import pytest
from hypothesis import HealthCheck, given, settings
Expand All @@ -7,6 +8,8 @@
from hypothesis_jsonschema._canonicalise import FALSEY, canonicalish
from jsonschema import Draft4Validator

import schemathesis
from schemathesis import DataGenerationMethod
from schemathesis.specs.openapi._hypothesis import STRING_FORMATS, is_valid_header
from schemathesis.specs.openapi.constants import LOCATION_TO_CONTAINER
from schemathesis.specs.openapi.negative import mutated, negative_schema
Expand Down Expand Up @@ -269,3 +272,46 @@ def test_no_unsatisfiable_schemas(data):
schema = {"type": "object", "required": ["foo"]}
mutated_schema = data.draw(mutated(schema, location="body", media_type="application/json"))
assert canonicalish(mutated_schema) != FALSEY


ARRAY_PARAMETER = {"type": "array", "minItems": 1, "items": {"type": "string", "format": "ipv4"}}
OBJECT_PARAMETER = {
"type": "object",
"minProperties": 1,
"properties": {"foo": {"type": "string", "format": "ipv4"}, "bar": {"type": "string", "format": "ipv4"}},
"additionalProperties": False,
}


@pytest.mark.parametrize("explode", (True, False))
@pytest.mark.parametrize(
"location, schema, style",
[("query", ARRAY_PARAMETER, style) for style in ("pipeDelimited", "spaceDelimited")]
+ [("query", OBJECT_PARAMETER, "deepObject")]
+ [
("path", parameter, style)
for parameter in [OBJECT_PARAMETER, ARRAY_PARAMETER]
for style in ("simple", "label", "matrix")
],
)
@pytest.mark.hypothesis_nested
def test_non_default_styles(empty_open_api_3_schema, location, schema, style, explode):
# See GH-1208
# When the schema contains a parameter with a not-default "style"
empty_open_api_3_schema["paths"]["/bug"] = {
"get": {
"parameters": [
{"name": "key", "in": location, "required": True, "style": style, "explode": explode, "schema": schema},
],
"responses": {"200": {"description": "OK"}},
}
}

schema = schemathesis.from_dict(empty_open_api_3_schema)

@given(case=schema["/bug"]["get"].as_strategy(data_generation_method=DataGenerationMethod.negative))
@settings(deadline=None, max_examples=10, suppress_health_check=[HealthCheck.too_slow, HealthCheck.filter_too_much])
def test(case):
assert_requests_call(case)

test()

0 comments on commit 9395a74

Please sign in to comment.