schemathesis 3.39.15__py3-none-any.whl → 4.0.0__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 +41 -79
- schemathesis/auths.py +111 -122
- schemathesis/checks.py +169 -60
- schemathesis/cli/__init__.py +15 -2117
- schemathesis/cli/commands/__init__.py +85 -0
- schemathesis/cli/commands/data.py +10 -0
- schemathesis/cli/commands/run/__init__.py +590 -0
- schemathesis/cli/commands/run/context.py +204 -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 +18 -0
- schemathesis/cli/commands/run/handlers/cassettes.py +474 -0
- schemathesis/cli/commands/run/handlers/junitxml.py +55 -0
- schemathesis/cli/commands/run/handlers/output.py +1628 -0
- schemathesis/cli/commands/run/loaders.py +114 -0
- schemathesis/cli/commands/run/validation.py +246 -0
- schemathesis/cli/constants.py +5 -58
- schemathesis/cli/core.py +19 -0
- schemathesis/cli/ext/fs.py +16 -0
- schemathesis/cli/ext/groups.py +84 -0
- schemathesis/cli/{options.py → ext/options.py} +36 -34
- schemathesis/config/__init__.py +189 -0
- schemathesis/config/_auth.py +51 -0
- schemathesis/config/_checks.py +268 -0
- schemathesis/config/_diff_base.py +99 -0
- schemathesis/config/_env.py +21 -0
- schemathesis/config/_error.py +156 -0
- schemathesis/config/_generation.py +149 -0
- schemathesis/config/_health_check.py +24 -0
- schemathesis/config/_operations.py +327 -0
- schemathesis/config/_output.py +171 -0
- schemathesis/config/_parameters.py +19 -0
- schemathesis/config/_phases.py +187 -0
- schemathesis/config/_projects.py +527 -0
- schemathesis/config/_rate_limit.py +17 -0
- schemathesis/config/_report.py +120 -0
- schemathesis/config/_validator.py +9 -0
- schemathesis/config/_warnings.py +25 -0
- schemathesis/config/schema.json +885 -0
- schemathesis/core/__init__.py +67 -0
- schemathesis/core/compat.py +32 -0
- schemathesis/core/control.py +2 -0
- schemathesis/core/curl.py +58 -0
- schemathesis/core/deserialization.py +65 -0
- schemathesis/core/errors.py +459 -0
- schemathesis/core/failures.py +315 -0
- schemathesis/core/fs.py +19 -0
- schemathesis/core/hooks.py +20 -0
- schemathesis/core/loaders.py +104 -0
- schemathesis/core/marks.py +66 -0
- schemathesis/{transports/content_types.py → core/media_types.py} +14 -12
- schemathesis/core/output/__init__.py +46 -0
- schemathesis/core/output/sanitization.py +54 -0
- schemathesis/{throttling.py → core/rate_limit.py} +16 -17
- schemathesis/core/registries.py +31 -0
- schemathesis/core/transforms.py +113 -0
- schemathesis/core/transport.py +223 -0
- schemathesis/core/validation.py +54 -0
- schemathesis/core/version.py +7 -0
- schemathesis/engine/__init__.py +28 -0
- schemathesis/engine/context.py +118 -0
- schemathesis/engine/control.py +36 -0
- schemathesis/engine/core.py +169 -0
- schemathesis/engine/errors.py +464 -0
- schemathesis/engine/events.py +258 -0
- schemathesis/engine/phases/__init__.py +88 -0
- schemathesis/{runner → engine/phases}/probes.py +52 -68
- schemathesis/engine/phases/stateful/__init__.py +68 -0
- schemathesis/engine/phases/stateful/_executor.py +356 -0
- schemathesis/engine/phases/stateful/context.py +85 -0
- schemathesis/engine/phases/unit/__init__.py +212 -0
- schemathesis/engine/phases/unit/_executor.py +416 -0
- schemathesis/engine/phases/unit/_pool.py +82 -0
- schemathesis/engine/recorder.py +247 -0
- schemathesis/errors.py +43 -0
- schemathesis/filters.py +17 -98
- schemathesis/generation/__init__.py +5 -33
- schemathesis/generation/case.py +317 -0
- schemathesis/generation/coverage.py +282 -175
- schemathesis/generation/hypothesis/__init__.py +36 -0
- schemathesis/generation/hypothesis/builder.py +800 -0
- schemathesis/generation/{_hypothesis.py → hypothesis/examples.py} +2 -11
- 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/metrics.py +93 -0
- schemathesis/generation/modes.py +20 -0
- schemathesis/generation/overrides.py +116 -0
- schemathesis/generation/stateful/__init__.py +37 -0
- schemathesis/generation/stateful/state_machine.py +278 -0
- schemathesis/graphql/__init__.py +15 -0
- schemathesis/graphql/checks.py +109 -0
- schemathesis/graphql/loaders.py +284 -0
- schemathesis/hooks.py +80 -101
- schemathesis/openapi/__init__.py +13 -0
- schemathesis/openapi/checks.py +455 -0
- schemathesis/openapi/generation/__init__.py +0 -0
- schemathesis/openapi/generation/filters.py +72 -0
- schemathesis/openapi/loaders.py +313 -0
- schemathesis/pytest/__init__.py +5 -0
- schemathesis/pytest/control_flow.py +7 -0
- schemathesis/pytest/lazy.py +281 -0
- schemathesis/pytest/loaders.py +36 -0
- schemathesis/{extra/pytest_plugin.py → pytest/plugin.py} +128 -108
- schemathesis/python/__init__.py +0 -0
- schemathesis/python/asgi.py +12 -0
- schemathesis/python/wsgi.py +12 -0
- schemathesis/schemas.py +537 -273
- schemathesis/specs/graphql/__init__.py +0 -1
- schemathesis/specs/graphql/_cache.py +1 -2
- schemathesis/specs/graphql/scalars.py +42 -6
- schemathesis/specs/graphql/schemas.py +141 -137
- schemathesis/specs/graphql/validation.py +11 -17
- schemathesis/specs/openapi/__init__.py +6 -1
- schemathesis/specs/openapi/_cache.py +1 -2
- schemathesis/specs/openapi/_hypothesis.py +142 -156
- schemathesis/specs/openapi/checks.py +368 -257
- schemathesis/specs/openapi/converter.py +4 -4
- schemathesis/specs/openapi/definitions.py +1 -1
- schemathesis/specs/openapi/examples.py +23 -21
- schemathesis/specs/openapi/expressions/__init__.py +31 -19
- schemathesis/specs/openapi/expressions/extractors.py +1 -4
- schemathesis/specs/openapi/expressions/lexer.py +1 -1
- schemathesis/specs/openapi/expressions/nodes.py +36 -41
- schemathesis/specs/openapi/expressions/parser.py +1 -1
- schemathesis/specs/openapi/formats.py +35 -7
- schemathesis/specs/openapi/media_types.py +53 -12
- schemathesis/specs/openapi/negative/__init__.py +7 -4
- schemathesis/specs/openapi/negative/mutations.py +6 -5
- schemathesis/specs/openapi/parameters.py +7 -10
- schemathesis/specs/openapi/patterns.py +94 -31
- schemathesis/specs/openapi/references.py +12 -53
- schemathesis/specs/openapi/schemas.py +238 -308
- schemathesis/specs/openapi/security.py +1 -1
- schemathesis/specs/openapi/serialization.py +12 -6
- schemathesis/specs/openapi/stateful/__init__.py +268 -133
- schemathesis/specs/openapi/stateful/control.py +87 -0
- schemathesis/specs/openapi/stateful/links.py +209 -0
- schemathesis/transport/__init__.py +142 -0
- schemathesis/transport/asgi.py +26 -0
- schemathesis/transport/prepare.py +124 -0
- schemathesis/transport/requests.py +244 -0
- schemathesis/{_xml.py → transport/serialization.py} +69 -11
- schemathesis/transport/wsgi.py +171 -0
- schemathesis-4.0.0.dist-info/METADATA +204 -0
- schemathesis-4.0.0.dist-info/RECORD +164 -0
- {schemathesis-3.39.15.dist-info → schemathesis-4.0.0.dist-info}/entry_points.txt +1 -1
- {schemathesis-3.39.15.dist-info → schemathesis-4.0.0.dist-info}/licenses/LICENSE +1 -1
- schemathesis/_compat.py +0 -74
- schemathesis/_dependency_versions.py +0 -19
- schemathesis/_hypothesis.py +0 -712
- schemathesis/_override.py +0 -50
- schemathesis/_patches.py +0 -21
- schemathesis/_rate_limiter.py +0 -7
- schemathesis/cli/callbacks.py +0 -466
- schemathesis/cli/cassettes.py +0 -561
- schemathesis/cli/context.py +0 -75
- schemathesis/cli/debug.py +0 -27
- schemathesis/cli/handlers.py +0 -19
- schemathesis/cli/junitxml.py +0 -124
- schemathesis/cli/output/__init__.py +0 -1
- schemathesis/cli/output/default.py +0 -920
- schemathesis/cli/output/short.py +0 -59
- schemathesis/cli/reporting.py +0 -79
- schemathesis/cli/sanitization.py +0 -26
- schemathesis/code_samples.py +0 -151
- schemathesis/constants.py +0 -54
- schemathesis/contrib/__init__.py +0 -11
- schemathesis/contrib/openapi/__init__.py +0 -11
- schemathesis/contrib/openapi/fill_missing_examples.py +0 -24
- schemathesis/contrib/openapi/formats/__init__.py +0 -9
- schemathesis/contrib/openapi/formats/uuid.py +0 -16
- schemathesis/contrib/unique_data.py +0 -41
- schemathesis/exceptions.py +0 -571
- schemathesis/experimental/__init__.py +0 -109
- schemathesis/extra/_aiohttp.py +0 -28
- schemathesis/extra/_flask.py +0 -13
- schemathesis/extra/_server.py +0 -18
- schemathesis/failures.py +0 -284
- schemathesis/fixups/__init__.py +0 -37
- schemathesis/fixups/fast_api.py +0 -41
- schemathesis/fixups/utf8_bom.py +0 -28
- schemathesis/generation/_methods.py +0 -44
- schemathesis/graphql.py +0 -3
- schemathesis/internal/__init__.py +0 -7
- schemathesis/internal/checks.py +0 -86
- schemathesis/internal/copy.py +0 -32
- schemathesis/internal/datetime.py +0 -5
- schemathesis/internal/deprecation.py +0 -37
- schemathesis/internal/diff.py +0 -15
- schemathesis/internal/extensions.py +0 -27
- schemathesis/internal/jsonschema.py +0 -36
- schemathesis/internal/output.py +0 -68
- schemathesis/internal/transformation.py +0 -26
- schemathesis/internal/validation.py +0 -34
- schemathesis/lazy.py +0 -474
- schemathesis/loaders.py +0 -122
- schemathesis/models.py +0 -1341
- schemathesis/parameters.py +0 -90
- schemathesis/runner/__init__.py +0 -605
- schemathesis/runner/events.py +0 -389
- schemathesis/runner/impl/__init__.py +0 -3
- schemathesis/runner/impl/context.py +0 -88
- schemathesis/runner/impl/core.py +0 -1280
- schemathesis/runner/impl/solo.py +0 -80
- schemathesis/runner/impl/threadpool.py +0 -391
- schemathesis/runner/serialization.py +0 -544
- schemathesis/sanitization.py +0 -252
- schemathesis/serializers.py +0 -328
- schemathesis/service/__init__.py +0 -18
- schemathesis/service/auth.py +0 -11
- schemathesis/service/ci.py +0 -202
- schemathesis/service/client.py +0 -133
- schemathesis/service/constants.py +0 -38
- schemathesis/service/events.py +0 -61
- schemathesis/service/extensions.py +0 -224
- schemathesis/service/hosts.py +0 -111
- schemathesis/service/metadata.py +0 -71
- schemathesis/service/models.py +0 -258
- schemathesis/service/report.py +0 -255
- schemathesis/service/serialization.py +0 -173
- schemathesis/service/usage.py +0 -66
- schemathesis/specs/graphql/loaders.py +0 -364
- schemathesis/specs/openapi/expressions/context.py +0 -16
- schemathesis/specs/openapi/links.py +0 -389
- schemathesis/specs/openapi/loaders.py +0 -707
- schemathesis/specs/openapi/stateful/statistic.py +0 -198
- schemathesis/specs/openapi/stateful/types.py +0 -14
- schemathesis/specs/openapi/validation.py +0 -26
- schemathesis/stateful/__init__.py +0 -147
- schemathesis/stateful/config.py +0 -97
- schemathesis/stateful/context.py +0 -135
- schemathesis/stateful/events.py +0 -274
- schemathesis/stateful/runner.py +0 -309
- schemathesis/stateful/sink.py +0 -68
- schemathesis/stateful/state_machine.py +0 -328
- schemathesis/stateful/statistic.py +0 -22
- schemathesis/stateful/validation.py +0 -100
- schemathesis/targets.py +0 -77
- schemathesis/transports/__init__.py +0 -369
- schemathesis/transports/asgi.py +0 -7
- schemathesis/transports/auth.py +0 -38
- schemathesis/transports/headers.py +0 -36
- schemathesis/transports/responses.py +0 -57
- schemathesis/types.py +0 -44
- schemathesis/utils.py +0 -164
- schemathesis-3.39.15.dist-info/METADATA +0 -293
- schemathesis-3.39.15.dist-info/RECORD +0 -160
- /schemathesis/{extra → cli/ext}/__init__.py +0 -0
- /schemathesis/{_lazy_import.py → core/lazy_import.py} +0 -0
- /schemathesis/{internal → core}/result.py +0 -0
- {schemathesis-3.39.15.dist-info → schemathesis-4.0.0.dist-info}/WHEEL +0 -0
@@ -0,0 +1,455 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import textwrap
|
4
|
+
from typing import TYPE_CHECKING, Any
|
5
|
+
|
6
|
+
from schemathesis.config import OutputConfig
|
7
|
+
from schemathesis.core.failures import Failure, Severity
|
8
|
+
from schemathesis.core.output import truncate_json
|
9
|
+
|
10
|
+
if TYPE_CHECKING:
|
11
|
+
from jsonschema import ValidationError
|
12
|
+
|
13
|
+
|
14
|
+
class UndefinedStatusCode(Failure):
|
15
|
+
"""Response has a status code that is not defined in the schema."""
|
16
|
+
|
17
|
+
__slots__ = (
|
18
|
+
"operation",
|
19
|
+
"status_code",
|
20
|
+
"defined_status_codes",
|
21
|
+
"allowed_status_codes",
|
22
|
+
"message",
|
23
|
+
"title",
|
24
|
+
"case_id",
|
25
|
+
"severity",
|
26
|
+
)
|
27
|
+
|
28
|
+
def __init__(
|
29
|
+
self,
|
30
|
+
*,
|
31
|
+
operation: str,
|
32
|
+
status_code: int,
|
33
|
+
defined_status_codes: list[str],
|
34
|
+
allowed_status_codes: list[int],
|
35
|
+
message: str,
|
36
|
+
title: str = "Undocumented HTTP status code",
|
37
|
+
case_id: str | None = None,
|
38
|
+
) -> None:
|
39
|
+
self.operation = operation
|
40
|
+
self.status_code = status_code
|
41
|
+
self.defined_status_codes = defined_status_codes
|
42
|
+
self.allowed_status_codes = allowed_status_codes
|
43
|
+
self.message = message
|
44
|
+
self.title = title
|
45
|
+
self.case_id = case_id
|
46
|
+
self.severity = Severity.MEDIUM
|
47
|
+
|
48
|
+
@property
|
49
|
+
def _unique_key(self) -> str:
|
50
|
+
return str(self.status_code)
|
51
|
+
|
52
|
+
|
53
|
+
class MissingHeaders(Failure):
|
54
|
+
"""Some required headers are missing."""
|
55
|
+
|
56
|
+
__slots__ = ("operation", "missing_headers", "message", "title", "case_id", "severity")
|
57
|
+
|
58
|
+
def __init__(
|
59
|
+
self,
|
60
|
+
*,
|
61
|
+
operation: str,
|
62
|
+
missing_headers: list[str],
|
63
|
+
message: str,
|
64
|
+
title: str = "Missing required headers",
|
65
|
+
case_id: str | None = None,
|
66
|
+
) -> None:
|
67
|
+
self.operation = operation
|
68
|
+
self.missing_headers = missing_headers
|
69
|
+
self.message = message
|
70
|
+
self.title = title
|
71
|
+
self.case_id = case_id
|
72
|
+
self.severity = Severity.MEDIUM
|
73
|
+
|
74
|
+
|
75
|
+
class JsonSchemaError(Failure):
|
76
|
+
"""Additional information about JSON Schema validation errors."""
|
77
|
+
|
78
|
+
__slots__ = (
|
79
|
+
"operation",
|
80
|
+
"validation_message",
|
81
|
+
"schema_path",
|
82
|
+
"schema",
|
83
|
+
"instance_path",
|
84
|
+
"instance",
|
85
|
+
"message",
|
86
|
+
"title",
|
87
|
+
"case_id",
|
88
|
+
"severity",
|
89
|
+
)
|
90
|
+
|
91
|
+
def __init__(
|
92
|
+
self,
|
93
|
+
*,
|
94
|
+
operation: str,
|
95
|
+
validation_message: str,
|
96
|
+
schema_path: list[str | int],
|
97
|
+
schema: dict[str, Any] | bool,
|
98
|
+
instance_path: list[str | int],
|
99
|
+
instance: None | bool | float | str | list | dict[str, Any],
|
100
|
+
message: str,
|
101
|
+
title: str = "Response violates schema",
|
102
|
+
case_id: str | None = None,
|
103
|
+
) -> None:
|
104
|
+
self.operation = operation
|
105
|
+
self.validation_message = validation_message
|
106
|
+
self.schema_path = schema_path
|
107
|
+
self.schema = schema
|
108
|
+
self.instance_path = instance_path
|
109
|
+
self.instance = instance
|
110
|
+
self.message = message
|
111
|
+
self.title = title
|
112
|
+
self.case_id = case_id
|
113
|
+
self.severity = Severity.HIGH
|
114
|
+
|
115
|
+
@property
|
116
|
+
def _unique_key(self) -> str:
|
117
|
+
return "/".join(map(str, self.schema_path))
|
118
|
+
|
119
|
+
@classmethod
|
120
|
+
def from_exception(
|
121
|
+
cls,
|
122
|
+
*,
|
123
|
+
title: str = "Response violates schema",
|
124
|
+
operation: str,
|
125
|
+
exc: ValidationError,
|
126
|
+
config: OutputConfig | None = None,
|
127
|
+
) -> JsonSchemaError:
|
128
|
+
schema = textwrap.indent(
|
129
|
+
truncate_json(exc.schema, config=config or OutputConfig(), max_lines=20), prefix=" "
|
130
|
+
)
|
131
|
+
value = textwrap.indent(
|
132
|
+
truncate_json(exc.instance, config=config or OutputConfig(), max_lines=20), prefix=" "
|
133
|
+
)
|
134
|
+
schema_path = list(exc.absolute_schema_path)
|
135
|
+
if len(schema_path) > 1:
|
136
|
+
# Exclude the last segment, which is already in the schema
|
137
|
+
schema_title = "Schema at "
|
138
|
+
for segment in schema_path[:-1]:
|
139
|
+
schema_title += f"/{segment}"
|
140
|
+
else:
|
141
|
+
schema_title = "Schema"
|
142
|
+
message = f"{exc.message}\n\n{schema_title}:\n\n{schema}\n\nValue:\n\n{value}"
|
143
|
+
return cls(
|
144
|
+
operation=operation,
|
145
|
+
title=title,
|
146
|
+
message=message,
|
147
|
+
validation_message=exc.message,
|
148
|
+
schema_path=schema_path,
|
149
|
+
schema=exc.schema,
|
150
|
+
instance_path=list(exc.absolute_path),
|
151
|
+
instance=exc.instance,
|
152
|
+
)
|
153
|
+
|
154
|
+
|
155
|
+
class MissingContentType(Failure):
|
156
|
+
"""Content type header is missing."""
|
157
|
+
|
158
|
+
__slots__ = ("operation", "media_types", "message", "title", "case_id", "severity")
|
159
|
+
|
160
|
+
def __init__(
|
161
|
+
self,
|
162
|
+
*,
|
163
|
+
operation: str,
|
164
|
+
media_types: list[str],
|
165
|
+
message: str,
|
166
|
+
title: str = "Missing Content-Type header",
|
167
|
+
case_id: str | None = None,
|
168
|
+
) -> None:
|
169
|
+
self.operation = operation
|
170
|
+
self.media_types = media_types
|
171
|
+
self.message = message
|
172
|
+
self.title = title
|
173
|
+
self.case_id = case_id
|
174
|
+
self.severity = Severity.MEDIUM
|
175
|
+
|
176
|
+
@property
|
177
|
+
def _unique_key(self) -> str:
|
178
|
+
return ""
|
179
|
+
|
180
|
+
|
181
|
+
class MalformedMediaType(Failure):
|
182
|
+
"""Media type name is malformed."""
|
183
|
+
|
184
|
+
__slots__ = ("operation", "actual", "defined", "message", "title", "case_id", "severity")
|
185
|
+
|
186
|
+
def __init__(
|
187
|
+
self,
|
188
|
+
*,
|
189
|
+
operation: str,
|
190
|
+
actual: str,
|
191
|
+
defined: str,
|
192
|
+
message: str,
|
193
|
+
title: str = "Malformed media type",
|
194
|
+
case_id: str | None = None,
|
195
|
+
) -> None:
|
196
|
+
self.operation = operation
|
197
|
+
self.actual = actual
|
198
|
+
self.defined = defined
|
199
|
+
self.message = message
|
200
|
+
self.title = title
|
201
|
+
self.case_id = case_id
|
202
|
+
self.severity = Severity.MEDIUM
|
203
|
+
|
204
|
+
|
205
|
+
class UndefinedContentType(Failure):
|
206
|
+
"""Response has Content-Type that is not documented in the schema."""
|
207
|
+
|
208
|
+
__slots__ = (
|
209
|
+
"operation",
|
210
|
+
"content_type",
|
211
|
+
"defined_content_types",
|
212
|
+
"message",
|
213
|
+
"title",
|
214
|
+
"case_id",
|
215
|
+
"severity",
|
216
|
+
)
|
217
|
+
|
218
|
+
def __init__(
|
219
|
+
self,
|
220
|
+
*,
|
221
|
+
operation: str,
|
222
|
+
content_type: str,
|
223
|
+
defined_content_types: list[str],
|
224
|
+
message: str,
|
225
|
+
title: str = "Undocumented Content-Type",
|
226
|
+
case_id: str | None = None,
|
227
|
+
) -> None:
|
228
|
+
self.operation = operation
|
229
|
+
self.content_type = content_type
|
230
|
+
self.defined_content_types = defined_content_types
|
231
|
+
self.message = message
|
232
|
+
self.title = title
|
233
|
+
self.case_id = case_id
|
234
|
+
self.severity = Severity.MEDIUM
|
235
|
+
|
236
|
+
@property
|
237
|
+
def _unique_key(self) -> str:
|
238
|
+
return self.content_type
|
239
|
+
|
240
|
+
|
241
|
+
class UseAfterFree(Failure):
|
242
|
+
"""Resource was used after a successful DELETE operation on it."""
|
243
|
+
|
244
|
+
__slots__ = ("operation", "message", "free", "usage", "title", "case_id", "severity")
|
245
|
+
|
246
|
+
def __init__(
|
247
|
+
self,
|
248
|
+
*,
|
249
|
+
operation: str,
|
250
|
+
message: str,
|
251
|
+
free: str,
|
252
|
+
usage: str,
|
253
|
+
title: str = "Use after free",
|
254
|
+
case_id: str | None = None,
|
255
|
+
) -> None:
|
256
|
+
self.operation = operation
|
257
|
+
self.message = message
|
258
|
+
self.free = free
|
259
|
+
self.usage = usage
|
260
|
+
self.title = title
|
261
|
+
self.case_id = case_id
|
262
|
+
self.severity = Severity.CRITICAL
|
263
|
+
|
264
|
+
@property
|
265
|
+
def _unique_key(self) -> str:
|
266
|
+
return ""
|
267
|
+
|
268
|
+
|
269
|
+
class EnsureResourceAvailability(Failure):
|
270
|
+
"""Resource is not available immediately after creation."""
|
271
|
+
|
272
|
+
__slots__ = ("operation", "message", "created_with", "not_available_with", "title", "case_id", "severity")
|
273
|
+
|
274
|
+
def __init__(
|
275
|
+
self,
|
276
|
+
*,
|
277
|
+
operation: str,
|
278
|
+
message: str,
|
279
|
+
created_with: str,
|
280
|
+
not_available_with: str,
|
281
|
+
title: str = "Resource is not available after creation",
|
282
|
+
case_id: str | None = None,
|
283
|
+
) -> None:
|
284
|
+
self.operation = operation
|
285
|
+
self.message = message
|
286
|
+
self.created_with = created_with
|
287
|
+
self.not_available_with = not_available_with
|
288
|
+
self.title = title
|
289
|
+
self.case_id = case_id
|
290
|
+
self.severity = Severity.MEDIUM
|
291
|
+
|
292
|
+
@property
|
293
|
+
def _unique_key(self) -> str:
|
294
|
+
return ""
|
295
|
+
|
296
|
+
|
297
|
+
class IgnoredAuth(Failure):
|
298
|
+
"""The API operation does not check the specified authentication."""
|
299
|
+
|
300
|
+
__slots__ = ("operation", "message", "title", "case_id", "severity")
|
301
|
+
|
302
|
+
def __init__(
|
303
|
+
self,
|
304
|
+
*,
|
305
|
+
operation: str,
|
306
|
+
message: str,
|
307
|
+
title: str = "API accepts requests without authentication",
|
308
|
+
case_id: str | None = None,
|
309
|
+
) -> None:
|
310
|
+
self.operation = operation
|
311
|
+
self.message = message
|
312
|
+
self.title = title
|
313
|
+
self.case_id = case_id
|
314
|
+
self.severity = Severity.CRITICAL
|
315
|
+
|
316
|
+
@property
|
317
|
+
def _unique_key(self) -> str:
|
318
|
+
return ""
|
319
|
+
|
320
|
+
|
321
|
+
class AcceptedNegativeData(Failure):
|
322
|
+
"""Response with negative data was accepted."""
|
323
|
+
|
324
|
+
__slots__ = ("operation", "message", "status_code", "expected_statuses", "title", "case_id", "severity")
|
325
|
+
|
326
|
+
def __init__(
|
327
|
+
self,
|
328
|
+
*,
|
329
|
+
operation: str,
|
330
|
+
message: str,
|
331
|
+
status_code: int,
|
332
|
+
expected_statuses: list[str],
|
333
|
+
title: str = "Accepted negative data",
|
334
|
+
case_id: str | None = None,
|
335
|
+
) -> None:
|
336
|
+
self.operation = operation
|
337
|
+
self.message = message
|
338
|
+
self.status_code = status_code
|
339
|
+
self.expected_statuses = expected_statuses
|
340
|
+
self.title = title
|
341
|
+
self.case_id = case_id
|
342
|
+
self.severity = Severity.MEDIUM
|
343
|
+
|
344
|
+
@property
|
345
|
+
def _unique_key(self) -> str:
|
346
|
+
return str(self.status_code)
|
347
|
+
|
348
|
+
|
349
|
+
class RejectedPositiveData(Failure):
|
350
|
+
"""Response with positive data was rejected."""
|
351
|
+
|
352
|
+
__slots__ = ("operation", "message", "status_code", "allowed_statuses", "title", "case_id", "severity")
|
353
|
+
|
354
|
+
def __init__(
|
355
|
+
self,
|
356
|
+
*,
|
357
|
+
operation: str,
|
358
|
+
message: str,
|
359
|
+
status_code: int,
|
360
|
+
allowed_statuses: list[str],
|
361
|
+
title: str = "Rejected positive data",
|
362
|
+
case_id: str | None = None,
|
363
|
+
) -> None:
|
364
|
+
self.operation = operation
|
365
|
+
self.message = message
|
366
|
+
self.status_code = status_code
|
367
|
+
self.allowed_statuses = allowed_statuses
|
368
|
+
self.title = title
|
369
|
+
self.case_id = case_id
|
370
|
+
self.severity = Severity.MEDIUM
|
371
|
+
|
372
|
+
@property
|
373
|
+
def _unique_key(self) -> str:
|
374
|
+
return str(self.status_code)
|
375
|
+
|
376
|
+
|
377
|
+
class MissingHeaderNotRejected(Failure):
|
378
|
+
"""API did not reject request without required header."""
|
379
|
+
|
380
|
+
__slots__ = (
|
381
|
+
"operation",
|
382
|
+
"header_name",
|
383
|
+
"status_code",
|
384
|
+
"expected_statuses",
|
385
|
+
"message",
|
386
|
+
"title",
|
387
|
+
"case_id",
|
388
|
+
"severity",
|
389
|
+
)
|
390
|
+
|
391
|
+
def __init__(
|
392
|
+
self,
|
393
|
+
*,
|
394
|
+
operation: str,
|
395
|
+
header_name: str,
|
396
|
+
status_code: int,
|
397
|
+
expected_statuses: list[int],
|
398
|
+
message: str,
|
399
|
+
title: str = "Missing header not rejected",
|
400
|
+
case_id: str | None = None,
|
401
|
+
) -> None:
|
402
|
+
self.operation = operation
|
403
|
+
self.header_name = header_name
|
404
|
+
self.status_code = status_code
|
405
|
+
self.expected_statuses = expected_statuses
|
406
|
+
self.message = message
|
407
|
+
self.title = title
|
408
|
+
self.case_id = case_id
|
409
|
+
self.severity = Severity.MEDIUM
|
410
|
+
|
411
|
+
@property
|
412
|
+
def _unique_key(self) -> str:
|
413
|
+
return self.header_name
|
414
|
+
|
415
|
+
|
416
|
+
class UnsupportedMethodResponse(Failure):
|
417
|
+
"""API response for unsupported HTTP method is incorrect."""
|
418
|
+
|
419
|
+
__slots__ = (
|
420
|
+
"operation",
|
421
|
+
"method",
|
422
|
+
"status_code",
|
423
|
+
"allow_header_present",
|
424
|
+
"failure_reason",
|
425
|
+
"message",
|
426
|
+
"title",
|
427
|
+
"case_id",
|
428
|
+
"severity",
|
429
|
+
)
|
430
|
+
|
431
|
+
def __init__(
|
432
|
+
self,
|
433
|
+
*,
|
434
|
+
operation: str,
|
435
|
+
method: str,
|
436
|
+
status_code: int,
|
437
|
+
allow_header_present: bool | None = None,
|
438
|
+
failure_reason: str, # "wrong_status" or "missing_allow_header"
|
439
|
+
message: str,
|
440
|
+
title: str = "Unsupported method incorrect response",
|
441
|
+
case_id: str | None = None,
|
442
|
+
) -> None:
|
443
|
+
self.operation = operation
|
444
|
+
self.method = method
|
445
|
+
self.status_code = status_code
|
446
|
+
self.allow_header_present = allow_header_present
|
447
|
+
self.failure_reason = failure_reason
|
448
|
+
self.message = message
|
449
|
+
self.title = title
|
450
|
+
self.case_id = case_id
|
451
|
+
self.severity = Severity.MEDIUM
|
452
|
+
|
453
|
+
@property
|
454
|
+
def _unique_key(self) -> str:
|
455
|
+
return self.failure_reason
|
File without changes
|
@@ -0,0 +1,72 @@
|
|
1
|
+
from collections.abc import Mapping
|
2
|
+
from typing import Any
|
3
|
+
|
4
|
+
from schemathesis.core import NOT_SET
|
5
|
+
from schemathesis.core.validation import contains_unicode_surrogate_pair, has_invalid_characters, is_latin_1_encodable
|
6
|
+
|
7
|
+
__all__ = [
|
8
|
+
"is_valid_path",
|
9
|
+
"is_valid_header",
|
10
|
+
"is_valid_urlencoded",
|
11
|
+
"is_valid_query",
|
12
|
+
]
|
13
|
+
|
14
|
+
|
15
|
+
def is_valid_path(parameters: dict[str, object]) -> bool:
|
16
|
+
"""Empty strings ("") are excluded from path by urllib3.
|
17
|
+
|
18
|
+
A path containing to "/" or "%2F" will lead to ambiguous path resolution in
|
19
|
+
many frameworks and libraries, such behaviour have been observed in both
|
20
|
+
WSGI and ASGI applications.
|
21
|
+
|
22
|
+
In this case one variable in the path template will be empty, which will lead to 404 in most of the cases.
|
23
|
+
Because of it this case doesn't bring much value and might lead to false positives results of Schemathesis runs.
|
24
|
+
"""
|
25
|
+
return not any(is_invalid_path_parameter(value) for value in parameters.values())
|
26
|
+
|
27
|
+
|
28
|
+
def is_invalid_path_parameter(value: Any) -> bool:
|
29
|
+
return (
|
30
|
+
value in ("/", "")
|
31
|
+
or contains_unicode_surrogate_pair(value)
|
32
|
+
or (
|
33
|
+
isinstance(value, str)
|
34
|
+
and (
|
35
|
+
("/" in value or "}" in value or "{" in value)
|
36
|
+
# Avoid situations when the path parameter contains only NULL bytes
|
37
|
+
# Many webservers remove such bytes and as the result, the test can target a different API operation
|
38
|
+
or (len(value) == value.count("\x00"))
|
39
|
+
)
|
40
|
+
)
|
41
|
+
)
|
42
|
+
|
43
|
+
|
44
|
+
def is_valid_header(headers: dict[str, object]) -> bool:
|
45
|
+
for name, value in headers.items():
|
46
|
+
if not is_latin_1_encodable(value):
|
47
|
+
return False
|
48
|
+
if has_invalid_characters(name, value):
|
49
|
+
return False
|
50
|
+
return True
|
51
|
+
|
52
|
+
|
53
|
+
def is_valid_query(query: dict[str, object]) -> bool:
|
54
|
+
for name, value in query.items():
|
55
|
+
if contains_unicode_surrogate_pair(name) or contains_unicode_surrogate_pair(value):
|
56
|
+
return False
|
57
|
+
return True
|
58
|
+
|
59
|
+
|
60
|
+
def is_valid_urlencoded(data: object) -> bool:
|
61
|
+
# TODO: write a test that will check if `requests` can send it
|
62
|
+
if data is NOT_SET or isinstance(data, Mapping):
|
63
|
+
return True
|
64
|
+
|
65
|
+
if hasattr(data, "__iter__"):
|
66
|
+
try:
|
67
|
+
for _, _ in data:
|
68
|
+
pass
|
69
|
+
return True
|
70
|
+
except (TypeError, ValueError):
|
71
|
+
return False
|
72
|
+
return False
|