Skip to content

Commit b795ef8

Browse files
committed
fix: Incorrect handling of allOf subschemas in testing explicit examples
Ref: schemathesis#2375 Signed-off-by: Dmitry Dygalo <dmitry@dygalo.dev>
1 parent eeae731 commit b795ef8

File tree

5 files changed

+144
-6
lines changed

5 files changed

+144
-6
lines changed

docs/changelog.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ Changelog
77
**Fixed**
88

99
- Incorrect default deadline for stateful tests in CLI.
10+
- Incorrect handling of ``allOf`` subschemas in testing explicit examples. :issue:`2375`
1011

1112
**Changed**
1213

src/schemathesis/specs/openapi/examples.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
from ..._hypothesis import get_single_example
1414
from ...constants import DEFAULT_RESPONSE_TIMEOUT
15+
from ...internal.copy import fast_deepcopy
1516
from ...models import APIOperation, Case
1617
from ._hypothesis import get_case_strategy, get_default_format_strategies
1718
from .constants import LOCATION_TO_CONTAINER
@@ -130,10 +131,26 @@ def extract_top_level(operation: APIOperation[OpenAPIParameter, Case]) -> Genera
130131
def _expand_subschemas(schema: dict[str, Any] | bool) -> Generator[dict[str, Any] | bool, None, None]:
131132
yield schema
132133
if isinstance(schema, dict):
133-
for key in ("anyOf", "oneOf", "allOf"):
134+
for key in ("anyOf", "oneOf"):
134135
if key in schema:
135136
for subschema in schema[key]:
136137
yield subschema
138+
if "allOf" in schema:
139+
subschema = fast_deepcopy(schema["allOf"][0])
140+
for sub in schema["allOf"][1:]:
141+
if isinstance(sub, dict):
142+
for key, value in sub.items():
143+
if key == "properties":
144+
subschema.setdefault("properties", {}).update(value)
145+
elif key == "required":
146+
subschema.setdefault("required", []).extend(value)
147+
elif key == "examples":
148+
subschema.setdefault("examples", []).extend(value)
149+
elif key == "example":
150+
subschema.setdefault("examples", []).append(value)
151+
else:
152+
subschema[key] = value
153+
yield subschema
137154

138155

