schemathesis 3.25.5__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 -1766
- 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/{cli → engine/phases}/probes.py +63 -70
- 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 +153 -39
- 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 +483 -367
- 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.5.dist-info → schemathesis-4.0.0a1.dist-info}/WHEEL +1 -1
- {schemathesis-3.25.5.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 -55
- 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 -765
- schemathesis/cli/output/short.py +0 -40
- 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 -1231
- schemathesis/parameters.py +0 -86
- schemathesis/runner/__init__.py +0 -555
- schemathesis/runner/events.py +0 -309
- schemathesis/runner/impl/__init__.py +0 -3
- schemathesis/runner/impl/core.py +0 -986
- 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 -315
- 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 -184
- 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.5.dist-info/METADATA +0 -356
- schemathesis-3.25.5.dist-info/RECORD +0 -134
- /schemathesis/{extra → cli/ext}/__init__.py +0 -0
- {schemathesis-3.25.5.dist-info → schemathesis-4.0.0a1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,412 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import textwrap
|
4
|
+
from dataclasses import dataclass, field
|
5
|
+
from typing import TYPE_CHECKING, Any
|
6
|
+
|
7
|
+
from schemathesis.core.failures import Failure, Severity
|
8
|
+
from schemathesis.core.output import OutputConfig, truncate_json
|
9
|
+
|
10
|
+
if TYPE_CHECKING:
|
11
|
+
from jsonschema import ValidationError
|
12
|
+
|
13
|
+
|
14
|
+
@dataclass
|
15
|
+
class NegativeDataRejectionConfig:
|
16
|
+
# 5xx will pass through
|
17
|
+
allowed_statuses: list[str] = field(default_factory=lambda: ["400", "401", "403", "404", "422", "428", "5xx"])
|
18
|
+
|
19
|
+
|
20
|
+
@dataclass
|
21
|
+
class PositiveDataAcceptanceConfig:
|
22
|
+
allowed_statuses: list[str] = field(default_factory=lambda: ["2xx", "401", "403", "404"])
|
23
|
+
|
24
|
+
|
25
|
+
@dataclass
|
26
|
+
class MissingRequiredHeaderConfig:
|
27
|
+
allowed_statuses: list[str] = field(default_factory=lambda: ["406"])
|
28
|
+
|
29
|
+
|
30
|
+
class UndefinedStatusCode(Failure):
|
31
|
+
"""Response has a status code that is not defined in the schema."""
|
32
|
+
|
33
|
+
__slots__ = (
|
34
|
+
"operation",
|
35
|
+
"status_code",
|
36
|
+
"defined_status_codes",
|
37
|
+
"allowed_status_codes",
|
38
|
+
"message",
|
39
|
+
"title",
|
40
|
+
"code",
|
41
|
+
"case_id",
|
42
|
+
"severity",
|
43
|
+
)
|
44
|
+
|
45
|
+
def __init__(
|
46
|
+
self,
|
47
|
+
*,
|
48
|
+
operation: str,
|
49
|
+
status_code: int,
|
50
|
+
defined_status_codes: list[str],
|
51
|
+
allowed_status_codes: list[int],
|
52
|
+
message: str,
|
53
|
+
title: str = "Undocumented HTTP status code",
|
54
|
+
code: str = "undefined_status_code",
|
55
|
+
case_id: str | None = None,
|
56
|
+
) -> None:
|
57
|
+
self.operation = operation
|
58
|
+
self.status_code = status_code
|
59
|
+
self.defined_status_codes = defined_status_codes
|
60
|
+
self.allowed_status_codes = allowed_status_codes
|
61
|
+
self.message = message
|
62
|
+
self.title = title
|
63
|
+
self.code = code
|
64
|
+
self.case_id = case_id
|
65
|
+
self.severity = Severity.MEDIUM
|
66
|
+
|
67
|
+
@property
|
68
|
+
def _unique_key(self) -> str:
|
69
|
+
return str(self.status_code)
|
70
|
+
|
71
|
+
|
72
|
+
class MissingHeaders(Failure):
|
73
|
+
"""Some required headers are missing."""
|
74
|
+
|
75
|
+
__slots__ = ("operation", "missing_headers", "message", "title", "code", "case_id", "severity")
|
76
|
+
|
77
|
+
def __init__(
|
78
|
+
self,
|
79
|
+
*,
|
80
|
+
operation: str,
|
81
|
+
missing_headers: list[str],
|
82
|
+
message: str,
|
83
|
+
title: str = "Missing required headers",
|
84
|
+
code: str = "missing_headers",
|
85
|
+
case_id: str | None = None,
|
86
|
+
) -> None:
|
87
|
+
self.operation = operation
|
88
|
+
self.missing_headers = missing_headers
|
89
|
+
self.message = message
|
90
|
+
self.title = title
|
91
|
+
self.code = code
|
92
|
+
self.case_id = case_id
|
93
|
+
self.severity = Severity.MEDIUM
|
94
|
+
|
95
|
+
|
96
|
+
class JsonSchemaError(Failure):
|
97
|
+
"""Additional information about JSON Schema validation errors."""
|
98
|
+
|
99
|
+
__slots__ = (
|
100
|
+
"operation",
|
101
|
+
"validation_message",
|
102
|
+
"schema_path",
|
103
|
+
"schema",
|
104
|
+
"instance_path",
|
105
|
+
"instance",
|
106
|
+
"message",
|
107
|
+
"title",
|
108
|
+
"code",
|
109
|
+
"case_id",
|
110
|
+
"severity",
|
111
|
+
)
|
112
|
+
|
113
|
+
def __init__(
|
114
|
+
self,
|
115
|
+
*,
|
116
|
+
operation: str,
|
117
|
+
validation_message: str,
|
118
|
+
schema_path: list[str | int],
|
119
|
+
schema: dict[str, Any] | bool,
|
120
|
+
instance_path: list[str | int],
|
121
|
+
instance: None | bool | float | str | list | dict[str, Any],
|
122
|
+
message: str,
|
123
|
+
title: str = "Response violates schema",
|
124
|
+
code: str = "json_schema",
|
125
|
+
case_id: str | None = None,
|
126
|
+
) -> None:
|
127
|
+
self.operation = operation
|
128
|
+
self.validation_message = validation_message
|
129
|
+
self.schema_path = schema_path
|
130
|
+
self.schema = schema
|
131
|
+
self.instance_path = instance_path
|
132
|
+
self.instance = instance
|
133
|
+
self.message = message
|
134
|
+
self.title = title
|
135
|
+
self.code = code
|
136
|
+
self.case_id = case_id
|
137
|
+
self.severity = Severity.HIGH
|
138
|
+
|
139
|
+
@property
|
140
|
+
def _unique_key(self) -> str:
|
141
|
+
return "/".join(map(str, self.schema_path))
|
142
|
+
|
143
|
+
@classmethod
|
144
|
+
def from_exception(
|
145
|
+
cls,
|
146
|
+
*,
|
147
|
+
title: str = "Response violates schema",
|
148
|
+
operation: str,
|
149
|
+
exc: ValidationError,
|
150
|
+
output_config: OutputConfig | None = None,
|
151
|
+
) -> JsonSchemaError:
|
152
|
+
output_config = OutputConfig.from_parent(output_config, max_lines=20)
|
153
|
+
schema = textwrap.indent(truncate_json(exc.schema, config=output_config), prefix=" ")
|
154
|
+
value = textwrap.indent(truncate_json(exc.instance, config=output_config), prefix=" ")
|
155
|
+
schema_path = list(exc.absolute_schema_path)
|
156
|
+
if len(schema_path) > 1:
|
157
|
+
# Exclude the last segment, which is already in the schema
|
158
|
+
schema_title = "Schema at "
|
159
|
+
for segment in schema_path[:-1]:
|
160
|
+
schema_title += f"/{segment}"
|
161
|
+
else:
|
162
|
+
schema_title = "Schema"
|
163
|
+
message = f"{exc.message}\n\n{schema_title}:\n\n{schema}\n\nValue:\n\n{value}"
|
164
|
+
return cls(
|
165
|
+
operation=operation,
|
166
|
+
title=title,
|
167
|
+
message=message,
|
168
|
+
validation_message=exc.message,
|
169
|
+
schema_path=schema_path,
|
170
|
+
schema=exc.schema,
|
171
|
+
instance_path=list(exc.absolute_path),
|
172
|
+
instance=exc.instance,
|
173
|
+
)
|
174
|
+
|
175
|
+
|
176
|
+
class MissingContentType(Failure):
|
177
|
+
"""Content type header is missing."""
|
178
|
+
|
179
|
+
__slots__ = ("operation", "media_types", "message", "title", "code", "case_id", "severity")
|
180
|
+
|
181
|
+
def __init__(
|
182
|
+
self,
|
183
|
+
*,
|
184
|
+
operation: str,
|
185
|
+
media_types: list[str],
|
186
|
+
message: str,
|
187
|
+
title: str = "Missing Content-Type header",
|
188
|
+
code: str = "missing_content_type",
|
189
|
+
case_id: str | None = None,
|
190
|
+
) -> None:
|
191
|
+
self.operation = operation
|
192
|
+
self.media_types = media_types
|
193
|
+
self.message = message
|
194
|
+
self.title = title
|
195
|
+
self.code = code
|
196
|
+
self.case_id = case_id
|
197
|
+
self.severity = Severity.MEDIUM
|
198
|
+
|
199
|
+
@property
|
200
|
+
def _unique_key(self) -> str:
|
201
|
+
return ""
|
202
|
+
|
203
|
+
|
204
|
+
class MalformedMediaType(Failure):
|
205
|
+
"""Media type name is malformed."""
|
206
|
+
|
207
|
+
__slots__ = ("operation", "actual", "defined", "message", "title", "code", "case_id", "severity")
|
208
|
+
|
209
|
+
def __init__(
|
210
|
+
self,
|
211
|
+
*,
|
212
|
+
operation: str,
|
213
|
+
actual: str,
|
214
|
+
defined: str,
|
215
|
+
message: str,
|
216
|
+
title: str = "Malformed media type",
|
217
|
+
code: str = "malformed_media_type",
|
218
|
+
case_id: str | None = None,
|
219
|
+
) -> None:
|
220
|
+
self.operation = operation
|
221
|
+
self.actual = actual
|
222
|
+
self.defined = defined
|
223
|
+
self.message = message
|
224
|
+
self.title = title
|
225
|
+
self.code = code
|
226
|
+
self.case_id = case_id
|
227
|
+
self.severity = Severity.MEDIUM
|
228
|
+
|
229
|
+
|
230
|
+
class UndefinedContentType(Failure):
|
231
|
+
"""Response has Content-Type that is not documented in the schema."""
|
232
|
+
|
233
|
+
__slots__ = (
|
234
|
+
"operation",
|
235
|
+
"content_type",
|
236
|
+
"defined_content_types",
|
237
|
+
"message",
|
238
|
+
"title",
|
239
|
+
"code",
|
240
|
+
"case_id",
|
241
|
+
"severity",
|
242
|
+
)
|
243
|
+
|
244
|
+
def __init__(
|
245
|
+
self,
|
246
|
+
*,
|
247
|
+
operation: str,
|
248
|
+
content_type: str,
|
249
|
+
defined_content_types: list[str],
|
250
|
+
message: str,
|
251
|
+
title: str = "Undocumented Content-Type",
|
252
|
+
code: str = "undefined_content_type",
|
253
|
+
case_id: str | None = None,
|
254
|
+
) -> None:
|
255
|
+
self.operation = operation
|
256
|
+
self.content_type = content_type
|
257
|
+
self.defined_content_types = defined_content_types
|
258
|
+
self.message = message
|
259
|
+
self.title = title
|
260
|
+
self.code = code
|
261
|
+
self.case_id = case_id
|
262
|
+
self.severity = Severity.MEDIUM
|
263
|
+
|
264
|
+
@property
|
265
|
+
def _unique_key(self) -> str:
|
266
|
+
return self.content_type
|
267
|
+
|
268
|
+
|
269
|
+
class UseAfterFree(Failure):
|
270
|
+
"""Resource was used after a successful DELETE operation on it."""
|
271
|
+
|
272
|
+
__slots__ = ("operation", "message", "free", "usage", "title", "code", "case_id", "severity")
|
273
|
+
|
274
|
+
def __init__(
|
275
|
+
self,
|
276
|
+
*,
|
277
|
+
operation: str,
|
278
|
+
message: str,
|
279
|
+
free: str,
|
280
|
+
usage: str,
|
281
|
+
title: str = "Use after free",
|
282
|
+
code: str = "use_after_free",
|
283
|
+
case_id: str | None = None,
|
284
|
+
) -> None:
|
285
|
+
self.operation = operation
|
286
|
+
self.message = message
|
287
|
+
self.free = free
|
288
|
+
self.usage = usage
|
289
|
+
self.title = title
|
290
|
+
self.code = code
|
291
|
+
self.case_id = case_id
|
292
|
+
self.severity = Severity.CRITICAL
|
293
|
+
|
294
|
+
@property
|
295
|
+
def _unique_key(self) -> str:
|
296
|
+
return ""
|
297
|
+
|
298
|
+
|
299
|
+
class EnsureResourceAvailability(Failure):
|
300
|
+
"""Resource is not available immediately after creation."""
|
301
|
+
|
302
|
+
__slots__ = ("operation", "message", "created_with", "not_available_with", "title", "code", "case_id", "severity")
|
303
|
+
|
304
|
+
def __init__(
|
305
|
+
self,
|
306
|
+
*,
|
307
|
+
operation: str,
|
308
|
+
message: str,
|
309
|
+
created_with: str,
|
310
|
+
not_available_with: str,
|
311
|
+
title: str = "Resource is not available after creation",
|
312
|
+
code: str = "ensure_resource_availability",
|
313
|
+
case_id: str | None = None,
|
314
|
+
) -> None:
|
315
|
+
self.operation = operation
|
316
|
+
self.message = message
|
317
|
+
self.created_with = created_with
|
318
|
+
self.not_available_with = not_available_with
|
319
|
+
self.title = title
|
320
|
+
self.code = code
|
321
|
+
self.case_id = case_id
|
322
|
+
self.severity = Severity.MEDIUM
|
323
|
+
|
324
|
+
@property
|
325
|
+
def _unique_key(self) -> str:
|
326
|
+
return ""
|
327
|
+
|
328
|
+
|
329
|
+
class IgnoredAuth(Failure):
|
330
|
+
"""The API operation does not check the specified authentication."""
|
331
|
+
|
332
|
+
__slots__ = ("operation", "message", "title", "code", "case_id", "severity")
|
333
|
+
|
334
|
+
def __init__(
|
335
|
+
self,
|
336
|
+
*,
|
337
|
+
operation: str,
|
338
|
+
message: str,
|
339
|
+
title: str = "Authentication declared but not enforced",
|
340
|
+
code: str = "ignored_auth",
|
341
|
+
case_id: str | None = None,
|
342
|
+
) -> None:
|
343
|
+
self.operation = operation
|
344
|
+
self.message = message
|
345
|
+
self.title = title
|
346
|
+
self.code = code
|
347
|
+
self.case_id = case_id
|
348
|
+
self.severity = Severity.CRITICAL
|
349
|
+
|
350
|
+
@property
|
351
|
+
def _unique_key(self) -> str:
|
352
|
+
return ""
|
353
|
+
|
354
|
+
|
355
|
+
class AcceptedNegativeData(Failure):
|
356
|
+
"""Response with negative data was accepted."""
|
357
|
+
|
358
|
+
__slots__ = ("operation", "message", "status_code", "allowed_statuses", "title", "code", "case_id", "severity")
|
359
|
+
|
360
|
+
def __init__(
|
361
|
+
self,
|
362
|
+
*,
|
363
|
+
operation: str,
|
364
|
+
message: str,
|
365
|
+
status_code: int,
|
366
|
+
allowed_statuses: list[str],
|
367
|
+
title: str = "Accepted negative data",
|
368
|
+
code: str = "accepted_negative_data",
|
369
|
+
case_id: str | None = None,
|
370
|
+
) -> None:
|
371
|
+
self.operation = operation
|
372
|
+
self.message = message
|
373
|
+
self.status_code = status_code
|
374
|
+
self.allowed_statuses = allowed_statuses
|
375
|
+
self.title = title
|
376
|
+
self.code = code
|
377
|
+
self.case_id = case_id
|
378
|
+
self.severity = Severity.MEDIUM
|
379
|
+
|
380
|
+
@property
|
381
|
+
def _unique_key(self) -> str:
|
382
|
+
return str(self.status_code)
|
383
|
+
|
384
|
+
|
385
|
+
class RejectedPositiveData(Failure):
|
386
|
+
"""Response with positive data was rejected."""
|
387
|
+
|
388
|
+
__slots__ = ("operation", "message", "status_code", "allowed_statuses", "title", "code", "case_id", "severity")
|
389
|
+
|
390
|
+
def __init__(
|
391
|
+
self,
|
392
|
+
*,
|
393
|
+
operation: str,
|
394
|
+
message: str,
|
395
|
+
status_code: int,
|
396
|
+
allowed_statuses: list[str],
|
397
|
+
title: str = "Rejected positive data",
|
398
|
+
code: str = "rejected_positive_data",
|
399
|
+
case_id: str | None = None,
|
400
|
+
) -> None:
|
401
|
+
self.operation = operation
|
402
|
+
self.message = message
|
403
|
+
self.status_code = status_code
|
404
|
+
self.allowed_statuses = allowed_statuses
|
405
|
+
self.title = title
|
406
|
+
self.code = code
|
407
|
+
self.case_id = case_id
|
408
|
+
self.severity = Severity.MEDIUM
|
409
|
+
|
410
|
+
@property
|
411
|
+
def _unique_key(self) -> str:
|
412
|
+
return str(self.status_code)
|
File without changes
|
@@ -0,0 +1,63 @@
|
|
1
|
+
from collections.abc import Mapping
|
2
|
+
|
3
|
+
from schemathesis.core import NOT_SET
|
4
|
+
from schemathesis.core.validation import contains_unicode_surrogate_pair, has_invalid_characters, is_latin_1_encodable
|
5
|
+
|
6
|
+
__all__ = [
|
7
|
+
"is_valid_path",
|
8
|
+
"is_valid_header",
|
9
|
+
"is_valid_urlencoded",
|
10
|
+
"is_valid_query",
|
11
|
+
]
|
12
|
+
|
13
|
+
|
14
|
+
def is_valid_path(parameters: dict[str, object]) -> bool:
|
15
|
+
"""Empty strings ("") are excluded from path by urllib3.
|
16
|
+
|
17
|
+
A path containing to "/" or "%2F" will lead to ambiguous path resolution in
|
18
|
+
many frameworks and libraries, such behaviour have been observed in both
|
19
|
+
WSGI and ASGI applications.
|
20
|
+
|
21
|
+
In this case one variable in the path template will be empty, which will lead to 404 in most of the cases.
|
22
|
+
Because of it this case doesn't bring much value and might lead to false positives results of Schemathesis runs.
|
23
|
+
"""
|
24
|
+
return not any(
|
25
|
+
(
|
26
|
+
value in ("/", "")
|
27
|
+
or contains_unicode_surrogate_pair(value)
|
28
|
+
or isinstance(value, str)
|
29
|
+
and ("/" in value or "}" in value or "{" in value)
|
30
|
+
)
|
31
|
+
for value in parameters.values()
|
32
|
+
)
|
33
|
+
|
34
|
+
|
35
|
+
def is_valid_header(headers: dict[str, object]) -> bool:
|
36
|
+
for name, value in headers.items():
|
37
|
+
if not is_latin_1_encodable(value):
|
38
|
+
return False
|
39
|
+
if has_invalid_characters(name, value):
|
40
|
+
return False
|
41
|
+
return True
|
42
|
+
|
43
|
+
|
44
|
+
def is_valid_query(query: dict[str, object]) -> bool:
|
45
|
+
for name, value in query.items():
|
46
|
+
if contains_unicode_surrogate_pair(name) or contains_unicode_surrogate_pair(value):
|
47
|
+
return False
|
48
|
+
return True
|
49
|
+
|
50
|
+
|
51
|
+
def is_valid_urlencoded(data: object) -> bool:
|
52
|
+
# TODO: write a test that will check if `requests` can send it
|
53
|
+
if data is NOT_SET or isinstance(data, Mapping):
|
54
|
+
return True
|
55
|
+
|
56
|
+
if hasattr(data, "__iter__"):
|
57
|
+
try:
|
58
|
+
for _, _ in data:
|
59
|
+
pass
|
60
|
+
return True
|
61
|
+
except (TypeError, ValueError):
|
62
|
+
return False
|
63
|
+
return False
|
@@ -0,0 +1,178 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import enum
|
4
|
+
import json
|
5
|
+
import re
|
6
|
+
from os import PathLike
|
7
|
+
from pathlib import Path
|
8
|
+
from typing import IO, TYPE_CHECKING, Any, Mapping
|
9
|
+
|
10
|
+
from schemathesis.core import media_types
|
11
|
+
from schemathesis.core.deserialization import deserialize_yaml
|
12
|
+
from schemathesis.core.errors import LoaderError, LoaderErrorKind
|
13
|
+
from schemathesis.core.loaders import load_from_url, prepare_request_kwargs, raise_for_status, require_relative_url
|
14
|
+
from schemathesis.hooks import HookContext, dispatch
|
15
|
+
from schemathesis.python import asgi, wsgi
|
16
|
+
|
17
|
+
if TYPE_CHECKING:
|
18
|
+
from schemathesis.specs.openapi.schemas import BaseOpenAPISchema
|
19
|
+
|
20
|
+
|
21
|
+
def from_asgi(path: str, app: Any, **kwargs: Any) -> BaseOpenAPISchema:
|
22
|
+
require_relative_url(path)
|
23
|
+
client = asgi.get_client(app)
|
24
|
+
response = load_from_url(client.get, url=path, **kwargs)
|
25
|
+
content_type = detect_content_type(headers=response.headers, path=path)
|
26
|
+
schema = load_content(response.text, content_type)
|
27
|
+
return from_dict(schema=schema).configure(app=app, location=path)
|
28
|
+
|
29
|
+
|
30
|
+
def from_wsgi(path: str, app: Any, **kwargs: Any) -> BaseOpenAPISchema:
|
31
|
+
require_relative_url(path)
|
32
|
+
prepare_request_kwargs(kwargs)
|
33
|
+
client = wsgi.get_client(app)
|
34
|
+
response = client.get(path=path, **kwargs)
|
35
|
+
raise_for_status(response)
|
36
|
+
content_type = detect_content_type(headers=response.headers, path=path)
|
37
|
+
schema = load_content(response.text, content_type)
|
38
|
+
return from_dict(schema=schema).configure(app=app, location=path)
|
39
|
+
|
40
|
+
|
41
|
+
def from_url(url: str, *, wait_for_schema: float | None = None, **kwargs: Any) -> BaseOpenAPISchema:
|
42
|
+
"""Load from URL."""
|
43
|
+
import requests
|
44
|
+
|
45
|
+
response = load_from_url(requests.get, url=url, wait_for_schema=wait_for_schema, **kwargs)
|
46
|
+
content_type = detect_content_type(headers=response.headers, path=url)
|
47
|
+
schema = load_content(response.text, content_type)
|
48
|
+
return from_dict(schema=schema).configure(location=url)
|
49
|
+
|
50
|
+
|
51
|
+
def from_path(path: PathLike | str, *, encoding: str = "utf-8") -> BaseOpenAPISchema:
|
52
|
+
"""Load from a filesystem path."""
|
53
|
+
with open(path, encoding=encoding) as file:
|
54
|
+
content_type = detect_content_type(headers=None, path=str(path))
|
55
|
+
schema = load_content(file.read(), content_type)
|
56
|
+
return from_dict(schema=schema).configure(location=Path(path).absolute().as_uri())
|
57
|
+
|
58
|
+
|
59
|
+
def from_file(file: IO[str] | str) -> BaseOpenAPISchema:
|
60
|
+
"""Load from file-like object or string."""
|
61
|
+
if isinstance(file, str):
|
62
|
+
data = file
|
63
|
+
else:
|
64
|
+
data = file.read()
|
65
|
+
try:
|
66
|
+
schema = json.loads(data)
|
67
|
+
except json.JSONDecodeError:
|
68
|
+
schema = _load_yaml(data)
|
69
|
+
return from_dict(schema)
|
70
|
+
|
71
|
+
|
72
|
+
def from_dict(schema: dict[str, Any]) -> BaseOpenAPISchema:
|
73
|
+
"""Base loader that others build upon."""
|
74
|
+
from schemathesis.specs.openapi.schemas import OpenApi30, SwaggerV20
|
75
|
+
|
76
|
+
if not isinstance(schema, dict):
|
77
|
+
raise LoaderError(LoaderErrorKind.OPEN_API_INVALID_SCHEMA, SCHEMA_INVALID_ERROR)
|
78
|
+
hook_context = HookContext()
|
79
|
+
dispatch("before_load_schema", hook_context, schema)
|
80
|
+
|
81
|
+
if "swagger" in schema:
|
82
|
+
instance = SwaggerV20(schema)
|
83
|
+
elif "openapi" in schema:
|
84
|
+
version = schema["openapi"]
|
85
|
+
if not OPENAPI_VERSION_RE.match(version):
|
86
|
+
raise LoaderError(
|
87
|
+
LoaderErrorKind.OPEN_API_UNSUPPORTED_VERSION,
|
88
|
+
f"The provided schema uses Open API {version}, which is currently not supported.",
|
89
|
+
)
|
90
|
+
instance = OpenApi30(schema)
|
91
|
+
else:
|
92
|
+
raise LoaderError(
|
93
|
+
LoaderErrorKind.OPEN_API_UNSPECIFIED_VERSION,
|
94
|
+
"Unable to determine the Open API version as it's not specified in the document.",
|
95
|
+
)
|
96
|
+
dispatch("after_load_schema", hook_context, instance)
|
97
|
+
return instance
|
98
|
+
|
99
|
+
|
100
|
+
class ContentType(enum.Enum):
|
101
|
+
"""Known content types for schema files."""
|
102
|
+
|
103
|
+
JSON = enum.auto()
|
104
|
+
YAML = enum.auto()
|
105
|
+
UNKNOWN = enum.auto()
|
106
|
+
|
107
|
+
|
108
|
+
def detect_content_type(*, headers: Mapping[str, str] | None = None, path: str | None = None) -> ContentType:
|
109
|
+
"""Detect content type from various sources."""
|
110
|
+
if headers is not None and (content_type := _detect_from_headers(headers)) != ContentType.UNKNOWN:
|
111
|
+
return content_type
|
112
|
+
if path is not None and (content_type := _detect_from_path(path)) != ContentType.UNKNOWN:
|
113
|
+
return content_type
|
114
|
+
return ContentType.UNKNOWN
|
115
|
+
|
116
|
+
|
117
|
+
def _detect_from_headers(headers: Mapping[str, str]) -> ContentType:
|
118
|
+
"""Detect content type from HTTP headers."""
|
119
|
+
content_type = headers.get("Content-Type", "").lower()
|
120
|
+
try:
|
121
|
+
if content_type and media_types.is_json(content_type):
|
122
|
+
return ContentType.JSON
|
123
|
+
if content_type and media_types.is_yaml(content_type):
|
124
|
+
return ContentType.YAML
|
125
|
+
except ValueError:
|
126
|
+
pass
|
127
|
+
return ContentType.UNKNOWN
|
128
|
+
|
129
|
+
|
130
|
+
def _detect_from_path(path: str) -> ContentType:
|
131
|
+
"""Detect content type from file path."""
|
132
|
+
suffix = Path(path).suffix.lower()
|
133
|
+
if suffix == ".json":
|
134
|
+
return ContentType.JSON
|
135
|
+
if suffix in (".yaml", ".yml"):
|
136
|
+
return ContentType.YAML
|
137
|
+
return ContentType.UNKNOWN
|
138
|
+
|
139
|
+
|
140
|
+
def load_content(content: str, content_type: ContentType) -> dict[str, Any]:
|
141
|
+
"""Load content using appropriate parser."""
|
142
|
+
if content_type == ContentType.JSON:
|
143
|
+
return _load_json(content)
|
144
|
+
if content_type == ContentType.YAML:
|
145
|
+
return _load_yaml(content)
|
146
|
+
# If type is unknown, try JSON first, then YAML
|
147
|
+
try:
|
148
|
+
return _load_json(content)
|
149
|
+
except json.JSONDecodeError:
|
150
|
+
return _load_yaml(content)
|
151
|
+
|
152
|
+
|
153
|
+
def _load_json(content: str) -> dict[str, Any]:
|
154
|
+
try:
|
155
|
+
return json.loads(content)
|
156
|
+
except json.JSONDecodeError as exc:
|
157
|
+
raise LoaderError(
|
158
|
+
LoaderErrorKind.SYNTAX_ERROR,
|
159
|
+
SCHEMA_SYNTAX_ERROR,
|
160
|
+
extras=[entry for entry in str(exc).splitlines() if entry],
|
161
|
+
) from exc
|
162
|
+
|
163
|
+
|
164
|
+
def _load_yaml(content: str) -> dict[str, Any]:
|
165
|
+
import yaml
|
166
|
+
|
167
|
+
try:
|
168
|
+
return deserialize_yaml(content)
|
169
|
+
except yaml.YAMLError as exc:
|
170
|
+
kind = LoaderErrorKind.SYNTAX_ERROR
|
171
|
+
message = SCHEMA_SYNTAX_ERROR
|
172
|
+
extras = [entry for entry in str(exc).splitlines() if entry]
|
173
|
+
raise LoaderError(kind, message, extras=extras) from exc
|
174
|
+
|
175
|
+
|
176
|
+
SCHEMA_INVALID_ERROR = "The provided API schema does not appear to be a valid OpenAPI schema"
|
177
|
+
SCHEMA_SYNTAX_ERROR = "API schema does not appear syntactically valid"
|
178
|
+
OPENAPI_VERSION_RE = re.compile(r"^3\.[01]\.[0-9](-.+)?$")
|