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.
Files changed (42) hide show
  1. schemathesis/_dependency_versions.py +1 -0
  2. schemathesis/_hypothesis.py +1 -0
  3. schemathesis/_xml.py +1 -0
  4. schemathesis/auths.py +1 -0
  5. schemathesis/cli/__init__.py +39 -37
  6. schemathesis/cli/cassettes.py +4 -4
  7. schemathesis/cli/context.py +6 -0
  8. schemathesis/cli/output/default.py +185 -45
  9. schemathesis/cli/output/short.py +8 -0
  10. schemathesis/experimental/__init__.py +7 -0
  11. schemathesis/filters.py +1 -0
  12. schemathesis/models.py +5 -2
  13. schemathesis/parameters.py +1 -0
  14. schemathesis/runner/__init__.py +36 -9
  15. schemathesis/runner/events.py +33 -1
  16. schemathesis/runner/impl/core.py +99 -23
  17. schemathesis/{cli → runner}/probes.py +32 -21
  18. schemathesis/runner/serialization.py +4 -2
  19. schemathesis/schemas.py +1 -0
  20. schemathesis/serializers.py +11 -3
  21. schemathesis/service/client.py +35 -2
  22. schemathesis/service/extensions.py +224 -0
  23. schemathesis/service/hosts.py +1 -0
  24. schemathesis/service/metadata.py +24 -0
  25. schemathesis/service/models.py +210 -2
  26. schemathesis/service/serialization.py +44 -1
  27. schemathesis/specs/openapi/__init__.py +1 -0
  28. schemathesis/specs/openapi/_hypothesis.py +9 -1
  29. schemathesis/specs/openapi/examples.py +22 -24
  30. schemathesis/specs/openapi/expressions/__init__.py +1 -0
  31. schemathesis/specs/openapi/expressions/lexer.py +1 -0
  32. schemathesis/specs/openapi/expressions/nodes.py +1 -0
  33. schemathesis/specs/openapi/links.py +1 -0
  34. schemathesis/specs/openapi/media_types.py +34 -0
  35. schemathesis/specs/openapi/negative/mutations.py +1 -0
  36. schemathesis/specs/openapi/schemas.py +10 -3
  37. schemathesis/specs/openapi/security.py +5 -1
  38. {schemathesis-3.25.5.dist-info → schemathesis-3.26.0.dist-info}/METADATA +8 -5
  39. {schemathesis-3.25.5.dist-info → schemathesis-3.26.0.dist-info}/RECORD +42 -40
  40. {schemathesis-3.25.5.dist-info → schemathesis-3.26.0.dist-info}/WHEEL +0 -0
  41. {schemathesis-3.25.5.dist-info → schemathesis-3.26.0.dist-info}/entry_points.txt +0 -0
  42. {schemathesis-3.25.5.dist-info → schemathesis-3.26.0.dist-info}/licenses/LICENSE +0 -0
@@ -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, TYPE_CHECKING
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 ..internal.deprecation import deprecated_function
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
- from . import events
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 hypothesis
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
 
@@ -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="An internal error occurred during the test run",
261
+ message=DEFAULT_INTERNAL_ERROR_MESSAGE,
230
262
  extras=[],
231
263
  )
232
264
 
@@ -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, cast, TYPE_CHECKING, Literal
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 ValidationError, SchemaError as JsonSchemaError
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.result import Ok
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 ...transports.responses import WSGIResponse, GenericResponse
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 = events.Initialized.from_schema(
109
- schema=self.schema, count_operations=self.count_operations, count_links=self.count_links, seed=self.seed
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() - initialized.start_time)
135
+ return events.Finished.from_results(results=results, running_time=time.monotonic() - start_time)
116
136
 
117
- if stop_event.is_set():
118
- yield _finish()
119
- return
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
- yield initialized
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: LoaderConfig
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) -> ProbeResultType:
54
+ def analyze_response(self, response: requests.Response) -> ProbeOutcome:
43
55
  raise NotImplementedError
44
56
 
45
57
 
46
- class ProbeResultType(str, enum.Enum):
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 ProbeResult:
59
- """Result of a probe."""
60
-
70
+ class ProbeRun:
61
71
  probe: Probe
62
- type: ProbeResultType
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.type == ProbeResultType.FAILURE
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
- "type": self.type.value,
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: LoaderConfig
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) -> ProbeResultType:
121
+ def analyze_response(self, response: requests.Response) -> ProbeOutcome:
112
122
  if response.status_code == 400:
113
- return ProbeResultType.FAILURE
114
- return ProbeResultType.SUCCESS
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: LoaderConfig) -> ProbeResult:
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, PreparedRequest
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 ProbeResult(probe, ProbeResultType.SKIP, None, None, None)
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 ProbeResult(probe, ProbeResultType.ERROR, req, None, exc)
149
+ return ProbeRun(probe, ProbeOutcome.ERROR, req, None, exc)
140
150
  result_type = probe.analyze_response(response)
141
- return ProbeResult(probe, result_type, request, response)
151
+ return ProbeRun(probe, result_type, request, response)
142
152
 
143
153
 
144
- def run(schema: BaseSchema, config: LoaderConfig) -> list[ProbeResult]:
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 str(exception).endswith(
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
@@ -6,6 +6,7 @@ Their responsibilities:
6
6
 
7
7
  They give only static definitions of paths.
8
8
  """
9
+
9
10
  from __future__ import annotations
10
11
  from collections.abc import Mapping, MutableMapping
11
12
  from contextlib import nullcontext
@@ -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", "application/x-yaml", "text/vnd.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)