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 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
- data: Auth = self.provider.get(case, context) # type: ignore[assignment]
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
- return self.provider.get(case, context)
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, F403
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.description == "Missing `Authorization` at header"
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:
@@ -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': {repr(error.instance)}\n\n"
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")'
@@ -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
 
@@ -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)