schemathesis 3.25.5__py3-none-any.whl → 3.25.6__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/cli/__init__.py +13 -19
- schemathesis/cli/context.py +3 -0
- schemathesis/cli/output/default.py +32 -7
- schemathesis/cli/output/short.py +4 -0
- schemathesis/models.py +5 -2
- schemathesis/runner/__init__.py +24 -9
- schemathesis/runner/events.py +21 -1
- schemathesis/runner/impl/core.py +71 -22
- schemathesis/{cli → runner}/probes.py +31 -21
- schemathesis/serializers.py +10 -2
- schemathesis/service/serialization.py +15 -0
- schemathesis/specs/openapi/_hypothesis.py +1 -1
- schemathesis/specs/openapi/examples.py +22 -24
- schemathesis/specs/openapi/schemas.py +10 -3
- {schemathesis-3.25.5.dist-info → schemathesis-3.25.6.dist-info}/METADATA +1 -1
- {schemathesis-3.25.5.dist-info → schemathesis-3.25.6.dist-info}/RECORD +19 -19
- {schemathesis-3.25.5.dist-info → schemathesis-3.25.6.dist-info}/WHEEL +0 -0
- {schemathesis-3.25.5.dist-info → schemathesis-3.25.6.dist-info}/entry_points.txt +0 -0
- {schemathesis-3.25.5.dist-info → schemathesis-3.25.6.dist-info}/licenses/LICENSE +0 -0
schemathesis/cli/__init__.py
CHANGED
|
@@ -41,16 +41,15 @@ from .._override import CaseOverride
|
|
|
41
41
|
from ..transports.auth import get_requests_auth
|
|
42
42
|
from ..hooks import GLOBAL_HOOK_DISPATCHER, HookContext, HookDispatcher, HookScope
|
|
43
43
|
from ..models import Case, CheckFunction
|
|
44
|
-
from ..runner import events, prepare_hypothesis_settings
|
|
44
|
+
from ..runner import events, prepare_hypothesis_settings, probes
|
|
45
45
|
from ..specs.graphql import loaders as gql_loaders
|
|
46
46
|
from ..specs.openapi import loaders as oas_loaders
|
|
47
|
-
from ..specs.openapi import formats
|
|
48
47
|
from ..stateful import Stateful
|
|
49
48
|
from ..targets import Target
|
|
50
49
|
from ..types import Filter, PathLike, RequestCert
|
|
51
50
|
from ..internal.datetime import current_datetime
|
|
52
51
|
from ..internal.validation import file_exists
|
|
53
|
-
from . import callbacks, cassettes, output
|
|
52
|
+
from . import callbacks, cassettes, output
|
|
54
53
|
from .constants import DEFAULT_WORKERS, MAX_WORKERS, MIN_WORKERS
|
|
55
54
|
from .context import ExecutionContext, FileReportContext, ServiceReportContext
|
|
56
55
|
from .debug import DebugOutputHandler
|
|
@@ -1100,10 +1099,9 @@ def into_event_stream(
|
|
|
1100
1099
|
tag=tag or None,
|
|
1101
1100
|
operation_id=operation_id or None,
|
|
1102
1101
|
)
|
|
1103
|
-
|
|
1104
|
-
run_probes(loaded_schema, config)
|
|
1102
|
+
schema = load_schema(config)
|
|
1105
1103
|
yield from runner.from_schema(
|
|
1106
|
-
|
|
1104
|
+
schema,
|
|
1107
1105
|
auth=auth,
|
|
1108
1106
|
auth_type=auth_type,
|
|
1109
1107
|
override=override,
|
|
@@ -1126,6 +1124,15 @@ def into_event_stream(
|
|
|
1126
1124
|
stateful_recursion_limit=stateful_recursion_limit,
|
|
1127
1125
|
hypothesis_settings=hypothesis_settings,
|
|
1128
1126
|
generation_config=generation_config,
|
|
1127
|
+
probe_config=probes.ProbeConfig(
|
|
1128
|
+
base_url=config.base_url,
|
|
1129
|
+
request_tls_verify=config.request_tls_verify,
|
|
1130
|
+
request_proxy=config.request_proxy,
|
|
1131
|
+
request_cert=config.request_cert,
|
|
1132
|
+
auth=config.auth,
|
|
1133
|
+
auth_type=config.auth_type,
|
|
1134
|
+
headers=config.headers,
|
|
1135
|
+
),
|
|
1129
1136
|
).execute()
|
|
1130
1137
|
except SchemaError as error:
|
|
1131
1138
|
yield events.InternalError.from_schema_error(error)
|
|
@@ -1133,19 +1140,6 @@ def into_event_stream(
|
|
|
1133
1140
|
yield events.InternalError.from_exc(exc)
|
|
1134
1141
|
|
|
1135
1142
|
|
|
1136
|
-
def run_probes(schema: BaseSchema, config: LoaderConfig) -> None:
|
|
1137
|
-
"""Discover capabilities of the tested app."""
|
|
1138
|
-
probe_results = probes.run(schema, config)
|
|
1139
|
-
for result in probe_results:
|
|
1140
|
-
if isinstance(result.probe, probes.NullByteInHeader) and result.is_failure:
|
|
1141
|
-
from ..specs.openapi._hypothesis import HEADER_FORMAT, header_values
|
|
1142
|
-
|
|
1143
|
-
formats.register(
|
|
1144
|
-
HEADER_FORMAT,
|
|
1145
|
-
header_values(blacklist_characters="\n\r\x00").map(str.lstrip),
|
|
1146
|
-
)
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
1143
|
def load_schema(config: LoaderConfig) -> BaseSchema:
|
|
1150
1144
|
"""Automatically load API schema."""
|
|
1151
1145
|
first: Callable[[LoaderConfig], BaseSchema]
|
schemathesis/cli/context.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
|
+
|
|
2
3
|
import os
|
|
3
4
|
import shutil
|
|
4
5
|
from dataclasses import dataclass, field
|
|
@@ -8,6 +9,7 @@ from typing import TYPE_CHECKING
|
|
|
8
9
|
from ..code_samples import CodeSampleStyle
|
|
9
10
|
from ..internal.deprecation import deprecated_property
|
|
10
11
|
from ..runner.serialization import SerializedTestResult
|
|
12
|
+
from ..runner.probes import ProbeRun
|
|
11
13
|
|
|
12
14
|
if TYPE_CHECKING:
|
|
13
15
|
import hypothesis
|
|
@@ -49,6 +51,7 @@ class ExecutionContext:
|
|
|
49
51
|
verbosity: int = 0
|
|
50
52
|
code_sample_style: CodeSampleStyle = CodeSampleStyle.default()
|
|
51
53
|
report: ServiceReportContext | FileReportContext | None = None
|
|
54
|
+
probes: list[ProbeRun] | None = None
|
|
52
55
|
|
|
53
56
|
@deprecated_property(removed_in="4.0", replacement="show_trace")
|
|
54
57
|
def show_errors_tracebacks(self) -> bool:
|
|
@@ -1,35 +1,37 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
|
+
|
|
2
3
|
import base64
|
|
3
4
|
import os
|
|
4
5
|
import platform
|
|
5
6
|
import shutil
|
|
6
7
|
import textwrap
|
|
7
8
|
import time
|
|
9
|
+
from importlib import metadata
|
|
8
10
|
from itertools import groupby
|
|
9
11
|
from queue import Queue
|
|
10
12
|
from typing import Any, Generator, cast
|
|
11
13
|
|
|
12
14
|
import click
|
|
13
|
-
from importlib import metadata
|
|
14
15
|
|
|
15
16
|
from ... import service
|
|
16
17
|
from ...code_samples import CodeSampleStyle
|
|
17
18
|
from ...constants import (
|
|
18
19
|
DISCORD_LINK,
|
|
20
|
+
FALSE_VALUES,
|
|
19
21
|
FLAKY_FAILURE_MESSAGE,
|
|
22
|
+
GITHUB_APP_LINK,
|
|
23
|
+
ISSUE_TRACKER_URL,
|
|
20
24
|
REPORT_SUGGESTION_ENV_VAR,
|
|
21
25
|
SCHEMATHESIS_TEST_CASE_HEADER,
|
|
22
26
|
SCHEMATHESIS_VERSION,
|
|
23
|
-
FALSE_VALUES,
|
|
24
|
-
ISSUE_TRACKER_URL,
|
|
25
|
-
GITHUB_APP_LINK,
|
|
26
27
|
)
|
|
27
28
|
from ...exceptions import RuntimeErrorType, prepare_response_payload
|
|
28
29
|
from ...experimental import GLOBAL_EXPERIMENTS
|
|
29
30
|
from ...models import Status
|
|
30
31
|
from ...runner import events
|
|
31
32
|
from ...runner.events import InternalErrorType, SchemaErrorType
|
|
32
|
-
from ...runner.
|
|
33
|
+
from ...runner.probes import ProbeOutcome
|
|
34
|
+
from ...runner.serialization import SerializedCheck, SerializedError, SerializedTestResult, deduplicate_failures
|
|
33
35
|
from ..context import ExecutionContext, FileReportContext, ServiceReportContext
|
|
34
36
|
from ..handlers import EventHandler
|
|
35
37
|
|
|
@@ -487,7 +489,7 @@ def display_service_unauthorized(hostname: str) -> None:
|
|
|
487
489
|
|
|
488
490
|
def display_service_error(event: service.Error, message_prefix: str = "") -> None:
|
|
489
491
|
"""Show information about an error during communication with Schemathesis.io."""
|
|
490
|
-
from requests import
|
|
492
|
+
from requests import HTTPError, RequestException, Response
|
|
491
493
|
|
|
492
494
|
if isinstance(event.exception, HTTPError):
|
|
493
495
|
response = cast(Response, event.exception.response)
|
|
@@ -694,7 +696,26 @@ def handle_initialized(context: ExecutionContext, event: events.Initialized) ->
|
|
|
694
696
|
click.secho(f"Collected API links: {links_count}", bold=True)
|
|
695
697
|
if isinstance(context.report, ServiceReportContext):
|
|
696
698
|
click.secho("Report to Schemathesis.io: ENABLED", bold=True)
|
|
697
|
-
|
|
699
|
+
|
|
700
|
+
|
|
701
|
+
def handle_before_probing(context: ExecutionContext, event: events.BeforeProbing) -> None:
|
|
702
|
+
click.secho("API probing: ...\r", bold=True, nl=False)
|
|
703
|
+
|
|
704
|
+
|
|
705
|
+
def handle_after_probing(context: ExecutionContext, event: events.AfterProbing) -> None:
|
|
706
|
+
context.probes = event.probes
|
|
707
|
+
status = "SKIP"
|
|
708
|
+
if event.probes is not None:
|
|
709
|
+
for probe in event.probes:
|
|
710
|
+
if probe.outcome in (ProbeOutcome.SUCCESS, ProbeOutcome.FAILURE):
|
|
711
|
+
# The probe itself has been executed
|
|
712
|
+
status = "SUCCESS"
|
|
713
|
+
elif probe.outcome == ProbeOutcome.ERROR:
|
|
714
|
+
status = "ERROR"
|
|
715
|
+
click.secho(f"API probing: {status}\r", bold=True, nl=False)
|
|
716
|
+
click.echo()
|
|
717
|
+
operations_count = cast(int, context.operations_count) # INVARIANT: should not be `None`
|
|
718
|
+
if operations_count >= 1:
|
|
698
719
|
click.echo()
|
|
699
720
|
|
|
700
721
|
|
|
@@ -752,6 +773,10 @@ class DefaultOutputStyleHandler(EventHandler):
|
|
|
752
773
|
"""Choose and execute a proper handler for the given event."""
|
|
753
774
|
if isinstance(event, events.Initialized):
|
|
754
775
|
handle_initialized(context, event)
|
|
776
|
+
if isinstance(event, events.BeforeProbing):
|
|
777
|
+
handle_before_probing(context, event)
|
|
778
|
+
if isinstance(event, events.AfterProbing):
|
|
779
|
+
handle_after_probing(context, event)
|
|
755
780
|
if isinstance(event, events.BeforeExecution):
|
|
756
781
|
handle_before_execution(context, event)
|
|
757
782
|
if isinstance(event, events.AfterExecution):
|
schemathesis/cli/output/short.py
CHANGED
|
@@ -26,6 +26,10 @@ class ShortOutputStyleHandler(EventHandler):
|
|
|
26
26
|
"""
|
|
27
27
|
if isinstance(event, events.Initialized):
|
|
28
28
|
default.handle_initialized(context, event)
|
|
29
|
+
if isinstance(event, events.BeforeProbing):
|
|
30
|
+
default.handle_before_probing(context, event)
|
|
31
|
+
if isinstance(event, events.AfterProbing):
|
|
32
|
+
default.handle_after_probing(context, event)
|
|
29
33
|
if isinstance(event, events.BeforeExecution):
|
|
30
34
|
handle_before_execution(context, event)
|
|
31
35
|
if isinstance(event, events.AfterExecution):
|
schemathesis/models.py
CHANGED
|
@@ -960,9 +960,12 @@ class Response:
|
|
|
960
960
|
@classmethod
|
|
961
961
|
def from_requests(cls, response: requests.Response) -> Response:
|
|
962
962
|
"""Create a response from requests.Response."""
|
|
963
|
-
|
|
963
|
+
raw = response.raw
|
|
964
|
+
raw_headers = raw.headers if raw is not None else {}
|
|
965
|
+
headers = {name: response.raw.headers.getlist(name) for name in raw_headers.keys()}
|
|
964
966
|
# Similar to http.client:319 (HTTP version detection in stdlib's `http` package)
|
|
965
|
-
|
|
967
|
+
version = raw.version if raw is not None else 10
|
|
968
|
+
http_version = "1.0" if version == 10 else "1.1"
|
|
966
969
|
|
|
967
970
|
def is_empty(_response: requests.Response) -> bool:
|
|
968
971
|
# Assume the response is empty if:
|
schemathesis/runner/__init__.py
CHANGED
|
@@ -1,34 +1,36 @@
|
|
|
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
|
|
32
34
|
|
|
33
35
|
|
|
34
36
|
@deprecated_function(removed_in="4.0", replacement="schemathesis.runner.from_schema")
|
|
@@ -75,6 +77,7 @@ def prepare(
|
|
|
75
77
|
hypothesis_report_multiple_bugs: bool | None = None,
|
|
76
78
|
hypothesis_suppress_health_check: list[hypothesis.HealthCheck] | None = None,
|
|
77
79
|
hypothesis_verbosity: hypothesis.Verbosity | None = None,
|
|
80
|
+
probe_config: ProbeConfig | None = None,
|
|
78
81
|
) -> Generator[events.ExecutionEvent, None, None]:
|
|
79
82
|
"""Prepare a generator that will run test cases against the given API definition."""
|
|
80
83
|
from ..checks import DEFAULT_CHECKS
|
|
@@ -128,6 +131,7 @@ def prepare(
|
|
|
128
131
|
stateful_recursion_limit=stateful_recursion_limit,
|
|
129
132
|
count_operations=count_operations,
|
|
130
133
|
count_links=count_links,
|
|
134
|
+
probe_config=probe_config,
|
|
131
135
|
)
|
|
132
136
|
|
|
133
137
|
|
|
@@ -188,6 +192,7 @@ def execute_from_schema(
|
|
|
188
192
|
stateful_recursion_limit: int = DEFAULT_STATEFUL_RECURSION_LIMIT,
|
|
189
193
|
count_operations: bool = True,
|
|
190
194
|
count_links: bool = True,
|
|
195
|
+
probe_config: ProbeConfig | None = None,
|
|
191
196
|
) -> Generator[events.ExecutionEvent, None, None]:
|
|
192
197
|
"""Execute tests for the given schema.
|
|
193
198
|
|
|
@@ -237,6 +242,7 @@ def execute_from_schema(
|
|
|
237
242
|
stateful_recursion_limit=stateful_recursion_limit,
|
|
238
243
|
count_operations=count_operations,
|
|
239
244
|
count_links=count_links,
|
|
245
|
+
probe_config=probe_config,
|
|
240
246
|
).execute()
|
|
241
247
|
except SchemaError as error:
|
|
242
248
|
yield events.InternalError.from_schema_error(error)
|
|
@@ -341,9 +347,11 @@ def from_schema(
|
|
|
341
347
|
stateful_recursion_limit: int = DEFAULT_STATEFUL_RECURSION_LIMIT,
|
|
342
348
|
count_operations: bool = True,
|
|
343
349
|
count_links: bool = True,
|
|
350
|
+
probe_config: ProbeConfig | None = None,
|
|
344
351
|
) -> BaseRunner:
|
|
345
|
-
from starlette.applications import Starlette
|
|
346
352
|
import hypothesis
|
|
353
|
+
from starlette.applications import Starlette
|
|
354
|
+
|
|
347
355
|
from ..checks import DEFAULT_CHECKS
|
|
348
356
|
from .impl import (
|
|
349
357
|
SingleThreadASGIRunner,
|
|
@@ -355,6 +363,7 @@ def from_schema(
|
|
|
355
363
|
)
|
|
356
364
|
|
|
357
365
|
checks = checks or DEFAULT_CHECKS
|
|
366
|
+
probe_config = probe_config or ProbeConfig()
|
|
358
367
|
|
|
359
368
|
hypothesis_settings = hypothesis_settings or hypothesis.settings(deadline=DEFAULT_DEADLINE)
|
|
360
369
|
generation_config = generation_config or GenerationConfig()
|
|
@@ -392,6 +401,7 @@ def from_schema(
|
|
|
392
401
|
stateful_recursion_limit=stateful_recursion_limit,
|
|
393
402
|
count_operations=count_operations,
|
|
394
403
|
count_links=count_links,
|
|
404
|
+
probe_config=probe_config,
|
|
395
405
|
)
|
|
396
406
|
if isinstance(schema.app, Starlette):
|
|
397
407
|
return ThreadPoolASGIRunner(
|
|
@@ -415,6 +425,7 @@ def from_schema(
|
|
|
415
425
|
stateful_recursion_limit=stateful_recursion_limit,
|
|
416
426
|
count_operations=count_operations,
|
|
417
427
|
count_links=count_links,
|
|
428
|
+
probe_config=probe_config,
|
|
418
429
|
)
|
|
419
430
|
return ThreadPoolWSGIRunner(
|
|
420
431
|
schema=schema,
|
|
@@ -438,6 +449,7 @@ def from_schema(
|
|
|
438
449
|
stateful_recursion_limit=stateful_recursion_limit,
|
|
439
450
|
count_operations=count_operations,
|
|
440
451
|
count_links=count_links,
|
|
452
|
+
probe_config=probe_config,
|
|
441
453
|
)
|
|
442
454
|
if not schema.app:
|
|
443
455
|
return SingleThreadRunner(
|
|
@@ -465,6 +477,7 @@ def from_schema(
|
|
|
465
477
|
stateful_recursion_limit=stateful_recursion_limit,
|
|
466
478
|
count_operations=count_operations,
|
|
467
479
|
count_links=count_links,
|
|
480
|
+
probe_config=probe_config,
|
|
468
481
|
)
|
|
469
482
|
if isinstance(schema.app, Starlette):
|
|
470
483
|
return SingleThreadASGIRunner(
|
|
@@ -488,6 +501,7 @@ def from_schema(
|
|
|
488
501
|
stateful_recursion_limit=stateful_recursion_limit,
|
|
489
502
|
count_operations=count_operations,
|
|
490
503
|
count_links=count_links,
|
|
504
|
+
probe_config=probe_config,
|
|
491
505
|
)
|
|
492
506
|
return SingleThreadWSGIRunner(
|
|
493
507
|
schema=schema,
|
|
@@ -510,6 +524,7 @@ def from_schema(
|
|
|
510
524
|
stateful_recursion_limit=stateful_recursion_limit,
|
|
511
525
|
count_operations=count_operations,
|
|
512
526
|
count_links=count_links,
|
|
527
|
+
probe_config=probe_config,
|
|
513
528
|
)
|
|
514
529
|
|
|
515
530
|
|
schemathesis/runner/events.py
CHANGED
|
@@ -14,6 +14,7 @@ from .serialization import SerializedError, SerializedTestResult
|
|
|
14
14
|
if TYPE_CHECKING:
|
|
15
15
|
from ..models import APIOperation, Status, TestResult, TestResultSet
|
|
16
16
|
from ..schemas import BaseSchema
|
|
17
|
+
from . import probes
|
|
17
18
|
|
|
18
19
|
|
|
19
20
|
@dataclass
|
|
@@ -60,6 +61,7 @@ class Initialized(ExecutionEvent):
|
|
|
60
61
|
schema: BaseSchema,
|
|
61
62
|
count_operations: bool = True,
|
|
62
63
|
count_links: bool = True,
|
|
64
|
+
start_time: float | None = None,
|
|
63
65
|
started_at: str | None = None,
|
|
64
66
|
seed: int | None,
|
|
65
67
|
) -> Initialized:
|
|
@@ -70,12 +72,27 @@ class Initialized(ExecutionEvent):
|
|
|
70
72
|
links_count=schema.links_count if count_links else None,
|
|
71
73
|
location=schema.location,
|
|
72
74
|
base_url=schema.get_base_url(),
|
|
75
|
+
start_time=start_time or time.monotonic(),
|
|
73
76
|
started_at=started_at or current_datetime(),
|
|
74
77
|
specification_name=schema.verbose_name,
|
|
75
78
|
seed=seed,
|
|
76
79
|
)
|
|
77
80
|
|
|
78
81
|
|
|
82
|
+
@dataclass
|
|
83
|
+
class BeforeProbing(ExecutionEvent):
|
|
84
|
+
pass
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@dataclass
|
|
88
|
+
class AfterProbing(ExecutionEvent):
|
|
89
|
+
probes: list[probes.ProbeRun] | None
|
|
90
|
+
|
|
91
|
+
def asdict(self, **kwargs: Any) -> dict[str, Any]:
|
|
92
|
+
probes = self.probes or []
|
|
93
|
+
return {"probes": [probe.serialize() for probe in probes], "events_type": self.__class__.__name__}
|
|
94
|
+
|
|
95
|
+
|
|
79
96
|
class CurrentOperationMixin:
|
|
80
97
|
method: str
|
|
81
98
|
path: str
|
|
@@ -188,6 +205,9 @@ class InternalErrorType(str, enum.Enum):
|
|
|
188
205
|
OTHER = "other"
|
|
189
206
|
|
|
190
207
|
|
|
208
|
+
DEFAULT_INTERNAL_ERROR_MESSAGE = "An internal error occurred during the test run"
|
|
209
|
+
|
|
210
|
+
|
|
191
211
|
@dataclass
|
|
192
212
|
class InternalError(ExecutionEvent):
|
|
193
213
|
"""An error that happened inside the runner."""
|
|
@@ -226,7 +246,7 @@ class InternalError(ExecutionEvent):
|
|
|
226
246
|
type_=InternalErrorType.OTHER,
|
|
227
247
|
subtype=None,
|
|
228
248
|
title="Test Execution Error",
|
|
229
|
-
message=
|
|
249
|
+
message=DEFAULT_INTERNAL_ERROR_MESSAGE,
|
|
230
250
|
extras=[],
|
|
231
251
|
)
|
|
232
252
|
|
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,56 @@ 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
|
|
55
|
+
from ...internal.datetime import current_datetime
|
|
53
56
|
from ...internal.result import Ok
|
|
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 ...specs.openapi import formats
|
|
58
61
|
from ...stateful import Feedback, Stateful
|
|
59
62
|
from ...targets import Target, TargetContext
|
|
60
63
|
from ...types import RawAuth, RequestCert
|
|
61
64
|
from ...utils import capture_hypothesis_output
|
|
65
|
+
from .. import probes
|
|
62
66
|
from ..serialization import SerializedTestResult
|
|
63
67
|
|
|
64
68
|
if TYPE_CHECKING:
|
|
65
|
-
from ...transports.responses import
|
|
69
|
+
from ...transports.responses import GenericResponse, WSGIResponse
|
|
66
70
|
|
|
67
71
|
|
|
68
72
|
def _should_count_towards_stop(event: events.ExecutionEvent) -> bool:
|
|
@@ -77,6 +81,7 @@ class BaseRunner:
|
|
|
77
81
|
targets: Iterable[Target]
|
|
78
82
|
hypothesis_settings: hypothesis.settings
|
|
79
83
|
generation_config: GenerationConfig
|
|
84
|
+
probe_config: probes.ProbeConfig
|
|
80
85
|
override: CaseOverride | None = None
|
|
81
86
|
auth: RawAuth | None = None
|
|
82
87
|
auth_type: str | None = None
|
|
@@ -104,26 +109,56 @@ class BaseRunner:
|
|
|
104
109
|
if self.auth is not None:
|
|
105
110
|
unregister_auth()
|
|
106
111
|
results = TestResultSet(seed=self.seed)
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
112
|
+
initialized = None
|
|
113
|
+
__probes = None
|
|
114
|
+
start_time = time.monotonic()
|
|
115
|
+
|
|
116
|
+
def _initialize() -> events.Initialized:
|
|
117
|
+
nonlocal initialized
|
|
118
|
+
initialized = events.Initialized.from_schema(
|
|
119
|
+
schema=self.schema,
|
|
120
|
+
count_operations=self.count_operations,
|
|
121
|
+
count_links=self.count_links,
|
|
122
|
+
seed=self.seed,
|
|
123
|
+
start_time=start_time,
|
|
124
|
+
)
|
|
125
|
+
return initialized
|
|
111
126
|
|
|
112
127
|
def _finish() -> events.Finished:
|
|
113
128
|
if has_all_not_found(results):
|
|
114
129
|
results.add_warning(ALL_NOT_FOUND_WARNING_MESSAGE)
|
|
115
|
-
return events.Finished.from_results(results=results, running_time=time.monotonic() -
|
|
130
|
+
return events.Finished.from_results(results=results, running_time=time.monotonic() - start_time)
|
|
116
131
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
return
|
|
132
|
+
def _before_probes() -> events.BeforeProbing:
|
|
133
|
+
return events.BeforeProbing()
|
|
120
134
|
|
|
121
|
-
|
|
135
|
+
def _run_probes() -> None:
|
|
136
|
+
if not self.dry_run:
|
|
137
|
+
nonlocal __probes
|
|
138
|
+
|
|
139
|
+
__probes = run_probes(self.schema, self.probe_config)
|
|
140
|
+
|
|
141
|
+
def _after_probes() -> events.AfterProbing:
|
|
142
|
+
_probes = cast(List[probes.ProbeRun], __probes)
|
|
143
|
+
return events.AfterProbing(probes=_probes)
|
|
122
144
|
|
|
123
145
|
if stop_event.is_set():
|
|
124
146
|
yield _finish()
|
|
125
147
|
return
|
|
126
148
|
|
|
149
|
+
for event_factory in (
|
|
150
|
+
_initialize,
|
|
151
|
+
_before_probes,
|
|
152
|
+
_run_probes,
|
|
153
|
+
_after_probes,
|
|
154
|
+
):
|
|
155
|
+
event = event_factory()
|
|
156
|
+
if event is not None:
|
|
157
|
+
yield event
|
|
158
|
+
if stop_event.is_set():
|
|
159
|
+
yield _finish()
|
|
160
|
+
return
|
|
161
|
+
|
|
127
162
|
try:
|
|
128
163
|
yield from self._execute(results, stop_event)
|
|
129
164
|
except KeyboardInterrupt:
|
|
@@ -228,6 +263,20 @@ class BaseRunner:
|
|
|
228
263
|
)
|
|
229
264
|
|
|
230
265
|
|
|
266
|
+
def run_probes(schema: BaseSchema, config: probes.ProbeConfig) -> list[probes.ProbeRun]:
|
|
267
|
+
"""Discover capabilities of the tested app."""
|
|
268
|
+
results = probes.run(schema, config)
|
|
269
|
+
for result in results:
|
|
270
|
+
if isinstance(result.probe, probes.NullByteInHeader) and result.is_failure:
|
|
271
|
+
from ...specs.openapi._hypothesis import HEADER_FORMAT, header_values
|
|
272
|
+
|
|
273
|
+
formats.register(
|
|
274
|
+
HEADER_FORMAT,
|
|
275
|
+
header_values(blacklist_characters="\n\r\x00").map(str.lstrip),
|
|
276
|
+
)
|
|
277
|
+
return results
|
|
278
|
+
|
|
279
|
+
|
|
231
280
|
@dataclass
|
|
232
281
|
class EventStream:
|
|
233
282
|
"""Schemathesis event stream.
|
|
@@ -21,13 +21,24 @@ from ..transports.auth import get_requests_auth
|
|
|
21
21
|
if TYPE_CHECKING:
|
|
22
22
|
import requests
|
|
23
23
|
|
|
24
|
+
from ..types import RequestCert
|
|
24
25
|
from ..schemas import BaseSchema
|
|
25
|
-
from . import LoaderConfig
|
|
26
26
|
|
|
27
27
|
|
|
28
28
|
HEADER_NAME = "X-Schemathesis-Probe"
|
|
29
29
|
|
|
30
30
|
|
|
31
|
+
@dataclass
|
|
32
|
+
class ProbeConfig:
|
|
33
|
+
base_url: str | None = None
|
|
34
|
+
request_tls_verify: bool | str = True
|
|
35
|
+
request_proxy: str | None = None
|
|
36
|
+
request_cert: RequestCert | None = None
|
|
37
|
+
auth: tuple[str, str] | None = None
|
|
38
|
+
auth_type: str | None = None
|
|
39
|
+
headers: dict[str, str] | None = None
|
|
40
|
+
|
|
41
|
+
|
|
31
42
|
@dataclass
|
|
32
43
|
class Probe:
|
|
33
44
|
"""A request to determine the capabilities of the application under test."""
|
|
@@ -35,15 +46,15 @@ class Probe:
|
|
|
35
46
|
name: str
|
|
36
47
|
|
|
37
48
|
def prepare_request(
|
|
38
|
-
self, session: requests.Session, request: requests.Request, schema: BaseSchema, config:
|
|
49
|
+
self, session: requests.Session, request: requests.Request, schema: BaseSchema, config: ProbeConfig
|
|
39
50
|
) -> requests.PreparedRequest:
|
|
40
51
|
raise NotImplementedError
|
|
41
52
|
|
|
42
|
-
def analyze_response(self, response: requests.Response) ->
|
|
53
|
+
def analyze_response(self, response: requests.Response) -> ProbeOutcome:
|
|
43
54
|
raise NotImplementedError
|
|
44
55
|
|
|
45
56
|
|
|
46
|
-
class
|
|
57
|
+
class ProbeOutcome(str, enum.Enum):
|
|
47
58
|
# Capability is supported
|
|
48
59
|
SUCCESS = "success"
|
|
49
60
|
# Capability is not supported
|
|
@@ -55,18 +66,16 @@ class ProbeResultType(str, enum.Enum):
|
|
|
55
66
|
|
|
56
67
|
|
|
57
68
|
@dataclass
|
|
58
|
-
class
|
|
59
|
-
"""Result of a probe."""
|
|
60
|
-
|
|
69
|
+
class ProbeRun:
|
|
61
70
|
probe: Probe
|
|
62
|
-
|
|
71
|
+
outcome: ProbeOutcome
|
|
63
72
|
request: requests.PreparedRequest | None = None
|
|
64
73
|
response: requests.Response | None = None
|
|
65
74
|
error: requests.RequestException | None = None
|
|
66
75
|
|
|
67
76
|
@property
|
|
68
77
|
def is_failure(self) -> bool:
|
|
69
|
-
return self.
|
|
78
|
+
return self.outcome == ProbeOutcome.FAILURE
|
|
70
79
|
|
|
71
80
|
def serialize(self) -> dict[str, Any]:
|
|
72
81
|
"""Serialize probe results so it can be sent over the network."""
|
|
@@ -87,7 +96,7 @@ class ProbeResult:
|
|
|
87
96
|
error = None
|
|
88
97
|
return {
|
|
89
98
|
"name": self.probe.name,
|
|
90
|
-
"
|
|
99
|
+
"outcome": self.outcome.value,
|
|
91
100
|
"request": request,
|
|
92
101
|
"response": response,
|
|
93
102
|
"error": error,
|
|
@@ -101,25 +110,25 @@ class NullByteInHeader(Probe):
|
|
|
101
110
|
name: str = "NULL_BYTE_IN_HEADER"
|
|
102
111
|
|
|
103
112
|
def prepare_request(
|
|
104
|
-
self, session: requests.Session, request: requests.Request, schema: BaseSchema, config:
|
|
113
|
+
self, session: requests.Session, request: requests.Request, schema: BaseSchema, config: ProbeConfig
|
|
105
114
|
) -> requests.PreparedRequest:
|
|
106
115
|
request.method = "GET"
|
|
107
116
|
request.url = config.base_url or schema.get_base_url()
|
|
108
117
|
request.headers = {"X-Schemathesis-Probe-Null": "\x00"}
|
|
109
118
|
return session.prepare_request(request)
|
|
110
119
|
|
|
111
|
-
def analyze_response(self, response: requests.Response) ->
|
|
120
|
+
def analyze_response(self, response: requests.Response) -> ProbeOutcome:
|
|
112
121
|
if response.status_code == 400:
|
|
113
|
-
return
|
|
114
|
-
return
|
|
122
|
+
return ProbeOutcome.FAILURE
|
|
123
|
+
return ProbeOutcome.SUCCESS
|
|
115
124
|
|
|
116
125
|
|
|
117
126
|
PROBES = (NullByteInHeader,)
|
|
118
127
|
|
|
119
128
|
|
|
120
|
-
def send(probe: Probe, session: requests.Session, schema: BaseSchema, config:
|
|
129
|
+
def send(probe: Probe, session: requests.Session, schema: BaseSchema, config: ProbeConfig) -> ProbeRun:
|
|
121
130
|
"""Send the probe to the application."""
|
|
122
|
-
from requests import Request, RequestException
|
|
131
|
+
from requests import PreparedRequest, Request, RequestException
|
|
123
132
|
from requests.exceptions import MissingSchema
|
|
124
133
|
from urllib3.exceptions import InsecureRequestWarning
|
|
125
134
|
|
|
@@ -129,23 +138,24 @@ def send(probe: Probe, session: requests.Session, schema: BaseSchema, config: Lo
|
|
|
129
138
|
request.headers["User-Agent"] = USER_AGENT
|
|
130
139
|
with warnings.catch_warnings():
|
|
131
140
|
warnings.simplefilter("ignore", InsecureRequestWarning)
|
|
132
|
-
response = session.send(request)
|
|
141
|
+
response = session.send(request, timeout=2)
|
|
133
142
|
except MissingSchema:
|
|
134
143
|
# In-process ASGI/WSGI testing will have local URLs and requires extra handling
|
|
135
144
|
# which is not currently implemented
|
|
136
|
-
return
|
|
145
|
+
return ProbeRun(probe, ProbeOutcome.SKIP, None, None, None)
|
|
137
146
|
except RequestException as exc:
|
|
138
147
|
req = exc.request if isinstance(exc.request, PreparedRequest) else None
|
|
139
|
-
return
|
|
148
|
+
return ProbeRun(probe, ProbeOutcome.ERROR, req, None, exc)
|
|
140
149
|
result_type = probe.analyze_response(response)
|
|
141
|
-
return
|
|
150
|
+
return ProbeRun(probe, result_type, request, response)
|
|
142
151
|
|
|
143
152
|
|
|
144
|
-
def run(schema: BaseSchema, config:
|
|
153
|
+
def run(schema: BaseSchema, config: ProbeConfig) -> list[ProbeRun]:
|
|
145
154
|
"""Run all probes against the given schema."""
|
|
146
155
|
from requests import Session
|
|
147
156
|
|
|
148
157
|
session = Session()
|
|
158
|
+
session.headers.update(config.headers or {})
|
|
149
159
|
session.verify = config.request_tls_verify
|
|
150
160
|
if config.request_cert is not None:
|
|
151
161
|
session.cert = config.request_cert
|
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,6 +168,8 @@ 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
|
|
|
@@ -19,6 +19,15 @@ def serialize_initialized(event: events.Initialized) -> dict[str, Any] | None:
|
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
|
|
22
|
+
def serialize_before_probing(_: events.BeforeProbing) -> None:
|
|
23
|
+
return None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def serialize_after_probing(event: events.AfterProbing) -> dict[str, Any] | None:
|
|
27
|
+
probes = event.probes or []
|
|
28
|
+
return {"probes": [probe.serialize() for probe in probes]}
|
|
29
|
+
|
|
30
|
+
|
|
22
31
|
def serialize_before_execution(event: events.BeforeExecution) -> dict[str, Any] | None:
|
|
23
32
|
return {
|
|
24
33
|
"correlation_id": event.correlation_id,
|
|
@@ -116,6 +125,8 @@ def serialize_finished(event: events.Finished) -> dict[str, Any] | None:
|
|
|
116
125
|
|
|
117
126
|
SERIALIZER_MAP = {
|
|
118
127
|
events.Initialized: serialize_initialized,
|
|
128
|
+
events.BeforeProbing: serialize_before_probing,
|
|
129
|
+
events.AfterProbing: serialize_after_probing,
|
|
119
130
|
events.BeforeExecution: serialize_before_execution,
|
|
120
131
|
events.AfterExecution: serialize_after_execution,
|
|
121
132
|
events.Interrupted: serialize_interrupted,
|
|
@@ -128,6 +139,8 @@ def serialize_event(
|
|
|
128
139
|
event: events.ExecutionEvent,
|
|
129
140
|
*,
|
|
130
141
|
on_initialized: SerializeFunc | None = None,
|
|
142
|
+
on_before_probing: SerializeFunc | None = None,
|
|
143
|
+
on_after_probing: SerializeFunc | None = None,
|
|
131
144
|
on_before_execution: SerializeFunc | None = None,
|
|
132
145
|
on_after_execution: SerializeFunc | None = None,
|
|
133
146
|
on_interrupted: SerializeFunc | None = None,
|
|
@@ -139,6 +152,8 @@ def serialize_event(
|
|
|
139
152
|
# Use the explicitly provided serializer for this event and fallback to default one if it is not provided
|
|
140
153
|
serializer = {
|
|
141
154
|
events.Initialized: on_initialized,
|
|
155
|
+
events.BeforeProbing: on_before_probing,
|
|
156
|
+
events.AfterProbing: on_after_probing,
|
|
142
157
|
events.BeforeExecution: on_before_execution,
|
|
143
158
|
events.AfterExecution: on_after_execution,
|
|
144
159
|
events.Interrupted: on_interrupted,
|
|
@@ -134,7 +134,7 @@ def get_case_strategy(
|
|
|
134
134
|
|
|
135
135
|
context = HookContext(operation)
|
|
136
136
|
|
|
137
|
-
generation_config = generation_config or
|
|
137
|
+
generation_config = generation_config or operation.schema.generation_config
|
|
138
138
|
|
|
139
139
|
path_parameters_ = generate_parameter(
|
|
140
140
|
"path", path_parameters, operation, draw, context, hooks, generator, generation_config
|
|
@@ -3,19 +3,24 @@ from __future__ import annotations
|
|
|
3
3
|
from contextlib import suppress
|
|
4
4
|
from dataclasses import dataclass
|
|
5
5
|
from functools import lru_cache
|
|
6
|
-
from itertools import
|
|
7
|
-
from typing import Any, Generator, Union, cast
|
|
6
|
+
from itertools import chain, cycle, islice
|
|
7
|
+
from typing import TYPE_CHECKING, Any, Generator, Union, cast
|
|
8
8
|
|
|
9
9
|
import requests
|
|
10
|
-
import hypothesis
|
|
11
|
-
from hypothesis_jsonschema import from_schema
|
|
12
10
|
from hypothesis.strategies import SearchStrategy
|
|
11
|
+
from hypothesis_jsonschema import from_schema
|
|
13
12
|
|
|
14
|
-
from .parameters import OpenAPIParameter, OpenAPIBody
|
|
15
13
|
from ...constants import DEFAULT_RESPONSE_TIMEOUT
|
|
16
14
|
from ...models import APIOperation, Case
|
|
17
|
-
from
|
|
15
|
+
from ..._hypothesis import get_single_example
|
|
16
|
+
from ._hypothesis import get_case_strategy, get_default_format_strategies
|
|
17
|
+
from .formats import STRING_FORMATS
|
|
18
18
|
from .constants import LOCATION_TO_CONTAINER
|
|
19
|
+
from .parameters import OpenAPIBody, OpenAPIParameter
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
from ...generation import GenerationConfig
|
|
19
24
|
|
|
20
25
|
|
|
21
26
|
@dataclass
|
|
@@ -248,7 +253,7 @@ def extract_from_schema(
|
|
|
248
253
|
# Generated by one of `anyOf` or similar sub-schemas
|
|
249
254
|
continue
|
|
250
255
|
subschema = operation.schema.prepare_schema(subschema)
|
|
251
|
-
generated = _generate_single_example(subschema)
|
|
256
|
+
generated = _generate_single_example(subschema, operation.schema.generation_config)
|
|
252
257
|
variants[name] = [generated]
|
|
253
258
|
# Calculate the maximum number of examples any property has
|
|
254
259
|
total_combos = max(len(examples) for examples in variants.values())
|
|
@@ -264,24 +269,17 @@ def extract_from_schema(
|
|
|
264
269
|
yield [value]
|
|
265
270
|
|
|
266
271
|
|
|
267
|
-
def _generate_single_example(
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
phases=(hypothesis.Phase.generate,),
|
|
277
|
-
suppress_health_check=list(hypothesis.HealthCheck),
|
|
272
|
+
def _generate_single_example(
|
|
273
|
+
schema: dict[str, Any],
|
|
274
|
+
generation_config: GenerationConfig,
|
|
275
|
+
) -> Any:
|
|
276
|
+
strategy = from_schema(
|
|
277
|
+
schema,
|
|
278
|
+
custom_formats={**get_default_format_strategies(), **STRING_FORMATS},
|
|
279
|
+
allow_x00=generation_config.allow_x00,
|
|
280
|
+
codec=generation_config.codec,
|
|
278
281
|
)
|
|
279
|
-
|
|
280
|
-
examples.append(ex)
|
|
281
|
-
|
|
282
|
-
example_generating_inner_function()
|
|
283
|
-
|
|
284
|
-
return examples[0]
|
|
282
|
+
return get_single_example(strategy)
|
|
285
283
|
|
|
286
284
|
|
|
287
285
|
def produce_combinations(examples: list[Example]) -> Generator[dict[str, Any], None, None]:
|
|
@@ -33,6 +33,7 @@ from ...auths import AuthStorage
|
|
|
33
33
|
from ...generation import DataGenerationMethod, GenerationConfig
|
|
34
34
|
from ...constants import HTTP_METHODS, NOT_SET
|
|
35
35
|
from ...exceptions import (
|
|
36
|
+
InternalError,
|
|
36
37
|
OperationSchemaError,
|
|
37
38
|
UsageError,
|
|
38
39
|
get_missing_content_type_error,
|
|
@@ -808,7 +809,7 @@ class SwaggerV20(BaseOpenAPISchema):
|
|
|
808
809
|
|
|
809
810
|
@property
|
|
810
811
|
def spec_version(self) -> str:
|
|
811
|
-
return self.raw_schema
|
|
812
|
+
return self.raw_schema.get("swagger", "2.0")
|
|
812
813
|
|
|
813
814
|
@property
|
|
814
815
|
def verbose_name(self) -> str:
|
|
@@ -1060,8 +1061,14 @@ class OpenApi30(SwaggerV20):
|
|
|
1060
1061
|
files = []
|
|
1061
1062
|
content = operation.definition.resolved["requestBody"]["content"]
|
|
1062
1063
|
# Open API 3.0 requires media types to be present. We can get here only if the schema defines
|
|
1063
|
-
# the "multipart/form-data" media type
|
|
1064
|
-
|
|
1064
|
+
# the "multipart/form-data" media type, or any other more general media type that matches it (like `*/*`)
|
|
1065
|
+
for media_type, entry in content.items():
|
|
1066
|
+
main, sub = parse_content_type(media_type)
|
|
1067
|
+
if main in ("*", "multipart") and sub in ("*", "form-data"):
|
|
1068
|
+
schema = entry["schema"]
|
|
1069
|
+
break
|
|
1070
|
+
else:
|
|
1071
|
+
raise InternalError("No 'multipart/form-data' media type found in the schema")
|
|
1065
1072
|
for name, property_schema in schema.get("properties", {}).items():
|
|
1066
1073
|
if name in form_data:
|
|
1067
1074
|
if isinstance(form_data[name], list):
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: schemathesis
|
|
3
|
-
Version: 3.25.
|
|
3
|
+
Version: 3.25.6
|
|
4
4
|
Summary: Property-based testing framework for Open API and GraphQL based apps
|
|
5
5
|
Project-URL: Documentation, https://schemathesis.readthedocs.io/en/stable/
|
|
6
6
|
Project-URL: Changelog, https://schemathesis.readthedocs.io/en/stable/changelog.html
|
|
@@ -16,30 +16,29 @@ schemathesis/graphql.py,sha256=YkoKWY5K8lxp7H3ikAs-IsoDbiPwJvChG7O8p3DgwtI,229
|
|
|
16
16
|
schemathesis/hooks.py,sha256=cNJgCh7SyLWT1sYDKF5ncDC80ld08CinvKo2IqLMV4g,12396
|
|
17
17
|
schemathesis/lazy.py,sha256=CivWpvesh4iYLSkatXbQPTEOruWmXvuZQ29gng5p9wM,14846
|
|
18
18
|
schemathesis/loaders.py,sha256=RJnrbf-3vZ7KXmRBkmr3uqWyg0eHzOnONABuudWcTIg,4586
|
|
19
|
-
schemathesis/models.py,sha256=
|
|
19
|
+
schemathesis/models.py,sha256=F2aWZW_BqCFzgv9s7KFkRt-EGQTimhKbYaehRtqC6Lw,46757
|
|
20
20
|
schemathesis/parameters.py,sha256=xHd3DAkq7z9FAnYDJXXeeOW8XKRsCIW23qtFOPYvGVs,2256
|
|
21
21
|
schemathesis/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
22
22
|
schemathesis/sanitization.py,sha256=WSV_MB5YRrYkp1pQRPHrzsidqsKcqYZiq64N9grKobo,8956
|
|
23
23
|
schemathesis/schemas.py,sha256=VfNMjBzQNSY7YFhtM_IfpjI48wyff0yZ_QnXM6gA48A,18251
|
|
24
|
-
schemathesis/serializers.py,sha256=
|
|
24
|
+
schemathesis/serializers.py,sha256=RUvFrZftIk3bcAlG2WP-HlP_r_xRyKSd3uRnyFomlFU,11587
|
|
25
25
|
schemathesis/targets.py,sha256=tzp7VZ2-7g2nZHCooRgFzTMtOVcbl0rvtNR421hQthA,1162
|
|
26
26
|
schemathesis/throttling.py,sha256=QQcS7TFpXHavUjm0kdFaCcCRhAGlgQ4y3XIbkdRoW20,1079
|
|
27
27
|
schemathesis/types.py,sha256=AglR5M0bce-YXeDRkweToXTP0GjNOWVjS_mIsxLobwc,919
|
|
28
28
|
schemathesis/utils.py,sha256=4HXvHysnHp-Uz2HfNgLfW5F5VjL-mtixrjjzRCEJhYo,5233
|
|
29
|
-
schemathesis/cli/__init__.py,sha256=
|
|
29
|
+
schemathesis/cli/__init__.py,sha256=1m3rkoJyK35uvE2NCVjL2FJ8kprqiRppRAcDtPtLAsQ,64586
|
|
30
30
|
schemathesis/cli/callbacks.py,sha256=HmD0WmSYf5x8v4xeZdOKzy_8CEk23NitUlL8JQ7LHdQ,14950
|
|
31
31
|
schemathesis/cli/cassettes.py,sha256=gPPvcuIj1WnoP-yqqvGCJNe49SWlDFu8OdQkOmY453A,12988
|
|
32
32
|
schemathesis/cli/constants.py,sha256=XoezT0_fHuiOY2e--cmBUhNieJsepcUoW8e48QuLSDI,1544
|
|
33
|
-
schemathesis/cli/context.py,sha256=
|
|
33
|
+
schemathesis/cli/context.py,sha256=jfCIzPsdnxsKzdtKKxVM8Jygr2fMIWlAWRi0rP1cmZ8,1731
|
|
34
34
|
schemathesis/cli/debug.py,sha256=PDEa-oHyz5bZ8aYjRYARwQaCv_AC6HM_L43LJfe4vUM,657
|
|
35
35
|
schemathesis/cli/handlers.py,sha256=62GPWPmgeGUz3pkDd01H4UCXcIy5a9yRtU7qaAeeR-A,389
|
|
36
36
|
schemathesis/cli/junitxml.py,sha256=jMZzYBYpBE7Rl5n1ct3J0bhhPa1lhBeYb33lTZE1eW8,1792
|
|
37
37
|
schemathesis/cli/options.py,sha256=7_dXcrPT0kWqAkm60cAT1J0IsTOcvNFxT1pcHYttBuI,2558
|
|
38
|
-
schemathesis/cli/probes.py,sha256=HAz5kWeXL9NiRFtLC86rDvBgpwDLJ8NnCDyVU74Yp9I,5245
|
|
39
38
|
schemathesis/cli/sanitization.py,sha256=v-9rsMCBpWhom0Bfzj_8c6InqxkVjSWK6kRoURa52Nk,727
|
|
40
39
|
schemathesis/cli/output/__init__.py,sha256=AXaUzQ1nhQ-vXhW4-X-91vE2VQtEcCOrGtQXXNN55iQ,29
|
|
41
|
-
schemathesis/cli/output/default.py,sha256=
|
|
42
|
-
schemathesis/cli/output/short.py,sha256=
|
|
40
|
+
schemathesis/cli/output/default.py,sha256=1Kmx6WTIhE2AMivoe631y9ES0KwSLnxwhHn56zmqxRw,33011
|
|
41
|
+
schemathesis/cli/output/short.py,sha256=kr_1WBPh-_hIST3rpV34py2oquKn1Il5E0ZBlmZjjA4,1846
|
|
43
42
|
schemathesis/contrib/__init__.py,sha256=FH8NL8NXgSKBFOF8Jy_EB6T4CJEaiM-tmDhz16B2o4k,187
|
|
44
43
|
schemathesis/contrib/unique_data.py,sha256=zxrmAlQH7Bcil9YSfy2EazwLj2rxLzOvAE3O6QRRkFY,1317
|
|
45
44
|
schemathesis/contrib/openapi/__init__.py,sha256=YMj_b2f3ohGwEt8QQXlxBT60wqvdCFS6516I4EHWVNM,217
|
|
@@ -64,11 +63,12 @@ schemathesis/internal/jsonschema.py,sha256=eQEkBLTdJ__U7Z0XYXB_RVBQJ4texfFO0AaOo
|
|
|
64
63
|
schemathesis/internal/result.py,sha256=Og_ZfwwQ6JFmW79ExC1ndjVMIOHJB7o9XtrfhYIjOOs,461
|
|
65
64
|
schemathesis/internal/transformation.py,sha256=ZNEtL8ezryLdP9-of4eSRBMSjjV_lBQ5DZZfoPGZFEU,449
|
|
66
65
|
schemathesis/internal/validation.py,sha256=G7i8jIMUpAeOnDsDF_eWYvRZe_yMprRswx0QAtMPyEw,966
|
|
67
|
-
schemathesis/runner/__init__.py,sha256=
|
|
68
|
-
schemathesis/runner/events.py,sha256=
|
|
66
|
+
schemathesis/runner/__init__.py,sha256=PZ2kdk4xI853BC3nmJsPRCbBFoR_PKgpjTja1dn9SPA,20888
|
|
67
|
+
schemathesis/runner/events.py,sha256=A0vyMQOM5OjsNMR7OZ-OtWEU7tKKggt-z81nBulvxxU,9950
|
|
68
|
+
schemathesis/runner/probes.py,sha256=WrMc_BX7PaaqNTL3UEDQd_nfgyw7b1kjrlaYUhUGSXM,5545
|
|
69
69
|
schemathesis/runner/serialization.py,sha256=WvAVInu_Pe088p5lWupHvWVDMUjepcf8CIfPi0tLYdg,15900
|
|
70
70
|
schemathesis/runner/impl/__init__.py,sha256=1E2iME8uthYPBh9MjwVBCTFV-P3fi7AdphCCoBBspjs,199
|
|
71
|
-
schemathesis/runner/impl/core.py,sha256=
|
|
71
|
+
schemathesis/runner/impl/core.py,sha256=uRQpd5KXHNwfXJSBu9nTwYTqaEOUSO6sb2oaQacq_Fo,38406
|
|
72
72
|
schemathesis/runner/impl/solo.py,sha256=Y_tNhVBVxcuxv65hN0FjxLlVSC41ebHMOM1xSzVrNk8,3358
|
|
73
73
|
schemathesis/runner/impl/threadpool.py,sha256=2-2Wvw7msezZtenZY5vU_x3FGLLVlH-ywvhU9hTwAAo,15073
|
|
74
74
|
schemathesis/service/__init__.py,sha256=cDVTCFD1G-vvhxZkJUwiToTAEQ-0ByIoqwXvJBCf_V8,472
|
|
@@ -81,7 +81,7 @@ schemathesis/service/hosts.py,sha256=WEiVaLpVka3mhl-T0LjD4WJDEGC-dqOFNmiB4H7Cx5Q
|
|
|
81
81
|
schemathesis/service/metadata.py,sha256=jsPeif2XbrwjZgzDD3YysuUgGaSZm6Hkc4e3raDewqo,1475
|
|
82
82
|
schemathesis/service/models.py,sha256=bY9g48TdtEPS0yO2lTKEsHtKl81Hh5vSJtMDe8kkWSI,850
|
|
83
83
|
schemathesis/service/report.py,sha256=ua1-cfa-TgGshZgimUQ3-YQqykhZqMHCkEificBKncM,8294
|
|
84
|
-
schemathesis/service/serialization.py,sha256=
|
|
84
|
+
schemathesis/service/serialization.py,sha256=Tx-gEBw6J1qMFMbE9O_r504DNSVHYpW5VhgoOH80AQc,7342
|
|
85
85
|
schemathesis/service/usage.py,sha256=Z-GCwFcW1pS6YdC-ziEOynikqgOttxp2Uyj_dfK5Q7A,2437
|
|
86
86
|
schemathesis/specs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
87
87
|
schemathesis/specs/graphql/__init__.py,sha256=fgyHtvWNUVWismBTOqxQtgLoTighTfvMv6v6QCD_Oyc,85
|
|
@@ -91,19 +91,19 @@ schemathesis/specs/graphql/scalars.py,sha256=W5oj6AcjiXpR-Z6eSSp1oPWl0mjH2NF-w87
|
|
|
91
91
|
schemathesis/specs/graphql/schemas.py,sha256=pbonjYD2neMujoMZZMM_MXFJt-gk1qsW59Qkfed9Ltg,13967
|
|
92
92
|
schemathesis/specs/graphql/validation.py,sha256=SqQbj9uymGUQxlHXc8HkQccIq25uwP5CvLF1zReb1Xg,1636
|
|
93
93
|
schemathesis/specs/openapi/__init__.py,sha256=Ue1Qod8IpzeJ5dwxjAHNv97Nh8NVzm_6fnHR2BIzfHY,220
|
|
94
|
-
schemathesis/specs/openapi/_hypothesis.py,sha256=
|
|
94
|
+
schemathesis/specs/openapi/_hypothesis.py,sha256=wVbww3uxbwvAwc-LVldV8xTJsnGaR4zj_9SIYsXAeUc,22173
|
|
95
95
|
schemathesis/specs/openapi/checks.py,sha256=1WB_UGNqptfJThWLUNbds1Q-IzOGbbHCOYPIhBYk-zs,5411
|
|
96
96
|
schemathesis/specs/openapi/constants.py,sha256=JqM_FHOenqS_MuUE9sxVQ8Hnw0DNM8cnKDwCwPLhID4,783
|
|
97
97
|
schemathesis/specs/openapi/converter.py,sha256=9TKeKvNi9MVvoNMfqoPz_cODO8oNrMSTXTOwLLfjD_Q,2799
|
|
98
98
|
schemathesis/specs/openapi/definitions.py,sha256=t5xffHLBnSgptBdDkSqYN1OfT-DaXoeUw2tIgNEe2q8,94057
|
|
99
|
-
schemathesis/specs/openapi/examples.py,sha256=
|
|
99
|
+
schemathesis/specs/openapi/examples.py,sha256=igLDfMpNhRMAk7mYBqi93CtVRQTZCWjCJ2KxlQFotdA,14932
|
|
100
100
|
schemathesis/specs/openapi/filters.py,sha256=Ei-QTFcGCvGSIunT-GYQrtqzB-kqvUePOcUuC7B7mT8,1436
|
|
101
101
|
schemathesis/specs/openapi/formats.py,sha256=JmmkQWNAj5XreXb7Edgj4LADAf4m86YulR_Ec8evpJ4,1220
|
|
102
102
|
schemathesis/specs/openapi/links.py,sha256=YryI35djHOhTikRV3FLafy_jhYFypF7E8Yb6dg9ksWc,14575
|
|
103
103
|
schemathesis/specs/openapi/loaders.py,sha256=Jm37MTUmbVVkOxoRAJOo_T_Ex-tWu2ir7YG7y-v-BjM,24014
|
|
104
104
|
schemathesis/specs/openapi/parameters.py,sha256=5jMZQZFsZNBFjG22Bqot-Rc65emiSA4E95rIzwImThk,13610
|
|
105
105
|
schemathesis/specs/openapi/references.py,sha256=YGunHAGubYPKbMqQtpFWZm1D4AGxB8wLuiVhE6T6cWo,8978
|
|
106
|
-
schemathesis/specs/openapi/schemas.py,sha256=
|
|
106
|
+
schemathesis/specs/openapi/schemas.py,sha256=mIoJkuaQeF5fcrjBFGuudglQJz93vbaMc0wFVJjdVTg,50424
|
|
107
107
|
schemathesis/specs/openapi/security.py,sha256=lNRD4RSYe341f_ayhl_bQuYlFbGzx4luhXi6qJwzrOY,6495
|
|
108
108
|
schemathesis/specs/openapi/serialization.py,sha256=jajqowTIiyEVWEx-Gy4ZinXZewNg0n_ipsGzz7JXP7c,11383
|
|
109
109
|
schemathesis/specs/openapi/utils.py,sha256=gmW4v6o6sZQifajfPquhFeWmZWGQM89mOOBYZvjnE7g,741
|
|
@@ -127,8 +127,8 @@ schemathesis/transports/auth.py,sha256=ZKFku9gjhIG6445qNC2p_64Yt9Iz_4azbvja8AMpt
|
|
|
127
127
|
schemathesis/transports/content_types.py,sha256=xU8RZWxz-CyWRqQTI2fGYQacB7KasoY7LL_bxPQdyPY,2144
|
|
128
128
|
schemathesis/transports/headers.py,sha256=EDxpm8su0AuhyqZUkMex-OFZMAJN_5NHah7fDT2HDZE,989
|
|
129
129
|
schemathesis/transports/responses.py,sha256=U6z1VW5w19c9qRRoyf2ljkuXR2smTfWikmrTGqlgl18,1619
|
|
130
|
-
schemathesis-3.25.
|
|
131
|
-
schemathesis-3.25.
|
|
132
|
-
schemathesis-3.25.
|
|
133
|
-
schemathesis-3.25.
|
|
134
|
-
schemathesis-3.25.
|
|
130
|
+
schemathesis-3.25.6.dist-info/METADATA,sha256=z6ZLtx0WmChQbDgAb0WmCJlts2QywnGX3CUU4z6FpcQ,15668
|
|
131
|
+
schemathesis-3.25.6.dist-info/WHEEL,sha256=hKi7AIIx6qfnsRbr087vpeJnrVUuDokDHZacPPMW7-Y,87
|
|
132
|
+
schemathesis-3.25.6.dist-info/entry_points.txt,sha256=VHyLcOG7co0nOeuk8WjgpRETk5P1E2iCLrn26Zkn5uk,158
|
|
133
|
+
schemathesis-3.25.6.dist-info/licenses/LICENSE,sha256=PsPYgrDhZ7g9uwihJXNG-XVb55wj2uYhkl2DD8oAzY0,1103
|
|
134
|
+
schemathesis-3.25.6.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|