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
@@ -1,8 +1,9 @@
1
1
  from __future__ import annotations
2
2
 
3
- from dataclasses import dataclass, field
3
+ from dataclasses import dataclass
4
4
  from typing import TYPE_CHECKING, Any, Mapping
5
5
 
6
+ from schemathesis import transport
6
7
  from schemathesis.checks import CHECKS, CheckContext, CheckFunction, run_checks
7
8
  from schemathesis.core import NOT_SET, SCHEMATHESIS_TEST_CASE_HEADER, NotSet, curl
8
9
  from schemathesis.core.failures import FailureGroup, failure_report_title, format_failures
@@ -11,42 +12,121 @@ from schemathesis.generation import generate_random_case_id
11
12
  from schemathesis.generation.meta import CaseMetadata
12
13
  from schemathesis.generation.overrides import Override, store_components
13
14
  from schemathesis.hooks import HookContext, dispatch
14
- from schemathesis.transport.prepare import prepare_request
15
+ from schemathesis.transport.prepare import prepare_path, prepare_request
15
16
 
16
17
  if TYPE_CHECKING:
18
+ import httpx
19
+ import requests
17
20
  import requests.auth
18
21
  from requests.structures import CaseInsensitiveDict
22
+ from werkzeug.test import TestResponse
19
23
 
20
24
  from schemathesis.schemas import APIOperation
21
25
 
22
26
 
27
+ def _default_headers() -> CaseInsensitiveDict:
28
+ from requests.structures import CaseInsensitiveDict
29
+
30
+ return CaseInsensitiveDict()
31
+
32
+
23
33
  @dataclass
24
34
  class Case:
25
- """A single test case parameters."""
35
+ """Generated test case data for a single API operation."""
26
36
 
27
37
  operation: APIOperation
28
38
  method: str
39
+ """HTTP verb (`GET`, `POST`, etc.)"""
29
40
  path: str
30
- # Unique test case identifier
31
- id: str = field(default_factory=generate_random_case_id, compare=False)
32
- path_parameters: dict[str, Any] | None = None
33
- headers: CaseInsensitiveDict | None = None
34
- cookies: dict[str, Any] | None = None
35
- query: dict[str, Any] | None = None
41
+ """Path template from schema (e.g., `/users/{user_id}`)"""
42
+ id: str
43
+ """Random ID sent in headers for log correlation"""
44
+ path_parameters: dict[str, Any]
45
+ """Generated path variables (e.g., `{"user_id": "123"}`)"""
46
+ headers: CaseInsensitiveDict
47
+ """Generated HTTP headers"""
48
+ cookies: dict[str, Any]
49
+ """Generated cookies"""
50
+ query: dict[str, Any]
51
+ """Generated query parameters"""
36
52
  # By default, there is no body, but we can't use `None` as the default value because it clashes with `null`
37
53
  # which is a valid payload.
38
- body: list | dict[str, Any] | str | int | float | bool | bytes | NotSet = NOT_SET
39
- # The media type for cases with a payload. For example, "application/json"
40
- media_type: str | None = None
54
+ body: list | dict[str, Any] | str | int | float | bool | bytes | NotSet
55
+ """Generated request body"""
56
+ media_type: str | None
57
+ """Media type from OpenAPI schema (e.g., "multipart/form-data")"""
41
58
 
42
- meta: CaseMetadata | None = field(compare=False, default=None)
59
+ meta: CaseMetadata | None
43
60
 
44
- _auth: requests.auth.AuthBase | None = None
45
- _has_explicit_auth: bool = False
61
+ _auth: requests.auth.AuthBase | None
62
+ _has_explicit_auth: bool
46
63
 
