schemathesis 4.0.0a10__py3-none-any.whl → 4.0.0a12__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- schemathesis/__init__.py +29 -30
- schemathesis/auths.py +65 -24
- schemathesis/checks.py +73 -39
- schemathesis/cli/commands/__init__.py +51 -3
- schemathesis/cli/commands/data.py +10 -0
- schemathesis/cli/commands/run/__init__.py +163 -274
- schemathesis/cli/commands/run/context.py +8 -4
- schemathesis/cli/commands/run/events.py +11 -1
- schemathesis/cli/commands/run/executor.py +70 -78
- schemathesis/cli/commands/run/filters.py +15 -165
- schemathesis/cli/commands/run/handlers/cassettes.py +105 -104
- schemathesis/cli/commands/run/handlers/junitxml.py +5 -4
- schemathesis/cli/commands/run/handlers/output.py +195 -121
- schemathesis/cli/commands/run/loaders.py +35 -50
- schemathesis/cli/commands/run/validation.py +52 -162
- schemathesis/cli/core.py +5 -3
- schemathesis/cli/ext/fs.py +7 -5
- schemathesis/cli/ext/options.py +0 -21
- schemathesis/config/__init__.py +189 -0
- schemathesis/config/_auth.py +51 -0
- schemathesis/config/_checks.py +268 -0
- schemathesis/config/_diff_base.py +99 -0
- schemathesis/config/_env.py +21 -0
- schemathesis/config/_error.py +156 -0
- schemathesis/config/_generation.py +149 -0
- schemathesis/config/_health_check.py +24 -0
- schemathesis/config/_operations.py +327 -0
- schemathesis/config/_output.py +171 -0
- schemathesis/config/_parameters.py +19 -0
- schemathesis/config/_phases.py +187 -0
- schemathesis/config/_projects.py +523 -0
- schemathesis/config/_rate_limit.py +17 -0
- schemathesis/config/_report.py +120 -0
- schemathesis/config/_validator.py +9 -0
- schemathesis/config/_warnings.py +25 -0
- schemathesis/config/schema.json +885 -0
- schemathesis/core/__init__.py +2 -0
- schemathesis/core/compat.py +16 -9
- schemathesis/core/errors.py +24 -4
- schemathesis/core/failures.py +6 -7
- schemathesis/core/hooks.py +20 -0
- schemathesis/core/output/__init__.py +14 -37
- schemathesis/core/output/sanitization.py +3 -146
- schemathesis/core/transport.py +36 -1
- schemathesis/core/validation.py +16 -0
- schemathesis/engine/__init__.py +2 -4
- schemathesis/engine/context.py +42 -43
- schemathesis/engine/core.py +7 -5
- schemathesis/engine/errors.py +60 -1
- schemathesis/engine/events.py +10 -2
- schemathesis/engine/phases/__init__.py +10 -0
- schemathesis/engine/phases/probes.py +11 -8
- schemathesis/engine/phases/stateful/__init__.py +2 -1
- schemathesis/engine/phases/stateful/_executor.py +104 -46
- schemathesis/engine/phases/stateful/context.py +2 -2
- schemathesis/engine/phases/unit/__init__.py +23 -15
- schemathesis/engine/phases/unit/_executor.py +110 -21
- schemathesis/engine/phases/unit/_pool.py +1 -1
- schemathesis/errors.py +2 -0
- schemathesis/filters.py +2 -3
- schemathesis/generation/__init__.py +5 -33
- schemathesis/generation/case.py +6 -3
- schemathesis/generation/coverage.py +154 -124
- schemathesis/generation/hypothesis/builder.py +70 -20
- schemathesis/generation/meta.py +3 -3
- schemathesis/generation/metrics.py +93 -0
- schemathesis/generation/modes.py +0 -8
- schemathesis/generation/overrides.py +37 -1
- schemathesis/generation/stateful/__init__.py +4 -0
- schemathesis/generation/stateful/state_machine.py +9 -1
- schemathesis/graphql/loaders.py +159 -16
- schemathesis/hooks.py +62 -35
- schemathesis/openapi/checks.py +12 -8
- schemathesis/openapi/generation/filters.py +10 -8
- schemathesis/openapi/loaders.py +142 -17
- schemathesis/pytest/lazy.py +2 -5
- schemathesis/pytest/loaders.py +24 -0
- schemathesis/pytest/plugin.py +33 -2
- schemathesis/schemas.py +21 -66
- schemathesis/specs/graphql/scalars.py +37 -3
- schemathesis/specs/graphql/schemas.py +23 -18
- schemathesis/specs/openapi/_hypothesis.py +26 -28
- schemathesis/specs/openapi/checks.py +37 -36
- schemathesis/specs/openapi/examples.py +4 -3
- schemathesis/specs/openapi/formats.py +32 -5
- schemathesis/specs/openapi/media_types.py +44 -1
- schemathesis/specs/openapi/negative/__init__.py +2 -2
- schemathesis/specs/openapi/patterns.py +46 -16
- schemathesis/specs/openapi/references.py +2 -3
- schemathesis/specs/openapi/schemas.py +19 -22
- schemathesis/specs/openapi/stateful/__init__.py +12 -6
- schemathesis/transport/__init__.py +54 -16
- schemathesis/transport/prepare.py +38 -13
- schemathesis/transport/requests.py +12 -9
- schemathesis/transport/wsgi.py +11 -12
- {schemathesis-4.0.0a10.dist-info → schemathesis-4.0.0a12.dist-info}/METADATA +50 -97
- schemathesis-4.0.0a12.dist-info/RECORD +164 -0
- schemathesis/cli/commands/run/checks.py +0 -79
- schemathesis/cli/commands/run/hypothesis.py +0 -78
- schemathesis/cli/commands/run/reports.py +0 -72
- schemathesis/cli/hooks.py +0 -36
- schemathesis/contrib/__init__.py +0 -9
- schemathesis/contrib/openapi/__init__.py +0 -9
- schemathesis/contrib/openapi/fill_missing_examples.py +0 -20
- schemathesis/engine/config.py +0 -59
- schemathesis/experimental/__init__.py +0 -72
- schemathesis/generation/targets.py +0 -69
- schemathesis-4.0.0a10.dist-info/RECORD +0 -153
- {schemathesis-4.0.0a10.dist-info → schemathesis-4.0.0a12.dist-info}/WHEEL +0 -0
- {schemathesis-4.0.0a10.dist-info → schemathesis-4.0.0a12.dist-info}/entry_points.txt +0 -0
- {schemathesis-4.0.0a10.dist-info → schemathesis-4.0.0a12.dist-info}/licenses/LICENSE +0 -0
schemathesis/core/__init__.py
CHANGED
schemathesis/core/compat.py
CHANGED
@@ -1,9 +1,10 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
|
+
import warnings
|
3
4
|
from typing import TYPE_CHECKING
|
4
5
|
|
5
6
|
if TYPE_CHECKING:
|
6
|
-
from jsonschema import RefResolutionError
|
7
|
+
from jsonschema import RefResolutionError, RefResolver
|
7
8
|
|
8
9
|
try:
|
9
10
|
BaseExceptionGroup = BaseExceptionGroup # type: ignore
|
@@ -11,15 +12,21 @@ except NameError:
|
|
11
12
|
from exceptiongroup import BaseExceptionGroup # type: ignore
|
12
13
|
|
13
14
|
|
14
|
-
def __getattr__(name: str) -> type[RefResolutionError] | type[BaseExceptionGroup]:
|
15
|
-
|
16
|
-
|
17
|
-
|
15
|
+
def __getattr__(name: str) -> type[RefResolutionError] | type[RefResolver] | type[BaseExceptionGroup]:
|
16
|
+
with warnings.catch_warnings():
|
17
|
+
warnings.simplefilter("ignore", DeprecationWarning)
|
18
|
+
if name == "RefResolutionError":
|
19
|
+
# `jsonschema` is pinned, this warning is not useful for the end user
|
20
|
+
from jsonschema import RefResolutionError
|
18
21
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
22
|
+
return RefResolutionError
|
23
|
+
if name == "RefResolver":
|
24
|
+
from jsonschema import RefResolver
|
25
|
+
|
26
|
+
return RefResolver
|
27
|
+
if name == "BaseExceptionGroup":
|
28
|
+
return BaseExceptionGroup
|
29
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
23
30
|
|
24
31
|
|
25
32
|
__all__ = ["BaseExceptionGroup", "RefResolutionError"]
|
schemathesis/core/errors.py
CHANGED
@@ -15,6 +15,7 @@ if TYPE_CHECKING:
|
|
15
15
|
from jsonschema import ValidationError
|
16
16
|
from requests import RequestException
|
17
17
|
|
18
|
+
from schemathesis.config import OutputConfig
|
18
19
|
from schemathesis.core.compat import RefResolutionError
|
19
20
|
|
20
21
|
|
@@ -44,7 +45,9 @@ class InvalidSchema(SchemathesisError):
|
|
44
45
|
self.method = method
|
45
46
|
|
46
47
|
@classmethod
|
47
|
-
def from_jsonschema_error(
|
48
|
+
def from_jsonschema_error(
|
49
|
+
cls, error: ValidationError, path: str | None, method: str | None, config: OutputConfig
|
50
|
+
) -> InvalidSchema:
|
48
51
|
if error.absolute_path:
|
49
52
|
part = error.absolute_path[-1]
|
50
53
|
if isinstance(part, int) and len(error.absolute_path) > 1:
|
@@ -56,7 +59,7 @@ class InvalidSchema(SchemathesisError):
|
|
56
59
|
message = "Invalid schema definition"
|
57
60
|
error_path = " -> ".join(str(entry) for entry in error.path) or "[root]"
|
58
61
|
message += f"\n\nLocation:\n {error_path}"
|
59
|
-
instance = truncate_json(error.instance)
|
62
|
+
instance = truncate_json(error.instance, config=config)
|
60
63
|
message += f"\n\nProblematic definition:\n{instance}"
|
61
64
|
message += "\n\nError details:\n "
|
62
65
|
# This default message contains the instance which we already printed
|
@@ -94,6 +97,20 @@ class InvalidSchema(SchemathesisError):
|
|
94
97
|
return actual_test
|
95
98
|
|
96
99
|
|
100
|
+
class HookError(SchemathesisError):
|
101
|
+
"""Happens during hooks loading."""
|
102
|
+
|
103
|
+
module_path: str
|
104
|
+
|
105
|
+
__slots__ = ("module_path",)
|
106
|
+
|
107
|
+
def __init__(self, module_path: str) -> None:
|
108
|
+
self.module_path = module_path
|
109
|
+
|
110
|
+
def __str__(self) -> str:
|
111
|
+
return f"Failed to load Schemathesis extensions from `{self.module_path}`"
|
112
|
+
|
113
|
+
|
97
114
|
class InvalidRegexType(InvalidSchema):
|
98
115
|
"""Raised when an invalid type is used where a regex pattern is expected."""
|
99
116
|
|
@@ -386,7 +403,10 @@ def get_request_error_extras(exc: RequestException) -> list[str]:
|
|
386
403
|
return [reason.strip()]
|
387
404
|
return [" ".join(map(_clean_inner_request_message, inner.args))]
|
388
405
|
if isinstance(exc, ChunkedEncodingError):
|
389
|
-
|
406
|
+
args = exc.args[0].args
|
407
|
+
if len(args) == 1:
|
408
|
+
return [str(args[0])]
|
409
|
+
return [str(args[1])]
|
390
410
|
return []
|
391
411
|
|
392
412
|
|
@@ -421,7 +441,7 @@ def split_traceback(traceback: str) -> list[str]:
|
|
421
441
|
|
422
442
|
|
423
443
|
def format_exception(
|
424
|
-
error:
|
444
|
+
error: BaseException,
|
425
445
|
*,
|
426
446
|
with_traceback: bool = False,
|
427
447
|
skip_frames: int = 0,
|
schemathesis/core/failures.py
CHANGED
@@ -9,8 +9,9 @@ from enum import Enum, auto
|
|
9
9
|
from json import JSONDecodeError
|
10
10
|
from typing import Any, Callable
|
11
11
|
|
12
|
+
from schemathesis.config import OutputConfig
|
12
13
|
from schemathesis.core.compat import BaseExceptionGroup
|
13
|
-
from schemathesis.core.output import
|
14
|
+
from schemathesis.core.output import prepare_response_payload
|
14
15
|
from schemathesis.core.transport import Response
|
15
16
|
|
16
17
|
|
@@ -123,11 +124,6 @@ class CustomFailure(Failure):
|
|
123
124
|
return self.origin
|
124
125
|
|
125
126
|
|
126
|
-
@dataclass
|
127
|
-
class MaxResponseTimeConfig:
|
128
|
-
limit: float = 10.0
|
129
|
-
|
130
|
-
|
131
127
|
class ResponseTimeExceeded(Failure):
|
132
128
|
"""Response took longer than expected."""
|
133
129
|
|
@@ -138,7 +134,7 @@ class ResponseTimeExceeded(Failure):
|
|
138
134
|
*,
|
139
135
|
operation: str,
|
140
136
|
elapsed: float,
|
141
|
-
deadline:
|
137
|
+
deadline: float,
|
142
138
|
message: str,
|
143
139
|
title: str = "Response time limit exceeded",
|
144
140
|
case_id: str | None = None,
|
@@ -245,6 +241,9 @@ class FailureGroup(BaseExceptionGroup):
|
|
245
241
|
|
246
242
|
exceptions: Sequence[Failure]
|
247
243
|
|
244
|
+
def __init__(self, exceptions: Sequence[Failure], message: str = "", /) -> None:
|
245
|
+
super().__init__(message, exceptions)
|
246
|
+
|
248
247
|
def __new__(cls, failures: Sequence[Failure], message: str | None = None) -> FailureGroup:
|
249
248
|
if message is None:
|
250
249
|
message = failure_report_title(failures)
|
@@ -0,0 +1,20 @@
|
|
1
|
+
import os
|
2
|
+
import sys
|
3
|
+
|
4
|
+
from schemathesis.core.errors import HookError
|
5
|
+
|
6
|
+
HOOKS_MODULE_ENV_VAR = "SCHEMATHESIS_HOOKS"
|
7
|
+
|
8
|
+
|
9
|
+
def load_from_env() -> None:
|
10
|
+
hooks = os.getenv(HOOKS_MODULE_ENV_VAR)
|
11
|
+
if hooks:
|
12
|
+
load_from_path(hooks)
|
13
|
+
|
14
|
+
|
15
|
+
def load_from_path(module_path: str) -> None:
|
16
|
+
try:
|
17
|
+
sys.path.append(os.getcwd()) # fix ModuleNotFoundError module in cwd
|
18
|
+
__import__(module_path)
|
19
|
+
except Exception as exc:
|
20
|
+
raise HookError(module_path) from exc
|
@@ -1,54 +1,32 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
3
|
import json
|
4
|
-
from
|
5
|
-
from typing import Any
|
4
|
+
from typing import TYPE_CHECKING, Any
|
6
5
|
|
7
|
-
|
8
|
-
|
9
|
-
MAX_LINES = 10
|
10
|
-
MAX_WIDTH = 80
|
11
|
-
|
12
|
-
|
13
|
-
@dataclass
|
14
|
-
class OutputConfig:
|
15
|
-
"""Options for configuring various aspects of Schemathesis output."""
|
16
|
-
|
17
|
-
sanitize: bool = True
|
18
|
-
truncate: bool = True
|
19
|
-
max_payload_size: int = MAX_PAYLOAD_SIZE
|
20
|
-
max_lines: int = MAX_LINES
|
21
|
-
max_width: int = MAX_WIDTH
|
6
|
+
if TYPE_CHECKING:
|
7
|
+
from schemathesis.config import OutputConfig
|
22
8
|
|
23
|
-
|
24
|
-
def from_parent(cls, parent: OutputConfig | None = None, **changes: Any) -> OutputConfig:
|
25
|
-
parent = parent or OutputConfig()
|
26
|
-
return parent.replace(**changes)
|
27
|
-
|
28
|
-
def replace(self, **changes: Any) -> OutputConfig:
|
29
|
-
"""Create a new instance with updated values."""
|
30
|
-
return replace(self, **changes)
|
9
|
+
TRUNCATED = "// Output truncated..."
|
31
10
|
|
32
11
|
|
33
|
-
def truncate_json(data: Any, *, config: OutputConfig | None = None) -> str:
|
34
|
-
config = config or OutputConfig()
|
12
|
+
def truncate_json(data: Any, *, config: OutputConfig, max_lines: int | None = None) -> str:
|
35
13
|
# Convert JSON to string with indentation
|
36
14
|
indent = 4
|
37
15
|
serialized = json.dumps(data, indent=indent)
|
38
|
-
if not config.
|
16
|
+
if not config.truncation.enabled:
|
39
17
|
return serialized
|
40
18
|
|
19
|
+
max_lines = max_lines if max_lines is not None else config.truncation.max_lines
|
41
20
|
# Split string by lines
|
42
|
-
|
43
21
|
lines = [
|
44
|
-
line[: config.max_width - 3] + "..." if len(line) > config.max_width else line
|
22
|
+
line[: config.truncation.max_width - 3] + "..." if len(line) > config.truncation.max_width else line
|
45
23
|
for line in serialized.split("\n")
|
46
24
|
]
|
47
25
|
|
48
|
-
if len(lines) <=
|
26
|
+
if len(lines) <= max_lines:
|
49
27
|
return "\n".join(lines)
|
50
28
|
|
51
|
-
truncated_lines = lines[:
|
29
|
+
truncated_lines = lines[: max_lines - 1]
|
52
30
|
indentation = " " * indent
|
53
31
|
truncated_lines.append(f"{indentation}{TRUNCATED}")
|
54
32
|
truncated_lines.append(lines[-1])
|
@@ -56,14 +34,13 @@ def truncate_json(data: Any, *, config: OutputConfig | None = None) -> str:
|
|
56
34
|
return "\n".join(truncated_lines)
|
57
35
|
|
58
36
|
|
59
|
-
def prepare_response_payload(payload: str, *, config: OutputConfig
|
37
|
+
def prepare_response_payload(payload: str, *, config: OutputConfig) -> str:
|
60
38
|
if payload.endswith("\r\n"):
|
61
39
|
payload = payload[:-2]
|
62
40
|
elif payload.endswith("\n"):
|
63
41
|
payload = payload[:-1]
|
64
|
-
|
65
|
-
if not config.truncate:
|
42
|
+
if not config.truncation.enabled:
|
66
43
|
return payload
|
67
|
-
if len(payload) > config.max_payload_size:
|
68
|
-
payload = payload[: config.max_payload_size] + f" {TRUNCATED}"
|
44
|
+
if len(payload) > config.truncation.max_payload_size:
|
45
|
+
payload = payload[: config.truncation.max_payload_size] + f" {TRUNCATED}"
|
69
46
|
return payload
|
@@ -1,160 +1,18 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
3
|
from collections.abc import MutableMapping, MutableSequence
|
4
|
-
from dataclasses import dataclass, replace
|
5
4
|
from typing import Any
|
6
5
|
from urllib.parse import parse_qs, urlencode, urlsplit, urlunsplit
|
7
6
|
|
8
|
-
from schemathesis.
|
7
|
+
from schemathesis.config import SanitizationConfig
|
9
8
|
|
10
|
-
# Exact keys to sanitize
|
11
|
-
DEFAULT_KEYS_TO_SANITIZE = frozenset(
|
12
|
-
(
|
13
|
-
"phpsessid",
|
14
|
-
"xsrf-token",
|
15
|
-
"_csrf",
|
16
|
-
"_csrf_token",
|
17
|
-
"_session",
|
18
|
-
"_xsrf",
|
19
|
-
"aiohttp_session",
|
20
|
-
"api_key",
|
21
|
-
"api-key",
|
22
|
-
"apikey",
|
23
|
-
"auth",
|
24
|
-
"authorization",
|
25
|
-
"connect.sid",
|
26
|
-
"cookie",
|
27
|
-
"credentials",
|
28
|
-
"csrf",
|
29
|
-
"csrf_token",
|
30
|
-
"csrf-token",
|
31
|
-
"csrftoken",
|
32
|
-
"ip_address",
|
33
|
-
"mysql_pwd",
|
34
|
-
"passwd",
|
35
|
-
"password",
|
36
|
-
"private_key",
|
37
|
-
"private-key",
|
38
|
-
"privatekey",
|
39
|
-
"remote_addr",
|
40
|
-
"remote-addr",
|
41
|
-
"secret",
|
42
|
-
"session",
|
43
|
-
"sessionid",
|
44
|
-
"set_cookie",
|
45
|
-
"set-cookie",
|
46
|
-
"token",
|
47
|
-
"x_api_key",
|
48
|
-
"x-api-key",
|
49
|
-
"x_csrftoken",
|
50
|
-
"x-csrftoken",
|
51
|
-
"x_forwarded_for",
|
52
|
-
"x-forwarded-for",
|
53
|
-
"x_real_ip",
|
54
|
-
"x-real-ip",
|
55
|
-
)
|
56
|
-
)
|
57
9
|
|
58
|
-
|
59
|
-
DEFAULT_SENSITIVE_MARKERS = frozenset(
|
60
|
-
(
|
61
|
-
"token",
|
62
|
-
"key",
|
63
|
-
"secret",
|
64
|
-
"password",
|
65
|
-
"auth",
|
66
|
-
"session",
|
67
|
-
"passwd",
|
68
|
-
"credential",
|
69
|
-
)
|
70
|
-
)
|
71
|
-
|
72
|
-
DEFAULT_REPLACEMENT = "[Filtered]"
|
73
|
-
|
74
|
-
|
75
|
-
@dataclass
|
76
|
-
class SanitizationConfig:
|
77
|
-
"""Configuration class for sanitizing sensitive data."""
|
78
|
-
|
79
|
-
keys_to_sanitize: frozenset[str] = DEFAULT_KEYS_TO_SANITIZE
|
80
|
-
sensitive_markers: frozenset[str] = DEFAULT_SENSITIVE_MARKERS
|
81
|
-
replacement: str = DEFAULT_REPLACEMENT
|
82
|
-
|
83
|
-
@classmethod
|
84
|
-
def from_config(
|
85
|
-
cls,
|
86
|
-
base_config: SanitizationConfig,
|
87
|
-
*,
|
88
|
-
replacement: str | NotSet = NOT_SET,
|
89
|
-
keys_to_sanitize: list[str] | NotSet = NOT_SET,
|
90
|
-
sensitive_markers: list[str] | NotSet = NOT_SET,
|
91
|
-
) -> SanitizationConfig:
|
92
|
-
"""Create a new config by replacing specified values."""
|
93
|
-
kwargs: dict[str, Any] = {}
|
94
|
-
if not isinstance(replacement, NotSet):
|
95
|
-
kwargs["replacement"] = replacement
|
96
|
-
if not isinstance(keys_to_sanitize, NotSet):
|
97
|
-
kwargs["keys_to_sanitize"] = frozenset(key.lower() for key in keys_to_sanitize)
|
98
|
-
if not isinstance(sensitive_markers, NotSet):
|
99
|
-
kwargs["sensitive_markers"] = frozenset(marker.lower() for marker in sensitive_markers)
|
100
|
-
return replace(base_config, **kwargs)
|
101
|
-
|
102
|
-
def extend(
|
103
|
-
self,
|
104
|
-
*,
|
105
|
-
keys_to_sanitize: list[str] | NotSet = NOT_SET,
|
106
|
-
sensitive_markers: list[str] | NotSet = NOT_SET,
|
107
|
-
) -> SanitizationConfig:
|
108
|
-
"""Create a new config by extending current sets."""
|
109
|
-
config = self
|
110
|
-
if not isinstance(keys_to_sanitize, NotSet):
|
111
|
-
new_keys = config.keys_to_sanitize.union(key.lower() for key in keys_to_sanitize)
|
112
|
-
config = replace(config, keys_to_sanitize=new_keys)
|
113
|
-
|
114
|
-
if not isinstance(sensitive_markers, NotSet):
|
115
|
-
new_markers = config.sensitive_markers.union(marker.lower() for marker in sensitive_markers)
|
116
|
-
config = replace(config, sensitive_markers=new_markers)
|
117
|
-
|
118
|
-
return config
|
119
|
-
|
120
|
-
|
121
|
-
_DEFAULT_SANITIZATION_CONFIG = SanitizationConfig()
|
122
|
-
|
123
|
-
|
124
|
-
def configure(
|
125
|
-
replacement: str | NotSet = NOT_SET,
|
126
|
-
keys_to_sanitize: list[str] | NotSet = NOT_SET,
|
127
|
-
sensitive_markers: list[str] | NotSet = NOT_SET,
|
128
|
-
) -> None:
|
129
|
-
"""Replace current sanitization configuration."""
|
130
|
-
global _DEFAULT_SANITIZATION_CONFIG
|
131
|
-
_DEFAULT_SANITIZATION_CONFIG = SanitizationConfig.from_config(
|
132
|
-
_DEFAULT_SANITIZATION_CONFIG,
|
133
|
-
replacement=replacement,
|
134
|
-
keys_to_sanitize=keys_to_sanitize,
|
135
|
-
sensitive_markers=sensitive_markers,
|
136
|
-
)
|
137
|
-
|
138
|
-
|
139
|
-
def extend(
|
140
|
-
keys_to_sanitize: list[str] | NotSet = NOT_SET,
|
141
|
-
sensitive_markers: list[str] | NotSet = NOT_SET,
|
142
|
-
) -> None:
|
143
|
-
"""Extend current sanitization configuration."""
|
144
|
-
global _DEFAULT_SANITIZATION_CONFIG
|
145
|
-
_DEFAULT_SANITIZATION_CONFIG = _DEFAULT_SANITIZATION_CONFIG.extend(
|
146
|
-
keys_to_sanitize=keys_to_sanitize,
|
147
|
-
sensitive_markers=sensitive_markers,
|
148
|
-
)
|
149
|
-
|
150
|
-
|
151
|
-
def sanitize_value(item: Any, *, config: SanitizationConfig | None = None) -> None:
|
10
|
+
def sanitize_value(item: Any, *, config: SanitizationConfig) -> None:
|
152
11
|
"""Sanitize sensitive values within a given item.
|
153
12
|
|
154
13
|
This function is recursive and will sanitize sensitive data within nested
|
155
14
|
dictionaries and lists as well.
|
156
15
|
"""
|
157
|
-
config = config or _DEFAULT_SANITIZATION_CONFIG
|
158
16
|
if isinstance(item, MutableMapping):
|
159
17
|
for key in list(item.keys()):
|
160
18
|
lower_key = key.lower()
|
@@ -172,12 +30,11 @@ def sanitize_value(item: Any, *, config: SanitizationConfig | None = None) -> No
|
|
172
30
|
sanitize_value(value, config=config)
|
173
31
|
|
174
32
|
|
175
|
-
def sanitize_url(url: str, *, config: SanitizationConfig
|
33
|
+
def sanitize_url(url: str, *, config: SanitizationConfig) -> str:
|
176
34
|
"""Sanitize sensitive parts of a given URL.
|
177
35
|
|
178
36
|
This function will sanitize the authority and query parameters in the URL.
|
179
37
|
"""
|
180
|
-
config = config or _DEFAULT_SANITIZATION_CONFIG
|
181
38
|
parsed = urlsplit(url)
|
182
39
|
|
183
40
|
# Sanitize authority
|
schemathesis/core/transport.py
CHANGED
@@ -27,7 +27,30 @@ def prepare_urlencoded(data: Any) -> Any:
|
|
27
27
|
|
28
28
|
|
29
29
|
class Response:
|
30
|
-
"""
|
30
|
+
"""HTTP response wrapper that normalizes different transport implementations.
|
31
|
+
|
32
|
+
Provides a consistent interface for accessing response data whether the request
|
33
|
+
was made via HTTP, ASGI, or WSGI transports.
|
34
|
+
"""
|
35
|
+
|
36
|
+
status_code: int
|
37
|
+
"""HTTP status code (e.g., 200, 404, 500)."""
|
38
|
+
headers: dict[str, list[str]]
|
39
|
+
"""Response headers with lowercase keys and list values."""
|
40
|
+
content: bytes
|
41
|
+
"""Raw response body as bytes."""
|
42
|
+
request: requests.PreparedRequest
|
43
|
+
"""The request that generated this response."""
|
44
|
+
elapsed: float
|
45
|
+
"""Response time in seconds."""
|
46
|
+
verify: bool
|
47
|
+
"""Whether TLS verification was enabled for the request."""
|
48
|
+
message: str
|
49
|
+
"""HTTP status message (e.g., "OK", "Not Found")."""
|
50
|
+
http_version: str
|
51
|
+
"""HTTP protocol version ("1.0" or "1.1")."""
|
52
|
+
encoding: str | None
|
53
|
+
"""Character encoding for text content, if detected."""
|
31
54
|
|
32
55
|
__slots__ = (
|
33
56
|
"status_code",
|
@@ -90,19 +113,31 @@ class Response:
|
|
90
113
|
|
91
114
|
@property
|
92
115
|
def text(self) -> str:
|
116
|
+
"""Decode response content as text using the detected or default encoding."""
|
93
117
|
return self.content.decode(self.encoding if self.encoding else "utf-8")
|
94
118
|
|
95
119
|
def json(self) -> Any:
|
120
|
+
"""Parse response content as JSON.
|
121
|
+
|
122
|
+
Returns:
|
123
|
+
Parsed JSON data (dict, list, or primitive types)
|
124
|
+
|
125
|
+
Raises:
|
126
|
+
json.JSONDecodeError: If content is not valid JSON
|
127
|
+
|
128
|
+
"""
|
96
129
|
if self._json is None:
|
97
130
|
self._json = json.loads(self.text)
|
98
131
|
return self._json
|
99
132
|
|
100
133
|
@property
|
101
134
|
def body_size(self) -> int | None:
|
135
|
+
"""Size of response body in bytes, or None if no content."""
|
102
136
|
return len(self.content) if self.content else None
|
103
137
|
|
104
138
|
@property
|
105
139
|
def encoded_body(self) -> str | None:
|
140
|
+
"""Base64-encoded response body for binary-safe serialization."""
|
106
141
|
if self._encoded_body is None and self.content:
|
107
142
|
self._encoded_body = base64.b64encode(self.content).decode()
|
108
143
|
return self._encoded_body
|
schemathesis/core/validation.py
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
import re
|
2
|
+
from urllib.parse import urlparse
|
2
3
|
|
3
4
|
# Adapted from http.client._is_illegal_header_value
|
4
5
|
INVALID_HEADER_RE = re.compile(r"\n(?![ \t])|\r(?![ \t\n])")
|
@@ -36,3 +37,18 @@ def contains_unicode_surrogate_pair(item: object) -> bool:
|
|
36
37
|
if isinstance(item, list):
|
37
38
|
return any(isinstance(item_, str) and bool(_contains_surrogate_pair(item_)) for item_ in item)
|
38
39
|
return isinstance(item, str) and bool(_contains_surrogate_pair(item))
|
40
|
+
|
41
|
+
|
42
|
+
INVALID_BASE_URL_MESSAGE = (
|
43
|
+
"The provided base URL is invalid. This URL serves as a prefix for all API endpoints you want to test. "
|
44
|
+
"Make sure it is a properly formatted URL."
|
45
|
+
)
|
46
|
+
|
47
|
+
|
48
|
+
def validate_base_url(value: str) -> None:
|
49
|
+
try:
|
50
|
+
netloc = urlparse(value).netloc
|
51
|
+
except ValueError as exc:
|
52
|
+
raise ValueError(INVALID_BASE_URL_MESSAGE) from exc
|
53
|
+
if value and not netloc:
|
54
|
+
raise ValueError(INVALID_BASE_URL_MESSAGE)
|
schemathesis/engine/__init__.py
CHANGED
@@ -3,8 +3,6 @@ from __future__ import annotations
|
|
3
3
|
from enum import Enum
|
4
4
|
from typing import TYPE_CHECKING
|
5
5
|
|
6
|
-
from schemathesis.engine.config import EngineConfig
|
7
|
-
|
8
6
|
if TYPE_CHECKING:
|
9
7
|
from schemathesis.engine.core import Engine
|
10
8
|
from schemathesis.schemas import BaseSchema
|
@@ -24,7 +22,7 @@ class Status(str, Enum):
|
|
24
22
|
_STATUS_ORDER = {Status.SUCCESS: 0, Status.FAILURE: 1, Status.ERROR: 2, Status.INTERRUPTED: 3, Status.SKIP: 4}
|
25
23
|
|
26
24
|
|
27
|
-
def from_schema(schema: BaseSchema
|
25
|
+
def from_schema(schema: BaseSchema) -> Engine:
|
28
26
|
from .core import Engine
|
29
27
|
|
30
|
-
return Engine(schema=schema
|
28
|
+
return Engine(schema=schema)
|