schemathesis 4.0.0a11__py3-none-any.whl → 4.0.0b1__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 +35 -27
- schemathesis/auths.py +85 -54
- schemathesis/checks.py +65 -36
- schemathesis/cli/commands/run/__init__.py +32 -27
- schemathesis/cli/commands/run/context.py +6 -1
- schemathesis/cli/commands/run/events.py +7 -1
- schemathesis/cli/commands/run/executor.py +12 -7
- schemathesis/cli/commands/run/handlers/output.py +188 -80
- schemathesis/cli/commands/run/validation.py +21 -6
- schemathesis/cli/constants.py +1 -1
- schemathesis/config/__init__.py +2 -1
- schemathesis/config/_generation.py +12 -13
- schemathesis/config/_operations.py +14 -0
- schemathesis/config/_phases.py +41 -5
- schemathesis/config/_projects.py +33 -1
- schemathesis/config/_report.py +6 -2
- schemathesis/config/_warnings.py +25 -0
- schemathesis/config/schema.json +49 -1
- schemathesis/core/errors.py +15 -19
- schemathesis/core/transport.py +117 -2
- schemathesis/engine/context.py +1 -0
- schemathesis/engine/errors.py +61 -2
- schemathesis/engine/events.py +10 -2
- schemathesis/engine/phases/probes.py +3 -0
- schemathesis/engine/phases/stateful/__init__.py +2 -1
- schemathesis/engine/phases/stateful/_executor.py +38 -5
- schemathesis/engine/phases/stateful/context.py +2 -2
- schemathesis/engine/phases/unit/_executor.py +36 -7
- schemathesis/generation/__init__.py +0 -3
- schemathesis/generation/case.py +153 -28
- schemathesis/generation/coverage.py +1 -1
- schemathesis/generation/hypothesis/builder.py +43 -19
- schemathesis/generation/metrics.py +93 -0
- schemathesis/generation/modes.py +0 -8
- schemathesis/generation/overrides.py +11 -27
- schemathesis/generation/stateful/__init__.py +17 -0
- schemathesis/generation/stateful/state_machine.py +32 -108
- schemathesis/graphql/loaders.py +152 -8
- schemathesis/hooks.py +63 -39
- schemathesis/openapi/checks.py +82 -20
- schemathesis/openapi/generation/filters.py +9 -2
- schemathesis/openapi/loaders.py +134 -8
- schemathesis/pytest/lazy.py +4 -31
- schemathesis/pytest/loaders.py +24 -0
- schemathesis/pytest/plugin.py +38 -6
- schemathesis/schemas.py +161 -94
- schemathesis/specs/graphql/scalars.py +37 -3
- schemathesis/specs/graphql/schemas.py +18 -9
- schemathesis/specs/openapi/_hypothesis.py +53 -34
- schemathesis/specs/openapi/checks.py +111 -47
- schemathesis/specs/openapi/expressions/nodes.py +1 -1
- schemathesis/specs/openapi/formats.py +30 -3
- schemathesis/specs/openapi/media_types.py +44 -1
- schemathesis/specs/openapi/negative/__init__.py +5 -3
- schemathesis/specs/openapi/negative/mutations.py +2 -2
- schemathesis/specs/openapi/parameters.py +0 -3
- schemathesis/specs/openapi/schemas.py +14 -93
- schemathesis/specs/openapi/stateful/__init__.py +2 -1
- schemathesis/specs/openapi/stateful/links.py +1 -63
- schemathesis/transport/__init__.py +54 -16
- schemathesis/transport/prepare.py +31 -7
- schemathesis/transport/requests.py +21 -9
- schemathesis/transport/serialization.py +0 -4
- schemathesis/transport/wsgi.py +15 -8
- {schemathesis-4.0.0a11.dist-info → schemathesis-4.0.0b1.dist-info}/METADATA +45 -87
- {schemathesis-4.0.0a11.dist-info → schemathesis-4.0.0b1.dist-info}/RECORD +69 -71
- schemathesis/contrib/__init__.py +0 -9
- schemathesis/contrib/openapi/__init__.py +0 -9
- schemathesis/contrib/openapi/fill_missing_examples.py +0 -20
- schemathesis/generation/targets.py +0 -69
- {schemathesis-4.0.0a11.dist-info → schemathesis-4.0.0b1.dist-info}/WHEEL +0 -0
- {schemathesis-4.0.0a11.dist-info → schemathesis-4.0.0b1.dist-info}/entry_points.txt +0 -0
- {schemathesis-4.0.0a11.dist-info → schemathesis-4.0.0b1.dist-info}/licenses/LICENSE +0 -0
schemathesis/openapi/checks.py
CHANGED
@@ -1,7 +1,6 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
3
|
import textwrap
|
4
|
-
from dataclasses import dataclass, field
|
5
4
|
from typing import TYPE_CHECKING, Any
|
6
5
|
|
7
6
|
from schemathesis.config import OutputConfig
|
@@ -12,24 +11,6 @@ if TYPE_CHECKING:
|
|
12
11
|
from jsonschema import ValidationError
|
13
12
|
|
14
13
|
|
15
|
-
@dataclass
|
16
|
-
class NegativeDataRejectionConfig:
|
17
|
-
# 5xx will pass through
|
18
|
-
allowed_statuses: list[str] = field(
|
19
|
-
default_factory=lambda: ["400", "401", "403", "404", "406", "422", "428", "5xx"]
|
20
|
-
)
|
21
|
-
|
22
|
-
|
23
|
-
@dataclass
|
24
|
-
class PositiveDataAcceptanceConfig:
|
25
|
-
allowed_statuses: list[str] = field(default_factory=lambda: ["2xx", "401", "403", "404"])
|
26
|
-
|
27
|
-
|
28
|
-
@dataclass
|
29
|
-
class MissingRequiredHeaderConfig:
|
30
|
-
allowed_statuses: list[str] = field(default_factory=lambda: ["406"])
|
31
|
-
|
32
|
-
|
33
14
|
class UndefinedStatusCode(Failure):
|
34
15
|
"""Response has a status code that is not defined in the schema."""
|
35
16
|
|
@@ -323,7 +304,7 @@ class IgnoredAuth(Failure):
|
|
323
304
|
*,
|
324
305
|
operation: str,
|
325
306
|
message: str,
|
326
|
-
title: str = "
|
307
|
+
title: str = "API accepts requests without authentication",
|
327
308
|
case_id: str | None = None,
|
328
309
|
) -> None:
|
329
310
|
self.operation = operation
|
@@ -391,3 +372,84 @@ class RejectedPositiveData(Failure):
|
|
391
372
|
@property
|
392
373
|
def _unique_key(self) -> str:
|
393
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
|
@@ -29,8 +29,15 @@ def is_invalid_path_parameter(value: Any) -> bool:
|
|
29
29
|
return (
|
30
30
|
value in ("/", "")
|
31
31
|
or contains_unicode_surrogate_pair(value)
|
32
|
-
or
|
33
|
-
|
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
|
+
)
|
34
41
|
)
|
35
42
|
|
36
43
|
|
schemathesis/openapi/loaders.py
CHANGED
@@ -20,15 +20,54 @@ if TYPE_CHECKING:
|
|
20
20
|
|
21
21
|
|
22
22
|
def from_asgi(path: str, app: Any, *, config: SchemathesisConfig | None = None, **kwargs: Any) -> BaseOpenAPISchema:
|
23
|
+
"""Load OpenAPI schema from an ASGI application.
|
24
|
+
|
25
|
+
Args:
|
26
|
+
path: Relative URL path to the OpenAPI schema endpoint (e.g., "/openapi.json")
|
27
|
+
app: ASGI application instance
|
28
|
+
config: Custom configuration. If `None`, uses auto-discovered config
|
29
|
+
**kwargs: Additional request parameters passed to the ASGI test client
|
30
|
+
|
31
|
+
Example:
|
32
|
+
```python
|
33
|
+
from fastapi import FastAPI
|
34
|
+
import schemathesis
|
35
|
+
|
36
|
+
app = FastAPI()
|
37
|
+
schema = schemathesis.openapi.from_asgi("/openapi.json", app)
|
38
|
+
```
|
39
|
+
|
40
|
+
"""
|
23
41
|
require_relative_url(path)
|
24
42
|
client = asgi.get_client(app)
|
25
43
|
response = load_from_url(client.get, url=path, **kwargs)
|
26
44
|
content_type = detect_content_type(headers=response.headers, path=path)
|
27
45
|
schema = load_content(response.text, content_type)
|
28
|
-
|
46
|
+
loaded = from_dict(schema=schema, config=config)
|
47
|
+
loaded.app = app
|
48
|
+
loaded.location = path
|
49
|
+
return loaded
|
29
50
|
|
30
51
|
|
31
52
|
def from_wsgi(path: str, app: Any, *, config: SchemathesisConfig | None = None, **kwargs: Any) -> BaseOpenAPISchema:
|
53
|
+
"""Load OpenAPI schema from a WSGI application.
|
54
|
+
|
55
|
+
Args:
|
56
|
+
path: Relative URL path to the OpenAPI schema endpoint (e.g., "/openapi.json")
|
57
|
+
app: WSGI application instance
|
58
|
+
config: Custom configuration. If `None`, uses auto-discovered config
|
59
|
+
**kwargs: Additional request parameters passed to the WSGI test client
|
60
|
+
|
61
|
+
Example:
|
62
|
+
```python
|
63
|
+
from flask import Flask
|
64
|
+
import schemathesis
|
65
|
+
|
66
|
+
app = Flask(__name__)
|
67
|
+
schema = schemathesis.openapi.from_wsgi("/openapi.json", app)
|
68
|
+
```
|
69
|
+
|
70
|
+
"""
|
32
71
|
require_relative_url(path)
|
33
72
|
prepare_request_kwargs(kwargs)
|
34
73
|
client = wsgi.get_client(app)
|
@@ -36,33 +75,101 @@ def from_wsgi(path: str, app: Any, *, config: SchemathesisConfig | None = None,
|
|
36
75
|
raise_for_status(response)
|
37
76
|
content_type = detect_content_type(headers=response.headers, path=path)
|
38
77
|
schema = load_content(response.text, content_type)
|
39
|
-
|
78
|
+
loaded = from_dict(schema=schema, config=config)
|
79
|
+
loaded.app = app
|
80
|
+
loaded.location = path
|
81
|
+
return loaded
|
40
82
|
|
41
83
|
|
42
84
|
def from_url(
|
43
85
|
url: str, *, config: SchemathesisConfig | None = None, wait_for_schema: float | None = None, **kwargs: Any
|
44
86
|
) -> BaseOpenAPISchema:
|
45
|
-
"""Load from URL.
|
87
|
+
"""Load OpenAPI schema from a URL.
|
88
|
+
|
89
|
+
Args:
|
90
|
+
url: Full URL to the OpenAPI schema
|
91
|
+
config: Custom configuration. If `None`, uses auto-discovered config
|
92
|
+
wait_for_schema: Maximum time in seconds to wait for schema availability
|
93
|
+
**kwargs: Additional parameters passed to `requests.get()` (headers, timeout, auth, etc.)
|
94
|
+
|
95
|
+
Example:
|
96
|
+
```python
|
97
|
+
import schemathesis
|
98
|
+
|
99
|
+
# Basic usage
|
100
|
+
schema = schemathesis.openapi.from_url("https://api.example.com/openapi.json")
|
101
|
+
|
102
|
+
# With authentication and timeout
|
103
|
+
schema = schemathesis.openapi.from_url(
|
104
|
+
"https://api.example.com/openapi.json",
|
105
|
+
headers={"Authorization": "Bearer token"},
|
106
|
+
timeout=30,
|
107
|
+
wait_for_schema=10.0
|
108
|
+
)
|
109
|
+
```
|
110
|
+
|
111
|
+
"""
|
46
112
|
import requests
|
47
113
|
|
48
114
|
response = load_from_url(requests.get, url=url, wait_for_schema=wait_for_schema, **kwargs)
|
49
115
|
content_type = detect_content_type(headers=response.headers, path=url)
|
50
116
|
schema = load_content(response.text, content_type)
|
51
|
-
|
117
|
+
loaded = from_dict(schema=schema, config=config)
|
118
|
+
loaded.location = url
|
119
|
+
return loaded
|
52
120
|
|
53
121
|
|
54
122
|
def from_path(
|
55
123
|
path: PathLike | str, *, config: SchemathesisConfig | None = None, encoding: str = "utf-8"
|
56
124
|
) -> BaseOpenAPISchema:
|
57
|
-
"""Load from a filesystem path.
|
125
|
+
"""Load OpenAPI schema from a filesystem path.
|
126
|
+
|
127
|
+
Args:
|
128
|
+
path: File path to the OpenAPI schema (supports JSON / YAML)
|
129
|
+
config: Custom configuration. If `None`, uses auto-discovered config
|
130
|
+
encoding: Text encoding for reading the file
|
131
|
+
|
132
|
+
Example:
|
133
|
+
```python
|
134
|
+
import schemathesis
|
135
|
+
|
136
|
+
# Load from file
|
137
|
+
schema = schemathesis.openapi.from_path("./specs/openapi.yaml")
|
138
|
+
|
139
|
+
# With custom encoding
|
140
|
+
schema = schemathesis.openapi.from_path("./specs/openapi.json", encoding="cp1252")
|
141
|
+
```
|
142
|
+
|
143
|
+
"""
|
58
144
|
with open(path, encoding=encoding) as file:
|
59
145
|
content_type = detect_content_type(headers=None, path=str(path))
|
60
146
|
schema = load_content(file.read(), content_type)
|
61
|
-
|
147
|
+
loaded = from_dict(schema=schema, config=config)
|
148
|
+
loaded.location = Path(path).absolute().as_uri()
|
149
|
+
return loaded
|
62
150
|
|
63
151
|
|
64
152
|
def from_file(file: IO[str] | str, *, config: SchemathesisConfig | None = None) -> BaseOpenAPISchema:
|
65
|
-
"""Load from file-like object or string.
|
153
|
+
"""Load OpenAPI schema from a file-like object or string.
|
154
|
+
|
155
|
+
Args:
|
156
|
+
file: File-like object or raw string containing the OpenAPI schema
|
157
|
+
config: Custom configuration. If `None`, uses auto-discovered config
|
158
|
+
|
159
|
+
Example:
|
160
|
+
```python
|
161
|
+
import schemathesis
|
162
|
+
|
163
|
+
# From string
|
164
|
+
schema_content = '{"openapi": "3.0.0", "info": {"title": "API"}}'
|
165
|
+
schema = schemathesis.openapi.from_file(schema_content)
|
166
|
+
|
167
|
+
# From file object
|
168
|
+
with open("openapi.yaml") as f:
|
169
|
+
schema = schemathesis.openapi.from_file(f)
|
170
|
+
```
|
171
|
+
|
172
|
+
"""
|
66
173
|
if isinstance(file, str):
|
67
174
|
data = file
|
68
175
|
else:
|
@@ -75,7 +182,26 @@ def from_file(file: IO[str] | str, *, config: SchemathesisConfig | None = None)
|
|
75
182
|
|
76
183
|
|
77
184
|
def from_dict(schema: dict[str, Any], *, config: SchemathesisConfig | None = None) -> BaseOpenAPISchema:
|
78
|
-
"""
|
185
|
+
"""Load OpenAPI schema from a dictionary.
|
186
|
+
|
187
|
+
Args:
|
188
|
+
schema: Dictionary containing the parsed OpenAPI schema
|
189
|
+
config: Custom configuration. If `None`, uses auto-discovered config
|
190
|
+
|
191
|
+
Example:
|
192
|
+
```python
|
193
|
+
import schemathesis
|
194
|
+
|
195
|
+
schema_dict = {
|
196
|
+
"openapi": "3.0.0",
|
197
|
+
"info": {"title": "My API", "version": "1.0.0"},
|
198
|
+
"paths": {"/users": {"get": {"responses": {"200": {"description": "OK"}}}}}
|
199
|
+
}
|
200
|
+
|
201
|
+
schema = schemathesis.openapi.from_dict(schema_dict)
|
202
|
+
```
|
203
|
+
|
204
|
+
"""
|
79
205
|
from schemathesis.specs.openapi.schemas import OpenApi30, SwaggerV20
|
80
206
|
|
81
207
|
if not isinstance(schema, dict):
|
schemathesis/pytest/lazy.py
CHANGED
@@ -12,6 +12,7 @@ from pytest_subtests import SubTests
|
|
12
12
|
from schemathesis.core.errors import InvalidSchema
|
13
13
|
from schemathesis.core.result import Ok, Result
|
14
14
|
from schemathesis.filters import FilterSet, FilterValue, MatcherFunc, RegexValue, is_deprecated
|
15
|
+
from schemathesis.generation import overrides
|
15
16
|
from schemathesis.generation.hypothesis.builder import HypothesisTestConfig, HypothesisTestMode, create_test
|
16
17
|
from schemathesis.generation.hypothesis.given import (
|
17
18
|
GivenArgsMark,
|
@@ -22,7 +23,6 @@ from schemathesis.generation.hypothesis.given import (
|
|
22
23
|
merge_given_args,
|
23
24
|
validate_given_args,
|
24
25
|
)
|
25
|
-
from schemathesis.generation.overrides import Override, OverrideMark, check_no_override_mark
|
26
26
|
from schemathesis.pytest.control_flow import fail_on_no_matches
|
27
27
|
from schemathesis.schemas import BaseSchema
|
28
28
|
|
@@ -174,17 +174,10 @@ class LazySchema:
|
|
174
174
|
node_id = request.node._nodeid
|
175
175
|
settings = getattr(wrapped_test, "_hypothesis_internal_use_settings", None)
|
176
176
|
|
177
|
-
as_strategy_kwargs:
|
177
|
+
def as_strategy_kwargs(_operation: APIOperation) -> dict[str, Any]:
|
178
|
+
override = overrides.for_operation(config=schema.config, operation=_operation)
|
178
179
|
|
179
|
-
|
180
|
-
if override is not None:
|
181
|
-
|
182
|
-
def as_strategy_kwargs(_operation: APIOperation) -> dict[str, Any]:
|
183
|
-
nonlocal override
|
184
|
-
|
185
|
-
return {
|
186
|
-
location: entry for location, entry in override.for_operation(_operation).items() if entry
|
187
|
-
}
|
180
|
+
return {location: entry for location, entry in override.items() if entry}
|
188
181
|
|
189
182
|
tests = list(
|
190
183
|
get_all_tests(
|
@@ -224,26 +217,6 @@ class LazySchema:
|
|
224
217
|
def given(self, *args: GivenInput, **kwargs: GivenInput) -> Callable:
|
225
218
|
return given_proxy(*args, **kwargs)
|
226
219
|
|
227
|
-
def override(
|
228
|
-
self,
|
229
|
-
*,
|
230
|
-
query: dict[str, str] | None = None,
|
231
|
-
headers: dict[str, str] | None = None,
|
232
|
-
cookies: dict[str, str] | None = None,
|
233
|
-
path_parameters: dict[str, str] | None = None,
|
234
|
-
) -> Callable[[Callable], Callable]:
|
235
|
-
"""Override Open API parameters with fixed values."""
|
236
|
-
|
237
|
-
def _add_override(test: Callable) -> Callable:
|
238
|
-
check_no_override_mark(test)
|
239
|
-
override = Override(
|
240
|
-
query=query or {}, headers=headers or {}, cookies=cookies or {}, path_parameters=path_parameters or {}
|
241
|
-
)
|
242
|
-
OverrideMark.set(test, override)
|
243
|
-
return test
|
244
|
-
|
245
|
-
return _add_override
|
246
|
-
|
247
220
|
|
248
221
|
def _copy_marks(source: Callable, target: Callable) -> None:
|
249
222
|
marks = getattr(source, "pytestmark", [])
|
schemathesis/pytest/loaders.py
CHANGED
@@ -7,6 +7,30 @@ if TYPE_CHECKING:
|
|
7
7
|
|
8
8
|
|
9
9
|
def from_fixture(name: str) -> LazySchema:
|
10
|
+
"""Create a lazy schema loader that resolves a pytest fixture at test runtime.
|
11
|
+
|
12
|
+
Args:
|
13
|
+
name: Name of the pytest fixture that returns a schema object
|
14
|
+
|
15
|
+
Example:
|
16
|
+
```python
|
17
|
+
import pytest
|
18
|
+
import schemathesis
|
19
|
+
|
20
|
+
@pytest.fixture
|
21
|
+
def api_schema():
|
22
|
+
return schemathesis.openapi.from_url("https://api.example.com/openapi.json")
|
23
|
+
|
24
|
+
# Create lazy schema from fixture
|
25
|
+
schema = schemathesis.pytest.from_fixture("api_schema")
|
26
|
+
|
27
|
+
# Use with parametrize to generate tests
|
28
|
+
@schema.parametrize()
|
29
|
+
def test_api(case):
|
30
|
+
case.call_and_validate()
|
31
|
+
```
|
32
|
+
|
33
|
+
"""
|
10
34
|
from schemathesis.pytest.lazy import LazySchema
|
11
35
|
|
12
36
|
return LazySchema(name)
|
schemathesis/pytest/plugin.py
CHANGED
@@ -21,9 +21,12 @@ from schemathesis.core.errors import (
|
|
21
21
|
InvalidRegexPattern,
|
22
22
|
InvalidSchema,
|
23
23
|
SerializationNotPossible,
|
24
|
+
format_exception,
|
24
25
|
)
|
26
|
+
from schemathesis.core.failures import FailureGroup
|
25
27
|
from schemathesis.core.marks import Mark
|
26
28
|
from schemathesis.core.result import Ok, Result
|
29
|
+
from schemathesis.generation import overrides
|
27
30
|
from schemathesis.generation.hypothesis.given import (
|
28
31
|
GivenArgsMark,
|
29
32
|
GivenKwargsMark,
|
@@ -32,7 +35,6 @@ from schemathesis.generation.hypothesis.given import (
|
|
32
35
|
validate_given_args,
|
33
36
|
)
|
34
37
|
from schemathesis.generation.hypothesis.reporting import ignore_hypothesis_output
|
35
|
-
from schemathesis.generation.overrides import OverrideMark
|
36
38
|
from schemathesis.pytest.control_flow import fail_on_no_matches
|
37
39
|
from schemathesis.schemas import APIOperation
|
38
40
|
|
@@ -108,6 +110,7 @@ class SchemathesisCase(PyCollector):
|
|
108
110
|
This implementation is based on the original one in pytest, but with slight adjustments
|
109
111
|
to produce tests out of hypothesis ones.
|
110
112
|
"""
|
113
|
+
from schemathesis.checks import load_all_checks
|
111
114
|
from schemathesis.generation.hypothesis.builder import (
|
112
115
|
HypothesisTestConfig,
|
113
116
|
HypothesisTestMode,
|
@@ -115,6 +118,8 @@ class SchemathesisCase(PyCollector):
|
|
115
118
|
make_async_test,
|
116
119
|
)
|
117
120
|
|
121
|
+
load_all_checks()
|
122
|
+
|
118
123
|
is_trio_test = False
|
119
124
|
for mark in getattr(self.test_function, "pytestmark", []):
|
120
125
|
if mark.name == "trio":
|
@@ -126,14 +131,22 @@ class SchemathesisCase(PyCollector):
|
|
126
131
|
if self.is_invalid_test:
|
127
132
|
funcobj = self.test_function
|
128
133
|
else:
|
129
|
-
|
134
|
+
as_strategy_kwargs = {}
|
135
|
+
|
136
|
+
auth = self.schema.config.auth_for(operation=operation)
|
137
|
+
if auth is not None:
|
138
|
+
from requests.auth import _basic_auth_str
|
139
|
+
|
140
|
+
as_strategy_kwargs["headers"] = {"Authorization": _basic_auth_str(*auth)}
|
141
|
+
headers = self.schema.config.headers_for(operation=operation)
|
142
|
+
if headers:
|
143
|
+
as_strategy_kwargs["headers"] = headers
|
144
|
+
|
145
|
+
override = overrides.for_operation(operation=operation, config=self.schema.config)
|
130
146
|
if override is not None:
|
131
|
-
|
132
|
-
for location, entry in override.for_operation(operation).items():
|
147
|
+
for location, entry in override.items():
|
133
148
|
if entry:
|
134
149
|
as_strategy_kwargs[location] = entry
|
135
|
-
else:
|
136
|
-
as_strategy_kwargs = {}
|
137
150
|
modes = []
|
138
151
|
phases = self.schema.config.phases_for(operation=operation)
|
139
152
|
if phases.examples.enabled:
|
@@ -148,6 +161,7 @@ class SchemathesisCase(PyCollector):
|
|
148
161
|
test_func=self.test_function,
|
149
162
|
config=HypothesisTestConfig(
|
150
163
|
modes=modes,
|
164
|
+
settings=self.schema.config.get_hypothesis_settings(operation=operation),
|
151
165
|
given_kwargs=self.given_kwargs,
|
152
166
|
project=self.schema.config,
|
153
167
|
as_strategy_kwargs=as_strategy_kwargs,
|
@@ -247,6 +261,24 @@ def pytest_pycollect_makeitem(collector: nodes.Collector, name: str, obj: Any) -
|
|
247
261
|
outcome.get_result()
|
248
262
|
|
249
263
|
|
264
|
+
@pytest.hookimpl(tryfirst=True) # type: ignore[misc]
|
265
|
+
def pytest_exception_interact(node: Function, call: pytest.CallInfo, report: pytest.TestReport) -> None:
|
266
|
+
if call.excinfo and call.excinfo.type is FailureGroup:
|
267
|
+
tb_entries = list(call.excinfo.traceback)
|
268
|
+
total_frames = len(tb_entries)
|
269
|
+
|
270
|
+
# Keep internal Schemathesis frames + one extra one from the caller
|
271
|
+
skip_frames = 0
|
272
|
+
for i in range(total_frames - 1, -1, -1):
|
273
|
+
entry = tb_entries[i]
|
274
|
+
|
275
|
+
if not str(entry.path).endswith("schemathesis/generation/case.py"):
|
276
|
+
skip_frames = i
|
277
|
+
break
|
278
|
+
|
279
|
+
report.longrepr = "".join(format_exception(call.excinfo.value, with_traceback=True, skip_frames=skip_frames))
|
280
|
+
|
281
|
+
|
250
282
|
@hookimpl(wrapper=True)
|
251
283
|
def pytest_pyfunc_call(pyfuncitem): # type:ignore
|
252
284
|
"""It is possible to have a Hypothesis exception in runtime.
|