47
- def __post_init__(self) -> None:
64
+ __slots__ = (
65
+ "operation",
66
+ "method",
67
+ "path",
68
+ "id",
69
+ "path_parameters",
70
+ "headers",
71
+ "cookies",
72
+ "query",
73
+ "body",
74
+ "media_type",
75
+ "meta",
76
+ "_auth",
77
+ "_has_explicit_auth",
78
+ "_components",
79
+ )
80
+
81
+ def __init__(
82
+ self,
83
+ operation: APIOperation,
84
+ method: str,
85
+ path: str,
86
+ *,
87
+ id: str | None = None,
88
+ path_parameters: dict[str, Any] | None = None,
89
+ headers: CaseInsensitiveDict | None = None,
90
+ cookies: dict[str, Any] | None = None,
91
+ query: dict[str, Any] | None = None,
92
+ body: list | dict[str, Any] | str | int | float | bool | bytes | "NotSet" = NOT_SET,
93
+ media_type: str | None = None,
94
+ meta: CaseMetadata | None = None,
95
+ _auth: requests.auth.AuthBase | None = None,
96
+ _has_explicit_auth: bool = False,
97
+ ) -> None:
98
+ self.operation = operation
99
+ self.method = method
100
+ self.path = path
101
+
102
+ self.id = id if id is not None else generate_random_case_id()
103
+ self.path_parameters = path_parameters if path_parameters is not None else {}
104
+ self.headers = headers if headers is not None else _default_headers()
105
+ self.cookies = cookies if cookies is not None else {}
106
+ self.query = query if query is not None else {}
107
+ self.body = body
108
+ self.media_type = media_type
109
+ self.meta = meta
110
+ self._auth = _auth
111
+ self._has_explicit_auth = _has_explicit_auth
48
112
  self._components = store_components(self)
49
113
 
114
+ def __eq__(self, other: object) -> bool:
115
+ if not isinstance(other, Case):
116
+ return NotImplemented
117
+
118
+ return (
119
+ self.operation == other.operation
120
+ and self.method == other.method
121
+ and self.path == other.path
122
+ and self.path_parameters == other.path_parameters
123
+ and self.headers == other.headers
124
+ and self.cookies == other.cookies
125
+ and self.query == other.query
126
+ and self.body == other.body
127
+ and self.media_type == other.media_type
128
+ )
129
+
50
130
  @property
51
131
  def _override(self) -> Override:
52
132
  return Override.from_components(self._components, self)
@@ -56,6 +136,8 @@ class Case:
56
136
  first = True
57
137
  for name in ("path_parameters", "headers", "cookies", "query", "body"):
58
138
  value = getattr(self, name)
139
+ if name != "body" and not value:
140
+ continue
59
141
  if value is not None and not isinstance(value, NotSet):
60
142
  if first:
61
143
  first = False
@@ -69,8 +151,19 @@ class Case:
69
151
 
70
152
  def _repr_pretty_(self, *args: Any, **kwargs: Any) -> None: ...
71
153
 
154
+ @property
155
+ def formatted_path(self) -> str:
156
+ """Path template with variables substituted (e.g., /users/{user_id} → /users/123)."""
157
+ return prepare_path(self.path, self.path_parameters)
158
+
72
159
  def as_curl_command(self, headers: Mapping[str, Any] | None = None, verify: bool = True) -> str:
73
- """Construct a curl command for a given case."""
160
+ """Generate a curl command that reproduces this test case.
161
+
162
+ Args:
163
+ headers: Additional headers to include in the command.
164
+ verify: When False, adds `--insecure` flag to curl command.
165
+
166
+ """
74
167
  request_data = prepare_request(self, headers, config=self.operation.schema.config.output.sanitization)
75
168
  return curl.generate(
76
169
  method=str(request_data.method),
@@ -82,7 +175,6 @@ class Case:
82
175
  )
83
176
 
84
177
  def as_transport_kwargs(self, base_url: str | None = None, headers: dict[str, str] | None = None) -> dict[str, Any]:
85
- """Convert the test case into a dictionary acceptable by the underlying transport call."""
86
178
  return self.operation.schema.transport.serialize_case(self, base_url=base_url, headers=headers)
87
179
 
