schemathesis 4.3.11__py3-none-any.whl → 4.3.12__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/generation/stateful/state_machine.py +10 -5
- schemathesis/specs/openapi/stateful/__init__.py +79 -32
- schemathesis/specs/openapi/stateful/dependencies/__init__.py +12 -1
- schemathesis/specs/openapi/stateful/dependencies/inputs.py +117 -1
- schemathesis/specs/openapi/stateful/dependencies/resources.py +16 -0
- schemathesis/specs/openapi/stateful/links.py +13 -4
- {schemathesis-4.3.11.dist-info → schemathesis-4.3.12.dist-info}/METADATA +1 -1
- {schemathesis-4.3.11.dist-info → schemathesis-4.3.12.dist-info}/RECORD +11 -11
- {schemathesis-4.3.11.dist-info → schemathesis-4.3.12.dist-info}/WHEEL +0 -0
- {schemathesis-4.3.11.dist-info → schemathesis-4.3.12.dist-info}/entry_points.txt +0 -0
- {schemathesis-4.3.11.dist-info → schemathesis-4.3.12.dist-info}/licenses/LICENSE +0 -0
|
@@ -37,16 +37,20 @@ class StepInput:
|
|
|
37
37
|
|
|
38
38
|
case: Case
|
|
39
39
|
transition: Transition | None # None for initial steps
|
|
40
|
-
#
|
|
40
|
+
# What parameters were actually applied
|
|
41
41
|
# Data extraction failures can prevent it, as well as transitions can be skipped in some cases
|
|
42
42
|
# to improve discovery of bugs triggered by non-stateful inputs during stateful testing
|
|
43
|
-
|
|
43
|
+
applied_parameters: list[str]
|
|
44
44
|
|
|
45
|
-
__slots__ = ("case", "transition", "
|
|
45
|
+
__slots__ = ("case", "transition", "applied_parameters")
|
|
46
46
|
|
|
47
47
|
@classmethod
|
|
48
48
|
def initial(cls, case: Case) -> StepInput:
|
|
49
|
-
return cls(case=case, transition=None,
|
|
49
|
+
return cls(case=case, transition=None, applied_parameters=[])
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def is_applied(self) -> bool:
|
|
53
|
+
return bool(self.applied_parameters)
|
|
50
54
|
|
|
51
55
|
|
|
52
56
|
@dataclass
|
|
@@ -69,8 +73,9 @@ class ExtractedParam:
|
|
|
69
73
|
|
|
70
74
|
definition: Any
|
|
71
75
|
value: Result[Any, Exception]
|
|
76
|
+
is_required: bool
|
|
72
77
|
|
|
73
|
-
__slots__ = ("definition", "value")
|
|
78
|
+
__slots__ = ("definition", "value", "is_required")
|
|
74
79
|
|
|
75
80
|
|
|
76
81
|
@dataclass
|
|
@@ -8,6 +8,7 @@ import jsonschema
|
|
|
8
8
|
from hypothesis import strategies as st
|
|
9
9
|
from hypothesis.stateful import Bundle, Rule, precondition, rule
|
|
10
10
|
|
|
11
|
+
from schemathesis.core import NOT_SET
|
|
11
12
|
from schemathesis.core.errors import InvalidStateMachine, InvalidTransition
|
|
12
13
|
from schemathesis.core.parameters import ParameterLocation
|
|
13
14
|
from schemathesis.core.result import Ok
|
|
@@ -49,7 +50,7 @@ class OpenAPIStateMachine(APIStateMachine):
|
|
|
49
50
|
# The proportion of negative tests generated for "root" transitions
|
|
50
51
|
NEGATIVE_TEST_CASES_THRESHOLD = 10
|
|
51
52
|
# How often some transition is skipped
|
|
52
|
-
|
|
53
|
+
BASE_EXPLORATION_RATE = 0.15
|
|
53
54
|
|
|
54
55
|
|
|
55
56
|
@dataclass
|
|
@@ -162,6 +163,7 @@ def create_state_machine(schema: BaseOpenAPISchema) -> type[APIStateMachine]:
|
|
|
162
163
|
for target in operations:
|
|
163
164
|
if target.label in transitions.operations:
|
|
164
165
|
incoming = transitions.operations[target.label].incoming
|
|
166
|
+
config = schema.config.generation_for(operation=target, phase="stateful")
|
|
165
167
|
if incoming:
|
|
166
168
|
for link in incoming:
|
|
167
169
|
bundle_name = f"{link.source.label} -> {link.status_code}"
|
|
@@ -169,7 +171,6 @@ def create_state_machine(schema: BaseOpenAPISchema) -> type[APIStateMachine]:
|
|
|
169
171
|
f"{link.source.label} -> {link.status_code} -> {link.name} -> {target.label}"
|
|
170
172
|
)
|
|
171
173
|
assert name not in rules, name
|
|
172
|
-
config = schema.config.generation_for(operation=target, phase="stateful")
|
|
173
174
|
rules[name] = precondition(is_transition_allowed(bundle_name, link.source.label, target.label))(
|
|
174
175
|
transition(
|
|
175
176
|
name=name,
|
|
@@ -181,7 +182,6 @@ def create_state_machine(schema: BaseOpenAPISchema) -> type[APIStateMachine]:
|
|
|
181
182
|
)
|
|
182
183
|
if target.label in roots.reliable or (not roots.reliable and target.label in roots.fallback):
|
|
183
184
|
name = _normalize_name(f"RANDOM -> {target.label}")
|
|
184
|
-
config = schema.config.generation_for(operation=target, phase="stateful")
|
|
185
185
|
if len(config.modes) == 1:
|
|
186
186
|
case_strategy = target.as_strategy(generation_mode=config.modes[0], phase=TestPhase.STATEFUL)
|
|
187
187
|
else:
|
|
@@ -249,50 +249,97 @@ def is_likely_root_transition(operation: APIOperation) -> bool:
|
|
|
249
249
|
|
|
250
250
|
|
|
251
251
|
def into_step_input(
|
|
252
|
-
target: APIOperation, link: OpenApiLink, modes: list[GenerationMode]
|
|
252
|
+
*, target: APIOperation, link: OpenApiLink, modes: list[GenerationMode]
|
|
253
253
|
) -> Callable[[StepOutput], st.SearchStrategy[StepInput]]:
|
|
254
|
+
"""A single transition between API operations."""
|
|
255
|
+
|
|
254
256
|
def builder(_output: StepOutput) -> st.SearchStrategy[StepInput]:
|
|
255
257
|
@st.composite # type: ignore[misc]
|
|
256
258
|
def inner(draw: st.DrawFn, output: StepOutput) -> StepInput:
|
|
259
|
+
random = draw(st.randoms(use_true_random=True))
|
|
260
|
+
|
|
261
|
+
def biased_coin(p: float) -> bool:
|
|
262
|
+
return random.random() < p
|
|
263
|
+
|
|
264
|
+
# Extract transition data from previous operation's output
|
|
257
265
|
transition = link.extract(output)
|
|
258
266
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
+
overrides: dict[str, Any] = {}
|
|
268
|
+
applied_parameters = []
|
|
269
|
+
for container, data in transition.parameters.items():
|
|
270
|
+
overrides[container] = {}
|
|
271
|
+
|
|
272
|
+
for name, extracted in data.items():
|
|
273
|
+
# Skip if extraction failed or returned unusable value
|
|
274
|
+
if not isinstance(extracted.value, Ok) or extracted.value.ok() in (None, UNRESOLVABLE):
|
|
275
|
+
continue
|
|
276
|
+
|
|
277
|
+
param_key = f"{container}.{name}"
|
|
278
|
+
|
|
279
|
+
# Calculate exploration rate based on parameter characteristics
|
|
280
|
+
exploration_rate = BASE_EXPLORATION_RATE
|
|
281
|
+
|
|
282
|
+
# Path parameters are critical for routing - use link values more often
|
|
283
|
+
if container == "path_parameters":
|
|
284
|
+
exploration_rate *= 0.5
|
|
285
|
+
|
|
286
|
+
# Required parameters should follow links more often, optional ones explored more
|
|
287
|
+
# Path params are always required, so they get both multipliers
|
|
288
|
+
if extracted.is_required:
|
|
289
|
+
exploration_rate *= 0.5
|
|
290
|
+
else:
|
|
291
|
+
# Explore optional parameters more to avoid only testing link-provided values
|
|
292
|
+
exploration_rate *= 3.0
|
|
293
|
+
|
|
294
|
+
if biased_coin(1 - exploration_rate):
|
|
295
|
+
overrides[container][name] = extracted.value.ok()
|
|
296
|
+
applied_parameters.append(param_key)
|
|
267
297
|
|
|
298
|
+
# Get the extracted body value
|
|
268
299
|
if (
|
|
269
300
|
transition.request_body is not None
|
|
270
301
|
and isinstance(transition.request_body.value, Ok)
|
|
271
302
|
and transition.request_body.value.ok() is not UNRESOLVABLE
|
|
272
|
-
and not link.merge_body
|
|
273
|
-
and draw(st.integers(min_value=0, max_value=99)) < USE_TRANSITION_THRESHOLD
|
|
274
303
|
):
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
304
|
+
request_body = transition.request_body.value.ok()
|
|
305
|
+
else:
|
|
306
|
+
request_body = NOT_SET
|
|
307
|
+
|
|
308
|
+
# Link suppose to replace the entire extracted body
|
|
309
|
+
if request_body is not NOT_SET and not link.merge_body and biased_coin(1 - BASE_EXPLORATION_RATE):
|
|
310
|
+
overrides["body"] = request_body
|
|
311
|
+
if isinstance(overrides["body"], dict):
|
|
312
|
+
applied_parameters.extend(f"body.{field}" for field in overrides["body"])
|
|
313
|
+
else:
|
|
314
|
+
applied_parameters.append("body")
|
|
278
315
|
|
|
279
316
|
cases = st.one_of(
|
|
280
|
-
target.as_strategy(generation_mode=mode, phase=TestPhase.STATEFUL, **
|
|
317
|
+
[target.as_strategy(generation_mode=mode, phase=TestPhase.STATEFUL, **overrides) for mode in modes]
|
|
281
318
|
)
|
|
282
319
|
case = draw(cases)
|
|
283
|
-
if
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
320
|
+
if request_body is not NOT_SET and link.merge_body:
|
|
321
|
+
if isinstance(request_body, dict):
|
|
322
|
+
selected_fields = {}
|
|
323
|
+
|
|
324
|
+
for field_name, field_value in request_body.items():
|
|
325
|
+
if field_value is UNRESOLVABLE:
|
|
326
|
+
continue
|
|
327
|
+
|
|
328
|
+
if biased_coin(1 - BASE_EXPLORATION_RATE):
|
|
329
|
+
selected_fields[field_name] = field_value
|
|
330
|
+
applied_parameters.append(f"body.{field_name}")
|
|
331
|
+
|
|
332
|
+
if selected_fields:
|
|
333
|
+
if isinstance(case.body, dict):
|
|
334
|
+
case.body = {**case.body, **selected_fields}
|
|
335
|
+
else:
|
|
336
|
+
# Can't merge into non-dict, replace entirely
|
|
337
|
+
case.body = selected_fields
|
|
338
|
+
elif biased_coin(1 - BASE_EXPLORATION_RATE):
|
|
339
|
+
case.body = request_body
|
|
340
|
+
applied_parameters.append("body")
|
|
341
|
+
|
|
342
|
+
# Re-validate generation mode after merging body
|
|
296
343
|
if case.meta and case.meta.generation.mode == GenerationMode.NEGATIVE:
|
|
297
344
|
# It is possible that the new body is now valid and the whole test case could be valid too
|
|
298
345
|
for alternative in case.operation.body:
|
|
@@ -304,7 +351,7 @@ def into_step_input(
|
|
|
304
351
|
)
|
|
305
352
|
if all(info.mode == GenerationMode.POSITIVE for info in case.meta.components.values()):
|
|
306
353
|
case.meta.generation.mode = GenerationMode.POSITIVE
|
|
307
|
-
return StepInput(case=case, transition=transition,
|
|
354
|
+
return StepInput(case=case, transition=transition, applied_parameters=applied_parameters)
|
|
308
355
|
|
|
309
356
|
return inner(output=_output)
|
|
310
357
|
|
|
@@ -10,7 +10,11 @@ from typing import TYPE_CHECKING, Any
|
|
|
10
10
|
from schemathesis.core import NOT_SET
|
|
11
11
|
from schemathesis.core.compat import RefResolutionError
|
|
12
12
|
from schemathesis.core.result import Ok
|
|
13
|
-
from schemathesis.specs.openapi.stateful.dependencies.inputs import
|
|
13
|
+
from schemathesis.specs.openapi.stateful.dependencies.inputs import (
|
|
14
|
+
extract_inputs,
|
|
15
|
+
merge_related_resources,
|
|
16
|
+
update_input_field_bindings,
|
|
17
|
+
)
|
|
14
18
|
from schemathesis.specs.openapi.stateful.dependencies.models import (
|
|
15
19
|
CanonicalizationCache,
|
|
16
20
|
Cardinality,
|
|
@@ -25,6 +29,7 @@ from schemathesis.specs.openapi.stateful.dependencies.models import (
|
|
|
25
29
|
ResourceMap,
|
|
26
30
|
)
|
|
27
31
|
from schemathesis.specs.openapi.stateful.dependencies.outputs import extract_outputs
|
|
32
|
+
from schemathesis.specs.openapi.stateful.dependencies.resources import remove_unused_resources
|
|
28
33
|
|
|
29
34
|
if TYPE_CHECKING:
|
|
30
35
|
from schemathesis.schemas import APIOperation
|
|
@@ -88,6 +93,12 @@ def analyze(schema: BaseOpenAPISchema) -> DependencyGraph:
|
|
|
88
93
|
for resource in updated_resources:
|
|
89
94
|
update_input_field_bindings(resource, operations)
|
|
90
95
|
|
|
96
|
+
# Merge parameter-inferred resources with schema-defined ones
|
|
97
|
+
merge_related_resources(operations, resources)
|
|
98
|
+
|
|
99
|
+
# Clean up orphaned resources
|
|
100
|
+
remove_unused_resources(operations, resources)
|
|
101
|
+
|
|
91
102
|
return DependencyGraph(operations=operations, resources=resources)
|
|
92
103
|
|
|
93
104
|
|
|
@@ -14,6 +14,7 @@ from schemathesis.specs.openapi.stateful.dependencies.models import (
|
|
|
14
14
|
DefinitionSource,
|
|
15
15
|
InputSlot,
|
|
16
16
|
OperationMap,
|
|
17
|
+
OutputSlot,
|
|
17
18
|
ResourceDefinition,
|
|
18
19
|
ResourceMap,
|
|
19
20
|
)
|
|
@@ -39,7 +40,7 @@ def extract_inputs(
|
|
|
39
40
|
creating placeholder resources if not yet discovered from their schemas.
|
|
40
41
|
"""
|
|
41
42
|
known_dependencies = set()
|
|
42
|
-
for param in operation.
|
|
43
|
+
for param in operation.iter_parameters():
|
|
43
44
|
input_slot = _resolve_parameter_dependency(
|
|
44
45
|
parameter_name=param.name,
|
|
45
46
|
parameter_location=param.location,
|
|
@@ -310,3 +311,118 @@ def update_input_field_bindings(resource_name: str, operations: OperationMap) ->
|
|
|
310
311
|
)
|
|
311
312
|
if new_field is not None:
|
|
312
313
|
input_slot.resource_field = new_field
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def merge_related_resources(operations: OperationMap, resources: ResourceMap) -> None:
|
|
317
|
+
"""Merge parameter-inferred resources with schema-defined resources from related operations."""
|
|
318
|
+
candidates = find_producer_consumer_candidates(operations)
|
|
319
|
+
|
|
320
|
+
for producer_name, consumer_name in candidates:
|
|
321
|
+
producer = operations[producer_name]
|
|
322
|
+
consumer = operations[consumer_name]
|
|
323
|
+
|
|
324
|
+
# Try to upgrade each input slot
|
|
325
|
+
for input_slot in consumer.inputs:
|
|
326
|
+
result = try_merge_input_resource(input_slot, producer.outputs, resources)
|
|
327
|
+
|
|
328
|
+
if result is not None:
|
|
329
|
+
new_resource_name, new_field_name = result
|
|
330
|
+
# Update input slot to use the better resource definition
|
|
331
|
+
input_slot.resource = resources[new_resource_name]
|
|
332
|
+
input_slot.resource_field = new_field_name
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def try_merge_input_resource(
|
|
336
|
+
input_slot: InputSlot,
|
|
337
|
+
producer_outputs: list[OutputSlot],
|
|
338
|
+
resources: ResourceMap,
|
|
339
|
+
) -> tuple[str, str] | None:
|
|
340
|
+
"""Try to upgrade an input's resource to a producer's resource."""
|
|
341
|
+
consumer_resource = input_slot.resource
|
|
342
|
+
|
|
343
|
+
# Only upgrade parameter-inferred resources (low confidence)
|
|
344
|
+
if consumer_resource.source != DefinitionSource.PARAMETER_INFERENCE:
|
|
345
|
+
return None
|
|
346
|
+
|
|
347
|
+
# Try each producer output
|
|
348
|
+
for output in producer_outputs:
|
|
349
|
+
producer_resource = resources[output.resource.name]
|
|
350
|
+
|
|
351
|
+
# Only merge to schema-defined resources (high confidence)
|
|
352
|
+
if producer_resource.source != DefinitionSource.SCHEMA_WITH_PROPERTIES:
|
|
353
|
+
continue
|
|
354
|
+
|
|
355
|
+
# Try to match the input parameter to producer's fields
|
|
356
|
+
param_name = input_slot.parameter_name
|
|
357
|
+
if not isinstance(param_name, str):
|
|
358
|
+
continue
|
|
359
|
+
|
|
360
|
+
for resource_name in (input_slot.resource.name, producer_resource.name):
|
|
361
|
+
matched_field = naming.find_matching_field(
|
|
362
|
+
parameter=param_name,
|
|
363
|
+
resource=resource_name,
|
|
364
|
+
fields=producer_resource.fields,
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
if matched_field is not None:
|
|
368
|
+
return (producer_resource.name, matched_field)
|
|
369
|
+
|
|
370
|
+
return None
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def find_producer_consumer_candidates(operations: OperationMap) -> list[tuple[str, str]]:
|
|
374
|
+
"""Find operation pairs that might produce/consume the same resource via REST patterns."""
|
|
375
|
+
candidates = []
|
|
376
|
+
|
|
377
|
+
# Group by base path to reduce comparisons
|
|
378
|
+
paths: dict[str, list[str]] = {}
|
|
379
|
+
for name, node in operations.items():
|
|
380
|
+
base = _extract_base_path(node.path)
|
|
381
|
+
paths.setdefault(base, []).append(name)
|
|
382
|
+
|
|
383
|
+
# Within each path group, find POST/PUT → GET/DELETE/PATCH patterns
|
|
384
|
+
for names in paths.values():
|
|
385
|
+
for producer_name in names:
|
|
386
|
+
producer = operations[producer_name]
|
|
387
|
+
# Producer must create/update and return data
|
|
388
|
+
if producer.method not in ("post", "put") or not producer.outputs:
|
|
389
|
+
continue
|
|
390
|
+
|
|
391
|
+
for consumer_name in names:
|
|
392
|
+
consumer = operations[consumer_name]
|
|
393
|
+
# Consumer must have path parameters
|
|
394
|
+
if not consumer.inputs:
|
|
395
|
+
continue
|
|
396
|
+
# Paths must be related (collection + item pattern)
|
|
397
|
+
if _is_collection_item_pattern(producer.path, consumer.path):
|
|
398
|
+
candidates.append((producer_name, consumer_name))
|
|
399
|
+
|
|
400
|
+
return candidates
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def _extract_base_path(path: str) -> str:
|
|
404
|
+
"""Extract collection path: /blog/posts/{id} -> /blog/posts."""
|
|
405
|
+
parts = [p for p in path.split("/") if not p.startswith("{")]
|
|
406
|
+
return "/".join(parts).rstrip("/")
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def _is_collection_item_pattern(collection_path: str, item_path: str) -> bool:
|
|
410
|
+
"""Check if paths follow REST collection/item pattern."""
|
|
411
|
+
# /blog/posts + /blog/posts/{postId}
|
|
412
|
+
normalized_collection = collection_path.rstrip("/")
|
|
413
|
+
normalized_item = item_path.rstrip("/")
|
|
414
|
+
|
|
415
|
+
# Must start with collection path
|
|
416
|
+
if not normalized_item.startswith(normalized_collection + "/"):
|
|
417
|
+
return False
|
|
418
|
+
|
|
419
|
+
# Extract the segment after collection path
|
|
420
|
+
remainder = normalized_item[len(normalized_collection) + 1 :]
|
|
421
|
+
|
|
422
|
+
# Must be a single path parameter: {paramName} with no slashes
|
|
423
|
+
return (
|
|
424
|
+
remainder.startswith("{")
|
|
425
|
+
and remainder.endswith("}")
|
|
426
|
+
and len(remainder) > 2 # Not empty {}
|
|
427
|
+
and "/" not in remainder
|
|
428
|
+
)
|
|
@@ -13,6 +13,7 @@ from schemathesis.specs.openapi.stateful.dependencies.models import (
|
|
|
13
13
|
CanonicalizationCache,
|
|
14
14
|
Cardinality,
|
|
15
15
|
DefinitionSource,
|
|
16
|
+
OperationMap,
|
|
16
17
|
ResourceDefinition,
|
|
17
18
|
ResourceMap,
|
|
18
19
|
extend_pointer,
|
|
@@ -310,3 +311,18 @@ def _extract_resource_from_schema(
|
|
|
310
311
|
resources[resource_name] = resource
|
|
311
312
|
|
|
312
313
|
return resource
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def remove_unused_resources(operations: OperationMap, resources: ResourceMap) -> None:
|
|
317
|
+
"""Remove resources that aren't referenced by any operation."""
|
|
318
|
+
# Collect all resource names currently in use
|
|
319
|
+
used_resources = set()
|
|
320
|
+
for operation in operations.values():
|
|
321
|
+
for input_slot in operation.inputs:
|
|
322
|
+
used_resources.add(input_slot.resource.name)
|
|
323
|
+
for output_slot in operation.outputs:
|
|
324
|
+
used_resources.add(output_slot.resource.name)
|
|
325
|
+
|
|
326
|
+
unused = set(resources.keys()) - used_resources
|
|
327
|
+
for resource_name in unused:
|
|
328
|
+
del resources[resource_name]
|
|
@@ -23,8 +23,9 @@ class NormalizedParameter:
|
|
|
23
23
|
name: str
|
|
24
24
|
expression: str
|
|
25
25
|
container_name: str
|
|
26
|
+
is_required: bool
|
|
26
27
|
|
|
27
|
-
__slots__ = ("location", "name", "expression", "container_name")
|
|
28
|
+
__slots__ = ("location", "name", "expression", "container_name", "is_required")
|
|
28
29
|
|
|
29
30
|
|
|
30
31
|
@dataclass(repr=False)
|
|
@@ -131,15 +132,21 @@ class OpenApiLink:
|
|
|
131
132
|
except Exception as exc:
|
|
132
133
|
errors.append(TransitionValidationError(str(exc)))
|
|
133
134
|
|
|
135
|
+
is_required = False
|
|
134
136
|
if hasattr(self, "target"):
|
|
135
137
|
try:
|
|
136
138
|
container_name = self._get_parameter_container(location, name)
|
|
137
139
|
except TransitionValidationError as exc:
|
|
138
140
|
errors.append(exc)
|
|
139
141
|
continue
|
|
142
|
+
|
|
143
|
+
for param in self.target.iter_parameters():
|
|
144
|
+
if param.name == name:
|
|
145
|
+
is_required = param.is_required
|
|
146
|
+
break
|
|
140
147
|
else:
|
|
141
148
|
continue
|
|
142
|
-
result.append(NormalizedParameter(location, name, expression, container_name))
|
|
149
|
+
result.append(NormalizedParameter(location, name, expression, container_name, is_required=is_required))
|
|
143
150
|
return result
|
|
144
151
|
|
|
145
152
|
def _get_parameter_container(self, location: ParameterLocation | None, name: str) -> str:
|
|
@@ -178,7 +185,9 @@ class OpenApiLink:
|
|
|
178
185
|
value = Ok(expressions.evaluate(parameter.expression, output))
|
|
179
186
|
except Exception as exc:
|
|
180
187
|
value = Err(exc)
|
|
181
|
-
container[parameter.name] = ExtractedParam(
|
|
188
|
+
container[parameter.name] = ExtractedParam(
|
|
189
|
+
definition=parameter.expression, value=value, is_required=parameter.is_required
|
|
190
|
+
)
|
|
182
191
|
return extracted
|
|
183
192
|
|
|
184
193
|
def extract_body(self, output: StepOutput) -> ExtractedParam | None:
|
|
@@ -188,7 +197,7 @@ class OpenApiLink:
|
|
|
188
197
|
value = Ok(expressions.evaluate(self.body, output, evaluate_nested=True))
|
|
189
198
|
except Exception as exc:
|
|
190
199
|
value = Err(exc)
|
|
191
|
-
return ExtractedParam(definition=self.body, value=value)
|
|
200
|
+
return ExtractedParam(definition=self.body, value=value, is_required=True)
|
|
192
201
|
return None
|
|
193
202
|
|
|
194
203
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: schemathesis
|
|
3
|
-
Version: 4.3.
|
|
3
|
+
Version: 4.3.12
|
|
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
|
|
@@ -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=3whIW5WDL_-IZIeZLB-qlxIr0_DNC6fb6pZ_0U7ifkE,9285
|
|
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
|
|
@@ -159,16 +159,16 @@ 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=T-iYOxPh3GfvKUxrc2f2u_GSeO0HUYajn2qVw2F6sGA,18802
|
|
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=
|
|
166
|
-
schemathesis/specs/openapi/stateful/dependencies/__init__.py,sha256=
|
|
167
|
-
schemathesis/specs/openapi/stateful/dependencies/inputs.py,sha256=
|
|
165
|
+
schemathesis/specs/openapi/stateful/links.py,sha256=P4CISEi-BVRtXd9cXBnHuvxInxW1LBa7DVYcnaZAhBU,8530
|
|
166
|
+
schemathesis/specs/openapi/stateful/dependencies/__init__.py,sha256=9FWF7tiP7GaOwapRFIYjsu16LxkosKCzBvzjkSTCsjU,8183
|
|
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
169
|
schemathesis/specs/openapi/stateful/dependencies/naming.py,sha256=NnXEFY1W3i18jEEYGgC_8oLoE7YOxdXgcMYtZvLj10w,12920
|
|
170
170
|
schemathesis/specs/openapi/stateful/dependencies/outputs.py,sha256=zvVUfQWNIuhMkKDpz5hsVGkkvkefLt1EswpJAnHajOw,1186
|
|
171
|
-
schemathesis/specs/openapi/stateful/dependencies/resources.py,sha256=
|
|
171
|
+
schemathesis/specs/openapi/stateful/dependencies/resources.py,sha256=MLitJnn1vUihhzuCIA-l7uXG6ne3YTUlnyAAbKaz2Ls,11824
|
|
172
172
|
schemathesis/specs/openapi/stateful/dependencies/schemas.py,sha256=RaG1BJH4D7-o5Qs2rIRQvS8ERntMUEs2I5jXUFaKMRo,14147
|
|
173
173
|
schemathesis/specs/openapi/types/__init__.py,sha256=VPsWtLJle__Kodw_QqtQ3OuvBzBcCIKsTOrXy3eA7OU,66
|
|
174
174
|
schemathesis/specs/openapi/types/v3.py,sha256=Vondr9Amk6JKCIM6i6RGcmTUjFfPgOOqzBXqerccLpo,1468
|
|
@@ -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.12.dist-info/METADATA,sha256=udwQ_n-qA4_1ZwxJDDhMNe6WUrUp4LJ8mnDdbOhapFY,8566
|
|
182
|
+
schemathesis-4.3.12.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
183
|
+
schemathesis-4.3.12.dist-info/entry_points.txt,sha256=hiK3un-xfgPdwj9uj16YVDtTNpO128bmk0U82SMv8ZQ,152
|
|
184
|
+
schemathesis-4.3.12.dist-info/licenses/LICENSE,sha256=2Ve4J8v5jMQAWrT7r1nf3bI8Vflk3rZVQefiF2zpxwg,1121
|
|
185
|
+
schemathesis-4.3.12.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|