schemathesis 4.3.15__py3-none-any.whl → 4.3.17__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of schemathesis might be problematic. Click here for more details.
- schemathesis/auths.py +24 -3
- schemathesis/checks.py +1 -1
- schemathesis/cli/commands/run/handlers/cassettes.py +1 -2
- schemathesis/cli/commands/run/handlers/output.py +5 -2
- schemathesis/config/_error.py +1 -1
- schemathesis/core/errors.py +30 -0
- schemathesis/engine/errors.py +12 -0
- schemathesis/engine/phases/unit/__init__.py +2 -2
- schemathesis/engine/phases/unit/_executor.py +4 -0
- schemathesis/generation/coverage.py +143 -50
- schemathesis/generation/hypothesis/builder.py +28 -7
- schemathesis/generation/meta.py +77 -2
- schemathesis/pytest/lazy.py +58 -12
- schemathesis/pytest/plugin.py +2 -2
- schemathesis/specs/openapi/_hypothesis.py +18 -98
- schemathesis/specs/openapi/adapter/parameters.py +181 -11
- schemathesis/specs/openapi/checks.py +5 -7
- schemathesis/specs/openapi/converter.py +1 -14
- schemathesis/specs/openapi/examples.py +4 -4
- schemathesis/specs/openapi/references.py +31 -1
- schemathesis/specs/openapi/schemas.py +5 -4
- schemathesis/transport/prepare.py +4 -3
- {schemathesis-4.3.15.dist-info → schemathesis-4.3.17.dist-info}/METADATA +6 -5
- {schemathesis-4.3.15.dist-info → schemathesis-4.3.17.dist-info}/RECORD +27 -27
- {schemathesis-4.3.15.dist-info → schemathesis-4.3.17.dist-info}/WHEEL +0 -0
- {schemathesis-4.3.15.dist-info → schemathesis-4.3.17.dist-info}/entry_points.txt +0 -0
- {schemathesis-4.3.15.dist-info → schemathesis-4.3.17.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
|
+
import inspect
|
|
4
5
|
import warnings
|
|
5
6
|
from dataclasses import dataclass
|
|
6
7
|
from enum import Enum
|
|
@@ -42,6 +43,7 @@ from schemathesis.generation.meta import (
|
|
|
42
43
|
CaseMetadata,
|
|
43
44
|
ComponentInfo,
|
|
44
45
|
CoveragePhaseData,
|
|
46
|
+
CoverageScenario,
|
|
45
47
|
GenerationInfo,
|
|
46
48
|
PhaseInfo,
|
|
47
49
|
)
|
|
@@ -227,7 +229,7 @@ def create_base_test(
|
|
|
227
229
|
|
|
228
230
|
funcobj = hypothesis.given(*args, **{**kwargs, "case": strategy})(test_wrapper)
|
|
229
231
|
|
|
230
|
-
if
|
|
232
|
+
if inspect.iscoroutinefunction(test_function):
|
|
231
233
|
funcobj.hypothesis.inner_test = make_async_test(test_function) # type: ignore
|
|
232
234
|
return funcobj
|
|
233
235
|
|
|
@@ -582,6 +584,7 @@ def _iter_coverage_cases(
|
|
|
582
584
|
coverage.GeneratedValue(
|
|
583
585
|
"value",
|
|
584
586
|
generation_mode=GenerationMode.NEGATIVE,
|
|
587
|
+
scenario=CoverageScenario.UNSUPPORTED_PATH_PATTERN,
|
|
585
588
|
description="Sample value for unsupported path parameter pattern",
|
|
586
589
|
parameter=name,
|
|
587
590
|
location="/",
|
|
@@ -638,6 +641,7 @@ def _iter_coverage_cases(
|
|
|
638
641
|
),
|
|
639
642
|
components=data.components,
|
|
640
643
|
phase=PhaseInfo.coverage(
|
|
644
|
+
scenario=value.scenario,
|
|
641
645
|
description=value.description,
|
|
642
646
|
location=value.location,
|
|
643
647
|
parameter=body.media_type,
|
|
@@ -660,6 +664,7 @@ def _iter_coverage_cases(
|
|
|
660
664
|
),
|
|
661
665
|
components=data.components,
|
|
662
666
|
phase=PhaseInfo.coverage(
|
|
667
|
+
scenario=next_value.scenario,
|
|
663
668
|
description=next_value.description,
|
|
664
669
|
location=next_value.location,
|
|
665
670
|
parameter=body.media_type,
|
|
@@ -680,7 +685,9 @@ def _iter_coverage_cases(
|
|
|
680
685
|
mode=GenerationMode.POSITIVE,
|
|
681
686
|
),
|
|
682
687
|
components=data.components,
|
|
683
|
-
phase=PhaseInfo.coverage(
|
|
688
|
+
phase=PhaseInfo.coverage(
|
|
689
|
+
scenario=CoverageScenario.DEFAULT_POSITIVE_TEST, description="Default positive test case"
|
|
690
|
+
),
|
|
684
691
|
),
|
|
685
692
|
)
|
|
686
693
|
|
|
@@ -706,6 +713,7 @@ def _iter_coverage_cases(
|
|
|
706
713
|
generation=GenerationInfo(time=instant.elapsed, mode=value.generation_mode),
|
|
707
714
|
components=data.components,
|
|
708
715
|
phase=PhaseInfo.coverage(
|
|
716
|
+
scenario=value.scenario,
|
|
709
717
|
description=value.description,
|
|
710
718
|
location=value.location,
|
|
711
719
|
parameter=name,
|
|
@@ -725,7 +733,10 @@ def _iter_coverage_cases(
|
|
|
725
733
|
_meta=CaseMetadata(
|
|
726
734
|
generation=GenerationInfo(time=instant.elapsed, mode=GenerationMode.NEGATIVE),
|
|
727
735
|
components=data.components,
|
|
728
|
-
phase=PhaseInfo.coverage(
|
|
736
|
+
phase=PhaseInfo.coverage(
|
|
737
|
+
scenario=CoverageScenario.UNSPECIFIED_HTTP_METHOD,
|
|
738
|
+
description=f"Unspecified HTTP method: {method.upper()}",
|
|
739
|
+
),
|
|
729
740
|
),
|
|
730
741
|
)
|
|
731
742
|
# Generate duplicate query parameters
|
|
@@ -750,6 +761,7 @@ def _iter_coverage_cases(
|
|
|
750
761
|
generation=GenerationInfo(time=instant.elapsed, mode=GenerationMode.NEGATIVE),
|
|
751
762
|
components=data.components,
|
|
752
763
|
phase=PhaseInfo.coverage(
|
|
764
|
+
scenario=CoverageScenario.DUPLICATE_PARAMETER,
|
|
753
765
|
description=f"Duplicate `{parameter.name}` query parameter",
|
|
754
766
|
parameter=parameter.name,
|
|
755
767
|
parameter_location=ParameterLocation.QUERY,
|
|
@@ -776,6 +788,7 @@ def _iter_coverage_cases(
|
|
|
776
788
|
generation=GenerationInfo(time=instant.elapsed, mode=GenerationMode.NEGATIVE),
|
|
777
789
|
components=data.components,
|
|
778
790
|
phase=PhaseInfo.coverage(
|
|
791
|
+
scenario=CoverageScenario.MISSING_PARAMETER,
|
|
779
792
|
description=f"Missing `{name}` at {location.value}",
|
|
780
793
|
parameter=name,
|
|
781
794
|
parameter_location=location,
|
|
@@ -802,6 +815,7 @@ def _iter_coverage_cases(
|
|
|
802
815
|
# Helper function to create and yield a case
|
|
803
816
|
def make_case(
|
|
804
817
|
container_values: dict,
|
|
818
|
+
scenario: CoverageScenario,
|
|
805
819
|
description: str,
|
|
806
820
|
_location: ParameterLocation,
|
|
807
821
|
_parameter: str | None,
|
|
@@ -818,6 +832,7 @@ def _iter_coverage_cases(
|
|
|
818
832
|
),
|
|
819
833
|
components=data.components,
|
|
820
834
|
phase=PhaseInfo.coverage(
|
|
835
|
+
scenario=scenario,
|
|
821
836
|
description=description,
|
|
822
837
|
parameter=_parameter,
|
|
823
838
|
parameter_location=_location,
|
|
@@ -861,6 +876,7 @@ def _iter_coverage_cases(
|
|
|
861
876
|
more = next(iterator)
|
|
862
877
|
yield make_case(
|
|
863
878
|
more.value,
|
|
879
|
+
more.scenario,
|
|
864
880
|
more.description,
|
|
865
881
|
_location,
|
|
866
882
|
more.parameter,
|
|
@@ -876,6 +892,7 @@ def _iter_coverage_cases(
|
|
|
876
892
|
if GenerationMode.POSITIVE in generation_modes:
|
|
877
893
|
yield make_case(
|
|
878
894
|
only_required,
|
|
895
|
+
CoverageScenario.OBJECT_ONLY_REQUIRED,
|
|
879
896
|
"Only required properties",
|
|
880
897
|
location,
|
|
881
898
|
None,
|
|
@@ -891,8 +908,9 @@ def _iter_coverage_cases(
|
|
|
891
908
|
assert case.meta is not None
|
|
892
909
|
assert isinstance(case.meta.phase.data, CoveragePhaseData)
|
|
893
910
|
# Already generated in one of the blocks above
|
|
894
|
-
if
|
|
895
|
-
|
|
911
|
+
if (
|
|
912
|
+
location != "path"
|
|
913
|
+
and case.meta.phase.data.scenario != CoverageScenario.OBJECT_MISSING_REQUIRED_PROPERTY
|
|
896
914
|
):
|
|
897
915
|
yield case
|
|
898
916
|
|
|
@@ -902,6 +920,7 @@ def _iter_coverage_cases(
|
|
|
902
920
|
if combo != base_container and GenerationMode.POSITIVE in generation_modes:
|
|
903
921
|
yield make_case(
|
|
904
922
|
combo,
|
|
923
|
+
CoverageScenario.OBJECT_REQUIRED_AND_OPTIONAL,
|
|
905
924
|
f"All required properties and optional '{opt_param}'",
|
|
906
925
|
location,
|
|
907
926
|
None,
|
|
@@ -914,8 +933,9 @@ def _iter_coverage_cases(
|
|
|
914
933
|
assert case.meta is not None
|
|
915
934
|
assert isinstance(case.meta.phase.data, CoveragePhaseData)
|
|
916
935
|
# Already generated in one of the blocks above
|
|
917
|
-
if
|
|
918
|
-
|
|
936
|
+
if (
|
|
937
|
+
location != "path"
|
|
938
|
+
and case.meta.phase.data.scenario != CoverageScenario.OBJECT_MISSING_REQUIRED_PROPERTY
|
|
919
939
|
):
|
|
920
940
|
yield case
|
|
921
941
|
|
|
@@ -927,6 +947,7 @@ def _iter_coverage_cases(
|
|
|
927
947
|
if combo != base_container:
|
|
928
948
|
yield make_case(
|
|
929
949
|
combo,
|
|
950
|
+
CoverageScenario.OBJECT_REQUIRED_AND_OPTIONAL,
|
|
930
951
|
f"All required and {size} optional properties",
|
|
931
952
|
location,
|
|
932
953
|
None,
|
schemathesis/generation/meta.py
CHANGED
|
@@ -16,6 +16,75 @@ class TestPhase(str, Enum):
|
|
|
16
16
|
STATEFUL = "stateful"
|
|
17
17
|
|
|
18
18
|
|
|
19
|
+
class CoverageScenario(str, Enum):
|
|
20
|
+
"""Coverage test scenario types."""
|
|
21
|
+
|
|
22
|
+
# Positive scenarios - Valid values
|
|
23
|
+
EXAMPLE_VALUE = "example_value"
|
|
24
|
+
DEFAULT_VALUE = "default_value"
|
|
25
|
+
ENUM_VALUE = "enum_value"
|
|
26
|
+
CONST_VALUE = "const_value"
|
|
27
|
+
VALID_STRING = "valid_string"
|
|
28
|
+
VALID_NUMBER = "valid_number"
|
|
29
|
+
VALID_BOOLEAN = "valid_boolean"
|
|
30
|
+
VALID_ARRAY = "valid_array"
|
|
31
|
+
VALID_OBJECT = "valid_object"
|
|
32
|
+
NULL_VALUE = "null_value"
|
|
33
|
+
|
|
34
|
+
# Positive scenarios - Boundary values for strings
|
|
35
|
+
MINIMUM_LENGTH_STRING = "minimum_length_string"
|
|
36
|
+
MAXIMUM_LENGTH_STRING = "maximum_length_string"
|
|
37
|
+
NEAR_BOUNDARY_LENGTH_STRING = "near_boundary_length_string"
|
|
38
|
+
|
|
39
|
+
# Positive scenarios - Boundary values for numbers
|
|
40
|
+
MINIMUM_VALUE = "minimum_value"
|
|
41
|
+
MAXIMUM_VALUE = "maximum_value"
|
|
42
|
+
NEAR_BOUNDARY_NUMBER = "near_boundary_number"
|
|
43
|
+
|
|
44
|
+
# Positive scenarios - Boundary values for arrays
|
|
45
|
+
MINIMUM_ITEMS_ARRAY = "minimum_items_array"
|
|
46
|
+
MAXIMUM_ITEMS_ARRAY = "maximum_items_array"
|
|
47
|
+
NEAR_BOUNDARY_ITEMS_ARRAY = "near_boundary_items_array"
|
|
48
|
+
ENUM_VALUE_ITEMS_ARRAY = "enum_value_items_array"
|
|
49
|
+
|
|
50
|
+
# Positive scenarios - Objects
|
|
51
|
+
OBJECT_ONLY_REQUIRED = "object_only_required"
|
|
52
|
+
OBJECT_REQUIRED_AND_OPTIONAL = "object_required_and_optional"
|
|
53
|
+
|
|
54
|
+
# Positive scenarios - Default test case
|
|
55
|
+
DEFAULT_POSITIVE_TEST = "default_positive_test"
|
|
56
|
+
|
|
57
|
+
# Negative scenarios - Boundary violations for numbers
|
|
58
|
+
VALUE_ABOVE_MAXIMUM = "value_above_maximum"
|
|
59
|
+
VALUE_BELOW_MINIMUM = "value_below_minimum"
|
|
60
|
+
|
|
61
|
+
# Negative scenarios - Boundary violations for strings
|
|
62
|
+
STRING_ABOVE_MAX_LENGTH = "string_above_max_length"
|
|
63
|
+
STRING_BELOW_MIN_LENGTH = "string_below_min_length"
|
|
64
|
+
|
|
65
|
+
# Negative scenarios - Boundary violations for arrays
|
|
66
|
+
ARRAY_ABOVE_MAX_ITEMS = "array_above_max_items"
|
|
67
|
+
ARRAY_BELOW_MIN_ITEMS = "array_below_min_items"
|
|
68
|
+
|
|
69
|
+
# Negative scenarios - Constraint violations
|
|
70
|
+
OBJECT_UNEXPECTED_PROPERTIES = "object_unexpected_properties"
|
|
71
|
+
OBJECT_MISSING_REQUIRED_PROPERTY = "object_missing_required_property"
|
|
72
|
+
INCORRECT_TYPE = "incorrect_type"
|
|
73
|
+
INVALID_ENUM_VALUE = "invalid_enum_value"
|
|
74
|
+
INVALID_FORMAT = "invalid_format"
|
|
75
|
+
INVALID_PATTERN = "invalid_pattern"
|
|
76
|
+
NOT_MULTIPLE_OF = "not_multiple_of"
|
|
77
|
+
NON_UNIQUE_ITEMS = "non_unique_items"
|
|
78
|
+
|
|
79
|
+
# Negative scenarios - Missing parameters
|
|
80
|
+
MISSING_PARAMETER = "missing_parameter"
|
|
81
|
+
DUPLICATE_PARAMETER = "duplicate_parameter"
|
|
82
|
+
|
|
83
|
+
# Negative scenarios - Unsupported patterns
|
|
84
|
+
UNSUPPORTED_PATH_PATTERN = "unsupported_path_pattern"
|
|
85
|
+
UNSPECIFIED_HTTP_METHOD = "unspecified_http_method"
|
|
86
|
+
|
|
87
|
+
|
|
19
88
|
@dataclass
|
|
20
89
|
class ComponentInfo:
|
|
21
90
|
"""Information about how a specific component was generated."""
|
|
@@ -50,12 +119,13 @@ class ExamplesPhaseData:
|
|
|
50
119
|
class CoveragePhaseData:
|
|
51
120
|
"""Metadata specific to coverage phase."""
|
|
52
121
|
|
|
122
|
+
scenario: CoverageScenario
|
|
53
123
|
description: str
|
|
54
124
|
location: str | None
|
|
55
125
|
parameter: str | None
|
|
56
126
|
parameter_location: ParameterLocation | None
|
|
57
127
|
|
|
58
|
-
__slots__ = ("description", "location", "parameter", "parameter_location")
|
|
128
|
+
__slots__ = ("scenario", "description", "location", "parameter", "parameter_location")
|
|
59
129
|
|
|
60
130
|
|
|
61
131
|
@dataclass
|
|
@@ -70,6 +140,7 @@ class PhaseInfo:
|
|
|
70
140
|
@classmethod
|
|
71
141
|
def coverage(
|
|
72
142
|
cls,
|
|
143
|
+
scenario: CoverageScenario,
|
|
73
144
|
description: str,
|
|
74
145
|
location: str | None = None,
|
|
75
146
|
parameter: str | None = None,
|
|
@@ -78,7 +149,11 @@ class PhaseInfo:
|
|
|
78
149
|
return cls(
|
|
79
150
|
name=TestPhase.COVERAGE,
|
|
80
151
|
data=CoveragePhaseData(
|
|
81
|
-
|
|
152
|
+
scenario=scenario,
|
|
153
|
+
description=description,
|
|
154
|
+
location=location,
|
|
155
|
+
parameter=parameter,
|
|
156
|
+
parameter_location=parameter_location,
|
|
82
157
|
),
|
|
83
158
|
)
|
|
84
159
|
|
schemathesis/pytest/lazy.py
CHANGED
|
@@ -37,7 +37,6 @@ def get_all_tests(
|
|
|
37
37
|
*,
|
|
38
38
|
schema: BaseSchema,
|
|
39
39
|
test_func: Callable,
|
|
40
|
-
modes: list[HypothesisTestMode],
|
|
41
40
|
settings: hypothesis.settings | None = None,
|
|
42
41
|
seed: int | None = None,
|
|
43
42
|
as_strategy_kwargs: Callable[[APIOperation], dict[str, Any]] | None = None,
|
|
@@ -51,11 +50,22 @@ def get_all_tests(
|
|
|
51
50
|
_as_strategy_kwargs = as_strategy_kwargs(operation)
|
|
52
51
|
else:
|
|
53
52
|
_as_strategy_kwargs = {}
|
|
53
|
+
|
|
54
|
+
# Get modes from config for this operation
|
|
55
|
+
modes = []
|
|
56
|
+
phases = schema.config.phases_for(operation=operation)
|
|
57
|
+
if phases.examples.enabled:
|
|
58
|
+
modes.append(HypothesisTestMode.EXAMPLES)
|
|
59
|
+
if phases.fuzzing.enabled:
|
|
60
|
+
modes.append(HypothesisTestMode.FUZZING)
|
|
61
|
+
if phases.coverage.enabled:
|
|
62
|
+
modes.append(HypothesisTestMode.COVERAGE)
|
|
63
|
+
|
|
54
64
|
test = create_test(
|
|
55
65
|
operation=operation,
|
|
56
66
|
test_func=test_func,
|
|
57
67
|
config=HypothesisTestConfig(
|
|
58
|
-
settings=settings,
|
|
68
|
+
settings=settings or schema.config.get_hypothesis_settings(operation=operation),
|
|
59
69
|
modes=modes,
|
|
60
70
|
seed=seed,
|
|
61
71
|
project=schema.config,
|
|
@@ -170,31 +180,57 @@ class LazySchema:
|
|
|
170
180
|
else:
|
|
171
181
|
given_kwargs = {}
|
|
172
182
|
|
|
173
|
-
def wrapped_test(request: FixtureRequest) -> None:
|
|
183
|
+
def wrapped_test(*args: Any, request: FixtureRequest, **kwargs: Any) -> None:
|
|
174
184
|
"""The actual test, which is executed by pytest."""
|
|
175
185
|
__tracebackhide__ = True
|
|
186
|
+
|
|
187
|
+
# Load all checks eagerly, so they are accessible inside the test function
|
|
188
|
+
from schemathesis.checks import load_all_checks
|
|
189
|
+
|
|
190
|
+
load_all_checks()
|
|
191
|
+
|
|
176
192
|
schema = get_schema(
|
|
177
193
|
request=request,
|
|
178
194
|
name=self.fixture_name,
|
|
179
195
|
test_function=test_func,
|
|
180
196
|
filter_set=self.filter_set,
|
|
181
197
|
)
|
|
182
|
-
|
|
198
|
+
# Check if test function is a method and inject self from request.instance
|
|
199
|
+
sig = signature(test_func)
|
|
200
|
+
if "self" in sig.parameters and request.instance is not None:
|
|
201
|
+
fixtures = {"self": request.instance}
|
|
202
|
+
fixtures.update(get_fixtures(test_func, request, given_kwargs))
|
|
203
|
+
else:
|
|
204
|
+
fixtures = get_fixtures(test_func, request, given_kwargs)
|
|
183
205
|
# Changing the node id is required for better reporting - the method and path will appear there
|
|
184
206
|
node_id = request.node._nodeid
|
|
185
207
|
settings = getattr(wrapped_test, "_hypothesis_internal_use_settings", None)
|
|
186
208
|
|
|
187
209
|
def as_strategy_kwargs(_operation: APIOperation) -> dict[str, Any]:
|
|
210
|
+
as_strategy_kwargs: dict[str, Any] = {}
|
|
211
|
+
|
|
212
|
+
auth = schema.config.auth_for(operation=_operation)
|
|
213
|
+
if auth is not None:
|
|
214
|
+
from requests.auth import _basic_auth_str
|
|
215
|
+
|
|
216
|
+
as_strategy_kwargs["headers"] = {"Authorization": _basic_auth_str(*auth)}
|
|
217
|
+
|
|
218
|
+
headers = schema.config.headers_for(operation=_operation)
|
|
219
|
+
if headers:
|
|
220
|
+
as_strategy_kwargs["headers"] = headers
|
|
221
|
+
|
|
188
222
|
override = overrides.for_operation(config=schema.config, operation=_operation)
|
|
223
|
+
for location, entry in override.items():
|
|
224
|
+
if entry:
|
|
225
|
+
as_strategy_kwargs[location.container_name] = entry
|
|
189
226
|
|
|
190
|
-
return
|
|
227
|
+
return as_strategy_kwargs
|
|
191
228
|
|
|
192
229
|
tests = list(
|
|
193
230
|
get_all_tests(
|
|
194
231
|
schema=schema,
|
|
195
232
|
test_func=test_func,
|
|
196
233
|
settings=settings,
|
|
197
|
-
modes=list(HypothesisTestMode),
|
|
198
234
|
as_strategy_kwargs=as_strategy_kwargs,
|
|
199
235
|
given_kwargs=given_kwargs,
|
|
200
236
|
)
|
|
@@ -213,14 +249,22 @@ class LazySchema:
|
|
|
213
249
|
_schema_error(subtests, result.err(), node_id)
|
|
214
250
|
subtests.item._nodeid = node_id
|
|
215
251
|
|
|
216
|
-
|
|
217
|
-
|
|
252
|
+
sig = signature(test_func)
|
|
253
|
+
if "self" in sig.parameters:
|
|
254
|
+
# For methods, wrap with staticmethod to prevent pytest from passing self
|
|
255
|
+
wrapped_test = staticmethod(wrapped_test) # type: ignore[assignment]
|
|
256
|
+
wrapped_func = wrapped_test.__func__ # type: ignore[attr-defined]
|
|
257
|
+
else:
|
|
258
|
+
wrapped_func = wrapped_test
|
|
259
|
+
|
|
260
|
+
wrapped_func = pytest.mark.usefixtures(self.fixture_name)(wrapped_func)
|
|
261
|
+
_copy_marks(test_func, wrapped_func)
|
|
218
262
|
|
|
219
263
|
# Needed to prevent a failure when settings are applied to the test function
|
|
220
|
-
|
|
221
|
-
|
|
264
|
+
wrapped_func.is_hypothesis_test = True # type: ignore
|
|
265
|
+
wrapped_func.hypothesis = HypothesisHandle(test_func, wrapped_func, given_kwargs) # type: ignore
|
|
222
266
|
|
|
223
|
-
return wrapped_test
|
|
267
|
+
return wrapped_test if "self" in sig.parameters else wrapped_func
|
|
224
268
|
|
|
225
269
|
return wrapper
|
|
226
270
|
|
|
@@ -287,5 +331,7 @@ def get_fixtures(func: Callable, request: FixtureRequest, given_kwargs: dict[str
|
|
|
287
331
|
"""Load fixtures, needed for the test function."""
|
|
288
332
|
sig = signature(func)
|
|
289
333
|
return {
|
|
290
|
-
name: request.getfixturevalue(name)
|
|
334
|
+
name: request.getfixturevalue(name)
|
|
335
|
+
for name in sig.parameters
|
|
336
|
+
if name not in ("case", "self") and name not in given_kwargs
|
|
291
337
|
}
|
schemathesis/pytest/plugin.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import
|
|
3
|
+
import inspect
|
|
4
4
|
import unittest
|
|
5
5
|
from functools import partial
|
|
6
6
|
from typing import TYPE_CHECKING, Any, Callable, Generator, Type, cast
|
|
@@ -172,7 +172,7 @@ class SchemathesisCase(PyCollector):
|
|
|
172
172
|
as_strategy_kwargs=as_strategy_kwargs,
|
|
173
173
|
),
|
|
174
174
|
)
|
|
175
|
-
if
|
|
175
|
+
if inspect.iscoroutinefunction(self.test_function):
|
|
176
176
|
# `pytest-trio` expects a coroutine function
|
|
177
177
|
if is_trio_test:
|
|
178
178
|
funcobj.hypothesis.inner_test = self.test_function # type: ignore
|
|
@@ -17,7 +17,6 @@ from schemathesis.core.control import SkipTest
|
|
|
17
17
|
from schemathesis.core.errors import SERIALIZERS_SUGGESTION_MESSAGE, SerializationNotPossible
|
|
18
18
|
from schemathesis.core.jsonschema.types import JsonSchema
|
|
19
19
|
from schemathesis.core.parameters import ParameterLocation
|
|
20
|
-
from schemathesis.core.transforms import deepclone
|
|
21
20
|
from schemathesis.core.transport import prepare_urlencoded
|
|
22
21
|
from schemathesis.generation.meta import (
|
|
23
22
|
CaseMetadata,
|
|
@@ -29,7 +28,7 @@ from schemathesis.generation.meta import (
|
|
|
29
28
|
StatefulPhaseData,
|
|
30
29
|
TestPhase,
|
|
31
30
|
)
|
|
32
|
-
from schemathesis.openapi.generation.filters import
|
|
31
|
+
from schemathesis.openapi.generation.filters import is_valid_urlencoded
|
|
33
32
|
from schemathesis.schemas import APIOperation
|
|
34
33
|
from schemathesis.specs.openapi.adapter.parameters import OpenApiBody, OpenApiParameterSet
|
|
35
34
|
|
|
@@ -116,7 +115,9 @@ def openapi_cases(
|
|
|
116
115
|
else:
|
|
117
116
|
candidates = operation.body.items
|
|
118
117
|
parameter = draw(st.sampled_from(candidates))
|
|
119
|
-
strategy = _get_body_strategy(
|
|
118
|
+
strategy = _get_body_strategy(
|
|
119
|
+
parameter, strategy_factory, operation, generation_config, draw, body_generator
|
|
120
|
+
)
|
|
120
121
|
strategy = apply_hooks(operation, ctx, hooks, strategy, ParameterLocation.BODY)
|
|
121
122
|
# Parameter may have a wildcard media type. In this case, choose any supported one
|
|
122
123
|
possible_media_types = sorted(
|
|
@@ -209,21 +210,14 @@ def _get_body_strategy(
|
|
|
209
210
|
operation: APIOperation,
|
|
210
211
|
generation_config: GenerationConfig,
|
|
211
212
|
draw: st.DrawFn,
|
|
213
|
+
generation_mode: GenerationMode,
|
|
212
214
|
) -> st.SearchStrategy:
|
|
213
|
-
from schemathesis.specs.openapi.schemas import BaseOpenAPISchema
|
|
214
|
-
|
|
215
215
|
if parameter.media_type in MEDIA_TYPES:
|
|
216
216
|
return MEDIA_TYPES[parameter.media_type]
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
strategy =
|
|
220
|
-
|
|
221
|
-
operation.label,
|
|
222
|
-
ParameterLocation.BODY,
|
|
223
|
-
parameter.media_type,
|
|
224
|
-
generation_config,
|
|
225
|
-
operation.schema.adapter.jsonschema_validator_cls,
|
|
226
|
-
)
|
|
217
|
+
|
|
218
|
+
# Use the cached strategy from the parameter
|
|
219
|
+
strategy = parameter.get_strategy(operation, generation_config, generation_mode)
|
|
220
|
+
|
|
227
221
|
# It is likely will be rejected, hence choose it rarely
|
|
228
222
|
if (
|
|
229
223
|
not parameter.is_required
|
|
@@ -241,7 +235,7 @@ def get_parameters_value(
|
|
|
241
235
|
operation: APIOperation,
|
|
242
236
|
ctx: HookContext,
|
|
243
237
|
hooks: HookDispatcher | None,
|
|
244
|
-
|
|
238
|
+
generation_mode: GenerationMode,
|
|
245
239
|
generation_config: GenerationConfig,
|
|
246
240
|
) -> dict[str, Any] | None:
|
|
247
241
|
"""Get the final value for the specified location.
|
|
@@ -250,10 +244,10 @@ def get_parameters_value(
|
|
|
250
244
|
generate those parts.
|
|
251
245
|
"""
|
|
252
246
|
if value is None:
|
|
253
|
-
strategy = get_parameters_strategy(operation,
|
|
247
|
+
strategy = get_parameters_strategy(operation, generation_mode, location, generation_config)
|
|
254
248
|
strategy = apply_hooks(operation, ctx, hooks, strategy, location)
|
|
255
249
|
return draw(strategy)
|
|
256
|
-
strategy = get_parameters_strategy(operation,
|
|
250
|
+
strategy = get_parameters_strategy(operation, generation_mode, location, generation_config, exclude=value.keys())
|
|
257
251
|
strategy = apply_hooks(operation, ctx, hooks, strategy, location)
|
|
258
252
|
new = draw(strategy)
|
|
259
253
|
if new is not None:
|
|
@@ -304,11 +298,8 @@ def generate_parameter(
|
|
|
304
298
|
):
|
|
305
299
|
# If we can't negate any parameter, generate positive ones
|
|
306
300
|
# If nothing else will be negated, then skip the test completely
|
|
307
|
-
strategy_factory = make_positive_strategy
|
|
308
301
|
generator = GenerationMode.POSITIVE
|
|
309
|
-
|
|
310
|
-
strategy_factory = GENERATOR_MODE_TO_STRATEGY_FACTORY[generator]
|
|
311
|
-
value = get_parameters_value(explicit, location, draw, operation, ctx, hooks, strategy_factory, generation_config)
|
|
302
|
+
value = get_parameters_value(explicit, location, draw, operation, ctx, hooks, generator, generation_config)
|
|
312
303
|
used_generator: GenerationMode | None = generator
|
|
313
304
|
if value == explicit:
|
|
314
305
|
# When we pass `explicit`, then its parts are excluded from generation of the final value
|
|
@@ -335,86 +326,22 @@ def can_negate_headers(operation: APIOperation, location: ParameterLocation) ->
|
|
|
335
326
|
headers = container.schema["properties"]
|
|
336
327
|
if not headers:
|
|
337
328
|
return True
|
|
338
|
-
return any(
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
def get_schema_for_location(location: ParameterLocation, parameters: OpenApiParameterSet) -> dict[str, Any]:
|
|
342
|
-
schema = deepclone(parameters.schema)
|
|
343
|
-
if location == ParameterLocation.PATH:
|
|
344
|
-
schema["required"] = list(schema["properties"])
|
|
345
|
-
# Shallow copy properties dict itself and each modified property
|
|
346
|
-
properties = schema.get("properties", {})
|
|
347
|
-
if properties:
|
|
348
|
-
schema["properties"] = {
|
|
349
|
-
key: {**value, "minLength": value.get("minLength", 1)}
|
|
350
|
-
if value.get("type") == "string" and "minLength" not in value
|
|
351
|
-
else value
|
|
352
|
-
for key, value in properties.items()
|
|
353
|
-
}
|
|
354
|
-
return schema
|
|
329
|
+
return any(
|
|
330
|
+
header not in ({"type": "string"}, {"type": "string", "format": HEADER_FORMAT}) for header in headers.values()
|
|
331
|
+
)
|
|
355
332
|
|
|
356
333
|
|
|
357
334
|
def get_parameters_strategy(
|
|
358
335
|
operation: APIOperation,
|
|
359
|
-
|
|
336
|
+
generation_mode: GenerationMode,
|
|
360
337
|
location: ParameterLocation,
|
|
361
338
|
generation_config: GenerationConfig,
|
|
362
339
|
exclude: Iterable[str] = (),
|
|
363
340
|
) -> st.SearchStrategy:
|
|
364
341
|
"""Create a new strategy for the case's component from the API operation parameters."""
|
|
365
|
-
from schemathesis.specs.openapi.schemas import BaseOpenAPISchema
|
|
366
|
-
|
|
367
342
|
container = getattr(operation, location.container_name)
|
|
368
343
|
if container:
|
|
369
|
-
|
|
370
|
-
if location == ParameterLocation.HEADER and exclude:
|
|
371
|
-
# Remove excluded headers case-insensitively
|
|
372
|
-
exclude_lower = {name.lower() for name in exclude}
|
|
373
|
-
schema["properties"] = {
|
|
374
|
-
key: value for key, value in schema["properties"].items() if key.lower() not in exclude_lower
|
|
375
|
-
}
|
|
376
|
-
if "required" in schema:
|
|
377
|
-
schema["required"] = [key for key in schema["required"] if key.lower() not in exclude_lower]
|
|
378
|
-
elif exclude:
|
|
379
|
-
# Non-header locations: remove by exact name
|
|
380
|
-
schema = dict(schema)
|
|
381
|
-
schema["properties"] = {key: value for key, value in schema["properties"].items() if key not in exclude}
|
|
382
|
-
if "required" in schema:
|
|
383
|
-
schema["required"] = [key for key in schema["required"] if key not in exclude]
|
|
384
|
-
if not schema["properties"] and strategy_factory is make_negative_strategy:
|
|
385
|
-
# Nothing to negate - all properties were excluded
|
|
386
|
-
strategy = st.none()
|
|
387
|
-
else:
|
|
388
|
-
assert isinstance(operation.schema, BaseOpenAPISchema)
|
|
389
|
-
strategy = strategy_factory(
|
|
390
|
-
schema,
|
|
391
|
-
operation.label,
|
|
392
|
-
location,
|
|
393
|
-
None,
|
|
394
|
-
generation_config,
|
|
395
|
-
operation.schema.adapter.jsonschema_validator_cls,
|
|
396
|
-
)
|
|
397
|
-
serialize = operation.get_parameter_serializer(location)
|
|
398
|
-
if serialize is not None:
|
|
399
|
-
strategy = strategy.map(serialize)
|
|
400
|
-
filter_func = {
|
|
401
|
-
ParameterLocation.PATH: is_valid_path,
|
|
402
|
-
ParameterLocation.HEADER: is_valid_header,
|
|
403
|
-
ParameterLocation.COOKIE: is_valid_header,
|
|
404
|
-
ParameterLocation.QUERY: is_valid_query,
|
|
405
|
-
}[location]
|
|
406
|
-
# Headers with special format do not need filtration
|
|
407
|
-
if not (location.is_in_header and _can_skip_header_filter(schema)):
|
|
408
|
-
strategy = strategy.filter(filter_func)
|
|
409
|
-
# Path & query parameters will be cast to string anyway, but having their JSON equivalents for
|
|
410
|
-
# `True` / `False` / `None` improves chances of them passing validation in apps
|
|
411
|
-
# that expect boolean / null types
|
|
412
|
-
# and not aware of Python-specific representation of those types
|
|
413
|
-
if location == ParameterLocation.PATH:
|
|
414
|
-
strategy = strategy.map(quote_all).map(jsonify_python_specific_types)
|
|
415
|
-
elif location == ParameterLocation.QUERY:
|
|
416
|
-
strategy = strategy.map(jsonify_python_specific_types)
|
|
417
|
-
return strategy
|
|
344
|
+
return container.get_strategy(operation, generation_config, generation_mode, exclude)
|
|
418
345
|
# No parameters defined for this location
|
|
419
346
|
return st.none()
|
|
420
347
|
|
|
@@ -464,13 +391,6 @@ def make_positive_strategy(
|
|
|
464
391
|
validator_cls: type[jsonschema.protocols.Validator],
|
|
465
392
|
) -> st.SearchStrategy:
|
|
466
393
|
"""Strategy for generating values that fit the schema."""
|
|
467
|
-
if location.is_in_header and isinstance(schema, dict):
|
|
468
|
-
# We try to enforce the right header values via "format"
|
|
469
|
-
# This way, only allowed values will be used during data generation, which reduces the amount of filtering later
|
|
470
|
-
# If a property schema contains `pattern` it leads to heavy filtering and worse performance - therefore, skip it
|
|
471
|
-
for sub_schema in schema.get("properties", {}).values():
|
|
472
|
-
if list(sub_schema) == ["type"] and sub_schema["type"] == "string":
|
|
473
|
-
sub_schema.setdefault("format", HEADER_FORMAT)
|
|
474
394
|
custom_formats = _build_custom_formats(generation_config)
|
|
475
395
|
return from_schema(
|
|
476
396
|
schema,
|