schemathesis 3.25.6__py3-none-any.whl → 4.0.0a1__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 +27 -65
- schemathesis/auths.py +102 -82
- schemathesis/checks.py +126 -46
- schemathesis/cli/__init__.py +11 -1760
- schemathesis/cli/__main__.py +4 -0
- schemathesis/cli/commands/__init__.py +37 -0
- schemathesis/cli/commands/run/__init__.py +662 -0
- schemathesis/cli/commands/run/checks.py +80 -0
- schemathesis/cli/commands/run/context.py +117 -0
- schemathesis/cli/commands/run/events.py +35 -0
- schemathesis/cli/commands/run/executor.py +138 -0
- schemathesis/cli/commands/run/filters.py +194 -0
- schemathesis/cli/commands/run/handlers/__init__.py +46 -0
- schemathesis/cli/commands/run/handlers/base.py +18 -0
- schemathesis/cli/commands/run/handlers/cassettes.py +494 -0
- schemathesis/cli/commands/run/handlers/junitxml.py +54 -0
- schemathesis/cli/commands/run/handlers/output.py +746 -0
- schemathesis/cli/commands/run/hypothesis.py +105 -0
- schemathesis/cli/commands/run/loaders.py +129 -0
- schemathesis/cli/{callbacks.py → commands/run/validation.py} +103 -174
- schemathesis/cli/constants.py +5 -52
- schemathesis/cli/core.py +17 -0
- schemathesis/cli/ext/fs.py +14 -0
- schemathesis/cli/ext/groups.py +55 -0
- schemathesis/cli/{options.py → ext/options.py} +39 -10
- schemathesis/cli/hooks.py +36 -0
- schemathesis/contrib/__init__.py +1 -3
- schemathesis/contrib/openapi/__init__.py +1 -3
- schemathesis/contrib/openapi/fill_missing_examples.py +3 -5
- schemathesis/core/__init__.py +58 -0
- schemathesis/core/compat.py +25 -0
- schemathesis/core/control.py +2 -0
- schemathesis/core/curl.py +58 -0
- schemathesis/core/deserialization.py +65 -0
- schemathesis/core/errors.py +370 -0
- schemathesis/core/failures.py +285 -0
- schemathesis/core/fs.py +19 -0
- schemathesis/{_lazy_import.py → core/lazy_import.py} +1 -0
- schemathesis/core/loaders.py +104 -0
- schemathesis/core/marks.py +66 -0
- schemathesis/{transports/content_types.py → core/media_types.py} +17 -13
- schemathesis/core/output/__init__.py +69 -0
- schemathesis/core/output/sanitization.py +197 -0
- schemathesis/core/rate_limit.py +60 -0
- schemathesis/core/registries.py +31 -0
- schemathesis/{internal → core}/result.py +1 -1
- schemathesis/core/transforms.py +113 -0
- schemathesis/core/transport.py +108 -0
- schemathesis/core/validation.py +38 -0
- schemathesis/core/version.py +7 -0
- schemathesis/engine/__init__.py +30 -0
- schemathesis/engine/config.py +59 -0
- schemathesis/engine/context.py +119 -0
- schemathesis/engine/control.py +36 -0
- schemathesis/engine/core.py +157 -0
- schemathesis/engine/errors.py +394 -0
- schemathesis/engine/events.py +337 -0
- schemathesis/engine/phases/__init__.py +66 -0
- schemathesis/{runner → engine/phases}/probes.py +50 -67
- schemathesis/engine/phases/stateful/__init__.py +65 -0
- schemathesis/engine/phases/stateful/_executor.py +326 -0
- schemathesis/engine/phases/stateful/context.py +85 -0
- schemathesis/engine/phases/unit/__init__.py +174 -0
- schemathesis/engine/phases/unit/_executor.py +321 -0
- schemathesis/engine/phases/unit/_pool.py +74 -0
- schemathesis/engine/recorder.py +241 -0
- schemathesis/errors.py +31 -0
- schemathesis/experimental/__init__.py +18 -14
- schemathesis/filters.py +103 -14
- schemathesis/generation/__init__.py +21 -37
- schemathesis/generation/case.py +190 -0
- schemathesis/generation/coverage.py +931 -0
- schemathesis/generation/hypothesis/__init__.py +30 -0
- schemathesis/generation/hypothesis/builder.py +585 -0
- schemathesis/generation/hypothesis/examples.py +50 -0
- schemathesis/generation/hypothesis/given.py +66 -0
- schemathesis/generation/hypothesis/reporting.py +14 -0
- schemathesis/generation/hypothesis/strategies.py +16 -0
- schemathesis/generation/meta.py +115 -0
- schemathesis/generation/modes.py +28 -0
- schemathesis/generation/overrides.py +96 -0
- schemathesis/generation/stateful/__init__.py +20 -0
- schemathesis/{stateful → generation/stateful}/state_machine.py +68 -81
- schemathesis/generation/targets.py +69 -0
- schemathesis/graphql/__init__.py +15 -0
- schemathesis/graphql/checks.py +115 -0
- schemathesis/graphql/loaders.py +131 -0
- schemathesis/hooks.py +99 -67
- schemathesis/openapi/__init__.py +13 -0
- schemathesis/openapi/checks.py +412 -0
- schemathesis/openapi/generation/__init__.py +0 -0
- schemathesis/openapi/generation/filters.py +63 -0
- schemathesis/openapi/loaders.py +178 -0
- schemathesis/pytest/__init__.py +5 -0
- schemathesis/pytest/control_flow.py +7 -0
- schemathesis/pytest/lazy.py +273 -0
- schemathesis/pytest/loaders.py +12 -0
- schemathesis/{extra/pytest_plugin.py → pytest/plugin.py} +106 -127
- schemathesis/python/__init__.py +0 -0
- schemathesis/python/asgi.py +12 -0
- schemathesis/python/wsgi.py +12 -0
- schemathesis/schemas.py +537 -261
- schemathesis/specs/graphql/__init__.py +0 -1
- schemathesis/specs/graphql/_cache.py +25 -0
- schemathesis/specs/graphql/nodes.py +1 -0
- schemathesis/specs/graphql/scalars.py +7 -5
- schemathesis/specs/graphql/schemas.py +215 -187
- schemathesis/specs/graphql/validation.py +11 -18
- schemathesis/specs/openapi/__init__.py +7 -1
- schemathesis/specs/openapi/_cache.py +122 -0
- schemathesis/specs/openapi/_hypothesis.py +146 -165
- schemathesis/specs/openapi/checks.py +565 -67
- schemathesis/specs/openapi/converter.py +33 -6
- schemathesis/specs/openapi/definitions.py +11 -18
- schemathesis/specs/openapi/examples.py +139 -23
- schemathesis/specs/openapi/expressions/__init__.py +37 -2
- schemathesis/specs/openapi/expressions/context.py +4 -6
- schemathesis/specs/openapi/expressions/extractors.py +23 -0
- schemathesis/specs/openapi/expressions/lexer.py +20 -18
- schemathesis/specs/openapi/expressions/nodes.py +38 -14
- schemathesis/specs/openapi/expressions/parser.py +26 -5
- schemathesis/specs/openapi/formats.py +45 -0
- schemathesis/specs/openapi/links.py +65 -165
- schemathesis/specs/openapi/media_types.py +32 -0
- schemathesis/specs/openapi/negative/__init__.py +7 -3
- schemathesis/specs/openapi/negative/mutations.py +24 -8
- schemathesis/specs/openapi/parameters.py +46 -30
- schemathesis/specs/openapi/patterns.py +137 -0
- schemathesis/specs/openapi/references.py +47 -57
- schemathesis/specs/openapi/schemas.py +478 -369
- schemathesis/specs/openapi/security.py +25 -7
- schemathesis/specs/openapi/serialization.py +11 -6
- schemathesis/specs/openapi/stateful/__init__.py +185 -73
- schemathesis/specs/openapi/utils.py +6 -1
- schemathesis/transport/__init__.py +104 -0
- schemathesis/transport/asgi.py +26 -0
- schemathesis/transport/prepare.py +99 -0
- schemathesis/transport/requests.py +221 -0
- schemathesis/{_xml.py → transport/serialization.py} +143 -28
- schemathesis/transport/wsgi.py +165 -0
- schemathesis-4.0.0a1.dist-info/METADATA +297 -0
- schemathesis-4.0.0a1.dist-info/RECORD +152 -0
- {schemathesis-3.25.6.dist-info → schemathesis-4.0.0a1.dist-info}/WHEEL +1 -1
- {schemathesis-3.25.6.dist-info → schemathesis-4.0.0a1.dist-info}/entry_points.txt +1 -1
- schemathesis/_compat.py +0 -74
- schemathesis/_dependency_versions.py +0 -17
- schemathesis/_hypothesis.py +0 -246
- schemathesis/_override.py +0 -49
- schemathesis/cli/cassettes.py +0 -375
- schemathesis/cli/context.py +0 -58
- schemathesis/cli/debug.py +0 -26
- schemathesis/cli/handlers.py +0 -16
- schemathesis/cli/junitxml.py +0 -43
- schemathesis/cli/output/__init__.py +0 -1
- schemathesis/cli/output/default.py +0 -790
- schemathesis/cli/output/short.py +0 -44
- schemathesis/cli/sanitization.py +0 -20
- schemathesis/code_samples.py +0 -149
- schemathesis/constants.py +0 -55
- schemathesis/contrib/openapi/formats/__init__.py +0 -9
- schemathesis/contrib/openapi/formats/uuid.py +0 -15
- schemathesis/contrib/unique_data.py +0 -41
- schemathesis/exceptions.py +0 -560
- schemathesis/extra/_aiohttp.py +0 -27
- schemathesis/extra/_flask.py +0 -10
- schemathesis/extra/_server.py +0 -17
- schemathesis/failures.py +0 -209
- schemathesis/fixups/__init__.py +0 -36
- schemathesis/fixups/fast_api.py +0 -41
- schemathesis/fixups/utf8_bom.py +0 -29
- schemathesis/graphql.py +0 -4
- schemathesis/internal/__init__.py +0 -7
- schemathesis/internal/copy.py +0 -13
- schemathesis/internal/datetime.py +0 -5
- schemathesis/internal/deprecation.py +0 -34
- schemathesis/internal/jsonschema.py +0 -35
- schemathesis/internal/transformation.py +0 -15
- schemathesis/internal/validation.py +0 -34
- schemathesis/lazy.py +0 -361
- schemathesis/loaders.py +0 -120
- schemathesis/models.py +0 -1234
- schemathesis/parameters.py +0 -86
- schemathesis/runner/__init__.py +0 -570
- schemathesis/runner/events.py +0 -329
- schemathesis/runner/impl/__init__.py +0 -3
- schemathesis/runner/impl/core.py +0 -1035
- schemathesis/runner/impl/solo.py +0 -90
- schemathesis/runner/impl/threadpool.py +0 -400
- schemathesis/runner/serialization.py +0 -411
- schemathesis/sanitization.py +0 -248
- schemathesis/serializers.py +0 -323
- schemathesis/service/__init__.py +0 -18
- schemathesis/service/auth.py +0 -11
- schemathesis/service/ci.py +0 -201
- schemathesis/service/client.py +0 -100
- schemathesis/service/constants.py +0 -38
- schemathesis/service/events.py +0 -57
- schemathesis/service/hosts.py +0 -107
- schemathesis/service/metadata.py +0 -46
- schemathesis/service/models.py +0 -49
- schemathesis/service/report.py +0 -255
- schemathesis/service/serialization.py +0 -199
- schemathesis/service/usage.py +0 -65
- schemathesis/specs/graphql/loaders.py +0 -344
- schemathesis/specs/openapi/filters.py +0 -49
- schemathesis/specs/openapi/loaders.py +0 -667
- schemathesis/specs/openapi/stateful/links.py +0 -92
- schemathesis/specs/openapi/validation.py +0 -25
- schemathesis/stateful/__init__.py +0 -133
- schemathesis/targets.py +0 -45
- schemathesis/throttling.py +0 -41
- schemathesis/transports/__init__.py +0 -5
- schemathesis/transports/auth.py +0 -15
- schemathesis/transports/headers.py +0 -35
- schemathesis/transports/responses.py +0 -52
- schemathesis/types.py +0 -35
- schemathesis/utils.py +0 -169
- schemathesis-3.25.6.dist-info/METADATA +0 -356
- schemathesis-3.25.6.dist-info/RECORD +0 -134
- /schemathesis/{extra → cli/ext}/__init__.py +0 -0
- {schemathesis-3.25.6.dist-info → schemathesis-4.0.0a1.dist-info}/licenses/LICENSE +0 -0
@@ -1,411 +0,0 @@
|
|
1
|
-
"""Transformation from Schemathesis-specific data structures to ones that can be serialized and sent over network.
|
2
|
-
|
3
|
-
They all consist of primitive types and don't have references to schemas, app, etc.
|
4
|
-
"""
|
5
|
-
from __future__ import annotations
|
6
|
-
import logging
|
7
|
-
import re
|
8
|
-
from dataclasses import dataclass, field
|
9
|
-
from typing import Any, TYPE_CHECKING, cast
|
10
|
-
|
11
|
-
from ..transports import serialize_payload
|
12
|
-
from ..code_samples import get_excluded_headers
|
13
|
-
from ..exceptions import (
|
14
|
-
FailureContext,
|
15
|
-
InternalError,
|
16
|
-
make_unique_by_key,
|
17
|
-
format_exception,
|
18
|
-
extract_requests_exception_details,
|
19
|
-
RuntimeErrorType,
|
20
|
-
DeadlineExceeded,
|
21
|
-
OperationSchemaError,
|
22
|
-
BodyInGetRequestError,
|
23
|
-
InvalidRegularExpression,
|
24
|
-
SerializationError,
|
25
|
-
UnboundPrefixError,
|
26
|
-
)
|
27
|
-
from ..models import Case, Check, Interaction, Request, Response, Status, TestResult
|
28
|
-
|
29
|
-
if TYPE_CHECKING:
|
30
|
-
import hypothesis.errors
|
31
|
-
from requests.structures import CaseInsensitiveDict
|
32
|
-
|
33
|
-
|
34
|
-
@dataclass
|
35
|
-
class SerializedCase:
|
36
|
-
# Case data
|
37
|
-
id: str
|
38
|
-
path_parameters: dict[str, Any] | None
|
39
|
-
headers: dict[str, Any] | None
|
40
|
-
cookies: dict[str, Any] | None
|
41
|
-
query: dict[str, Any] | None
|
42
|
-
body: str | None
|
43
|
-
media_type: str | None
|
44
|
-
data_generation_method: str | None
|
45
|
-
# Operation data
|
46
|
-
method: str
|
47
|
-
url: str
|
48
|
-
path_template: str
|
49
|
-
verbose_name: str
|
50
|
-
# Transport info
|
51
|
-
verify: bool
|
52
|
-
# Headers coming from sources outside data generation
|
53
|
-
extra_headers: dict[str, Any]
|
54
|
-
|
55
|
-
@classmethod
|
56
|
-
def from_case(cls, case: Case, headers: dict[str, Any] | None, verify: bool) -> SerializedCase:
|
57
|
-
# `headers` include not only explicitly provided headers but also ones added by hooks, custom auth, etc.
|
58
|
-
request_data = case.prepare_code_sample_data(headers)
|
59
|
-
serialized_body = _serialize_body(request_data.body)
|
60
|
-
return cls(
|
61
|
-
id=case.id,
|
62
|
-
path_parameters=case.path_parameters,
|
63
|
-
headers=dict(case.headers) if case.headers is not None else None,
|
64
|
-
cookies=case.cookies,
|
65
|
-
query=case.query,
|
66
|
-
body=serialized_body,
|
67
|
-
media_type=case.media_type,
|
68
|
-
data_generation_method=case.data_generation_method.as_short_name()
|
69
|
-
if case.data_generation_method is not None
|
70
|
-
else None,
|
71
|
-
method=case.method,
|
72
|
-
url=request_data.url,
|
73
|
-
path_template=case.path,
|
74
|
-
verbose_name=case.operation.verbose_name,
|
75
|
-
verify=verify,
|
76
|
-
extra_headers=request_data.headers,
|
77
|
-
)
|
78
|
-
|
79
|
-
|
80
|
-
def _serialize_body(body: str | bytes | None) -> str | None:
|
81
|
-
if body is None:
|
82
|
-
return None
|
83
|
-
if isinstance(body, str):
|
84
|
-
body = body.encode("utf-8")
|
85
|
-
return serialize_payload(body)
|
86
|
-
|
87
|
-
|
88
|
-
@dataclass
|
89
|
-
class SerializedCheck:
|
90
|
-
# Check name
|
91
|
-
name: str
|
92
|
-
# Check result
|
93
|
-
value: Status
|
94
|
-
request: Request
|
95
|
-
response: Response | None
|
96
|
-
# Generated example
|
97
|
-
example: SerializedCase
|
98
|
-
# Message could be absent for plain `assert` statements
|
99
|
-
message: str | None = None
|
100
|
-
# Failure-specific context
|
101
|
-
context: FailureContext | None = None
|
102
|
-
# Cases & responses that were made before this one
|
103
|
-
history: list[SerializedHistoryEntry] = field(default_factory=list)
|
104
|
-
|
105
|
-
@classmethod
|
106
|
-
def from_check(cls, check: Check) -> SerializedCheck:
|
107
|
-
import requests
|
108
|
-
from ..transports.responses import WSGIResponse
|
109
|
-
|
110
|
-
if check.response is not None:
|
111
|
-
request = Request.from_prepared_request(check.response.request)
|
112
|
-
elif check.request is not None:
|
113
|
-
# Response is not available, but it is not an error (only time-out behaves this way at the moment)
|
114
|
-
request = Request.from_prepared_request(check.request)
|
115
|
-
else:
|
116
|
-
raise InternalError("Can not find request data")
|
117
|
-
|
118
|
-
response: Response | None
|
119
|
-
if isinstance(check.response, requests.Response):
|
120
|
-
response = Response.from_requests(check.response)
|
121
|
-
elif isinstance(check.response, WSGIResponse):
|
122
|
-
response = Response.from_wsgi(check.response, check.elapsed)
|
123
|
-
else:
|
124
|
-
response = None
|
125
|
-
headers = _get_headers(request.headers)
|
126
|
-
history = get_serialized_history(check.example)
|
127
|
-
return cls(
|
128
|
-
name=check.name,
|
129
|
-
value=check.value,
|
130
|
-
example=SerializedCase.from_case(
|
131
|
-
check.example, headers, verify=response.verify if response is not None else True
|
132
|
-
),
|
133
|
-
message=check.message,
|
134
|
-
request=request,
|
135
|
-
response=response,
|
136
|
-
context=check.context,
|
137
|
-
history=history,
|
138
|
-
)
|
139
|
-
|
140
|
-
|
141
|
-
def _get_headers(headers: dict[str, Any] | CaseInsensitiveDict) -> dict[str, str]:
|
142
|
-
return {key: value[0] for key, value in headers.items() if key not in get_excluded_headers()}
|
143
|
-
|
144
|
-
|
145
|
-
@dataclass
|
146
|
-
class SerializedHistoryEntry:
|
147
|
-
case: SerializedCase
|
148
|
-
response: Response
|
149
|
-
|
150
|
-
|
151
|
-
def get_serialized_history(case: Case) -> list[SerializedHistoryEntry]:
|
152
|
-
import requests
|
153
|
-
|
154
|
-
history = []
|
155
|
-
while case.source is not None:
|
156
|
-
history_request = case.source.response.request
|
157
|
-
headers = _get_headers(history_request.headers)
|
158
|
-
if isinstance(case.source.response, requests.Response):
|
159
|
-
history_response = Response.from_requests(case.source.response)
|
160
|
-
verify = history_response.verify
|
161
|
-
else:
|
162
|
-
history_response = Response.from_wsgi(case.source.response, case.source.elapsed)
|
163
|
-
verify = True
|
164
|
-
entry = SerializedHistoryEntry(
|
165
|
-
case=SerializedCase.from_case(case.source.case, headers, verify=verify), response=history_response
|
166
|
-
)
|
167
|
-
history.append(entry)
|
168
|
-
case = case.source.case
|
169
|
-
return history
|
170
|
-
|
171
|
-
|
172
|
-
@dataclass
|
173
|
-
class SerializedError:
|
174
|
-
type: RuntimeErrorType
|
175
|
-
title: str | None
|
176
|
-
message: str | None
|
177
|
-
extras: list[str]
|
178
|
-
|
179
|
-
# Exception info
|
180
|
-
exception: str
|
181
|
-
exception_with_traceback: str
|
182
|
-
|
183
|
-
@classmethod
|
184
|
-
def with_exception(
|
185
|
-
cls,
|
186
|
-
type_: RuntimeErrorType,
|
187
|
-
title: str | None,
|
188
|
-
message: str | None,
|
189
|
-
extras: list[str],
|
190
|
-
exception: Exception,
|
191
|
-
) -> SerializedError:
|
192
|
-
return cls(
|
193
|
-
type=type_,
|
194
|
-
title=title,
|
195
|
-
message=message,
|
196
|
-
extras=extras,
|
197
|
-
exception=format_exception(exception),
|
198
|
-
exception_with_traceback=format_exception(exception, True),
|
199
|
-
)
|
200
|
-
|
201
|
-
@classmethod
|
202
|
-
def from_exception(cls, exception: Exception) -> SerializedError:
|
203
|
-
import requests
|
204
|
-
import hypothesis.errors
|
205
|
-
from hypothesis import HealthCheck
|
206
|
-
|
207
|
-
title = "Runtime Error"
|
208
|
-
message: str | None
|
209
|
-
if isinstance(exception, requests.RequestException):
|
210
|
-
if isinstance(exception, requests.exceptions.SSLError):
|
211
|
-
type_ = RuntimeErrorType.CONNECTION_SSL
|
212
|
-
elif isinstance(exception, requests.exceptions.ConnectionError):
|
213
|
-
type_ = RuntimeErrorType.CONNECTION_OTHER
|
214
|
-
else:
|
215
|
-
type_ = RuntimeErrorType.NETWORK_OTHER
|
216
|
-
message, extras = extract_requests_exception_details(exception)
|
217
|
-
title = "Network Error"
|
218
|
-
elif isinstance(exception, DeadlineExceeded):
|
219
|
-
type_ = RuntimeErrorType.HYPOTHESIS_DEADLINE_EXCEEDED
|
220
|
-
message = str(exception).strip()
|
221
|
-
extras = []
|
222
|
-
elif isinstance(exception, hypothesis.errors.InvalidArgument) and str(exception).startswith("Scalar "):
|
223
|
-
# Comes from `hypothesis-graphql`
|
224
|
-
scalar_name = _scalar_name_from_error(exception)
|
225
|
-
type_ = RuntimeErrorType.HYPOTHESIS_UNSUPPORTED_GRAPHQL_SCALAR
|
226
|
-
message = f"Scalar type '{scalar_name}' is not recognized"
|
227
|
-
extras = []
|
228
|
-
title = "Unknown GraphQL Scalar"
|
229
|
-
elif isinstance(exception, hypothesis.errors.InvalidArgument) and str(exception).endswith(
|
230
|
-
"larger than Hypothesis is designed to handle"
|
231
|
-
):
|
232
|
-
type_ = RuntimeErrorType.HYPOTHESIS_HEALTH_CHECK_LARGE_BASE_EXAMPLE
|
233
|
-
message = HEALTH_CHECK_MESSAGE_LARGE_BASE_EXAMPLE
|
234
|
-
extras = []
|
235
|
-
title = "Failed Health Check"
|
236
|
-
elif isinstance(exception, hypothesis.errors.Unsatisfiable):
|
237
|
-
type_ = RuntimeErrorType.HYPOTHESIS_UNSATISFIABLE
|
238
|
-
message = f"{exception}. Possible reasons:"
|
239
|
-
extras = [
|
240
|
-
"- Contradictory schema constraints, such as a minimum value exceeding the maximum.",
|
241
|
-
"- Excessive schema complexity, which hinders parameter generation.",
|
242
|
-
]
|
243
|
-
title = "Schema Error"
|
244
|
-
elif isinstance(exception, hypothesis.errors.FailedHealthCheck):
|
245
|
-
health_check = _health_check_from_error(exception)
|
246
|
-
if health_check is not None:
|
247
|
-
message, type_ = {
|
248
|
-
HealthCheck.data_too_large: (
|
249
|
-
HEALTH_CHECK_MESSAGE_DATA_TOO_LARGE,
|
250
|
-
RuntimeErrorType.HYPOTHESIS_HEALTH_CHECK_DATA_TOO_LARGE,
|
251
|
-
),
|
252
|
-
HealthCheck.filter_too_much: (
|
253
|
-
HEALTH_CHECK_MESSAGE_FILTER_TOO_MUCH,
|
254
|
-
RuntimeErrorType.HYPOTHESIS_HEALTH_CHECK_FILTER_TOO_MUCH,
|
255
|
-
),
|
256
|
-
HealthCheck.too_slow: (
|
257
|
-
HEALTH_CHECK_MESSAGE_TOO_SLOW,
|
258
|
-
RuntimeErrorType.HYPOTHESIS_HEALTH_CHECK_TOO_SLOW,
|
259
|
-
),
|
260
|
-
HealthCheck.large_base_example: (
|
261
|
-
HEALTH_CHECK_MESSAGE_LARGE_BASE_EXAMPLE,
|
262
|
-
RuntimeErrorType.HYPOTHESIS_HEALTH_CHECK_LARGE_BASE_EXAMPLE,
|
263
|
-
),
|
264
|
-
}[health_check]
|
265
|
-
else:
|
266
|
-
type_ = RuntimeErrorType.UNCLASSIFIED
|
267
|
-
message = str(exception)
|
268
|
-
extras = []
|
269
|
-
title = "Failed Health Check"
|
270
|
-
elif isinstance(exception, OperationSchemaError):
|
271
|
-
if isinstance(exception, BodyInGetRequestError):
|
272
|
-
type_ = RuntimeErrorType.SCHEMA_BODY_IN_GET_REQUEST
|
273
|
-
elif isinstance(exception, InvalidRegularExpression) and exception.is_valid_type:
|
274
|
-
type_ = RuntimeErrorType.SCHEMA_INVALID_REGULAR_EXPRESSION
|
275
|
-
else:
|
276
|
-
type_ = RuntimeErrorType.SCHEMA_GENERIC
|
277
|
-
message = exception.message
|
278
|
-
extras = []
|
279
|
-
title = "Schema Error"
|
280
|
-
elif isinstance(exception, SerializationError):
|
281
|
-
if isinstance(exception, UnboundPrefixError):
|
282
|
-
type_ = RuntimeErrorType.SERIALIZATION_UNBOUNDED_PREFIX
|
283
|
-
title = "XML serialization error"
|
284
|
-
else:
|
285
|
-
title = "Serialization not possible"
|
286
|
-
type_ = RuntimeErrorType.SERIALIZATION_NOT_POSSIBLE
|
287
|
-
message = str(exception)
|
288
|
-
extras = []
|
289
|
-
else:
|
290
|
-
type_ = RuntimeErrorType.UNCLASSIFIED
|
291
|
-
message = str(exception)
|
292
|
-
extras = []
|
293
|
-
return cls.with_exception(type_=type_, exception=exception, title=title, message=message, extras=extras)
|
294
|
-
|
295
|
-
|
296
|
-
HEALTH_CHECK_MESSAGE_DATA_TOO_LARGE = """There's a notable occurrence of examples surpassing the maximum size limit.
|
297
|
-
Typically, generating excessively large examples can compromise the quality of test outcomes.
|
298
|
-
|
299
|
-
Consider revising the schema to more accurately represent typical use cases
|
300
|
-
or applying constraints to reduce the data size."""
|
301
|
-
HEALTH_CHECK_MESSAGE_FILTER_TOO_MUCH = """A significant number of generated examples are being filtered out, indicating
|
302
|
-
that the schema's constraints may be too complex.
|
303
|
-
|
304
|
-
This level of filtration can slow down testing and affect the distribution
|
305
|
-
of generated data. Review and simplify the schema constraints where
|
306
|
-
possible to mitigate this issue."""
|
307
|
-
HEALTH_CHECK_MESSAGE_TOO_SLOW = "Data generation is extremely slow. Consider reducing the complexity of the schema."
|
308
|
-
HEALTH_CHECK_MESSAGE_LARGE_BASE_EXAMPLE = """A health check has identified that the smallest example derived from the schema
|
309
|
-
is excessively large, potentially leading to inefficient test execution.
|
310
|
-
|
311
|
-
This is commonly due to schemas that specify large-scale data structures by
|
312
|
-
default, such as an array with an extensive number of elements.
|
313
|
-
|
314
|
-
Consider revising the schema to more accurately represent typical use cases
|
315
|
-
or applying constraints to reduce the data size."""
|
316
|
-
|
317
|
-
|
318
|
-
def _health_check_from_error(exception: hypothesis.errors.FailedHealthCheck) -> hypothesis.HealthCheck | None:
|
319
|
-
from hypothesis import HealthCheck
|
320
|
-
|
321
|
-
match = re.search(r"add HealthCheck\.(\w+) to the suppress_health_check ", str(exception))
|
322
|
-
if match:
|
323
|
-
return {
|
324
|
-
"data_too_large": HealthCheck.data_too_large,
|
325
|
-
"filter_too_much": HealthCheck.filter_too_much,
|
326
|
-
"too_slow": HealthCheck.too_slow,
|
327
|
-
"large_base_example": HealthCheck.large_base_example,
|
328
|
-
}.get(match.group(1))
|
329
|
-
return None
|
330
|
-
|
331
|
-
|
332
|
-
def _scalar_name_from_error(exception: hypothesis.errors.InvalidArgument) -> str:
|
333
|
-
# This one is always available as the format is checked upfront
|
334
|
-
match = re.search(r"Scalar '(\w+)' is not supported", str(exception))
|
335
|
-
match = cast(re.Match, match)
|
336
|
-
return match.group(1)
|
337
|
-
|
338
|
-
|
339
|
-
@dataclass
|
340
|
-
class SerializedInteraction:
|
341
|
-
request: Request
|
342
|
-
response: Response
|
343
|
-
checks: list[SerializedCheck]
|
344
|
-
status: Status
|
345
|
-
recorded_at: str
|
346
|
-
|
347
|
-
@classmethod
|
348
|
-
def from_interaction(cls, interaction: Interaction) -> SerializedInteraction:
|
349
|
-
return cls(
|
350
|
-
request=interaction.request,
|
351
|
-
response=interaction.response,
|
352
|
-
checks=[SerializedCheck.from_check(check) for check in interaction.checks],
|
353
|
-
status=interaction.status,
|
354
|
-
recorded_at=interaction.recorded_at,
|
355
|
-
)
|
356
|
-
|
357
|
-
|
358
|
-
@dataclass
|
359
|
-
class SerializedTestResult:
|
360
|
-
method: str
|
361
|
-
path: str
|
362
|
-
verbose_name: str
|
363
|
-
has_failures: bool
|
364
|
-
has_errors: bool
|
365
|
-
has_logs: bool
|
366
|
-
is_errored: bool
|
367
|
-
is_flaky: bool
|
368
|
-
is_skipped: bool
|
369
|
-
skip_reason: str | None
|
370
|
-
seed: int | None
|
371
|
-
data_generation_method: list[str]
|
372
|
-
checks: list[SerializedCheck]
|
373
|
-
logs: list[str]
|
374
|
-
errors: list[SerializedError]
|
375
|
-
interactions: list[SerializedInteraction]
|
376
|
-
|
377
|
-
@classmethod
|
378
|
-
def from_test_result(cls, result: TestResult) -> SerializedTestResult:
|
379
|
-
formatter = logging.Formatter("[%(asctime)s] %(levelname)s in %(module)s: %(message)s")
|
380
|
-
return cls(
|
381
|
-
method=result.method,
|
382
|
-
path=result.path,
|
383
|
-
verbose_name=result.verbose_name,
|
384
|
-
has_failures=result.has_failures,
|
385
|
-
has_errors=result.has_errors,
|
386
|
-
has_logs=result.has_logs,
|
387
|
-
is_errored=result.is_errored,
|
388
|
-
is_flaky=result.is_flaky,
|
389
|
-
is_skipped=result.is_skipped,
|
390
|
-
skip_reason=result.skip_reason,
|
391
|
-
seed=result.seed,
|
392
|
-
data_generation_method=[m.as_short_name() for m in result.data_generation_method],
|
393
|
-
checks=[SerializedCheck.from_check(check) for check in result.checks],
|
394
|
-
logs=[formatter.format(record) for record in result.logs],
|
395
|
-
errors=[SerializedError.from_exception(error) for error in result.errors],
|
396
|
-
interactions=[SerializedInteraction.from_interaction(interaction) for interaction in result.interactions],
|
397
|
-
)
|
398
|
-
|
399
|
-
|
400
|
-
def deduplicate_failures(checks: list[SerializedCheck]) -> list[SerializedCheck]:
|
401
|
-
"""Return only unique checks that should be displayed in the output."""
|
402
|
-
seen: set[tuple[str | None, ...]] = set()
|
403
|
-
unique_checks = []
|
404
|
-
for check in reversed(checks):
|
405
|
-
# There are also could be checks that didn't fail
|
406
|
-
if check.value == Status.failure:
|
407
|
-
key = make_unique_by_key(check.name, check.message, check.context)
|
408
|
-
if key not in seen:
|
409
|
-
unique_checks.append(check)
|
410
|
-
seen.add(key)
|
411
|
-
return unique_checks
|
schemathesis/sanitization.py
DELETED
@@ -1,248 +0,0 @@
|
|
1
|
-
from __future__ import annotations
|
2
|
-
import threading
|
3
|
-
from collections.abc import MutableMapping, MutableSequence
|
4
|
-
from dataclasses import dataclass, replace
|
5
|
-
from typing import TYPE_CHECKING, Any, cast
|
6
|
-
from urllib.parse import parse_qs, urlencode, urlsplit, urlunsplit
|
7
|
-
|
8
|
-
from .constants import NOT_SET
|
9
|
-
|
10
|
-
if TYPE_CHECKING:
|
11
|
-
from requests import PreparedRequest
|
12
|
-
from .models import Case, CaseSource, Request
|
13
|
-
from .runner.serialization import SerializedCase, SerializedCheck, SerializedInteraction
|
14
|
-
from .transports.responses import GenericResponse
|
15
|
-
|
16
|
-
# Exact keys to sanitize
|
17
|
-
DEFAULT_KEYS_TO_SANITIZE = frozenset(
|
18
|
-
(
|
19
|
-
"phpsessid",
|
20
|
-
"xsrf-token",
|
21
|
-
"_csrf",
|
22
|
-
"_csrf_token",
|
23
|
-
"_session",
|
24
|
-
"_xsrf",
|
25
|
-
"aiohttp_session",
|
26
|
-
"api_key",
|
27
|
-
"api-key",
|
28
|
-
"apikey",
|
29
|
-
"auth",
|
30
|
-
"authorization",
|
31
|
-
"connect.sid",
|
32
|
-
"cookie",
|
33
|
-
"credentials",
|
34
|
-
"csrf",
|
35
|
-
"csrf_token",
|
36
|
-
"csrf-token",
|
37
|
-
"csrftoken",
|
38
|
-
"ip_address",
|
39
|
-
"mysql_pwd",
|
40
|
-
"passwd",
|
41
|
-
"password",
|
42
|
-
"private_key",
|
43
|
-
"private-key",
|
44
|
-
"privatekey",
|
45
|
-
"remote_addr",
|
46
|
-
"remote-addr",
|
47
|
-
"secret",
|
48
|
-
"session",
|
49
|
-
"sessionid",
|
50
|
-
"set_cookie",
|
51
|
-
"set-cookie",
|
52
|
-
"token",
|
53
|
-
"x_api_key",
|
54
|
-
"x-api-key",
|
55
|
-
"x_csrftoken",
|
56
|
-
"x-csrftoken",
|
57
|
-
"x_forwarded_for",
|
58
|
-
"x-forwarded-for",
|
59
|
-
"x_real_ip",
|
60
|
-
"x-real-ip",
|
61
|
-
)
|
62
|
-
)
|
63
|
-
|
64
|
-
# Markers indicating potentially sensitive keys
|
65
|
-
DEFAULT_SENSITIVE_MARKERS = frozenset(
|
66
|
-
(
|
67
|
-
"token",
|
68
|
-
"key",
|
69
|
-
"secret",
|
70
|
-
"password",
|
71
|
-
"auth",
|
72
|
-
"session",
|
73
|
-
"passwd",
|
74
|
-
"credential",
|
75
|
-
)
|
76
|
-
)
|
77
|
-
|
78
|
-
DEFAULT_REPLACEMENT = "[Filtered]"
|
79
|
-
|
80
|
-
|
81
|
-
@dataclass
|
82
|
-
class Config:
|
83
|
-
"""Configuration class for sanitizing sensitive data.
|
84
|
-
|
85
|
-
:param FrozenSet[str] keys_to_sanitize: The exact keys to sanitize (case-insensitive).
|
86
|
-
:param FrozenSet[str] sensitive_markers: Markers indicating potentially sensitive keys (case-insensitive).
|
87
|
-
:param str replacement: The replacement string for sanitized values.
|
88
|
-
"""
|
89
|
-
|
90
|
-
keys_to_sanitize: frozenset[str] = DEFAULT_KEYS_TO_SANITIZE
|
91
|
-
sensitive_markers: frozenset[str] = DEFAULT_SENSITIVE_MARKERS
|
92
|
-
replacement: str = DEFAULT_REPLACEMENT
|
93
|
-
|
94
|
-
def with_keys_to_sanitize(self, *keys: str) -> Config:
|
95
|
-
"""Create a new configuration with additional keys to sanitize."""
|
96
|
-
new_keys_to_sanitize = self.keys_to_sanitize.union([key.lower() for key in keys])
|
97
|
-
return replace(self, keys_to_sanitize=frozenset(new_keys_to_sanitize))
|
98
|
-
|
99
|
-
def without_keys_to_sanitize(self, *keys: str) -> Config:
|
100
|
-
"""Create a new configuration without certain keys to sanitize."""
|
101
|
-
new_keys_to_sanitize = self.keys_to_sanitize.difference([key.lower() for key in keys])
|
102
|
-
return replace(self, keys_to_sanitize=frozenset(new_keys_to_sanitize))
|
103
|
-
|
104
|
-
def with_sensitive_markers(self, *markers: str) -> Config:
|
105
|
-
"""Create a new configuration with additional sensitive markers."""
|
106
|
-
new_sensitive_markers = self.sensitive_markers.union([key.lower() for key in markers])
|
107
|
-
return replace(self, sensitive_markers=frozenset(new_sensitive_markers))
|
108
|
-
|
109
|
-
def without_sensitive_markers(self, *markers: str) -> Config:
|
110
|
-
"""Create a new configuration without certain sensitive markers."""
|
111
|
-
new_sensitive_markers = self.sensitive_markers.difference([key.lower() for key in markers])
|
112
|
-
return replace(self, sensitive_markers=frozenset(new_sensitive_markers))
|
113
|
-
|
114
|
-
|
115
|
-
_thread_local = threading.local()
|
116
|
-
|
117
|
-
|
118
|
-
def _get_default_sanitization_config() -> Config:
|
119
|
-
# Initialize the thread-local default sanitization config if not already set
|
120
|
-
if not hasattr(_thread_local, "default_sanitization_config"):
|
121
|
-
_thread_local.default_sanitization_config = Config()
|
122
|
-
return _thread_local.default_sanitization_config
|
123
|
-
|
124
|
-
|
125
|
-
def configure(config: Config) -> None:
|
126
|
-
_thread_local.default_sanitization_config = config
|
127
|
-
|
128
|
-
|
129
|
-
def sanitize_value(item: Any, *, config: Config | None = None) -> None:
|
130
|
-
"""Sanitize sensitive values within a given item.
|
131
|
-
|
132
|
-
This function is recursive and will sanitize sensitive data within nested
|
133
|
-
dictionaries and lists as well.
|
134
|
-
"""
|
135
|
-
config = config or _get_default_sanitization_config()
|
136
|
-
if isinstance(item, MutableMapping):
|
137
|
-
for key in list(item.keys()):
|
138
|
-
lower_key = key.lower()
|
139
|
-
if lower_key in config.keys_to_sanitize or any(marker in lower_key for marker in config.sensitive_markers):
|
140
|
-
if isinstance(item[key], list):
|
141
|
-
item[key] = [config.replacement]
|
142
|
-
else:
|
143
|
-
item[key] = config.replacement
|
144
|
-
for value in item.values():
|
145
|
-
if isinstance(value, (MutableMapping, MutableSequence)):
|
146
|
-
sanitize_value(value, config=config)
|
147
|
-
elif isinstance(item, MutableSequence):
|
148
|
-
for value in item:
|
149
|
-
if isinstance(value, (MutableMapping, MutableSequence)):
|
150
|
-
sanitize_value(value, config=config)
|
151
|
-
|
152
|
-
|
153
|
-
def sanitize_case(case: Case, *, config: Config | None = None) -> None:
|
154
|
-
"""Sanitize sensitive values within a given case."""
|
155
|
-
if case.path_parameters is not None:
|
156
|
-
sanitize_value(case.path_parameters, config=config)
|
157
|
-
if case.headers is not None:
|
158
|
-
sanitize_value(case.headers, config=config)
|
159
|
-
if case.cookies is not None:
|
160
|
-
sanitize_value(case.cookies, config=config)
|
161
|
-
if case.query is not None:
|
162
|
-
sanitize_value(case.query, config=config)
|
163
|
-
if case.body not in (None, NOT_SET):
|
164
|
-
sanitize_value(case.body, config=config)
|
165
|
-
if case.source is not None:
|
166
|
-
sanitize_history(case.source, config=config)
|
167
|
-
|
168
|
-
|
169
|
-
def sanitize_history(source: CaseSource, *, config: Config | None = None) -> None:
|
170
|
-
"""Recursively sanitize history of case/response pairs."""
|
171
|
-
current: CaseSource | None = source
|
172
|
-
while current is not None:
|
173
|
-
sanitize_case(current.case, config=config)
|
174
|
-
sanitize_response(current.response, config=config)
|
175
|
-
current = current.case.source
|
176
|
-
|
177
|
-
|
178
|
-
def sanitize_response(response: GenericResponse, *, config: Config | None = None) -> None:
|
179
|
-
# Sanitize headers
|
180
|
-
sanitize_value(response.headers, config=config)
|
181
|
-
|
182
|
-
|
183
|
-
def sanitize_request(request: PreparedRequest | Request, *, config: Config | None = None) -> None:
|
184
|
-
from requests import PreparedRequest
|
185
|
-
|
186
|
-
if isinstance(request, PreparedRequest) and request.url:
|
187
|
-
request.url = sanitize_url(request.url, config=config)
|
188
|
-
else:
|
189
|
-
request = cast("Request", request)
|
190
|
-
request.uri = sanitize_url(request.uri, config=config)
|
191
|
-
# Sanitize headers
|
192
|
-
sanitize_value(request.headers, config=config)
|
193
|
-
|
194
|
-
|
195
|
-
def sanitize_output(case: Case, response: GenericResponse | None = None, *, config: Config | None = None) -> None:
|
196
|
-
sanitize_case(case, config=config)
|
197
|
-
if response is not None:
|
198
|
-
sanitize_response(response, config=config)
|
199
|
-
sanitize_request(response.request, config=config)
|
200
|
-
|
201
|
-
|
202
|
-
def sanitize_url(url: str, *, config: Config | None = None) -> str:
|
203
|
-
"""Sanitize sensitive parts of a given URL.
|
204
|
-
|
205
|
-
This function will sanitize the authority and query parameters in the URL.
|
206
|
-
"""
|
207
|
-
config = config or _get_default_sanitization_config()
|
208
|
-
parsed = urlsplit(url)
|
209
|
-
|
210
|
-
# Sanitize authority
|
211
|
-
netloc_parts = parsed.netloc.split("@")
|
212
|
-
if len(netloc_parts) > 1:
|
213
|
-
netloc = f"{config.replacement}@{netloc_parts[-1]}"
|
214
|
-
else:
|
215
|
-
netloc = parsed.netloc
|
216
|
-
|
217
|
-
# Sanitize query parameters
|
218
|
-
query = parse_qs(parsed.query, keep_blank_values=True)
|
219
|
-
sanitize_value(query, config=config)
|
220
|
-
sanitized_query = urlencode(query, doseq=True)
|
221
|
-
|
222
|
-
# Reconstruct the URL
|
223
|
-
sanitized_url_parts = parsed._replace(netloc=netloc, query=sanitized_query)
|
224
|
-
return urlunsplit(sanitized_url_parts)
|
225
|
-
|
226
|
-
|
227
|
-
def sanitize_serialized_check(check: SerializedCheck, *, config: Config | None = None) -> None:
|
228
|
-
sanitize_request(check.request, config=config)
|
229
|
-
response = check.response
|
230
|
-
if response:
|
231
|
-
sanitize_value(response.headers, config=config)
|
232
|
-
sanitize_serialized_case(check.example, config=config)
|
233
|
-
for entry in check.history:
|
234
|
-
sanitize_serialized_case(entry.case, config=config)
|
235
|
-
sanitize_value(entry.response.headers, config=config)
|
236
|
-
|
237
|
-
|
238
|
-
def sanitize_serialized_case(case: SerializedCase, *, config: Config | None = None) -> None:
|
239
|
-
for value in (case.path_parameters, case.headers, case.cookies, case.query, case.extra_headers):
|
240
|
-
if value is not None:
|
241
|
-
sanitize_value(value, config=config)
|
242
|
-
|
243
|
-
|
244
|
-
def sanitize_serialized_interaction(interaction: SerializedInteraction, *, config: Config | None = None) -> None:
|
245
|
-
sanitize_request(interaction.request, config=config)
|
246
|
-
sanitize_value(interaction.response.headers, config=config)
|
247
|
-
for check in interaction.checks:
|
248
|
-
sanitize_serialized_check(check, config=config)
|