schemathesis 4.0.0a1__py3-none-any.whl → 4.0.0a2__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.
- schemathesis/checks.py +6 -4
- schemathesis/cli/commands/run/__init__.py +4 -4
- schemathesis/cli/commands/run/events.py +4 -9
- schemathesis/cli/commands/run/executor.py +6 -3
- schemathesis/cli/commands/run/filters.py +27 -19
- schemathesis/cli/commands/run/handlers/base.py +1 -1
- schemathesis/cli/commands/run/handlers/cassettes.py +1 -3
- schemathesis/cli/commands/run/handlers/output.py +765 -143
- schemathesis/cli/commands/run/validation.py +1 -1
- schemathesis/cli/ext/options.py +4 -1
- schemathesis/core/failures.py +54 -24
- schemathesis/engine/core.py +1 -1
- schemathesis/engine/events.py +3 -97
- schemathesis/engine/phases/stateful/__init__.py +1 -0
- schemathesis/engine/phases/stateful/_executor.py +19 -44
- schemathesis/engine/phases/unit/__init__.py +1 -0
- schemathesis/engine/phases/unit/_executor.py +2 -1
- schemathesis/engine/phases/unit/_pool.py +1 -1
- schemathesis/engine/recorder.py +8 -3
- schemathesis/generation/stateful/state_machine.py +53 -36
- schemathesis/graphql/checks.py +3 -9
- schemathesis/openapi/checks.py +8 -33
- schemathesis/schemas.py +34 -14
- schemathesis/specs/graphql/schemas.py +16 -15
- schemathesis/specs/openapi/expressions/__init__.py +11 -15
- schemathesis/specs/openapi/expressions/nodes.py +20 -20
- schemathesis/specs/openapi/links.py +126 -119
- schemathesis/specs/openapi/schemas.py +18 -22
- schemathesis/specs/openapi/stateful/__init__.py +77 -55
- {schemathesis-4.0.0a1.dist-info → schemathesis-4.0.0a2.dist-info}/METADATA +1 -1
- {schemathesis-4.0.0a1.dist-info → schemathesis-4.0.0a2.dist-info}/RECORD +34 -35
- schemathesis/specs/openapi/expressions/context.py +0 -14
- {schemathesis-4.0.0a1.dist-info → schemathesis-4.0.0a2.dist-info}/WHEEL +0 -0
- {schemathesis-4.0.0a1.dist-info → schemathesis-4.0.0a2.dist-info}/entry_points.txt +0 -0
- {schemathesis-4.0.0a1.dist-info → schemathesis-4.0.0a2.dist-info}/licenses/LICENSE +0 -0
@@ -11,6 +11,7 @@ from hypothesis.stateful import RuleBasedStateMachine
|
|
11
11
|
|
12
12
|
from schemathesis.checks import CheckFunction
|
13
13
|
from schemathesis.core.errors import IncorrectUsage
|
14
|
+
from schemathesis.core.result import Result
|
14
15
|
from schemathesis.core.transport import Response
|
15
16
|
from schemathesis.generation.case import Case
|
16
17
|
|
@@ -18,7 +19,7 @@ if TYPE_CHECKING:
|
|
18
19
|
import hypothesis
|
19
20
|
from requests.structures import CaseInsensitiveDict
|
20
21
|
|
21
|
-
from schemathesis.schemas import
|
22
|
+
from schemathesis.schemas import BaseSchema
|
22
23
|
|
23
24
|
|
24
25
|
NO_LINKS_ERROR_MESSAGE = (
|
@@ -36,12 +37,51 @@ DEFAULT_STATE_MACHINE_SETTINGS = hypothesis.settings(
|
|
36
37
|
|
37
38
|
|
38
39
|
@dataclass
|
39
|
-
class
|
40
|
+
class StepInput:
|
41
|
+
"""Input for a single state machine step."""
|
42
|
+
|
43
|
+
case: Case
|
44
|
+
transition: Transition | None # None for initial steps
|
45
|
+
|
46
|
+
__slots__ = ("case", "transition")
|
47
|
+
|
48
|
+
@classmethod
|
49
|
+
def initial(cls, case: Case) -> StepInput:
|
50
|
+
return cls(case=case, transition=None)
|
51
|
+
|
52
|
+
|
53
|
+
@dataclass
|
54
|
+
class Transition:
|
55
|
+
"""Data about transition execution."""
|
56
|
+
|
57
|
+
# ID of the transition (e.g. link name)
|
58
|
+
id: str
|
59
|
+
parent_id: str
|
60
|
+
parameters: dict[str, dict[str, ExtractedParam]]
|
61
|
+
request_body: ExtractedParam | None
|
62
|
+
|
63
|
+
__slots__ = ("id", "parent_id", "parameters", "request_body")
|
64
|
+
|
65
|
+
|
66
|
+
@dataclass
|
67
|
+
class ExtractedParam:
|
68
|
+
"""Result of parameter extraction."""
|
69
|
+
|
70
|
+
definition: Any
|
71
|
+
value: Result[Any, Exception]
|
72
|
+
|
73
|
+
__slots__ = ("definition", "value")
|
74
|
+
|
75
|
+
|
76
|
+
@dataclass
|
77
|
+
class StepOutput:
|
40
78
|
"""Output from a single transition of a state machine."""
|
41
79
|
|
42
80
|
response: Response
|
43
81
|
case: Case
|
44
82
|
|
83
|
+
__slots__ = ("response", "case")
|
84
|
+
|
45
85
|
|
46
86
|
def _normalize_name(name: str) -> str:
|
47
87
|
return re.sub(r"\W|^(?=\d)", "_", name).replace("__", "_")
|
@@ -89,10 +129,10 @@ class APIStateMachine(RuleBasedStateMachine):
|
|
89
129
|
target = _normalize_name(target)
|
90
130
|
return super()._new_name(target) # type: ignore
|
91
131
|
|
92
|
-
def _get_target_for_result(self, result:
|
132
|
+
def _get_target_for_result(self, result: StepOutput) -> str | None:
|
93
133
|
raise NotImplementedError
|
94
134
|
|
95
|
-
def _add_result_to_targets(self, targets: tuple[str, ...], result:
|
135
|
+
def _add_result_to_targets(self, targets: tuple[str, ...], result: StepOutput | None) -> None:
|
96
136
|
if result is None:
|
97
137
|
return
|
98
138
|
target = self._get_target_for_result(result)
|
@@ -115,19 +155,11 @@ class APIStateMachine(RuleBasedStateMachine):
|
|
115
155
|
# To provide the return type in the rendered documentation
|
116
156
|
teardown.__doc__ = RuleBasedStateMachine.teardown.__doc__
|
117
157
|
|
118
|
-
def
|
119
|
-
raise NotImplementedError
|
120
|
-
|
121
|
-
def _step(self, case: Case, previous: StepResult | None = None, link: Direction | None = None) -> StepResult | None:
|
122
|
-
# This method is a proxy that is used under the hood during the state machine initialization.
|
123
|
-
# The whole point of having it is to make it possible to override `step`; otherwise, custom "step" is ignored.
|
124
|
-
# It happens because, at the point of initialization, the final class is not yet created.
|
158
|
+
def _step(self, input: StepInput) -> StepOutput | None:
|
125
159
|
__tracebackhide__ = True
|
126
|
-
|
127
|
-
return self.step(case, (previous, link))
|
128
|
-
return self.step(case, None)
|
160
|
+
return self.step(input)
|
129
161
|
|
130
|
-
def step(self,
|
162
|
+
def step(self, input: StepInput) -> StepOutput:
|
131
163
|
"""A single state machine step.
|
132
164
|
|
133
165
|
:param Case case: Generated test case data that should be sent in an API call to the tested API operation.
|
@@ -137,15 +169,12 @@ class APIStateMachine(RuleBasedStateMachine):
|
|
137
169
|
It is the most high-level point to extend the testing process. You probably don't need it in most cases.
|
138
170
|
"""
|
139
171
|
__tracebackhide__ = True
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
self.
|
144
|
-
|
145
|
-
response
|
146
|
-
self.after_call(response, case)
|
147
|
-
self.validate_response(response, case)
|
148
|
-
return self.store_result(response, case)
|
172
|
+
self.before_call(input.case)
|
173
|
+
kwargs = self.get_call_kwargs(input.case)
|
174
|
+
response = self.call(input.case, **kwargs)
|
175
|
+
self.after_call(response, input.case)
|
176
|
+
self.validate_response(response, input.case)
|
177
|
+
return StepOutput(response, input.case)
|
149
178
|
|
150
179
|
def before_call(self, case: Case) -> None:
|
151
180
|
"""Hook method for modifying the case data before making a request.
|
@@ -271,15 +300,3 @@ class APIStateMachine(RuleBasedStateMachine):
|
|
271
300
|
"""
|
272
301
|
__tracebackhide__ = True
|
273
302
|
case.validate_response(response, additional_checks=additional_checks)
|
274
|
-
|
275
|
-
def store_result(self, response: Response, case: Case) -> StepResult:
|
276
|
-
return StepResult(response, case)
|
277
|
-
|
278
|
-
|
279
|
-
class Direction:
|
280
|
-
name: str
|
281
|
-
status_code: str
|
282
|
-
operation: APIOperation
|
283
|
-
|
284
|
-
def set_data(self, case: Case, **kwargs: Any) -> None:
|
285
|
-
raise NotImplementedError
|
schemathesis/graphql/checks.py
CHANGED
@@ -11,7 +11,7 @@ if TYPE_CHECKING:
|
|
11
11
|
class UnexpectedGraphQLResponse(Failure):
|
12
12
|
"""GraphQL response is not a JSON object."""
|
13
13
|
|
14
|
-
__slots__ = ("operation", "type_name", "title", "message", "
|
14
|
+
__slots__ = ("operation", "type_name", "title", "message", "case_id", "severity")
|
15
15
|
|
16
16
|
def __init__(
|
17
17
|
self,
|
@@ -20,14 +20,12 @@ class UnexpectedGraphQLResponse(Failure):
|
|
20
20
|
type_name: str,
|
21
21
|
title: str = "Unexpected GraphQL Response",
|
22
22
|
message: str,
|
23
|
-
code: str = "graphql_unexpected_response",
|
24
23
|
case_id: str | None = None,
|
25
24
|
) -> None:
|
26
25
|
self.operation = operation
|
27
26
|
self.type_name = type_name
|
28
27
|
self.title = title
|
29
28
|
self.message = message
|
30
|
-
self.code = code
|
31
29
|
self.case_id = case_id
|
32
30
|
self.severity = Severity.MEDIUM
|
33
31
|
|
@@ -39,7 +37,7 @@ class UnexpectedGraphQLResponse(Failure):
|
|
39
37
|
class GraphQLClientError(Failure):
|
40
38
|
"""GraphQL query has not been executed."""
|
41
39
|
|
42
|
-
__slots__ = ("operation", "errors", "title", "message", "
|
40
|
+
__slots__ = ("operation", "errors", "title", "message", "case_id", "_unique_key_cache", "severity")
|
43
41
|
|
44
42
|
def __init__(
|
45
43
|
self,
|
@@ -48,14 +46,12 @@ class GraphQLClientError(Failure):
|
|
48
46
|
message: str,
|
49
47
|
errors: list[GraphQLFormattedError],
|
50
48
|
title: str = "GraphQL client error",
|
51
|
-
code: str = "graphql_client_error",
|
52
49
|
case_id: str | None = None,
|
53
50
|
) -> None:
|
54
51
|
self.operation = operation
|
55
52
|
self.errors = errors
|
56
53
|
self.title = title
|
57
54
|
self.message = message
|
58
|
-
self.code = code
|
59
55
|
self.case_id = case_id
|
60
56
|
self._unique_key_cache: str | None = None
|
61
57
|
self.severity = Severity.MEDIUM
|
@@ -70,7 +66,7 @@ class GraphQLClientError(Failure):
|
|
70
66
|
class GraphQLServerError(Failure):
|
71
67
|
"""GraphQL response indicates at least one server error."""
|
72
68
|
|
73
|
-
__slots__ = ("operation", "errors", "title", "message", "
|
69
|
+
__slots__ = ("operation", "errors", "title", "message", "case_id", "_unique_key_cache", "severity")
|
74
70
|
|
75
71
|
def __init__(
|
76
72
|
self,
|
@@ -79,14 +75,12 @@ class GraphQLServerError(Failure):
|
|
79
75
|
message: str,
|
80
76
|
errors: list[GraphQLFormattedError],
|
81
77
|
title: str = "GraphQL server error",
|
82
|
-
code: str = "graphql_server_error",
|
83
78
|
case_id: str | None = None,
|
84
79
|
) -> None:
|
85
80
|
self.operation = operation
|
86
81
|
self.errors = errors
|
87
82
|
self.title = title
|
88
83
|
self.message = message
|
89
|
-
self.code = code
|
90
84
|
self.case_id = case_id
|
91
85
|
self._unique_key_cache: str | None = None
|
92
86
|
self.severity = Severity.CRITICAL
|
schemathesis/openapi/checks.py
CHANGED
@@ -37,7 +37,6 @@ class UndefinedStatusCode(Failure):
|
|
37
37
|
"allowed_status_codes",
|
38
38
|
"message",
|
39
39
|
"title",
|
40
|
-
"code",
|
41
40
|
"case_id",
|
42
41
|
"severity",
|
43
42
|
)
|
@@ -51,7 +50,6 @@ class UndefinedStatusCode(Failure):
|
|
51
50
|
allowed_status_codes: list[int],
|
52
51
|
message: str,
|
53
52
|
title: str = "Undocumented HTTP status code",
|
54
|
-
code: str = "undefined_status_code",
|
55
53
|
case_id: str | None = None,
|
56
54
|
) -> None:
|
57
55
|
self.operation = operation
|
@@ -60,7 +58,6 @@ class UndefinedStatusCode(Failure):
|
|
60
58
|
self.allowed_status_codes = allowed_status_codes
|
61
59
|
self.message = message
|
62
60
|
self.title = title
|
63
|
-
self.code = code
|
64
61
|
self.case_id = case_id
|
65
62
|
self.severity = Severity.MEDIUM
|
66
63
|
|
@@ -72,7 +69,7 @@ class UndefinedStatusCode(Failure):
|
|
72
69
|
class MissingHeaders(Failure):
|
73
70
|
"""Some required headers are missing."""
|
74
71
|
|
75
|
-
__slots__ = ("operation", "missing_headers", "message", "title", "
|
72
|
+
__slots__ = ("operation", "missing_headers", "message", "title", "case_id", "severity")
|
76
73
|
|
77
74
|
def __init__(
|
78
75
|
self,
|
@@ -81,14 +78,12 @@ class MissingHeaders(Failure):
|
|
81
78
|
missing_headers: list[str],
|
82
79
|
message: str,
|
83
80
|
title: str = "Missing required headers",
|
84
|
-
code: str = "missing_headers",
|
85
81
|
case_id: str | None = None,
|
86
82
|
) -> None:
|
87
83
|
self.operation = operation
|
88
84
|
self.missing_headers = missing_headers
|
89
85
|
self.message = message
|
90
86
|
self.title = title
|
91
|
-
self.code = code
|
92
87
|
self.case_id = case_id
|
93
88
|
self.severity = Severity.MEDIUM
|
94
89
|
|
@@ -105,7 +100,6 @@ class JsonSchemaError(Failure):
|
|
105
100
|
"instance",
|
106
101
|
"message",
|
107
102
|
"title",
|
108
|
-
"code",
|
109
103
|
"case_id",
|
110
104
|
"severity",
|
111
105
|
)
|
@@ -121,7 +115,6 @@ class JsonSchemaError(Failure):
|
|
121
115
|
instance: None | bool | float | str | list | dict[str, Any],
|
122
116
|
message: str,
|
123
117
|
title: str = "Response violates schema",
|
124
|
-
code: str = "json_schema",
|
125
118
|
case_id: str | None = None,
|
126
119
|
) -> None:
|
127
120
|
self.operation = operation
|
@@ -132,7 +125,6 @@ class JsonSchemaError(Failure):
|
|
132
125
|
self.instance = instance
|
133
126
|
self.message = message
|
134
127
|
self.title = title
|
135
|
-
self.code = code
|
136
128
|
self.case_id = case_id
|
137
129
|
self.severity = Severity.HIGH
|
138
130
|
|
@@ -176,7 +168,7 @@ class JsonSchemaError(Failure):
|
|
176
168
|
class MissingContentType(Failure):
|
177
169
|
"""Content type header is missing."""
|
178
170
|
|
179
|
-
__slots__ = ("operation", "media_types", "message", "title", "
|
171
|
+
__slots__ = ("operation", "media_types", "message", "title", "case_id", "severity")
|
180
172
|
|
181
173
|
def __init__(
|
182
174
|
self,
|
@@ -185,14 +177,12 @@ class MissingContentType(Failure):
|
|
185
177
|
media_types: list[str],
|
186
178
|
message: str,
|
187
179
|
title: str = "Missing Content-Type header",
|
188
|
-
code: str = "missing_content_type",
|
189
180
|
case_id: str | None = None,
|
190
181
|
) -> None:
|
191
182
|
self.operation = operation
|
192
183
|
self.media_types = media_types
|
193
184
|
self.message = message
|
194
185
|
self.title = title
|
195
|
-
self.code = code
|
196
186
|
self.case_id = case_id
|
197
187
|
self.severity = Severity.MEDIUM
|
198
188
|
|
@@ -204,7 +194,7 @@ class MissingContentType(Failure):
|
|
204
194
|
class MalformedMediaType(Failure):
|
205
195
|
"""Media type name is malformed."""
|
206
196
|
|
207
|
-
__slots__ = ("operation", "actual", "defined", "message", "title", "
|
197
|
+
__slots__ = ("operation", "actual", "defined", "message", "title", "case_id", "severity")
|
208
198
|
|
209
199
|
def __init__(
|
210
200
|
self,
|
@@ -214,7 +204,6 @@ class MalformedMediaType(Failure):
|
|
214
204
|
defined: str,
|
215
205
|
message: str,
|
216
206
|
title: str = "Malformed media type",
|
217
|
-
code: str = "malformed_media_type",
|
218
207
|
case_id: str | None = None,
|
219
208
|
) -> None:
|
220
209
|
self.operation = operation
|
@@ -222,7 +211,6 @@ class MalformedMediaType(Failure):
|
|
222
211
|
self.defined = defined
|
223
212
|
self.message = message
|
224
213
|
self.title = title
|
225
|
-
self.code = code
|
226
214
|
self.case_id = case_id
|
227
215
|
self.severity = Severity.MEDIUM
|
228
216
|
|
@@ -236,7 +224,6 @@ class UndefinedContentType(Failure):
|
|
236
224
|
"defined_content_types",
|
237
225
|
"message",
|
238
226
|
"title",
|
239
|
-
"code",
|
240
227
|
"case_id",
|
241
228
|
"severity",
|
242
229
|
)
|
@@ -249,7 +236,6 @@ class UndefinedContentType(Failure):
|
|
249
236
|
defined_content_types: list[str],
|
250
237
|
message: str,
|
251
238
|
title: str = "Undocumented Content-Type",
|
252
|
-
code: str = "undefined_content_type",
|
253
239
|
case_id: str | None = None,
|
254
240
|
) -> None:
|
255
241
|
self.operation = operation
|
@@ -257,7 +243,6 @@ class UndefinedContentType(Failure):
|
|
257
243
|
self.defined_content_types = defined_content_types
|
258
244
|
self.message = message
|
259
245
|
self.title = title
|
260
|
-
self.code = code
|
261
246
|
self.case_id = case_id
|
262
247
|
self.severity = Severity.MEDIUM
|
263
248
|
|
@@ -269,7 +254,7 @@ class UndefinedContentType(Failure):
|
|
269
254
|
class UseAfterFree(Failure):
|
270
255
|
"""Resource was used after a successful DELETE operation on it."""
|
271
256
|
|
272
|
-
__slots__ = ("operation", "message", "free", "usage", "title", "
|
257
|
+
__slots__ = ("operation", "message", "free", "usage", "title", "case_id", "severity")
|
273
258
|
|
274
259
|
def __init__(
|
275
260
|
self,
|
@@ -279,7 +264,6 @@ class UseAfterFree(Failure):
|
|
279
264
|
free: str,
|
280
265
|
usage: str,
|
281
266
|
title: str = "Use after free",
|
282
|
-
code: str = "use_after_free",
|
283
267
|
case_id: str | None = None,
|
284
268
|
) -> None:
|
285
269
|
self.operation = operation
|
@@ -287,7 +271,6 @@ class UseAfterFree(Failure):
|
|
287
271
|
self.free = free
|
288
272
|
self.usage = usage
|
289
273
|
self.title = title
|
290
|
-
self.code = code
|
291
274
|
self.case_id = case_id
|
292
275
|
self.severity = Severity.CRITICAL
|
293
276
|
|
@@ -299,7 +282,7 @@ class UseAfterFree(Failure):
|
|
299
282
|
class EnsureResourceAvailability(Failure):
|
300
283
|
"""Resource is not available immediately after creation."""
|
301
284
|
|
302
|
-
__slots__ = ("operation", "message", "created_with", "not_available_with", "title", "
|
285
|
+
__slots__ = ("operation", "message", "created_with", "not_available_with", "title", "case_id", "severity")
|
303
286
|
|
304
287
|
def __init__(
|
305
288
|
self,
|
@@ -309,7 +292,6 @@ class EnsureResourceAvailability(Failure):
|
|
309
292
|
created_with: str,
|
310
293
|
not_available_with: str,
|
311
294
|
title: str = "Resource is not available after creation",
|
312
|
-
code: str = "ensure_resource_availability",
|
313
295
|
case_id: str | None = None,
|
314
296
|
) -> None:
|
315
297
|
self.operation = operation
|
@@ -317,7 +299,6 @@ class EnsureResourceAvailability(Failure):
|
|
317
299
|
self.created_with = created_with
|
318
300
|
self.not_available_with = not_available_with
|
319
301
|
self.title = title
|
320
|
-
self.code = code
|
321
302
|
self.case_id = case_id
|
322
303
|
self.severity = Severity.MEDIUM
|
323
304
|
|
@@ -329,7 +310,7 @@ class EnsureResourceAvailability(Failure):
|
|
329
310
|
class IgnoredAuth(Failure):
|
330
311
|
"""The API operation does not check the specified authentication."""
|
331
312
|
|
332
|
-
__slots__ = ("operation", "message", "title", "
|
313
|
+
__slots__ = ("operation", "message", "title", "case_id", "severity")
|
333
314
|
|
334
315
|
def __init__(
|
335
316
|
self,
|
@@ -337,13 +318,11 @@ class IgnoredAuth(Failure):
|
|
337
318
|
operation: str,
|
338
319
|
message: str,
|
339
320
|
title: str = "Authentication declared but not enforced",
|
340
|
-
code: str = "ignored_auth",
|
341
321
|
case_id: str | None = None,
|
342
322
|
) -> None:
|
343
323
|
self.operation = operation
|
344
324
|
self.message = message
|
345
325
|
self.title = title
|
346
|
-
self.code = code
|
347
326
|
self.case_id = case_id
|
348
327
|
self.severity = Severity.CRITICAL
|
349
328
|
|
@@ -355,7 +334,7 @@ class IgnoredAuth(Failure):
|
|
355
334
|
class AcceptedNegativeData(Failure):
|
356
335
|
"""Response with negative data was accepted."""
|
357
336
|
|
358
|
-
__slots__ = ("operation", "message", "status_code", "allowed_statuses", "title", "
|
337
|
+
__slots__ = ("operation", "message", "status_code", "allowed_statuses", "title", "case_id", "severity")
|
359
338
|
|
360
339
|
def __init__(
|
361
340
|
self,
|
@@ -365,7 +344,6 @@ class AcceptedNegativeData(Failure):
|
|
365
344
|
status_code: int,
|
366
345
|
allowed_statuses: list[str],
|
367
346
|
title: str = "Accepted negative data",
|
368
|
-
code: str = "accepted_negative_data",
|
369
347
|
case_id: str | None = None,
|
370
348
|
) -> None:
|
371
349
|
self.operation = operation
|
@@ -373,7 +351,6 @@ class AcceptedNegativeData(Failure):
|
|
373
351
|
self.status_code = status_code
|
374
352
|
self.allowed_statuses = allowed_statuses
|
375
353
|
self.title = title
|
376
|
-
self.code = code
|
377
354
|
self.case_id = case_id
|
378
355
|
self.severity = Severity.MEDIUM
|
379
356
|
|
@@ -385,7 +362,7 @@ class AcceptedNegativeData(Failure):
|
|
385
362
|
class RejectedPositiveData(Failure):
|
386
363
|
"""Response with positive data was rejected."""
|
387
364
|
|
388
|
-
__slots__ = ("operation", "message", "status_code", "allowed_statuses", "title", "
|
365
|
+
__slots__ = ("operation", "message", "status_code", "allowed_statuses", "title", "case_id", "severity")
|
389
366
|
|
390
367
|
def __init__(
|
391
368
|
self,
|
@@ -395,7 +372,6 @@ class RejectedPositiveData(Failure):
|
|
395
372
|
status_code: int,
|
396
373
|
allowed_statuses: list[str],
|
397
374
|
title: str = "Rejected positive data",
|
398
|
-
code: str = "rejected_positive_data",
|
399
375
|
case_id: str | None = None,
|
400
376
|
) -> None:
|
401
377
|
self.operation = operation
|
@@ -403,7 +379,6 @@ class RejectedPositiveData(Failure):
|
|
403
379
|
self.status_code = status_code
|
404
380
|
self.allowed_statuses = allowed_statuses
|
405
381
|
self.title = title
|
406
|
-
self.code = code
|
407
382
|
self.case_id = case_id
|
408
383
|
self.severity = Severity.MEDIUM
|
409
384
|
|
schemathesis/schemas.py
CHANGED
@@ -2,7 +2,7 @@ from __future__ import annotations
|
|
2
2
|
|
3
3
|
from collections.abc import Mapping
|
4
4
|
from dataclasses import dataclass, field
|
5
|
-
from functools import lru_cache, partial
|
5
|
+
from functools import cached_property, lru_cache, partial
|
6
6
|
from itertools import chain
|
7
7
|
from typing import (
|
8
8
|
TYPE_CHECKING,
|
@@ -57,6 +57,34 @@ def get_full_path(base_path: str, path: str) -> str:
|
|
57
57
|
return unquote(urljoin(base_path, quote(path.lstrip("/"))))
|
58
58
|
|
59
59
|
|
60
|
+
@dataclass
|
61
|
+
class FilteredCount:
|
62
|
+
"""Count of total items and those passing filters."""
|
63
|
+
|
64
|
+
total: int
|
65
|
+
selected: int
|
66
|
+
|
67
|
+
__slots__ = ("total", "selected")
|
68
|
+
|
69
|
+
def __init__(self) -> None:
|
70
|
+
self.total = 0
|
71
|
+
self.selected = 0
|
72
|
+
|
73
|
+
|
74
|
+
@dataclass
|
75
|
+
class ApiStatistic:
|
76
|
+
"""Statistics about API operations and links."""
|
77
|
+
|
78
|
+
operations: FilteredCount
|
79
|
+
links: FilteredCount
|
80
|
+
|
81
|
+
__slots__ = ("operations", "links")
|
82
|
+
|
83
|
+
def __init__(self) -> None:
|
84
|
+
self.operations = FilteredCount()
|
85
|
+
self.links = FilteredCount()
|
86
|
+
|
87
|
+
|
60
88
|
@dataclass
|
61
89
|
class ApiOperationsCount:
|
62
90
|
"""Statistics about API operations."""
|
@@ -84,7 +112,6 @@ class BaseSchema(Mapping):
|
|
84
112
|
generation_config: GenerationConfig = field(default_factory=GenerationConfig)
|
85
113
|
output_config: OutputConfig = field(default_factory=OutputConfig)
|
86
114
|
rate_limiter: Limiter | None = None
|
87
|
-
_operations_count: ApiOperationsCount | None = None
|
88
115
|
|
89
116
|
def __post_init__(self) -> None:
|
90
117
|
self.hook = to_filterable_hook(self.hooks) # type: ignore[method-assign]
|
@@ -186,7 +213,7 @@ class BaseSchema(Mapping):
|
|
186
213
|
raise NotImplementedError
|
187
214
|
|
188
215
|
def __len__(self) -> int:
|
189
|
-
return self.
|
216
|
+
return self.statistic.operations.total
|
190
217
|
|
191
218
|
def hook(self, hook: str | Callable) -> Callable:
|
192
219
|
return self.hooks.register(hook)
|
@@ -225,18 +252,11 @@ class BaseSchema(Mapping):
|
|
225
252
|
def validate(self) -> None:
|
226
253
|
raise NotImplementedError
|
227
254
|
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
self._operations_count = self._do_count_operations()
|
232
|
-
return self._operations_count
|
255
|
+
@cached_property
|
256
|
+
def statistic(self) -> ApiStatistic:
|
257
|
+
return self._measure_statistic()
|
233
258
|
|
234
|
-
def
|
235
|
-
"""Implementation-specific counting logic."""
|
236
|
-
raise NotImplementedError
|
237
|
-
|
238
|
-
@property
|
239
|
-
def links_count(self) -> int:
|
259
|
+
def _measure_statistic(self) -> ApiStatistic:
|
240
260
|
raise NotImplementedError
|
241
261
|
|
242
262
|
def get_all_operations(
|
@@ -24,9 +24,11 @@ from hypothesis import strategies as st
|
|
24
24
|
from hypothesis_graphql import strategies as gql_st
|
25
25
|
from requests.structures import CaseInsensitiveDict
|
26
26
|
|
27
|
+
from schemathesis import auths
|
27
28
|
from schemathesis.core import NOT_SET, NotSet, Specification
|
28
29
|
from schemathesis.core.errors import InvalidSchema, OperationNotFound
|
29
30
|
from schemathesis.core.result import Ok, Result
|
31
|
+
from schemathesis.generation import GenerationConfig, GenerationMode
|
30
32
|
from schemathesis.generation.case import Case
|
31
33
|
from schemathesis.generation.meta import (
|
32
34
|
CaseMetadata,
|
@@ -38,12 +40,16 @@ from schemathesis.generation.meta import (
|
|
38
40
|
PhaseInfo,
|
39
41
|
TestPhase,
|
40
42
|
)
|
43
|
+
from schemathesis.hooks import HookContext, HookDispatcher, apply_to_all_dispatchers
|
44
|
+
from schemathesis.schemas import (
|
45
|
+
APIOperation,
|
46
|
+
APIOperationMap,
|
47
|
+
ApiStatistic,
|
48
|
+
BaseSchema,
|
49
|
+
OperationDefinition,
|
50
|
+
)
|
51
|
+
from schemathesis.specs.openapi.constants import LOCATION_TO_CONTAINER
|
41
52
|
|
42
|
-
from ... import auths
|
43
|
-
from ...generation import GenerationConfig, GenerationMode
|
44
|
-
from ...hooks import HookContext, HookDispatcher, apply_to_all_dispatchers
|
45
|
-
from ...schemas import APIOperation, APIOperationMap, ApiOperationsCount, BaseSchema, OperationDefinition
|
46
|
-
from ..openapi.constants import LOCATION_TO_CONTAINER
|
47
53
|
from ._cache import OperationCache
|
48
54
|
from .scalars import CUSTOM_SCALARS, get_extra_scalar_strategies
|
49
55
|
|
@@ -140,8 +146,8 @@ class GraphQLSchema(BaseSchema):
|
|
140
146
|
def _get_base_path(self) -> str:
|
141
147
|
return cast(str, urlsplit(self.location).path)
|
142
148
|
|
143
|
-
def
|
144
|
-
|
149
|
+
def _measure_statistic(self) -> ApiStatistic:
|
150
|
+
statistic = ApiStatistic()
|
145
151
|
raw_schema = self.raw_schema["__schema"]
|
146
152
|
dummy_operation = APIOperation(
|
147
153
|
base_url=self.get_base_url(),
|
@@ -159,16 +165,11 @@ class GraphQLSchema(BaseSchema):
|
|
159
165
|
for type_def in raw_schema.get("types", []):
|
160
166
|
if type_def["name"] == query_type_name:
|
161
167
|
for field in type_def["fields"]:
|
162
|
-
|
168
|
+
statistic.operations.total += 1
|
163
169
|
dummy_operation.label = f"{query_type_name}.{field['name']}"
|
164
170
|
if not self._should_skip(dummy_operation):
|
165
|
-
|
166
|
-
return
|
167
|
-
|
168
|
-
@property
|
169
|
-
def links_count(self) -> int:
|
170
|
-
# Links are not supported for GraphQL
|
171
|
-
return 0
|
171
|
+
statistic.operations.selected += 1
|
172
|
+
return statistic
|
172
173
|
|
173
174
|
def get_all_operations(
|
174
175
|
self, generation_config: GenerationConfig | None = None
|
@@ -8,42 +8,38 @@ from __future__ import annotations
|
|
8
8
|
import json
|
9
9
|
from typing import Any
|
10
10
|
|
11
|
+
from schemathesis.generation.stateful.state_machine import StepOutput
|
12
|
+
|
11
13
|
from . import lexer, nodes, parser
|
12
|
-
from .context import ExpressionContext
|
13
14
|
|
14
|
-
__all__ = [
|
15
|
-
"lexer",
|
16
|
-
"nodes",
|
17
|
-
"parser",
|
18
|
-
"ExpressionContext",
|
19
|
-
]
|
15
|
+
__all__ = ["lexer", "nodes", "parser"]
|
20
16
|
|
21
17
|
|
22
|
-
def evaluate(expr: Any,
|
18
|
+
def evaluate(expr: Any, output: StepOutput, evaluate_nested: bool = False) -> Any:
|
23
19
|
"""Evaluate runtime expression in context."""
|
24
20
|
if isinstance(expr, (dict, list)) and evaluate_nested:
|
25
|
-
return _evaluate_nested(expr,
|
21
|
+
return _evaluate_nested(expr, output)
|
26
22
|
if not isinstance(expr, str):
|
27
23
|
# Can be a non-string constant
|
28
24
|
return expr
|
29
|
-
parts = [node.evaluate(
|
25
|
+
parts = [node.evaluate(output) for node in parser.parse(expr)]
|
30
26
|
if len(parts) == 1:
|
31
27
|
return parts[0] # keep the return type the same as the internal value type
|
32
28
|
# otherwise, concatenate into a string
|
33
29
|
return "".join(str(part) for part in parts if part is not None)
|
34
30
|
|
35
31
|
|
36
|
-
def _evaluate_nested(expr: dict[str, Any] | list,
|
32
|
+
def _evaluate_nested(expr: dict[str, Any] | list, output: StepOutput) -> Any:
|
37
33
|
if isinstance(expr, dict):
|
38
34
|
return {
|
39
|
-
_evaluate_object_key(key,
|
35
|
+
_evaluate_object_key(key, output): evaluate(value, output, evaluate_nested=True)
|
40
36
|
for key, value in expr.items()
|
41
37
|
}
|
42
|
-
return [evaluate(item,
|
38
|
+
return [evaluate(item, output, evaluate_nested=True) for item in expr]
|
43
39
|
|
44
40
|
|
45
|
-
def _evaluate_object_key(key: str,
|
46
|
-
evaluated = evaluate(key,
|
41
|
+
def _evaluate_object_key(key: str, output: StepOutput) -> Any:
|
42
|
+
evaluated = evaluate(key, output)
|
47
43
|
if isinstance(evaluated, str):
|
48
44
|
return evaluated
|
49
45
|
if isinstance(evaluated, bool):
|