schemathesis 3.15.4__py3-none-any.whl → 4.4.2__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 +53 -25
- schemathesis/auths.py +507 -0
- schemathesis/checks.py +190 -25
- schemathesis/cli/__init__.py +27 -1219
- schemathesis/cli/__main__.py +4 -0
- schemathesis/cli/commands/__init__.py +133 -0
- schemathesis/cli/commands/data.py +10 -0
- schemathesis/cli/commands/run/__init__.py +602 -0
- schemathesis/cli/commands/run/context.py +228 -0
- schemathesis/cli/commands/run/events.py +60 -0
- schemathesis/cli/commands/run/executor.py +157 -0
- schemathesis/cli/commands/run/filters.py +53 -0
- schemathesis/cli/commands/run/handlers/__init__.py +46 -0
- schemathesis/cli/commands/run/handlers/base.py +45 -0
- schemathesis/cli/commands/run/handlers/cassettes.py +464 -0
- schemathesis/cli/commands/run/handlers/junitxml.py +60 -0
- schemathesis/cli/commands/run/handlers/output.py +1750 -0
- schemathesis/cli/commands/run/loaders.py +118 -0
- schemathesis/cli/commands/run/validation.py +256 -0
- schemathesis/cli/constants.py +5 -0
- schemathesis/cli/core.py +19 -0
- schemathesis/cli/ext/fs.py +16 -0
- schemathesis/cli/ext/groups.py +203 -0
- schemathesis/cli/ext/options.py +81 -0
- schemathesis/config/__init__.py +202 -0
- schemathesis/config/_auth.py +51 -0
- schemathesis/config/_checks.py +268 -0
- schemathesis/config/_diff_base.py +101 -0
- schemathesis/config/_env.py +21 -0
- schemathesis/config/_error.py +163 -0
- schemathesis/config/_generation.py +157 -0
- schemathesis/config/_health_check.py +24 -0
- schemathesis/config/_operations.py +335 -0
- schemathesis/config/_output.py +171 -0
- schemathesis/config/_parameters.py +19 -0
- schemathesis/config/_phases.py +253 -0
- schemathesis/config/_projects.py +543 -0
- schemathesis/config/_rate_limit.py +17 -0
- schemathesis/config/_report.py +120 -0
- schemathesis/config/_validator.py +9 -0
- schemathesis/config/_warnings.py +89 -0
- schemathesis/config/schema.json +975 -0
- schemathesis/core/__init__.py +72 -0
- schemathesis/core/adapter.py +34 -0
- schemathesis/core/compat.py +32 -0
- schemathesis/core/control.py +2 -0
- schemathesis/core/curl.py +100 -0
- schemathesis/core/deserialization.py +210 -0
- schemathesis/core/errors.py +588 -0
- schemathesis/core/failures.py +316 -0
- schemathesis/core/fs.py +19 -0
- schemathesis/core/hooks.py +20 -0
- schemathesis/core/jsonschema/__init__.py +13 -0
- schemathesis/core/jsonschema/bundler.py +183 -0
- schemathesis/core/jsonschema/keywords.py +40 -0
- schemathesis/core/jsonschema/references.py +222 -0
- schemathesis/core/jsonschema/types.py +41 -0
- schemathesis/core/lazy_import.py +15 -0
- schemathesis/core/loaders.py +107 -0
- schemathesis/core/marks.py +66 -0
- schemathesis/core/media_types.py +79 -0
- schemathesis/core/output/__init__.py +46 -0
- schemathesis/core/output/sanitization.py +54 -0
- schemathesis/core/parameters.py +45 -0
- schemathesis/core/rate_limit.py +60 -0
- schemathesis/core/registries.py +34 -0
- schemathesis/core/result.py +27 -0
- schemathesis/core/schema_analysis.py +17 -0
- schemathesis/core/shell.py +203 -0
- schemathesis/core/transforms.py +144 -0
- schemathesis/core/transport.py +223 -0
- schemathesis/core/validation.py +73 -0
- schemathesis/core/version.py +7 -0
- schemathesis/engine/__init__.py +28 -0
- schemathesis/engine/context.py +152 -0
- schemathesis/engine/control.py +44 -0
- schemathesis/engine/core.py +201 -0
- schemathesis/engine/errors.py +446 -0
- schemathesis/engine/events.py +284 -0
- schemathesis/engine/observations.py +42 -0
- schemathesis/engine/phases/__init__.py +108 -0
- schemathesis/engine/phases/analysis.py +28 -0
- schemathesis/engine/phases/probes.py +172 -0
- schemathesis/engine/phases/stateful/__init__.py +68 -0
- schemathesis/engine/phases/stateful/_executor.py +364 -0
- schemathesis/engine/phases/stateful/context.py +85 -0
- schemathesis/engine/phases/unit/__init__.py +220 -0
- schemathesis/engine/phases/unit/_executor.py +459 -0
- schemathesis/engine/phases/unit/_pool.py +82 -0
- schemathesis/engine/recorder.py +254 -0
- schemathesis/errors.py +47 -0
- schemathesis/filters.py +395 -0
- schemathesis/generation/__init__.py +25 -0
- schemathesis/generation/case.py +478 -0
- schemathesis/generation/coverage.py +1528 -0
- schemathesis/generation/hypothesis/__init__.py +121 -0
- schemathesis/generation/hypothesis/builder.py +992 -0
- schemathesis/generation/hypothesis/examples.py +56 -0
- schemathesis/generation/hypothesis/given.py +66 -0
- schemathesis/generation/hypothesis/reporting.py +285 -0
- schemathesis/generation/meta.py +227 -0
- schemathesis/generation/metrics.py +93 -0
- schemathesis/generation/modes.py +20 -0
- schemathesis/generation/overrides.py +127 -0
- schemathesis/generation/stateful/__init__.py +37 -0
- schemathesis/generation/stateful/state_machine.py +294 -0
- schemathesis/graphql/__init__.py +15 -0
- schemathesis/graphql/checks.py +109 -0
- schemathesis/graphql/loaders.py +285 -0
- schemathesis/hooks.py +270 -91
- schemathesis/openapi/__init__.py +13 -0
- schemathesis/openapi/checks.py +467 -0
- schemathesis/openapi/generation/__init__.py +0 -0
- schemathesis/openapi/generation/filters.py +72 -0
- schemathesis/openapi/loaders.py +315 -0
- schemathesis/pytest/__init__.py +5 -0
- schemathesis/pytest/control_flow.py +7 -0
- schemathesis/pytest/lazy.py +341 -0
- schemathesis/pytest/loaders.py +36 -0
- schemathesis/pytest/plugin.py +357 -0
- schemathesis/python/__init__.py +0 -0
- schemathesis/python/asgi.py +12 -0
- schemathesis/python/wsgi.py +12 -0
- schemathesis/schemas.py +682 -257
- schemathesis/specs/graphql/__init__.py +0 -1
- schemathesis/specs/graphql/nodes.py +26 -2
- schemathesis/specs/graphql/scalars.py +77 -12
- schemathesis/specs/graphql/schemas.py +367 -148
- schemathesis/specs/graphql/validation.py +33 -0
- schemathesis/specs/openapi/__init__.py +9 -1
- schemathesis/specs/openapi/_hypothesis.py +555 -318
- schemathesis/specs/openapi/adapter/__init__.py +10 -0
- schemathesis/specs/openapi/adapter/parameters.py +729 -0
- schemathesis/specs/openapi/adapter/protocol.py +59 -0
- schemathesis/specs/openapi/adapter/references.py +19 -0
- schemathesis/specs/openapi/adapter/responses.py +368 -0
- schemathesis/specs/openapi/adapter/security.py +144 -0
- schemathesis/specs/openapi/adapter/v2.py +30 -0
- schemathesis/specs/openapi/adapter/v3_0.py +30 -0
- schemathesis/specs/openapi/adapter/v3_1.py +30 -0
- schemathesis/specs/openapi/analysis.py +96 -0
- schemathesis/specs/openapi/checks.py +748 -82
- schemathesis/specs/openapi/converter.py +176 -37
- schemathesis/specs/openapi/definitions.py +599 -4
- schemathesis/specs/openapi/examples.py +581 -165
- schemathesis/specs/openapi/expressions/__init__.py +52 -5
- schemathesis/specs/openapi/expressions/extractors.py +25 -0
- schemathesis/specs/openapi/expressions/lexer.py +34 -31
- schemathesis/specs/openapi/expressions/nodes.py +97 -46
- schemathesis/specs/openapi/expressions/parser.py +35 -13
- schemathesis/specs/openapi/formats.py +122 -0
- schemathesis/specs/openapi/media_types.py +75 -0
- schemathesis/specs/openapi/negative/__init__.py +93 -73
- schemathesis/specs/openapi/negative/mutations.py +294 -103
- schemathesis/specs/openapi/negative/utils.py +0 -9
- schemathesis/specs/openapi/patterns.py +458 -0
- schemathesis/specs/openapi/references.py +60 -81
- schemathesis/specs/openapi/schemas.py +647 -666
- schemathesis/specs/openapi/serialization.py +53 -30
- schemathesis/specs/openapi/stateful/__init__.py +403 -68
- schemathesis/specs/openapi/stateful/control.py +87 -0
- schemathesis/specs/openapi/stateful/dependencies/__init__.py +232 -0
- schemathesis/specs/openapi/stateful/dependencies/inputs.py +428 -0
- schemathesis/specs/openapi/stateful/dependencies/models.py +341 -0
- schemathesis/specs/openapi/stateful/dependencies/naming.py +491 -0
- schemathesis/specs/openapi/stateful/dependencies/outputs.py +34 -0
- schemathesis/specs/openapi/stateful/dependencies/resources.py +339 -0
- schemathesis/specs/openapi/stateful/dependencies/schemas.py +447 -0
- schemathesis/specs/openapi/stateful/inference.py +254 -0
- schemathesis/specs/openapi/stateful/links.py +219 -78
- schemathesis/specs/openapi/types/__init__.py +3 -0
- schemathesis/specs/openapi/types/common.py +23 -0
- schemathesis/specs/openapi/types/v2.py +129 -0
- schemathesis/specs/openapi/types/v3.py +134 -0
- schemathesis/specs/openapi/utils.py +7 -6
- schemathesis/specs/openapi/warnings.py +75 -0
- schemathesis/transport/__init__.py +224 -0
- schemathesis/transport/asgi.py +26 -0
- schemathesis/transport/prepare.py +126 -0
- schemathesis/transport/requests.py +278 -0
- schemathesis/transport/serialization.py +329 -0
- schemathesis/transport/wsgi.py +175 -0
- schemathesis-4.4.2.dist-info/METADATA +213 -0
- schemathesis-4.4.2.dist-info/RECORD +192 -0
- {schemathesis-3.15.4.dist-info → schemathesis-4.4.2.dist-info}/WHEEL +1 -1
- schemathesis-4.4.2.dist-info/entry_points.txt +6 -0
- {schemathesis-3.15.4.dist-info → schemathesis-4.4.2.dist-info/licenses}/LICENSE +1 -1
- schemathesis/_compat.py +0 -57
- schemathesis/_hypothesis.py +0 -123
- schemathesis/auth.py +0 -214
- schemathesis/cli/callbacks.py +0 -240
- schemathesis/cli/cassettes.py +0 -351
- schemathesis/cli/context.py +0 -38
- schemathesis/cli/debug.py +0 -21
- schemathesis/cli/handlers.py +0 -11
- schemathesis/cli/junitxml.py +0 -41
- schemathesis/cli/options.py +0 -70
- schemathesis/cli/output/__init__.py +0 -1
- schemathesis/cli/output/default.py +0 -521
- schemathesis/cli/output/short.py +0 -40
- schemathesis/constants.py +0 -88
- schemathesis/exceptions.py +0 -257
- schemathesis/extra/_aiohttp.py +0 -27
- schemathesis/extra/_flask.py +0 -10
- schemathesis/extra/_server.py +0 -16
- schemathesis/extra/pytest_plugin.py +0 -251
- schemathesis/failures.py +0 -145
- schemathesis/fixups/__init__.py +0 -29
- schemathesis/fixups/fast_api.py +0 -30
- schemathesis/graphql.py +0 -5
- schemathesis/internal.py +0 -6
- schemathesis/lazy.py +0 -301
- schemathesis/models.py +0 -1113
- schemathesis/parameters.py +0 -91
- schemathesis/runner/__init__.py +0 -470
- schemathesis/runner/events.py +0 -242
- schemathesis/runner/impl/__init__.py +0 -3
- schemathesis/runner/impl/core.py +0 -791
- schemathesis/runner/impl/solo.py +0 -85
- schemathesis/runner/impl/threadpool.py +0 -367
- schemathesis/runner/serialization.py +0 -206
- schemathesis/serializers.py +0 -253
- schemathesis/service/__init__.py +0 -18
- schemathesis/service/auth.py +0 -10
- schemathesis/service/client.py +0 -62
- schemathesis/service/constants.py +0 -25
- schemathesis/service/events.py +0 -39
- schemathesis/service/handler.py +0 -46
- schemathesis/service/hosts.py +0 -74
- schemathesis/service/metadata.py +0 -42
- schemathesis/service/models.py +0 -21
- schemathesis/service/serialization.py +0 -184
- schemathesis/service/worker.py +0 -39
- schemathesis/specs/graphql/loaders.py +0 -215
- schemathesis/specs/openapi/constants.py +0 -7
- schemathesis/specs/openapi/expressions/context.py +0 -12
- schemathesis/specs/openapi/expressions/pointers.py +0 -29
- schemathesis/specs/openapi/filters.py +0 -44
- schemathesis/specs/openapi/links.py +0 -303
- schemathesis/specs/openapi/loaders.py +0 -453
- schemathesis/specs/openapi/parameters.py +0 -430
- schemathesis/specs/openapi/security.py +0 -129
- schemathesis/specs/openapi/validation.py +0 -24
- schemathesis/stateful.py +0 -358
- schemathesis/targets.py +0 -32
- schemathesis/types.py +0 -38
- schemathesis/utils.py +0 -475
- schemathesis-3.15.4.dist-info/METADATA +0 -202
- schemathesis-3.15.4.dist-info/RECORD +0 -99
- schemathesis-3.15.4.dist-info/entry_points.txt +0 -7
- /schemathesis/{extra → cli/ext}/__init__.py +0 -0
|
@@ -1,80 +1,221 @@
|
|
|
1
|
-
from
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
from
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
from
|
|
8
|
-
from
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from functools import lru_cache
|
|
5
|
+
from typing import Any, Callable
|
|
6
|
+
|
|
7
|
+
from schemathesis.core import NOT_SET, NotSet
|
|
8
|
+
from schemathesis.core.errors import InvalidTransition, OperationNotFound, TransitionValidationError, format_transition
|
|
9
|
+
from schemathesis.core.parameters import ParameterLocation
|
|
10
|
+
from schemathesis.core.result import Err, Ok, Result
|
|
11
|
+
from schemathesis.generation.stateful.state_machine import ExtractedParam, StepOutput, Transition
|
|
12
|
+
from schemathesis.schemas import APIOperation
|
|
13
|
+
from schemathesis.specs.openapi import expressions
|
|
14
|
+
|
|
15
|
+
SCHEMATHESIS_LINK_EXTENSION = "x-schemathesis"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class NormalizedParameter:
|
|
20
|
+
"""Processed link parameter with resolved container information."""
|
|
21
|
+
|
|
22
|
+
location: ParameterLocation | None
|
|
23
|
+
name: str
|
|
24
|
+
expression: str
|
|
25
|
+
container_name: str
|
|
26
|
+
is_required: bool
|
|
27
|
+
|
|
28
|
+
__slots__ = ("location", "name", "expression", "container_name", "is_required")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass(repr=False)
|
|
32
|
+
class OpenApiLink:
|
|
33
|
+
"""Represents an OpenAPI link between operations."""
|
|
34
|
+
|
|
35
|
+
name: str
|
|
36
|
+
status_code: str
|
|
37
|
+
source: APIOperation
|
|
38
|
+
target: APIOperation
|
|
39
|
+
parameters: list[NormalizedParameter]
|
|
40
|
+
body: dict[str, Any] | NotSet
|
|
41
|
+
merge_body: bool
|
|
42
|
+
is_inferred: bool
|
|
43
|
+
|
|
44
|
+
__slots__ = (
|
|
45
|
+
"name",
|
|
46
|
+
"status_code",
|
|
47
|
+
"source",
|
|
48
|
+
"target",
|
|
49
|
+
"parameters",
|
|
50
|
+
"body",
|
|
51
|
+
"merge_body",
|
|
52
|
+
"is_inferred",
|
|
53
|
+
"_cached_extract",
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
def __init__(self, name: str, status_code: str, definition: dict[str, Any], source: APIOperation):
|
|
57
|
+
from schemathesis.specs.openapi.schemas import BaseOpenAPISchema
|
|
58
|
+
|
|
59
|
+
self.name = name
|
|
60
|
+
self.status_code = status_code
|
|
61
|
+
self.source = source
|
|
62
|
+
assert isinstance(source.schema, BaseOpenAPISchema)
|
|
63
|
+
errors = []
|
|
64
|
+
|
|
65
|
+
get_operation: Callable[[str], APIOperation]
|
|
66
|
+
if "operationId" in definition:
|
|
67
|
+
operation_reference = definition["operationId"]
|
|
68
|
+
get_operation = source.schema.find_operation_by_id
|
|
69
|
+
else:
|
|
70
|
+
operation_reference = definition["operationRef"]
|
|
71
|
+
get_operation = source.schema.find_operation_by_reference
|
|
72
|
+
|
|
73
|
+
try:
|
|
74
|
+
self.target = get_operation(operation_reference)
|
|
75
|
+
target = self.target.label
|
|
76
|
+
except OperationNotFound:
|
|
77
|
+
target = operation_reference
|
|
78
|
+
errors.append(TransitionValidationError(f"Operation '{operation_reference}' not found"))
|
|
79
|
+
|
|
80
|
+
extension = definition.get(SCHEMATHESIS_LINK_EXTENSION)
|
|
81
|
+
self.parameters = self._normalize_parameters(definition.get("parameters", {}), errors)
|
|
82
|
+
self.body = definition.get("requestBody", NOT_SET)
|
|
83
|
+
self.merge_body = extension.get("merge_body", True) if extension else True
|
|
84
|
+
self.is_inferred = extension.get("is_inferred", False) if extension else False
|
|
85
|
+
|
|
86
|
+
if errors:
|
|
87
|
+
raise InvalidTransition(
|
|
88
|
+
name=self.name,
|
|
89
|
+
source=self.source.label,
|
|
90
|
+
target=target,
|
|
91
|
+
status_code=self.status_code,
|
|
92
|
+
errors=errors,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
self._cached_extract = lru_cache(8)(self._extract_impl)
|
|
96
|
+
|
|
97
|
+
@property
|
|
98
|
+
def full_name(self) -> str:
|
|
99
|
+
return format_transition(self.source.label, self.status_code, self.name, self.target.label)
|
|
100
|
+
|
|
101
|
+
def _normalize_parameters(
|
|
102
|
+
self, parameters: dict[str, str], errors: list[TransitionValidationError]
|
|
103
|
+
) -> list[NormalizedParameter]:
|
|
104
|
+
"""Process link parameters and resolve their container locations.
|
|
105
|
+
|
|
106
|
+
Handles both explicit locations (e.g., "path.id") and implicit ones resolved from target operation.
|
|
107
|
+
"""
|
|
108
|
+
result = []
|
|
109
|
+
for parameter, expression in parameters.items():
|
|
110
|
+
location: ParameterLocation | None
|
|
111
|
+
try:
|
|
112
|
+
# The parameter name is prefixed with its location. Example: `path.id`
|
|
113
|
+
_location, name = tuple(parameter.split("."))
|
|
114
|
+
location = ParameterLocation(_location)
|
|
115
|
+
except ValueError:
|
|
116
|
+
location = None
|
|
117
|
+
name = parameter
|
|
118
|
+
|
|
119
|
+
if isinstance(expression, str):
|
|
120
|
+
try:
|
|
121
|
+
parsed = expressions.parser.parse(expression)
|
|
122
|
+
# Find NonBodyRequest nodes that reference source parameters
|
|
123
|
+
for node in parsed:
|
|
124
|
+
if isinstance(node, expressions.nodes.NonBodyRequest):
|
|
125
|
+
# Check if parameter exists in source operation
|
|
126
|
+
if not any(
|
|
127
|
+
p.name == node.parameter and p.location == node.location
|
|
128
|
+
for p in self.source.iter_parameters()
|
|
129
|
+
):
|
|
130
|
+
errors.append(
|
|
131
|
+
TransitionValidationError(
|
|
132
|
+
f"Expression `{expression}` references non-existent {node.location} parameter "
|
|
133
|
+
f"`{node.parameter}` in `{self.source.label}`"
|
|
134
|
+
)
|
|
135
|
+
)
|
|
136
|
+
except Exception as exc:
|
|
137
|
+
errors.append(TransitionValidationError(str(exc)))
|
|
138
|
+
|
|
139
|
+
is_required = False
|
|
140
|
+
if hasattr(self, "target"):
|
|
141
|
+
try:
|
|
142
|
+
container_name = self._get_parameter_container(location, name)
|
|
143
|
+
except TransitionValidationError as exc:
|
|
144
|
+
errors.append(exc)
|
|
145
|
+
continue
|
|
146
|
+
|
|
147
|
+
for param in self.target.iter_parameters():
|
|
148
|
+
if param.name == name:
|
|
149
|
+
is_required = param.is_required
|
|
150
|
+
break
|
|
151
|
+
else:
|
|
152
|
+
continue
|
|
153
|
+
result.append(NormalizedParameter(location, name, expression, container_name, is_required=is_required))
|
|
154
|
+
return result
|
|
155
|
+
|
|
156
|
+
def _get_parameter_container(self, location: ParameterLocation | None, name: str) -> str:
|
|
157
|
+
"""Resolve parameter container either from explicit location or by looking up in target operation."""
|
|
158
|
+
if location:
|
|
159
|
+
return location.container_name
|
|
160
|
+
|
|
161
|
+
for param in self.target.iter_parameters():
|
|
162
|
+
if param.name == name:
|
|
163
|
+
return param.location.container_name
|
|
164
|
+
raise TransitionValidationError(f"Parameter `{name}` is not defined in API operation `{self.target.label}`")
|
|
165
|
+
|
|
166
|
+
def extract(self, output: StepOutput) -> Transition:
|
|
167
|
+
return self._cached_extract(StepOutputWrapper(output))
|
|
168
|
+
|
|
169
|
+
def _extract_impl(self, wrapper: StepOutputWrapper) -> Transition:
|
|
170
|
+
output = wrapper.output
|
|
171
|
+
return Transition(
|
|
172
|
+
id=self.full_name,
|
|
173
|
+
parent_id=output.case.id,
|
|
174
|
+
is_inferred=self.is_inferred,
|
|
175
|
+
parameters=self.extract_parameters(output),
|
|
176
|
+
request_body=self.extract_body(output),
|
|
27
177
|
)
|
|
28
|
-
connections[target_operation.verbose_name].append(_convert_strategy(strategy, link))
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
def _convert_strategy(
|
|
32
|
-
strategy: st.SearchStrategy[StepResult], link: OpenAPILink
|
|
33
|
-
) -> st.SearchStrategy[Tuple[StepResult, OpenAPILink]]:
|
|
34
|
-
# This function is required to capture values properly (it won't work properly when lambda is defined in a loop)
|
|
35
|
-
return strategy.map(lambda out: (out, link))
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
def make_response_filter(status_code: str, all_status_codes: List[str]) -> FilterFunction:
|
|
39
|
-
"""Create a filter for stored responses.
|
|
40
|
-
|
|
41
|
-
This filter will decide whether some response is suitable to use as a source for requesting some API operation.
|
|
42
|
-
"""
|
|
43
|
-
if status_code == "default":
|
|
44
|
-
return default_status_code(all_status_codes)
|
|
45
|
-
return match_status_code(status_code)
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
def match_status_code(status_code: str) -> FilterFunction:
|
|
49
|
-
"""Create a filter function that matches all responses with the given status code.
|
|
50
|
-
|
|
51
|
-
Note that the status code can contain "X", which means any digit.
|
|
52
|
-
For example, 50X will match all status codes from 500 to 509.
|
|
53
|
-
"""
|
|
54
|
-
status_codes = set(expand_status_code(status_code))
|
|
55
|
-
|
|
56
|
-
def compare(result: StepResult) -> bool:
|
|
57
|
-
return result.response.status_code in status_codes
|
|
58
|
-
|
|
59
|
-
# This name is displayed in the resulting strategy representation. For example, if you run your tests with
|
|
60
|
-
# `--hypothesis-show-statistics`, then you can see `Bundle(name='GET /users/{user_id}').filter(match_200_response)`
|
|
61
|
-
# which gives you information about the particularly used filter.
|
|
62
|
-
compare.__name__ = f"match_{status_code}_response"
|
|
63
|
-
|
|
64
|
-
return compare
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
def default_status_code(status_codes: List[str]) -> FilterFunction:
|
|
68
|
-
"""Create a filter that matches all "default" responses.
|
|
69
|
-
|
|
70
|
-
In Open API, the "default" response is the one that is used if no other options were matched.
|
|
71
|
-
Therefore we need to match only responses that were not matched by other listed status codes.
|
|
72
|
-
"""
|
|
73
|
-
expanded_status_codes = {
|
|
74
|
-
status_code for value in status_codes if value != "default" for status_code in expand_status_code(value)
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
def match_default_response(result: StepResult) -> bool:
|
|
78
|
-
return result.response.status_code not in expanded_status_codes
|
|
79
178
|
|
|
80
|
-
|
|
179
|
+
def extract_parameters(self, output: StepOutput) -> dict[str, dict[str, ExtractedParam]]:
|
|
180
|
+
"""Extract parameters using runtime expressions.
|
|
181
|
+
|
|
182
|
+
Returns a two-level dictionary: container -> parameter name -> extracted value
|
|
183
|
+
"""
|
|
184
|
+
extracted: dict[str, dict[str, ExtractedParam]] = {}
|
|
185
|
+
for parameter in self.parameters:
|
|
186
|
+
container = extracted.setdefault(parameter.container_name, {})
|
|
187
|
+
value: Result[Any, Exception]
|
|
188
|
+
try:
|
|
189
|
+
value = Ok(expressions.evaluate(parameter.expression, output))
|
|
190
|
+
except Exception as exc:
|
|
191
|
+
value = Err(exc)
|
|
192
|
+
container[parameter.name] = ExtractedParam(
|
|
193
|
+
definition=parameter.expression, value=value, is_required=parameter.is_required
|
|
194
|
+
)
|
|
195
|
+
return extracted
|
|
196
|
+
|
|
197
|
+
def extract_body(self, output: StepOutput) -> ExtractedParam | None:
|
|
198
|
+
if not isinstance(self.body, NotSet):
|
|
199
|
+
value: Result[Any, Exception]
|
|
200
|
+
try:
|
|
201
|
+
value = Ok(expressions.evaluate(self.body, output, evaluate_nested=True))
|
|
202
|
+
except Exception as exc:
|
|
203
|
+
value = Err(exc)
|
|
204
|
+
return ExtractedParam(definition=self.body, value=value, is_required=True)
|
|
205
|
+
return None
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
@dataclass
|
|
209
|
+
class StepOutputWrapper:
|
|
210
|
+
"""Wrapper for StepOutput that uses only case_id for hash-based caching."""
|
|
211
|
+
|
|
212
|
+
output: StepOutput
|
|
213
|
+
|
|
214
|
+
__slots__ = ("output",)
|
|
215
|
+
|
|
216
|
+
def __hash__(self) -> int:
|
|
217
|
+
return hash(self.output.case.id)
|
|
218
|
+
|
|
219
|
+
def __eq__(self, other: object) -> bool:
|
|
220
|
+
assert isinstance(other, StepOutputWrapper)
|
|
221
|
+
return self.output.case.id == other.output.case.id
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Common type definitions shared across OpenAPI versions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Mapping, Union
|
|
6
|
+
|
|
7
|
+
from typing_extensions import NotRequired, TypeAlias, TypedDict
|
|
8
|
+
|
|
9
|
+
Reference = TypedDict("Reference", {"$ref": str})
|
|
10
|
+
"""JSON Reference object with $ref key."""
|
|
11
|
+
|
|
12
|
+
SchemaObject = TypedDict("SchemaObject", {"$ref": str})
|
|
13
|
+
"""Schema object that may be a reference."""
|
|
14
|
+
|
|
15
|
+
_SecurityTypeKey = TypedDict("_SecurityTypeKey", {"x-original-security-type": NotRequired[str]})
|
|
16
|
+
"""Type for x-original-security-type extension added by Schemathesis."""
|
|
17
|
+
|
|
18
|
+
# Type aliases for commonly used patterns
|
|
19
|
+
Schema: TypeAlias = Union[SchemaObject, bool]
|
|
20
|
+
"""JSON Schema can be an object or boolean."""
|
|
21
|
+
|
|
22
|
+
SchemaOrRef: TypeAlias = Union[Mapping[str, Any], Reference]
|
|
23
|
+
"""Schema definition that may be a reference or inline object."""
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""Swagger 2.0 type definitions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Literal, Mapping, Union
|
|
6
|
+
|
|
7
|
+
from typing_extensions import NotRequired, TypeAlias, TypedDict
|
|
8
|
+
|
|
9
|
+
from schemathesis.specs.openapi.types.common import Reference, SchemaOrRef, _SecurityTypeKey
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class BodyParameter(TypedDict):
|
|
13
|
+
"""Swagger 2.0 body parameter."""
|
|
14
|
+
|
|
15
|
+
name: str
|
|
16
|
+
description: NotRequired[str]
|
|
17
|
+
required: NotRequired[bool]
|
|
18
|
+
schema: SchemaOrRef
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
_BodyParameterIn = TypedDict("_BodyParameterIn", {"in": Literal["body"]})
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class BodyParameterWithIn(BodyParameter, _BodyParameterIn):
|
|
25
|
+
"""Body parameter with 'in' field."""
|
|
26
|
+
|
|
27
|
+
pass
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class NonBodyParameter(TypedDict):
|
|
31
|
+
"""Swagger 2.0 non-body parameter (path/query/header/formData)."""
|
|
32
|
+
|
|
33
|
+
name: str
|
|
34
|
+
description: NotRequired[str]
|
|
35
|
+
required: NotRequired[bool]
|
|
36
|
+
type: NotRequired[Literal["string", "number", "integer", "boolean", "array", "file"]]
|
|
37
|
+
format: NotRequired[str]
|
|
38
|
+
items: NotRequired[SchemaOrRef]
|
|
39
|
+
collectionFormat: NotRequired[Literal["csv", "ssv", "tsv", "pipes"]]
|
|
40
|
+
default: NotRequired[Any]
|
|
41
|
+
maximum: NotRequired[float]
|
|
42
|
+
exclusiveMaximum: NotRequired[bool]
|
|
43
|
+
minimum: NotRequired[float]
|
|
44
|
+
exclusiveMinimum: NotRequired[bool]
|
|
45
|
+
maxLength: NotRequired[int]
|
|
46
|
+
minLength: NotRequired[int]
|
|
47
|
+
pattern: NotRequired[str]
|
|
48
|
+
maxItems: NotRequired[int]
|
|
49
|
+
minItems: NotRequired[int]
|
|
50
|
+
uniqueItems: NotRequired[bool]
|
|
51
|
+
enum: NotRequired[list[Any]]
|
|
52
|
+
multipleOf: NotRequired[float]
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
_NonBodyParameterIn = TypedDict("_NonBodyParameterIn", {"in": Literal["path", "query", "header", "formData"]})
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class NonBodyParameterWithIn(NonBodyParameter, _NonBodyParameterIn):
|
|
59
|
+
"""Non-body parameter with 'in' field."""
|
|
60
|
+
|
|
61
|
+
pass
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
Parameter: TypeAlias = Union[BodyParameterWithIn, NonBodyParameterWithIn, Reference]
|
|
65
|
+
"""Swagger 2.0 parameter (body, non-body, or reference)."""
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class Header(TypedDict):
|
|
69
|
+
"""Swagger 2.0 response header."""
|
|
70
|
+
|
|
71
|
+
type: Literal["string", "number", "integer", "boolean", "array"]
|
|
72
|
+
description: NotRequired[str]
|
|
73
|
+
format: NotRequired[str]
|
|
74
|
+
items: NotRequired[SchemaOrRef]
|
|
75
|
+
collectionFormat: NotRequired[Literal["csv", "ssv", "tsv", "pipes"]]
|
|
76
|
+
default: NotRequired[Any]
|
|
77
|
+
maximum: NotRequired[float]
|
|
78
|
+
exclusiveMaximum: NotRequired[bool]
|
|
79
|
+
minimum: NotRequired[float]
|
|
80
|
+
exclusiveMinimum: NotRequired[bool]
|
|
81
|
+
maxLength: NotRequired[int]
|
|
82
|
+
minLength: NotRequired[int]
|
|
83
|
+
pattern: NotRequired[str]
|
|
84
|
+
maxItems: NotRequired[int]
|
|
85
|
+
minItems: NotRequired[int]
|
|
86
|
+
uniqueItems: NotRequired[bool]
|
|
87
|
+
enum: NotRequired[list[Any]]
|
|
88
|
+
multipleOf: NotRequired[float]
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
HeaderOrRef: TypeAlias = Union[Header, Reference]
|
|
92
|
+
"""Header definition or reference."""
|
|
93
|
+
|
|
94
|
+
Headers: TypeAlias = Mapping[str, HeaderOrRef]
|
|
95
|
+
"""Mapping from header name to header definition."""
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class Response(TypedDict):
|
|
99
|
+
"""Swagger 2.0 response object."""
|
|
100
|
+
|
|
101
|
+
description: str
|
|
102
|
+
schema: NotRequired[SchemaOrRef]
|
|
103
|
+
headers: NotRequired[dict[str, HeaderOrRef]]
|
|
104
|
+
examples: NotRequired[dict[str, Any]]
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
ResponseOrRef: TypeAlias = Union[Response, Reference]
|
|
108
|
+
"""Response definition or reference."""
|
|
109
|
+
|
|
110
|
+
Responses: TypeAlias = Mapping[str, ResponseOrRef]
|
|
111
|
+
"""Mapping from status code to response definition."""
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class Operation(TypedDict):
|
|
115
|
+
responses: Responses
|
|
116
|
+
parameters: NotRequired[list[Parameter]]
|
|
117
|
+
consumes: NotRequired[list[str]]
|
|
118
|
+
produces: NotRequired[list[str]]
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
# Security parameter types
|
|
122
|
+
class SecurityParameter(NonBodyParameter, _SecurityTypeKey):
|
|
123
|
+
"""Swagger 2.0 synthetic security parameter.
|
|
124
|
+
|
|
125
|
+
Created from security definitions (apiKey or basic auth).
|
|
126
|
+
Follows the same structure as NonBodyParameter since v2 has inline types.
|
|
127
|
+
"""
|
|
128
|
+
|
|
129
|
+
pass
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"""OpenAPI 3.0 and 3.1 type definitions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Literal, Mapping, Union
|
|
6
|
+
|
|
7
|
+
from typing_extensions import NotRequired, TypeAlias, TypedDict
|
|
8
|
+
|
|
9
|
+
from schemathesis.specs.openapi.types.common import Reference, SchemaOrRef, _SecurityTypeKey
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Example(TypedDict):
|
|
13
|
+
value: NotRequired[Any]
|
|
14
|
+
externalValue: NotRequired[str]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class MediaType(TypedDict):
|
|
18
|
+
schema: SchemaOrRef
|
|
19
|
+
example: NotRequired[Any]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Link(TypedDict):
|
|
23
|
+
operationId: NotRequired[str]
|
|
24
|
+
operationRef: NotRequired[str]
|
|
25
|
+
parameters: NotRequired[dict[str, Any]]
|
|
26
|
+
requestBody: NotRequired[Any]
|
|
27
|
+
server: NotRequired[Any]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class Header(TypedDict):
|
|
31
|
+
required: NotRequired[bool]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class Response(TypedDict):
|
|
35
|
+
headers: NotRequired[dict[str, HeaderOrRef]]
|
|
36
|
+
content: NotRequired[dict[str, MediaType]]
|
|
37
|
+
links: NotRequired[dict[str, LinkOrRef]]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class RequestBody(TypedDict):
|
|
41
|
+
content: dict[str, MediaType]
|
|
42
|
+
required: NotRequired[bool]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
_ResponsesBase = Mapping[str, Union[Response, Reference]]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class Responses(_ResponsesBase):
|
|
49
|
+
pass
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
_HeadersBase = Mapping[str, Union[Header, Reference]]
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class Headers(_HeadersBase):
|
|
56
|
+
pass
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
ExampleOrRef: TypeAlias = Union[Example, Reference]
|
|
60
|
+
"""Example definition or reference."""
|
|
61
|
+
|
|
62
|
+
HeaderOrRef: TypeAlias = Union[Header, Reference]
|
|
63
|
+
"""Header definition or reference."""
|
|
64
|
+
|
|
65
|
+
LinkOrRef: TypeAlias = Union[Link, Reference]
|
|
66
|
+
"""Link definition or reference."""
|
|
67
|
+
|
|
68
|
+
ResponseOrRef: TypeAlias = Union[Response, Reference]
|
|
69
|
+
"""Response definition or reference."""
|
|
70
|
+
|
|
71
|
+
RequestBodyOrRef: TypeAlias = Union[RequestBody, Reference]
|
|
72
|
+
"""Request body definition or reference."""
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class ParameterWithSchema(TypedDict):
|
|
76
|
+
"""OpenAPI 3.0/3.1 parameter with schema."""
|
|
77
|
+
|
|
78
|
+
name: str
|
|
79
|
+
description: NotRequired[str]
|
|
80
|
+
required: NotRequired[bool]
|
|
81
|
+
deprecated: NotRequired[bool]
|
|
82
|
+
allowEmptyValue: NotRequired[bool]
|
|
83
|
+
schema: SchemaOrRef
|
|
84
|
+
style: NotRequired[str]
|
|
85
|
+
explode: NotRequired[bool]
|
|
86
|
+
allowReserved: NotRequired[bool]
|
|
87
|
+
example: NotRequired[Any]
|
|
88
|
+
examples: NotRequired[dict[str, ExampleOrRef]]
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
_ParameterIn = TypedDict("_ParameterIn", {"in": Literal["path", "query", "header", "cookie"]})
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class ParameterWithSchemaAndIn(ParameterWithSchema, _ParameterIn):
|
|
95
|
+
"""Parameter with schema and 'in' field."""
|
|
96
|
+
|
|
97
|
+
pass
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class ParameterWithContent(TypedDict):
|
|
101
|
+
"""OpenAPI 3.0/3.1 parameter with content."""
|
|
102
|
+
|
|
103
|
+
name: str
|
|
104
|
+
description: NotRequired[str]
|
|
105
|
+
required: NotRequired[bool]
|
|
106
|
+
deprecated: NotRequired[bool]
|
|
107
|
+
content: dict[str, MediaType]
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class ParameterWithContentAndIn(ParameterWithContent, _ParameterIn):
|
|
111
|
+
"""Parameter with content and 'in' field."""
|
|
112
|
+
|
|
113
|
+
pass
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
Parameter: TypeAlias = Union[ParameterWithSchemaAndIn, ParameterWithContentAndIn, Reference]
|
|
117
|
+
"""OpenAPI 3.x parameter (with schema, with content, or reference)."""
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class Operation(TypedDict):
|
|
121
|
+
responses: Responses
|
|
122
|
+
requestBody: NotRequired[RequestBodyOrRef]
|
|
123
|
+
parameters: NotRequired[list[Parameter]]
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
# Security parameter types
|
|
127
|
+
class SecurityParameter(ParameterWithSchema, _SecurityTypeKey):
|
|
128
|
+
"""OpenAPI 3.x synthetic security parameter.
|
|
129
|
+
|
|
130
|
+
Created from security schemes (apiKey or http auth).
|
|
131
|
+
Follows the same structure as ParameterWithSchema since v3 uses nested schema.
|
|
132
|
+
"""
|
|
133
|
+
|
|
134
|
+
pass
|
|
@@ -1,14 +1,15 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
import string
|
|
2
|
-
from itertools import product
|
|
3
|
-
from typing import Generator
|
|
4
|
+
from itertools import chain, product
|
|
5
|
+
from typing import Generator
|
|
4
6
|
|
|
5
7
|
|
|
6
|
-
def expand_status_code(status_code:
|
|
8
|
+
def expand_status_code(status_code: str | int) -> Generator[int, None, None]:
|
|
7
9
|
chars = [list(string.digits) if digit == "X" else [digit] for digit in str(status_code).upper()]
|
|
8
10
|
for expanded in product(*chars):
|
|
9
11
|
yield int("".join(expanded))
|
|
10
12
|
|
|
11
13
|
|
|
12
|
-
def
|
|
13
|
-
|
|
14
|
-
return location in ("header", "cookie")
|
|
14
|
+
def expand_status_codes(status_codes: list[str]) -> set[int]:
|
|
15
|
+
return set(chain.from_iterable(expand_status_code(code) for code in status_codes))
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""OpenAPI-specific static schema warnings."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
from schemathesis.config import SchemathesisWarning
|
|
9
|
+
from schemathesis.core import deserialization
|
|
10
|
+
from schemathesis.core.errors import MalformedMediaType
|
|
11
|
+
from schemathesis.core.jsonschema.types import get_type
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from schemathesis.schemas import APIOperation
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class MissingDeserializerWarning:
|
|
19
|
+
"""Warning for responses with structured schemas but no registered deserializer."""
|
|
20
|
+
|
|
21
|
+
operation_label: str
|
|
22
|
+
"""Label of the operation (e.g., 'GET /users')."""
|
|
23
|
+
|
|
24
|
+
status_code: str
|
|
25
|
+
"""HTTP status code for the response."""
|
|
26
|
+
|
|
27
|
+
content_type: str
|
|
28
|
+
"""Media type that has no deserializer."""
|
|
29
|
+
|
|
30
|
+
__slots__ = ("operation_label", "status_code", "content_type")
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def kind(self) -> SchemathesisWarning:
|
|
34
|
+
"""The warning kind for configuration matching."""
|
|
35
|
+
return SchemathesisWarning.MISSING_DESERIALIZER
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def message(self) -> str:
|
|
39
|
+
"""Human-readable description of the warning."""
|
|
40
|
+
return f"Cannot validate response {self.status_code}: no deserializer registered for {self.content_type}"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def detect_missing_deserializers(operation: APIOperation) -> list[MissingDeserializerWarning]:
|
|
44
|
+
"""Detect responses with structured schemas but no registered deserializer."""
|
|
45
|
+
warnings: list[MissingDeserializerWarning] = []
|
|
46
|
+
|
|
47
|
+
for status_code, response in operation.responses.items():
|
|
48
|
+
raw_schema = getattr(response, "get_raw_schema", lambda: None)()
|
|
49
|
+
if raw_schema is None:
|
|
50
|
+
continue
|
|
51
|
+
|
|
52
|
+
schema_types = get_type(raw_schema)
|
|
53
|
+
is_structured = any(t in ("object", "array") for t in schema_types)
|
|
54
|
+
|
|
55
|
+
if not is_structured:
|
|
56
|
+
continue
|
|
57
|
+
|
|
58
|
+
content_types = response.definition.get("content", {}).keys() if response.definition else []
|
|
59
|
+
|
|
60
|
+
for content_type in content_types:
|
|
61
|
+
try:
|
|
62
|
+
has_deserializer = deserialization.has_deserializer(content_type)
|
|
63
|
+
except MalformedMediaType:
|
|
64
|
+
continue
|
|
65
|
+
|
|
66
|
+
if not has_deserializer:
|
|
67
|
+
warnings.append(
|
|
68
|
+
MissingDeserializerWarning(
|
|
69
|
+
operation_label=operation.label,
|
|
70
|
+
status_code=status_code,
|
|
71
|
+
content_type=content_type,
|
|
72
|
+
)
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
return warnings
|