schemathesis 3.19.7__py3-none-any.whl → 3.20.1__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/_compat.py +3 -2
- schemathesis/_hypothesis.py +21 -6
- schemathesis/_xml.py +177 -0
- schemathesis/auths.py +48 -10
- schemathesis/cli/__init__.py +77 -19
- schemathesis/cli/callbacks.py +42 -18
- schemathesis/cli/context.py +2 -1
- schemathesis/cli/output/default.py +102 -34
- schemathesis/cli/sanitization.py +15 -0
- schemathesis/code_samples.py +141 -0
- schemathesis/constants.py +1 -24
- schemathesis/exceptions.py +127 -26
- schemathesis/experimental/__init__.py +85 -0
- schemathesis/extra/pytest_plugin.py +10 -4
- schemathesis/fixups/__init__.py +8 -2
- schemathesis/fixups/fast_api.py +11 -1
- schemathesis/fixups/utf8_bom.py +7 -1
- schemathesis/hooks.py +63 -0
- schemathesis/lazy.py +10 -4
- schemathesis/loaders.py +57 -0
- schemathesis/models.py +120 -96
- schemathesis/parameters.py +3 -0
- schemathesis/runner/__init__.py +3 -0
- schemathesis/runner/events.py +55 -20
- schemathesis/runner/impl/core.py +54 -54
- schemathesis/runner/serialization.py +75 -34
- schemathesis/sanitization.py +248 -0
- schemathesis/schemas.py +21 -6
- schemathesis/serializers.py +32 -3
- schemathesis/service/serialization.py +5 -1
- schemathesis/specs/graphql/loaders.py +44 -13
- schemathesis/specs/graphql/schemas.py +56 -25
- schemathesis/specs/openapi/_hypothesis.py +11 -23
- schemathesis/specs/openapi/definitions.py +572 -0
- schemathesis/specs/openapi/loaders.py +100 -49
- schemathesis/specs/openapi/parameters.py +2 -2
- schemathesis/specs/openapi/schemas.py +87 -13
- schemathesis/specs/openapi/security.py +1 -0
- schemathesis/stateful.py +2 -2
- schemathesis/utils.py +30 -9
- schemathesis-3.20.1.dist-info/METADATA +342 -0
- {schemathesis-3.19.7.dist-info → schemathesis-3.20.1.dist-info}/RECORD +45 -39
- schemathesis-3.19.7.dist-info/METADATA +0 -291
- {schemathesis-3.19.7.dist-info → schemathesis-3.20.1.dist-info}/WHEEL +0 -0
- {schemathesis-3.19.7.dist-info → schemathesis-3.20.1.dist-info}/entry_points.txt +0 -0
- {schemathesis-3.19.7.dist-info → schemathesis-3.20.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import io
|
|
2
2
|
import json
|
|
3
3
|
import pathlib
|
|
4
|
+
import re
|
|
4
5
|
from typing import IO, Any, Callable, Dict, List, Optional, Tuple, Union, cast
|
|
5
6
|
from urllib.parse import urljoin
|
|
6
7
|
|
|
@@ -15,10 +16,13 @@ from starlette_testclient import TestClient as ASGIClient
|
|
|
15
16
|
from werkzeug.test import Client
|
|
16
17
|
from yarl import URL
|
|
17
18
|
|
|
18
|
-
from ...
|
|
19
|
-
from ...
|
|
19
|
+
from ... import experimental, fixups
|
|
20
|
+
from ...code_samples import CodeSampleStyle
|
|
21
|
+
from ...constants import DEFAULT_DATA_GENERATION_METHODS, WAIT_FOR_SCHEMA_INTERVAL
|
|
22
|
+
from ...exceptions import SchemaError, SchemaErrorType
|
|
20
23
|
from ...hooks import HookContext, dispatch
|
|
21
24
|
from ...lazy import LazySchema
|
|
25
|
+
from ...loaders import load_schema_from_url
|
|
22
26
|
from ...throttling import build_limiter
|
|
23
27
|
from ...types import DataGenerationMethodInput, Filter, NotSet, PathLike
|
|
24
28
|
from ...utils import (
|
|
@@ -65,6 +69,7 @@ def from_path(
|
|
|
65
69
|
code_sample_style: str = CodeSampleStyle.default().name,
|
|
66
70
|
rate_limit: Optional[str] = None,
|
|
67
71
|
encoding: str = "utf8",
|
|
72
|
+
sanitize_output: bool = True,
|
|
68
73
|
) -> BaseOpenAPISchema:
|
|
69
74
|
"""Load Open API schema via a file from an OS path.
|
|
70
75
|
|
|
@@ -87,6 +92,7 @@ def from_path(
|
|
|
87
92
|
code_sample_style=code_sample_style,
|
|
88
93
|
location=pathlib.Path(path).absolute().as_uri(),
|
|
89
94
|
rate_limit=rate_limit,
|
|
95
|
+
sanitize_output=sanitize_output,
|
|
90
96
|
__expects_json=_is_json_path(path),
|
|
91
97
|
)
|
|
92
98
|
|
|
@@ -108,6 +114,7 @@ def from_uri(
|
|
|
108
114
|
code_sample_style: str = CodeSampleStyle.default().name,
|
|
109
115
|
wait_for_schema: Optional[float] = None,
|
|
110
116
|
rate_limit: Optional[str] = None,
|
|
117
|
+
sanitize_output: bool = True,
|
|
111
118
|
**kwargs: Any,
|
|
112
119
|
) -> BaseOpenAPISchema:
|
|
113
120
|
"""Load Open API schema from the network.
|
|
@@ -134,44 +141,35 @@ def from_uri(
|
|
|
134
141
|
else:
|
|
135
142
|
_load_schema = requests.get
|
|
136
143
|
|
|
137
|
-
response = _load_schema(uri, **kwargs)
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
)
|
|
157
|
-
except SchemaLoadingError as exc:
|
|
158
|
-
content_type = response.headers.get("Content-Type")
|
|
159
|
-
if content_type is not None:
|
|
160
|
-
raise SchemaLoadingError(f"{exc.args[0]}. The actual response has `{content_type}` Content-Type") from exc
|
|
161
|
-
raise
|
|
144
|
+
response = load_schema_from_url(lambda: _load_schema(uri, **kwargs))
|
|
145
|
+
return from_file(
|
|
146
|
+
response.text,
|
|
147
|
+
app=app,
|
|
148
|
+
base_url=base_url,
|
|
149
|
+
method=method,
|
|
150
|
+
endpoint=endpoint,
|
|
151
|
+
tag=tag,
|
|
152
|
+
operation_id=operation_id,
|
|
153
|
+
skip_deprecated_operations=skip_deprecated_operations,
|
|
154
|
+
validate_schema=validate_schema,
|
|
155
|
+
force_schema_version=force_schema_version,
|
|
156
|
+
data_generation_methods=data_generation_methods,
|
|
157
|
+
code_sample_style=code_sample_style,
|
|
158
|
+
location=uri,
|
|
159
|
+
rate_limit=rate_limit,
|
|
160
|
+
sanitize_output=sanitize_output,
|
|
161
|
+
__expects_json=_is_json_response(response),
|
|
162
|
+
)
|
|
162
163
|
|
|
163
164
|
|
|
164
|
-
SCHEMA_LOADING_ERROR =
|
|
165
|
-
"It seems like the schema you are trying to load is malformed. "
|
|
166
|
-
"Schemathesis expects API schemas in JSON or YAML formats"
|
|
167
|
-
)
|
|
165
|
+
SCHEMA_LOADING_ERROR = "Received unsupported content while expecting a JSON or YAML payload for Open API"
|
|
168
166
|
|
|
169
167
|
|
|
170
168
|
def _load_yaml(data: str) -> Dict[str, Any]:
|
|
171
169
|
try:
|
|
172
170
|
return yaml.load(data, StringDatesYAMLLoader)
|
|
173
171
|
except yaml.YAMLError as exc:
|
|
174
|
-
raise
|
|
172
|
+
raise SchemaError(SchemaErrorType.UNEXPECTED_CONTENT_TYPE, SCHEMA_LOADING_ERROR) from exc
|
|
175
173
|
|
|
176
174
|
|
|
177
175
|
def from_file(
|
|
@@ -190,6 +188,7 @@ def from_file(
|
|
|
190
188
|
code_sample_style: str = CodeSampleStyle.default().name,
|
|
191
189
|
location: Optional[str] = None,
|
|
192
190
|
rate_limit: Optional[str] = None,
|
|
191
|
+
sanitize_output: bool = True,
|
|
193
192
|
__expects_json: bool = False,
|
|
194
193
|
**kwargs: Any, # needed in the runner to have compatible API across all loaders
|
|
195
194
|
) -> BaseOpenAPISchema:
|
|
@@ -226,9 +225,17 @@ def from_file(
|
|
|
226
225
|
code_sample_style=code_sample_style,
|
|
227
226
|
location=location,
|
|
228
227
|
rate_limit=rate_limit,
|
|
228
|
+
sanitize_output=sanitize_output,
|
|
229
229
|
)
|
|
230
230
|
|
|
231
231
|
|
|
232
|
+
def _is_fast_api(app: Any) -> bool:
|
|
233
|
+
for cls in app.__class__.__mro__:
|
|
234
|
+
if f"{cls.__module__}.{cls.__qualname__}" == "fastapi.applications.FastAPI":
|
|
235
|
+
return True
|
|
236
|
+
return False
|
|
237
|
+
|
|
238
|
+
|
|
232
239
|
def from_dict(
|
|
233
240
|
raw_schema: Dict[str, Any],
|
|
234
241
|
*,
|
|
@@ -245,6 +252,7 @@ def from_dict(
|
|
|
245
252
|
code_sample_style: str = CodeSampleStyle.default().name,
|
|
246
253
|
location: Optional[str] = None,
|
|
247
254
|
rate_limit: Optional[str] = None,
|
|
255
|
+
sanitize_output: bool = True,
|
|
248
256
|
) -> BaseOpenAPISchema:
|
|
249
257
|
"""Load Open API schema from a Python dictionary.
|
|
250
258
|
|
|
@@ -252,6 +260,12 @@ def from_dict(
|
|
|
252
260
|
"""
|
|
253
261
|
_code_sample_style = CodeSampleStyle.from_str(code_sample_style)
|
|
254
262
|
hook_context = HookContext()
|
|
263
|
+
is_openapi_31 = raw_schema.get("openapi", "").startswith("3.1")
|
|
264
|
+
is_fast_api_fixup_installed = fixups.is_installed("fast_api")
|
|
265
|
+
if is_fast_api_fixup_installed and is_openapi_31:
|
|
266
|
+
fixups.fast_api.uninstall()
|
|
267
|
+
elif _is_fast_api(app):
|
|
268
|
+
fixups.fast_api.adjust_schema(raw_schema)
|
|
255
269
|
dispatch("before_load_schema", hook_context, raw_schema)
|
|
256
270
|
rate_limiter: Optional[Limiter] = None
|
|
257
271
|
if rate_limit is not None:
|
|
@@ -273,12 +287,27 @@ def from_dict(
|
|
|
273
287
|
code_sample_style=_code_sample_style,
|
|
274
288
|
location=location,
|
|
275
289
|
rate_limiter=rate_limiter,
|
|
290
|
+
sanitize_output=sanitize_output,
|
|
276
291
|
)
|
|
277
292
|
dispatch("after_load_schema", hook_context, instance)
|
|
278
293
|
return instance
|
|
279
294
|
|
|
280
|
-
def init_openapi_3() -> OpenApi30:
|
|
281
|
-
|
|
295
|
+
def init_openapi_3(forced: bool) -> OpenApi30:
|
|
296
|
+
version = raw_schema["openapi"]
|
|
297
|
+
if (
|
|
298
|
+
not (is_openapi_31 and experimental.OPEN_API_3_1.is_enabled)
|
|
299
|
+
and not forced
|
|
300
|
+
and not OPENAPI_30_VERSION_RE.match(version)
|
|
301
|
+
):
|
|
302
|
+
raise SchemaError(
|
|
303
|
+
SchemaErrorType.OPEN_API_UNSUPPORTED_VERSION,
|
|
304
|
+
f"The provided schema uses Open API {version}, which is currently not supported.",
|
|
305
|
+
)
|
|
306
|
+
if is_openapi_31:
|
|
307
|
+
validator = definitions.OPENAPI_31_VALIDATOR
|
|
308
|
+
else:
|
|
309
|
+
validator = definitions.OPENAPI_30_VALIDATOR
|
|
310
|
+
_maybe_validate_schema(raw_schema, validator, validate_schema)
|
|
282
311
|
instance = OpenApi30(
|
|
283
312
|
raw_schema,
|
|
284
313
|
app=app,
|
|
@@ -293,6 +322,7 @@ def from_dict(
|
|
|
293
322
|
code_sample_style=_code_sample_style,
|
|
294
323
|
location=location,
|
|
295
324
|
rate_limiter=rate_limiter,
|
|
325
|
+
sanitize_output=sanitize_output,
|
|
296
326
|
)
|
|
297
327
|
dispatch("after_load_schema", hook_context, instance)
|
|
298
328
|
return instance
|
|
@@ -300,22 +330,31 @@ def from_dict(
|
|
|
300
330
|
if force_schema_version == "20":
|
|
301
331
|
return init_openapi_2()
|
|
302
332
|
if force_schema_version == "30":
|
|
303
|
-
return init_openapi_3()
|
|
333
|
+
return init_openapi_3(forced=True)
|
|
304
334
|
if "swagger" in raw_schema:
|
|
305
335
|
return init_openapi_2()
|
|
306
336
|
if "openapi" in raw_schema:
|
|
307
|
-
return init_openapi_3()
|
|
308
|
-
raise
|
|
337
|
+
return init_openapi_3(forced=False)
|
|
338
|
+
raise SchemaError(
|
|
339
|
+
SchemaErrorType.OPEN_API_UNSPECIFIED_VERSION,
|
|
340
|
+
"Unable to determine the Open API version as it's not specified in the document.",
|
|
341
|
+
)
|
|
342
|
+
|
|
309
343
|
|
|
344
|
+
OPENAPI_30_VERSION_RE = re.compile(r"^3\.0\.\d(-.+)?$")
|
|
310
345
|
|
|
311
346
|
# It is a common case when API schemas are stored in the YAML format and HTTP status codes are numbers
|
|
312
347
|
# The Open API spec requires HTTP status codes as strings
|
|
313
348
|
DOC_ENTRY = "https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#patterned-fields-1"
|
|
314
|
-
NUMERIC_STATUS_CODES_MESSAGE = f"""
|
|
315
|
-
|
|
316
|
-
{DOC_ENTRY}
|
|
349
|
+
NUMERIC_STATUS_CODES_MESSAGE = f"""Numeric HTTP status codes detected in your YAML schema.
|
|
350
|
+
According to the Open API specification, status codes must be strings, not numbers.
|
|
351
|
+
For more details, check the Open API documentation: {DOC_ENTRY}
|
|
352
|
+
|
|
317
353
|
Please, stringify the following status codes:"""
|
|
318
|
-
|
|
354
|
+
NON_STRING_OBJECT_KEY_MESSAGE = (
|
|
355
|
+
"The Open API specification requires all keys in the schema to be strings. "
|
|
356
|
+
"You have some keys that are not strings."
|
|
357
|
+
)
|
|
319
358
|
|
|
320
359
|
|
|
321
360
|
def _format_status_codes(status_codes: List[Tuple[int, List[Union[str, int]]]]) -> str:
|
|
@@ -339,12 +378,18 @@ def _maybe_validate_schema(
|
|
|
339
378
|
status_codes = validation.find_numeric_http_status_codes(instance)
|
|
340
379
|
if status_codes:
|
|
341
380
|
message = _format_status_codes(status_codes)
|
|
342
|
-
raise
|
|
381
|
+
raise SchemaError(
|
|
382
|
+
SchemaErrorType.YAML_NUMERIC_STATUS_CODES, f"{NUMERIC_STATUS_CODES_MESSAGE}\n{message}"
|
|
383
|
+
) from exc
|
|
343
384
|
# Some other pattern error
|
|
344
|
-
raise
|
|
345
|
-
raise
|
|
385
|
+
raise SchemaError(SchemaErrorType.YAML_NON_STRING_KEYS, NON_STRING_OBJECT_KEY_MESSAGE) from exc
|
|
386
|
+
raise SchemaError(SchemaErrorType.UNCLASSIFIED, "Unknown error") from exc
|
|
346
387
|
except ValidationError as exc:
|
|
347
|
-
raise
|
|
388
|
+
raise SchemaError(
|
|
389
|
+
SchemaErrorType.OPEN_API_INVALID_SCHEMA,
|
|
390
|
+
"The provided API schema does not appear to be a valid OpenAPI schema",
|
|
391
|
+
extras=[entry for entry in str(exc).splitlines() if entry],
|
|
392
|
+
) from exc
|
|
348
393
|
|
|
349
394
|
|
|
350
395
|
def from_pytest_fixture(
|
|
@@ -361,6 +406,7 @@ def from_pytest_fixture(
|
|
|
361
406
|
data_generation_methods: Union[DataGenerationMethodInput, NotSet] = NOT_SET,
|
|
362
407
|
code_sample_style: str = CodeSampleStyle.default().name,
|
|
363
408
|
rate_limit: Optional[str] = None,
|
|
409
|
+
sanitize_output: bool = True,
|
|
364
410
|
) -> LazySchema:
|
|
365
411
|
"""Load schema from a ``pytest`` fixture.
|
|
366
412
|
|
|
@@ -394,6 +440,7 @@ def from_pytest_fixture(
|
|
|
394
440
|
data_generation_methods=_data_generation_methods,
|
|
395
441
|
code_sample_style=_code_sample_style,
|
|
396
442
|
rate_limiter=rate_limiter,
|
|
443
|
+
sanitize_output=sanitize_output,
|
|
397
444
|
)
|
|
398
445
|
|
|
399
446
|
|
|
@@ -412,6 +459,7 @@ def from_wsgi(
|
|
|
412
459
|
data_generation_methods: DataGenerationMethodInput = DEFAULT_DATA_GENERATION_METHODS,
|
|
413
460
|
code_sample_style: str = CodeSampleStyle.default().name,
|
|
414
461
|
rate_limit: Optional[str] = None,
|
|
462
|
+
sanitize_output: bool = True,
|
|
415
463
|
**kwargs: Any,
|
|
416
464
|
) -> BaseOpenAPISchema:
|
|
417
465
|
"""Load Open API schema from a WSGI app.
|
|
@@ -422,8 +470,7 @@ def from_wsgi(
|
|
|
422
470
|
require_relative_url(schema_path)
|
|
423
471
|
setup_headers(kwargs)
|
|
424
472
|
client = Client(app, WSGIResponse)
|
|
425
|
-
response = client.get(schema_path, **kwargs)
|
|
426
|
-
HTTPError.check_response(response, schema_path)
|
|
473
|
+
response = load_schema_from_url(lambda: client.get(schema_path, **kwargs))
|
|
427
474
|
return from_file(
|
|
428
475
|
response.data,
|
|
429
476
|
app=app,
|
|
@@ -439,6 +486,7 @@ def from_wsgi(
|
|
|
439
486
|
code_sample_style=code_sample_style,
|
|
440
487
|
location=schema_path,
|
|
441
488
|
rate_limit=rate_limit,
|
|
489
|
+
sanitize_output=sanitize_output,
|
|
442
490
|
__expects_json=_is_json_response(response),
|
|
443
491
|
)
|
|
444
492
|
|
|
@@ -466,6 +514,7 @@ def from_aiohttp(
|
|
|
466
514
|
data_generation_methods: DataGenerationMethodInput = DEFAULT_DATA_GENERATION_METHODS,
|
|
467
515
|
code_sample_style: str = CodeSampleStyle.default().name,
|
|
468
516
|
rate_limit: Optional[str] = None,
|
|
517
|
+
sanitize_output: bool = True,
|
|
469
518
|
**kwargs: Any,
|
|
470
519
|
) -> BaseOpenAPISchema:
|
|
471
520
|
"""Load Open API schema from an AioHTTP app.
|
|
@@ -491,6 +540,7 @@ def from_aiohttp(
|
|
|
491
540
|
data_generation_methods=data_generation_methods,
|
|
492
541
|
code_sample_style=code_sample_style,
|
|
493
542
|
rate_limit=rate_limit,
|
|
543
|
+
sanitize_output=sanitize_output,
|
|
494
544
|
**kwargs,
|
|
495
545
|
)
|
|
496
546
|
|
|
@@ -510,6 +560,7 @@ def from_asgi(
|
|
|
510
560
|
data_generation_methods: DataGenerationMethodInput = DEFAULT_DATA_GENERATION_METHODS,
|
|
511
561
|
code_sample_style: str = CodeSampleStyle.default().name,
|
|
512
562
|
rate_limit: Optional[str] = None,
|
|
563
|
+
sanitize_output: bool = True,
|
|
513
564
|
**kwargs: Any,
|
|
514
565
|
) -> BaseOpenAPISchema:
|
|
515
566
|
"""Load Open API schema from an ASGI app.
|
|
@@ -520,8 +571,7 @@ def from_asgi(
|
|
|
520
571
|
require_relative_url(schema_path)
|
|
521
572
|
setup_headers(kwargs)
|
|
522
573
|
client = ASGIClient(app)
|
|
523
|
-
response = client.get(schema_path, **kwargs)
|
|
524
|
-
HTTPError.check_response(response, schema_path)
|
|
574
|
+
response = load_schema_from_url(lambda: client.get(schema_path, **kwargs))
|
|
525
575
|
return from_file(
|
|
526
576
|
response.text,
|
|
527
577
|
app=app,
|
|
@@ -537,5 +587,6 @@ def from_asgi(
|
|
|
537
587
|
code_sample_style=code_sample_style,
|
|
538
588
|
location=schema_path,
|
|
539
589
|
rate_limit=rate_limit,
|
|
590
|
+
sanitize_output=sanitize_output,
|
|
540
591
|
__expects_json=_is_json_response(response),
|
|
541
592
|
)
|
|
@@ -2,7 +2,7 @@ import json
|
|
|
2
2
|
from dataclasses import dataclass
|
|
3
3
|
from typing import Any, ClassVar, Dict, Iterable, List, Optional, Tuple
|
|
4
4
|
|
|
5
|
-
from ...exceptions import
|
|
5
|
+
from ...exceptions import OperationSchemaError
|
|
6
6
|
from ...models import APIOperation
|
|
7
7
|
from ...parameters import Parameter
|
|
8
8
|
from .converter import to_json_schema_recursive
|
|
@@ -412,7 +412,7 @@ def get_parameter_schema(operation: APIOperation, data: Dict[str, Any]) -> Dict[
|
|
|
412
412
|
try:
|
|
413
413
|
content = data["content"]
|
|
414
414
|
except KeyError as exc:
|
|
415
|
-
raise
|
|
415
|
+
raise OperationSchemaError(
|
|
416
416
|
MISSING_SCHEMA_OR_CONTENT_MESSAGE.format(location=data.get("in", ""), name=data.get("name", "<UNKNOWN>")),
|
|
417
417
|
path=operation.path,
|
|
418
418
|
method=operation.method,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import itertools
|
|
2
2
|
import json
|
|
3
3
|
from collections import defaultdict
|
|
4
|
-
from contextlib import ExitStack, contextmanager
|
|
4
|
+
from contextlib import ExitStack, contextmanager, suppress
|
|
5
5
|
from dataclasses import dataclass, field
|
|
6
6
|
from difflib import get_close_matches
|
|
7
7
|
from hashlib import sha1
|
|
@@ -15,6 +15,7 @@ from typing import (
|
|
|
15
15
|
Generator,
|
|
16
16
|
Iterable,
|
|
17
17
|
List,
|
|
18
|
+
NoReturn,
|
|
18
19
|
Optional,
|
|
19
20
|
Sequence,
|
|
20
21
|
Tuple,
|
|
@@ -29,17 +30,17 @@ import requests
|
|
|
29
30
|
from hypothesis.strategies import SearchStrategy
|
|
30
31
|
from requests.structures import CaseInsensitiveDict
|
|
31
32
|
|
|
32
|
-
from ... import failures
|
|
33
|
+
from ... import experimental, failures
|
|
33
34
|
from ...auths import AuthStorage
|
|
34
35
|
from ...constants import HTTP_METHODS, DataGenerationMethod
|
|
35
36
|
from ...exceptions import (
|
|
36
|
-
|
|
37
|
+
OperationSchemaError,
|
|
37
38
|
UsageError,
|
|
38
39
|
get_missing_content_type_error,
|
|
39
40
|
get_response_parsing_error,
|
|
40
41
|
get_schema_validation_error,
|
|
41
42
|
)
|
|
42
|
-
from ...hooks import HookContext, HookDispatcher
|
|
43
|
+
from ...hooks import GLOBAL_HOOK_DISPATCHER, HookContext, HookDispatcher, should_skip_operation
|
|
43
44
|
from ...models import APIOperation, Case, OperationDefinition
|
|
44
45
|
from ...schemas import BaseSchema
|
|
45
46
|
from ...stateful import APIStateMachine, Stateful, StatefulTest
|
|
@@ -58,6 +59,7 @@ from ...utils import (
|
|
|
58
59
|
from . import links, serialization
|
|
59
60
|
from ._hypothesis import get_case_strategy
|
|
60
61
|
from .converter import to_json_schema, to_json_schema_recursive
|
|
62
|
+
from .definitions import OPENAPI_30_VALIDATOR, OPENAPI_31_VALIDATOR, SWAGGER_20_VALIDATOR
|
|
61
63
|
from .examples import get_strategies_from_examples
|
|
62
64
|
from .filters import (
|
|
63
65
|
should_skip_by_operation_id,
|
|
@@ -78,7 +80,7 @@ from .references import RECURSION_DEPTH_LIMIT, ConvertingResolver, InliningResol
|
|
|
78
80
|
from .security import BaseSecurityProcessor, OpenAPISecurityProcessor, SwaggerSecurityProcessor
|
|
79
81
|
from .stateful import create_state_machine
|
|
80
82
|
|
|
81
|
-
SCHEMA_ERROR_MESSAGE = "
|
|
83
|
+
SCHEMA_ERROR_MESSAGE = "Ensure that the definition complies with the OpenAPI specification"
|
|
82
84
|
SCHEMA_PARSING_ERRORS = (KeyError, AttributeError, jsonschema.exceptions.RefResolutionError)
|
|
83
85
|
|
|
84
86
|
|
|
@@ -146,7 +148,9 @@ class BaseOpenAPISchema(BaseSchema):
|
|
|
146
148
|
continue
|
|
147
149
|
return total
|
|
148
150
|
|
|
149
|
-
def get_all_operations(
|
|
151
|
+
def get_all_operations(
|
|
152
|
+
self, hooks: Optional[HookDispatcher] = None
|
|
153
|
+
) -> Generator[Result[APIOperation, OperationSchemaError], None, None]:
|
|
150
154
|
"""Iterate over all operations defined in the API.
|
|
151
155
|
|
|
152
156
|
Each yielded item is either `Ok` or `Err`, depending on the presence of errors during schema processing.
|
|
@@ -162,11 +166,12 @@ class BaseOpenAPISchema(BaseSchema):
|
|
|
162
166
|
In both cases, Schemathesis lets the callee decide what to do with these variants. It allows it to test valid
|
|
163
167
|
operations and show errors for invalid ones.
|
|
164
168
|
"""
|
|
169
|
+
__tracebackhide__ = True
|
|
165
170
|
try:
|
|
166
171
|
paths = self.raw_schema["paths"]
|
|
167
172
|
except KeyError as exc:
|
|
168
173
|
# Missing `paths` is not recoverable
|
|
169
|
-
|
|
174
|
+
self._raise_invalid_schema(exc)
|
|
170
175
|
|
|
171
176
|
context = HookContext()
|
|
172
177
|
for path, methods in paths.items():
|
|
@@ -199,19 +204,55 @@ class BaseOpenAPISchema(BaseSchema):
|
|
|
199
204
|
raw_definition = OperationDefinition(
|
|
200
205
|
raw_methods[method], resolved_definition, scope, parameters
|
|
201
206
|
)
|
|
202
|
-
|
|
207
|
+
operation = self.make_operation(path, method, parameters, raw_definition)
|
|
208
|
+
context = HookContext(operation=operation)
|
|
209
|
+
if (
|
|
210
|
+
should_skip_operation(GLOBAL_HOOK_DISPATCHER, context)
|
|
211
|
+
or should_skip_operation(self.hooks, context)
|
|
212
|
+
or (hooks and should_skip_operation(hooks, context))
|
|
213
|
+
):
|
|
214
|
+
continue
|
|
215
|
+
yield Ok(operation)
|
|
203
216
|
except SCHEMA_PARSING_ERRORS as exc:
|
|
204
217
|
yield self._into_err(exc, path, method)
|
|
205
218
|
except SCHEMA_PARSING_ERRORS as exc:
|
|
206
219
|
yield self._into_err(exc, path, method)
|
|
207
220
|
|
|
208
|
-
def _into_err(self, error: Exception, path: Optional[str], method: Optional[str]) -> Err[
|
|
221
|
+
def _into_err(self, error: Exception, path: Optional[str], method: Optional[str]) -> Err[OperationSchemaError]:
|
|
222
|
+
__tracebackhide__ = True
|
|
209
223
|
try:
|
|
210
224
|
full_path = self.get_full_path(path) if isinstance(path, str) else None
|
|
211
|
-
|
|
212
|
-
except
|
|
225
|
+
self._raise_invalid_schema(error, full_path, path, method)
|
|
226
|
+
except OperationSchemaError as exc:
|
|
213
227
|
return Err(exc)
|
|
214
228
|
|
|
229
|
+
def _raise_invalid_schema(
|
|
230
|
+
self,
|
|
231
|
+
error: Exception,
|
|
232
|
+
full_path: Optional[str] = None,
|
|
233
|
+
path: Optional[str] = None,
|
|
234
|
+
method: Optional[str] = None,
|
|
235
|
+
) -> NoReturn:
|
|
236
|
+
__tracebackhide__ = True
|
|
237
|
+
if isinstance(error, jsonschema.exceptions.RefResolutionError):
|
|
238
|
+
raise OperationSchemaError.from_reference_resolution_error(
|
|
239
|
+
error, path=path, method=method, full_path=full_path
|
|
240
|
+
) from None
|
|
241
|
+
try:
|
|
242
|
+
self.validate()
|
|
243
|
+
except jsonschema.ValidationError as exc:
|
|
244
|
+
raise OperationSchemaError.from_jsonschema_error(
|
|
245
|
+
exc, path=path, method=method, full_path=full_path
|
|
246
|
+
) from None
|
|
247
|
+
raise OperationSchemaError(SCHEMA_ERROR_MESSAGE, path=path, method=method, full_path=full_path) from error
|
|
248
|
+
|
|
249
|
+
def validate(self) -> None:
|
|
250
|
+
with suppress(TypeError):
|
|
251
|
+
self._validate()
|
|
252
|
+
|
|
253
|
+
def _validate(self) -> None:
|
|
254
|
+
raise NotImplementedError
|
|
255
|
+
|
|
215
256
|
def collect_parameters(
|
|
216
257
|
self, parameters: Iterable[Dict[str, Any]], definition: Dict[str, Any]
|
|
217
258
|
) -> List[OpenAPIParameter]:
|
|
@@ -238,6 +279,7 @@ class BaseOpenAPISchema(BaseSchema):
|
|
|
238
279
|
raw_definition: OperationDefinition,
|
|
239
280
|
) -> APIOperation:
|
|
240
281
|
"""Create JSON schemas for the query, body, etc from Swagger parameters definitions."""
|
|
282
|
+
__tracebackhide__ = True
|
|
241
283
|
base_url = self.get_base_url()
|
|
242
284
|
operation: APIOperation[OpenAPIParameter, Case] = APIOperation(
|
|
243
285
|
path=path,
|
|
@@ -353,7 +395,9 @@ class BaseOpenAPISchema(BaseSchema):
|
|
|
353
395
|
responses = operation.definition.resolved["responses"]
|
|
354
396
|
except KeyError as exc:
|
|
355
397
|
# Possible to get if `validate_schema=False` is passed during schema creation
|
|
356
|
-
|
|
398
|
+
path = operation.path
|
|
399
|
+
full_path = self.get_full_path(path) if isinstance(path, str) else None
|
|
400
|
+
self._raise_invalid_schema(exc, full_path, path, operation.method)
|
|
357
401
|
status_code = str(response.status_code)
|
|
358
402
|
if status_code in responses:
|
|
359
403
|
return responses[status_code]
|
|
@@ -494,9 +538,13 @@ class BaseOpenAPISchema(BaseSchema):
|
|
|
494
538
|
resolver = ConvertingResolver(
|
|
495
539
|
self.location or "", self.raw_schema, nullable_name=self.nullable_name, is_response_schema=True
|
|
496
540
|
)
|
|
541
|
+
if self.spec_version.startswith("3.1") and experimental.OPEN_API_3_1.is_enabled:
|
|
542
|
+
cls = jsonschema.Draft202012Validator
|
|
543
|
+
else:
|
|
544
|
+
cls = jsonschema.Draft4Validator
|
|
497
545
|
with in_scopes(resolver, scopes):
|
|
498
546
|
try:
|
|
499
|
-
jsonschema.validate(data, schema, cls=
|
|
547
|
+
jsonschema.validate(data, schema, cls=cls, resolver=resolver)
|
|
500
548
|
except jsonschema.ValidationError as exc:
|
|
501
549
|
exc_class = get_schema_validation_error(exc)
|
|
502
550
|
raise exc_class(
|
|
@@ -656,6 +704,9 @@ class SwaggerV20(BaseOpenAPISchema):
|
|
|
656
704
|
def verbose_name(self) -> str:
|
|
657
705
|
return f"Swagger {self.spec_version}"
|
|
658
706
|
|
|
707
|
+
def _validate(self) -> None:
|
|
708
|
+
SWAGGER_20_VALIDATOR.validate(self.raw_schema)
|
|
709
|
+
|
|
659
710
|
def _get_base_path(self) -> str:
|
|
660
711
|
return self.raw_schema.get("basePath", "/")
|
|
661
712
|
|
|
@@ -806,6 +857,12 @@ class SwaggerV20(BaseOpenAPISchema):
|
|
|
806
857
|
consumes = global_consumes
|
|
807
858
|
return consumes
|
|
808
859
|
|
|
860
|
+
def _get_payload_schema(self, definition: Dict[str, Any], media_type: str) -> Optional[Dict[str, Any]]:
|
|
861
|
+
for parameter in definition.get("parameters", []):
|
|
862
|
+
if parameter["in"] == "body":
|
|
863
|
+
return parameter["schema"]
|
|
864
|
+
return None
|
|
865
|
+
|
|
809
866
|
|
|
810
867
|
class OpenApi30(SwaggerV20):
|
|
811
868
|
nullable_name = "nullable"
|
|
@@ -824,6 +881,13 @@ class OpenApi30(SwaggerV20):
|
|
|
824
881
|
def verbose_name(self) -> str:
|
|
825
882
|
return f"Open API {self.spec_version}"
|
|
826
883
|
|
|
884
|
+
def _validate(self) -> None:
|
|
885
|
+
if self.spec_version.startswith("3.1"):
|
|
886
|
+
# Currently we treat Open API 3.1 as 3.0 in some regard
|
|
887
|
+
OPENAPI_31_VALIDATOR.validate(self.raw_schema)
|
|
888
|
+
else:
|
|
889
|
+
OPENAPI_30_VALIDATOR.validate(self.raw_schema)
|
|
890
|
+
|
|
827
891
|
def _get_base_path(self) -> str:
|
|
828
892
|
servers = self.raw_schema.get("servers", [])
|
|
829
893
|
if servers:
|
|
@@ -898,3 +962,13 @@ class OpenApi30(SwaggerV20):
|
|
|
898
962
|
files.append((name, (None, form_data[name])))
|
|
899
963
|
# `None` is the default value for `files` and `data` arguments in `requests.request`
|
|
900
964
|
return files or None, None
|
|
965
|
+
|
|
966
|
+
def _get_payload_schema(self, definition: Dict[str, Any], media_type: str) -> Optional[Dict[str, Any]]:
|
|
967
|
+
if "requestBody" in definition:
|
|
968
|
+
if "$ref" in definition["requestBody"]:
|
|
969
|
+
body = self.resolver.resolve_all(definition["requestBody"], RECURSION_DEPTH_LIMIT)
|
|
970
|
+
else:
|
|
971
|
+
body = definition["requestBody"]
|
|
972
|
+
if "content" in body and media_type in body["content"]:
|
|
973
|
+
return body["content"][media_type]["schema"]
|
|
974
|
+
return None
|
|
@@ -16,6 +16,7 @@ class BaseSecurityProcessor:
|
|
|
16
16
|
|
|
17
17
|
def process_definitions(self, schema: Dict[str, Any], operation: APIOperation, resolver: RefResolver) -> None:
|
|
18
18
|
"""Add relevant security parameters to data generation."""
|
|
19
|
+
__tracebackhide__ = True
|
|
19
20
|
for definition in self._get_active_definitions(schema, operation, resolver):
|
|
20
21
|
name = definition.get("name")
|
|
21
22
|
location = definition.get("in")
|
schemathesis/stateful.py
CHANGED
|
@@ -11,7 +11,7 @@ from requests.structures import CaseInsensitiveDict
|
|
|
11
11
|
from starlette.applications import Starlette
|
|
12
12
|
|
|
13
13
|
from ._compat import IS_HYPOTHESIS_ABOVE_6_68_1
|
|
14
|
-
from .exceptions import
|
|
14
|
+
from .exceptions import OperationSchemaError
|
|
15
15
|
from .models import APIOperation, Case, CheckFunction
|
|
16
16
|
from .utils import NOT_SET, GenericResponse, Ok, Result
|
|
17
17
|
|
|
@@ -100,7 +100,7 @@ class Feedback:
|
|
|
100
100
|
settings: Optional[hypothesis.settings],
|
|
101
101
|
seed: Optional[int],
|
|
102
102
|
as_strategy_kwargs: Optional[Dict[str, Any]],
|
|
103
|
-
) -> Generator[Result[Tuple[APIOperation, Callable],
|
|
103
|
+
) -> Generator[Result[Tuple[APIOperation, Callable], OperationSchemaError], None, None]:
|
|
104
104
|
"""Generate additional tests that use data from the previous ones."""
|
|
105
105
|
from ._hypothesis import create_test
|
|
106
106
|
|