schemathesis 3.25.5__py3-none-any.whl → 3.26.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- schemathesis/_dependency_versions.py +1 -0
- schemathesis/_hypothesis.py +1 -0
- schemathesis/_xml.py +1 -0
- schemathesis/auths.py +1 -0
- schemathesis/cli/__init__.py +39 -37
- schemathesis/cli/cassettes.py +4 -4
- schemathesis/cli/context.py +6 -0
- schemathesis/cli/output/default.py +185 -45
- schemathesis/cli/output/short.py +8 -0
- schemathesis/experimental/__init__.py +7 -0
- schemathesis/filters.py +1 -0
- schemathesis/models.py +5 -2
- schemathesis/parameters.py +1 -0
- schemathesis/runner/__init__.py +36 -9
- schemathesis/runner/events.py +33 -1
- schemathesis/runner/impl/core.py +99 -23
- schemathesis/{cli → runner}/probes.py +32 -21
- schemathesis/runner/serialization.py +4 -2
- schemathesis/schemas.py +1 -0
- schemathesis/serializers.py +11 -3
- schemathesis/service/client.py +35 -2
- schemathesis/service/extensions.py +224 -0
- schemathesis/service/hosts.py +1 -0
- schemathesis/service/metadata.py +24 -0
- schemathesis/service/models.py +210 -2
- schemathesis/service/serialization.py +44 -1
- schemathesis/specs/openapi/__init__.py +1 -0
- schemathesis/specs/openapi/_hypothesis.py +9 -1
- schemathesis/specs/openapi/examples.py +22 -24
- schemathesis/specs/openapi/expressions/__init__.py +1 -0
- schemathesis/specs/openapi/expressions/lexer.py +1 -0
- schemathesis/specs/openapi/expressions/nodes.py +1 -0
- schemathesis/specs/openapi/links.py +1 -0
- schemathesis/specs/openapi/media_types.py +34 -0
- schemathesis/specs/openapi/negative/mutations.py +1 -0
- schemathesis/specs/openapi/schemas.py +10 -3
- schemathesis/specs/openapi/security.py +5 -1
- {schemathesis-3.25.5.dist-info → schemathesis-3.26.0.dist-info}/METADATA +8 -5
- {schemathesis-3.25.5.dist-info → schemathesis-3.26.0.dist-info}/RECORD +42 -40
- {schemathesis-3.25.5.dist-info → schemathesis-3.26.0.dist-info}/WHEEL +0 -0
- {schemathesis-3.25.5.dist-info → schemathesis-3.26.0.dist-info}/entry_points.txt +0 -0
- {schemathesis-3.25.5.dist-info → schemathesis-3.26.0.dist-info}/licenses/LICENSE +0 -0
schemathesis/runner/__init__.py
CHANGED
|
@@ -1,34 +1,37 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from random import Random
|
|
4
|
-
from typing import Any, Callable, Generator, Iterable
|
|
4
|
+
from typing import TYPE_CHECKING, Any, Callable, Generator, Iterable
|
|
5
5
|
from urllib.parse import urlparse
|
|
6
6
|
|
|
7
7
|
from .._override import CaseOverride
|
|
8
|
-
from ..generation import DEFAULT_DATA_GENERATION_METHODS, DataGenerationMethod, GenerationConfig
|
|
9
8
|
from ..constants import (
|
|
10
9
|
DEFAULT_DEADLINE,
|
|
11
10
|
DEFAULT_STATEFUL_RECURSION_LIMIT,
|
|
12
11
|
HYPOTHESIS_IN_MEMORY_DATABASE_IDENTIFIER,
|
|
13
12
|
)
|
|
14
|
-
from ..
|
|
13
|
+
from ..exceptions import SchemaError
|
|
14
|
+
from ..generation import DEFAULT_DATA_GENERATION_METHODS, DataGenerationMethod, GenerationConfig
|
|
15
15
|
from ..internal.datetime import current_datetime
|
|
16
|
+
from ..internal.deprecation import deprecated_function
|
|
16
17
|
from ..internal.validation import file_exists
|
|
17
|
-
from ..transports.auth import get_requests_auth
|
|
18
|
-
from ..exceptions import SchemaError
|
|
19
18
|
from ..loaders import load_app
|
|
20
19
|
from ..specs.graphql import loaders as gql_loaders
|
|
21
20
|
from ..specs.openapi import loaders as oas_loaders
|
|
22
21
|
from ..targets import DEFAULT_TARGETS, Target
|
|
22
|
+
from ..transports.auth import get_requests_auth
|
|
23
23
|
from ..types import Filter, NotSet, RawAuth, RequestCert
|
|
24
|
+
from .probes import ProbeConfig
|
|
24
25
|
|
|
25
26
|
if TYPE_CHECKING:
|
|
26
|
-
|
|
27
|
+
import hypothesis
|
|
28
|
+
|
|
27
29
|
from ..models import CheckFunction
|
|
28
30
|
from ..schemas import BaseSchema
|
|
29
|
-
from .impl import BaseRunner
|
|
30
31
|
from ..stateful import Stateful
|
|
31
|
-
import
|
|
32
|
+
from . import events
|
|
33
|
+
from .impl import BaseRunner
|
|
34
|
+
from ..service.client import ServiceClient
|
|
32
35
|
|
|
33
36
|
|
|
34
37
|
@deprecated_function(removed_in="4.0", replacement="schemathesis.runner.from_schema")
|
|
@@ -75,6 +78,8 @@ def prepare(
|
|
|
75
78
|
hypothesis_report_multiple_bugs: bool | None = None,
|
|
76
79
|
hypothesis_suppress_health_check: list[hypothesis.HealthCheck] | None = None,
|
|
77
80
|
hypothesis_verbosity: hypothesis.Verbosity | None = None,
|
|
81
|
+
probe_config: ProbeConfig | None = None,
|
|
82
|
+
service_client: ServiceClient | None = None,
|
|
78
83
|
) -> Generator[events.ExecutionEvent, None, None]:
|
|
79
84
|
"""Prepare a generator that will run test cases against the given API definition."""
|
|
80
85
|
from ..checks import DEFAULT_CHECKS
|
|
@@ -128,6 +133,8 @@ def prepare(
|
|
|
128
133
|
stateful_recursion_limit=stateful_recursion_limit,
|
|
129
134
|
count_operations=count_operations,
|
|
130
135
|
count_links=count_links,
|
|
136
|
+
probe_config=probe_config,
|
|
137
|
+
service_client=service_client,
|
|
131
138
|
)
|
|
132
139
|
|
|
133
140
|
|
|
@@ -188,6 +195,8 @@ def execute_from_schema(
|
|
|
188
195
|
stateful_recursion_limit: int = DEFAULT_STATEFUL_RECURSION_LIMIT,
|
|
189
196
|
count_operations: bool = True,
|
|
190
197
|
count_links: bool = True,
|
|
198
|
+
probe_config: ProbeConfig | None = None,
|
|
199
|
+
service_client: ServiceClient | None,
|
|
191
200
|
) -> Generator[events.ExecutionEvent, None, None]:
|
|
192
201
|
"""Execute tests for the given schema.
|
|
193
202
|
|
|
@@ -237,6 +246,8 @@ def execute_from_schema(
|
|
|
237
246
|
stateful_recursion_limit=stateful_recursion_limit,
|
|
238
247
|
count_operations=count_operations,
|
|
239
248
|
count_links=count_links,
|
|
249
|
+
probe_config=probe_config,
|
|
250
|
+
service_client=service_client,
|
|
240
251
|
).execute()
|
|
241
252
|
except SchemaError as error:
|
|
242
253
|
yield events.InternalError.from_schema_error(error)
|
|
@@ -341,9 +352,12 @@ def from_schema(
|
|
|
341
352
|
stateful_recursion_limit: int = DEFAULT_STATEFUL_RECURSION_LIMIT,
|
|
342
353
|
count_operations: bool = True,
|
|
343
354
|
count_links: bool = True,
|
|
355
|
+
probe_config: ProbeConfig | None = None,
|
|
356
|
+
service_client: ServiceClient | None = None,
|
|
344
357
|
) -> BaseRunner:
|
|
345
|
-
from starlette.applications import Starlette
|
|
346
358
|
import hypothesis
|
|
359
|
+
from starlette.applications import Starlette
|
|
360
|
+
|
|
347
361
|
from ..checks import DEFAULT_CHECKS
|
|
348
362
|
from .impl import (
|
|
349
363
|
SingleThreadASGIRunner,
|
|
@@ -355,6 +369,7 @@ def from_schema(
|
|
|
355
369
|
)
|
|
356
370
|
|
|
357
371
|
checks = checks or DEFAULT_CHECKS
|
|
372
|
+
probe_config = probe_config or ProbeConfig()
|
|
358
373
|
|
|
359
374
|
hypothesis_settings = hypothesis_settings or hypothesis.settings(deadline=DEFAULT_DEADLINE)
|
|
360
375
|
generation_config = generation_config or GenerationConfig()
|
|
@@ -392,6 +407,8 @@ def from_schema(
|
|
|
392
407
|
stateful_recursion_limit=stateful_recursion_limit,
|
|
393
408
|
count_operations=count_operations,
|
|
394
409
|
count_links=count_links,
|
|
410
|
+
probe_config=probe_config,
|
|
411
|
+
service_client=service_client,
|
|
395
412
|
)
|
|
396
413
|
if isinstance(schema.app, Starlette):
|
|
397
414
|
return ThreadPoolASGIRunner(
|
|
@@ -415,6 +432,8 @@ def from_schema(
|
|
|
415
432
|
stateful_recursion_limit=stateful_recursion_limit,
|
|
416
433
|
count_operations=count_operations,
|
|
417
434
|
count_links=count_links,
|
|
435
|
+
probe_config=probe_config,
|
|
436
|
+
service_client=service_client,
|
|
418
437
|
)
|
|
419
438
|
return ThreadPoolWSGIRunner(
|
|
420
439
|
schema=schema,
|
|
@@ -438,6 +457,8 @@ def from_schema(
|
|
|
438
457
|
stateful_recursion_limit=stateful_recursion_limit,
|
|
439
458
|
count_operations=count_operations,
|
|
440
459
|
count_links=count_links,
|
|
460
|
+
probe_config=probe_config,
|
|
461
|
+
service_client=service_client,
|
|
441
462
|
)
|
|
442
463
|
if not schema.app:
|
|
443
464
|
return SingleThreadRunner(
|
|
@@ -465,6 +486,8 @@ def from_schema(
|
|
|
465
486
|
stateful_recursion_limit=stateful_recursion_limit,
|
|
466
487
|
count_operations=count_operations,
|
|
467
488
|
count_links=count_links,
|
|
489
|
+
probe_config=probe_config,
|
|
490
|
+
service_client=service_client,
|
|
468
491
|
)
|
|
469
492
|
if isinstance(schema.app, Starlette):
|
|
470
493
|
return SingleThreadASGIRunner(
|
|
@@ -488,6 +511,8 @@ def from_schema(
|
|
|
488
511
|
stateful_recursion_limit=stateful_recursion_limit,
|
|
489
512
|
count_operations=count_operations,
|
|
490
513
|
count_links=count_links,
|
|
514
|
+
probe_config=probe_config,
|
|
515
|
+
service_client=service_client,
|
|
491
516
|
)
|
|
492
517
|
return SingleThreadWSGIRunner(
|
|
493
518
|
schema=schema,
|
|
@@ -510,6 +535,8 @@ def from_schema(
|
|
|
510
535
|
stateful_recursion_limit=stateful_recursion_limit,
|
|
511
536
|
count_operations=count_operations,
|
|
512
537
|
count_links=count_links,
|
|
538
|
+
probe_config=probe_config,
|
|
539
|
+
service_client=service_client,
|
|
513
540
|
)
|
|
514
541
|
|
|
515
542
|
|
schemathesis/runner/events.py
CHANGED
|
@@ -6,6 +6,7 @@ from dataclasses import asdict, dataclass, field
|
|
|
6
6
|
from typing import Any, TYPE_CHECKING
|
|
7
7
|
|
|
8
8
|
from ..internal.datetime import current_datetime
|
|
9
|
+
from ..internal.result import Result
|
|
9
10
|
from ..generation import DataGenerationMethod
|
|
10
11
|
from ..exceptions import SchemaError, SchemaErrorType, format_exception, RuntimeErrorType
|
|
11
12
|
from .serialization import SerializedError, SerializedTestResult
|
|
@@ -14,6 +15,8 @@ from .serialization import SerializedError, SerializedTestResult
|
|
|
14
15
|
if TYPE_CHECKING:
|
|
15
16
|
from ..models import APIOperation, Status, TestResult, TestResultSet
|
|
16
17
|
from ..schemas import BaseSchema
|
|
18
|
+
from ..service.models import AnalysisResult
|
|
19
|
+
from . import probes
|
|
17
20
|
|
|
18
21
|
|
|
19
22
|
@dataclass
|
|
@@ -60,6 +63,7 @@ class Initialized(ExecutionEvent):
|
|
|
60
63
|
schema: BaseSchema,
|
|
61
64
|
count_operations: bool = True,
|
|
62
65
|
count_links: bool = True,
|
|
66
|
+
start_time: float | None = None,
|
|
63
67
|
started_at: str | None = None,
|
|
64
68
|
seed: int | None,
|
|
65
69
|
) -> Initialized:
|
|
@@ -70,12 +74,37 @@ class Initialized(ExecutionEvent):
|
|
|
70
74
|
links_count=schema.links_count if count_links else None,
|
|
71
75
|
location=schema.location,
|
|
72
76
|
base_url=schema.get_base_url(),
|
|
77
|
+
start_time=start_time or time.monotonic(),
|
|
73
78
|
started_at=started_at or current_datetime(),
|
|
74
79
|
specification_name=schema.verbose_name,
|
|
75
80
|
seed=seed,
|
|
76
81
|
)
|
|
77
82
|
|
|
78
83
|
|
|
84
|
+
@dataclass
|
|
85
|
+
class BeforeProbing(ExecutionEvent):
|
|
86
|
+
pass
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@dataclass
|
|
90
|
+
class AfterProbing(ExecutionEvent):
|
|
91
|
+
probes: list[probes.ProbeRun] | None
|
|
92
|
+
|
|
93
|
+
def asdict(self, **kwargs: Any) -> dict[str, Any]:
|
|
94
|
+
probes = self.probes or []
|
|
95
|
+
return {"probes": [probe.serialize() for probe in probes], "events_type": self.__class__.__name__}
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@dataclass
|
|
99
|
+
class BeforeAnalysis(ExecutionEvent):
|
|
100
|
+
pass
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@dataclass
|
|
104
|
+
class AfterAnalysis(ExecutionEvent):
|
|
105
|
+
analysis: Result[AnalysisResult, Exception] | None
|
|
106
|
+
|
|
107
|
+
|
|
79
108
|
class CurrentOperationMixin:
|
|
80
109
|
method: str
|
|
81
110
|
path: str
|
|
@@ -188,6 +217,9 @@ class InternalErrorType(str, enum.Enum):
|
|
|
188
217
|
OTHER = "other"
|
|
189
218
|
|
|
190
219
|
|
|
220
|
+
DEFAULT_INTERNAL_ERROR_MESSAGE = "An internal error occurred during the test run"
|
|
221
|
+
|
|
222
|
+
|
|
191
223
|
@dataclass
|
|
192
224
|
class InternalError(ExecutionEvent):
|
|
193
225
|
"""An error that happened inside the runner."""
|
|
@@ -226,7 +258,7 @@ class InternalError(ExecutionEvent):
|
|
|
226
258
|
type_=InternalErrorType.OTHER,
|
|
227
259
|
subtype=None,
|
|
228
260
|
title="Test Execution Error",
|
|
229
|
-
message=
|
|
261
|
+
message=DEFAULT_INTERNAL_ERROR_MESSAGE,
|
|
230
262
|
extras=[],
|
|
231
263
|
)
|
|
232
264
|
|
schemathesis/runner/impl/core.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
|
+
|
|
2
3
|
import logging
|
|
3
4
|
import re
|
|
4
5
|
import threading
|
|
@@ -8,7 +9,7 @@ import uuid
|
|
|
8
9
|
from contextlib import contextmanager
|
|
9
10
|
from dataclasses import dataclass, field
|
|
10
11
|
from types import TracebackType
|
|
11
|
-
from typing import Any, Callable, Generator, Iterable,
|
|
12
|
+
from typing import TYPE_CHECKING, Any, Callable, Generator, Iterable, List, Literal, cast
|
|
12
13
|
from warnings import WarningMessage, catch_warnings
|
|
13
14
|
|
|
14
15
|
import hypothesis
|
|
@@ -16,53 +17,59 @@ import requests
|
|
|
16
17
|
from _pytest.logging import LogCaptureHandler, catching_logs
|
|
17
18
|
from hypothesis.errors import HypothesisException, InvalidArgument
|
|
18
19
|
from hypothesis_jsonschema._canonicalise import HypothesisRefResolutionError
|
|
19
|
-
from jsonschema.exceptions import
|
|
20
|
+
from jsonschema.exceptions import SchemaError as JsonSchemaError
|
|
21
|
+
from jsonschema.exceptions import ValidationError
|
|
20
22
|
from requests.auth import HTTPDigestAuth, _basic_auth_str
|
|
21
23
|
|
|
22
|
-
from ..._override import CaseOverride
|
|
23
24
|
from ... import failures, hooks
|
|
24
25
|
from ..._compat import MultipleFailures
|
|
25
26
|
from ..._hypothesis import (
|
|
26
|
-
has_unsatisfied_example_mark,
|
|
27
|
-
get_non_serializable_mark,
|
|
28
|
-
get_invalid_regex_mark,
|
|
29
27
|
get_invalid_example_headers_mark,
|
|
28
|
+
get_invalid_regex_mark,
|
|
29
|
+
get_non_serializable_mark,
|
|
30
|
+
has_unsatisfied_example_mark,
|
|
30
31
|
)
|
|
32
|
+
from ..._override import CaseOverride
|
|
31
33
|
from ...auths import unregister as unregister_auth
|
|
32
|
-
from ...generation import DataGenerationMethod, GenerationConfig
|
|
33
34
|
from ...constants import (
|
|
34
35
|
DEFAULT_STATEFUL_RECURSION_LIMIT,
|
|
35
36
|
RECURSIVE_REFERENCE_ERROR_MESSAGE,
|
|
36
|
-
USER_AGENT,
|
|
37
37
|
SERIALIZERS_SUGGESTION_MESSAGE,
|
|
38
|
+
USER_AGENT,
|
|
38
39
|
)
|
|
39
40
|
from ...exceptions import (
|
|
40
41
|
CheckFailed,
|
|
41
42
|
DeadlineExceeded,
|
|
43
|
+
InvalidHeadersExample,
|
|
42
44
|
InvalidRegularExpression,
|
|
43
45
|
NonCheckError,
|
|
44
46
|
OperationSchemaError,
|
|
47
|
+
SerializationNotPossible,
|
|
45
48
|
SkipTest,
|
|
49
|
+
format_exception,
|
|
46
50
|
get_grouped_exception,
|
|
47
51
|
maybe_set_assertion_message,
|
|
48
|
-
format_exception,
|
|
49
|
-
SerializationNotPossible,
|
|
50
|
-
InvalidHeadersExample,
|
|
51
52
|
)
|
|
53
|
+
from ...generation import DataGenerationMethod, GenerationConfig
|
|
52
54
|
from ...hooks import HookContext, get_all_by_name
|
|
53
|
-
from ...internal.
|
|
55
|
+
from ...internal.datetime import current_datetime
|
|
56
|
+
from ...internal.result import Err, Ok, Result
|
|
54
57
|
from ...models import APIOperation, Case, Check, CheckFunction, Status, TestResult, TestResultSet
|
|
55
58
|
from ...runner import events
|
|
56
|
-
from ...internal.datetime import current_datetime
|
|
57
59
|
from ...schemas import BaseSchema
|
|
60
|
+
from ...service import extensions
|
|
61
|
+
from ...service.models import AnalysisResult, AnalysisSuccess
|
|
62
|
+
from ...specs.openapi import formats
|
|
58
63
|
from ...stateful import Feedback, Stateful
|
|
59
64
|
from ...targets import Target, TargetContext
|
|
60
65
|
from ...types import RawAuth, RequestCert
|
|
61
66
|
from ...utils import capture_hypothesis_output
|
|
67
|
+
from .. import probes
|
|
62
68
|
from ..serialization import SerializedTestResult
|
|
63
69
|
|
|
64
70
|
if TYPE_CHECKING:
|
|
65
|
-
from ...
|
|
71
|
+
from ...service.client import ServiceClient
|
|
72
|
+
from ...transports.responses import GenericResponse, WSGIResponse
|
|
66
73
|
|
|
67
74
|
|
|
68
75
|
def _should_count_towards_stop(event: events.ExecutionEvent) -> bool:
|
|
@@ -77,6 +84,7 @@ class BaseRunner:
|
|
|
77
84
|
targets: Iterable[Target]
|
|
78
85
|
hypothesis_settings: hypothesis.settings
|
|
79
86
|
generation_config: GenerationConfig
|
|
87
|
+
probe_config: probes.ProbeConfig
|
|
80
88
|
override: CaseOverride | None = None
|
|
81
89
|
auth: RawAuth | None = None
|
|
82
90
|
auth_type: str | None = None
|
|
@@ -92,6 +100,7 @@ class BaseRunner:
|
|
|
92
100
|
stateful_recursion_limit: int = DEFAULT_STATEFUL_RECURSION_LIMIT
|
|
93
101
|
count_operations: bool = True
|
|
94
102
|
count_links: bool = True
|
|
103
|
+
service_client: ServiceClient | None = None
|
|
95
104
|
_failures_counter: int = 0
|
|
96
105
|
|
|
97
106
|
def execute(self) -> EventStream:
|
|
@@ -104,26 +113,79 @@ class BaseRunner:
|
|
|
104
113
|
if self.auth is not None:
|
|
105
114
|
unregister_auth()
|
|
106
115
|
results = TestResultSet(seed=self.seed)
|
|
107
|
-
|
|
108
|
-
initialized =
|
|
109
|
-
|
|
110
|
-
|
|
116
|
+
start_time = time.monotonic()
|
|
117
|
+
initialized = None
|
|
118
|
+
__probes = None
|
|
119
|
+
__analysis: Result[AnalysisResult, Exception] | None = None
|
|
120
|
+
|
|
121
|
+
def _initialize() -> events.Initialized:
|
|
122
|
+
nonlocal initialized
|
|
123
|
+
initialized = events.Initialized.from_schema(
|
|
124
|
+
schema=self.schema,
|
|
125
|
+
count_operations=self.count_operations,
|
|
126
|
+
count_links=self.count_links,
|
|
127
|
+
seed=self.seed,
|
|
128
|
+
start_time=start_time,
|
|
129
|
+
)
|
|
130
|
+
return initialized
|
|
111
131
|
|
|
112
132
|
def _finish() -> events.Finished:
|
|
113
133
|
if has_all_not_found(results):
|
|
114
134
|
results.add_warning(ALL_NOT_FOUND_WARNING_MESSAGE)
|
|
115
|
-
return events.Finished.from_results(results=results, running_time=time.monotonic() -
|
|
135
|
+
return events.Finished.from_results(results=results, running_time=time.monotonic() - start_time)
|
|
116
136
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
137
|
+
def _before_probes() -> events.BeforeProbing:
|
|
138
|
+
return events.BeforeProbing()
|
|
139
|
+
|
|
140
|
+
def _run_probes() -> None:
|
|
141
|
+
if not self.dry_run:
|
|
142
|
+
nonlocal __probes
|
|
143
|
+
|
|
144
|
+
__probes = run_probes(self.schema, self.probe_config)
|
|
145
|
+
|
|
146
|
+
def _after_probes() -> events.AfterProbing:
|
|
147
|
+
_probes = cast(List[probes.ProbeRun], __probes)
|
|
148
|
+
return events.AfterProbing(probes=_probes)
|
|
120
149
|
|
|
121
|
-
|
|
150
|
+
def _before_analysis() -> events.BeforeAnalysis:
|
|
151
|
+
return events.BeforeAnalysis()
|
|
152
|
+
|
|
153
|
+
def _run_analysis() -> None:
|
|
154
|
+
nonlocal __analysis, __probes
|
|
155
|
+
|
|
156
|
+
if self.service_client is not None:
|
|
157
|
+
try:
|
|
158
|
+
_probes = cast(List[probes.ProbeRun], __probes)
|
|
159
|
+
result = self.service_client.analyze_schema(_probes, self.schema.raw_schema)
|
|
160
|
+
if isinstance(result, AnalysisSuccess):
|
|
161
|
+
extensions.apply(result.extensions, self.schema)
|
|
162
|
+
__analysis = Ok(result)
|
|
163
|
+
except Exception as exc:
|
|
164
|
+
__analysis = Err(exc)
|
|
165
|
+
|
|
166
|
+
def _after_analysis() -> events.AfterAnalysis:
|
|
167
|
+
return events.AfterAnalysis(analysis=__analysis)
|
|
122
168
|
|
|
123
169
|
if stop_event.is_set():
|
|
124
170
|
yield _finish()
|
|
125
171
|
return
|
|
126
172
|
|
|
173
|
+
for event_factory in (
|
|
174
|
+
_initialize,
|
|
175
|
+
_before_probes,
|
|
176
|
+
_run_probes,
|
|
177
|
+
_after_probes,
|
|
178
|
+
_before_analysis,
|
|
179
|
+
_run_analysis,
|
|
180
|
+
_after_analysis,
|
|
181
|
+
):
|
|
182
|
+
event = event_factory()
|
|
183
|
+
if event is not None:
|
|
184
|
+
yield event
|
|
185
|
+
if stop_event.is_set():
|
|
186
|
+
yield _finish()
|
|
187
|
+
return
|
|
188
|
+
|
|
127
189
|
try:
|
|
128
190
|
yield from self._execute(results, stop_event)
|
|
129
191
|
except KeyboardInterrupt:
|
|
@@ -228,6 +290,20 @@ class BaseRunner:
|
|
|
228
290
|
)
|
|
229
291
|
|
|
230
292
|
|
|
293
|
+
def run_probes(schema: BaseSchema, config: probes.ProbeConfig) -> list[probes.ProbeRun]:
|
|
294
|
+
"""Discover capabilities of the tested app."""
|
|
295
|
+
results = probes.run(schema, config)
|
|
296
|
+
for result in results:
|
|
297
|
+
if isinstance(result.probe, probes.NullByteInHeader) and result.is_failure:
|
|
298
|
+
from ...specs.openapi._hypothesis import HEADER_FORMAT, header_values
|
|
299
|
+
|
|
300
|
+
formats.register(
|
|
301
|
+
HEADER_FORMAT,
|
|
302
|
+
header_values(blacklist_characters="\n\r\x00").map(str.lstrip),
|
|
303
|
+
)
|
|
304
|
+
return results
|
|
305
|
+
|
|
306
|
+
|
|
231
307
|
@dataclass
|
|
232
308
|
class EventStream:
|
|
233
309
|
"""Schemathesis event stream.
|
|
@@ -5,6 +5,7 @@ the application supports certain inputs. This is done to avoid false positives i
|
|
|
5
5
|
For example, certail web servers do not support NULL bytes in headers, in such cases, the generated test case
|
|
6
6
|
will not reach the tested application at all.
|
|
7
7
|
"""
|
|
8
|
+
|
|
8
9
|
from __future__ import annotations
|
|
9
10
|
|
|
10
11
|
import enum
|
|
@@ -21,13 +22,24 @@ from ..transports.auth import get_requests_auth
|
|
|
21
22
|
if TYPE_CHECKING:
|
|
22
23
|
import requests
|
|
23
24
|
|
|
25
|
+
from ..types import RequestCert
|
|
24
26
|
from ..schemas import BaseSchema
|
|
25
|
-
from . import LoaderConfig
|
|
26
27
|
|
|
27
28
|
|
|
28
29
|
HEADER_NAME = "X-Schemathesis-Probe"
|
|
29
30
|
|
|
30
31
|
|
|
32
|
+
@dataclass
|
|
33
|
+
class ProbeConfig:
|
|
34
|
+
base_url: str | None = None
|
|
35
|
+
request_tls_verify: bool | str = True
|
|
36
|
+
request_proxy: str | None = None
|
|
37
|
+
request_cert: RequestCert | None = None
|
|
38
|
+
auth: tuple[str, str] | None = None
|
|
39
|
+
auth_type: str | None = None
|
|
40
|
+
headers: dict[str, str] | None = None
|
|
41
|
+
|
|
42
|
+
|
|
31
43
|
@dataclass
|
|
32
44
|
class Probe:
|
|
33
45
|
"""A request to determine the capabilities of the application under test."""
|
|
@@ -35,15 +47,15 @@ class Probe:
|
|
|
35
47
|
name: str
|
|
36
48
|
|
|
37
49
|
def prepare_request(
|
|
38
|
-
self, session: requests.Session, request: requests.Request, schema: BaseSchema, config:
|
|
50
|
+
self, session: requests.Session, request: requests.Request, schema: BaseSchema, config: ProbeConfig
|
|
39
51
|
) -> requests.PreparedRequest:
|
|
40
52
|
raise NotImplementedError
|
|
41
53
|
|
|
42
|
-
def analyze_response(self, response: requests.Response) ->
|
|
54
|
+
def analyze_response(self, response: requests.Response) -> ProbeOutcome:
|
|
43
55
|
raise NotImplementedError
|
|
44
56
|
|
|
45
57
|
|
|
46
|
-
class
|
|
58
|
+
class ProbeOutcome(str, enum.Enum):
|
|
47
59
|
# Capability is supported
|
|
48
60
|
SUCCESS = "success"
|
|
49
61
|
# Capability is not supported
|
|
@@ -55,18 +67,16 @@ class ProbeResultType(str, enum.Enum):
|
|
|
55
67
|
|
|
56
68
|
|
|
57
69
|
@dataclass
|
|
58
|
-
class
|
|
59
|
-
"""Result of a probe."""
|
|
60
|
-
|
|
70
|
+
class ProbeRun:
|
|
61
71
|
probe: Probe
|
|
62
|
-
|
|
72
|
+
outcome: ProbeOutcome
|
|
63
73
|
request: requests.PreparedRequest | None = None
|
|
64
74
|
response: requests.Response | None = None
|
|
65
75
|
error: requests.RequestException | None = None
|
|
66
76
|
|
|
67
77
|
@property
|
|
68
78
|
def is_failure(self) -> bool:
|
|
69
|
-
return self.
|
|
79
|
+
return self.outcome == ProbeOutcome.FAILURE
|
|
70
80
|
|
|
71
81
|
def serialize(self) -> dict[str, Any]:
|
|
72
82
|
"""Serialize probe results so it can be sent over the network."""
|
|
@@ -87,7 +97,7 @@ class ProbeResult:
|
|
|
87
97
|
error = None
|
|
88
98
|
return {
|
|
89
99
|
"name": self.probe.name,
|
|
90
|
-
"
|
|
100
|
+
"outcome": self.outcome.value,
|
|
91
101
|
"request": request,
|
|
92
102
|
"response": response,
|
|
93
103
|
"error": error,
|
|
@@ -101,25 +111,25 @@ class NullByteInHeader(Probe):
|
|
|
101
111
|
name: str = "NULL_BYTE_IN_HEADER"
|
|
102
112
|
|
|
103
113
|
def prepare_request(
|
|
104
|
-
self, session: requests.Session, request: requests.Request, schema: BaseSchema, config:
|
|
114
|
+
self, session: requests.Session, request: requests.Request, schema: BaseSchema, config: ProbeConfig
|
|
105
115
|
) -> requests.PreparedRequest:
|
|
106
116
|
request.method = "GET"
|
|
107
117
|
request.url = config.base_url or schema.get_base_url()
|
|
108
118
|
request.headers = {"X-Schemathesis-Probe-Null": "\x00"}
|
|
109
119
|
return session.prepare_request(request)
|
|
110
120
|
|
|
111
|
-
def analyze_response(self, response: requests.Response) ->
|
|
121
|
+
def analyze_response(self, response: requests.Response) -> ProbeOutcome:
|
|
112
122
|
if response.status_code == 400:
|
|
113
|
-
return
|
|
114
|
-
return
|
|
123
|
+
return ProbeOutcome.FAILURE
|
|
124
|
+
return ProbeOutcome.SUCCESS
|
|
115
125
|
|
|
116
126
|
|
|
117
127
|
PROBES = (NullByteInHeader,)
|
|
118
128
|
|
|
119
129
|
|
|
120
|
-
def send(probe: Probe, session: requests.Session, schema: BaseSchema, config:
|
|
130
|
+
def send(probe: Probe, session: requests.Session, schema: BaseSchema, config: ProbeConfig) -> ProbeRun:
|
|
121
131
|
"""Send the probe to the application."""
|
|
122
|
-
from requests import Request, RequestException
|
|
132
|
+
from requests import PreparedRequest, Request, RequestException
|
|
123
133
|
from requests.exceptions import MissingSchema
|
|
124
134
|
from urllib3.exceptions import InsecureRequestWarning
|
|
125
135
|
|
|
@@ -129,23 +139,24 @@ def send(probe: Probe, session: requests.Session, schema: BaseSchema, config: Lo
|
|
|
129
139
|
request.headers["User-Agent"] = USER_AGENT
|
|
130
140
|
with warnings.catch_warnings():
|
|
131
141
|
warnings.simplefilter("ignore", InsecureRequestWarning)
|
|
132
|
-
response = session.send(request)
|
|
142
|
+
response = session.send(request, timeout=2)
|
|
133
143
|
except MissingSchema:
|
|
134
144
|
# In-process ASGI/WSGI testing will have local URLs and requires extra handling
|
|
135
145
|
# which is not currently implemented
|
|
136
|
-
return
|
|
146
|
+
return ProbeRun(probe, ProbeOutcome.SKIP, None, None, None)
|
|
137
147
|
except RequestException as exc:
|
|
138
148
|
req = exc.request if isinstance(exc.request, PreparedRequest) else None
|
|
139
|
-
return
|
|
149
|
+
return ProbeRun(probe, ProbeOutcome.ERROR, req, None, exc)
|
|
140
150
|
result_type = probe.analyze_response(response)
|
|
141
|
-
return
|
|
151
|
+
return ProbeRun(probe, result_type, request, response)
|
|
142
152
|
|
|
143
153
|
|
|
144
|
-
def run(schema: BaseSchema, config:
|
|
154
|
+
def run(schema: BaseSchema, config: ProbeConfig) -> list[ProbeRun]:
|
|
145
155
|
"""Run all probes against the given schema."""
|
|
146
156
|
from requests import Session
|
|
147
157
|
|
|
148
158
|
session = Session()
|
|
159
|
+
session.headers.update(config.headers or {})
|
|
149
160
|
session.verify = config.request_tls_verify
|
|
150
161
|
if config.request_cert is not None:
|
|
151
162
|
session.cert = config.request_cert
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
They all consist of primitive types and don't have references to schemas, app, etc.
|
|
4
4
|
"""
|
|
5
|
+
|
|
5
6
|
from __future__ import annotations
|
|
6
7
|
import logging
|
|
7
8
|
import re
|
|
@@ -226,8 +227,9 @@ class SerializedError:
|
|
|
226
227
|
message = f"Scalar type '{scalar_name}' is not recognized"
|
|
227
228
|
extras = []
|
|
228
229
|
title = "Unknown GraphQL Scalar"
|
|
229
|
-
elif isinstance(exception, hypothesis.errors.InvalidArgument) and
|
|
230
|
-
"larger than Hypothesis is designed to handle"
|
|
230
|
+
elif isinstance(exception, hypothesis.errors.InvalidArgument) and (
|
|
231
|
+
str(exception).endswith("larger than Hypothesis is designed to handle")
|
|
232
|
+
or "can neber generate an example, because min_size is larger than Hypothesis suports."
|
|
231
233
|
):
|
|
232
234
|
type_ = RuntimeErrorType.HYPOTHESIS_HEALTH_CHECK_LARGE_BASE_EXAMPLE
|
|
233
235
|
message = HEALTH_CHECK_MESSAGE_LARGE_BASE_EXAMPLE
|
schemathesis/schemas.py
CHANGED
schemathesis/serializers.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
|
+
|
|
2
3
|
import binascii
|
|
3
4
|
import os
|
|
4
5
|
from dataclasses import dataclass
|
|
@@ -10,13 +11,14 @@ from typing import (
|
|
|
10
11
|
Collection,
|
|
11
12
|
Dict,
|
|
12
13
|
Generator,
|
|
13
|
-
cast,
|
|
14
14
|
Protocol,
|
|
15
|
+
cast,
|
|
15
16
|
runtime_checkable,
|
|
16
17
|
)
|
|
17
18
|
|
|
18
|
-
from .internal.copy import fast_deepcopy
|
|
19
19
|
from ._xml import _to_xml
|
|
20
|
+
from .internal.copy import fast_deepcopy
|
|
21
|
+
from .internal.jsonschema import traverse_schema
|
|
20
22
|
from .transports.content_types import (
|
|
21
23
|
is_json_media_type,
|
|
22
24
|
is_plain_text_media_type,
|
|
@@ -150,6 +152,10 @@ class JSONSerializer:
|
|
|
150
152
|
return _to_json(value)
|
|
151
153
|
|
|
152
154
|
|
|
155
|
+
def _replace_binary(value: dict) -> dict:
|
|
156
|
+
return {key: value.data if isinstance(value, Binary) else value for key, value in value.items()}
|
|
157
|
+
|
|
158
|
+
|
|
153
159
|
def _to_yaml(value: Any) -> dict[str, Any]:
|
|
154
160
|
import yaml
|
|
155
161
|
|
|
@@ -162,10 +168,12 @@ def _to_yaml(value: Any) -> dict[str, Any]:
|
|
|
162
168
|
return {"data": value}
|
|
163
169
|
if isinstance(value, Binary):
|
|
164
170
|
return {"data": value.data}
|
|
171
|
+
if isinstance(value, (list, dict)):
|
|
172
|
+
value = traverse_schema(value, _replace_binary)
|
|
165
173
|
return {"data": yaml.dump(value, Dumper=SafeDumper)}
|
|
166
174
|
|
|
167
175
|
|
|
168
|
-
@register("text/yaml", aliases=("text/x-yaml", "
|
|
176
|
+
@register("text/yaml", aliases=("text/x-yaml", "text/vnd.yaml", "text/yml", "application/yaml", "application/x-yaml"))
|
|
169
177
|
class YAMLSerializer:
|
|
170
178
|
def as_requests(self, context: SerializerContext, value: Any) -> dict[str, Any]:
|
|
171
179
|
return _to_yaml(value)
|