139156
def _find_parameter_examples_definition(

src/schemathesis/stateful/runner.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@
77
from typing import TYPE_CHECKING, Any, Generator, Iterator, Type
88

99
import hypothesis
10-
from hypothesis.stateful import Rule
1110
import requests
1211
from hypothesis.control import current_build_context
1312
from hypothesis.errors import Flaky
13+
from hypothesis.stateful import Rule
1414

1515
from ..exceptions import CheckFailed
1616
from ..targets import TargetMetricCollector

test/cli/test_reporting.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
from hypothesis import given, strategies as st
1+
from hypothesis import given
2+
from hypothesis import strategies as st
23

34
from schemathesis.cli.reporting import group_by_case
45
from schemathesis.code_samples import CodeSampleStyle
56
from schemathesis.models import Request, Response
6-
from schemathesis.runner.serialization import SerializedCheck, Status, SerializedCase
7+
from schemathesis.runner.serialization import SerializedCase, SerializedCheck, Status
78

89

910
@given(

test/specs/openapi/test_examples.py

Lines changed: 121 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -641,7 +641,7 @@ def test_examples_ref_missing_components(empty_open_api_3_schema):
641641
assert example.query == {"q": {"foo-1": "foo-11", "spam-1": {"inner": "example"}}}
642642

643643

644-
@pytest.mark.parametrize("key", ("anyOf", "oneOf", "allOf"))
644+
@pytest.mark.parametrize("key", ("anyOf", "oneOf"))
645645
def test_examples_in_any_of_top_level(empty_open_api_3_schema, key):
646646
empty_open_api_3_schema["paths"] = {
647647
"/test": {
@@ -706,7 +706,71 @@ def test_examples_in_any_of_top_level(empty_open_api_3_schema, key):
706706
]
707707

708708

709-
@pytest.mark.parametrize("key", ("anyOf", "oneOf", "allOf"))
709+
def test_examples_in_all_of_top_level(empty_open_api_3_schema):
710+
empty_open_api_3_schema["paths"] = {
711+
"/test": {
712+
"post": {
713+
"parameters": [
714+
{
715+
"name": "q",
716+
"in": "query",
717+
"schema": {
718+
"allOf": [
719+
{
720+
"example": "foo-1-1",
721+
"examples": ["foo-1-2"],
722+
"type": "string",
723+
},
724+
{
725+
"example": "foo-2-1",
726+
"examples": ["foo-2-2"],
727+
"type": "string",
728+
},
729+
True,
730+
]
731+
},
732+
}
733+
],
734+
"requestBody": {
735+
"content": {
736+
"application/json": {
737+
"schema": {
738+
"allOf": [
739+
{
740+
"example": "body-1-1",
741+
"examples": ["body-1-2"],
742+
"type": "string",
743+
},
744+
{
745+
"example": "body-2-1",
746+
"examples": ["body-2-2"],
747+
"type": "string",
748+
},
749+
True,
750+
]
751+
}
752+
},
753+
}
754+
},
755+
"responses": {"default": {"description": "OK"}},
756+
},
757+
}
758+
}
759+
schema = schemathesis.from_dict(empty_open_api_3_schema)
760+
extracted = [example_to_dict(example) for example in examples.extract_top_level(schema["/test"]["POST"])]
761+
assert extracted == [
762+
{"container": "query", "name": "q", "value": "foo-1-1"},
763+
{"container": "query", "name": "q", "value": "foo-1-2"},
764+
{"container": "query", "name": "q", "value": "foo-2-1"},
765+
{"container": "query", "name": "q", "value": "foo-2-2"},
766+
{"media_type": "application/json", "value": "body-1-1"},
767+
{"media_type": "application/json", "value": "body-1-2"},
768+
{"media_type": "application/json", "value": "body-2-1"},
769+
{"media_type": "application/json", "value": "body-2-2"},
770+
]
771+
772+
773+
@pytest.mark.parametrize("key", ("anyOf", "oneOf"))
710774
def test_examples_in_any_of_in_schemas(empty_open_api_3_schema, key):
711775
empty_open_api_3_schema["paths"] = {
712776
"/test": {
@@ -1245,3 +1309,58 @@ def test_property_examples_behind_ref():
12451309
"media_type": "application/json",
12461310
}
12471311
]
1312+
1313+
1314+
def test_property_examples_with_all_of():
1315+
# See GH-2375
1316+
raw_schema = {
1317+
"openapi": "3.0.3",
1318+
"info": {"title": "Test API", "version": "1.0.0"},
1319+
"paths": {
1320+
"/peers": {
1321+
"post": {
1322+
"requestBody": {
1323+
"content": {
1324+
"application/json": {
1325+
"schema": {"$ref": "#/components/schemas/peer"},
1326+
}
1327+
}
1328+
},
1329+
"responses": {"200": {"description": "Successful operation"}},
1330+
}
1331+
}
1332+
},
1333+
"components": {
1334+
"schemas": {
1335+
"peer": {
1336+
"type": "object",
1337+
"properties": {"outbound_proxy": {"$ref": "#/components/schemas/outbound_proxy_with_port"}},
1338+
"required": ["outbound_proxy"],
1339+
},
1340+
"outbound_proxy_common": {
1341+
"type": "object",
1342+
"properties": {"host": {"type": "string", "format": "ipv4", "example": "10.22.22.191"}},
1343+
"required": ["host"],
1344+
},
1345+
"outbound_proxy_with_port": {
1346+
"allOf": [
1347+
{"$ref": "#/components/schemas/outbound_proxy_common"},
1348+
{
1349+
"type": "object",
1350+
"properties": {"port": {"type": "integer", "example": 8080}},
1351+
"required": ["port"],
1352+
},
1353+
]
1354+
},
1355+
}
1356+
},
1357+
}
1358+
schema = schemathesis.from_dict(raw_schema)
1359+
operation = schema["/peers"]["POST"]
1360+
extracted = [example_to_dict(example) for example in examples.extract_from_schemas(operation)]
1361+
assert extracted == [
1362+
{
1363+
"value": {"outbound_proxy": {"host": "10.22.22.191", "port": 8080}},
1364+
"media_type": "application/json",
1365+
}
1366+
]

0 commit comments

Comments
 (0)