88
180
  def call(
@@ -94,11 +186,28 @@ class Case:
94
186
  cookies: dict[str, Any] | None = None,
95
187
  **kwargs: Any,
96
188
  ) -> Response:
189
+ """Make an HTTP request using this test case's data without validation.
190
+
191
+ Use when you need to validate response separately
192
+
193
+ Args:
194
+ base_url: Override the schema's base URL.
195
+ session: Reuse an existing requests session.
196
+ headers: Additional headers.
197
+ params: Additional query parameters.
198
+ cookies: Additional cookies.
199
+ **kwargs: Additional transport-level arguments.
200
+
201
+ """
97
202
  hook_context = HookContext(operation=self.operation)
98
203
  dispatch("before_call", hook_context, self, **kwargs)
99
204
  if self.operation.app is not None:
100
- kwargs["app"] = self.operation.app
101
- response = self.operation.schema.transport.send(
205
+ kwargs.setdefault("app", self.operation.app)
206
+ if "app" in kwargs:
207
+ transport_ = transport.get(kwargs["app"])
208
+ else:
209
+ transport_ = self.operation.schema.transport
210
+ response = transport_.send(
102
211
  self,
103
212
  session=session,
104
213
  base_url=base_url,
@@ -112,26 +221,29 @@ class Case:
112
221
 
113
222
  def validate_response(
114
223
  self,
115
- response: Response,
224
+ response: Response | httpx.Response | requests.Response | TestResponse,
116
225
  checks: list[CheckFunction] | None = None,
117
226
  additional_checks: list[CheckFunction] | None = None,
118
227
  excluded_checks: list[CheckFunction] | None = None,
119
228
  headers: dict[str, Any] | None = None,
120
229
  transport_kwargs: dict[str, Any] | None = None,
121
230
  ) -> None:
122
- """Validate application response.
231
+ """Validate a response against the API schema and built-in checks.
123
232
 
124
- By default, all available checks will be applied.
233
+ Args:
234
+ response: Response to validate.
235
+ checks: Explicit set of checks to run.
236
+ additional_checks: Additional custom checks to run.
237
+ excluded_checks: Built-in checks to skip.
238
+ headers: Headers used in the original request.
239
+ transport_kwargs: Transport arguments used in the original request.
125
240
 
126
- :param response: Application response.
127
- :param checks: A tuple of check functions that accept ``response`` and ``case``.
128
- :param additional_checks: A tuple of additional checks that will be executed after ones from the ``checks``
129
- argument.
130
- :param excluded_checks: Checks excluded from the default ones.
131
241
  """
132
242
  __tracebackhide__ = True
133
243
  from requests.structures import CaseInsensitiveDict
134
244
 
245
+ response = Response.from_any(response)
246
+
135
247
  checks = [
136
248
  check
137
249
  for check in list(checks or CHECKS.get_all()) + list(additional_checks or [])
@@ -180,6 +292,18 @@ class Case:
180
292
  excluded_checks: list[CheckFunction] | None = None,
181
293
  **kwargs: Any,
182
294
  ) -> Response:
295
+ """Make an HTTP request and validates the response automatically.
296
+
297
+ Args:
298
+ base_url: Override the schema's base URL.
299
+ session: Reuse an existing requests session.
300
+ headers: Additional headers to send.
301
+ checks: Explicit set of checks to run.
302
+ additional_checks: Additional custom checks to run.
303
+ excluded_checks: Built-in checks to skip.
304
+ **kwargs: Additional transport-level arguments.
305
+
306
+ """
183
307
  __tracebackhide__ = True
184
308
  response = self.call(base_url, session, headers, **kwargs)
185
309
  self.validate_response(
@@ -103,11 +103,11 @@ def create_test(
103
103
  if config.settings is not None:
104
104
  # Merge the user-provided settings with the current ones
105
105
  settings = hypothesis.settings(
106
- settings,
106
+ config.settings,
107
107
  **{
108
- item: getattr(config.settings, item)
108
+ item: getattr(settings, item)
109
109
  for item in all_settings
110
- if getattr(config.settings, item) != getattr(default, item)
110
+ if getattr(settings, item) != getattr(default, item)
111
111
  },
112
112
  )
113
113
 
@@ -475,7 +475,7 @@ def _iter_coverage_cases(
475
475
  data = template.with_body(value=value, media_type=body.media_type)
476
476
  yield operation.Case(
477
477
  **data.kwargs,
478
- meta=CaseMetadata(
478
+ _meta=CaseMetadata(
479
479
  generation=GenerationInfo(
480
480
  time=elapsed,
481
481
  mode=value.generation_mode,
@@ -497,7 +497,7 @@ def _iter_coverage_cases(
497
497
  data = template.with_body(value=next_value, media_type=body.media_type)
498
498
  yield operation.Case(
499
499
  **data.kwargs,
500
- meta=CaseMetadata(
500
+ _meta=CaseMetadata(
501
501
  generation=GenerationInfo(
502
502
  time=instant.elapsed,
503
503
  mode=next_value.generation_mode,
@@ -518,7 +518,7 @@ def _iter_coverage_cases(
518
518
  seen_positive.insert(data.kwargs)
519
519
  yield operation.Case(
520
520
  **data.kwargs,
521
- meta=CaseMetadata(
521
+ _meta=CaseMetadata(
522
522
  generation=GenerationInfo(
523
523
  time=template_time,
524
524
  mode=GenerationMode.POSITIVE,
@@ -546,7 +546,7 @@ def _iter_coverage_cases(
546
546
 
547
547
  yield operation.Case(
548
548
  **data.kwargs,
549
- meta=CaseMetadata(
549
+ _meta=CaseMetadata(
550
550
  generation=GenerationInfo(time=instant.elapsed, mode=value.generation_mode),
551
551
  components=data.components,
552
552
  phase=PhaseInfo.coverage(
@@ -566,7 +566,7 @@ def _iter_coverage_cases(
566
566
  yield operation.Case(
567
567
  **data.kwargs,
568
568
  method=method.upper(),
569
- meta=CaseMetadata(
569
+ _meta=CaseMetadata(
570
570
  generation=GenerationInfo(time=instant.elapsed, mode=GenerationMode.NEGATIVE),
571
571
  components=data.components,
572
572
  phase=PhaseInfo.coverage(description=f"Unspecified HTTP method: {method.upper()}"),
@@ -588,7 +588,7 @@ def _iter_coverage_cases(
588
588
  )
589
589
  yield operation.Case(
590
590
  **data.kwargs,
591
- meta=CaseMetadata(
591
+ _meta=CaseMetadata(
592
592
  generation=GenerationInfo(time=instant.elapsed, mode=GenerationMode.NEGATIVE),
593
593
  components=data.components,
594
594
  phase=PhaseInfo.coverage(
@@ -613,7 +613,7 @@ def _iter_coverage_cases(
613
613
  )
614
614
  yield operation.Case(
615
615
  **data.kwargs,
616
- meta=CaseMetadata(
616
+ _meta=CaseMetadata(
617
617
  generation=GenerationInfo(time=instant.elapsed, mode=GenerationMode.NEGATIVE),
618
618
  components=data.components,
619
619
  phase=PhaseInfo.coverage(
@@ -655,7 +655,7 @@ def _iter_coverage_cases(
655
655
  )
656
656
  return operation.Case(
657
657
  **data.kwargs,
658
- meta=CaseMetadata(
658
+ _meta=CaseMetadata(
659
659
  generation=GenerationInfo(
660
660
  time=_instant.elapsed,
661
661
  mode=_generation_mode,
@@ -781,7 +781,7 @@ def _case_to_kwargs(case: Case) -> dict:
781
781
  kwargs = {}
782
782
  for container_name in LOCATION_TO_CONTAINER.values():
783
783
  value = getattr(case, container_name)
784
- if isinstance(value, CaseInsensitiveDict):
784
+ if isinstance(value, CaseInsensitiveDict) and value:
785
785
  kwargs[container_name] = dict(value)
786
786
  elif value and value is not NOT_SET:
787
787
  kwargs[container_name] = value
@@ -2,17 +2,15 @@ from __future__ import annotations
2
2
 
3
3
  from collections.abc import Mapping
4
4
  from dataclasses import dataclass
5
- from typing import TYPE_CHECKING, Any, Callable
5
+ from typing import TYPE_CHECKING, Any, Iterator
6
6
 
7
7
  from schemathesis.config import ProjectConfig
8
- from schemathesis.core.errors import IncorrectUsage
9
- from schemathesis.core.marks import Mark
10
8
  from schemathesis.core.transforms import diff
11
9
  from schemathesis.generation.meta import ComponentKind
12
10
 
13
11
  if TYPE_CHECKING:
14
12
  from schemathesis.generation.case import Case
15
- from schemathesis.schemas import APIOperation, Parameter, ParameterSet
13
+ from schemathesis.schemas import APIOperation, Parameter
16
14
 
17
15
 
18
16
  @dataclass
@@ -24,13 +22,15 @@ class Override:
24
22
  cookies: dict[str, str]
25
23
  path_parameters: dict[str, str]
26
24
 
27
- def for_operation(self, operation: APIOperation) -> dict[str, dict[str, str]]:
28
- return {
29
- "query": (_for_parameters(self.query, operation.query)),
30
- "headers": (_for_parameters(self.headers, operation.headers)),
31
- "cookies": (_for_parameters(self.cookies, operation.cookies)),
32
- "path_parameters": (_for_parameters(self.path_parameters, operation.path_parameters)),
33
- }
25
+ def items(self) -> Iterator[tuple[str, dict[str, str]]]:
26
+ for key, value in (
27
+ ("query", self.query),
28
+ ("headers", self.headers),
29
+ ("cookies", self.cookies),
30
+ ("path_parameters", self.path_parameters),
31
+ ):
32
+ if value:
33
+ yield key, value
34
34
 
35
35
  @classmethod
36
36
  def from_components(cls, components: dict[ComponentKind, StoredValue], case: Case) -> Override:
@@ -77,14 +77,6 @@ def _get_override_value(param: Parameter, parameters: dict[str, Any]) -> Any:
77
77
  return None
78
78
 
79
79
 
80
- def _for_parameters(overridden: dict[str, str], defined: ParameterSet) -> dict[str, str]:
81
- output = {}
82
- for param in defined:
83
- if param.name in overridden:
84
- output[param.name] = overridden[param.name]
85
- return output
86
-
87
-
88
80
  @dataclass
89
81
  class StoredValue:
90
82
  value: dict[str, Any] | None
@@ -122,11 +114,3 @@ def store_components(case: Case) -> dict[ComponentKind, StoredValue]:
122
114
  ComponentKind.PATH_PARAMETERS,
123
115
  ]
124
116
  }
125
-
126
-
127
- OverrideMark = Mark[Override](attr_name="override")
128
-
129
-
130
- def check_no_override_mark(test: Callable) -> None:
131
- if OverrideMark.is_set(test):
132
- raise IncorrectUsage(f"`{test.__name__}` has already been decorated with `override`.")
@@ -7,6 +7,19 @@ if TYPE_CHECKING:
7
7
 
8
8
  from schemathesis.generation.stateful.state_machine import APIStateMachine
9
9
 
10
+ __all__ = [
11
+ "APIStateMachine",
12
+ ]
13
+
14
+
15
+ def __getattr__(name: str) -> type[APIStateMachine]:
16
+ if name == "APIStateMachine":
17
+ from schemathesis.generation.stateful.state_machine import APIStateMachine
18
+
19
+ return APIStateMachine
20
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
21
+
22
+
10
23
  STATEFUL_TESTS_LABEL = "Stateful tests"
11
24
 
12
25
 
@@ -129,9 +129,10 @@ def _normalize_name(name: str) -> str:
129
129
 
130
130
 
131
131
  class APIStateMachine(RuleBasedStateMachine):
132
- """The base class for state machines generated from API schemas.
132
+ """State machine for executing API operation sequences based on OpenAPI links.
133
133
 
134
- Exposes additional extension points in the testing process.
134
+ Automatically generates test scenarios by chaining API operations according
135
+ to their defined relationships in the schema.
135
136
  """
136
137
 
137
138
  # This is a convenience attribute, which happened to clash with `RuleBasedStateMachine` instance level attribute
@@ -193,17 +194,22 @@ class APIStateMachine(RuleBasedStateMachine):
193
194
 
194
195
  @classmethod
195
196
  def run(cls, *, settings: hypothesis.settings | None = None) -> None:
196
- """Run state machine as a test."""
197
+ """Execute the state machine test scenarios.
198
+
199
+ Args:
200
+ settings: Hypothesis settings for test execution.
201
+
202
+ """
197
203
  from . import run_state_machine_as_test
198
204
 
199
205
  __tracebackhide__ = True
200
206
  return run_state_machine_as_test(cls, settings=settings)
201
207
 
202
208
  def setup(self) -> None:
203
- """Hook method that runs unconditionally in the beginning of each test scenario."""
209
+ """Called once at the beginning of each test scenario."""
204
210
 
205
211
  def teardown(self) -> None:
206
- pass
212
+ """Called once at the end of each test scenario."""
207
213
 
208
214
  # To provide the return type in the rendered documentation
209
215
  teardown.__doc__ = RuleBasedStateMachine.teardown.__doc__
@@ -213,14 +219,6 @@ class APIStateMachine(RuleBasedStateMachine):
213
219
  return self.step(input)
214
220
 
215
221
  def step(self, input: StepInput) -> StepOutput:
216
- """A single state machine step.
217
-
218
- :param Case case: Generated test case data that should be sent in an API call to the tested API operation.
219
- :param previous: Optional result from the previous step and the direction in which this step should be done.
220
-
221
- Schemathesis prepares data, makes a call and validates the received response.
222
- It is the most high-level point to extend the testing process. You probably don't need it in most cases.
223
- """
224
222
  __tracebackhide__ = True
225
223
  self.before_call(input.case)
226
224
  kwargs = self.get_call_kwargs(input.case)
@@ -230,126 +228,51 @@ class APIStateMachine(RuleBasedStateMachine):
230
228
  return StepOutput(response, input.case)
231
229
 
232
230
  def before_call(self, case: Case) -> None:
233
- """Hook method for modifying the case data before making a request.
234
-
235
- :param Case case: Generated test case data that should be sent in an API call to the tested API operation.
236
-
237
- Use it if you want to inject static data, for example,
238
- a query parameter that should always be used in API calls:
231
+ """Called before each API operation in the scenario.
239
232
 
240
- .. code-block:: python
233
+ Args:
234
+ case: Test case data for the operation.
241
235
 
242
- class APIWorkflow(schema.as_state_machine()):
243
- def before_call(self, case):
244
- case.query = case.query or {}
245
- case.query["test"] = "true"
246
-
247
- You can also modify data only for some operations:
248
-
249
- .. code-block:: python
250
-
251
- class APIWorkflow(schema.as_state_machine()):
252
- def before_call(self, case):
253
- if case.method == "PUT" and case.path == "/items":
254
- case.body["is_fake"] = True
255
236
  """
256
237
 
257
238
  def after_call(self, response: Response, case: Case) -> None:
258
- """Hook method for additional actions with case or response instances.
259
-
260
- :param response: Response from the application under test.
261
- :param Case case: Generated test case data that should be sent in an API call to the tested API operation.
262
-
263
- For example, you can log all response statuses by using this hook:
264
-
265
- .. code-block:: python
266
-
267
- import logging
239
+ """Called after each API operation in the scenario.
268
240
 
269
- logger = logging.getLogger(__file__)
270
- logger.setLevel(logging.INFO)
241
+ Args:
242
+ response: HTTP response from the operation.
243
+ case: Test case data that was executed.
271
244
 
272
-
273
- class APIWorkflow(schema.as_state_machine()):
274
- def after_call(self, response, case):
275
- logger.info(
276
- "%s %s -> %d",
277
- case.method,
278
- case.path,
279
- response.status_code,
280
- )
281
-
282
-
283
- # POST /users/ -> 201
284
- # GET /users/{user_id} -> 200
285
- # PATCH /users/{user_id} -> 200
286
- # GET /users/{user_id} -> 200
287
- # PATCH /users/{user_id} -> 500
288
245
  """
289
246
 
290
247
  def call(self, case: Case, **kwargs: Any) -> Response:
291
- """Make a request to the API.
292
-
293
- :param Case case: Generated test case data that should be sent in an API call to the tested API operation.
294
- :param kwargs: Keyword arguments that will be passed to the appropriate ``case.call_*`` method.
295
- :return: Response from the application under test.
296
-
297
- Note that WSGI/ASGI applications are detected automatically in this method. Depending on the result of this
298
- detection the state machine will call the ``call`` method.
299
-
300
- Usually, you don't need to override this method unless you are building a different state machine on top of this
301
- one and want to customize the transport layer itself.
302
- """
303
248
  return case.call(**kwargs)
304
249
 
305
250
  def get_call_kwargs(self, case: Case) -> dict[str, Any]:
306
- """Create custom keyword arguments that will be passed to the :meth:`Case.call` method.
307
-
308
- Mostly they are proxied to the :func:`requests.request` call.
251
+ """Returns keyword arguments for the API call.
309
252
 
310
- :param Case case: Generated test case data that should be sent in an API call to the tested API operation.
253
+ Args:
254
+ case: Test case being executed.
311
255
 
312
- .. code-block:: python
256
+ Returns:
257
+ Dictionary passed to the `case.call()` method.
313
258
 
314
- class APIWorkflow(schema.as_state_machine()):
315
- def get_call_kwargs(self, case):
316
- return {"verify": False}
317
-
318
- The above example disables the server's TLS certificate verification.
319
259
  """
320
260
  return {}
321
261
 
322
262
  def validate_response(
323
263
  self, response: Response, case: Case, additional_checks: list[CheckFunction] | None = None, **kwargs: Any
324
264
  ) -> None:
325
- """Validate an API response.
326
-
327
- :param response: Response from the application under test.
328
- :param Case case: Generated test case data that should be sent in an API call to the tested API operation.
329
- :param additional_checks: A list of checks that will be run together with the default ones.
330
- :raises FailureGroup: If any of the supplied checks failed.
331
-
332
- If you need to change the default checks or provide custom validation rules, you can do it here.
333
-
334
- .. code-block:: python
335
-
336
- def my_check(response, case):
337
- ... # some assertions
338
-
339
-
340
- class APIWorkflow(schema.as_state_machine()):
341
- def validate_response(self, response, case):
342
- case.validate_response(response, checks=(my_check,))
265
+ """Validates the API response using configured checks.
343
266
 
344
- The state machine from the example above will execute only the ``my_check`` check instead of all
345
- available checks.
267
+ Args:
268
+ response: HTTP response to validate.
269
+ case: Test case that generated the response.
270
+ additional_checks: Extra validation functions to run.
271
+ kwargs: Transport-level keyword arguments.
346
272
 
347
- Each check function should accept ``response`` as the first argument and ``case`` as the second one and raise
348
- ``AssertionError`` if the check fails.
273
+ Raises:
274
+ FailureGroup: When validation checks fail.
349
275
 
350
- **Note** that it is preferred to pass check functions as an argument to ``case.validate_response``.
351
- In this case, all checks will be executed, and you'll receive a grouped exception that contains results from
352
- all provided checks rather than only the first encountered exception.
353
276
  """
354
277
  __tracebackhide__ = True
355
278
  case.validate_response(response, additional_checks=additional_checks, transport_kwargs=kwargs)