schemathesis 4.0.0a12__py3-none-any.whl → 4.0.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.
Files changed (41) hide show
  1. schemathesis/__init__.py +9 -4
  2. schemathesis/auths.py +20 -30
  3. schemathesis/checks.py +5 -0
  4. schemathesis/cli/commands/run/__init__.py +9 -6
  5. schemathesis/cli/commands/run/handlers/output.py +13 -0
  6. schemathesis/cli/constants.py +1 -1
  7. schemathesis/config/_operations.py +16 -21
  8. schemathesis/config/_projects.py +5 -1
  9. schemathesis/core/errors.py +10 -17
  10. schemathesis/core/transport.py +81 -1
  11. schemathesis/engine/errors.py +1 -1
  12. schemathesis/generation/case.py +152 -28
  13. schemathesis/generation/hypothesis/builder.py +12 -12
  14. schemathesis/generation/overrides.py +11 -27
  15. schemathesis/generation/stateful/__init__.py +13 -0
  16. schemathesis/generation/stateful/state_machine.py +31 -108
  17. schemathesis/graphql/loaders.py +14 -4
  18. schemathesis/hooks.py +1 -4
  19. schemathesis/openapi/checks.py +82 -20
  20. schemathesis/openapi/generation/filters.py +9 -2
  21. schemathesis/openapi/loaders.py +14 -4
  22. schemathesis/pytest/lazy.py +4 -31
  23. schemathesis/pytest/plugin.py +21 -11
  24. schemathesis/schemas.py +153 -89
  25. schemathesis/specs/graphql/schemas.py +6 -6
  26. schemathesis/specs/openapi/_hypothesis.py +39 -14
  27. schemathesis/specs/openapi/checks.py +95 -34
  28. schemathesis/specs/openapi/expressions/nodes.py +1 -1
  29. schemathesis/specs/openapi/negative/__init__.py +5 -3
  30. schemathesis/specs/openapi/negative/mutations.py +2 -2
  31. schemathesis/specs/openapi/parameters.py +0 -3
  32. schemathesis/specs/openapi/schemas.py +6 -91
  33. schemathesis/specs/openapi/stateful/links.py +1 -63
  34. schemathesis/transport/requests.py +12 -1
  35. schemathesis/transport/serialization.py +0 -4
  36. schemathesis/transport/wsgi.py +7 -0
  37. {schemathesis-4.0.0a12.dist-info → schemathesis-4.0.1.dist-info}/METADATA +8 -10
  38. {schemathesis-4.0.0a12.dist-info → schemathesis-4.0.1.dist-info}/RECORD +41 -41
  39. {schemathesis-4.0.0a12.dist-info → schemathesis-4.0.1.dist-info}/WHEEL +0 -0
  40. {schemathesis-4.0.0a12.dist-info → schemathesis-4.0.1.dist-info}/entry_points.txt +0 -0
  41. {schemathesis-4.0.0a12.dist-info → schemathesis-4.0.1.dist-info}/licenses/LICENSE +0 -0
@@ -21,10 +21,12 @@ from schemathesis.openapi.checks import (
21
21
  JsonSchemaError,
22
22
  MalformedMediaType,
23
23
  MissingContentType,
24
+ MissingHeaderNotRejected,
24
25
  MissingHeaders,
25
26
  RejectedPositiveData,
26
27
  UndefinedContentType,
27
28
  UndefinedStatusCode,
29
+ UnsupportedMethodResponse,
28
30
  UseAfterFree,
29
31
  )
30
32
  from schemathesis.specs.openapi.constants import LOCATION_TO_CONTAINER
@@ -33,9 +35,7 @@ from schemathesis.transport.prepare import prepare_path
33
35
  from .utils import expand_status_code, expand_status_codes
34
36
 
35
37
  if TYPE_CHECKING:
36
- from requests import PreparedRequest
37
-
38
- from ...schemas import APIOperation
38
+ from schemathesis.schemas import APIOperation
39
39
 
40
40
 
41
41
  def is_unexpected_http_status_case(case: Case) -> bool:
@@ -289,8 +289,14 @@ def missing_required_header(ctx: CheckContext, response: Response, case: Case) -
289
289
  config = ctx.config.missing_required_header
