schemathesis 3.35.5__py3-none-any.whl → 3.36.1__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.
@@ -21,6 +21,7 @@ from hypothesis.errors import HypothesisException, InvalidArgument
21
21
  from hypothesis_jsonschema._canonicalise import HypothesisRefResolutionError
22
22
  from jsonschema.exceptions import SchemaError as JsonSchemaError
23
23
  from jsonschema.exceptions import ValidationError
24
+ from requests.structures import CaseInsensitiveDict
24
25
  from urllib3.exceptions import InsecureRequestWarning
25
26
 
26
27
  from ... import experimental, failures, hooks
@@ -56,9 +57,10 @@ from ...exceptions import (
56
57
  )
57
58
  from ...generation import DataGenerationMethod, GenerationConfig
58
59
  from ...hooks import HookContext, get_all_by_name
60
+ from ...internal.checks import CheckContext
59
61
  from ...internal.datetime import current_datetime
60
62
  from ...internal.result import Err, Ok, Result
61
- from ...models import APIOperation, Case, Check, CheckFunction, Status, TestResult
63
+ from ...models import APIOperation, Case, Check, Status, TestResult
62
64
  from ...runner import events
63
65
  from ...service import extensions
64
66
  from ...service.models import AnalysisResult, AnalysisSuccess
@@ -75,13 +77,16 @@ from ..serialization import SerializedTestResult
75
77
  from .context import RunnerContext
76
78
 
77
79
  if TYPE_CHECKING:
78
- from ...types import RawAuth
79
- from ...schemas import BaseSchema
80
- from ..._override import CaseOverride
81
- from requests.auth import HTTPDigestAuth
82
80
  from types import TracebackType
81
+
82
+ from requests.auth import HTTPDigestAuth
83
+
84
+ from ..._override import CaseOverride
85
+ from ...internal.checks import CheckFunction
86
+ from ...schemas import BaseSchema
83
87
  from ...service.client import ServiceClient
84
88
  from ...transports.responses import GenericResponse, WSGIResponse
89
+ from ...types import RawAuth
85
90
 
86
91
 
87
92
  def _should_count_towards_stop(event: events.ExecutionEvent) -> bool:
@@ -95,7 +100,7 @@ class BaseRunner:
95
100
  max_response_time: int | None
96
101
  targets: Iterable[Target]
97
102
  hypothesis_settings: hypothesis.settings
98
- generation_config: GenerationConfig
103
+ generation_config: GenerationConfig | None
99
104
  probe_config: probes.ProbeConfig
100
105
  request_config: RequestConfig = field(default_factory=RequestConfig)
101
106
  override: CaseOverride | None = None
@@ -107,6 +112,7 @@ class BaseRunner:
107
112
  exit_first: bool = False
108
113
  max_failures: int | None = None
109
114
  started_at: str = field(default_factory=current_datetime)
115
+ unique_data: bool = False
110
116
  dry_run: bool = False
111
117
  stateful: Stateful | None = None
112
118
  stateful_recursion_limit: int = DEFAULT_STATEFUL_RECURSION_LIMIT
@@ -125,7 +131,7 @@ class BaseRunner:
125
131
  # If auth is explicitly provided, then the global provider is ignored
126
132
  if self.auth is not None:
127
133
  unregister_auth()
128
- ctx = RunnerContext(self.seed, stop_event)
134
+ ctx = RunnerContext(auth=self.auth, seed=self.seed, stop_event=stop_event, unique_data=self.unique_data)
129
135
  start_time = time.monotonic()
130
136
  initialized = None
131
137
  __probes = None
@@ -333,7 +339,7 @@ class BaseRunner:
333
339
  maker: Callable,
334
340
  test_func: Callable,
335
341
  settings: hypothesis.settings,
336
- generation_config: GenerationConfig,
342
+ generation_config: GenerationConfig | None,
337
343
  ctx: RunnerContext,
338
344
  recursion_level: int = 0,
339
345
  headers: dict[str, Any] | None = None,
