schemathesis 4.1.4__py3-none-any.whl → 4.2.0__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/cli/commands/run/executor.py +1 -1
- schemathesis/cli/commands/run/handlers/base.py +28 -1
- schemathesis/cli/commands/run/handlers/cassettes.py +10 -12
- schemathesis/cli/commands/run/handlers/junitxml.py +5 -6
- schemathesis/cli/commands/run/handlers/output.py +7 -1
- schemathesis/cli/ext/fs.py +1 -1
- schemathesis/config/_diff_base.py +3 -1
- schemathesis/config/_operations.py +2 -0
- schemathesis/config/_phases.py +21 -4
- schemathesis/config/_projects.py +10 -2
- schemathesis/core/adapter.py +34 -0
- schemathesis/core/errors.py +29 -5
- schemathesis/core/jsonschema/__init__.py +13 -0
- schemathesis/core/jsonschema/bundler.py +163 -0
- schemathesis/{specs/openapi/constants.py → core/jsonschema/keywords.py} +0 -8
- schemathesis/core/jsonschema/references.py +122 -0
- schemathesis/core/jsonschema/types.py +41 -0
- schemathesis/core/media_types.py +6 -4
- schemathesis/core/parameters.py +37 -0
- schemathesis/core/transforms.py +25 -2
- schemathesis/core/validation.py +19 -0
- schemathesis/engine/context.py +1 -1
- schemathesis/engine/errors.py +11 -18
- schemathesis/engine/phases/stateful/_executor.py +1 -1
- schemathesis/engine/phases/unit/_executor.py +30 -13
- schemathesis/errors.py +4 -0
- schemathesis/filters.py +2 -2
- schemathesis/generation/coverage.py +87 -11
- schemathesis/generation/hypothesis/__init__.py +4 -1
- schemathesis/generation/hypothesis/builder.py +108 -70
- schemathesis/generation/meta.py +5 -14
- schemathesis/generation/overrides.py +17 -17
- schemathesis/pytest/lazy.py +1 -1
- schemathesis/pytest/plugin.py +1 -6
- schemathesis/schemas.py +22 -72
- schemathesis/specs/graphql/schemas.py +27 -16
- schemathesis/specs/openapi/_hypothesis.py +83 -68
- schemathesis/specs/openapi/adapter/__init__.py +10 -0
- schemathesis/specs/openapi/adapter/parameters.py +504 -0
- schemathesis/specs/openapi/adapter/protocol.py +57 -0
- schemathesis/specs/openapi/adapter/references.py +19 -0
- schemathesis/specs/openapi/adapter/responses.py +329 -0
- schemathesis/specs/openapi/adapter/security.py +141 -0
- schemathesis/specs/openapi/adapter/v2.py +28 -0
- schemathesis/specs/openapi/adapter/v3_0.py +28 -0
- schemathesis/specs/openapi/adapter/v3_1.py +28 -0
- schemathesis/specs/openapi/checks.py +99 -90
- schemathesis/specs/openapi/converter.py +114 -27
- schemathesis/specs/openapi/examples.py +210 -168
- schemathesis/specs/openapi/negative/__init__.py +12 -7
- schemathesis/specs/openapi/negative/mutations.py +68 -40
- schemathesis/specs/openapi/references.py +2 -175
- schemathesis/specs/openapi/schemas.py +142 -490
- schemathesis/specs/openapi/serialization.py +15 -7
- schemathesis/specs/openapi/stateful/__init__.py +17 -12
- schemathesis/specs/openapi/stateful/inference.py +13 -11
- schemathesis/specs/openapi/stateful/links.py +5 -20
- schemathesis/specs/openapi/types/__init__.py +3 -0
- schemathesis/specs/openapi/types/v3.py +68 -0
- schemathesis/specs/openapi/utils.py +1 -13
- schemathesis/transport/requests.py +3 -11
- schemathesis/transport/serialization.py +63 -27
- schemathesis/transport/wsgi.py +1 -8
- {schemathesis-4.1.4.dist-info → schemathesis-4.2.0.dist-info}/METADATA +2 -2
- {schemathesis-4.1.4.dist-info → schemathesis-4.2.0.dist-info}/RECORD +68 -53
- schemathesis/specs/openapi/parameters.py +0 -405
- schemathesis/specs/openapi/security.py +0 -162
- {schemathesis-4.1.4.dist-info → schemathesis-4.2.0.dist-info}/WHEEL +0 -0
- {schemathesis-4.1.4.dist-info → schemathesis-4.2.0.dist-info}/entry_points.txt +0 -0
- {schemathesis-4.1.4.dist-info → schemathesis-4.2.0.dist-info}/licenses/LICENSE +0 -0
@@ -21,8 +21,15 @@ from schemathesis import auths
|
|
21
21
|
from schemathesis.auths import AuthStorage, AuthStorageMark
|
22
22
|
from schemathesis.config import GenerationConfig, ProjectConfig
|
23
23
|
from schemathesis.core import INJECTED_PATH_PARAMETER_KEY, NOT_SET, NotSet, SpecificationFeature, media_types
|
24
|
-
from schemathesis.core.errors import
|
24
|
+
from schemathesis.core.errors import (
|
25
|
+
InfiniteRecursiveReference,
|
26
|
+
InvalidSchema,
|
27
|
+
MalformedMediaType,
|
28
|
+
SerializationNotPossible,
|
29
|
+
UnresolvableReference,
|
30
|
+
)
|
25
31
|
from schemathesis.core.marks import Mark
|
32
|
+
from schemathesis.core.parameters import LOCATION_TO_CONTAINER, ParameterLocation
|
26
33
|
from schemathesis.core.transforms import deepclone
|
27
34
|
from schemathesis.core.transport import prepare_urlencoded
|
28
35
|
from schemathesis.core.validation import has_invalid_characters, is_latin_1_encodable
|
@@ -34,7 +41,6 @@ from schemathesis.generation.hypothesis.given import GivenInput
|
|
34
41
|
from schemathesis.generation.meta import (
|
35
42
|
CaseMetadata,
|
36
43
|
ComponentInfo,
|
37
|
-
ComponentKind,
|
38
44
|
CoveragePhaseData,
|
39
45
|
GenerationInfo,
|
40
46
|
PhaseInfo,
|
@@ -263,16 +269,15 @@ def generate_example_cases(
|
|
263
269
|
**kwargs: Any,
|
264
270
|
) -> Generator[Case]:
|
265
271
|
"""Add examples to the Hypothesis test, if they are specified in the schema."""
|
266
|
-
from hypothesis_jsonschema._canonicalise import HypothesisRefResolutionError
|
267
|
-
|
268
272
|
try:
|
269
273
|
result: list[Case] = [
|
270
274
|
examples.generate_one(strategy) for strategy in operation.get_strategies_from_examples(**kwargs)
|
271
275
|
]
|
272
276
|
except (
|
273
277
|
InvalidSchema,
|
274
|
-
|
278
|
+
InfiniteRecursiveReference,
|
275
279
|
Unsatisfiable,
|
280
|
+
UnresolvableReference,
|
276
281
|
SerializationNotPossible,
|
277
282
|
SchemaError,
|
278
283
|
) as exc:
|
@@ -283,6 +288,10 @@ def generate_example_cases(
|
|
283
288
|
NonSerializableMark.set(test, exc)
|
284
289
|
if isinstance(exc, SchemaError):
|
285
290
|
InvalidRegexMark.set(test, exc)
|
291
|
+
if isinstance(exc, InfiniteRecursiveReference):
|
292
|
+
InfiniteRecursiveReferenceMark.set(test, exc)
|
293
|
+
if isinstance(exc, UnresolvableReference):
|
294
|
+
UnresolvableReferenceMark.set(test, exc)
|
286
295
|
|
287
296
|
if fill_missing and not result:
|
288
297
|
strategy = operation.as_strategy()
|
@@ -347,7 +356,7 @@ def generate_coverage_cases(
|
|
347
356
|
unexpected_methods: set[str],
|
348
357
|
generation_config: GenerationConfig,
|
349
358
|
) -> Generator[Case]:
|
350
|
-
from schemathesis.
|
359
|
+
from schemathesis.core.parameters import LOCATION_TO_CONTAINER
|
351
360
|
|
352
361
|
auth_context = auths.AuthContext(
|
353
362
|
operation=operation,
|
@@ -397,7 +406,7 @@ class Template:
|
|
397
406
|
__slots__ = ("_components", "_template", "_serializers")
|
398
407
|
|
399
408
|
def __init__(self, serializers: dict[str, Callable]) -> None:
|
400
|
-
self._components: dict[
|
409
|
+
self._components: dict[ParameterLocation, ComponentInfo] = {}
|
401
410
|
self._template: dict[str, Any] = {}
|
402
411
|
self._serializers = serializers
|
403
412
|
|
@@ -410,24 +419,20 @@ class Template:
|
|
410
419
|
def get(self, key: str, default: Any = None) -> dict:
|
411
420
|
return self._template.get(key, default)
|
412
421
|
|
413
|
-
def add_parameter(self, location:
|
414
|
-
|
415
|
-
|
416
|
-
component_name = LOCATION_TO_CONTAINER[location]
|
417
|
-
kind = ComponentKind(component_name)
|
418
|
-
info = self._components.get(kind)
|
422
|
+
def add_parameter(self, location: ParameterLocation, name: str, value: coverage.GeneratedValue) -> None:
|
423
|
+
info = self._components.get(location)
|
419
424
|
if info is None:
|
420
|
-
self._components[
|
425
|
+
self._components[location] = ComponentInfo(mode=value.generation_mode)
|
421
426
|
elif value.generation_mode == GenerationMode.NEGATIVE:
|
422
427
|
info.mode = GenerationMode.NEGATIVE
|
423
428
|
|
424
|
-
container = self._template.setdefault(
|
429
|
+
container = self._template.setdefault(location.container_name, {})
|
425
430
|
container[name] = value.value
|
426
431
|
|
427
432
|
def set_body(self, body: coverage.GeneratedValue, media_type: str) -> None:
|
428
433
|
self._template["body"] = body.value
|
429
434
|
self._template["media_type"] = media_type
|
430
|
-
self._components[
|
435
|
+
self._components[ParameterLocation.BODY] = ComponentInfo(mode=body.generation_mode)
|
431
436
|
|
432
437
|
def _serialize(self, kwargs: dict[str, Any]) -> dict[str, Any]:
|
433
438
|
from schemathesis.specs.openapi._hypothesis import quote_all
|
@@ -454,21 +459,24 @@ class Template:
|
|
454
459
|
def with_body(self, *, media_type: str, value: coverage.GeneratedValue) -> TemplateValue:
|
455
460
|
kwargs = {**self._template, "media_type": media_type, "body": value.value}
|
456
461
|
kwargs = self._serialize(kwargs)
|
457
|
-
components = {**self._components,
|
462
|
+
components = {**self._components, ParameterLocation.BODY: ComponentInfo(mode=value.generation_mode)}
|
458
463
|
return TemplateValue(kwargs=kwargs, components=components)
|
459
464
|
|
460
|
-
def with_parameter(
|
461
|
-
|
462
|
-
|
463
|
-
|
464
|
-
|
465
|
-
|
466
|
-
|
465
|
+
def with_parameter(
|
466
|
+
self, *, location: ParameterLocation, name: str, value: coverage.GeneratedValue
|
467
|
+
) -> TemplateValue:
|
468
|
+
container = self._template[location.container_name]
|
469
|
+
return self.with_location(
|
470
|
+
location=location,
|
471
|
+
value={**container, name: value.value},
|
472
|
+
generation_mode=value.generation_mode,
|
467
473
|
)
|
468
474
|
|
469
|
-
def
|
470
|
-
|
471
|
-
|
475
|
+
def with_location(
|
476
|
+
self, *, location: ParameterLocation, value: Any, generation_mode: GenerationMode
|
477
|
+
) -> TemplateValue:
|
478
|
+
kwargs = {**self._template, location.container_name: value}
|
479
|
+
components = {**self._components, location: ComponentInfo(mode=generation_mode)}
|
472
480
|
kwargs = self._serialize(kwargs)
|
473
481
|
return TemplateValue(kwargs=kwargs, components=components)
|
474
482
|
|
@@ -476,7 +484,7 @@ class Template:
|
|
476
484
|
@dataclass
|
477
485
|
class TemplateValue:
|
478
486
|
kwargs: dict[str, Any]
|
479
|
-
components: dict[
|
487
|
+
components: dict[ParameterLocation, ComponentInfo]
|
480
488
|
|
481
489
|
__slots__ = ("kwargs", "components")
|
482
490
|
|
@@ -510,17 +518,17 @@ def _iter_coverage_cases(
|
|
510
518
|
generation_config: GenerationConfig,
|
511
519
|
) -> Generator[Case, None, None]:
|
512
520
|
from schemathesis.specs.openapi._hypothesis import _build_custom_formats
|
513
|
-
from schemathesis.specs.openapi.
|
514
|
-
from schemathesis.specs.openapi.
|
521
|
+
from schemathesis.specs.openapi.examples import find_matching_in_responses
|
522
|
+
from schemathesis.specs.openapi.media_types import MEDIA_TYPES
|
515
523
|
from schemathesis.specs.openapi.schemas import BaseOpenAPISchema
|
516
524
|
from schemathesis.specs.openapi.serialization import get_serializers_for_operation
|
517
525
|
|
518
|
-
generators: dict[tuple[
|
526
|
+
generators: dict[tuple[ParameterLocation, str], Generator[coverage.GeneratedValue, None, None]] = {}
|
519
527
|
serializers = get_serializers_for_operation(operation)
|
520
528
|
template = Template(serializers)
|
521
529
|
|
522
530
|
instant = Instant()
|
523
|
-
responses =
|
531
|
+
responses = list(operation.responses.iter_examples())
|
524
532
|
# NOTE: The HEAD method is excluded
|
525
533
|
unexpected_methods = unexpected_methods or {"get", "put", "post", "delete", "options", "patch", "trace"}
|
526
534
|
custom_formats = _build_custom_formats(generation_config)
|
@@ -528,17 +536,23 @@ def _iter_coverage_cases(
|
|
528
536
|
seen_negative = coverage.HashSet()
|
529
537
|
seen_positive = coverage.HashSet()
|
530
538
|
assert isinstance(operation.schema, BaseOpenAPISchema)
|
531
|
-
validator_cls = operation.schema.
|
539
|
+
validator_cls = operation.schema.adapter.jsonschema_validator_cls
|
532
540
|
|
533
541
|
for parameter in operation.iter_parameters():
|
534
542
|
location = parameter.location
|
535
543
|
name = parameter.name
|
536
|
-
schema = parameter.
|
544
|
+
schema = parameter.unoptimized_schema
|
545
|
+
examples = parameter.examples
|
546
|
+
if examples:
|
547
|
+
schema = dict(schema)
|
548
|
+
schema["examples"] = examples
|
537
549
|
for value in find_matching_in_responses(responses, parameter.name):
|
538
550
|
schema.setdefault("examples", []).append(value)
|
539
551
|
gen = coverage.cover_schema_iter(
|
540
552
|
coverage.CoverageContext(
|
553
|
+
root_schema=schema,
|
541
554
|
location=location,
|
555
|
+
media_type=None,
|
542
556
|
generation_modes=generation_modes,
|
543
557
|
is_required=parameter.is_required,
|
544
558
|
custom_formats=custom_formats,
|
@@ -548,6 +562,27 @@ def _iter_coverage_cases(
|
|
548
562
|
)
|
549
563
|
value = next(gen, NOT_SET)
|
550
564
|
if isinstance(value, NotSet):
|
565
|
+
if location == ParameterLocation.PATH:
|
566
|
+
# Can't skip path parameters - they should be filled
|
567
|
+
schema = dict(schema)
|
568
|
+
schema.setdefault("type", "string")
|
569
|
+
schema.setdefault("minLength", 1)
|
570
|
+
gen = coverage.cover_schema_iter(
|
571
|
+
coverage.CoverageContext(
|
572
|
+
root_schema=schema,
|
573
|
+
location=location,
|
574
|
+
media_type=None,
|
575
|
+
generation_modes=[GenerationMode.POSITIVE],
|
576
|
+
is_required=parameter.is_required,
|
577
|
+
custom_formats=custom_formats,
|
578
|
+
validator_cls=validator_cls,
|
579
|
+
),
|
580
|
+
schema,
|
581
|
+
)
|
582
|
+
value = next(gen, NOT_SET)
|
583
|
+
assert not isinstance(value, NotSet), f"It should always be possible: {schema!r}"
|
584
|
+
template.add_parameter(location, name, value)
|
585
|
+
continue
|
551
586
|
continue
|
552
587
|
template.add_parameter(location, name, value)
|
553
588
|
generators[(location, name)] = gen
|
@@ -555,15 +590,24 @@ def _iter_coverage_cases(
|
|
555
590
|
if operation.body:
|
556
591
|
for body in operation.body:
|
557
592
|
instant = Instant()
|
558
|
-
schema = body.
|
559
|
-
|
560
|
-
definition = body.definition if isinstance(body.definition, dict) else {}
|
561
|
-
examples = [example["value"] for example in definition.get("examples", {}).values() if "value" in example]
|
593
|
+
schema = body.unoptimized_schema
|
594
|
+
examples = body.examples
|
562
595
|
if examples:
|
563
|
-
schema
|
596
|
+
schema = dict(schema)
|
597
|
+
# User-registered media types should only handle text / binary data
|
598
|
+
if body.media_type in MEDIA_TYPES:
|
599
|
+
schema["examples"] = [example for example in examples if isinstance(example, (str, bytes))]
|
600
|
+
else:
|
601
|
+
schema["examples"] = examples
|
602
|
+
try:
|
603
|
+
media_type = media_types.parse(body.media_type)
|
604
|
+
except MalformedMediaType:
|
605
|
+
media_type = None
|
564
606
|
gen = coverage.cover_schema_iter(
|
565
607
|
coverage.CoverageContext(
|
566
|
-
|
608
|
+
root_schema=schema,
|
609
|
+
location=ParameterLocation.BODY,
|
610
|
+
media_type=media_type,
|
567
611
|
generation_modes=generation_modes,
|
568
612
|
is_required=body.is_required,
|
569
613
|
custom_formats=custom_formats,
|
@@ -591,7 +635,7 @@ def _iter_coverage_cases(
|
|
591
635
|
description=value.description,
|
592
636
|
location=value.location,
|
593
637
|
parameter=body.media_type,
|
594
|
-
parameter_location=
|
638
|
+
parameter_location=ParameterLocation.BODY,
|
595
639
|
),
|
596
640
|
),
|
597
641
|
)
|
@@ -613,7 +657,7 @@ def _iter_coverage_cases(
|
|
613
657
|
description=next_value.description,
|
614
658
|
location=next_value.location,
|
615
659
|
parameter=body.media_type,
|
616
|
-
parameter_location=
|
660
|
+
parameter_location=ParameterLocation.BODY,
|
617
661
|
),
|
618
662
|
),
|
619
663
|
)
|
@@ -689,8 +733,8 @@ def _iter_coverage_cases(
|
|
689
733
|
# I.e. contains just `default` value without any other keywords
|
690
734
|
value = container.get(parameter.name, NOT_SET)
|
691
735
|
if value is not NOT_SET:
|
692
|
-
data = template.
|
693
|
-
|
736
|
+
data = template.with_location(
|
737
|
+
location=ParameterLocation.QUERY,
|
694
738
|
value={**container, parameter.name: [value, value]},
|
695
739
|
generation_mode=GenerationMode.NEGATIVE,
|
696
740
|
)
|
@@ -702,20 +746,19 @@ def _iter_coverage_cases(
|
|
702
746
|
phase=PhaseInfo.coverage(
|
703
747
|
description=f"Duplicate `{parameter.name}` query parameter",
|
704
748
|
parameter=parameter.name,
|
705
|
-
parameter_location=
|
749
|
+
parameter_location=ParameterLocation.QUERY,
|
706
750
|
),
|
707
751
|
),
|
708
752
|
)
|
709
753
|
# Generate missing required parameters
|
710
754
|
for parameter in operation.iter_parameters():
|
711
|
-
if parameter.is_required and parameter.location !=
|
755
|
+
if parameter.is_required and parameter.location != ParameterLocation.PATH:
|
712
756
|
instant = Instant()
|
713
757
|
name = parameter.name
|
714
758
|
location = parameter.location
|
715
|
-
|
716
|
-
|
717
|
-
|
718
|
-
container_name=container_name,
|
759
|
+
container = template.get(location.container_name, {})
|
760
|
+
data = template.with_location(
|
761
|
+
location=location,
|
719
762
|
value={k: v for k, v in container.items() if k != name},
|
720
763
|
generation_mode=GenerationMode.NEGATIVE,
|
721
764
|
)
|
@@ -727,7 +770,7 @@ def _iter_coverage_cases(
|
|
727
770
|
generation=GenerationInfo(time=instant.elapsed, mode=GenerationMode.NEGATIVE),
|
728
771
|
components=data.components,
|
729
772
|
phase=PhaseInfo.coverage(
|
730
|
-
description=f"Missing `{name}` at {location}",
|
773
|
+
description=f"Missing `{name}` at {location.value}",
|
731
774
|
parameter=name,
|
732
775
|
parameter_location=location,
|
733
776
|
),
|
@@ -735,14 +778,14 @@ def _iter_coverage_cases(
|
|
735
778
|
)
|
736
779
|
# Generate combinations for each location
|
737
780
|
for location, parameter_set in [
|
738
|
-
(
|
739
|
-
(
|
740
|
-
(
|
781
|
+
(ParameterLocation.QUERY, operation.query),
|
782
|
+
(ParameterLocation.HEADER, operation.headers),
|
783
|
+
(ParameterLocation.COOKIE, operation.cookies),
|
741
784
|
]:
|
742
785
|
if not parameter_set:
|
743
786
|
continue
|
744
787
|
|
745
|
-
container_name =
|
788
|
+
container_name = location.container_name
|
746
789
|
base_container = template.get(container_name, {})
|
747
790
|
|
748
791
|
# Get required and optional parameters
|
@@ -754,15 +797,12 @@ def _iter_coverage_cases(
|
|
754
797
|
def make_case(
|
755
798
|
container_values: dict,
|
756
799
|
description: str,
|
757
|
-
_location:
|
758
|
-
_container_name: str,
|
800
|
+
_location: ParameterLocation,
|
759
801
|
_parameter: str | None,
|
760
802
|
_generation_mode: GenerationMode,
|
761
803
|
_instant: Instant,
|
762
804
|
) -> Case:
|
763
|
-
data = template.
|
764
|
-
container_name=_container_name, value=container_values, generation_mode=_generation_mode
|
765
|
-
)
|
805
|
+
data = template.with_location(location=_location, value=container_values, generation_mode=_generation_mode)
|
766
806
|
return operation.Case(
|
767
807
|
**data.kwargs,
|
768
808
|
_meta=CaseMetadata(
|
@@ -784,7 +824,7 @@ def _iter_coverage_cases(
|
|
784
824
|
) -> dict[str, Any]:
|
785
825
|
return {
|
786
826
|
"properties": {
|
787
|
-
parameter.name: parameter.
|
827
|
+
parameter.name: parameter.optimized_schema
|
788
828
|
for parameter in _parameter_set
|
789
829
|
if parameter.name in combination
|
790
830
|
},
|
@@ -793,12 +833,14 @@ def _iter_coverage_cases(
|
|
793
833
|
}
|
794
834
|
|
795
835
|
def _yield_negative(
|
796
|
-
subschema: dict[str, Any], _location:
|
836
|
+
subschema: dict[str, Any], _location: ParameterLocation, is_required: bool
|
797
837
|
) -> Generator[Case, None, None]:
|
798
838
|
iterator = iter(
|
799
839
|
coverage.cover_schema_iter(
|
800
840
|
coverage.CoverageContext(
|
841
|
+
root_schema=subschema,
|
801
842
|
location=_location,
|
843
|
+
media_type=None,
|
802
844
|
generation_modes=[GenerationMode.NEGATIVE],
|
803
845
|
is_required=is_required,
|
804
846
|
custom_formats=custom_formats,
|
@@ -815,7 +857,6 @@ def _iter_coverage_cases(
|
|
815
857
|
more.value,
|
816
858
|
more.description,
|
817
859
|
_location,
|
818
|
-
_container_name,
|
819
860
|
more.parameter,
|
820
861
|
GenerationMode.NEGATIVE,
|
821
862
|
instant,
|
@@ -831,14 +872,13 @@ def _iter_coverage_cases(
|
|
831
872
|
only_required,
|
832
873
|
"Only required properties",
|
833
874
|
location,
|
834
|
-
container_name,
|
835
875
|
None,
|
836
876
|
GenerationMode.POSITIVE,
|
837
877
|
Instant(),
|
838
878
|
)
|
839
879
|
if GenerationMode.NEGATIVE in generation_modes:
|
840
880
|
subschema = _combination_schema(only_required, required, parameter_set)
|
841
|
-
for case in _yield_negative(subschema, location,
|
881
|
+
for case in _yield_negative(subschema, location, is_required=bool(required)):
|
842
882
|
kwargs = _case_to_kwargs(case)
|
843
883
|
if not seen_negative.insert(kwargs):
|
844
884
|
continue
|
@@ -858,14 +898,13 @@ def _iter_coverage_cases(
|
|
858
898
|
combo,
|
859
899
|
f"All required properties and optional '{opt_param}'",
|
860
900
|
location,
|
861
|
-
container_name,
|
862
901
|
None,
|
863
902
|
GenerationMode.POSITIVE,
|
864
903
|
Instant(),
|
865
904
|
)
|
866
905
|
if GenerationMode.NEGATIVE in generation_modes:
|
867
906
|
subschema = _combination_schema(combo, required, parameter_set)
|
868
|
-
for case in _yield_negative(subschema, location,
|
907
|
+
for case in _yield_negative(subschema, location, is_required=bool(required)):
|
869
908
|
assert case.meta is not None
|
870
909
|
assert isinstance(case.meta.phase.data, CoveragePhaseData)
|
871
910
|
# Already generated in one of the blocks above
|
@@ -884,7 +923,6 @@ def _iter_coverage_cases(
|
|
884
923
|
combo,
|
885
924
|
f"All required and {size} optional properties",
|
886
925
|
location,
|
887
|
-
container_name,
|
888
926
|
None,
|
889
927
|
GenerationMode.POSITIVE,
|
890
928
|
Instant(),
|
@@ -892,8 +930,6 @@ def _iter_coverage_cases(
|
|
892
930
|
|
893
931
|
|
894
932
|
def _case_to_kwargs(case: Case) -> dict:
|
895
|
-
from schemathesis.specs.openapi.constants import LOCATION_TO_CONTAINER
|
896
|
-
|
897
933
|
kwargs = {}
|
898
934
|
for container_name in LOCATION_TO_CONTAINER.values():
|
899
935
|
value = getattr(case, container_name)
|
@@ -915,3 +951,5 @@ NonSerializableMark = Mark[SerializationNotPossible](attr_name="non_serializable
|
|
915
951
|
InvalidRegexMark = Mark[SchemaError](attr_name="invalid_regex")
|
916
952
|
InvalidHeadersExampleMark = Mark[dict[str, str]](attr_name="invalid_example_header")
|
917
953
|
MissingPathParameters = Mark[InvalidSchema](attr_name="missing_path_parameters")
|
954
|
+
InfiniteRecursiveReferenceMark = Mark[InfiniteRecursiveReference](attr_name="infinite_recursive_reference")
|
955
|
+
UnresolvableReferenceMark = Mark[UnresolvableReference](attr_name="unresolvable_reference")
|
schemathesis/generation/meta.py
CHANGED
@@ -3,6 +3,7 @@ from __future__ import annotations
|
|
3
3
|
from dataclasses import dataclass
|
4
4
|
from enum import Enum
|
5
5
|
|
6
|
+
from schemathesis.core.parameters import ParameterLocation
|
6
7
|
from schemathesis.generation import GenerationMode
|
7
8
|
|
8
9
|
|
@@ -15,16 +16,6 @@ class TestPhase(str, Enum):
|
|
15
16
|
STATEFUL = "stateful"
|
16
17
|
|
17
18
|
|
18
|
-
class ComponentKind(str, Enum):
|
19
|
-
"""Components that can be generated."""
|
20
|
-
|
21
|
-
QUERY = "query"
|
22
|
-
PATH_PARAMETERS = "path_parameters"
|
23
|
-
HEADERS = "headers"
|
24
|
-
COOKIES = "cookies"
|
25
|
-
BODY = "body"
|
26
|
-
|
27
|
-
|
28
19
|
@dataclass
|
29
20
|
class ComponentInfo:
|
30
21
|
"""Information about how a specific component was generated."""
|
@@ -62,7 +53,7 @@ class CoveragePhaseData:
|
|
62
53
|
description: str
|
63
54
|
location: str | None
|
64
55
|
parameter: str | None
|
65
|
-
parameter_location:
|
56
|
+
parameter_location: ParameterLocation | None
|
66
57
|
|
67
58
|
__slots__ = ("description", "location", "parameter", "parameter_location")
|
68
59
|
|
@@ -82,7 +73,7 @@ class PhaseInfo:
|
|
82
73
|
description: str,
|
83
74
|
location: str | None = None,
|
84
75
|
parameter: str | None = None,
|
85
|
-
parameter_location:
|
76
|
+
parameter_location: ParameterLocation | None = None,
|
86
77
|
) -> PhaseInfo:
|
87
78
|
return cls(
|
88
79
|
name=TestPhase.COVERAGE,
|
@@ -111,7 +102,7 @@ class CaseMetadata:
|
|
111
102
|
"""Complete metadata for generated cases."""
|
112
103
|
|
113
104
|
generation: GenerationInfo
|
114
|
-
components: dict[
|
105
|
+
components: dict[ParameterLocation, ComponentInfo]
|
115
106
|
phase: PhaseInfo
|
116
107
|
|
117
108
|
__slots__ = ("generation", "components", "phase")
|
@@ -119,7 +110,7 @@ class CaseMetadata:
|
|
119
110
|
def __init__(
|
120
111
|
self,
|
121
112
|
generation: GenerationInfo,
|
122
|
-
components: dict[
|
113
|
+
components: dict[ParameterLocation, ComponentInfo],
|
123
114
|
phase: PhaseInfo,
|
124
115
|
) -> None:
|
125
116
|
self.generation = generation
|
@@ -5,12 +5,12 @@ from dataclasses import dataclass
|
|
5
5
|
from typing import TYPE_CHECKING, Any, Iterator
|
6
6
|
|
7
7
|
from schemathesis.config import ProjectConfig
|
8
|
+
from schemathesis.core.parameters import ParameterLocation
|
8
9
|
from schemathesis.core.transforms import diff
|
9
|
-
from schemathesis.generation.meta import ComponentKind
|
10
10
|
|
11
11
|
if TYPE_CHECKING:
|
12
12
|
from schemathesis.generation.case import Case
|
13
|
-
from schemathesis.schemas import APIOperation,
|
13
|
+
from schemathesis.schemas import APIOperation, OperationParameter
|
14
14
|
|
15
15
|
|
16
16
|
@dataclass
|
@@ -24,21 +24,21 @@ class Override:
|
|
24
24
|
|
25
25
|
__slots__ = ("query", "headers", "cookies", "path_parameters")
|
26
26
|
|
27
|
-
def items(self) -> Iterator[tuple[
|
27
|
+
def items(self) -> Iterator[tuple[ParameterLocation, dict[str, str]]]:
|
28
28
|
for key, value in (
|
29
|
-
(
|
30
|
-
(
|
31
|
-
(
|
32
|
-
(
|
29
|
+
(ParameterLocation.QUERY, self.query),
|
30
|
+
(ParameterLocation.HEADER, self.headers),
|
31
|
+
(ParameterLocation.COOKIE, self.cookies),
|
32
|
+
(ParameterLocation.PATH, self.path_parameters),
|
33
33
|
):
|
34
34
|
if value:
|
35
35
|
yield key, value
|
36
36
|
|
37
37
|
@classmethod
|
38
|
-
def from_components(cls, components: dict[
|
38
|
+
def from_components(cls, components: dict[ParameterLocation, StoredValue], case: Case) -> Override:
|
39
39
|
return Override(
|
40
40
|
**{
|
41
|
-
kind.
|
41
|
+
kind.container_name: get_component_diff(stored=stored, current=getattr(case, kind.container_name))
|
42
42
|
for kind, stored in components.items()
|
43
43
|
}
|
44
44
|
)
|
@@ -69,9 +69,9 @@ def for_operation(config: ProjectConfig, *, operation: APIOperation) -> Override
|
|
69
69
|
return output
|
70
70
|
|
71
71
|
|
72
|
-
def _get_override_value(param:
|
72
|
+
def _get_override_value(param: OperationParameter, parameters: dict[str, Any]) -> Any:
|
73
73
|
key = param.name
|
74
|
-
full_key = f"{param.location}.{param.name}"
|
74
|
+
full_key = f"{param.location.value}.{param.name}"
|
75
75
|
if key in parameters:
|
76
76
|
return parameters[key]
|
77
77
|
elif full_key in parameters:
|
@@ -102,17 +102,17 @@ def get_component_diff(stored: StoredValue, current: dict[str, Any] | None) -> d
|
|
102
102
|
return current
|
103
103
|
|
104
104
|
|
105
|
-
def store_components(case: Case) -> dict[
|
105
|
+
def store_components(case: Case) -> dict[ParameterLocation, StoredValue]:
|
106
106
|
"""Store original component states for a test case."""
|
107
107
|
return {
|
108
108
|
kind: StoredValue(
|
109
|
-
value=store_original_state(getattr(case, kind.
|
109
|
+
value=store_original_state(getattr(case, kind.container_name)),
|
110
110
|
is_generated=bool(case.meta and kind in case.meta.components),
|
111
111
|
)
|
112
112
|
for kind in [
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
113
|
+
ParameterLocation.QUERY,
|
114
|
+
ParameterLocation.HEADER,
|
115
|
+
ParameterLocation.COOKIE,
|
116
|
+
ParameterLocation.PATH,
|
117
117
|
]
|
118
118
|
}
|
schemathesis/pytest/lazy.py
CHANGED
@@ -187,7 +187,7 @@ class LazySchema:
|
|
187
187
|
def as_strategy_kwargs(_operation: APIOperation) -> dict[str, Any]:
|
188
188
|
override = overrides.for_operation(config=schema.config, operation=_operation)
|
189
189
|
|
190
|
-
return {location: entry for location, entry in override.items() if entry}
|
190
|
+
return {location.container_name: entry for location, entry in override.items() if entry}
|
191
191
|
|
192
192
|
tests = list(
|
193
193
|
get_all_tests(
|
schemathesis/pytest/plugin.py
CHANGED
@@ -14,7 +14,6 @@ from jsonschema.exceptions import SchemaError
|
|
14
14
|
|
15
15
|
from schemathesis.core.control import SkipTest
|
16
16
|
from schemathesis.core.errors import (
|
17
|
-
RECURSIVE_REFERENCE_ERROR_MESSAGE,
|
18
17
|
SERIALIZERS_SUGGESTION_MESSAGE,
|
19
18
|
IncorrectUsage,
|
20
19
|
InvalidHeadersExample,
|
@@ -147,7 +146,7 @@ class SchemathesisCase(PyCollector):
|
|
147
146
|
if override is not None:
|
148
147
|
for location, entry in override.items():
|
149
148
|
if entry:
|
150
|
-
as_strategy_kwargs[location] = entry
|
149
|
+
as_strategy_kwargs[location.container_name] = entry
|
151
150
|
modes = []
|
152
151
|
phases = self.schema.config.phases_for(operation=operation)
|
153
152
|
if phases.examples.enabled:
|
@@ -293,8 +292,6 @@ def pytest_pyfunc_call(pyfuncitem): # type:ignore
|
|
293
292
|
|
294
293
|
For example - kwargs validation is failed for some strategy.
|
295
294
|
"""
|
296
|
-
from hypothesis_jsonschema._canonicalise import HypothesisRefResolutionError
|
297
|
-
|
298
295
|
from schemathesis.generation.hypothesis.builder import (
|
299
296
|
InvalidHeadersExampleMark,
|
300
297
|
InvalidRegexMark,
|
@@ -312,8 +309,6 @@ def pytest_pyfunc_call(pyfuncitem): # type:ignore
|
|
312
309
|
if "Inconsistent args" in str(exc) and "@example()" in str(exc):
|
313
310
|
raise IncorrectUsage(GIVEN_AND_EXPLICIT_EXAMPLES_ERROR_MESSAGE) from None
|
314
311
|
raise InvalidSchema(exc.args[0]) from None
|
315
|
-
except HypothesisRefResolutionError:
|
316
|
-
pytest.skip(RECURSIVE_REFERENCE_ERROR_MESSAGE)
|
317
312
|
except (SkipTest, unittest.SkipTest) as exc:
|
318
313
|
if UnsatisfiableExampleMark.is_set(pyfuncitem.obj):
|
319
314
|
raise Unsatisfiable("Failed to generate test cases from examples for this API operation") from None
|