290
290
  expected_statuses = expand_status_codes(config.expected_statuses or [])
291
291
  if response.status_code not in expected_statuses:
292
- allowed = f"Allowed statuses: {', '.join(map(str, expected_statuses))}"
293
- raise AssertionError(f"Unexpected response status for a missing header: {response.status_code}\n{allowed}")
292
+ allowed = ", ".join(map(str, expected_statuses))
293
+ raise MissingHeaderNotRejected(
294
+ operation=f"{case.method} {case.path}",
295
+ header_name=data.parameter,
296
+ status_code=response.status_code,
297
+ expected_statuses=list(expected_statuses),
298
+ message=f"Missing header not rejected (got {response.status_code}, expected {allowed})",
299
+ )
294
300
  return None
295
301
 
296
302
 
@@ -302,13 +308,24 @@ def unsupported_method(ctx: CheckContext, response: Response, case: Case) -> boo
302
308
  data = meta.phase.data
303
309
  if data.description and data.description.startswith("Unspecified HTTP method:"):
304
310
  if response.status_code != 405:
305
- raise AssertionError(
306
- f"Unexpected response status for unspecified HTTP method: {response.status_code}\nExpected: 405"
311
+ raise UnsupportedMethodResponse(
312
+ operation=case.operation.label,
313
+ method=cast(str, response.request.method),
314
+ status_code=response.status_code,
315
+ failure_reason="wrong_status",
316
+ message=f"Wrong status for unsupported method {response.request.method} (got {response.status_code}, expected 405)",
307
317
  )
308
318
 
309
319
  allow_header = response.headers.get("allow")
310
320
  if not allow_header:
311
- raise AssertionError("Missing 'Allow' header in 405 Method Not Allowed response")
321
+ raise UnsupportedMethodResponse(
322
+ operation=case.operation.label,
323
+ method=cast(str, response.request.method),
324
+ status_code=response.status_code,
325
+ allow_header_present=False,
326
+ failure_reason="missing_allow_header",
327
+ message=f"Missing Allow header for unsupported method {response.request.method}",
328
+ )
312
329
  return None
313
330
 
314
331
 
@@ -458,6 +475,12 @@ def ensure_resource_availability(ctx: CheckContext, response: Response, case: Ca
458
475
  )
459
476
 
460
477
 
478
+ class AuthScenario(str, enum.Enum):
479
+ NO_AUTH = "no_auth"
480
+ INVALID_AUTH = "invalid_auth"
481
+ GENERATED_AUTH = "generated_auth"
482
+
483
+
461
484
  class AuthKind(str, enum.Enum):
462
485
  EXPLICIT = "explicit"
463
486
  GENERATED = "generated"
@@ -473,23 +496,28 @@ def ignored_auth(ctx: CheckContext, response: Response, case: Case) -> bool | No
473
496
  security_parameters = _get_security_parameters(case.operation)
474
497
  # Authentication is required for this API operation and response is successful
475
498
  if security_parameters and 200 <= response.status_code < 300:
476
- auth = _contains_auth(ctx, case, response.request, security_parameters)
499
+ auth = _contains_auth(ctx, case, response, security_parameters)
477
500
  if auth == AuthKind.EXPLICIT:
478
501
  # Auth is explicitly set, it is expected to be valid
479
502
  # Check if invalid auth will give an error
480
503
  no_auth_case = remove_auth(case, security_parameters)
481
504
  kwargs = ctx._transport_kwargs or {}
482
505
  kwargs.copy()
483
- if "headers" in kwargs:
484
- headers = kwargs["headers"].copy()
485
- _remove_auth_from_explicit_headers(headers, security_parameters)
486
- kwargs["headers"] = headers
506
+ for location, container_name in (
507
+ ("header", "headers"),
508
+ ("cookie", "cookies"),
509
+ ("query", "query"),
510
+ ):
511
+ if container_name in kwargs:
512
+ container = kwargs[container_name].copy()
513
+ _remove_auth_from_container(container, security_parameters, location=location)
514
+ kwargs[container_name] = container
487
515
  kwargs.pop("session", None)
488
516
  ctx._record_case(parent_id=case.id, case=no_auth_case)
489
517
  no_auth_response = case.operation.schema.transport.send(no_auth_case, **kwargs)
490
518
  ctx._record_response(case_id=no_auth_case.id, response=no_auth_response)
491
519
  if no_auth_response.status_code != 401:
492
- _raise_no_auth_error(no_auth_response, no_auth_case, "that requires authentication")
520
+ _raise_no_auth_error(no_auth_response, no_auth_case, AuthScenario.NO_AUTH)
493
521
  # Try to set invalid auth and check if it succeeds
494
522
  for parameter in security_parameters:
495
523
  invalid_auth_case = remove_auth(case, security_parameters)
@@ -498,22 +526,38 @@ def ignored_auth(ctx: CheckContext, response: Response, case: Case) -> bool | No
498
526
  invalid_auth_response = case.operation.schema.transport.send(invalid_auth_case, **kwargs)
499
527
  ctx._record_response(case_id=invalid_auth_case.id, response=invalid_auth_response)
500
528
  if invalid_auth_response.status_code != 401:
501
- _raise_no_auth_error(invalid_auth_response, invalid_auth_case, "with any auth")
529
+ _raise_no_auth_error(invalid_auth_response, invalid_auth_case, AuthScenario.INVALID_AUTH)
502
530
  elif auth == AuthKind.GENERATED:
503
531
  # If this auth is generated which means it is likely invalid, then
504
532
  # this request should have been an error
505
- _raise_no_auth_error(response, case, "with invalid auth")
533
+ _raise_no_auth_error(response, case, AuthScenario.GENERATED_AUTH)
506
534
  else:
507
535
  # Successful response when there is no auth
508
- _raise_no_auth_error(response, case, "that requires authentication")
536
+ _raise_no_auth_error(response, case, AuthScenario.NO_AUTH)
509
537
  return None
510
538
 
511
539
 
512
- def _raise_no_auth_error(response: Response, case: Case, suffix: str) -> NoReturn:
540
+ def _raise_no_auth_error(response: Response, case: Case, auth: AuthScenario) -> NoReturn:
513
541
  reason = http.client.responses.get(response.status_code, "Unknown")
542
+
543
+ if auth == AuthScenario.NO_AUTH:
544
+ title = "API accepts requests without authentication"
545
+ detail = None
546
+ elif auth == AuthScenario.INVALID_AUTH:
547
+ title = "API accepts invalid authentication"
548
+ detail = "invalid credentials provided"
549
+ else:
550
+ title = "API accepts invalid authentication"
551
+ detail = "generated auth likely invalid"
552
+
553
+ message = f"Expected 401, got `{response.status_code} {reason}` for `{case.operation.label}`"
554
+ if detail is not None:
555
+ message = f"{message} ({detail})"
556
+
514
557
  raise IgnoredAuth(
515
558
  operation=case.operation.label,
516
- message=f"The API returned `{response.status_code} {reason}` for `{case.operation.label}` {suffix}.",
559
+ message=message,
560
+ title=title,
517
561
  case_id=case.id,
518
562
  )
519
563
 
@@ -534,7 +578,7 @@ def _get_security_parameters(operation: APIOperation) -> list[SecurityParameter]
534
578
 
535
579
 
536
580
  def _contains_auth(
537
- ctx: CheckContext, case: Case, request: PreparedRequest, security_parameters: list[SecurityParameter]
581
+ ctx: CheckContext, case: Case, response: Response, security_parameters: list[SecurityParameter]
538
582
  ) -> AuthKind | None:
539
583
  """Whether a request has authentication declared in the schema."""
540
584
  from requests.cookies import RequestsCookieJar
@@ -542,6 +586,7 @@ def _contains_auth(
542
586
  # If auth comes from explicit `auth` option or a custom auth, it is always explicit
543
587
  if ctx._auth is not None or case._has_explicit_auth:
544
588
  return AuthKind.EXPLICIT
589
+ request = response.request
545
590
  parsed = urlparse(request.url)
546
591
  query = parse_qs(parsed.query) # type: ignore
547
592
  # Load the `Cookie` header separately, because it is possible that `request._cookies` and the header are out of sync
@@ -563,19 +608,35 @@ def _contains_auth(
563
608
  for parameter in security_parameters:
564
609
  name = parameter["name"]
565
610
  if has_header(parameter):
566
- if (ctx._headers is not None and name in ctx._headers) or (ctx._override and name in ctx._override.headers):
611
+ if (
612
+ # Explicit CLI headers
613
+ (ctx._headers is not None and name in ctx._headers)
614
+ # Other kinds of overrides
615
+ or (ctx._override and name in ctx._override.headers)
616
+ or (response._override and name in response._override.headers)
617
+ ):
567
618
  return AuthKind.EXPLICIT
568
619
  return AuthKind.GENERATED
569
620
  if has_cookie(parameter):
570
- if ctx._headers is not None and "Cookie" in ctx._headers:
571
- cookies = cast(RequestsCookieJar, ctx._headers["Cookie"]) # type: ignore
572
- if name in cookies:
573
- return AuthKind.EXPLICIT
574
- if ctx._override and name in ctx._override.cookies:
621
+ for headers in [
622
+ ctx._headers,
623
+ (ctx._override.headers if ctx._override else None),
624
+ (response._override.headers if response._override else None),
625
+ ]:
626
+ if headers is not None and "Cookie" in headers:
627
+ jar = cast(RequestsCookieJar, headers["Cookie"])
628
+ if name in jar:
629
+ return AuthKind.EXPLICIT
630
+
631
+ if (ctx._override and name in ctx._override.cookies) or (
632
+ response._override and name in response._override.cookies
633
+ ):
575
634
  return AuthKind.EXPLICIT
576
635
  return AuthKind.GENERATED
577
636
  if has_query(parameter):
578
- if ctx._override and name in ctx._override.query:
637
+ if (ctx._override and name in ctx._override.query) or (
638
+ response._override and name in response._override.query
639
+ ):
579
640
  return AuthKind.EXPLICIT
580
641
  return AuthKind.GENERATED
581
642
 
@@ -587,9 +648,9 @@ def remove_auth(case: Case, security_parameters: list[SecurityParameter]) -> Cas
587
648
 
588
649
  It mutates `case` in place.
589
650
  """
590
- headers = case.headers.copy() if case.headers else None
591
- query = case.query.copy() if case.query else None
592
- cookies = case.cookies.copy() if case.cookies else None
651
+ headers = case.headers.copy()
652
+ query = case.query.copy()
653
+ cookies = case.cookies.copy()
593
654
  for parameter in security_parameters:
594
655
  name = parameter["name"]
595
656
  if parameter["in"] == "header" and headers:
@@ -602,7 +663,7 @@ def remove_auth(case: Case, security_parameters: list[SecurityParameter]) -> Cas
602
663
  operation=case.operation,
603
664
  method=case.method,
604
665
  path=case.path,
605
- path_parameters=case.path_parameters.copy() if case.path_parameters else None,
666
+ path_parameters=case.path_parameters.copy(),
606
667
  headers=headers,
607
668
  cookies=cookies,
608
669
  query=query,
@@ -612,11 +673,11 @@ def remove_auth(case: Case, security_parameters: list[SecurityParameter]) -> Cas
612
673
  )
613
674
 
614
675
 
615
- def _remove_auth_from_explicit_headers(headers: dict, security_parameters: list[SecurityParameter]) -> None:
676
+ def _remove_auth_from_container(container: dict, security_parameters: list[SecurityParameter], location: str) -> None:
616
677
  for parameter in security_parameters:
617
678
  name = parameter["name"]
618
- if parameter["in"] == "header":
619
- headers.pop(name, None)
679
+ if parameter["in"] == location:
680
+ container.pop(name, None)
620
681
 
621
682
 
622
683
  def _set_auth_for_case(case: Case, parameter: SecurityParameter) -> None:
@@ -87,7 +87,7 @@ class NonBodyRequest(Node):
87
87
  extractor: Extractor | None = None
88
88
 
89
89
  def evaluate(self, output: StepOutput) -> str | Unresolvable:
90
- container: dict | CaseInsensitiveDict = {
90
+ container = {
91
91
  "query": output.case.query,
92
92
  "path": output.case.path_parameters,
93
93
  "header": output.case.headers,
@@ -28,16 +28,17 @@ class CacheKey:
28
28
  operation_name: str
29
29
  location: str
30
30
  schema: Schema
31
+ validator_cls: type[jsonschema.Validator]
31
32
 
32
33
  def __hash__(self) -> int:
33
34
  return hash((self.operation_name, self.location))
34
35
 
35
36
 
36
37
  @lru_cache
37
- def get_validator(cache_key: CacheKey) -> jsonschema.Draft4Validator:
38
+ def get_validator(cache_key: CacheKey) -> jsonschema.Validator:
38
39
  """Get JSON Schema validator for the given schema."""
39
40
  # Each operation / location combo has only a single schema, therefore could be cached
40
- return jsonschema.Draft4Validator(cache_key.schema)
41
+ return cache_key.validator_cls(cache_key.schema)
41
42
 
42
43
 
43
44
  @lru_cache
@@ -63,6 +64,7 @@ def negative_schema(
63
64
  generation_config: GenerationConfig,
64
65
  *,
65
66
  custom_formats: dict[str, st.SearchStrategy[str]],
67
+ validator_cls: type[jsonschema.Validator],
66
68
  ) -> st.SearchStrategy:
67
69
  """A strategy for instances that DO NOT match the input schema.
68
70
 
@@ -70,7 +72,7 @@ def negative_schema(
70
72
  """
71
73
  # The mutated schema is passed to `from_schema` and guarded against producing instances valid against
72
74
  # the original schema.
73
- cache_key = CacheKey(operation_name, location, schema)
75
+ cache_key = CacheKey(operation_name, location, schema, validator_cls)
74
76
  validator = get_validator(cache_key)
75
77
  keywords, non_keywords = split_schema(cache_key)
76
78
 
@@ -402,8 +402,8 @@ def negate_constraints(context: MutationContext, draw: Draw, schema: Schema) ->
402
402
  if key in DEPENDENCIES:
403
403
  # If this keyword has a dependency, then it should be also negated
404
404
  dependency = DEPENDENCIES[key]
405
- if dependency not in negated:
406
- negated[dependency] = copied[dependency] # Assuming the schema is valid
405
+ if dependency not in negated and dependency in copied:
406
+ negated[dependency] = copied[dependency]
407
407
  else:
408
408
  schema[key] = value
409
409
  if is_negated:
@@ -314,9 +314,6 @@ def parameters_to_json_schema(
314
314
  ) -> dict[str, Any]:
315
315
  """Create an "object" JSON schema from a list of Open API parameters.
316
316
 
317
- :param List[OpenAPIParameter] parameters: A list of Open API parameters related to the same location. All of
318
- them are expected to have the same "in" value.
319
-
320
317
  For each input parameter, there will be a property in the output schema.
321
318
 
322
319
  This:
@@ -39,7 +39,6 @@ from schemathesis.core.transport import Response
39
39
  from schemathesis.core.validation import INVALID_HEADER_RE
40
40
  from schemathesis.generation.case import Case
41
41
  from schemathesis.generation.meta import CaseMetadata
42
- from schemathesis.generation.overrides import Override, OverrideMark, check_no_override_mark
43
42
  from schemathesis.openapi.checks import JsonSchemaError, MissingContentType
44
43
  from schemathesis.specs.openapi.stateful import links
45
44
 
@@ -259,26 +258,6 @@ class BaseOpenAPISchema(BaseSchema):
259
258
  # Ignore errors
260
259
  continue
261
260
 
262
- def override(
263
- self,
264
- *,
265
- query: dict[str, str] | None = None,
266
- headers: dict[str, str] | None = None,
267
- cookies: dict[str, str] | None = None,
268
- path_parameters: dict[str, str] | None = None,
269
- ) -> Callable[[Callable], Callable]:
270
- """Override Open API parameters with fixed values."""
271
-
272
- def _add_override(test: Callable) -> Callable:
273
- check_no_override_mark(test)
274
- override = Override(
275
- query=query or {}, headers=headers or {}, cookies=cookies or {}, path_parameters=path_parameters or {}
276
- )
277
- OverrideMark.set(test, override)
278
- return test
279
-
280
- return _add_override
281
-
282
261
  def _resolve_until_no_references(self, value: dict[str, Any]) -> dict[str, Any]:
283
262
  while "$ref" in value:
284
263
  _, value = self.resolver.resolve(value["$ref"])
@@ -596,52 +575,6 @@ class BaseOpenAPISchema(BaseSchema):
596
575
  def as_state_machine(self) -> type[APIStateMachine]:
597
576
  return create_state_machine(self)
598
577
 
599
- def add_link(
600
- self,
601
- source: APIOperation,
602
- target: str | APIOperation,
603
- status_code: str | int,
604
- parameters: dict[str, str] | None = None,
605
- request_body: Any = None,
606
- name: str | None = None,
607
- ) -> None:
608
- """Add a new Open API link to the schema definition.
609
-
610
- :param APIOperation source: This operation is the source of data
611
- :param target: This operation will receive the data from this link.
612
- Can be an ``APIOperation`` instance or a reference like this - ``#/paths/~1users~1{userId}/get``
613
- :param str status_code: The link is triggered when the source API operation responds with this status code.
614
- :param parameters: A dictionary that describes how parameters should be extracted from the matched response.
615
- The key represents the parameter name in the target API operation, and the value is a runtime
616
- expression string.
617
- :param request_body: A literal value or runtime expression to use as a request body when
618
- calling the target operation.
619
- :param str name: Explicit link name.
620
-
621
- .. code-block:: python
622
-
623
- schema = schemathesis.openapi.from_url("http://0.0.0.0/schema.yaml")
624
-
625
- schema.add_link(
626
- source=schema["/users/"]["POST"],
627
- target=schema["/users/{userId}"]["GET"],
628
- status_code="201",
629
- parameters={"userId": "$response.body#/id"},
630
- )
631
- """
632
- if parameters is None and request_body is None:
633
- raise ValueError("You need to provide `parameters` or `request_body`.")
634
- links.add_link(
635
- resolver=self.resolver,
636
- responses=self[source.path][source.method].definition.raw["responses"],
637
- links_field=self.links_field,
638
- parameters=parameters,
639
- request_body=request_body,
640
- status_code=status_code,
641
- target=target,
642
- name=name,
643
- )
644
-
645
578
  def get_links(self, operation: APIOperation) -> dict[str, dict[str, Any]]:
646
579
  result: dict[str, dict[str, Any]] = defaultdict(dict)
647
580
  for status_code, link in links.get_all_links(operation):
@@ -909,7 +842,7 @@ class MethodMap(Mapping):
909
842
  try:
910
843
  return self._init_operation(item)
911
844
  except LookupError as exc:
912
- available_methods = ", ".join(map(str.upper, self))
845
+ available_methods = ", ".join(key.upper() for key in self if key in HTTP_METHODS)
913
846
  message = f"Method `{item.upper()}` not found."
914
847
  if available_methods:
915
848
  message += f" Available methods: {available_methods}"
@@ -1007,12 +940,6 @@ class SwaggerV20(BaseOpenAPISchema):
1007
940
  def prepare_multipart(
1008
941
  self, form_data: dict[str, Any], operation: APIOperation
1009
942
  ) -> tuple[list | None, dict[str, Any] | None]:
1010
- """Prepare form data for sending with `requests`.
1011
-
1012
- :param form_data: Raw generated data as a dictionary.
1013
- :param operation: The tested API operation for which the data was generated.
1014
- :return: `files` and `data` values for `requests.request`.
1015
- """
1016
943
  files, data = [], {}
1017
944
  # If there is no content types specified for the request or "application/x-www-form-urlencoded" is specified
1018
945
  # explicitly, then use it., but if "multipart/form-data" is specified, then use it
@@ -1056,7 +983,7 @@ class SwaggerV20(BaseOpenAPISchema):
1056
983
  method: str | None = None,
1057
984
  path: str | None = None,
1058
985
  path_parameters: dict[str, Any] | None = None,
1059
- headers: dict[str, Any] | None = None,
986
+ headers: dict[str, Any] | CaseInsensitiveDict | None = None,
1060
987
  cookies: dict[str, Any] | None = None,
1061
988
  query: dict[str, Any] | None = None,
1062
989
  body: list | dict[str, Any] | str | int | float | bool | bytes | NotSet = NOT_SET,
@@ -1069,22 +996,16 @@ class SwaggerV20(BaseOpenAPISchema):
1069
996
  operation=operation,
1070
997
  method=method or operation.method.upper(),
1071
998
  path=path or operation.path,
1072
- path_parameters=path_parameters,
1073
- headers=CaseInsensitiveDict(headers) if headers is not None else headers,
1074
- cookies=cookies,
1075
- query=query,
999
+ path_parameters=path_parameters or {},
1000
+ headers=CaseInsensitiveDict() if headers is None else CaseInsensitiveDict(headers),
1001
+ cookies=cookies or {},
1002
+ query=query or {},
1076
1003
  body=body,
1077
1004
  media_type=media_type,
1078
1005
  meta=meta,
1079
1006
  )
1080
1007
 
1081
1008
  def _get_consumes_for_operation(self, definition: dict[str, Any]) -> list[str]:
1082
- """Get the `consumes` value for the given API operation.
1083
-
1084
- :param definition: Raw API operation definition.
1085
- :return: A list of media-types for this operation.
1086
- :rtype: List[str]
1087
- """
1088
1009
  global_consumes = self.raw_schema.get("consumes", [])
1089
1010
  consumes = definition.get("consumes", [])
1090
1011
  if not consumes:
@@ -1183,12 +1104,6 @@ class OpenApi30(SwaggerV20):
1183
1104
  def prepare_multipart(
1184
1105
  self, form_data: dict[str, Any], operation: APIOperation
1185
1106
  ) -> tuple[list | None, dict[str, Any] | None]:
1186
- """Prepare form data for sending with `requests`.
1187
-
1188
- :param form_data: Raw generated data as a dictionary.
1189
- :param operation: The tested API operation for which the data was generated.
1190
- :return: `files` and `data` values for `requests.request`.
1191
- """
1192
1107
  files = []
1193
1108
  definition = operation.definition.raw
1194
1109
  if "$ref" in definition["requestBody"]:
@@ -2,7 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  from dataclasses import dataclass
4
4
  from functools import lru_cache
5
- from typing import TYPE_CHECKING, Any, Callable, Generator, Literal, Union, cast
5
+ from typing import Any, Callable, Generator, Literal, cast
6
6
 
7
7
  from schemathesis.core import NOT_SET, NotSet
8
8
  from schemathesis.core.errors import InvalidTransition, OperationNotFound, TransitionValidationError
@@ -13,10 +13,6 @@ from schemathesis.specs.openapi import expressions
13
13
  from schemathesis.specs.openapi.constants import LOCATION_TO_CONTAINER
14
14
  from schemathesis.specs.openapi.references import RECURSION_DEPTH_LIMIT
15
15
 
16
- if TYPE_CHECKING:
17
- from jsonschema import RefResolver
18
-
19
-
20
16
  SCHEMATHESIS_LINK_EXTENSION = "x-schemathesis"
21
17
  ParameterLocation = Literal["path", "query", "header", "cookie", "body"]
22
18
 
@@ -211,61 +207,3 @@ def get_all_links(
211
207
  yield status_code, Ok(link)
212
208
  except InvalidTransition as exc:
213
209
  yield status_code, Err(exc)
214
-
215
-
216
- StatusCode = Union[str, int]
217
-
218
-
219
- def _get_response_by_status_code(responses: dict[StatusCode, dict[str, Any]], status_code: str | int) -> dict:
220
- if isinstance(status_code, int):
221
- # Invalid schemas may contain status codes as integers
222
- if status_code in responses:
223
- return responses[status_code]
224
- # Passed here as an integer, but there is no such status code as int
225
- # We cast it to a string because it is either there already and we'll get relevant responses, otherwise
226
- # a new dict will be created because there is no such status code in the schema (as an int or a string)
227
- return responses.setdefault(str(status_code), {})
228
- if status_code.isnumeric():
229
- # Invalid schema but the status code is passed as a string
230
- numeric_status_code = int(status_code)
231
- if numeric_status_code in responses:
232
- return responses[numeric_status_code]
233
- # All status codes as strings, including `default` and patterned values like `5XX`
234
- return responses.setdefault(status_code, {})
235
-
236
-
237
- def add_link(
238
- resolver: RefResolver,
239
- responses: dict[StatusCode, dict[str, Any]],
240
- links_field: str,
241
- parameters: dict[str, str] | None,
242
- request_body: Any,
243
- status_code: StatusCode,
244
- target: str | APIOperation,
245
- name: str | None = None,
246
- ) -> None:
247
- response = _get_response_by_status_code(responses, status_code)
248
- if "$ref" in response:
249
- _, response = resolver.resolve(response["$ref"])
250
- links_definition = response.setdefault(links_field, {})
251
- new_link: dict[str, str | dict[str, str]] = {}
252
- if parameters is not None:
253
- new_link["parameters"] = parameters
254
- if request_body is not None:
255
- new_link["requestBody"] = request_body
256
- if isinstance(target, str):
257
- name = name or target
258
- new_link["operationRef"] = target
259
- else:
260
- name = name or f"{target.method.upper()} {target.path}"
261
- # operationId is a dict lookup which is more efficient than using `operationRef`, since it
262
- # doesn't involve reference resolving when we will look up for this target during testing.
263
- if "operationId" in target.definition.raw:
264
- new_link["operationId"] = target.definition.raw["operationId"]
265
- else:
266
- new_link["operationRef"] = target.operation_reference
267
- # The name is arbitrary, so we don't really case what it is,
268
- # but it should not override existing links
269
- while name in links_definition:
270
- name += "_new"
271
- links_definition[name] = new_link
@@ -11,6 +11,7 @@ from schemathesis.core import NotSet
11
11
  from schemathesis.core.rate_limit import ratelimit
12
12
  from schemathesis.core.transforms import deepclone, merge_at
13
13
  from schemathesis.core.transport import DEFAULT_RESPONSE_TIMEOUT, Response
14
+ from schemathesis.generation.overrides import Override
14
15
  from schemathesis.transport import BaseTransport, SerializationContext
15
16
  from schemathesis.transport.prepare import prepare_body, prepare_headers, prepare_url
16
17
  from schemathesis.transport.serialization import Binary, serialize_binary, serialize_json, serialize_xml, serialize_yaml
@@ -104,7 +105,17 @@ class RequestsTransport(BaseTransport["requests.Session"]):
104
105
  rate_limit = config.rate_limit_for(operation=case.operation)
105
106
  with ratelimit(rate_limit, config.base_url):
106
107
  response = session.request(**data) # type: ignore
107
- return Response.from_requests(response, verify=verify)
108
+ return Response.from_requests(
109
+ response,
110
+ verify=verify,
111
+ _override=Override(
112
+ query=kwargs.get("params") or {},
113
+ headers=kwargs.get("headers") or {},
114
+ cookies=kwargs.get("cookies") or {},
115
+ path_parameters={},
116
+ ),
117
+ )
118
+
108
119
  finally:
109
120
  if close_session:
110
121
  session.close()
@@ -82,10 +82,6 @@ def serialize_xml(
82
82
  """Serialize a generated Python object as an XML string.
83
83
 
84
84
  Schemas may contain additional information for fine-tuned XML serialization.
85
-
86
- :param value: Generated value
87
- :param raw_schema: The payload definition with not resolved references.
88
- :param resolved_schema: The payload definition with all references resolved.
89
85
  """
90
86
  if isinstance(value, (bytes, str)):
91
87
  return {"data": value}
@@ -9,6 +9,7 @@ from schemathesis.core.rate_limit import ratelimit
9
9
  from schemathesis.core.transforms import merge_at
10
10
  from schemathesis.core.transport import Response
11
11
  from schemathesis.generation.case import Case
12
+ from schemathesis.generation.overrides import Override
12
13
  from schemathesis.python import wsgi
13
14
  from schemathesis.transport import BaseTransport, SerializationContext
14
15
  from schemathesis.transport.prepare import normalize_base_url, prepare_body, prepare_headers, prepare_path
@@ -99,6 +100,12 @@ class WSGITransport(BaseTransport["werkzeug.Client"]):
99
100
  request=requests.Request(**requests_kwargs).prepare(),
100
101
  elapsed=elapsed,
101
102
  verify=False,
103
+ _override=Override(
104
+ query=kwargs.get("params") or {},
105
+ headers=kwargs.get("headers") or {},
106
+ cookies=kwargs.get("cookies") or {},
107
+ path_parameters={},
108
+ ),
102
109
  )
103
110
 
104
111