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/__init__.py
CHANGED
@@ -1,44 +1,52 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
|
-
from schemathesis import
|
3
|
+
from schemathesis import errors, graphql, openapi, pytest
|
4
|
+
from schemathesis.auths import AuthContext, AuthProvider, auth
|
4
5
|
from schemathesis.checks import CheckContext, CheckFunction, check
|
5
|
-
from schemathesis.
|
6
|
+
from schemathesis.config import SchemathesisConfig as Config
|
6
7
|
from schemathesis.core.transport import Response
|
7
8
|
from schemathesis.core.version import SCHEMATHESIS_VERSION
|
8
|
-
from schemathesis.generation import GenerationMode
|
9
|
+
from schemathesis.generation import GenerationMode, stateful
|
9
10
|
from schemathesis.generation.case import Case
|
10
|
-
from schemathesis.generation.
|
11
|
-
from schemathesis.hooks import HookContext
|
12
|
-
from schemathesis.schemas import BaseSchema
|
11
|
+
from schemathesis.generation.metrics import MetricContext, MetricFunction, metric
|
12
|
+
from schemathesis.hooks import HookContext, hook
|
13
|
+
from schemathesis.schemas import APIOperation, BaseSchema
|
14
|
+
from schemathesis.transport import SerializationContext, serializer
|
13
15
|
|
14
16
|
__version__ = SCHEMATHESIS_VERSION
|
15
17
|
|
16
|
-
# Public API
|
17
|
-
auth = auths.GLOBAL_AUTH_STORAGE
|
18
|
-
hook = hooks.register
|
19
|
-
|
20
18
|
__all__ = [
|
19
|
+
"__version__",
|
20
|
+
# Core data structures
|
21
21
|
"Case",
|
22
|
-
"CheckContext",
|
23
|
-
"CheckFunction",
|
24
|
-
"GenerationMode",
|
25
22
|
"Response",
|
26
|
-
"
|
27
|
-
"TargetFunction",
|
28
|
-
"HookContext",
|
23
|
+
"APIOperation",
|
29
24
|
"BaseSchema",
|
30
|
-
"
|
31
|
-
"
|
32
|
-
"
|
33
|
-
|
34
|
-
"engine",
|
25
|
+
"Config",
|
26
|
+
"GenerationMode",
|
27
|
+
"stateful",
|
28
|
+
# Public errors
|
35
29
|
"errors",
|
36
|
-
|
37
|
-
"hook",
|
38
|
-
"hooks",
|
30
|
+
# Spec or usage specific namespaces
|
39
31
|
"openapi",
|
32
|
+
"graphql",
|
40
33
|
"pytest",
|
41
|
-
|
42
|
-
"
|
43
|
-
"
|
34
|
+
# Hooks
|
35
|
+
"hook",
|
36
|
+
"HookContext",
|
37
|
+
# Checks
|
38
|
+
"check",
|
39
|
+
"CheckContext",
|
40
|
+
"CheckFunction",
|
41
|
+
# Auth
|
42
|
+
"auth",
|
43
|
+
"AuthContext",
|
44
|
+
"AuthProvider",
|
45
|
+
# Targeted Property-based Testing
|
46
|
+
"metric",
|
47
|
+
"MetricContext",
|
48
|
+
"MetricFunction",
|
49
|
+
# Serialization
|
50
|
+
"serializer",
|
51
|
+
"SerializationContext",
|
44
52
|
]
|
schemathesis/auths.py
CHANGED
@@ -34,14 +34,34 @@ Auth = TypeVar("Auth")
|
|
34
34
|
|
35
35
|
@dataclass
|
36
36
|
class AuthContext:
|
37
|
-
"""
|
37
|
+
"""Runtime context passed to authentication providers during token generation.
|
38
|
+
|
39
|
+
Provides access to the current API operation and application instance when
|
40
|
+
auth providers need operation-specific tokens or application state.
|
41
|
+
|
42
|
+
Example:
|
43
|
+
```python
|
44
|
+
@schemathesis.auth()
|
45
|
+
class ContextAwareAuth:
|
46
|
+
def get(self, case, context):
|
47
|
+
# Access operation details
|
48
|
+
if "/admin/" in context.operation.path:
|
49
|
+
return self.get_admin_token()
|
50
|
+
else:
|
51
|
+
return self.get_user_token()
|
52
|
+
|
53
|
+
def set(self, case, data, context):
|
54
|
+
case.headers = {"Authorization": f"Bearer {data}"}
|
55
|
+
```
|
38
56
|
|
39
|
-
:ivar APIOperation operation: API operation that is currently being processed.
|
40
|
-
:ivar app: Optional Python application if the WSGI / ASGI integration is used.
|
41
57
|
"""
|
42
58
|
|
43
59
|
operation: APIOperation
|
60
|
+
"""API operation currently being processed for authentication."""
|
44
61
|
app: Any | None
|
62
|
+
"""Python application instance (ASGI/WSGI app) when using app integration, `None` otherwise."""
|
63
|
+
|
64
|
+
__slots__ = ("operation", "app")
|
45
65
|
|
46
66
|
|
47
67
|
CacheKeyFunction = Callable[["Case", "AuthContext"], Union[str, int]]
|
@@ -49,22 +69,28 @@ CacheKeyFunction = Callable[["Case", "AuthContext"], Union[str, int]]
|
|
49
69
|
|
50
70
|
@runtime_checkable
|
51
71
|
class AuthProvider(Generic[Auth], Protocol):
|
52
|
-
"""
|
72
|
+
"""Protocol for implementing custom authentication in API tests."""
|
53
73
|
|
54
|
-
def get(self, case: Case,
|
55
|
-
"""
|
74
|
+
def get(self, case: Case, ctx: AuthContext) -> Auth | None:
|
75
|
+
"""Obtain authentication data for the test case.
|
76
|
+
|
77
|
+
Args:
|
78
|
+
case: Generated test case requiring authentication.
|
79
|
+
ctx: Authentication state and configuration.
|
80
|
+
|
81
|
+
Returns:
|
82
|
+
Authentication data (e.g., token, credentials) or `None`.
|
56
83
|
|
57
|
-
:param Case case: Generated test case.
|
58
|
-
:param AuthContext context: Holds state relevant for the authentication process.
|
59
|
-
:return: Any authentication data you find useful for your use case. For example, it could be an access token.
|
60
84
|
"""
|
61
85
|
|
62
|
-
def set(self, case: Case, data: Auth,
|
63
|
-
"""
|
86
|
+
def set(self, case: Case, data: Auth, ctx: AuthContext) -> None:
|
87
|
+
"""Apply authentication data to the test case.
|
88
|
+
|
89
|
+
Args:
|
90
|
+
case: Test case to modify.
|
91
|
+
data: Authentication data from the `get` method.
|
92
|
+
ctx: Authentication state and configuration.
|
64
93
|
|
65
|
-
:param Optional[Auth] data: Authentication data you got from the ``get`` method.
|
66
|
-
:param Case case: Generated test case.
|
67
|
-
:param AuthContext context: Holds state relevant for the authentication process.
|
68
94
|
"""
|
69
95
|
|
70
96
|
|
@@ -289,7 +315,7 @@ class AuthStorage(Generic[Auth]):
|
|
289
315
|
) -> FilterableRegisterAuth | FilterableApplyAuth:
|
290
316
|
if provider_class is not None:
|
291
317
|
return self.apply(provider_class, refresh_interval=refresh_interval, cache_by_key=cache_by_key)
|
292
|
-
return self.
|
318
|
+
return self.auth(refresh_interval=refresh_interval, cache_by_key=cache_by_key)
|
293
319
|
|
294
320
|
def set_from_requests(self, auth: requests.auth.AuthBase) -> FilterableRequestsAuth:
|
295
321
|
"""Use `requests` auth instance as an auth provider."""
|
@@ -313,8 +339,9 @@ class AuthStorage(Generic[Auth]):
|
|
313
339
|
) -> None:
|
314
340
|
if not issubclass(provider_class, AuthProvider):
|
315
341
|
raise TypeError(
|
316
|
-
f"`{provider_class.__name__}`
|
317
|
-
f"
|
342
|
+
f"`{provider_class.__name__}` does not implement the `AuthProvider` protocol. "
|
343
|
+
f"Auth providers must have `get` and `set` methods. "
|
344
|
+
f"See `schemathesis.AuthProvider` documentation for examples."
|
318
345
|
)
|
319
346
|
provider: AuthProvider
|
320
347
|
# Apply caching if desired
|
@@ -333,30 +360,12 @@ class AuthStorage(Generic[Auth]):
|
|
333
360
|
provider = SelectiveAuthProvider(provider, filter_set)
|
334
361
|
self.providers.append(provider)
|
335
362
|
|
336
|
-
def
|
363
|
+
def auth(
|
337
364
|
self,
|
338
365
|
*,
|
339
366
|
refresh_interval: int | None = DEFAULT_REFRESH_INTERVAL,
|
340
367
|
cache_by_key: CacheKeyFunction | None = None,
|
341
368
|
) -> FilterableRegisterAuth:
|
342
|
-
"""Register a new auth provider.
|
343
|
-
|
344
|
-
.. code-block:: python
|
345
|
-
|
346
|
-
@schemathesis.auth()
|
347
|
-
class TokenAuth:
|
348
|
-
def get(self, context):
|
349
|
-
response = requests.post(
|
350
|
-
"https://example.schemathesis.io/api/token/",
|
351
|
-
json={"username": "demo", "password": "test"},
|
352
|
-
)
|
353
|
-
data = response.json()
|
354
|
-
return data["access_token"]
|
355
|
-
|
356
|
-
def set(self, case, data, context):
|
357
|
-
# Modify `case` the way you need
|
358
|
-
case.headers = {"Authorization": f"Bearer {data}"}
|
359
|
-
"""
|
360
369
|
filter_set = FilterSet()
|
361
370
|
|
362
371
|
def wrapper(provider_class: type[AuthProvider]) -> type[AuthProvider]:
|
@@ -387,23 +396,6 @@ class AuthStorage(Generic[Auth]):
|
|
387
396
|
refresh_interval: int | None = DEFAULT_REFRESH_INTERVAL,
|
388
397
|
cache_by_key: CacheKeyFunction | None = None,
|
389
398
|
) -> FilterableApplyAuth:
|
390
|
-
"""Register auth provider only on one test function.
|
391
|
-
|
392
|
-
:param Type[AuthProvider] provider_class: Authentication provider class.
|
393
|
-
:param Optional[int] refresh_interval: Cache duration in seconds.
|
394
|
-
|
395
|
-
.. code-block:: python
|
396
|
-
|
397
|
-
class Auth:
|
398
|
-
...
|
399
|
-
|
400
|
-
|
401
|
-
@schema.auth(Auth)
|
402
|
-
@schema.parametrize()
|
403
|
-
def test_api(case):
|
404
|
-
...
|
405
|
-
|
406
|
-
"""
|
407
399
|
filter_set = FilterSet()
|
408
400
|
|
409
401
|
def wrapper(test: Callable) -> Callable:
|
@@ -451,5 +443,44 @@ def set_on_case(case: Case, context: AuthContext, auth_storage: AuthStorage | No
|
|
451
443
|
|
452
444
|
# Global auth API
|
453
445
|
GLOBAL_AUTH_STORAGE: AuthStorage = AuthStorage()
|
454
|
-
register = GLOBAL_AUTH_STORAGE.register
|
455
446
|
unregister = GLOBAL_AUTH_STORAGE.unregister
|
447
|
+
|
448
|
+
|
449
|
+
def auth(
|
450
|
+
*,
|
451
|
+
refresh_interval: int | None = DEFAULT_REFRESH_INTERVAL,
|
452
|
+
cache_by_key: CacheKeyFunction | None = None,
|
453
|
+
) -> FilterableRegisterAuth:
|
454
|
+
"""Register a dynamic authentication provider for APIs with expiring tokens.
|
455
|
+
|
456
|
+
Args:
|
457
|
+
refresh_interval: Seconds between token refreshes. Default is `300`. Use `None` to disable caching
|
458
|
+
cache_by_key: Function to generate cache keys for different auth contexts (e.g., OAuth scopes)
|
459
|
+
|
460
|
+
Example:
|
461
|
+
```python
|
462
|
+
import schemathesis
|
463
|
+
import requests
|
464
|
+
|
465
|
+
@schemathesis.auth()
|
466
|
+
class TokenAuth:
|
467
|
+
def get(self, case, context):
|
468
|
+
\"\"\"Fetch fresh authentication token\"\"\"
|
469
|
+
response = requests.post(
|
470
|
+
"http://localhost:8000/auth/token",
|
471
|
+
json={"username": "demo", "password": "test"}
|
472
|
+
)
|
473
|
+
return response.json()["access_token"]
|
474
|
+
|
475
|
+
def set(self, case, data, context):
|
476
|
+
\"\"\"Apply token to test case headers\"\"\"
|
477
|
+
case.headers = case.headers or {}
|
478
|
+
case.headers["Authorization"] = f"Bearer {data}"
|
479
|
+
```
|
480
|
+
|
481
|
+
"""
|
482
|
+
return GLOBAL_AUTH_STORAGE.auth(refresh_interval=refresh_interval, cache_by_key=cache_by_key)
|
483
|
+
|
484
|
+
|
485
|
+
auth.__dict__ = GLOBAL_AUTH_STORAGE.auth.__dict__
|
486
|
+
auth.set_from_requests = GLOBAL_AUTH_STORAGE.set_from_requests # type: ignore[attr-defined]
|
schemathesis/checks.py
CHANGED
@@ -26,20 +26,21 @@ CheckFunction = Callable[["CheckContext", "Response", "Case"], Optional[bool]]
|
|
26
26
|
|
27
27
|
|
28
28
|
class CheckContext:
|
29
|
-
"""
|
29
|
+
"""Runtime context passed to validation check functions during API testing.
|
30
30
|
|
31
|
-
Provides access to
|
31
|
+
Provides access to configuration for currently checked endpoint.
|
32
32
|
"""
|
33
33
|
|
34
|
-
|
35
|
-
|
36
|
-
|
34
|
+
_override: Override | None
|
35
|
+
_auth: tuple[str, str] | None
|
36
|
+
_headers: CaseInsensitiveDict | None
|
37
37
|
config: ChecksConfig
|
38
|
-
|
39
|
-
|
40
|
-
|
38
|
+
"""Configuration settings for validation checks."""
|
39
|
+
_transport_kwargs: dict[str, Any] | None
|
40
|
+
_recorder: ScenarioRecorder | None
|
41
|
+
_checks: list[CheckFunction]
|
41
42
|
|
42
|
-
__slots__ = ("
|
43
|
+
__slots__ = ("_override", "_auth", "_headers", "config", "_transport_kwargs", "_recorder", "_checks")
|
43
44
|
|
44
45
|
def __init__(
|
45
46
|
self,
|
@@ -50,55 +51,83 @@ class CheckContext:
|
|
50
51
|
transport_kwargs: dict[str, Any] | None,
|
51
52
|
recorder: ScenarioRecorder | None = None,
|
52
53
|
) -> None:
|
53
|
-
self.
|
54
|
-
self.
|
55
|
-
self.
|
54
|
+
self._override = override
|
55
|
+
self._auth = auth
|
56
|
+
self._headers = headers
|
56
57
|
self.config = config
|
57
|
-
self.
|
58
|
-
self.
|
59
|
-
self.
|
58
|
+
self._transport_kwargs = transport_kwargs
|
59
|
+
self._recorder = recorder
|
60
|
+
self._checks = []
|
60
61
|
for check in CHECKS.get_all():
|
61
62
|
name = check.__name__
|
62
63
|
if self.config.get_by_name(name=name).enabled:
|
63
|
-
self.
|
64
|
+
self._checks.append(check)
|
64
65
|
if self.config.max_response_time.enabled:
|
65
|
-
self.
|
66
|
+
self._checks.append(max_response_time)
|
66
67
|
|
67
|
-
def
|
68
|
-
if self.
|
69
|
-
return self.
|
68
|
+
def _find_parent(self, *, case_id: str) -> Case | None:
|
69
|
+
if self._recorder is not None:
|
70
|
+
return self._recorder.find_parent(case_id=case_id)
|
70
71
|
return None
|
71
72
|
|
72
|
-
def
|
73
|
-
if self.
|
74
|
-
yield from self.
|
73
|
+
def _find_related(self, *, case_id: str) -> Iterator[Case]:
|
74
|
+
if self._recorder is not None:
|
75
|
+
yield from self._recorder.find_related(case_id=case_id)
|
75
76
|
|
76
|
-
def
|
77
|
-
if self.
|
78
|
-
return self.
|
77
|
+
def _find_response(self, *, case_id: str) -> Response | None:
|
78
|
+
if self._recorder is not None:
|
79
|
+
return self._recorder.find_response(case_id=case_id)
|
79
80
|
return None
|
80
81
|
|
81
|
-
def
|
82
|
-
if self.
|
83
|
-
self.
|
82
|
+
def _record_case(self, *, parent_id: str, case: Case) -> None:
|
83
|
+
if self._recorder is not None:
|
84
|
+
self._recorder.record_case(parent_id=parent_id, transition=None, case=case)
|
84
85
|
|
85
|
-
def
|
86
|
-
if self.
|
87
|
-
self.
|
86
|
+
def _record_response(self, *, case_id: str, response: Response) -> None:
|
87
|
+
if self._recorder is not None:
|
88
|
+
self._recorder.record_response(case_id=case_id, response=response)
|
88
89
|
|
89
90
|
|
90
91
|
CHECKS = Registry[CheckFunction]()
|
91
|
-
|
92
|
+
|
93
|
+
|
94
|
+
def load_all_checks() -> None:
|
95
|
+
# NOTE: Trigger registering all Open API checks
|
96
|
+
from schemathesis.specs.openapi.checks import status_code_conformance # noqa: F401, F403
|
97
|
+
|
98
|
+
|
99
|
+
def check(func: CheckFunction) -> CheckFunction:
|
100
|
+
"""Register a custom validation check to run against API responses.
|
101
|
+
|
102
|
+
Args:
|
103
|
+
func: Function that takes `(ctx: CheckContext, response: Response, case: Case)` and raises `AssertionError` on validation failure
|
104
|
+
|
105
|
+
Example:
|
106
|
+
```python
|
107
|
+
import schemathesis
|
108
|
+
|
109
|
+
@schemathesis.check
|
110
|
+
def check_cors_headers(ctx, response, case):
|
111
|
+
\"\"\"Verify CORS headers are present\"\"\"
|
112
|
+
if "Access-Control-Allow-Origin" not in response.headers:
|
113
|
+
raise AssertionError("Missing CORS headers")
|
114
|
+
```
|
115
|
+
|
116
|
+
"""
|
117
|
+
return CHECKS.register(func)
|
92
118
|
|
93
119
|
|
94
120
|
@check
|
95
121
|
def not_a_server_error(ctx: CheckContext, response: Response, case: Case) -> bool | None:
|
96
122
|
"""A check to verify that the response is not a server-side error."""
|
97
|
-
from .specs.graphql.schemas import GraphQLSchema
|
98
|
-
from .specs.graphql.validation import validate_graphql_response
|
123
|
+
from schemathesis.specs.graphql.schemas import GraphQLSchema
|
124
|
+
from schemathesis.specs.graphql.validation import validate_graphql_response
|
125
|
+
from schemathesis.specs.openapi.utils import expand_status_codes
|
126
|
+
|
127
|
+
expected_statuses = expand_status_codes(ctx.config.not_a_server_error.expected_statuses or [])
|
99
128
|
|
100
129
|
status_code = response.status_code
|
101
|
-
if status_code
|
130
|
+
if status_code not in expected_statuses:
|
102
131
|
raise ServerError(operation=case.operation.label, status_code=status_code)
|
103
132
|
if isinstance(case.operation.schema, GraphQLSchema):
|
104
133
|
try:
|
@@ -6,8 +6,7 @@ from typing import Any, Callable
|
|
6
6
|
import click
|
7
7
|
from click.utils import LazyFile
|
8
8
|
|
9
|
-
from schemathesis import
|
10
|
-
from schemathesis.checks import CHECKS
|
9
|
+
from schemathesis.checks import CHECKS, load_all_checks
|
11
10
|
from schemathesis.cli.commands.run import executor, validation
|
12
11
|
from schemathesis.cli.commands.run.filters import with_filters
|
13
12
|
from schemathesis.cli.constants import DEFAULT_WORKERS, MAX_WORKERS, MIN_WORKERS
|
@@ -19,14 +18,19 @@ from schemathesis.cli.ext.options import (
|
|
19
18
|
CustomHelpMessageChoice,
|
20
19
|
RegistryChoice,
|
21
20
|
)
|
22
|
-
from schemathesis.config import
|
21
|
+
from schemathesis.config import (
|
22
|
+
DEFAULT_REPORT_DIRECTORY,
|
23
|
+
HealthCheck,
|
24
|
+
ReportFormat,
|
25
|
+
SchemathesisConfig,
|
26
|
+
SchemathesisWarning,
|
27
|
+
)
|
23
28
|
from schemathesis.core import HYPOTHESIS_IN_MEMORY_DATABASE_IDENTIFIER
|
24
29
|
from schemathesis.core.transport import DEFAULT_RESPONSE_TIMEOUT
|
25
|
-
from schemathesis.generation import
|
26
|
-
from schemathesis.generation.
|
30
|
+
from schemathesis.generation import GenerationMode
|
31
|
+
from schemathesis.generation.metrics import METRICS, MetricFunction
|
27
32
|
|
28
|
-
|
29
|
-
from schemathesis.specs.openapi.checks import * # noqa: F401, F403
|
33
|
+
load_all_checks()
|
30
34
|
|
31
35
|
COLOR_OPTIONS_INVALID_USAGE_MESSAGE = "Can't use `--no-color` and `--force-color` simultaneously"
|
32
36
|
|
@@ -83,6 +87,14 @@ DEFAULT_PHASES = ["examples", "coverage", "fuzzing", "stateful"]
|
|
83
87
|
default=None,
|
84
88
|
envvar="SCHEMATHESIS_WAIT_FOR_SCHEMA",
|
85
89
|
)
|
90
|
+
@grouped_option(
|
91
|
+
"--warnings",
|
92
|
+
help="Control warning display: 'off' to disable all, or comma-separated list of warning types to enable",
|
93
|
+
type=str,
|
94
|
+
default=None,
|
95
|
+
callback=validation.validate_warnings,
|
96
|
+
metavar="WARNINGS",
|
97
|
+
)
|
86
98
|
@group("API validation options")
|
87
99
|
@grouped_option(
|
88
100
|
"--checks",
|
@@ -296,7 +308,7 @@ DEFAULT_PHASES = ["examples", "coverage", "fuzzing", "stateful"]
|
|
296
308
|
"generation_modes",
|
297
309
|
help="Test data generation mode",
|
298
310
|
type=click.Choice([item.value for item in GenerationMode] + ["all"]),
|
299
|
-
default=
|
311
|
+
default="all",
|
300
312
|
callback=validation.convert_generation_mode,
|
301
313
|
show_default=True,
|
302
314
|
metavar="",
|
@@ -349,7 +361,7 @@ DEFAULT_PHASES = ["examples", "coverage", "fuzzing", "stateful"]
|
|
349
361
|
"generation_maximize",
|
350
362
|
multiple=True,
|
351
363
|
help="Guide input generation to values more likely to expose bugs via targeted property-based testing",
|
352
|
-
type=RegistryChoice(
|
364
|
+
type=RegistryChoice(METRICS),
|
353
365
|
default=None,
|
354
366
|
callback=validation.convert_maximize,
|
355
367
|
show_default=True,
|
@@ -390,15 +402,6 @@ DEFAULT_PHASES = ["examples", "coverage", "fuzzing", "stateful"]
|
|
390
402
|
show_default=True,
|
391
403
|
metavar="BOOLEAN",
|
392
404
|
)
|
393
|
-
@grouped_option(
|
394
|
-
"--contrib-openapi-fill-missing-examples",
|
395
|
-
"contrib_openapi_fill_missing_examples",
|
396
|
-
help="Enable generation of random examples for API operations that do not have explicit examples",
|
397
|
-
is_flag=True,
|
398
|
-
default=False,
|
399
|
-
show_default=True,
|
400
|
-
metavar="BOOLEAN",
|
401
|
-
)
|
402
405
|
@group("Global options")
|
403
406
|
@grouped_option("--no-color", help="Disable ANSI color escape codes", type=bool, is_flag=True)
|
404
407
|
@grouped_option("--force-color", help="Explicitly tells to enable ANSI color escape codes", type=bool, is_flag=True)
|
@@ -442,6 +445,7 @@ def run(
|
|
442
445
|
base_url: str | None,
|
443
446
|
wait_for_schema: float | None = None,
|
444
447
|
suppress_health_check: list[HealthCheck] | None,
|
448
|
+
warnings: bool | list[SchemathesisWarning] | None,
|
445
449
|
rate_limit: str | None = None,
|
446
450
|
request_timeout: int | None = None,
|
447
451
|
request_tls_verify: bool = True,
|
@@ -456,11 +460,10 @@ def run(
|
|
456
460
|
report_preserve_bytes: bool = False,
|
457
461
|
output_sanitize: bool = True,
|
458
462
|
output_truncate: bool = True,
|
459
|
-
|
460
|
-
generation_modes: list[GenerationMode] = DEFAULT_GENERATOR_MODES,
|
463
|
+
generation_modes: list[GenerationMode],
|
461
464
|
generation_seed: int | None = None,
|
462
465
|
generation_max_examples: int | None = None,
|
463
|
-
generation_maximize: list[
|
466
|
+
generation_maximize: list[MetricFunction] | None,
|
464
467
|
generation_deterministic: bool = False,
|
465
468
|
generation_database: str | None = None,
|
466
469
|
generation_unique_inputs: bool = False,
|
@@ -473,10 +476,14 @@ def run(
|
|
473
476
|
no_color: bool = False,
|
474
477
|
**__kwargs: Any,
|
475
478
|
) -> None:
|
476
|
-
"""
|
479
|
+
"""Generate and run property-based tests against your API.
|
477
480
|
|
478
|
-
|
479
|
-
|
481
|
+
\b
|
482
|
+
LOCATION can be:
|
483
|
+
- Local file: ./openapi.json, ./schema.yaml, ./schema.graphql
|
484
|
+
- OpenAPI URL: https://api.example.com/openapi.json
|
485
|
+
- GraphQL URL: https://api.example.com/graphql/
|
486
|
+
""" # noqa: D301
|
480
487
|
if no_color and force_color:
|
481
488
|
raise click.UsageError(COLOR_OPTIONS_INVALID_USAGE_MESSAGE)
|
482
489
|
|
@@ -494,9 +501,6 @@ def run(
|
|
494
501
|
|
495
502
|
validation.validate_auth_overlap(auth, headers)
|
496
503
|
|
497
|
-
if contrib_openapi_fill_missing_examples:
|
498
|
-
contrib.openapi.fill_missing_examples.install()
|
499
|
-
|
500
504
|
# Then override the global config from CLI options
|
501
505
|
config.update(
|
502
506
|
color=color,
|
@@ -528,6 +532,7 @@ def run(
|
|
528
532
|
request_cert=request_cert,
|
529
533
|
request_cert_key=request_cert_key,
|
530
534
|
proxy=request_proxy,
|
535
|
+
warnings=warnings,
|
531
536
|
)
|
532
537
|
# These are filters for what API operations should be tested
|
533
538
|
filter_set = {
|
@@ -1,8 +1,9 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
3
|
from dataclasses import dataclass, field
|
4
|
-
from typing import TYPE_CHECKING, Generator
|
4
|
+
from typing import TYPE_CHECKING, Callable, Generator
|
5
5
|
|
6
|
+
from schemathesis.cli.commands.run.events import LoadingFinished
|
6
7
|
from schemathesis.config import ProjectConfig
|
7
8
|
from schemathesis.core.failures import Failure
|
8
9
|
from schemathesis.core.result import Err, Ok
|
@@ -11,6 +12,7 @@ from schemathesis.core.transport import Response
|
|
11
12
|
from schemathesis.engine import Status, events
|
12
13
|
from schemathesis.engine.recorder import CaseNode, ScenarioRecorder
|
13
14
|
from schemathesis.generation.case import Case
|
15
|
+
from schemathesis.schemas import APIOperation
|
14
16
|
|
15
17
|
if TYPE_CHECKING:
|
16
18
|
from schemathesis.generation.stateful.state_machine import ExtractionFailure
|
@@ -177,6 +179,7 @@ class ExecutionContext:
|
|
177
179
|
"""Storage for the current context of the execution."""
|
178
180
|
|
179
181
|
config: ProjectConfig
|
182
|
+
find_operation_by_label: Callable[[str], APIOperation | None] | None = None
|
180
183
|
statistic: Statistic = field(default_factory=Statistic)
|
181
184
|
exit_code: int = 0
|
182
185
|
initialization_lines: list[str | Generator[str, None, None]] = field(default_factory=list)
|
@@ -189,6 +192,8 @@ class ExecutionContext:
|
|
189
192
|
self.summary_lines.append(line)
|
190
193
|
|
191
194
|
def on_event(self, event: events.EngineEvent) -> None:
|
195
|
+
if isinstance(event, LoadingFinished):
|
196
|
+
self.find_operation_by_label = event.find_operation_by_label
|
192
197
|
if isinstance(event, events.ScenarioFinished):
|
193
198
|
self.statistic.on_scenario_finished(event.recorder)
|
194
199
|
elif isinstance(event, events.NonFatalError) or (
|
@@ -1,10 +1,13 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
1
3
|
import time
|
2
4
|
import uuid
|
5
|
+
from typing import Callable
|
3
6
|
|
4
7
|
from schemathesis.config import ProjectConfig
|
5
8
|
from schemathesis.core import Specification
|
6
9
|
from schemathesis.engine import events
|
7
|
-
from schemathesis.schemas import ApiStatistic
|
10
|
+
from schemathesis.schemas import APIOperation, ApiStatistic
|
8
11
|
|
9
12
|
|
10
13
|
class LoadingStarted(events.EngineEvent):
|
@@ -28,6 +31,7 @@ class LoadingFinished(events.EngineEvent):
|
|
28
31
|
"statistic",
|
29
32
|
"schema",
|
30
33
|
"config",
|
34
|
+
"find_operation_by_label",
|
31
35
|
)
|
32
36
|
|
33
37
|
def __init__(
|
@@ -41,6 +45,7 @@ class LoadingFinished(events.EngineEvent):
|
|
41
45
|
statistic: ApiStatistic,
|
42
46
|
schema: dict,
|
43
47
|
config: ProjectConfig,
|
48
|
+
find_operation_by_label: Callable[[str], APIOperation | None],
|
44
49
|
) -> None:
|
45
50
|
self.id = uuid.uuid4()
|
46
51
|
self.timestamp = time.time()
|
@@ -52,3 +57,4 @@ class LoadingFinished(events.EngineEvent):
|
|
52
57
|
self.schema = schema
|
53
58
|
self.base_path = base_path
|
54
59
|
self.config = config
|
60
|
+
self.find_operation_by_label = find_operation_by_label
|
@@ -76,6 +76,7 @@ def into_event_stream(*, location: str, config: ProjectConfig, filter_set: dict[
|
|
76
76
|
schema=schema.raw_schema,
|
77
77
|
config=schema.config,
|
78
78
|
base_path=schema.base_path,
|
79
|
+
find_operation_by_label=schema.find_operation_by_label,
|
79
80
|
)
|
80
81
|
|
81
82
|
try:
|
@@ -121,17 +122,21 @@ def _execute(
|
|
121
122
|
args: list[str],
|
122
123
|
params: dict[str, Any],
|
123
124
|
) -> None:
|
124
|
-
handlers
|
125
|
-
ctx =
|
125
|
+
handlers: list[EventHandler] = []
|
126
|
+
ctx: ExecutionContext | None = None
|
126
127
|
|
127
128
|
def shutdown() -> None:
|
128
|
-
|
129
|
-
_handler
|
130
|
-
|
131
|
-
for handler in handlers:
|
132
|
-
handler.start(ctx)
|
129
|
+
if ctx is not None:
|
130
|
+
for _handler in handlers:
|
131
|
+
_handler.shutdown(ctx)
|
133
132
|
|
134
133
|
try:
|
134
|
+
handlers = initialize_handlers(config=config, args=args, params=params)
|
135
|
+
ctx = ExecutionContext(config=config)
|
136
|
+
|
137
|
+
for handler in handlers:
|
138
|
+
handler.start(ctx)
|
139
|
+
|
135
140
|
for event in event_stream:
|
136
141
|
ctx.on_event(event)
|
137
142
|
for handler in handlers:
|