@@ -561,9 +567,10 @@ def run_test(
561
567
  try:
562
568
  with catch_warnings(record=True) as warnings, capture_hypothesis_output() as hypothesis_output:
563
569
  test(
564
- checks,
565
- targets,
566
- result,
570
+ ctx=ctx,
571
+ checks=checks,
572
+ targets=targets,
573
+ result=result,
567
574
  errors=errors,
568
575
  headers=headers,
569
576
  data_generation_methods=data_generation_methods,
@@ -789,6 +796,7 @@ def deduplicate_errors(errors: list[Exception]) -> Generator[Exception, None, No
789
796
  def run_checks(
790
797
  *,
791
798
  case: Case,
799
+ ctx: CheckContext,
792
800
  checks: Iterable[CheckFunction],
793
801
  check_results: list[Check],
794
802
  result: TestResult,
@@ -811,7 +819,7 @@ def run_checks(
811
819
  check_name = check.__name__
812
820
  copied_case = case.partial_deepcopy()
813
821
  try:
814
- skip_check = check(response, copied_case)
822
+ skip_check = check(ctx, response, copied_case)
815
823
  if not skip_check:
816
824
  check_result = result.add_success(check_name, copied_case, response, elapsed_time)
817
825
  check_results.append(check_result)
@@ -897,7 +905,33 @@ def _force_data_generation_method(values: list[DataGenerationMethod], case: Case
897
905
  values[:] = [data_generation_method]
898
906
 
899
907
 
908
+ def cached_test_func(f: Callable) -> Callable:
909
+ def wrapped(*, ctx: RunnerContext, case: Case, **kwargs: Any) -> None:
910
+ if ctx.unique_data:
911
+ cached = ctx.get_cached_outcome(case)
912
+ if isinstance(cached, BaseException):
913
+ raise cached
914
+ elif cached is None:
915
+ return None
916
+ try:
917
+ f(ctx=ctx, case=case, **kwargs)
918
+ except BaseException as exc:
919
+ ctx.cache_outcome(case, exc)
920
+ raise
921
+ else:
922
+ ctx.cache_outcome(case, None)
923
+ else:
924
+ f(ctx=ctx, case=case, **kwargs)
925
+
926
+ wrapped.__name__ = f.__name__
927
+
928
+ return wrapped
929
+
930
+
931
+ @cached_test_func
900
932
  def network_test(
933
+ *,
934
+ ctx: RunnerContext,
901
935
  case: Case,
902
936
  checks: Iterable[CheckFunction],
903
937
  targets: Iterable[Target],
@@ -921,6 +955,7 @@ def network_test(
921
955
  headers["User-Agent"] = USER_AGENT
922
956
  if not dry_run:
923
957
  args = (
958
+ ctx,
924
959
  checks,
925
960
  targets,
926
961
  result,
@@ -939,6 +974,7 @@ def network_test(
939
974
 
940
975
  def _network_test(
941
976
  case: Case,
977
+ ctx: RunnerContext,
942
978
  checks: Iterable[CheckFunction],
943
979
  targets: Iterable[Target],
944
980
  result: TestResult,
@@ -980,9 +1016,12 @@ def _network_test(
980
1016
  context = TargetContext(case=case, response=response, response_time=response.elapsed.total_seconds())
981
1017
  run_targets(targets, context)
982
1018
  status = Status.success
1019
+
1020
+ check_ctx = CheckContext(auth=ctx.auth, headers=CaseInsensitiveDict(headers) if headers else None)
983
1021
  try:
984
1022
  run_checks(
985
1023
  case=case,
1024
+ ctx=check_ctx,
986
1025
  checks=checks,
987
1026
  check_results=check_results,
988
1027
  result=result,
@@ -1009,7 +1048,9 @@ def get_session(auth: HTTPDigestAuth | RawAuth | None = None) -> Generator[reque
1009
1048
  yield session
1010
1049
 
1011
1050
 
1051
+ @cached_test_func
1012
1052
  def wsgi_test(
1053
+ ctx: RunnerContext,
1013
1054
  case: Case,
1014
1055
  checks: Iterable[CheckFunction],
1015
1056
  targets: Iterable[Target],
@@ -1030,6 +1071,7 @@ def wsgi_test(
1030
1071
  headers = prepare_wsgi_headers(headers, auth, auth_type)
1031
1072
  if not dry_run:
1032
1073
  args = (
1074
+ ctx,
1033
1075
  checks,
1034
1076
  targets,
1035
1077
  result,
@@ -1046,6 +1088,7 @@ def wsgi_test(
1046
1088
 
1047
1089
  def _wsgi_test(
1048
1090
  case: Case,
1091
+ ctx: RunnerContext,
1049
1092
  checks: Iterable[CheckFunction],
1050
1093
  targets: Iterable[Target],
1051
1094
  result: TestResult,
@@ -1066,9 +1109,11 @@ def _wsgi_test(
1066
1109
  result.logs.extend(recorded.records)
1067
1110
  status = Status.success
1068
1111
  check_results: list[Check] = []
1112
+ check_ctx = CheckContext(auth=ctx.auth, headers=CaseInsensitiveDict(headers) if headers else None)
1069
1113
  try:
1070
1114
  run_checks(
1071
1115
  case=case,
1116
+ ctx=check_ctx,
1072
1117
  checks=checks,
1073
1118
  check_results=check_results,
1074
1119
  result=result,
@@ -1087,7 +1132,9 @@ def _wsgi_test(
1087
1132
  return response
1088
1133
 
1089
1134
 
1135
+ @cached_test_func
1090
1136
  def asgi_test(
1137
+ ctx: RunnerContext,
1091
1138
  case: Case,
1092
1139
  checks: Iterable[CheckFunction],
1093
1140
  targets: Iterable[Target],
@@ -1108,6 +1155,7 @@ def asgi_test(
1108
1155
 
1109
1156
  if not dry_run:
1110
1157
  args = (
1158
+ ctx,
1111
1159
  checks,
1112
1160
  targets,
1113
1161
  result,
@@ -1124,6 +1172,7 @@ def asgi_test(
1124
1172
 
1125
1173
  def _asgi_test(
1126
1174
  case: Case,
1175
+ ctx: RunnerContext,
1127
1176
  checks: Iterable[CheckFunction],
1128
1177
  targets: Iterable[Target],
1129
1178
  result: TestResult,
@@ -1140,9 +1189,11 @@ def _asgi_test(
1140
1189
  run_targets(targets, context)
1141
1190
  status = Status.success
1142
1191
  check_results: list[Check] = []
1192
+ check_ctx = CheckContext(auth=ctx.auth, headers=CaseInsensitiveDict(headers) if headers else None)
1143
1193
  try:
1144
1194
  run_checks(
1145
1195
  case=case,
1196
+ ctx=check_ctx,
1146
1197
  checks=checks,
1147
1198
  check_results=check_results,
1148
1199
  result=result,
@@ -8,8 +8,8 @@ from .. import events
8
8
  from .core import BaseRunner, asgi_test, get_session, network_test, wsgi_test
9
9
 
10
10
  if TYPE_CHECKING:
11
- from .context import RunnerContext
12
11
  from .. import events
12
+ from .context import RunnerContext
13
13
 
14
14
 
15
15
  @dataclass
@@ -13,7 +13,6 @@ from hypothesis.errors import HypothesisWarning
13
13
 
14
14
  from ..._hypothesis import create_test
15
15
  from ...internal.result import Ok
16
- from ...models import CheckFunction
17
16
  from ...stateful import Feedback, Stateful
18
17
  from ...transports.auth import get_requests_auth
19
18
  from ...utils import capture_hypothesis_output
@@ -21,13 +20,13 @@ from .. import events
21
20
  from .core import BaseRunner, asgi_test, get_session, handle_schema_error, network_test, run_test, wsgi_test
22
21
 
23
22
  if TYPE_CHECKING:
24
- from .context import RunnerContext
25
23
  import hypothesis
26
24
 
27
25
  from ...generation import DataGenerationMethod, GenerationConfig
28
- from ...models import CheckFunction
26
+ from ...internal.checks import CheckFunction
29
27
  from ...targets import Target
30
28
  from ...types import RawAuth
29
+ from .context import RunnerContext
31
30
 
32
31
 
33
32
  def _run_task(
@@ -235,7 +234,7 @@ class ThreadPoolRunner(BaseRunner):
235
234
  # It would be better to have a separate producer thread and communicate via threading events.
236
235
  # Though it is a bit more complex, so the current solution is suboptimal in terms of resources utilization,
237
236
  # but good enough and easy enough to implement.
238
- tasks_generator = iter(self.schema.get_all_operations())
237
+ tasks_generator = iter(self.schema.get_all_operations(generation_config=self.generation_config))
239
238
  generator_done = threading.Event()
240
239
  tasks_queue: Queue = Queue()
241
240
  # Add at least `workers_num` tasks first, so all workers are busy
schemathesis/schemas.py CHANGED
@@ -241,7 +241,7 @@ class BaseSchema(Mapping):
241
241
  raise NotImplementedError
242
242
 
243
243
  def get_all_operations(
244
- self, hooks: HookDispatcher | None = None
244
+ self, hooks: HookDispatcher | None = None, generation_config: GenerationConfig | None = None
245
245
  ) -> Generator[Result[APIOperation, OperationSchemaError], None, None]:
246
246
  raise NotImplementedError
247
247
 
@@ -276,7 +276,7 @@ class BaseSchema(Mapping):
276
276
  _given_kwargs: dict[str, GivenInput] | None = None,
277
277
  ) -> Generator[Result[tuple[APIOperation, Callable], OperationSchemaError], None, None]:
278
278
  """Generate all operations and Hypothesis tests for them."""
279
- for result in self.get_all_operations(hooks=hooks):
279
+ for result in self.get_all_operations(hooks=hooks, generation_config=generation_config):
280
280
  if isinstance(result, Ok):
281
281
  operation = result.ok()
282
282
  _as_strategy_kwargs: dict[str, Any] | None
@@ -139,12 +139,12 @@ def from_url(
139
139
  interval=WAIT_FOR_SCHEMA_INTERVAL,
140
140
  )
141
141
  def _load_schema(_uri: str, **_kwargs: Any) -> requests.Response:
142
- _kwargs.setdefault("timeout", DEFAULT_RESPONSE_TIMEOUT / 1000)
143
- return requests.post(_uri, **kwargs)
142
+ return requests.post(_uri, **_kwargs)
144
143
 
145
144
  else:
146
145
  _load_schema = requests.post
147
146
 
147
+ kwargs.setdefault("timeout", DEFAULT_RESPONSE_TIMEOUT / 1000)
148
148
  response = load_schema_from_url(lambda: _load_schema(url, **kwargs))
149
149
  raw_schema = extract_schema_from_response(response)
150
150
  return from_dict(
@@ -27,7 +27,7 @@ from requests.structures import CaseInsensitiveDict
27
27
 
28
28
  from ... import auths
29
29
  from ...checks import not_a_server_error
30
- from ...constants import NOT_SET
30
+ from ...constants import NOT_SET, SCHEMATHESIS_TEST_CASE_HEADER
31
31
  from ...exceptions import OperationNotFound, OperationSchemaError
32
32
  from ...generation import DataGenerationMethod, GenerationConfig
33
33
  from ...hooks import (
@@ -38,7 +38,7 @@ from ...hooks import (
38
38
  should_skip_operation,
39
39
  )
40
40
  from ...internal.result import Ok, Result
41
- from ...models import APIOperation, Case, CheckFunction, OperationDefinition
41
+ from ...models import APIOperation, Case, OperationDefinition
42
42
  from ...schemas import APIOperationMap, BaseSchema
43
43
  from ...types import Body, Cookies, Headers, NotSet, PathParameters, Query
44
44
  from ..openapi.constants import LOCATION_TO_CONTAINER
@@ -49,6 +49,7 @@ if TYPE_CHECKING:
49
49
  from hypothesis.strategies import SearchStrategy
50
50
 
51
51
  from ...auths import AuthStorage
52
+ from ...internal.checks import CheckFunction
52
53
  from ...stateful import Stateful, StatefulTest
53
54
  from ...transports.responses import GenericResponse
54
55
 
@@ -61,6 +62,9 @@ class RootType(enum.Enum):
61
62
 
62
63
  @dataclass(repr=False)
63
64
  class GraphQLCase(Case):
65
+ def __hash__(self) -> int:
66
+ return hash(self.as_curl_command({SCHEMATHESIS_TEST_CASE_HEADER: "0"}))
67
+
64
68
  def _get_url(self, base_url: str | None) -> str:
65
69
  base_url = self._get_base_url(base_url)
66
70
  # Replace the path, in case if the user provided any path parameters via hooks
@@ -78,11 +82,12 @@ class GraphQLCase(Case):
78
82
  additional_checks: tuple[CheckFunction, ...] = (),
79
83
  excluded_checks: tuple[CheckFunction, ...] = (),
80
84
  code_sample_style: str | None = None,
85
+ headers: dict[str, Any] | None = None,
81
86
  ) -> None:
82
87
  checks = checks or (not_a_server_error,)
83
88
  checks += additional_checks
84
89
  checks = tuple(check for check in checks if check not in excluded_checks)
85
- return super().validate_response(response, checks, code_sample_style=code_sample_style)
90
+ return super().validate_response(response, checks, code_sample_style=code_sample_style, headers=headers)
86
91
 
87
92
 
88
93
  C = TypeVar("C", bound=Case)
@@ -186,8 +191,7 @@ class GraphQLSchema(BaseSchema):
186
191
  return 0
187
192
 
188
193
  def get_all_operations(
189
- self,
190
- hooks: HookDispatcher | None = None,
194
+ self, hooks: HookDispatcher | None = None, generation_config: GenerationConfig | None = None
191
195
  ) -> Generator[Result[APIOperation, OperationSchemaError], None, None]:
192
196
  schema = self.client_schema
193
197
  for root_type, operation_type in (
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from dataclasses import dataclass
4
+ import enum
4
5
  from http.cookies import SimpleCookie
5
6
  from typing import TYPE_CHECKING, Any, Dict, Generator, NoReturn, cast
6
7
  from urllib.parse import parse_qs, urlparse
@@ -25,11 +26,12 @@ from .utils import expand_status_code
25
26
  if TYPE_CHECKING:
26
27
  from requests import PreparedRequest
27
28
 
29
+ from ...internal.checks import CheckContext
28
30
  from ...models import APIOperation, Case
29
31
  from ...transports.responses import GenericResponse
30
32
 
31
33
 
32
- def status_code_conformance(response: GenericResponse, case: Case) -> bool | None:
34
+ def status_code_conformance(ctx: CheckContext, response: GenericResponse, case: Case) -> bool | None:
33
35
  from .schemas import BaseOpenAPISchema
34
36
 
35
37
  if not isinstance(case.operation.schema, BaseOpenAPISchema):
@@ -60,7 +62,7 @@ def _expand_responses(responses: dict[str | int, Any]) -> Generator[int, None, N
60
62
  yield from expand_status_code(code)
61
63
 
62
64
 
63
- def content_type_conformance(response: GenericResponse, case: Case) -> bool | None:
65
+ def content_type_conformance(ctx: CheckContext, response: GenericResponse, case: Case) -> bool | None:
64
66
  from .schemas import BaseOpenAPISchema
65
67
 
66
68
  if not isinstance(case.operation.schema, BaseOpenAPISchema):
@@ -115,7 +117,7 @@ def _reraise_malformed_media_type(case: Case, exc: ValueError, location: str, ac
115
117
  ) from exc
116
118
 
117
119
 
118
- def response_headers_conformance(response: GenericResponse, case: Case) -> bool | None:
120
+ def response_headers_conformance(ctx: CheckContext, response: GenericResponse, case: Case) -> bool | None:
119
121
  import jsonschema
120
122
 
121
123
  from .parameters import OpenAPI20Parameter, OpenAPI30Parameter
@@ -171,11 +173,11 @@ def response_headers_conformance(response: GenericResponse, case: Case) -> bool
171
173
  )
172
174
  except jsonschema.ValidationError as exc:
173
175
  exc_class = get_schema_validation_error(case.operation.verbose_name, exc)
174
- ctx = failures.ValidationErrorContext.from_exception(
176
+ error_ctx = failures.ValidationErrorContext.from_exception(
175
177
  exc, output_config=case.operation.schema.output_config
176
178
  )
177
179
  try:
178
- raise exc_class("Response header does not conform to the schema", context=ctx) from exc
180
+ raise exc_class("Response header does not conform to the schema", context=error_ctx) from exc
179
181
  except Exception as exc:
180
182
  errors.append(exc)
181
183
  return _maybe_raise_one_or_more(errors) # type: ignore[func-returns-value]
@@ -203,7 +205,7 @@ def _coerce_header_value(value: str, schema: dict[str, Any]) -> str | int | floa
203
205
  return value
204
206
 
205
207
 
206
- def response_schema_conformance(response: GenericResponse, case: Case) -> bool | None:
208
+ def response_schema_conformance(ctx: CheckContext, response: GenericResponse, case: Case) -> bool | None:
207
209
  from .schemas import BaseOpenAPISchema
208
210
 
209
211
  if not isinstance(case.operation.schema, BaseOpenAPISchema):
@@ -211,7 +213,7 @@ def response_schema_conformance(response: GenericResponse, case: Case) -> bool |
211
213
  return case.operation.validate_response(response)
212
214
 
213
215
 
214
- def negative_data_rejection(response: GenericResponse, case: Case) -> bool | None:
216
+ def negative_data_rejection(ctx: CheckContext, response: GenericResponse, case: Case) -> bool | None:
215
217
  from .schemas import BaseOpenAPISchema
216
218
 
217
219
  if not isinstance(case.operation.schema, BaseOpenAPISchema):
@@ -258,7 +260,7 @@ def has_only_additional_properties_in_non_body_parameters(case: Case) -> bool:
258
260
  return True
259
261
 
260
262
 
261
- def use_after_free(response: GenericResponse, original: Case) -> bool | None:
263
+ def use_after_free(ctx: CheckContext, response: GenericResponse, original: Case) -> bool | None:
262
264
  from ...transports.responses import get_reason
263
265
  from .schemas import BaseOpenAPISchema
264
266
 
@@ -298,7 +300,7 @@ def use_after_free(response: GenericResponse, original: Case) -> bool | None:
298
300
  return None
299
301
 
300
302
 
301
- def ensure_resource_availability(response: GenericResponse, original: Case) -> bool | None:
303
+ def ensure_resource_availability(ctx: CheckContext, response: GenericResponse, original: Case) -> bool | None:
302
304
  from ...transports.responses import get_reason
303
305
  from .schemas import BaseOpenAPISchema
304
306
 
@@ -332,7 +334,12 @@ def ensure_resource_availability(response: GenericResponse, original: Case) -> b
332
334
  return None
333
335
 
334
336
 
335
- def ignored_auth(response: GenericResponse, case: Case) -> bool | None:
337
+ class AuthKind(enum.Enum):
338
+ EXPLICIT = "explicit"
339
+ GENERATED = "generated"
340
+
341
+
342
+ def ignored_auth(ctx: CheckContext, response: GenericResponse, case: Case) -> bool | None:
336
343
  """Check if an operation declares authentication as a requirement but does not actually enforce it."""
337
344
  from .schemas import BaseOpenAPISchema
338
345
 
@@ -340,32 +347,49 @@ def ignored_auth(response: GenericResponse, case: Case) -> bool | None:
340
347
  return True
341
348
  security_parameters = _get_security_parameters(case.operation)
342
349
  # Authentication is required for this API operation and response is successful
343
- # Will it still be successful if there is no auth?
344
350
  if security_parameters and 200 <= response.status_code < 300:
345
- if _contains_auth(response.request, security_parameters):
346
- # If there is auth in the request, then drop it and retry the call
351
+ auth = _contains_auth(ctx, case, response.request, security_parameters)
352
+ if auth == AuthKind.EXPLICIT:
353
+ # Auth is explicitly set, it is expected to be valid
354
+ # Check if invalid auth will give an error
347
355
  _remove_auth_from_case(case, security_parameters)
348
356
  new_response = case.operation.schema.transport.send(case)
349
357
  if 200 <= new_response.status_code < 300:
350
- # Mutate the response object in place on the best effort basis
351
- if hasattr(response, "__attrs__"):
352
- for attribute in new_response.__attrs__:
353
- setattr(response, attribute, getattr(new_response, attribute))
354
- else:
355
- response.__dict__.update(new_response.__dict__)
356
- _raise_auth_error(new_response, case.operation.verbose_name)
358
+ _update_response(response, new_response)
359
+ _raise_no_auth_error(new_response, case.operation.verbose_name, "that requires authentication")
360
+ # Try to set invalid auth and check if it succeeds
361
+ for parameter in security_parameters:
362
+ _set_auth_for_case(case, parameter)
363
+ new_response = case.operation.schema.transport.send(case)
364
+ if 200 <= new_response.status_code < 300:
365
+ _update_response(response, new_response)
366
+ _raise_no_auth_error(new_response, case.operation.verbose_name, "with any auth")
367
+ _remove_auth_from_case(case, security_parameters)
368
+ elif auth == AuthKind.GENERATED:
369
+ # If this auth is generated which means it is likely invalid, then
370
+ # this request should have been an error
371
+ _raise_no_auth_error(response, case.operation.verbose_name, "with invalid auth")
357
372
  else:
358
373
  # Successful response when there is no auth
359
- _raise_auth_error(response, case.operation.verbose_name)
374
+ _raise_no_auth_error(response, case.operation.verbose_name, "that requires authentication")
360
375
  return None
361
376
 
362
377
 
363
- def _raise_auth_error(response: GenericResponse, operation: str) -> NoReturn:
378
+ def _update_response(old: GenericResponse, new: GenericResponse) -> None:
379
+ # Mutate the response object in place on the best effort basis
380
+ if hasattr(old, "__attrs__"):
381
+ for attribute in new.__attrs__:
382
+ setattr(old, attribute, getattr(new, attribute))
383
+ else:
384
+ old.__dict__.update(new.__dict__)
385
+
386
+
387
+ def _raise_no_auth_error(response: GenericResponse, operation: str, suffix: str) -> NoReturn:
364
388
  from ...transports.responses import get_reason
365
389
 
366
390
  exc_class = get_ignored_auth_error(operation)
367
391
  reason = get_reason(response.status_code)
368
- message = f"The API returned `{response.status_code} {reason}` for `{operation}` that requires authentication."
392
+ message = f"The API returned `{response.status_code} {reason}` for `{operation}` {suffix}."
369
393
  raise exc_class(
370
394
  failures.IgnoredAuth.title,
371
395
  context=failures.IgnoredAuth(message=message),
@@ -387,10 +411,15 @@ def _get_security_parameters(operation: APIOperation) -> list[SecurityParameter]
387
411
  ]
388
412
 
389
413
 
390
- def _contains_auth(request: PreparedRequest, security_parameters: list[SecurityParameter]) -> bool:
414
+ def _contains_auth(
415
+ ctx: CheckContext, case: Case, request: PreparedRequest, security_parameters: list[SecurityParameter]
416
+ ) -> AuthKind | None:
391
417
  """Whether a request has authentication declared in the schema."""
392
418
  from requests.cookies import RequestsCookieJar
393
419
 
420
+ # If auth comes from explicit `auth` option or a custom auth, it is always explicit
421
+ if ctx.auth is not None or case._has_explicit_auth:
422
+ return AuthKind.EXPLICIT
394
423
  parsed = urlparse(request.url)
395
424
  query = parse_qs(parsed.query) # type: ignore
396
425
  # Load the `Cookie` header separately, because it is possible that `request._cookies` and the header are out of sync
@@ -410,10 +439,20 @@ def _contains_auth(request: PreparedRequest, security_parameters: list[SecurityP
410
439
  return p["in"] == "cookie" and (p["name"] in cookies or p["name"] in header_cookies)
411
440
 
412
441
  for parameter in security_parameters:
413
- if has_header(parameter) or has_query(parameter) or has_cookie(parameter):
414
- return True
442
+ if has_header(parameter):
443
+ if ctx.headers is not None and parameter["name"] in ctx.headers:
444
+ return AuthKind.EXPLICIT
445
+ return AuthKind.GENERATED
446
+ if has_cookie(parameter):
447
+ if ctx.headers is not None and "Cookie" in ctx.headers:
448
+ cookies = cast(RequestsCookieJar, ctx.headers["Cookie"]) # type: ignore
449
+ if parameter["name"] in cookies:
450
+ return AuthKind.EXPLICIT
451
+ return AuthKind.GENERATED
452
+ if has_query(parameter):
453
+ return AuthKind.GENERATED
415
454
 
416
- return False
455
+ return None
417
456
 
418
457
 
419
458
  def _remove_auth_from_case(case: Case, security_parameters: list[SecurityParameter]) -> None:
@@ -431,6 +470,19 @@ def _remove_auth_from_case(case: Case, security_parameters: list[SecurityParamet
431
470
  case.cookies.pop(name, None)
432
471
 
433
472
 
473
+ def _set_auth_for_case(case: Case, parameter: SecurityParameter) -> None:
474
+ name = parameter["name"]
475
+ for location, attr_name in (
476
+ ("header", "headers"),
477
+ ("query", "query"),
478
+ ("cookie", "cookies"),
479
+ ):
480
+ if parameter["in"] == location:
481
+ container = getattr(case, attr_name, {})
482
+ container[name] = "SCHEMATHESIS-INVALID-VALUE"
483
+ setattr(case, attr_name, container)
484
+
485
+
434
486
  @dataclass
435
487
  class ResourcePath:
436
488
  """A path to a resource with variables."""
@@ -163,12 +163,12 @@ def from_uri(
163
163
  interval=WAIT_FOR_SCHEMA_INTERVAL,
164
164
  )
165
165
  def _load_schema(_uri: str, **_kwargs: Any) -> requests.Response:
166
- _kwargs.setdefault("timeout", DEFAULT_RESPONSE_TIMEOUT / 1000)
167
- return requests.get(_uri, **kwargs)
166
+ return requests.get(_uri, **_kwargs)
168
167
 
169
168
  else:
170
169
  _load_schema = requests.get
171
170
 
171
+ kwargs.setdefault("timeout", DEFAULT_RESPONSE_TIMEOUT / 1000)
172
172
  response = load_schema_from_url(lambda: _load_schema(uri, **kwargs))
173
173
  return from_file(
174
174
  response.text,
@@ -253,7 +253,7 @@ class BaseOpenAPISchema(BaseSchema):
253
253
  return self.collect_parameters(itertools.chain(parameters, shared_parameters), operation)
254
254
 
255
255
  def get_all_operations(
256
- self, hooks: HookDispatcher | None = None
256
+ self, hooks: HookDispatcher | None = None, generation_config: GenerationConfig | None = None
257
257
  ) -> Generator[Result[APIOperation, OperationSchemaError], None, None]:
258
258
  """Iterate over all operations defined in the API.
259
259
 
@@ -308,7 +308,17 @@ class BaseOpenAPISchema(BaseSchema):
308
308
  continue
309
309
  parameters = resolved.get("parameters", ())
310
310
  parameters = collect_parameters(itertools.chain(parameters, shared_parameters), resolved)
311
- operation = make_operation(path, method, parameters, entry, resolved, scope)
311
+ operation = make_operation(
312
+ path,
313
+ method,
314
+ parameters,
315
+ entry,
316
+ resolved,
317
+ scope,
318
+ with_security_parameters=generation_config.with_security_parameters
319
+ if generation_config
320
+ else None,
321
+ )
312
322
  context = HookContext(operation=operation)
313
323
  if (
314
324
  should_skip_operation(GLOBAL_HOOK_DISPATCHER, context)
@@ -383,6 +393,7 @@ class BaseOpenAPISchema(BaseSchema):
383
393
  raw: dict[str, Any],
384
394
  resolved: dict[str, Any],
385
395
  scope: str,
396
+ with_security_parameters: bool | None = None,
386
397
  ) -> APIOperation:
387
398
  """Create JSON schemas for the query, body, etc from Swagger parameters definitions."""
388
399
  __tracebackhide__ = True
@@ -397,7 +408,12 @@ class BaseOpenAPISchema(BaseSchema):
397
408
  )
398
409
  for parameter in parameters:
399
410
  operation.add_parameter(parameter)
400
- if self.generation_config.with_security_parameters:
411
+ with_security_parameters = (
412
+ with_security_parameters
413
+ if with_security_parameters is not None
414
+ else self.generation_config.with_security_parameters
415
+ )
416
+ if with_security_parameters:
401
417
  self.security.process_definitions(self.raw_schema, operation, self.resolver)
402
418
  self.dispatch_hook("before_init_operation", HookContext(operation=operation), operation)
403
419
  return operation
@@ -69,6 +69,7 @@ class StatefulTestRunnerConfig:
69
69
  max_response_time: int | None = None
70
70
  dry_run: bool = False
71
71
  targets: list[Target] = field(default_factory=list)
72
+ unique_data: bool = False
72
73
 
73
74
  def __post_init__(self) -> None:
74
75
  import hypothesis