schemathesis 4.3.15__py3-none-any.whl → 4.3.17__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.
Potentially problematic release.
This version of schemathesis might be problematic. Click here for more details.
- schemathesis/auths.py +24 -3
- schemathesis/checks.py +1 -1
- schemathesis/cli/commands/run/handlers/cassettes.py +1 -2
- schemathesis/cli/commands/run/handlers/output.py +5 -2
- schemathesis/config/_error.py +1 -1
- schemathesis/core/errors.py +30 -0
- schemathesis/engine/errors.py +12 -0
- schemathesis/engine/phases/unit/__init__.py +2 -2
- schemathesis/engine/phases/unit/_executor.py +4 -0
- schemathesis/generation/coverage.py +143 -50
- schemathesis/generation/hypothesis/builder.py +28 -7
- schemathesis/generation/meta.py +77 -2
- schemathesis/pytest/lazy.py +58 -12
- schemathesis/pytest/plugin.py +2 -2
- schemathesis/specs/openapi/_hypothesis.py +18 -98
- schemathesis/specs/openapi/adapter/parameters.py +181 -11
- schemathesis/specs/openapi/checks.py +5 -7
- schemathesis/specs/openapi/converter.py +1 -14
- schemathesis/specs/openapi/examples.py +4 -4
- schemathesis/specs/openapi/references.py +31 -1
- schemathesis/specs/openapi/schemas.py +5 -4
- schemathesis/transport/prepare.py +4 -3
- {schemathesis-4.3.15.dist-info → schemathesis-4.3.17.dist-info}/METADATA +6 -5
- {schemathesis-4.3.15.dist-info → schemathesis-4.3.17.dist-info}/RECORD +27 -27
- {schemathesis-4.3.15.dist-info → schemathesis-4.3.17.dist-info}/WHEEL +0 -0
- {schemathesis-4.3.15.dist-info → schemathesis-4.3.17.dist-info}/entry_points.txt +0 -0
- {schemathesis-4.3.15.dist-info → schemathesis-4.3.17.dist-info}/licenses/LICENSE +0 -0
schemathesis/auths.py
CHANGED
|
@@ -17,7 +17,7 @@ from typing import (
|
|
|
17
17
|
runtime_checkable,
|
|
18
18
|
)
|
|
19
19
|
|
|
20
|
-
from schemathesis.core.errors import IncorrectUsage
|
|
20
|
+
from schemathesis.core.errors import AuthenticationError, IncorrectUsage
|
|
21
21
|
from schemathesis.core.marks import Mark
|
|
22
22
|
from schemathesis.filters import FilterSet, FilterValue, MatcherFunc, attach_filter_chain
|
|
23
23
|
from schemathesis.generation.case import Case
|
|
@@ -128,6 +128,7 @@ class CachingAuthProvider(Generic[Auth]):
|
|
|
128
128
|
|
|
129
129
|
def get(self, case: Case, context: AuthContext) -> Auth | None:
|
|
130
130
|
"""Get cached auth value."""
|
|
131
|
+
__tracebackhide__ = True
|
|
131
132
|
cache_entry = self._get_cache_entry(case, context)
|
|
132
133
|
if cache_entry is None or self.timer() >= cache_entry.expires:
|
|
133
134
|
with self._refresh_lock:
|
|
@@ -136,7 +137,11 @@ class CachingAuthProvider(Generic[Auth]):
|
|
|
136
137
|
# Another thread updated the cache
|
|
137
138
|
return cache_entry.data
|
|
138
139
|
# We know that optional auth is possible only inside a higher-level wrapper
|
|
139
|
-
|
|
140
|
+
try:
|
|
141
|
+
data: Auth = self.provider.get(case, context) # type: ignore[assignment]
|
|
142
|
+
except Exception as exc:
|
|
143
|
+
provider_name = self.provider.__class__.__name__
|
|
144
|
+
raise AuthenticationError(provider_name, "get", str(exc)) from exc
|
|
140
145
|
self._set_cache_entry(data, case, context)
|
|
141
146
|
return data
|
|
142
147
|
return cache_entry.data
|
|
@@ -270,11 +275,25 @@ class SelectiveAuthProvider(Generic[Auth]):
|
|
|
270
275
|
filter_set: FilterSet
|
|
271
276
|
|
|
272
277
|
def get(self, case: Case, context: AuthContext) -> Auth | None:
|
|
278
|
+
__tracebackhide__ = True
|
|
273
279
|
if self.filter_set.match(context):
|
|
274
|
-
|
|
280
|
+
try:
|
|
281
|
+
return self.provider.get(case, context)
|
|
282
|
+
except AuthenticationError:
|
|
283
|
+
# Already wrapped, re-raise as-is
|
|
284
|
+
raise
|
|
285
|
+
except Exception as exc:
|
|
286
|
+
# Need to unwrap to get the actual provider class name
|
|
287
|
+
provider = self.provider
|
|
288
|
+
# Unwrap caching providers
|
|
289
|
+
while isinstance(provider, (CachingAuthProvider, KeyedCachingAuthProvider)):
|
|
290
|
+
provider = provider.provider
|
|
291
|
+
provider_name = provider.__class__.__name__
|
|
292
|
+
raise AuthenticationError(provider_name, "get", str(exc)) from exc
|
|
275
293
|
return None
|
|
276
294
|
|
|
277
295
|
def set(self, case: Case, data: Auth, context: AuthContext) -> None:
|
|
296
|
+
__tracebackhide__ = True
|
|
278
297
|
self.provider.set(case, data, context)
|
|
279
298
|
|
|
280
299
|
|
|
@@ -418,6 +437,7 @@ class AuthStorage(Generic[Auth]):
|
|
|
418
437
|
|
|
419
438
|
def set(self, case: Case, context: AuthContext) -> None:
|
|
420
439
|
"""Set authentication data on a generated test case."""
|
|
440
|
+
__tracebackhide__ = True
|
|
421
441
|
if not self.is_defined:
|
|
422
442
|
raise IncorrectUsage("No auth provider is defined.")
|
|
423
443
|
for provider in self.providers:
|
|
@@ -433,6 +453,7 @@ def set_on_case(case: Case, context: AuthContext, auth_storage: AuthStorage | No
|
|
|
433
453
|
|
|
434
454
|
If there is no auth defined, then this function is no-op.
|
|
435
455
|
"""
|
|
456
|
+
__tracebackhide__ = True
|
|
436
457
|
if auth_storage is not None:
|
|
437
458
|
auth_storage.set(case, context)
|
|
438
459
|
elif case.operation.schema.auth.is_defined:
|
schemathesis/checks.py
CHANGED
|
@@ -93,7 +93,7 @@ CHECKS = Registry[CheckFunction]()
|
|
|
93
93
|
|
|
94
94
|
def load_all_checks() -> None:
|
|
95
95
|
# NOTE: Trigger registering all Open API checks
|
|
96
|
-
from schemathesis.specs.openapi.checks import status_code_conformance # noqa: F401
|
|
96
|
+
from schemathesis.specs.openapi.checks import status_code_conformance # noqa: F401
|
|
97
97
|
|
|
98
98
|
|
|
99
99
|
def check(func: CheckFunction) -> CheckFunction:
|
|
@@ -123,8 +123,7 @@ def vcr_writer(output: TextOutput, config: ProjectConfig, queue: Queue) -> None:
|
|
|
123
123
|
current_id = 1
|
|
124
124
|
|
|
125
125
|
def write_header_values(stream: IO, values: list[str]) -> None:
|
|
126
|
-
for v in values
|
|
127
|
-
stream.write(f" - {json.dumps(v)}\n")
|
|
126
|
+
stream.writelines(f" - {json.dumps(v)}\n" for v in values)
|
|
128
127
|
|
|
129
128
|
if config.output.sanitization.enabled:
|
|
130
129
|
sanitization_keys = config.output.sanitization.keys_to_sanitize
|
|
@@ -20,13 +20,14 @@ from schemathesis.config import ProjectConfig, ReportFormat, SchemathesisWarning
|
|
|
20
20
|
from schemathesis.core.errors import LoaderError, LoaderErrorKind, format_exception, split_traceback
|
|
21
21
|
from schemathesis.core.failures import MessageBlock, Severity, format_failures
|
|
22
22
|
from schemathesis.core.output import prepare_response_payload
|
|
23
|
+
from schemathesis.core.parameters import ParameterLocation
|
|
23
24
|
from schemathesis.core.result import Ok
|
|
24
25
|
from schemathesis.core.version import SCHEMATHESIS_VERSION
|
|
25
26
|
from schemathesis.engine import Status, events
|
|
26
27
|
from schemathesis.engine.phases import PhaseName, PhaseSkipReason
|
|
27
28
|
from schemathesis.engine.phases.probes import ProbeOutcome
|
|
28
29
|
from schemathesis.engine.recorder import Interaction, ScenarioRecorder
|
|
29
|
-
from schemathesis.generation.meta import CoveragePhaseData
|
|
30
|
+
from schemathesis.generation.meta import CoveragePhaseData, CoverageScenario
|
|
30
31
|
from schemathesis.generation.modes import GenerationMode
|
|
31
32
|
from schemathesis.schemas import ApiStatistic
|
|
32
33
|
|
|
@@ -1079,7 +1080,9 @@ class OutputHandler(EventHandler):
|
|
|
1079
1080
|
return bool(
|
|
1080
1081
|
case.meta
|
|
1081
1082
|
and isinstance(case.meta.phase.data, CoveragePhaseData)
|
|
1082
|
-
and case.meta.phase.data.
|
|
1083
|
+
and case.meta.phase.data.scenario == CoverageScenario.MISSING_PARAMETER
|
|
1084
|
+
and case.meta.phase.data.parameter == "Authorization"
|
|
1085
|
+
and case.meta.phase.data.parameter_location == ParameterLocation.HEADER
|
|
1083
1086
|
)
|
|
1084
1087
|
|
|
1085
1088
|
if SchemathesisWarning.MISSING_AUTH in warnings:
|
schemathesis/config/_error.py
CHANGED
|
@@ -142,7 +142,7 @@ def _format_anyof_error(error: ValidationError) -> str:
|
|
|
142
142
|
)
|
|
143
143
|
elif list(error.schema_path) == ["properties", "workers", "anyOf"]:
|
|
144
144
|
return (
|
|
145
|
-
f"Invalid value for 'workers': {
|
|
145
|
+
f"Invalid value for 'workers': {error.instance!r}\n\n"
|
|
146
146
|
f"Expected either:\n"
|
|
147
147
|
f" - A positive integer (e.g., workers = 4)\n"
|
|
148
148
|
f' - The string "auto" for automatic detection (workers = "auto")'
|
schemathesis/core/errors.py
CHANGED
|
@@ -161,6 +161,8 @@ class InvalidSchema(SchemathesisError):
|
|
|
161
161
|
message += "\n File reference could not be resolved. Check that the file exists."
|
|
162
162
|
elif reference.startswith(("#/components", "#/definitions")):
|
|
163
163
|
message += "\n Component does not exist in the schema."
|
|
164
|
+
elif isinstance(error.__cause__, RemoteDocumentError):
|
|
165
|
+
message += f"\n {error.__cause__}"
|
|
164
166
|
return cls(message, path=path, method=method)
|
|
165
167
|
|
|
166
168
|
def as_failing_test_function(self) -> Callable:
|
|
@@ -176,6 +178,13 @@ class InvalidSchema(SchemathesisError):
|
|
|
176
178
|
return actual_test
|
|
177
179
|
|
|
178
180
|
|
|
181
|
+
class RemoteDocumentError(SchemathesisError):
|
|
182
|
+
"""Remote reference resolution failed.
|
|
183
|
+
|
|
184
|
+
This exception carries more context than the default one in `jsonschema`.
|
|
185
|
+
"""
|
|
186
|
+
|
|
187
|
+
|
|
179
188
|
class HookError(SchemathesisError):
|
|
180
189
|
"""Happens during hooks loading."""
|
|
181
190
|
|
|
@@ -316,6 +325,27 @@ class IncorrectUsage(SchemathesisError):
|
|
|
316
325
|
"""Indicates incorrect usage of Schemathesis' public API."""
|
|
317
326
|
|
|
318
327
|
|
|
328
|
+
class AuthenticationError(SchemathesisError):
|
|
329
|
+
"""Error during authentication provider execution.
|
|
330
|
+
|
|
331
|
+
This error wraps exceptions that occur when obtaining or setting
|
|
332
|
+
authentication data via custom auth providers.
|
|
333
|
+
"""
|
|
334
|
+
|
|
335
|
+
def __init__(self, provider_name: str, method: str, message: str) -> None:
|
|
336
|
+
self.provider_name = provider_name
|
|
337
|
+
self.method = method
|
|
338
|
+
self.message = message
|
|
339
|
+
super().__init__(
|
|
340
|
+
f"Error in '{provider_name}.{method}()': {message}\n\n"
|
|
341
|
+
f"Common causes:\n"
|
|
342
|
+
f" - Auth endpoint returned an error response\n"
|
|
343
|
+
f" - Response format doesn't match expectations (text vs JSON)\n"
|
|
344
|
+
f" - Network or connection issues\n"
|
|
345
|
+
f" - Logic error in the authentication provider implementation"
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
|
|
319
349
|
class NoLinksFound(IncorrectUsage):
|
|
320
350
|
"""Raised when no valid links are available for stateful testing."""
|
|
321
351
|
|
schemathesis/engine/errors.py
CHANGED
|
@@ -16,6 +16,7 @@ from hypothesis import HealthCheck
|
|
|
16
16
|
|
|
17
17
|
from schemathesis import errors
|
|
18
18
|
from schemathesis.core.errors import (
|
|
19
|
+
AuthenticationError,
|
|
19
20
|
InfiniteRecursiveReference,
|
|
20
21
|
InvalidTransition,
|
|
21
22
|
SerializationNotPossible,
|
|
@@ -105,6 +106,7 @@ class EngineErrorInfo:
|
|
|
105
106
|
RuntimeErrorKind.HYPOTHESIS_UNSUPPORTED_GRAPHQL_SCALAR: "Unknown GraphQL Scalar",
|
|
106
107
|
RuntimeErrorKind.SERIALIZATION_UNBOUNDED_PREFIX: "XML serialization error",
|
|
107
108
|
RuntimeErrorKind.SERIALIZATION_NOT_POSSIBLE: "Serialization not possible",
|
|
109
|
+
RuntimeErrorKind.AUTHENTICATION_ERROR: "Authentication Error",
|
|
108
110
|
}.get(self._kind, "Runtime Error")
|
|
109
111
|
|
|
110
112
|
@property
|
|
@@ -165,6 +167,9 @@ class EngineErrorInfo:
|
|
|
165
167
|
|
|
166
168
|
@cached_property
|
|
167
169
|
def traceback(self) -> str:
|
|
170
|
+
# For AuthenticationError, show only the original exception's traceback
|
|
171
|
+
if isinstance(self._error, AuthenticationError) and self._error.__cause__ is not None:
|
|
172
|
+
return format_exception(self._error.__cause__, with_traceback=True)
|
|
168
173
|
return format_exception(self._error, with_traceback=True)
|
|
169
174
|
|
|
170
175
|
def format(self, *, bold: Callable[[str], str] = str, indent: str = " ") -> str:
|
|
@@ -254,6 +259,9 @@ class RuntimeErrorKind(str, enum.Enum):
|
|
|
254
259
|
CONNECTION_OTHER = "connection_other"
|
|
255
260
|
NETWORK_OTHER = "network_other"
|
|
256
261
|
|
|
262
|
+
# Authentication issues
|
|
263
|
+
AUTHENTICATION_ERROR = "authentication_error"
|
|
264
|
+
|
|
257
265
|
# Hypothesis issues
|
|
258
266
|
HYPOTHESIS_UNSATISFIABLE = "hypothesis_unsatisfiable"
|
|
259
267
|
HYPOTHESIS_UNSUPPORTED_GRAPHQL_SCALAR = "hypothesis_unsupported_graphql_scalar"
|
|
@@ -281,6 +289,10 @@ def _classify(*, error: Exception) -> RuntimeErrorKind:
|
|
|
281
289
|
import requests
|
|
282
290
|
from hypothesis import HealthCheck
|
|
283
291
|
|
|
292
|
+
# Authentication errors
|
|
293
|
+
if isinstance(error, AuthenticationError):
|
|
294
|
+
return RuntimeErrorKind.AUTHENTICATION_ERROR
|
|
295
|
+
|
|
284
296
|
# Network-related errors
|
|
285
297
|
if isinstance(error, requests.RequestException):
|
|
286
298
|
if isinstance(error, requests.exceptions.SSLError):
|
|
@@ -11,7 +11,7 @@ import warnings
|
|
|
11
11
|
from queue import Queue
|
|
12
12
|
from typing import TYPE_CHECKING, Any
|
|
13
13
|
|
|
14
|
-
from schemathesis.core.errors import InvalidSchema
|
|
14
|
+
from schemathesis.core.errors import AuthenticationError, InvalidSchema
|
|
15
15
|
from schemathesis.core.result import Ok
|
|
16
16
|
from schemathesis.engine import Status, events
|
|
17
17
|
from schemathesis.engine.phases import PhaseName, PhaseSkipReason
|
|
@@ -182,7 +182,7 @@ def worker_task(
|
|
|
182
182
|
as_strategy_kwargs=as_strategy_kwargs,
|
|
183
183
|
),
|
|
184
184
|
)
|
|
185
|
-
except (InvalidSchema, InvalidArgument) as exc:
|
|
185
|
+
except (InvalidSchema, InvalidArgument, AuthenticationError) as exc:
|
|
186
186
|
on_error(exc, method=operation.method, path=operation.path)
|
|
187
187
|
continue
|
|
188
188
|
|
|
@@ -19,6 +19,7 @@ from schemathesis.core.compat import BaseExceptionGroup
|
|
|
19
19
|
from schemathesis.core.control import SkipTest
|
|
20
20
|
from schemathesis.core.errors import (
|
|
21
21
|
SERIALIZERS_SUGGESTION_MESSAGE,
|
|
22
|
+
AuthenticationError,
|
|
22
23
|
InternalError,
|
|
23
24
|
InvalidHeadersExample,
|
|
24
25
|
InvalidRegexPattern,
|
|
@@ -173,6 +174,9 @@ def run_test(
|
|
|
173
174
|
# We need more clear error message here
|
|
174
175
|
status = Status.ERROR
|
|
175
176
|
yield non_fatal_error(build_unsatisfiable_error(operation, with_tip=False))
|
|
177
|
+
except AuthenticationError as exc:
|
|
178
|
+
status = Status.ERROR
|
|
179
|
+
yield non_fatal_error(exc)
|
|
176
180
|
except KeyboardInterrupt:
|
|
177
181
|
yield scenario_finished(Status.INTERRUPTED)
|
|
178
182
|
yield events.Interrupted(phase=phase)
|