schemathesis 4.3.12__py3-none-any.whl → 4.3.13__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of schemathesis might be problematic. Click here for more details.
- schemathesis/core/errors.py +80 -11
- schemathesis/engine/phases/unit/_executor.py +4 -0
- schemathesis/generation/stateful/state_machine.py +3 -0
- schemathesis/specs/openapi/schemas.py +18 -5
- schemathesis/specs/openapi/stateful/__init__.py +3 -5
- schemathesis/specs/openapi/stateful/dependencies/naming.py +13 -10
- schemathesis/specs/openapi/stateful/dependencies/resources.py +7 -1
- schemathesis/specs/openapi/stateful/dependencies/schemas.py +25 -9
- schemathesis/specs/openapi/stateful/links.py +6 -2
- {schemathesis-4.3.12.dist-info → schemathesis-4.3.13.dist-info}/METADATA +1 -1
- {schemathesis-4.3.12.dist-info → schemathesis-4.3.13.dist-info}/RECORD +14 -14
- {schemathesis-4.3.12.dist-info → schemathesis-4.3.13.dist-info}/WHEEL +0 -0
- {schemathesis-4.3.12.dist-info → schemathesis-4.3.13.dist-info}/entry_points.txt +0 -0
- {schemathesis-4.3.12.dist-info → schemathesis-4.3.13.dist-info}/licenses/LICENSE +0 -0
schemathesis/core/errors.py
CHANGED
|
@@ -5,6 +5,8 @@ from __future__ import annotations
|
|
|
5
5
|
import enum
|
|
6
6
|
import re
|
|
7
7
|
import traceback
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from textwrap import indent
|
|
8
10
|
from types import TracebackType
|
|
9
11
|
from typing import TYPE_CHECKING, Any, Callable, NoReturn
|
|
10
12
|
|
|
@@ -33,6 +35,60 @@ class SchemathesisError(Exception):
|
|
|
33
35
|
"""Base exception class for all Schemathesis errors."""
|
|
34
36
|
|
|
35
37
|
|
|
38
|
+
class DefinitionKind(str, enum.Enum):
|
|
39
|
+
SCHEMA = "Schema Object"
|
|
40
|
+
SECURITY_SCHEME = "Security Scheme Object"
|
|
41
|
+
RESPONSES = "Responses Object"
|
|
42
|
+
PARAMETER = "Parameter Object"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass
|
|
46
|
+
class SchemaLocation:
|
|
47
|
+
kind: DefinitionKind
|
|
48
|
+
# Hint about where the definition is located
|
|
49
|
+
hint: str | None
|
|
50
|
+
# Open API spec version
|
|
51
|
+
version: str
|
|
52
|
+
|
|
53
|
+
__slots__ = ("kind", "hint", "version")
|
|
54
|
+
|
|
55
|
+
@classmethod
|
|
56
|
+
def response_schema(cls, version: str) -> SchemaLocation:
|
|
57
|
+
return cls(kind=DefinitionKind.SCHEMA, hint="in response definition", version=version)
|
|
58
|
+
|
|
59
|
+
@classmethod
|
|
60
|
+
def maybe_from_error_path(cls, path: list[str | int], version: str) -> SchemaLocation | None:
|
|
61
|
+
if len(path) == 3 and path[:2] == ["components", "securitySchemes"]:
|
|
62
|
+
return cls(kind=DefinitionKind.SECURITY_SCHEME, hint=f"definition for `{path[2]}`", version=version)
|
|
63
|
+
if len(path) == 3 and path[:2] == ["components", "schemas"]:
|
|
64
|
+
return cls(kind=DefinitionKind.SCHEMA, hint=f"definition for `{path[2]}`", version=version)
|
|
65
|
+
if len(path) == 4 and path[0] == "paths" and path[-1] == "responses":
|
|
66
|
+
return cls(kind=DefinitionKind.RESPONSES, hint=None, version=version)
|
|
67
|
+
if len(path) == 5 and path[0] == "paths" and path[3] == "parameters":
|
|
68
|
+
return cls(kind=DefinitionKind.PARAMETER, hint=f"at index {path[4]}", version=version)
|
|
69
|
+
|
|
70
|
+
return None
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def message(self) -> str:
|
|
74
|
+
message = f"Invalid {self.kind.value}"
|
|
75
|
+
if self.hint is not None:
|
|
76
|
+
message += f" {self.hint}"
|
|
77
|
+
else:
|
|
78
|
+
message += " definition"
|
|
79
|
+
return message
|
|
80
|
+
|
|
81
|
+
@property
|
|
82
|
+
def specification_url(self) -> str:
|
|
83
|
+
anchor = {
|
|
84
|
+
DefinitionKind.SCHEMA: "schema-object",
|
|
85
|
+
DefinitionKind.SECURITY_SCHEME: "security-scheme-object",
|
|
86
|
+
DefinitionKind.RESPONSES: "responses-object",
|
|
87
|
+
DefinitionKind.PARAMETER: "parameter-object",
|
|
88
|
+
}[self.kind]
|
|
89
|
+
return f"https://spec.openapis.org/oas/v{self.version}#{anchor}"
|
|
90
|
+
|
|
91
|
+
|
|
36
92
|
class InvalidSchema(SchemathesisError):
|
|
37
93
|
"""Indicates errors in API schema validation or processing."""
|
|
38
94
|
|
|
@@ -56,9 +112,16 @@ class InvalidSchema(SchemathesisError):
|
|
|
56
112
|
|
|
57
113
|
@classmethod
|
|
58
114
|
def from_jsonschema_error(
|
|
59
|
-
cls,
|
|
115
|
+
cls,
|
|
116
|
+
error: ValidationError | JsonSchemaError,
|
|
117
|
+
path: str | None,
|
|
118
|
+
method: str | None,
|
|
119
|
+
config: OutputConfig,
|
|
120
|
+
location: SchemaLocation | None = None,
|
|
60
121
|
) -> InvalidSchema:
|
|
61
|
-
if
|
|
122
|
+
if location is not None:
|
|
123
|
+
message = location.message
|
|
124
|
+
elif error.absolute_path:
|
|
62
125
|
part = error.absolute_path[-1]
|
|
63
126
|
if isinstance(part, int) and len(error.absolute_path) > 1:
|
|
64
127
|
parent = error.absolute_path[-2]
|
|
@@ -70,14 +133,18 @@ class InvalidSchema(SchemathesisError):
|
|
|
70
133
|
error_path = " -> ".join(str(entry) for entry in error.path) or "[root]"
|
|
71
134
|
message += f"\n\nLocation:\n {error_path}"
|
|
72
135
|
instance = truncate_json(error.instance, config=config)
|
|
73
|
-
message += f"\n\nProblematic definition:\n{instance}"
|
|
136
|
+
message += f"\n\nProblematic definition:\n{indent(instance, ' ')}"
|
|
74
137
|
message += "\n\nError details:\n "
|
|
75
138
|
# This default message contains the instance which we already printed
|
|
76
139
|
if "is not valid under any of the given schemas" in error.message:
|
|
77
140
|
message += "The provided definition doesn't match any of the expected formats or types."
|
|
78
141
|
else:
|
|
79
142
|
message += error.message
|
|
80
|
-
message +=
|
|
143
|
+
message += "\n\n"
|
|
144
|
+
if location is not None:
|
|
145
|
+
message += f"See: {location.specification_url}"
|
|
146
|
+
else:
|
|
147
|
+
message += SCHEMA_ERROR_SUGGESTION
|
|
81
148
|
return cls(message, path=path, method=method)
|
|
82
149
|
|
|
83
150
|
@classmethod
|
|
@@ -86,12 +153,14 @@ class InvalidSchema(SchemathesisError):
|
|
|
86
153
|
) -> InvalidSchema:
|
|
87
154
|
notes = getattr(error, "__notes__", [])
|
|
88
155
|
# Some exceptions don't have the actual reference in them, hence we add it manually via notes
|
|
89
|
-
|
|
90
|
-
message = "Unresolvable
|
|
156
|
+
reference = str(notes[0])
|
|
157
|
+
message = "Unresolvable reference in the schema"
|
|
91
158
|
# Get the pointer value from "Unresolvable JSON pointer: 'components/UnknownParameter'"
|
|
92
|
-
message += f"\n\nError details:\n
|
|
93
|
-
|
|
94
|
-
|
|
159
|
+
message += f"\n\nError details:\n Reference: {reference}"
|
|
160
|
+
if not reference.startswith(("http://", "https://", "#/")):
|
|
161
|
+
message += "\n File reference could not be resolved. Check that the file exists."
|
|
162
|
+
elif reference.startswith(("#/components", "#/definitions")):
|
|
163
|
+
message += "\n Component does not exist in the schema."
|
|
95
164
|
return cls(message, path=path, method=method)
|
|
96
165
|
|
|
97
166
|
def as_failing_test_function(self) -> Callable:
|
|
@@ -154,13 +223,13 @@ class InvalidStateMachine(SchemathesisError):
|
|
|
154
223
|
for source, target_groups in by_source.items():
|
|
155
224
|
for (target, status), transitions in target_groups.items():
|
|
156
225
|
for transition in transitions:
|
|
157
|
-
result += f"\n\n {
|
|
226
|
+
result += f"\n\n {format_transition(source, status, transition.name, target)}\n"
|
|
158
227
|
for error in transition.errors:
|
|
159
228
|
result += f"\n - {error.message}"
|
|
160
229
|
return result
|
|
161
230
|
|
|
162
231
|
|
|
163
|
-
def
|
|
232
|
+
def format_transition(source: str, status: str, transition: str, target: str) -> str:
|
|
164
233
|
return f"{source} -> [{status}] {transition} -> {target}"
|
|
165
234
|
|
|
166
235
|
|
|
@@ -25,6 +25,7 @@ from schemathesis.core.errors import (
|
|
|
25
25
|
InvalidRegexType,
|
|
26
26
|
InvalidSchema,
|
|
27
27
|
MalformedMediaType,
|
|
28
|
+
SchemaLocation,
|
|
28
29
|
SerializationNotPossible,
|
|
29
30
|
)
|
|
30
31
|
from schemathesis.core.failures import Failure, FailureGroup
|
|
@@ -198,6 +199,9 @@ def run_test(
|
|
|
198
199
|
path=operation.path,
|
|
199
200
|
method=operation.method,
|
|
200
201
|
config=ctx.config.output,
|
|
202
|
+
location=SchemaLocation.maybe_from_error_path(
|
|
203
|
+
list(exc.absolute_path), ctx.schema.specification.version
|
|
204
|
+
),
|
|
201
205
|
)
|
|
202
206
|
)
|
|
203
207
|
except InvalidArgument as exc:
|
|
@@ -50,6 +50,9 @@ class StepInput:
|
|
|
50
50
|
|
|
51
51
|
@property
|
|
52
52
|
def is_applied(self) -> bool:
|
|
53
|
+
# If the transition has no parameters or body, count it as applied
|
|
54
|
+
if self.transition is not None and not self.transition.parameters and self.transition.request_body is None:
|
|
55
|
+
return True
|
|
53
56
|
return bool(self.applied_parameters)
|
|
54
57
|
|
|
55
58
|
|
|
@@ -27,7 +27,13 @@ from requests.structures import CaseInsensitiveDict
|
|
|
27
27
|
from schemathesis.core import INJECTED_PATH_PARAMETER_KEY, NOT_SET, NotSet, Specification, deserialization, media_types
|
|
28
28
|
from schemathesis.core.adapter import OperationParameter, ResponsesContainer
|
|
29
29
|
from schemathesis.core.compat import RefResolutionError
|
|
30
|
-
from schemathesis.core.errors import
|
|
30
|
+
from schemathesis.core.errors import (
|
|
31
|
+
SCHEMA_ERROR_SUGGESTION,
|
|
32
|
+
InfiniteRecursiveReference,
|
|
33
|
+
InvalidSchema,
|
|
34
|
+
OperationNotFound,
|
|
35
|
+
SchemaLocation,
|
|
36
|
+
)
|
|
31
37
|
from schemathesis.core.failures import Failure, FailureGroup, MalformedJson
|
|
32
38
|
from schemathesis.core.result import Err, Ok, Result
|
|
33
39
|
from schemathesis.core.transport import Response
|
|
@@ -61,7 +67,6 @@ if TYPE_CHECKING:
|
|
|
61
67
|
from schemathesis.generation.stateful import APIStateMachine
|
|
62
68
|
|
|
63
69
|
HTTP_METHODS = frozenset({"get", "put", "post", "delete", "options", "head", "patch", "trace"})
|
|
64
|
-
SCHEMA_ERROR_MESSAGE = "Ensure that the definition complies with the OpenAPI specification"
|
|
65
70
|
SCHEMA_PARSING_ERRORS = (KeyError, AttributeError, RefResolutionError, InvalidSchema, InfiniteRecursiveReference)
|
|
66
71
|
|
|
67
72
|
|
|
@@ -317,9 +322,13 @@ class BaseOpenAPISchema(BaseSchema):
|
|
|
317
322
|
self.validate()
|
|
318
323
|
except jsonschema.ValidationError as exc:
|
|
319
324
|
raise InvalidSchema.from_jsonschema_error(
|
|
320
|
-
exc,
|
|
325
|
+
exc,
|
|
326
|
+
path=path,
|
|
327
|
+
method=method,
|
|
328
|
+
config=self.config.output,
|
|
329
|
+
location=SchemaLocation.maybe_from_error_path(list(exc.absolute_path), self.specification.version),
|
|
321
330
|
) from None
|
|
322
|
-
raise InvalidSchema(
|
|
331
|
+
raise InvalidSchema(SCHEMA_ERROR_SUGGESTION, path=path, method=method) from error
|
|
323
332
|
|
|
324
333
|
def validate(self) -> None:
|
|
325
334
|
with suppress(TypeError):
|
|
@@ -540,7 +549,11 @@ class BaseOpenAPISchema(BaseSchema):
|
|
|
540
549
|
definition.validator.validate(data)
|
|
541
550
|
except jsonschema.SchemaError as exc:
|
|
542
551
|
raise InvalidSchema.from_jsonschema_error(
|
|
543
|
-
exc,
|
|
552
|
+
exc,
|
|
553
|
+
path=operation.path,
|
|
554
|
+
method=operation.method,
|
|
555
|
+
config=self.config.output,
|
|
556
|
+
location=SchemaLocation.response_schema(self.specification.version),
|
|
544
557
|
) from exc
|
|
545
558
|
except jsonschema.ValidationError as exc:
|
|
546
559
|
failures.append(
|
|
@@ -167,9 +167,7 @@ def create_state_machine(schema: BaseOpenAPISchema) -> type[APIStateMachine]:
|
|
|
167
167
|
if incoming:
|
|
168
168
|
for link in incoming:
|
|
169
169
|
bundle_name = f"{link.source.label} -> {link.status_code}"
|
|
170
|
-
name = _normalize_name(
|
|
171
|
-
f"{link.source.label} -> {link.status_code} -> {link.name} -> {target.label}"
|
|
172
|
-
)
|
|
170
|
+
name = _normalize_name(link.full_name)
|
|
173
171
|
assert name not in rules, name
|
|
174
172
|
rules[name] = precondition(is_transition_allowed(bundle_name, link.source.label, target.label))(
|
|
175
173
|
transition(
|
|
@@ -237,8 +235,8 @@ def classify_root_transitions(operations: list[APIOperation], transitions: ApiTr
|
|
|
237
235
|
|
|
238
236
|
def is_likely_root_transition(operation: APIOperation) -> bool:
|
|
239
237
|
"""Check if operation is likely to succeed as a root transition."""
|
|
240
|
-
# POST operations
|
|
241
|
-
if operation.method == "post"
|
|
238
|
+
# POST operations are likely to create resources
|
|
239
|
+
if operation.method == "post":
|
|
242
240
|
return True
|
|
243
241
|
|
|
244
242
|
# GET operations without path parameters are likely to return lists
|
|
@@ -394,13 +394,13 @@ def find_matching_field(*, parameter: str, resource: str, fields: list[str]) ->
|
|
|
394
394
|
return field
|
|
395
395
|
|
|
396
396
|
# Extract parameter components
|
|
397
|
-
parameter_prefix,
|
|
397
|
+
parameter_prefix, parameter_suffix = _split_parameter_name(parameter)
|
|
398
398
|
parameter_prefix_normalized = _normalize_for_matching(parameter_prefix)
|
|
399
399
|
|
|
400
400
|
# Parameter has resource prefix, field might not
|
|
401
401
|
# Example: `channelId` - `Channel.id`
|
|
402
402
|
if parameter_prefix and parameter_prefix_normalized == resource_normalized:
|
|
403
|
-
suffix_normalized = _normalize_for_matching(
|
|
403
|
+
suffix_normalized = _normalize_for_matching(parameter_suffix)
|
|
404
404
|
|
|
405
405
|
for field in fields:
|
|
406
406
|
field_normalized = _normalize_for_matching(field)
|
|
@@ -409,8 +409,8 @@ def find_matching_field(*, parameter: str, resource: str, fields: list[str]) ->
|
|
|
409
409
|
|
|
410
410
|
# Parameter has no prefix, field might have resource prefix
|
|
411
411
|
# Example: `id` - `Channel.channelId`
|
|
412
|
-
if not parameter_prefix and
|
|
413
|
-
expected_field_normalized = resource_normalized + _normalize_for_matching(
|
|
412
|
+
if not parameter_prefix and parameter_suffix:
|
|
413
|
+
expected_field_normalized = resource_normalized + _normalize_for_matching(parameter_suffix)
|
|
414
414
|
|
|
415
415
|
for field in fields:
|
|
416
416
|
field_normalized = _normalize_for_matching(field)
|
|
@@ -433,7 +433,7 @@ def _normalize_for_matching(text: str) -> str:
|
|
|
433
433
|
return text.lower().replace("_", "").replace("-", "")
|
|
434
434
|
|
|
435
435
|
|
|
436
|
-
def _split_parameter_name(
|
|
436
|
+
def _split_parameter_name(parameter_name: str) -> tuple[str, str]:
|
|
437
437
|
"""Split parameter into (prefix, suffix) components.
|
|
438
438
|
|
|
439
439
|
Examples:
|
|
@@ -444,13 +444,16 @@ def _split_parameter_name(param_name: str) -> tuple[str, str]:
|
|
|
444
444
|
"channel_id" -> ("channel", "_id")
|
|
445
445
|
|
|
446
446
|
"""
|
|
447
|
-
if
|
|
448
|
-
return (
|
|
447
|
+
if parameter_name.endswith("Id") and len(parameter_name) > 2:
|
|
448
|
+
return (parameter_name[:-2], "Id")
|
|
449
449
|
|
|
450
|
-
if
|
|
451
|
-
return (
|
|
450
|
+
if parameter_name.endswith("_id") and len(parameter_name) > 3:
|
|
451
|
+
return (parameter_name[:-3], "_id")
|
|
452
452
|
|
|
453
|
-
|
|
453
|
+
if parameter_name.endswith("_guid") and len(parameter_name) > 5:
|
|
454
|
+
return (parameter_name[:-5], "_guid")
|
|
455
|
+
|
|
456
|
+
return ("", parameter_name)
|
|
454
457
|
|
|
455
458
|
|
|
456
459
|
def strip_affixes(name: str, prefixes: list[str], suffixes: list[str]) -> str:
|
|
@@ -294,7 +294,13 @@ def _extract_resource_from_schema(
|
|
|
294
294
|
properties = resolved.get("properties")
|
|
295
295
|
if properties:
|
|
296
296
|
fields = sorted(properties)
|
|
297
|
-
types = {
|
|
297
|
+
types = {}
|
|
298
|
+
for field, subschema in properties.items():
|
|
299
|
+
if isinstance(subschema, dict):
|
|
300
|
+
_, resolved_subschema = maybe_resolve(subschema, resolver, "")
|
|
301
|
+
else:
|
|
302
|
+
resolved_subschema = subschema
|
|
303
|
+
types[field] = set(get_type(cast(dict, resolved_subschema)))
|
|
298
304
|
source = DefinitionSource.SCHEMA_WITH_PROPERTIES
|
|
299
305
|
else:
|
|
300
306
|
fields = []
|
|
@@ -232,30 +232,38 @@ def unwrap_schema(
|
|
|
232
232
|
_, resolved = maybe_resolve(array_schema, resolver, "")
|
|
233
233
|
pointer = f"/{encode_pointer(array_field)}"
|
|
234
234
|
|
|
235
|
+
uses_parent_ref = False
|
|
235
236
|
# Try to unwrap one more time
|
|
236
237
|
if resolved.get("type") == "array" or "items" in resolved:
|
|
237
238
|
nested_items = resolved.get("items")
|
|
238
239
|
if isinstance(nested_items, dict):
|
|
239
240
|
_, resolved_items = maybe_resolve(nested_items, resolver, "")
|
|
240
|
-
external_tag = _detect_externally_tagged_pattern(resolved_items, path)
|
|
241
|
+
external_tag = _detect_externally_tagged_pattern(resolved_items, path, parent_ref)
|
|
241
242
|
if external_tag:
|
|
242
|
-
|
|
243
|
+
external_tag_, uses_parent_ref = external_tag
|
|
244
|
+
nested_properties = resolved_items["properties"][external_tag_]
|
|
243
245
|
_, resolved = maybe_resolve(nested_properties, resolver, "")
|
|
244
|
-
pointer += f"/{encode_pointer(
|
|
246
|
+
pointer += f"/{encode_pointer(external_tag_)}"
|
|
245
247
|
|
|
248
|
+
ref = parent_ref if uses_parent_ref else array_schema.get("$ref")
|
|
246
249
|
return UnwrappedSchema(pointer=pointer, schema=resolved, ref=array_schema.get("$ref"))
|
|
247
250
|
|
|
248
251
|
# External tag
|
|
249
|
-
external_tag = _detect_externally_tagged_pattern(schema, path)
|
|
252
|
+
external_tag = _detect_externally_tagged_pattern(schema, path, parent_ref)
|
|
250
253
|
if external_tag:
|
|
251
|
-
|
|
254
|
+
external_tag_, uses_parent_ref = external_tag
|
|
255
|
+
tagged_schema = properties[external_tag_]
|
|
252
256
|
_, resolved_tagged = maybe_resolve(tagged_schema, resolver, "")
|
|
253
257
|
|
|
254
258
|
resolved = try_unwrap_all_of(resolved_tagged)
|
|
255
|
-
ref =
|
|
259
|
+
ref = (
|
|
260
|
+
parent_ref
|
|
261
|
+
if uses_parent_ref
|
|
262
|
+
else resolved.get("$ref") or resolved_tagged.get("$ref") or tagged_schema.get("$ref")
|
|
263
|
+
)
|
|
256
264
|
|
|
257
265
|
_, resolved = maybe_resolve(resolved, resolver, "")
|
|
258
|
-
return UnwrappedSchema(pointer=f"/{encode_pointer(
|
|
266
|
+
return UnwrappedSchema(pointer=f"/{encode_pointer(external_tag_)}", schema=resolved, ref=ref)
|
|
259
267
|
|
|
260
268
|
# No wrapper - single object at root
|
|
261
269
|
return UnwrappedSchema(pointer="/", schema=schema, ref=schema.get("$ref"))
|
|
@@ -391,7 +399,9 @@ def _is_pagination_wrapper(
|
|
|
391
399
|
return None
|
|
392
400
|
|
|
393
401
|
|
|
394
|
-
def _detect_externally_tagged_pattern(
|
|
402
|
+
def _detect_externally_tagged_pattern(
|
|
403
|
+
schema: Mapping[str, Any], path: str, parent_ref: str | None
|
|
404
|
+
) -> tuple[str, bool] | None:
|
|
395
405
|
"""Detect externally tagged resource pattern.
|
|
396
406
|
|
|
397
407
|
Pattern: {ResourceName: [...]} or {resourceName: [...]}
|
|
@@ -420,12 +430,18 @@ def _detect_externally_tagged_pattern(schema: Mapping[str, Any], path: str) -> s
|
|
|
420
430
|
# `data_request`
|
|
421
431
|
naming.to_snake_case(resource_name),
|
|
422
432
|
}
|
|
433
|
+
parent_names = set()
|
|
434
|
+
if parent_ref is not None:
|
|
435
|
+
maybe_resource_name = resource_name_from_ref(parent_ref)
|
|
436
|
+
parent_names.add(naming.to_plural(maybe_resource_name.lower()))
|
|
437
|
+
parent_names.add(naming.to_snake_case(maybe_resource_name))
|
|
438
|
+
possible_names = possible_names.union(parent_names)
|
|
423
439
|
|
|
424
440
|
for name, subschema in properties.items():
|
|
425
441
|
if name.lower() not in possible_names:
|
|
426
442
|
continue
|
|
427
443
|
|
|
428
444
|
if isinstance(subschema, dict) and "object" in get_type(subschema):
|
|
429
|
-
return name
|
|
445
|
+
return name, name.lower() in parent_names
|
|
430
446
|
|
|
431
447
|
return None
|
|
@@ -5,7 +5,7 @@ from functools import lru_cache
|
|
|
5
5
|
from typing import Any, Callable
|
|
6
6
|
|
|
7
7
|
from schemathesis.core import NOT_SET, NotSet
|
|
8
|
-
from schemathesis.core.errors import InvalidTransition, OperationNotFound, TransitionValidationError
|
|
8
|
+
from schemathesis.core.errors import InvalidTransition, OperationNotFound, TransitionValidationError, format_transition
|
|
9
9
|
from schemathesis.core.parameters import ParameterLocation
|
|
10
10
|
from schemathesis.core.result import Err, Ok, Result
|
|
11
11
|
from schemathesis.generation.stateful.state_machine import ExtractedParam, StepOutput, Transition
|
|
@@ -94,6 +94,10 @@ class OpenApiLink:
|
|
|
94
94
|
|
|
95
95
|
self._cached_extract = lru_cache(8)(self._extract_impl)
|
|
96
96
|
|
|
97
|
+
@property
|
|
98
|
+
def full_name(self) -> str:
|
|
99
|
+
return format_transition(self.source.label, self.status_code, self.name, self.target.label)
|
|
100
|
+
|
|
97
101
|
def _normalize_parameters(
|
|
98
102
|
self, parameters: dict[str, str], errors: list[TransitionValidationError]
|
|
99
103
|
) -> list[NormalizedParameter]:
|
|
@@ -165,7 +169,7 @@ class OpenApiLink:
|
|
|
165
169
|
def _extract_impl(self, wrapper: StepOutputWrapper) -> Transition:
|
|
166
170
|
output = wrapper.output
|
|
167
171
|
return Transition(
|
|
168
|
-
id=
|
|
172
|
+
id=self.full_name,
|
|
169
173
|
parent_id=output.case.id,
|
|
170
174
|
is_inferred=self.is_inferred,
|
|
171
175
|
parameters=self.extract_parameters(output),
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: schemathesis
|
|
3
|
-
Version: 4.3.
|
|
3
|
+
Version: 4.3.13
|
|
4
4
|
Summary: Property-based testing framework for Open API and GraphQL based apps
|
|
5
5
|
Project-URL: Documentation, https://schemathesis.readthedocs.io/en/stable/
|
|
6
6
|
Project-URL: Changelog, https://github.com/schemathesis/schemathesis/blob/master/CHANGELOG.md
|
|
@@ -52,7 +52,7 @@ schemathesis/core/compat.py,sha256=9BWCrFoqN2sJIaiht_anxe8kLjYMR7t0iiOkXqLRUZ8,1
|
|
|
52
52
|
schemathesis/core/control.py,sha256=IzwIc8HIAEMtZWW0Q0iXI7T1niBpjvcLlbuwOSmy5O8,130
|
|
53
53
|
schemathesis/core/curl.py,sha256=jrPL9KpNHteyJ6A1oxJRSkL5bfuBeuPs3xh9Z_ml2cE,1892
|
|
54
54
|
schemathesis/core/deserialization.py,sha256=qjXUPaz_mc1OSgXzTUSkC8tuVR8wgVQtb9g3CcAF6D0,2951
|
|
55
|
-
schemathesis/core/errors.py,sha256=
|
|
55
|
+
schemathesis/core/errors.py,sha256=sr23WgbD-52n5fmC-QBn2suzNUbsB1okvXIs_L5EyR0,19918
|
|
56
56
|
schemathesis/core/failures.py,sha256=yFpAxWdEnm0Ri8z8RqRI9H7vcLH5ztOeSIi4m4SGx5g,8996
|
|
57
57
|
schemathesis/core/fs.py,sha256=ItQT0_cVwjDdJX9IiI7EnU75NI2H3_DCEyyUjzg_BgI,472
|
|
58
58
|
schemathesis/core/hooks.py,sha256=qhbkkRSf8URJ4LKv2wmKRINKpquUOgxQzWBHKWRWo3Q,475
|
|
@@ -89,7 +89,7 @@ schemathesis/engine/phases/stateful/__init__.py,sha256=Lz1rgNqCfUSIz173XqCGsiMuU
|
|
|
89
89
|
schemathesis/engine/phases/stateful/_executor.py,sha256=yRpUJqKLTKMVRy7hEXPwmI23CtgGIprz341lCJwvTrU,15613
|
|
90
90
|
schemathesis/engine/phases/stateful/context.py,sha256=A7X1SLDOWFpCvFN9IiIeNVZM0emjqatmJL_k9UsO7vM,2946
|
|
91
91
|
schemathesis/engine/phases/unit/__init__.py,sha256=9dDcxyj887pktnE9YDIPNaR-vc7iqKQWIrFr77SbUTQ,8786
|
|
92
|
-
schemathesis/engine/phases/unit/_executor.py,sha256=
|
|
92
|
+
schemathesis/engine/phases/unit/_executor.py,sha256=4wr7POpPfeI7_Mx6i2pk2efyK1FxKGjXdMwi_MURTDU,17427
|
|
93
93
|
schemathesis/engine/phases/unit/_pool.py,sha256=iU0hdHDmohPnEv7_S1emcabuzbTf-Cznqwn0pGQ5wNQ,2480
|
|
94
94
|
schemathesis/generation/__init__.py,sha256=tvNO2FLiY8z3fZ_kL_QJhSgzXfnT4UqwSXMHCwfLI0g,645
|
|
95
95
|
schemathesis/generation/case.py,sha256=SLMw6zkzmeiZdaIij8_0tjTF70BrMlRSWREaqWii0uM,12508
|
|
@@ -104,7 +104,7 @@ schemathesis/generation/hypothesis/examples.py,sha256=6eGaKUEC3elmKsaqfKj1sLvM8E
|
|
|
104
104
|
schemathesis/generation/hypothesis/given.py,sha256=sTZR1of6XaHAPWtHx2_WLlZ50M8D5Rjux0GmWkWjDq4,2337
|
|
105
105
|
schemathesis/generation/hypothesis/reporting.py,sha256=uDVow6Ya8YFkqQuOqRsjbzsbyP4KKfr3jA7ZaY4FuKY,279
|
|
106
106
|
schemathesis/generation/stateful/__init__.py,sha256=s7jiJEnguIj44IsRyMi8afs-8yjIUuBbzW58bH5CHjs,1042
|
|
107
|
-
schemathesis/generation/stateful/state_machine.py,sha256=
|
|
107
|
+
schemathesis/generation/stateful/state_machine.py,sha256=CiVtpBEeotpNOUkYO3vJLKRe89gdT1kjguZ88vbfqs0,9500
|
|
108
108
|
schemathesis/graphql/__init__.py,sha256=_eO6MAPHGgiADVGRntnwtPxmuvk666sAh-FAU4cG9-0,326
|
|
109
109
|
schemathesis/graphql/checks.py,sha256=IADbxiZjgkBWrC5yzHDtohRABX6zKXk5w_zpWNwdzYo,3186
|
|
110
110
|
schemathesis/graphql/loaders.py,sha256=2tgG4HIvFmjHLr_KexVXnT8hSBM-dKG_fuXTZgE97So,9445
|
|
@@ -137,7 +137,7 @@ schemathesis/specs/openapi/formats.py,sha256=4tYRdckauHxkJCmOhmdwDq_eOpHPaKloi89
|
|
|
137
137
|
schemathesis/specs/openapi/media_types.py,sha256=F5M6TKl0s6Z5X8mZpPsWDEdPBvxclKRcUOc41eEwKbo,2472
|
|
138
138
|
schemathesis/specs/openapi/patterns.py,sha256=GqPZEXMRdWENQxanWjBOalIZ2MQUjuxk21kmdiI703E,18027
|
|
139
139
|
schemathesis/specs/openapi/references.py,sha256=AW1laU23BkiRf0EEFM538vyVFLXycGUiucGVV461le0,1927
|
|
140
|
-
schemathesis/specs/openapi/schemas.py,sha256=
|
|
140
|
+
schemathesis/specs/openapi/schemas.py,sha256=ONFB8kMBrryZL_tKHWvxnBjyUHoHh_MAUqxjuVDc78c,34034
|
|
141
141
|
schemathesis/specs/openapi/serialization.py,sha256=RPNdadne5wdhsGmjSvgKLRF58wpzpRx3wura8PsHM3o,12152
|
|
142
142
|
schemathesis/specs/openapi/utils.py,sha256=XkOJT8qD-6uhq-Tmwxk_xYku1Gy5F9pKL3ldNg_DRZw,522
|
|
143
143
|
schemathesis/specs/openapi/adapter/__init__.py,sha256=YEovBgLjnXd3WGPMJXq0KbSGHezkRlEv4dNRO7_evfk,249
|
|
@@ -159,17 +159,17 @@ schemathesis/specs/openapi/negative/__init__.py,sha256=B78vps314fJOMZwlPdv7vUHo7
|
|
|
159
159
|
schemathesis/specs/openapi/negative/mutations.py,sha256=9U352xJsdZBR-Zfy1V7_X3a5i91LIUS9Zqotrzp3BLA,21000
|
|
160
160
|
schemathesis/specs/openapi/negative/types.py,sha256=a7buCcVxNBG6ILBM3A7oNTAX0lyDseEtZndBuej8MbI,174
|
|
161
161
|
schemathesis/specs/openapi/negative/utils.py,sha256=ozcOIuASufLqZSgnKUACjX-EOZrrkuNdXX0SDnLoGYA,168
|
|
162
|
-
schemathesis/specs/openapi/stateful/__init__.py,sha256=
|
|
162
|
+
schemathesis/specs/openapi/stateful/__init__.py,sha256=RpGqyjKShp2X94obaHnCR9TO6Qt_ZAarlB-awlyMzUY,18654
|
|
163
163
|
schemathesis/specs/openapi/stateful/control.py,sha256=QaXLSbwQWtai5lxvvVtQV3BLJ8n5ePqSKB00XFxp-MA,3695
|
|
164
164
|
schemathesis/specs/openapi/stateful/inference.py,sha256=B99jSTDVi2yKxU7-raIb91xpacOrr0nZkEZY5Ej3eCY,9783
|
|
165
|
-
schemathesis/specs/openapi/stateful/links.py,sha256=
|
|
165
|
+
schemathesis/specs/openapi/stateful/links.py,sha256=TEZ7wudPdRdP-pg5XNIgYbual_9n2arBKnS2n8SxiaU,8629
|
|
166
166
|
schemathesis/specs/openapi/stateful/dependencies/__init__.py,sha256=9FWF7tiP7GaOwapRFIYjsu16LxkosKCzBvzjkSTCsjU,8183
|
|
167
167
|
schemathesis/specs/openapi/stateful/dependencies/inputs.py,sha256=sQydINThS6vp9-OnTKCb_unoVP4m3Ho-0xTG0K7ps8Q,15915
|
|
168
168
|
schemathesis/specs/openapi/stateful/dependencies/models.py,sha256=Kl482Hwq2M8lYAdqGmf_8Yje3voSj1WLDUIujRUDWDQ,12286
|
|
169
|
-
schemathesis/specs/openapi/stateful/dependencies/naming.py,sha256=
|
|
169
|
+
schemathesis/specs/openapi/stateful/dependencies/naming.py,sha256=GoO_Tw04u_7ix6qsAzMDoJooXZqIRAIV8sLphL4mGnw,13084
|
|
170
170
|
schemathesis/specs/openapi/stateful/dependencies/outputs.py,sha256=zvVUfQWNIuhMkKDpz5hsVGkkvkefLt1EswpJAnHajOw,1186
|
|
171
|
-
schemathesis/specs/openapi/stateful/dependencies/resources.py,sha256=
|
|
172
|
-
schemathesis/specs/openapi/stateful/dependencies/schemas.py,sha256=
|
|
171
|
+
schemathesis/specs/openapi/stateful/dependencies/resources.py,sha256=eUWE26ZixPtSuHl7laF2asS_-6qklQRhyQlk7e99hlc,12087
|
|
172
|
+
schemathesis/specs/openapi/stateful/dependencies/schemas.py,sha256=TKM6hEuyLm5NB-jQeEbIXF7GZF-7mqYLq3OsTBRDpws,14878
|
|
173
173
|
schemathesis/specs/openapi/types/__init__.py,sha256=VPsWtLJle__Kodw_QqtQ3OuvBzBcCIKsTOrXy3eA7OU,66
|
|
174
174
|
schemathesis/specs/openapi/types/v3.py,sha256=Vondr9Amk6JKCIM6i6RGcmTUjFfPgOOqzBXqerccLpo,1468
|
|
175
175
|
schemathesis/transport/__init__.py,sha256=6yg_RfV_9L0cpA6qpbH-SL9_3ggtHQji9CZrpIkbA6s,5321
|
|
@@ -178,8 +178,8 @@ schemathesis/transport/prepare.py,sha256=erYXRaxpQokIDzaIuvt_csHcw72iHfCyNq8VNEz
|
|
|
178
178
|
schemathesis/transport/requests.py,sha256=wriRI9fprTplE_qEZLEz1TerX6GwkE3pwr6ZnU2o6vQ,10648
|
|
179
179
|
schemathesis/transport/serialization.py,sha256=GwO6OAVTmL1JyKw7HiZ256tjV4CbrRbhQN0ep1uaZwI,11157
|
|
180
180
|
schemathesis/transport/wsgi.py,sha256=kQtasFre6pjdJWRKwLA_Qb-RyQHCFNpaey9ubzlFWKI,5907
|
|
181
|
-
schemathesis-4.3.
|
|
182
|
-
schemathesis-4.3.
|
|
183
|
-
schemathesis-4.3.
|
|
184
|
-
schemathesis-4.3.
|
|
185
|
-
schemathesis-4.3.
|
|
181
|
+
schemathesis-4.3.13.dist-info/METADATA,sha256=n-G9iaIj5lgmV9oro_G7FL4f2tAeI9xOXaLe95r6WnA,8566
|
|
182
|
+
schemathesis-4.3.13.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
183
|
+
schemathesis-4.3.13.dist-info/entry_points.txt,sha256=hiK3un-xfgPdwj9uj16YVDtTNpO128bmk0U82SMv8ZQ,152
|
|
184
|
+
schemathesis-4.3.13.dist-info/licenses/LICENSE,sha256=2Ve4J8v5jMQAWrT7r1nf3bI8Vflk3rZVQefiF2zpxwg,1121
|
|
185
|
+
schemathesis-4.3.13.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|