schemathesis 3.25.6__py3-none-any.whl → 4.0.0a1__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.
- schemathesis/__init__.py +27 -65
- schemathesis/auths.py +102 -82
- schemathesis/checks.py +126 -46
- schemathesis/cli/__init__.py +11 -1760
- schemathesis/cli/__main__.py +4 -0
- schemathesis/cli/commands/__init__.py +37 -0
- schemathesis/cli/commands/run/__init__.py +662 -0
- schemathesis/cli/commands/run/checks.py +80 -0
- schemathesis/cli/commands/run/context.py +117 -0
- schemathesis/cli/commands/run/events.py +35 -0
- schemathesis/cli/commands/run/executor.py +138 -0
- schemathesis/cli/commands/run/filters.py +194 -0
- schemathesis/cli/commands/run/handlers/__init__.py +46 -0
- schemathesis/cli/commands/run/handlers/base.py +18 -0
- schemathesis/cli/commands/run/handlers/cassettes.py +494 -0
- schemathesis/cli/commands/run/handlers/junitxml.py +54 -0
- schemathesis/cli/commands/run/handlers/output.py +746 -0
- schemathesis/cli/commands/run/hypothesis.py +105 -0
- schemathesis/cli/commands/run/loaders.py +129 -0
- schemathesis/cli/{callbacks.py → commands/run/validation.py} +103 -174
- schemathesis/cli/constants.py +5 -52
- schemathesis/cli/core.py +17 -0
- schemathesis/cli/ext/fs.py +14 -0
- schemathesis/cli/ext/groups.py +55 -0
- schemathesis/cli/{options.py → ext/options.py} +39 -10
- schemathesis/cli/hooks.py +36 -0
- schemathesis/contrib/__init__.py +1 -3
- schemathesis/contrib/openapi/__init__.py +1 -3
- schemathesis/contrib/openapi/fill_missing_examples.py +3 -5
- schemathesis/core/__init__.py +58 -0
- schemathesis/core/compat.py +25 -0
- schemathesis/core/control.py +2 -0
- schemathesis/core/curl.py +58 -0
- schemathesis/core/deserialization.py +65 -0
- schemathesis/core/errors.py +370 -0
- schemathesis/core/failures.py +285 -0
- schemathesis/core/fs.py +19 -0
- schemathesis/{_lazy_import.py → core/lazy_import.py} +1 -0
- schemathesis/core/loaders.py +104 -0
- schemathesis/core/marks.py +66 -0
- schemathesis/{transports/content_types.py → core/media_types.py} +17 -13
- schemathesis/core/output/__init__.py +69 -0
- schemathesis/core/output/sanitization.py +197 -0
- schemathesis/core/rate_limit.py +60 -0
- schemathesis/core/registries.py +31 -0
- schemathesis/{internal → core}/result.py +1 -1
- schemathesis/core/transforms.py +113 -0
- schemathesis/core/transport.py +108 -0
- schemathesis/core/validation.py +38 -0
- schemathesis/core/version.py +7 -0
- schemathesis/engine/__init__.py +30 -0
- schemathesis/engine/config.py +59 -0
- schemathesis/engine/context.py +119 -0
- schemathesis/engine/control.py +36 -0
- schemathesis/engine/core.py +157 -0
- schemathesis/engine/errors.py +394 -0
- schemathesis/engine/events.py +337 -0
- schemathesis/engine/phases/__init__.py +66 -0
- schemathesis/{runner → engine/phases}/probes.py +50 -67
- schemathesis/engine/phases/stateful/__init__.py +65 -0
- schemathesis/engine/phases/stateful/_executor.py +326 -0
- schemathesis/engine/phases/stateful/context.py +85 -0
- schemathesis/engine/phases/unit/__init__.py +174 -0
- schemathesis/engine/phases/unit/_executor.py +321 -0
- schemathesis/engine/phases/unit/_pool.py +74 -0
- schemathesis/engine/recorder.py +241 -0
- schemathesis/errors.py +31 -0
- schemathesis/experimental/__init__.py +18 -14
- schemathesis/filters.py +103 -14
- schemathesis/generation/__init__.py +21 -37
- schemathesis/generation/case.py +190 -0
- schemathesis/generation/coverage.py +931 -0
- schemathesis/generation/hypothesis/__init__.py +30 -0
- schemathesis/generation/hypothesis/builder.py +585 -0
- schemathesis/generation/hypothesis/examples.py +50 -0
- schemathesis/generation/hypothesis/given.py +66 -0
- schemathesis/generation/hypothesis/reporting.py +14 -0
- schemathesis/generation/hypothesis/strategies.py +16 -0
- schemathesis/generation/meta.py +115 -0
- schemathesis/generation/modes.py +28 -0
- schemathesis/generation/overrides.py +96 -0
- schemathesis/generation/stateful/__init__.py +20 -0
- schemathesis/{stateful → generation/stateful}/state_machine.py +68 -81
- schemathesis/generation/targets.py +69 -0
- schemathesis/graphql/__init__.py +15 -0
- schemathesis/graphql/checks.py +115 -0
- schemathesis/graphql/loaders.py +131 -0
- schemathesis/hooks.py +99 -67
- schemathesis/openapi/__init__.py +13 -0
- schemathesis/openapi/checks.py +412 -0
- schemathesis/openapi/generation/__init__.py +0 -0
- schemathesis/openapi/generation/filters.py +63 -0
- schemathesis/openapi/loaders.py +178 -0
- schemathesis/pytest/__init__.py +5 -0
- schemathesis/pytest/control_flow.py +7 -0
- schemathesis/pytest/lazy.py +273 -0
- schemathesis/pytest/loaders.py +12 -0
- schemathesis/{extra/pytest_plugin.py → pytest/plugin.py} +106 -127
- schemathesis/python/__init__.py +0 -0
- schemathesis/python/asgi.py +12 -0
- schemathesis/python/wsgi.py +12 -0
- schemathesis/schemas.py +537 -261
- schemathesis/specs/graphql/__init__.py +0 -1
- schemathesis/specs/graphql/_cache.py +25 -0
- schemathesis/specs/graphql/nodes.py +1 -0
- schemathesis/specs/graphql/scalars.py +7 -5
- schemathesis/specs/graphql/schemas.py +215 -187
- schemathesis/specs/graphql/validation.py +11 -18
- schemathesis/specs/openapi/__init__.py +7 -1
- schemathesis/specs/openapi/_cache.py +122 -0
- schemathesis/specs/openapi/_hypothesis.py +146 -165
- schemathesis/specs/openapi/checks.py +565 -67
- schemathesis/specs/openapi/converter.py +33 -6
- schemathesis/specs/openapi/definitions.py +11 -18
- schemathesis/specs/openapi/examples.py +139 -23
- schemathesis/specs/openapi/expressions/__init__.py +37 -2
- schemathesis/specs/openapi/expressions/context.py +4 -6
- schemathesis/specs/openapi/expressions/extractors.py +23 -0
- schemathesis/specs/openapi/expressions/lexer.py +20 -18
- schemathesis/specs/openapi/expressions/nodes.py +38 -14
- schemathesis/specs/openapi/expressions/parser.py +26 -5
- schemathesis/specs/openapi/formats.py +45 -0
- schemathesis/specs/openapi/links.py +65 -165
- schemathesis/specs/openapi/media_types.py +32 -0
- schemathesis/specs/openapi/negative/__init__.py +7 -3
- schemathesis/specs/openapi/negative/mutations.py +24 -8
- schemathesis/specs/openapi/parameters.py +46 -30
- schemathesis/specs/openapi/patterns.py +137 -0
- schemathesis/specs/openapi/references.py +47 -57
- schemathesis/specs/openapi/schemas.py +478 -369
- schemathesis/specs/openapi/security.py +25 -7
- schemathesis/specs/openapi/serialization.py +11 -6
- schemathesis/specs/openapi/stateful/__init__.py +185 -73
- schemathesis/specs/openapi/utils.py +6 -1
- schemathesis/transport/__init__.py +104 -0
- schemathesis/transport/asgi.py +26 -0
- schemathesis/transport/prepare.py +99 -0
- schemathesis/transport/requests.py +221 -0
- schemathesis/{_xml.py → transport/serialization.py} +143 -28
- schemathesis/transport/wsgi.py +165 -0
- schemathesis-4.0.0a1.dist-info/METADATA +297 -0
- schemathesis-4.0.0a1.dist-info/RECORD +152 -0
- {schemathesis-3.25.6.dist-info → schemathesis-4.0.0a1.dist-info}/WHEEL +1 -1
- {schemathesis-3.25.6.dist-info → schemathesis-4.0.0a1.dist-info}/entry_points.txt +1 -1
- schemathesis/_compat.py +0 -74
- schemathesis/_dependency_versions.py +0 -17
- schemathesis/_hypothesis.py +0 -246
- schemathesis/_override.py +0 -49
- schemathesis/cli/cassettes.py +0 -375
- schemathesis/cli/context.py +0 -58
- schemathesis/cli/debug.py +0 -26
- schemathesis/cli/handlers.py +0 -16
- schemathesis/cli/junitxml.py +0 -43
- schemathesis/cli/output/__init__.py +0 -1
- schemathesis/cli/output/default.py +0 -790
- schemathesis/cli/output/short.py +0 -44
- schemathesis/cli/sanitization.py +0 -20
- schemathesis/code_samples.py +0 -149
- schemathesis/constants.py +0 -55
- schemathesis/contrib/openapi/formats/__init__.py +0 -9
- schemathesis/contrib/openapi/formats/uuid.py +0 -15
- schemathesis/contrib/unique_data.py +0 -41
- schemathesis/exceptions.py +0 -560
- schemathesis/extra/_aiohttp.py +0 -27
- schemathesis/extra/_flask.py +0 -10
- schemathesis/extra/_server.py +0 -17
- schemathesis/failures.py +0 -209
- schemathesis/fixups/__init__.py +0 -36
- schemathesis/fixups/fast_api.py +0 -41
- schemathesis/fixups/utf8_bom.py +0 -29
- schemathesis/graphql.py +0 -4
- schemathesis/internal/__init__.py +0 -7
- schemathesis/internal/copy.py +0 -13
- schemathesis/internal/datetime.py +0 -5
- schemathesis/internal/deprecation.py +0 -34
- schemathesis/internal/jsonschema.py +0 -35
- schemathesis/internal/transformation.py +0 -15
- schemathesis/internal/validation.py +0 -34
- schemathesis/lazy.py +0 -361
- schemathesis/loaders.py +0 -120
- schemathesis/models.py +0 -1234
- schemathesis/parameters.py +0 -86
- schemathesis/runner/__init__.py +0 -570
- schemathesis/runner/events.py +0 -329
- schemathesis/runner/impl/__init__.py +0 -3
- schemathesis/runner/impl/core.py +0 -1035
- schemathesis/runner/impl/solo.py +0 -90
- schemathesis/runner/impl/threadpool.py +0 -400
- schemathesis/runner/serialization.py +0 -411
- schemathesis/sanitization.py +0 -248
- schemathesis/serializers.py +0 -323
- schemathesis/service/__init__.py +0 -18
- schemathesis/service/auth.py +0 -11
- schemathesis/service/ci.py +0 -201
- schemathesis/service/client.py +0 -100
- schemathesis/service/constants.py +0 -38
- schemathesis/service/events.py +0 -57
- schemathesis/service/hosts.py +0 -107
- schemathesis/service/metadata.py +0 -46
- schemathesis/service/models.py +0 -49
- schemathesis/service/report.py +0 -255
- schemathesis/service/serialization.py +0 -199
- schemathesis/service/usage.py +0 -65
- schemathesis/specs/graphql/loaders.py +0 -344
- schemathesis/specs/openapi/filters.py +0 -49
- schemathesis/specs/openapi/loaders.py +0 -667
- schemathesis/specs/openapi/stateful/links.py +0 -92
- schemathesis/specs/openapi/validation.py +0 -25
- schemathesis/stateful/__init__.py +0 -133
- schemathesis/targets.py +0 -45
- schemathesis/throttling.py +0 -41
- schemathesis/transports/__init__.py +0 -5
- schemathesis/transports/auth.py +0 -15
- schemathesis/transports/headers.py +0 -35
- schemathesis/transports/responses.py +0 -52
- schemathesis/types.py +0 -35
- schemathesis/utils.py +0 -169
- schemathesis-3.25.6.dist-info/METADATA +0 -356
- schemathesis-3.25.6.dist-info/RECORD +0 -134
- /schemathesis/{extra → cli/ext}/__init__.py +0 -0
- {schemathesis-3.25.6.dist-info → schemathesis-4.0.0a1.dist-info}/licenses/LICENSE +0 -0
@@ -1,18 +1,21 @@
|
|
1
1
|
import os
|
2
2
|
from dataclasses import dataclass, field
|
3
3
|
|
4
|
-
from
|
4
|
+
from schemathesis.core import string_to_boolean
|
5
5
|
|
6
6
|
|
7
7
|
@dataclass(eq=False)
|
8
8
|
class Experiment:
|
9
9
|
name: str
|
10
|
-
verbose_name: str
|
11
10
|
env_var: str
|
12
11
|
description: str
|
13
12
|
discussion_url: str
|
14
13
|
_storage: "ExperimentSet" = field(repr=False)
|
15
14
|
|
15
|
+
@property
|
16
|
+
def label(self) -> str:
|
17
|
+
return self.name.lower().replace(" ", "-")
|
18
|
+
|
16
19
|
def enable(self) -> None:
|
17
20
|
self._storage.enable(self)
|
18
21
|
|
@@ -25,7 +28,7 @@ class Experiment:
|
|
25
28
|
|
26
29
|
@property
|
27
30
|
def is_env_var_set(self) -> bool:
|
28
|
-
return os.getenv(self.env_var, "")
|
31
|
+
return string_to_boolean(os.getenv(self.env_var, "")) is True
|
29
32
|
|
30
33
|
|
31
34
|
@dataclass
|
@@ -33,12 +36,9 @@ class ExperimentSet:
|
|
33
36
|
available: set = field(default_factory=set)
|
34
37
|
enabled: set = field(default_factory=set)
|
35
38
|
|
36
|
-
def create_experiment(
|
37
|
-
self, name: str, verbose_name: str, env_var: str, description: str, discussion_url: str
|
38
|
-
) -> Experiment:
|
39
|
+
def create_experiment(self, name: str, env_var: str, description: str, discussion_url: str) -> Experiment:
|
39
40
|
instance = Experiment(
|
40
41
|
name=name,
|
41
|
-
verbose_name=verbose_name,
|
42
42
|
env_var=f"{ENV_PREFIX}_{env_var}",
|
43
43
|
description=description,
|
44
44
|
discussion_url=discussion_url,
|
@@ -64,11 +64,15 @@ class ExperimentSet:
|
|
64
64
|
|
65
65
|
ENV_PREFIX = "SCHEMATHESIS_EXPERIMENTAL"
|
66
66
|
GLOBAL_EXPERIMENTS = ExperimentSet()
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
67
|
+
COVERAGE_PHASE = GLOBAL_EXPERIMENTS.create_experiment(
|
68
|
+
name="Coverage phase",
|
69
|
+
env_var="COVERAGE_PHASE",
|
70
|
+
description="Generate covering test cases",
|
71
|
+
discussion_url="https://github.com/schemathesis/schemathesis/discussions/2418",
|
72
|
+
)
|
73
|
+
POSITIVE_DATA_ACCEPTANCE = GLOBAL_EXPERIMENTS.create_experiment(
|
74
|
+
name="Positive Data Acceptance",
|
75
|
+
env_var="POSITIVE_DATA_ACCEPTANCE",
|
76
|
+
description="Verifying schema-conformant data is accepted",
|
77
|
+
discussion_url="https://github.com/schemathesis/schemathesis/discussions/2499",
|
74
78
|
)
|
schemathesis/filters.py
CHANGED
@@ -1,15 +1,19 @@
|
|
1
1
|
"""Filtering system that allows users to filter API operations based on certain criteria."""
|
2
|
+
|
2
3
|
from __future__ import annotations
|
4
|
+
|
5
|
+
import json
|
3
6
|
import re
|
4
7
|
from dataclasses import dataclass, field
|
5
8
|
from functools import partial
|
6
9
|
from types import SimpleNamespace
|
7
|
-
from typing import TYPE_CHECKING, Callable, List,
|
10
|
+
from typing import TYPE_CHECKING, Any, Callable, List, Protocol, Union
|
8
11
|
|
9
|
-
from .
|
12
|
+
from schemathesis.core.errors import IncorrectUsage
|
13
|
+
from schemathesis.core.transforms import resolve_pointer
|
10
14
|
|
11
15
|
if TYPE_CHECKING:
|
12
|
-
from .
|
16
|
+
from schemathesis.schemas import APIOperation
|
13
17
|
|
14
18
|
|
15
19
|
class HasAPIOperation(Protocol):
|
@@ -49,16 +53,21 @@ class Matcher:
|
|
49
53
|
func = partial(by_value_list, attribute=attribute, expected=expected)
|
50
54
|
else:
|
51
55
|
func = partial(by_value, attribute=attribute, expected=expected)
|
52
|
-
label = f"{attribute}={
|
56
|
+
label = f"{attribute}={expected!r}"
|
53
57
|
return cls(func, label=label, _hash=hash(label))
|
54
58
|
|
55
59
|
@classmethod
|
56
60
|
def for_regex(cls, attribute: str, regex: RegexValue) -> Matcher:
|
57
61
|
"""Matcher that checks whether the specified attribute has the provided regex."""
|
58
62
|
if isinstance(regex, str):
|
59
|
-
|
63
|
+
flags: re.RegexFlag | int
|
64
|
+
if attribute == "method":
|
65
|
+
flags = re.IGNORECASE
|
66
|
+
else:
|
67
|
+
flags = 0
|
68
|
+
regex = re.compile(regex, flags=flags)
|
60
69
|
func = partial(by_regex, attribute=attribute, regex=regex)
|
61
|
-
label = f"{attribute}_regex={
|
70
|
+
label = f"{attribute}_regex={regex!r}"
|
62
71
|
return cls(func, label=label, _hash=hash(label))
|
63
72
|
|
64
73
|
def match(self, ctx: HasAPIOperation) -> bool:
|
@@ -69,6 +78,8 @@ class Matcher:
|
|
69
78
|
def get_operation_attribute(operation: APIOperation, attribute: str) -> str | list[str] | None:
|
70
79
|
if attribute == "tag":
|
71
80
|
return operation.tags
|
81
|
+
if attribute == "operation_id":
|
82
|
+
return operation.definition.raw.get("operationId")
|
72
83
|
# Just uppercase `method`
|
73
84
|
value = getattr(operation, attribute)
|
74
85
|
if attribute == "method":
|
@@ -99,8 +110,8 @@ def by_regex(ctx: HasAPIOperation, attribute: str, regex: re.Pattern) -> bool:
|
|
99
110
|
if value is None:
|
100
111
|
return False
|
101
112
|
if isinstance(value, list):
|
102
|
-
return any(bool(regex.
|
103
|
-
return bool(regex.
|
113
|
+
return any(bool(regex.search(entry)) for entry in value)
|
114
|
+
return bool(regex.search(value))
|
104
115
|
|
105
116
|
|
106
117
|
@dataclass(repr=False, frozen=True)
|
@@ -109,6 +120,8 @@ class Filter:
|
|
109
120
|
|
110
121
|
matchers: tuple[Matcher, ...]
|
111
122
|
|
123
|
+
__slots__ = ("matchers",)
|
124
|
+
|
112
125
|
def __repr__(self) -> str:
|
113
126
|
inner = " && ".join(matcher.label for matcher in self.matchers)
|
114
127
|
return f"<{self.__class__.__name__}: [{inner}]>"
|
@@ -125,8 +138,17 @@ class Filter:
|
|
125
138
|
class FilterSet:
|
126
139
|
"""Combines multiple filters to apply inclusion and exclusion rules on API operations."""
|
127
140
|
|
128
|
-
_includes: set[Filter]
|
129
|
-
_excludes: set[Filter]
|
141
|
+
_includes: set[Filter]
|
142
|
+
_excludes: set[Filter]
|
143
|
+
|
144
|
+
__slots__ = ("_includes", "_excludes")
|
145
|
+
|
146
|
+
def __init__(self, _includes: set[Filter] | None = None, _excludes: set[Filter] | None = None) -> None:
|
147
|
+
self._includes = _includes or set()
|
148
|
+
self._excludes = _excludes or set()
|
149
|
+
|
150
|
+
def clone(self) -> FilterSet:
|
151
|
+
return FilterSet(_includes=self._includes.copy(), _excludes=self._excludes.copy())
|
130
152
|
|
131
153
|
def apply_to(self, operations: list[APIOperation]) -> list[APIOperation]:
|
132
154
|
"""Get a filtered list of the given operations that match the filters."""
|
@@ -166,6 +188,8 @@ class FilterSet:
|
|
166
188
|
path_regex: RegexValue | None = None,
|
167
189
|
tag: FilterValue | None = None,
|
168
190
|
tag_regex: RegexValue | None = None,
|
191
|
+
operation_id: FilterValue | None = None,
|
192
|
+
operation_id_regex: RegexValue | None = None,
|
169
193
|
) -> None:
|
170
194
|
"""Add a new INCLUDE filter."""
|
171
195
|
self._add_filter(
|
@@ -179,6 +203,8 @@ class FilterSet:
|
|
179
203
|
path_regex=path_regex,
|
180
204
|
tag=tag,
|
181
205
|
tag_regex=tag_regex,
|
206
|
+
operation_id=operation_id,
|
207
|
+
operation_id_regex=operation_id_regex,
|
182
208
|
)
|
183
209
|
|
184
210
|
def exclude(
|
@@ -193,6 +219,8 @@ class FilterSet:
|
|
193
219
|
path_regex: RegexValue | None = None,
|
194
220
|
tag: FilterValue | None = None,
|
195
221
|
tag_regex: RegexValue | None = None,
|
222
|
+
operation_id: FilterValue | None = None,
|
223
|
+
operation_id_regex: RegexValue | None = None,
|
196
224
|
) -> None:
|
197
225
|
"""Add a new EXCLUDE filter."""
|
198
226
|
self._add_filter(
|
@@ -206,6 +234,8 @@ class FilterSet:
|
|
206
234
|
path_regex=path_regex,
|
207
235
|
tag=tag,
|
208
236
|
tag_regex=tag_regex,
|
237
|
+
operation_id=operation_id,
|
238
|
+
operation_id_regex=operation_id_regex,
|
209
239
|
)
|
210
240
|
|
211
241
|
def _add_filter(
|
@@ -221,29 +251,32 @@ class FilterSet:
|
|
221
251
|
path_regex: RegexValue | None = None,
|
222
252
|
tag: FilterValue | None = None,
|
223
253
|
tag_regex: RegexValue | None = None,
|
254
|
+
operation_id: FilterValue | None = None,
|
255
|
+
operation_id_regex: RegexValue | None = None,
|
224
256
|
) -> None:
|
225
257
|
matchers = []
|
226
258
|
if func is not None:
|
227
259
|
matchers.append(Matcher.for_function(func))
|
228
260
|
for attribute, expected, regex in (
|
229
|
-
("
|
261
|
+
("label", name, name_regex),
|
230
262
|
("method", method, method_regex),
|
231
263
|
("path", path, path_regex),
|
232
264
|
("tag", tag, tag_regex),
|
265
|
+
("operation_id", operation_id, operation_id_regex),
|
233
266
|
):
|
234
267
|
if expected is not None and regex is not None:
|
235
268
|
# To match anything the regex should match the expected value, hence passing them together is useless
|
236
|
-
raise
|
269
|
+
raise IncorrectUsage(ERROR_EXPECTED_AND_REGEX)
|
237
270
|
if expected is not None:
|
238
271
|
matchers.append(Matcher.for_value(attribute, expected))
|
239
272
|
if regex is not None:
|
240
273
|
matchers.append(Matcher.for_regex(attribute, regex))
|
241
274
|
|
242
275
|
if not matchers:
|
243
|
-
raise
|
276
|
+
raise IncorrectUsage(ERROR_EMPTY_FILTER)
|
244
277
|
filter_ = Filter(matchers=tuple(matchers))
|
245
278
|
if filter_ in self._includes or filter_ in self._excludes:
|
246
|
-
raise
|
279
|
+
raise IncorrectUsage(ERROR_FILTER_EXISTS)
|
247
280
|
if include:
|
248
281
|
self._includes.add(filter_)
|
249
282
|
else:
|
@@ -274,8 +307,12 @@ def attach_filter_chain(
|
|
274
307
|
name_regex: str | None = None,
|
275
308
|
method: FilterValue | None = None,
|
276
309
|
method_regex: str | None = None,
|
310
|
+
tag: FilterValue | None = None,
|
311
|
+
tag_regex: RegexValue | None = None,
|
277
312
|
path: FilterValue | None = None,
|
278
313
|
path_regex: str | None = None,
|
314
|
+
operation_id: FilterValue | None = None,
|
315
|
+
operation_id_regex: RegexValue | None = None,
|
279
316
|
) -> Callable:
|
280
317
|
__tracebackhide__ = True
|
281
318
|
filter_func(
|
@@ -284,8 +321,12 @@ def attach_filter_chain(
|
|
284
321
|
name_regex=name_regex,
|
285
322
|
method=method,
|
286
323
|
method_regex=method_regex,
|
324
|
+
tag=tag,
|
325
|
+
tag_regex=tag_regex,
|
287
326
|
path=path,
|
288
327
|
path_regex=path_regex,
|
328
|
+
operation_id=operation_id,
|
329
|
+
operation_id_regex=operation_id_regex,
|
289
330
|
)
|
290
331
|
return target
|
291
332
|
|
@@ -293,3 +334,51 @@ def attach_filter_chain(
|
|
293
334
|
proxy.__name__ = attribute
|
294
335
|
|
295
336
|
setattr(target, attribute, proxy)
|
337
|
+
|
338
|
+
|
339
|
+
def is_deprecated(ctx: HasAPIOperation) -> bool:
|
340
|
+
return ctx.operation.definition.raw.get("deprecated") is True
|
341
|
+
|
342
|
+
|
343
|
+
def parse_expression(expression: str) -> tuple[str, str, Any]:
|
344
|
+
expression = expression.strip()
|
345
|
+
|
346
|
+
# Find the operator
|
347
|
+
for op in ("==", "!="):
|
348
|
+
try:
|
349
|
+
pointer, value = expression.split(op, 1)
|
350
|
+
break
|
351
|
+
except ValueError:
|
352
|
+
continue
|
353
|
+
else:
|
354
|
+
raise ValueError(f"Invalid expression: {expression}")
|
355
|
+
|
356
|
+
pointer = pointer.strip()
|
357
|
+
value = value.strip()
|
358
|
+
if not pointer or not value:
|
359
|
+
raise ValueError(f"Invalid expression: {expression}")
|
360
|
+
# Parse the JSON value
|
361
|
+
try:
|
362
|
+
return pointer, op, json.loads(value)
|
363
|
+
except json.JSONDecodeError:
|
364
|
+
# If it's not valid JSON, treat it as a string
|
365
|
+
return pointer, op, value
|
366
|
+
|
367
|
+
|
368
|
+
def expression_to_filter_function(expression: str) -> Callable[[HasAPIOperation], bool]:
|
369
|
+
pointer, op, value = parse_expression(expression)
|
370
|
+
|
371
|
+
if op == "==":
|
372
|
+
|
373
|
+
def filter_function(ctx: HasAPIOperation) -> bool:
|
374
|
+
definition = ctx.operation.definition.resolved
|
375
|
+
resolved = resolve_pointer(definition, pointer)
|
376
|
+
return resolved == value
|
377
|
+
else:
|
378
|
+
|
379
|
+
def filter_function(ctx: HasAPIOperation) -> bool:
|
380
|
+
definition = ctx.operation.definition.resolved
|
381
|
+
resolved = resolve_pointer(definition, pointer)
|
382
|
+
return resolved != value
|
383
|
+
|
384
|
+
return filter_function
|
@@ -1,46 +1,16 @@
|
|
1
1
|
from __future__ import annotations
|
2
|
-
import random
|
3
|
-
from dataclasses import dataclass
|
4
|
-
from enum import Enum
|
5
|
-
from typing import Union, Iterable
|
6
|
-
|
7
|
-
|
8
|
-
class DataGenerationMethod(str, Enum):
|
9
|
-
"""Defines what data Schemathesis generates for tests."""
|
10
|
-
|
11
|
-
# Generate data, that fits the API schema
|
12
|
-
positive = "positive"
|
13
|
-
# Doesn't fit the API schema
|
14
|
-
negative = "negative"
|
15
|
-
|
16
|
-
@classmethod
|
17
|
-
def default(cls) -> DataGenerationMethod:
|
18
|
-
return cls.positive
|
19
|
-
|
20
|
-
@classmethod
|
21
|
-
def all(cls) -> list[DataGenerationMethod]:
|
22
|
-
return list(DataGenerationMethod)
|
23
2
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
DataGenerationMethod.negative: "N",
|
28
|
-
}[self]
|
29
|
-
|
30
|
-
@property
|
31
|
-
def is_negative(self) -> bool:
|
32
|
-
return self == DataGenerationMethod.negative
|
3
|
+
import random
|
4
|
+
from dataclasses import dataclass, field
|
5
|
+
from typing import TYPE_CHECKING
|
33
6
|
|
34
|
-
|
35
|
-
def ensure_list(cls, value: DataGenerationMethodInput) -> list[DataGenerationMethod]:
|
36
|
-
if isinstance(value, DataGenerationMethod):
|
37
|
-
return [value]
|
38
|
-
return list(value)
|
7
|
+
from schemathesis.generation.modes import GenerationMode as GenerationMode
|
39
8
|
|
9
|
+
if TYPE_CHECKING:
|
10
|
+
from hypothesis.strategies import SearchStrategy
|
40
11
|
|
41
|
-
DataGenerationMethodInput = Union[DataGenerationMethod, Iterable[DataGenerationMethod]]
|
42
12
|
|
43
|
-
|
13
|
+
DEFAULT_GENERATOR_MODES = (GenerationMode.default(),)
|
44
14
|
|
45
15
|
|
46
16
|
CASE_ID_ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
|
@@ -58,11 +28,25 @@ def generate_random_case_id(length: int = 6) -> str:
|
|
58
28
|
return output
|
59
29
|
|
60
30
|
|
31
|
+
@dataclass
|
32
|
+
class HeaderConfig:
|
33
|
+
"""Configuration for generating headers."""
|
34
|
+
|
35
|
+
strategy: SearchStrategy[str] | None = None
|
36
|
+
|
37
|
+
|
61
38
|
@dataclass
|
62
39
|
class GenerationConfig:
|
63
40
|
"""Holds various configuration options relevant for data generation."""
|
64
41
|
|
42
|
+
modes: list[GenerationMode] = field(default_factory=lambda: [GenerationMode.default()])
|
65
43
|
# Allow generating `\x00` bytes in strings
|
66
44
|
allow_x00: bool = True
|
45
|
+
# Allowing using `null` for optional arguments in GraphQL queries
|
46
|
+
graphql_allow_null: bool = True
|
67
47
|
# Generate strings using the given codec
|
68
48
|
codec: str | None = "utf-8"
|
49
|
+
# Whether to generate security parameters
|
50
|
+
with_security_parameters: bool = True
|
51
|
+
# Header generation configuration
|
52
|
+
headers: HeaderConfig = field(default_factory=HeaderConfig)
|
@@ -0,0 +1,190 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from dataclasses import dataclass, field
|
4
|
+
from typing import TYPE_CHECKING, Any, Mapping
|
5
|
+
|
6
|
+
from schemathesis.checks import CHECKS, CheckContext, CheckFunction, run_checks
|
7
|
+
from schemathesis.core import NOT_SET, SCHEMATHESIS_TEST_CASE_HEADER, NotSet, curl
|
8
|
+
from schemathesis.core.failures import FailureGroup, failure_report_title, format_failures
|
9
|
+
from schemathesis.core.transport import Response
|
10
|
+
from schemathesis.generation import generate_random_case_id
|
11
|
+
from schemathesis.generation.meta import CaseMetadata
|
12
|
+
from schemathesis.generation.overrides import Override, store_components
|
13
|
+
from schemathesis.hooks import HookContext, dispatch
|
14
|
+
from schemathesis.transport.prepare import prepare_request
|
15
|
+
|
16
|
+
if TYPE_CHECKING:
|
17
|
+
import requests.auth
|
18
|
+
from requests.structures import CaseInsensitiveDict
|
19
|
+
|
20
|
+
from schemathesis.schemas import APIOperation
|
21
|
+
|
22
|
+
|
23
|
+
@dataclass
|
24
|
+
class Case:
|
25
|
+
"""A single test case parameters."""
|
26
|
+
|
27
|
+
operation: APIOperation
|
28
|
+
method: str
|
29
|
+
path: str
|
30
|
+
# Unique test case identifier
|
31
|
+
id: str = field(default_factory=generate_random_case_id, compare=False)
|
32
|
+
path_parameters: dict[str, Any] | None = None
|
33
|
+
headers: CaseInsensitiveDict | None = None
|
34
|
+
cookies: dict[str, Any] | None = None
|
35
|
+
query: dict[str, Any] | None = None
|
36
|
+
# By default, there is no body, but we can't use `None` as the default value because it clashes with `null`
|
37
|
+
# which is a valid payload.
|
38
|
+
body: list | dict[str, Any] | str | int | float | bool | bytes | NotSet = NOT_SET
|
39
|
+
# The media type for cases with a payload. For example, "application/json"
|
40
|
+
media_type: str | None = None
|
41
|
+
|
42
|
+
meta: CaseMetadata | None = field(compare=False, default=None)
|
43
|
+
|
44
|
+
_auth: requests.auth.AuthBase | None = None
|
45
|
+
_has_explicit_auth: bool = False
|
46
|
+
|
47
|
+
def __post_init__(self) -> None:
|
48
|
+
self._components = store_components(self)
|
49
|
+
|
50
|
+
@property
|
51
|
+
def _override(self) -> Override:
|
52
|
+
return Override.from_components(self._components, self)
|
53
|
+
|
54
|
+
def __repr__(self) -> str:
|
55
|
+
output = f"{self.__class__.__name__}("
|
56
|
+
first = True
|
57
|
+
for name in ("path_parameters", "headers", "cookies", "query", "body"):
|
58
|
+
value = getattr(self, name)
|
59
|
+
if value is not None and not isinstance(value, NotSet):
|
60
|
+
if first:
|
61
|
+
first = False
|
62
|
+
else:
|
63
|
+
output += ", "
|
64
|
+
output += f"{name}={value!r}"
|
65
|
+
return f"{output})"
|
66
|
+
|
67
|
+
def __hash__(self) -> int:
|
68
|
+
return hash(self.as_curl_command({SCHEMATHESIS_TEST_CASE_HEADER: "0"}))
|
69
|
+
|
70
|
+
def _repr_pretty_(self, *args: Any, **kwargs: Any) -> None: ...
|
71
|
+
|
72
|
+
def as_curl_command(self, headers: Mapping[str, Any] | None = None, verify: bool = True) -> str:
|
73
|
+
"""Construct a curl command for a given case."""
|
74
|
+
request_data = prepare_request(self, headers, self.operation.schema.output_config.sanitize)
|
75
|
+
return curl.generate(
|
76
|
+
method=str(request_data.method),
|
77
|
+
url=str(request_data.url),
|
78
|
+
body=request_data.body,
|
79
|
+
verify=verify,
|
80
|
+
headers=dict(request_data.headers),
|
81
|
+
known_generated_headers=dict(self.headers or {}),
|
82
|
+
)
|
83
|
+
|
84
|
+
def as_transport_kwargs(self, base_url: str | None = None, headers: dict[str, str] | None = None) -> dict[str, Any]:
|
85
|
+
"""Convert the test case into a dictionary acceptable by the underlying transport call."""
|
86
|
+
return self.operation.schema.transport.serialize_case(self, base_url=base_url, headers=headers)
|
87
|
+
|
88
|
+
def call(
|
89
|
+
self,
|
90
|
+
base_url: str | None = None,
|
91
|
+
session: requests.Session | None = None,
|
92
|
+
headers: dict[str, Any] | None = None,
|
93
|
+
params: dict[str, Any] | None = None,
|
94
|
+
cookies: dict[str, Any] | None = None,
|
95
|
+
**kwargs: Any,
|
96
|
+
) -> Response:
|
97
|
+
hook_context = HookContext(operation=self.operation)
|
98
|
+
dispatch("before_call", hook_context, self, **kwargs)
|
99
|
+
if self.operation.app is not None:
|
100
|
+
kwargs["app"] = self.operation.app
|
101
|
+
response = self.operation.schema.transport.send(
|
102
|
+
self,
|
103
|
+
session=session,
|
104
|
+
base_url=base_url,
|
105
|
+
headers=headers,
|
106
|
+
params=params,
|
107
|
+
cookies=cookies,
|
108
|
+
**kwargs,
|
109
|
+
)
|
110
|
+
dispatch("after_call", hook_context, self, response)
|
111
|
+
return response
|
112
|
+
|
113
|
+
def validate_response(
|
114
|
+
self,
|
115
|
+
response: Response,
|
116
|
+
checks: list[CheckFunction] | None = None,
|
117
|
+
additional_checks: list[CheckFunction] | None = None,
|
118
|
+
excluded_checks: list[CheckFunction] | None = None,
|
119
|
+
headers: dict[str, Any] | None = None,
|
120
|
+
transport_kwargs: dict[str, Any] | None = None,
|
121
|
+
) -> None:
|
122
|
+
"""Validate application response.
|
123
|
+
|
124
|
+
By default, all available checks will be applied.
|
125
|
+
|
126
|
+
:param response: Application response.
|
127
|
+
:param checks: A tuple of check functions that accept ``response`` and ``case``.
|
128
|
+
:param additional_checks: A tuple of additional checks that will be executed after ones from the ``checks``
|
129
|
+
argument.
|
130
|
+
:param excluded_checks: Checks excluded from the default ones.
|
131
|
+
"""
|
132
|
+
__tracebackhide__ = True
|
133
|
+
from requests.structures import CaseInsensitiveDict
|
134
|
+
|
135
|
+
checks = [
|
136
|
+
check
|
137
|
+
for check in list(checks or CHECKS.get_all()) + list(additional_checks or [])
|
138
|
+
if check not in set(excluded_checks or [])
|
139
|
+
]
|
140
|
+
|
141
|
+
ctx = CheckContext(
|
142
|
+
override=self._override,
|
143
|
+
auth=None,
|
144
|
+
headers=CaseInsensitiveDict(headers) if headers else None,
|
145
|
+
config={},
|
146
|
+
transport_kwargs=transport_kwargs,
|
147
|
+
recorder=None,
|
148
|
+
)
|
149
|
+
failures = run_checks(
|
150
|
+
case=self,
|
151
|
+
response=response,
|
152
|
+
ctx=ctx,
|
153
|
+
checks=checks,
|
154
|
+
on_failure=lambda _, collected, failure: collected.add(failure),
|
155
|
+
)
|
156
|
+
if failures:
|
157
|
+
_failures = list(failures)
|
158
|
+
message = failure_report_title(_failures) + "\n"
|
159
|
+
verify = getattr(response, "verify", True)
|
160
|
+
curl = self.as_curl_command(headers=dict(response.request.headers), verify=verify)
|
161
|
+
message += format_failures(
|
162
|
+
case_id=None,
|
163
|
+
response=response,
|
164
|
+
failures=_failures,
|
165
|
+
curl=curl,
|
166
|
+
config=self.operation.schema.output_config,
|
167
|
+
)
|
168
|
+
raise FailureGroup(_failures, message) from None
|
169
|
+
|
170
|
+
def call_and_validate(
|
171
|
+
self,
|
172
|
+
base_url: str | None = None,
|
173
|
+
session: requests.Session | None = None,
|
174
|
+
headers: dict[str, Any] | None = None,
|
175
|
+
checks: list[CheckFunction] | None = None,
|
176
|
+
additional_checks: list[CheckFunction] | None = None,
|
177
|
+
excluded_checks: list[CheckFunction] | None = None,
|
178
|
+
**kwargs: Any,
|
179
|
+
) -> Response:
|
180
|
+
__tracebackhide__ = True
|
181
|
+
response = self.call(base_url, session, headers, **kwargs)
|
182
|
+
self.validate_response(
|
183
|
+
response,
|
184
|
+
checks,
|
185
|
+
headers=headers,
|
186
|
+
additional_checks=additional_checks,
|
187
|
+
excluded_checks=excluded_checks,
|
188
|
+
transport_kwargs=kwargs,
|
189
|
+
)
|
190
|
+
return response
|