schemathesis 3.15.4__py3-none-any.whl → 4.4.2__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/__init__.py +53 -25
- schemathesis/auths.py +507 -0
- schemathesis/checks.py +190 -25
- schemathesis/cli/__init__.py +27 -1219
- schemathesis/cli/__main__.py +4 -0
- schemathesis/cli/commands/__init__.py +133 -0
- schemathesis/cli/commands/data.py +10 -0
- schemathesis/cli/commands/run/__init__.py +602 -0
- schemathesis/cli/commands/run/context.py +228 -0
- schemathesis/cli/commands/run/events.py +60 -0
- schemathesis/cli/commands/run/executor.py +157 -0
- schemathesis/cli/commands/run/filters.py +53 -0
- schemathesis/cli/commands/run/handlers/__init__.py +46 -0
- schemathesis/cli/commands/run/handlers/base.py +45 -0
- schemathesis/cli/commands/run/handlers/cassettes.py +464 -0
- schemathesis/cli/commands/run/handlers/junitxml.py +60 -0
- schemathesis/cli/commands/run/handlers/output.py +1750 -0
- schemathesis/cli/commands/run/loaders.py +118 -0
- schemathesis/cli/commands/run/validation.py +256 -0
- schemathesis/cli/constants.py +5 -0
- schemathesis/cli/core.py +19 -0
- schemathesis/cli/ext/fs.py +16 -0
- schemathesis/cli/ext/groups.py +203 -0
- schemathesis/cli/ext/options.py +81 -0
- schemathesis/config/__init__.py +202 -0
- schemathesis/config/_auth.py +51 -0
- schemathesis/config/_checks.py +268 -0
- schemathesis/config/_diff_base.py +101 -0
- schemathesis/config/_env.py +21 -0
- schemathesis/config/_error.py +163 -0
- schemathesis/config/_generation.py +157 -0
- schemathesis/config/_health_check.py +24 -0
- schemathesis/config/_operations.py +335 -0
- schemathesis/config/_output.py +171 -0
- schemathesis/config/_parameters.py +19 -0
- schemathesis/config/_phases.py +253 -0
- schemathesis/config/_projects.py +543 -0
- schemathesis/config/_rate_limit.py +17 -0
- schemathesis/config/_report.py +120 -0
- schemathesis/config/_validator.py +9 -0
- schemathesis/config/_warnings.py +89 -0
- schemathesis/config/schema.json +975 -0
- schemathesis/core/__init__.py +72 -0
- schemathesis/core/adapter.py +34 -0
- schemathesis/core/compat.py +32 -0
- schemathesis/core/control.py +2 -0
- schemathesis/core/curl.py +100 -0
- schemathesis/core/deserialization.py +210 -0
- schemathesis/core/errors.py +588 -0
- schemathesis/core/failures.py +316 -0
- schemathesis/core/fs.py +19 -0
- schemathesis/core/hooks.py +20 -0
- schemathesis/core/jsonschema/__init__.py +13 -0
- schemathesis/core/jsonschema/bundler.py +183 -0
- schemathesis/core/jsonschema/keywords.py +40 -0
- schemathesis/core/jsonschema/references.py +222 -0
- schemathesis/core/jsonschema/types.py +41 -0
- schemathesis/core/lazy_import.py +15 -0
- schemathesis/core/loaders.py +107 -0
- schemathesis/core/marks.py +66 -0
- schemathesis/core/media_types.py +79 -0
- schemathesis/core/output/__init__.py +46 -0
- schemathesis/core/output/sanitization.py +54 -0
- schemathesis/core/parameters.py +45 -0
- schemathesis/core/rate_limit.py +60 -0
- schemathesis/core/registries.py +34 -0
- schemathesis/core/result.py +27 -0
- schemathesis/core/schema_analysis.py +17 -0
- schemathesis/core/shell.py +203 -0
- schemathesis/core/transforms.py +144 -0
- schemathesis/core/transport.py +223 -0
- schemathesis/core/validation.py +73 -0
- schemathesis/core/version.py +7 -0
- schemathesis/engine/__init__.py +28 -0
- schemathesis/engine/context.py +152 -0
- schemathesis/engine/control.py +44 -0
- schemathesis/engine/core.py +201 -0
- schemathesis/engine/errors.py +446 -0
- schemathesis/engine/events.py +284 -0
- schemathesis/engine/observations.py +42 -0
- schemathesis/engine/phases/__init__.py +108 -0
- schemathesis/engine/phases/analysis.py +28 -0
- schemathesis/engine/phases/probes.py +172 -0
- schemathesis/engine/phases/stateful/__init__.py +68 -0
- schemathesis/engine/phases/stateful/_executor.py +364 -0
- schemathesis/engine/phases/stateful/context.py +85 -0
- schemathesis/engine/phases/unit/__init__.py +220 -0
- schemathesis/engine/phases/unit/_executor.py +459 -0
- schemathesis/engine/phases/unit/_pool.py +82 -0
- schemathesis/engine/recorder.py +254 -0
- schemathesis/errors.py +47 -0
- schemathesis/filters.py +395 -0
- schemathesis/generation/__init__.py +25 -0
- schemathesis/generation/case.py +478 -0
- schemathesis/generation/coverage.py +1528 -0
- schemathesis/generation/hypothesis/__init__.py +121 -0
- schemathesis/generation/hypothesis/builder.py +992 -0
- schemathesis/generation/hypothesis/examples.py +56 -0
- schemathesis/generation/hypothesis/given.py +66 -0
- schemathesis/generation/hypothesis/reporting.py +285 -0
- schemathesis/generation/meta.py +227 -0
- schemathesis/generation/metrics.py +93 -0
- schemathesis/generation/modes.py +20 -0
- schemathesis/generation/overrides.py +127 -0
- schemathesis/generation/stateful/__init__.py +37 -0
- schemathesis/generation/stateful/state_machine.py +294 -0
- schemathesis/graphql/__init__.py +15 -0
- schemathesis/graphql/checks.py +109 -0
- schemathesis/graphql/loaders.py +285 -0
- schemathesis/hooks.py +270 -91
- schemathesis/openapi/__init__.py +13 -0
- schemathesis/openapi/checks.py +467 -0
- schemathesis/openapi/generation/__init__.py +0 -0
- schemathesis/openapi/generation/filters.py +72 -0
- schemathesis/openapi/loaders.py +315 -0
- schemathesis/pytest/__init__.py +5 -0
- schemathesis/pytest/control_flow.py +7 -0
- schemathesis/pytest/lazy.py +341 -0
- schemathesis/pytest/loaders.py +36 -0
- schemathesis/pytest/plugin.py +357 -0
- schemathesis/python/__init__.py +0 -0
- schemathesis/python/asgi.py +12 -0
- schemathesis/python/wsgi.py +12 -0
- schemathesis/schemas.py +682 -257
- schemathesis/specs/graphql/__init__.py +0 -1
- schemathesis/specs/graphql/nodes.py +26 -2
- schemathesis/specs/graphql/scalars.py +77 -12
- schemathesis/specs/graphql/schemas.py +367 -148
- schemathesis/specs/graphql/validation.py +33 -0
- schemathesis/specs/openapi/__init__.py +9 -1
- schemathesis/specs/openapi/_hypothesis.py +555 -318
- schemathesis/specs/openapi/adapter/__init__.py +10 -0
- schemathesis/specs/openapi/adapter/parameters.py +729 -0
- schemathesis/specs/openapi/adapter/protocol.py +59 -0
- schemathesis/specs/openapi/adapter/references.py +19 -0
- schemathesis/specs/openapi/adapter/responses.py +368 -0
- schemathesis/specs/openapi/adapter/security.py +144 -0
- schemathesis/specs/openapi/adapter/v2.py +30 -0
- schemathesis/specs/openapi/adapter/v3_0.py +30 -0
- schemathesis/specs/openapi/adapter/v3_1.py +30 -0
- schemathesis/specs/openapi/analysis.py +96 -0
- schemathesis/specs/openapi/checks.py +748 -82
- schemathesis/specs/openapi/converter.py +176 -37
- schemathesis/specs/openapi/definitions.py +599 -4
- schemathesis/specs/openapi/examples.py +581 -165
- schemathesis/specs/openapi/expressions/__init__.py +52 -5
- schemathesis/specs/openapi/expressions/extractors.py +25 -0
- schemathesis/specs/openapi/expressions/lexer.py +34 -31
- schemathesis/specs/openapi/expressions/nodes.py +97 -46
- schemathesis/specs/openapi/expressions/parser.py +35 -13
- schemathesis/specs/openapi/formats.py +122 -0
- schemathesis/specs/openapi/media_types.py +75 -0
- schemathesis/specs/openapi/negative/__init__.py +93 -73
- schemathesis/specs/openapi/negative/mutations.py +294 -103
- schemathesis/specs/openapi/negative/utils.py +0 -9
- schemathesis/specs/openapi/patterns.py +458 -0
- schemathesis/specs/openapi/references.py +60 -81
- schemathesis/specs/openapi/schemas.py +647 -666
- schemathesis/specs/openapi/serialization.py +53 -30
- schemathesis/specs/openapi/stateful/__init__.py +403 -68
- schemathesis/specs/openapi/stateful/control.py +87 -0
- schemathesis/specs/openapi/stateful/dependencies/__init__.py +232 -0
- schemathesis/specs/openapi/stateful/dependencies/inputs.py +428 -0
- schemathesis/specs/openapi/stateful/dependencies/models.py +341 -0
- schemathesis/specs/openapi/stateful/dependencies/naming.py +491 -0
- schemathesis/specs/openapi/stateful/dependencies/outputs.py +34 -0
- schemathesis/specs/openapi/stateful/dependencies/resources.py +339 -0
- schemathesis/specs/openapi/stateful/dependencies/schemas.py +447 -0
- schemathesis/specs/openapi/stateful/inference.py +254 -0
- schemathesis/specs/openapi/stateful/links.py +219 -78
- schemathesis/specs/openapi/types/__init__.py +3 -0
- schemathesis/specs/openapi/types/common.py +23 -0
- schemathesis/specs/openapi/types/v2.py +129 -0
- schemathesis/specs/openapi/types/v3.py +134 -0
- schemathesis/specs/openapi/utils.py +7 -6
- schemathesis/specs/openapi/warnings.py +75 -0
- schemathesis/transport/__init__.py +224 -0
- schemathesis/transport/asgi.py +26 -0
- schemathesis/transport/prepare.py +126 -0
- schemathesis/transport/requests.py +278 -0
- schemathesis/transport/serialization.py +329 -0
- schemathesis/transport/wsgi.py +175 -0
- schemathesis-4.4.2.dist-info/METADATA +213 -0
- schemathesis-4.4.2.dist-info/RECORD +192 -0
- {schemathesis-3.15.4.dist-info → schemathesis-4.4.2.dist-info}/WHEEL +1 -1
- schemathesis-4.4.2.dist-info/entry_points.txt +6 -0
- {schemathesis-3.15.4.dist-info → schemathesis-4.4.2.dist-info/licenses}/LICENSE +1 -1
- schemathesis/_compat.py +0 -57
- schemathesis/_hypothesis.py +0 -123
- schemathesis/auth.py +0 -214
- schemathesis/cli/callbacks.py +0 -240
- schemathesis/cli/cassettes.py +0 -351
- schemathesis/cli/context.py +0 -38
- schemathesis/cli/debug.py +0 -21
- schemathesis/cli/handlers.py +0 -11
- schemathesis/cli/junitxml.py +0 -41
- schemathesis/cli/options.py +0 -70
- schemathesis/cli/output/__init__.py +0 -1
- schemathesis/cli/output/default.py +0 -521
- schemathesis/cli/output/short.py +0 -40
- schemathesis/constants.py +0 -88
- schemathesis/exceptions.py +0 -257
- schemathesis/extra/_aiohttp.py +0 -27
- schemathesis/extra/_flask.py +0 -10
- schemathesis/extra/_server.py +0 -16
- schemathesis/extra/pytest_plugin.py +0 -251
- schemathesis/failures.py +0 -145
- schemathesis/fixups/__init__.py +0 -29
- schemathesis/fixups/fast_api.py +0 -30
- schemathesis/graphql.py +0 -5
- schemathesis/internal.py +0 -6
- schemathesis/lazy.py +0 -301
- schemathesis/models.py +0 -1113
- schemathesis/parameters.py +0 -91
- schemathesis/runner/__init__.py +0 -470
- schemathesis/runner/events.py +0 -242
- schemathesis/runner/impl/__init__.py +0 -3
- schemathesis/runner/impl/core.py +0 -791
- schemathesis/runner/impl/solo.py +0 -85
- schemathesis/runner/impl/threadpool.py +0 -367
- schemathesis/runner/serialization.py +0 -206
- schemathesis/serializers.py +0 -253
- schemathesis/service/__init__.py +0 -18
- schemathesis/service/auth.py +0 -10
- schemathesis/service/client.py +0 -62
- schemathesis/service/constants.py +0 -25
- schemathesis/service/events.py +0 -39
- schemathesis/service/handler.py +0 -46
- schemathesis/service/hosts.py +0 -74
- schemathesis/service/metadata.py +0 -42
- schemathesis/service/models.py +0 -21
- schemathesis/service/serialization.py +0 -184
- schemathesis/service/worker.py +0 -39
- schemathesis/specs/graphql/loaders.py +0 -215
- schemathesis/specs/openapi/constants.py +0 -7
- schemathesis/specs/openapi/expressions/context.py +0 -12
- schemathesis/specs/openapi/expressions/pointers.py +0 -29
- schemathesis/specs/openapi/filters.py +0 -44
- schemathesis/specs/openapi/links.py +0 -303
- schemathesis/specs/openapi/loaders.py +0 -453
- schemathesis/specs/openapi/parameters.py +0 -430
- schemathesis/specs/openapi/security.py +0 -129
- schemathesis/specs/openapi/validation.py +0 -24
- schemathesis/stateful.py +0 -358
- schemathesis/targets.py +0 -32
- schemathesis/types.py +0 -38
- schemathesis/utils.py +0 -475
- schemathesis-3.15.4.dist-info/METADATA +0 -202
- schemathesis-3.15.4.dist-info/RECORD +0 -99
- schemathesis-3.15.4.dist-info/entry_points.txt +0 -7
- /schemathesis/{extra → cli/ext}/__init__.py +0 -0
|
@@ -1,22 +1,41 @@
|
|
|
1
1
|
"""Schema mutations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
2
5
|
import enum
|
|
3
|
-
from
|
|
6
|
+
from dataclasses import dataclass
|
|
4
7
|
from functools import wraps
|
|
5
|
-
from typing import Any, Callable,
|
|
8
|
+
from typing import Any, Callable, Optional, Sequence, TypeVar
|
|
6
9
|
|
|
7
|
-
import attr
|
|
8
10
|
from hypothesis import reject
|
|
9
11
|
from hypothesis import strategies as st
|
|
10
12
|
from hypothesis.strategies._internal.featureflags import FeatureStrategy
|
|
13
|
+
from hypothesis_jsonschema._canonicalise import canonicalish
|
|
14
|
+
from typing_extensions import TypeAlias
|
|
15
|
+
|
|
16
|
+
from schemathesis.core.jsonschema import BUNDLE_STORAGE_KEY, get_type
|
|
17
|
+
from schemathesis.core.jsonschema.types import JsonSchemaObject
|
|
18
|
+
from schemathesis.core.parameters import ParameterLocation
|
|
19
|
+
from schemathesis.core.transforms import deepclone
|
|
11
20
|
|
|
12
|
-
from ..utils import is_header_location
|
|
13
21
|
from .types import Draw, Schema
|
|
14
|
-
from .utils import can_negate
|
|
22
|
+
from .utils import can_negate
|
|
15
23
|
|
|
16
24
|
T = TypeVar("T")
|
|
17
25
|
|
|
18
26
|
|
|
19
|
-
|
|
27
|
+
@dataclass
|
|
28
|
+
class MutationMetadata:
|
|
29
|
+
"""Metadata about a mutation that was applied."""
|
|
30
|
+
|
|
31
|
+
parameter: str | None
|
|
32
|
+
description: str
|
|
33
|
+
location: str | None
|
|
34
|
+
|
|
35
|
+
__slots__ = ("parameter", "description", "location")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class MutationResult(int, enum.Enum):
|
|
20
39
|
"""The result of applying some mutation to some schema.
|
|
21
40
|
|
|
22
41
|
Failing to mutate something means that by applying some mutation, it is not possible to change
|
|
@@ -28,17 +47,17 @@ class MutationResult(enum.Enum):
|
|
|
28
47
|
SUCCESS = 1
|
|
29
48
|
FAILURE = 2
|
|
30
49
|
|
|
31
|
-
def __ior__(self, other: Any) ->
|
|
50
|
+
def __ior__(self, other: Any) -> MutationResult:
|
|
32
51
|
return self | other
|
|
33
52
|
|
|
34
|
-
def __or__(self, other: Any) ->
|
|
53
|
+
def __or__(self, other: Any) -> MutationResult:
|
|
35
54
|
# Syntactic sugar to simplify handling of multiple results
|
|
36
55
|
if self == MutationResult.SUCCESS:
|
|
37
56
|
return self
|
|
38
57
|
return other
|
|
39
58
|
|
|
40
59
|
|
|
41
|
-
Mutation = Callable[["MutationContext", Draw, Schema], MutationResult]
|
|
60
|
+
Mutation: TypeAlias = Callable[["MutationContext", Draw, Schema], tuple[MutationResult, Optional[MutationMetadata]]]
|
|
42
61
|
ANY_TYPE_KEYS = {"$ref", "allOf", "anyOf", "const", "else", "enum", "if", "not", "oneOf", "then", "type"}
|
|
43
62
|
TYPE_SPECIFIC_KEYS = {
|
|
44
63
|
"number": ("multipleOf", "maximum", "exclusiveMaximum", "minimum", "exclusiveMinimum"),
|
|
@@ -58,31 +77,59 @@ TYPE_SPECIFIC_KEYS = {
|
|
|
58
77
|
}
|
|
59
78
|
|
|
60
79
|
|
|
61
|
-
@
|
|
80
|
+
@dataclass
|
|
62
81
|
class MutationContext:
|
|
63
82
|
"""Meta information about the current mutation state."""
|
|
64
83
|
|
|
65
84
|
# The original schema
|
|
66
|
-
keywords: Schema
|
|
67
|
-
non_keywords: Schema
|
|
85
|
+
keywords: Schema # only keywords
|
|
86
|
+
non_keywords: Schema # everything else
|
|
68
87
|
# Schema location within API operation (header, query, etc)
|
|
69
|
-
location:
|
|
88
|
+
location: ParameterLocation
|
|
70
89
|
# Payload media type, if available
|
|
71
|
-
media_type:
|
|
90
|
+
media_type: str | None
|
|
91
|
+
# Whether generating unexpected parameters is permitted
|
|
92
|
+
allow_extra_parameters: bool
|
|
93
|
+
|
|
94
|
+
__slots__ = ("keywords", "non_keywords", "location", "media_type", "allow_extra_parameters")
|
|
95
|
+
|
|
96
|
+
def __init__(
|
|
97
|
+
self,
|
|
98
|
+
*,
|
|
99
|
+
keywords: Schema,
|
|
100
|
+
non_keywords: Schema,
|
|
101
|
+
location: ParameterLocation,
|
|
102
|
+
media_type: str | None,
|
|
103
|
+
allow_extra_parameters: bool,
|
|
104
|
+
) -> None:
|
|
105
|
+
self.keywords = keywords
|
|
106
|
+
self.non_keywords = non_keywords
|
|
107
|
+
self.location = location
|
|
108
|
+
self.media_type = media_type
|
|
109
|
+
self.allow_extra_parameters = allow_extra_parameters
|
|
72
110
|
|
|
73
111
|
@property
|
|
74
|
-
def
|
|
75
|
-
return
|
|
112
|
+
def is_path_location(self) -> bool:
|
|
113
|
+
return self.location == ParameterLocation.PATH
|
|
76
114
|
|
|
77
115
|
@property
|
|
78
|
-
def
|
|
79
|
-
return self.location ==
|
|
116
|
+
def is_query_location(self) -> bool:
|
|
117
|
+
return self.location == ParameterLocation.QUERY
|
|
118
|
+
|
|
119
|
+
def ensure_bundle(self, schema: Schema) -> None:
|
|
120
|
+
"""Ensure schema has the bundle from context if needed.
|
|
80
121
|
|
|
81
|
-
|
|
122
|
+
This is necessary when working with nested schemas (e.g., property schemas)
|
|
123
|
+
that may contain bundled references but don't have the x-bundled key themselves.
|
|
124
|
+
"""
|
|
125
|
+
if BUNDLE_STORAGE_KEY in self.non_keywords and BUNDLE_STORAGE_KEY not in schema:
|
|
126
|
+
schema[BUNDLE_STORAGE_KEY] = self.non_keywords[BUNDLE_STORAGE_KEY]
|
|
127
|
+
|
|
128
|
+
def mutate(self, draw: Draw) -> tuple[Schema, MutationMetadata | None]:
|
|
82
129
|
# On the top level, Schemathesis creates "object" schemas for all parameter "in" values except "body", which is
|
|
83
|
-
# taken as-is. Therefore we can only apply mutations that won't change the Open API semantics of the schema.
|
|
84
|
-
mutations:
|
|
85
|
-
if self.location in (
|
|
130
|
+
# taken as-is. Therefore, we can only apply mutations that won't change the Open API semantics of the schema.
|
|
131
|
+
mutations: list[Mutation]
|
|
132
|
+
if self.location in (ParameterLocation.HEADER, ParameterLocation.COOKIE, ParameterLocation.QUERY):
|
|
86
133
|
# These objects follow this pattern:
|
|
87
134
|
# {
|
|
88
135
|
# "properties": properties,
|
|
@@ -104,24 +151,34 @@ class MutationContext:
|
|
|
104
151
|
# Body can be of any type and does not have any specific type semantic.
|
|
105
152
|
mutations = draw(ordered(get_mutations(draw, self.keywords)))
|
|
106
153
|
# Deep copy all keywords to avoid modifying the original schema
|
|
107
|
-
new_schema =
|
|
108
|
-
|
|
109
|
-
|
|
154
|
+
new_schema = deepclone(self.keywords)
|
|
155
|
+
# Add x-bundled before mutations so they can resolve bundled references
|
|
156
|
+
if BUNDLE_STORAGE_KEY in self.non_keywords:
|
|
157
|
+
new_schema[BUNDLE_STORAGE_KEY] = self.non_keywords[BUNDLE_STORAGE_KEY]
|
|
158
|
+
enabled_mutations = draw(st.shared(FeatureStrategy(), key="mutations"))
|
|
159
|
+
# Always apply at least one mutation, otherwise everything is rejected, and we'd like to avoid it
|
|
160
|
+
# for performance reasons
|
|
161
|
+
always_applied_mutation = draw(st.sampled_from(mutations))
|
|
162
|
+
result, metadata = always_applied_mutation(self, draw, new_schema)
|
|
110
163
|
for mutation in mutations:
|
|
111
|
-
if enabled_mutations.is_enabled(mutation.__name__):
|
|
112
|
-
|
|
164
|
+
if mutation is not always_applied_mutation and enabled_mutations.is_enabled(mutation.__name__):
|
|
165
|
+
mut_result, mut_metadata = mutation(self, draw, new_schema)
|
|
166
|
+
result |= mut_result
|
|
167
|
+
# Keep first successful mutation's metadata
|
|
168
|
+
if metadata is None and mut_metadata is not None:
|
|
169
|
+
metadata = mut_metadata
|
|
113
170
|
if result == MutationResult.FAILURE:
|
|
114
171
|
# If we failed to apply anything, then reject the whole case
|
|
115
|
-
reject()
|
|
172
|
+
reject()
|
|
116
173
|
new_schema.update(self.non_keywords)
|
|
117
|
-
if self.
|
|
174
|
+
if self.location.is_in_header:
|
|
118
175
|
# All headers should have names that can be sent over network
|
|
119
176
|
new_schema["propertyNames"] = {"type": "string", "format": "_header_name"}
|
|
120
177
|
for sub_schema in new_schema.get("properties", {}).values():
|
|
121
178
|
sub_schema["type"] = "string"
|
|
122
179
|
if len(sub_schema) == 1:
|
|
123
180
|
sub_schema["format"] = "_header_value"
|
|
124
|
-
if draw(st.booleans()):
|
|
181
|
+
if self.allow_extra_parameters and draw(st.booleans()):
|
|
125
182
|
# In headers, `additionalProperties` are False by default, which means that Schemathesis won't generate
|
|
126
183
|
# any headers that are not defined. This change adds the possibility of generating valid extra headers
|
|
127
184
|
new_schema["additionalProperties"] = {"type": "string", "format": "_header_value"}
|
|
@@ -134,21 +191,20 @@ class MutationContext:
|
|
|
134
191
|
and "minProperties" not in new_schema.get("not", {})
|
|
135
192
|
):
|
|
136
193
|
new_schema.setdefault("minProperties", 1)
|
|
137
|
-
return new_schema
|
|
194
|
+
return new_schema, metadata
|
|
138
195
|
|
|
139
196
|
|
|
140
197
|
def for_types(*allowed_types: str) -> Callable[[Mutation], Mutation]:
|
|
141
198
|
"""Immediately return FAILURE for schemas with types not from ``allowed_types``."""
|
|
142
|
-
|
|
143
199
|
_allowed_types = set(allowed_types)
|
|
144
200
|
|
|
145
201
|
def wrapper(mutation: Mutation) -> Mutation:
|
|
146
202
|
@wraps(mutation)
|
|
147
|
-
def inner(
|
|
203
|
+
def inner(ctx: MutationContext, draw: Draw, schema: Schema) -> tuple[MutationResult, MutationMetadata | None]:
|
|
148
204
|
types = get_type(schema)
|
|
149
205
|
if _allowed_types & set(types):
|
|
150
|
-
return mutation(
|
|
151
|
-
return MutationResult.FAILURE
|
|
206
|
+
return mutation(ctx, draw, schema)
|
|
207
|
+
return MutationResult.FAILURE, None
|
|
152
208
|
|
|
153
209
|
return inner
|
|
154
210
|
|
|
@@ -156,7 +212,9 @@ def for_types(*allowed_types: str) -> Callable[[Mutation], Mutation]:
|
|
|
156
212
|
|
|
157
213
|
|
|
158
214
|
@for_types("object")
|
|
159
|
-
def remove_required_property(
|
|
215
|
+
def remove_required_property(
|
|
216
|
+
ctx: MutationContext, draw: Draw, schema: Schema
|
|
217
|
+
) -> tuple[MutationResult, MutationMetadata | None]:
|
|
160
218
|
"""Remove a required property.
|
|
161
219
|
|
|
162
220
|
Effect: Some property won't be generated.
|
|
@@ -164,13 +222,13 @@ def remove_required_property(context: MutationContext, draw: Draw, schema: Schem
|
|
|
164
222
|
required = schema.get("required")
|
|
165
223
|
if not required:
|
|
166
224
|
# No required properties - can't mutate
|
|
167
|
-
return MutationResult.FAILURE
|
|
225
|
+
return MutationResult.FAILURE, None
|
|
168
226
|
if len(required) == 1:
|
|
169
227
|
property_name = draw(st.sampled_from(sorted(required)))
|
|
170
228
|
else:
|
|
171
229
|
candidate = draw(st.sampled_from(sorted(required)))
|
|
172
|
-
enabled_properties = draw(st.shared(FeatureStrategy(), key="properties"))
|
|
173
|
-
candidates = [candidate
|
|
230
|
+
enabled_properties = draw(st.shared(FeatureStrategy(), key="properties"))
|
|
231
|
+
candidates = [candidate, *sorted([prop for prop in required if enabled_properties.is_enabled(prop)])]
|
|
174
232
|
property_name = draw(st.sampled_from(candidates))
|
|
175
233
|
required.remove(property_name)
|
|
176
234
|
if not required:
|
|
@@ -186,45 +244,73 @@ def remove_required_property(context: MutationContext, draw: Draw, schema: Schem
|
|
|
186
244
|
# This property still can be generated via `patternProperties`, but this implementation doesn't cover this case
|
|
187
245
|
# Its probability is relatively low, and the complete solution compatible with Draft 4 will require extra complexity
|
|
188
246
|
# The output filter covers cases like this
|
|
189
|
-
|
|
247
|
+
metadata = MutationMetadata(
|
|
248
|
+
parameter=property_name,
|
|
249
|
+
description="Required property removed",
|
|
250
|
+
location=f"/properties/{property_name}",
|
|
251
|
+
)
|
|
252
|
+
return MutationResult.SUCCESS, metadata
|
|
190
253
|
|
|
191
254
|
|
|
192
|
-
def change_type(
|
|
255
|
+
def change_type(
|
|
256
|
+
ctx: MutationContext, draw: Draw, schema: JsonSchemaObject
|
|
257
|
+
) -> tuple[MutationResult, MutationMetadata | None]:
|
|
193
258
|
"""Change type of values accepted by a schema."""
|
|
194
259
|
if "type" not in schema:
|
|
195
260
|
# The absence of this keyword means that the schema values can be of any type;
|
|
196
261
|
# Therefore, we can't choose a different type
|
|
197
|
-
return MutationResult.FAILURE
|
|
198
|
-
if
|
|
262
|
+
return MutationResult.FAILURE, None
|
|
263
|
+
if ctx.media_type == "application/x-www-form-urlencoded":
|
|
199
264
|
# Form data should be an object, do not change it
|
|
200
|
-
return MutationResult.FAILURE
|
|
201
|
-
if
|
|
202
|
-
|
|
203
|
-
|
|
265
|
+
return MutationResult.FAILURE, None
|
|
266
|
+
# For headers, query and path parameters, if the current type is string, then it already
|
|
267
|
+
# includes all possible values as those parameters will be stringified before sending,
|
|
268
|
+
# therefore it can't be negated.
|
|
269
|
+
old_types = get_type(schema)
|
|
270
|
+
if "string" in old_types and (ctx.location.is_in_header or ctx.is_path_location or ctx.is_query_location):
|
|
271
|
+
return MutationResult.FAILURE, None
|
|
272
|
+
candidates = _get_type_candidates(ctx, schema)
|
|
204
273
|
if not candidates:
|
|
205
274
|
# Schema covers all possible types, not possible to choose something else
|
|
206
|
-
return MutationResult.FAILURE
|
|
275
|
+
return MutationResult.FAILURE, None
|
|
207
276
|
if len(candidates) == 1:
|
|
208
277
|
new_type = candidates.pop()
|
|
209
278
|
schema["type"] = new_type
|
|
279
|
+
_ensure_query_serializes_to_non_empty(ctx, schema)
|
|
280
|
+
prevent_unsatisfiable_schema(schema, new_type)
|
|
281
|
+
else:
|
|
282
|
+
# Choose one type that will be present in the final candidates list
|
|
283
|
+
candidate = draw(st.sampled_from(sorted(candidates)))
|
|
284
|
+
candidates.remove(candidate)
|
|
285
|
+
enabled_types = draw(st.shared(FeatureStrategy(), key="types"))
|
|
286
|
+
remaining_candidates = [
|
|
287
|
+
candidate,
|
|
288
|
+
*sorted([candidate for candidate in candidates if enabled_types.is_enabled(candidate)]),
|
|
289
|
+
]
|
|
290
|
+
new_type = draw(st.sampled_from(remaining_candidates))
|
|
291
|
+
schema["type"] = new_type
|
|
292
|
+
_ensure_query_serializes_to_non_empty(ctx, schema)
|
|
210
293
|
prevent_unsatisfiable_schema(schema, new_type)
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
[candidate for candidate in candidates if enabled_types.is_enabled(candidate)]
|
|
294
|
+
|
|
295
|
+
old_type_str = " | ".join(sorted(old_types)) if len(old_types) > 1 else old_types[0]
|
|
296
|
+
metadata = MutationMetadata(
|
|
297
|
+
parameter=None,
|
|
298
|
+
description=f"Invalid type {new_type} (expected {old_type_str})",
|
|
299
|
+
location=None,
|
|
218
300
|
)
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
301
|
+
return MutationResult.SUCCESS, metadata
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def _ensure_query_serializes_to_non_empty(ctx: MutationContext, schema: Schema) -> None:
|
|
305
|
+
if ctx.is_query_location and schema.get("type") == "array":
|
|
306
|
+
# Query parameters with empty arrays or arrays of `None` or empty arrays / objects will not appear in the final URL
|
|
307
|
+
schema["minItems"] = schema.get("minItems") or 1
|
|
308
|
+
schema.setdefault("items", {}).update({"not": {"enum": [None, [], {}]}})
|
|
223
309
|
|
|
224
310
|
|
|
225
|
-
def _get_type_candidates(
|
|
311
|
+
def _get_type_candidates(ctx: MutationContext, schema: Schema) -> set[str]:
|
|
226
312
|
types = set(get_type(schema))
|
|
227
|
-
if
|
|
313
|
+
if ctx.is_path_location:
|
|
228
314
|
candidates = {"string", "integer", "number", "boolean", "null"} - types
|
|
229
315
|
else:
|
|
230
316
|
candidates = {"string", "integer", "number", "object", "array", "boolean", "null"} - types
|
|
@@ -253,7 +339,9 @@ def drop_not_type_specific_keywords(schema: Schema, new_type: str) -> None:
|
|
|
253
339
|
|
|
254
340
|
|
|
255
341
|
@for_types("object")
|
|
256
|
-
def change_properties(
|
|
342
|
+
def change_properties(
|
|
343
|
+
ctx: MutationContext, draw: Draw, schema: Schema
|
|
344
|
+
) -> tuple[MutationResult, MutationMetadata | None]:
|
|
257
345
|
"""Mutate individual object schema properties.
|
|
258
346
|
|
|
259
347
|
Effect: Some properties will not validate the original schema
|
|
@@ -261,12 +349,18 @@ def change_properties(context: MutationContext, draw: Draw, schema: Schema) -> M
|
|
|
261
349
|
properties = sorted(schema.get("properties", {}).items())
|
|
262
350
|
if not properties:
|
|
263
351
|
# No properties to mutate
|
|
264
|
-
return MutationResult.FAILURE
|
|
352
|
+
return MutationResult.FAILURE, None
|
|
265
353
|
# Order properties randomly and iterate over them until at least one mutation is successfully applied to at least
|
|
266
354
|
# one property
|
|
267
|
-
ordered_properties =
|
|
355
|
+
ordered_properties = [
|
|
356
|
+
(name, canonicalish(subschema) if isinstance(subschema, bool) else subschema)
|
|
357
|
+
for name, subschema in draw(ordered(properties, unique_by=lambda x: x[0]))
|
|
358
|
+
]
|
|
359
|
+
nested_metadata = None
|
|
268
360
|
for property_name, property_schema in ordered_properties:
|
|
269
|
-
|
|
361
|
+
ctx.ensure_bundle(property_schema)
|
|
362
|
+
result, nested_metadata = apply_until_success(ctx, draw, property_schema)
|
|
363
|
+
if result == MutationResult.SUCCESS:
|
|
270
364
|
# It is still possible to generate "positive" cases, for example, when this property is optional.
|
|
271
365
|
# They are filtered out on the upper level anyway, but to avoid performance penalty we adjust the schema
|
|
272
366
|
# so the generated samples are less likely to be "positive"
|
|
@@ -280,31 +374,47 @@ def change_properties(context: MutationContext, draw: Draw, schema: Schema) -> M
|
|
|
280
374
|
break
|
|
281
375
|
else:
|
|
282
376
|
# No successful mutations
|
|
283
|
-
return MutationResult.FAILURE
|
|
284
|
-
enabled_properties = draw(st.shared(FeatureStrategy(), key="properties"))
|
|
285
|
-
enabled_mutations = draw(st.shared(FeatureStrategy(), key="mutations"))
|
|
377
|
+
return MutationResult.FAILURE, None
|
|
378
|
+
enabled_properties = draw(st.shared(FeatureStrategy(), key="properties"))
|
|
379
|
+
enabled_mutations = draw(st.shared(FeatureStrategy(), key="mutations"))
|
|
286
380
|
for name, property_schema in properties:
|
|
287
381
|
# Skip already mutated property
|
|
288
|
-
if name == property_name:
|
|
382
|
+
if name == property_name:
|
|
289
383
|
# Pylint: `properties` variable has at least one element as it is checked at the beginning of the function
|
|
290
384
|
# Then those properties are ordered and iterated over, therefore `property_name` is always defined
|
|
291
385
|
continue
|
|
292
386
|
if enabled_properties.is_enabled(name):
|
|
387
|
+
ctx.ensure_bundle(property_schema)
|
|
293
388
|
for mutation in get_mutations(draw, property_schema):
|
|
294
389
|
if enabled_mutations.is_enabled(mutation.__name__):
|
|
295
|
-
mutation(
|
|
296
|
-
|
|
390
|
+
mutation(ctx, draw, property_schema)
|
|
391
|
+
|
|
392
|
+
# Use nested metadata description if available, otherwise use generic description
|
|
393
|
+
if nested_metadata and nested_metadata.description:
|
|
394
|
+
description = nested_metadata.description
|
|
395
|
+
else:
|
|
396
|
+
description = "Property constraint violated"
|
|
397
|
+
|
|
398
|
+
metadata = MutationMetadata(
|
|
399
|
+
parameter=property_name,
|
|
400
|
+
description=description,
|
|
401
|
+
location=f"/properties/{property_name}",
|
|
402
|
+
)
|
|
403
|
+
return MutationResult.SUCCESS, metadata
|
|
297
404
|
|
|
298
405
|
|
|
299
|
-
def apply_until_success(
|
|
406
|
+
def apply_until_success(
|
|
407
|
+
ctx: MutationContext, draw: Draw, schema: Schema
|
|
408
|
+
) -> tuple[MutationResult, MutationMetadata | None]:
|
|
300
409
|
for mutation in get_mutations(draw, schema):
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
410
|
+
result, metadata = mutation(ctx, draw, schema)
|
|
411
|
+
if result == MutationResult.SUCCESS:
|
|
412
|
+
return MutationResult.SUCCESS, metadata
|
|
413
|
+
return MutationResult.FAILURE, None
|
|
304
414
|
|
|
305
415
|
|
|
306
416
|
@for_types("array")
|
|
307
|
-
def change_items(
|
|
417
|
+
def change_items(ctx: MutationContext, draw: Draw, schema: Schema) -> tuple[MutationResult, MutationMetadata | None]:
|
|
308
418
|
"""Mutate individual array items.
|
|
309
419
|
|
|
310
420
|
Effect: Some items will not validate the original schema
|
|
@@ -312,92 +422,173 @@ def change_items(context: MutationContext, draw: Draw, schema: Schema) -> Mutati
|
|
|
312
422
|
items = schema.get("items", {})
|
|
313
423
|
if not items:
|
|
314
424
|
# No items to mutate
|
|
315
|
-
return MutationResult.FAILURE
|
|
425
|
+
return MutationResult.FAILURE, None
|
|
426
|
+
# For query/path/header/cookie, string items cannot be meaningfully mutated
|
|
427
|
+
# because all types serialize to strings anyway
|
|
428
|
+
if ctx.location.is_in_header or ctx.is_path_location or ctx.is_query_location:
|
|
429
|
+
items = schema.get("items", {})
|
|
430
|
+
if isinstance(items, dict):
|
|
431
|
+
items_types = get_type(items)
|
|
432
|
+
if "string" in items_types:
|
|
433
|
+
return MutationResult.FAILURE, None
|
|
316
434
|
if isinstance(items, dict):
|
|
317
|
-
return _change_items_object(
|
|
435
|
+
return _change_items_object(ctx, draw, schema, items)
|
|
318
436
|
if isinstance(items, list):
|
|
319
|
-
return _change_items_array(
|
|
320
|
-
return MutationResult.FAILURE
|
|
437
|
+
return _change_items_array(ctx, draw, schema, items)
|
|
438
|
+
return MutationResult.FAILURE, None
|
|
321
439
|
|
|
322
440
|
|
|
323
|
-
def _change_items_object(
|
|
441
|
+
def _change_items_object(
|
|
442
|
+
ctx: MutationContext, draw: Draw, schema: Schema, items: Schema
|
|
443
|
+
) -> tuple[MutationResult, MutationMetadata | None]:
|
|
444
|
+
ctx.ensure_bundle(items)
|
|
324
445
|
result = MutationResult.FAILURE
|
|
446
|
+
metadata = None
|
|
325
447
|
for mutation in get_mutations(draw, items):
|
|
326
|
-
|
|
448
|
+
mut_result, mut_metadata = mutation(ctx, draw, items)
|
|
449
|
+
result |= mut_result
|
|
450
|
+
if metadata is None and mut_metadata is not None:
|
|
451
|
+
metadata = mut_metadata
|
|
327
452
|
if result == MutationResult.FAILURE:
|
|
328
|
-
return MutationResult.FAILURE
|
|
453
|
+
return MutationResult.FAILURE, None
|
|
329
454
|
min_items = schema.get("minItems", 0)
|
|
330
455
|
schema["minItems"] = max(min_items, 1)
|
|
331
|
-
|
|
456
|
+
# Use nested metadata description if available, update location to show it's in array items
|
|
457
|
+
if metadata:
|
|
458
|
+
metadata = MutationMetadata(
|
|
459
|
+
parameter=None,
|
|
460
|
+
description=f"Array item: {metadata.description}",
|
|
461
|
+
location="/items",
|
|
462
|
+
)
|
|
463
|
+
return MutationResult.SUCCESS, metadata
|
|
332
464
|
|
|
333
465
|
|
|
334
|
-
def _change_items_array(
|
|
466
|
+
def _change_items_array(
|
|
467
|
+
ctx: MutationContext, draw: Draw, schema: Schema, items: list
|
|
468
|
+
) -> tuple[MutationResult, MutationMetadata | None]:
|
|
335
469
|
latest_success_index = None
|
|
470
|
+
metadata = None
|
|
336
471
|
for idx, item in enumerate(items):
|
|
472
|
+
ctx.ensure_bundle(item)
|
|
337
473
|
result = MutationResult.FAILURE
|
|
338
474
|
for mutation in get_mutations(draw, item):
|
|
339
|
-
|
|
475
|
+
mut_result, mut_metadata = mutation(ctx, draw, item)
|
|
476
|
+
result |= mut_result
|
|
477
|
+
if metadata is None and mut_metadata is not None:
|
|
478
|
+
metadata = mut_metadata
|
|
340
479
|
if result == MutationResult.SUCCESS:
|
|
341
480
|
latest_success_index = idx
|
|
342
481
|
if latest_success_index is None:
|
|
343
|
-
return MutationResult.FAILURE
|
|
482
|
+
return MutationResult.FAILURE, None
|
|
344
483
|
min_items = schema.get("minItems", 0)
|
|
345
484
|
schema["minItems"] = max(min_items, latest_success_index + 1)
|
|
346
|
-
|
|
485
|
+
# Use nested metadata description if available, update location to show specific array index
|
|
486
|
+
if metadata:
|
|
487
|
+
metadata = MutationMetadata(
|
|
488
|
+
parameter=None,
|
|
489
|
+
description=f"Array item at index {latest_success_index}: {metadata.description}",
|
|
490
|
+
location=f"/items/{latest_success_index}",
|
|
491
|
+
)
|
|
492
|
+
return MutationResult.SUCCESS, metadata
|
|
347
493
|
|
|
348
494
|
|
|
349
|
-
def negate_constraints(
|
|
495
|
+
def negate_constraints(
|
|
496
|
+
ctx: MutationContext, draw: Draw, schema: Schema
|
|
497
|
+
) -> tuple[MutationResult, MutationMetadata | None]:
|
|
350
498
|
"""Negate schema constrains while keeping the original type."""
|
|
499
|
+
ctx.ensure_bundle(schema)
|
|
351
500
|
if not can_negate(schema):
|
|
352
|
-
return MutationResult.FAILURE
|
|
501
|
+
return MutationResult.FAILURE, None
|
|
353
502
|
copied = schema.copy()
|
|
503
|
+
# Preserve x-bundled before clearing
|
|
504
|
+
bundled = schema.get(BUNDLE_STORAGE_KEY)
|
|
354
505
|
schema.clear()
|
|
506
|
+
if bundled is not None:
|
|
507
|
+
schema[BUNDLE_STORAGE_KEY] = bundled
|
|
355
508
|
is_negated = False
|
|
509
|
+
negated_keys = []
|
|
356
510
|
|
|
357
|
-
def is_mutation_candidate(k: str) -> bool:
|
|
511
|
+
def is_mutation_candidate(k: str, v: Any) -> bool:
|
|
358
512
|
# Should we negate this key?
|
|
513
|
+
if k == "required":
|
|
514
|
+
return v != []
|
|
515
|
+
if k in ("example", "examples"):
|
|
516
|
+
return False
|
|
517
|
+
if ctx.is_path_location and k == "minLength" and v == 1:
|
|
518
|
+
# Empty path parameter will be filtered out
|
|
519
|
+
return False
|
|
520
|
+
if (
|
|
521
|
+
not ctx.allow_extra_parameters
|
|
522
|
+
and k == "additionalProperties"
|
|
523
|
+
and ctx.location in (ParameterLocation.QUERY, ParameterLocation.HEADER, ParameterLocation.COOKIE)
|
|
524
|
+
):
|
|
525
|
+
return False
|
|
359
526
|
return not (
|
|
360
527
|
k in ("type", "properties", "items", "minItems")
|
|
361
|
-
or (k == "additionalProperties" and
|
|
528
|
+
or (k == "additionalProperties" and ctx.location.is_in_header)
|
|
362
529
|
)
|
|
363
530
|
|
|
364
|
-
enabled_keywords = draw(st.shared(FeatureStrategy(), key="keywords"))
|
|
531
|
+
enabled_keywords = draw(st.shared(FeatureStrategy(), key="keywords"))
|
|
365
532
|
candidates = []
|
|
366
|
-
mutation_candidates = [key for key in copied if is_mutation_candidate(key)]
|
|
533
|
+
mutation_candidates = [key for key, value in copied.items() if is_mutation_candidate(key, value)]
|
|
367
534
|
if mutation_candidates:
|
|
368
535
|
# There should be at least one mutated keyword
|
|
369
|
-
candidate = draw(st.sampled_from([key for key in copied if is_mutation_candidate(key)]))
|
|
536
|
+
candidate = draw(st.sampled_from([key for key, value in copied.items() if is_mutation_candidate(key, value)]))
|
|
370
537
|
candidates.append(candidate)
|
|
371
538
|
# If the chosen candidate has dependency, then the dependency should also be present in the final schema
|
|
372
539
|
if candidate in DEPENDENCIES:
|
|
373
540
|
candidates.append(DEPENDENCIES[candidate])
|
|
374
541
|
for key, value in copied.items():
|
|
375
|
-
if is_mutation_candidate(key):
|
|
542
|
+
if is_mutation_candidate(key, value):
|
|
376
543
|
if key in candidates or enabled_keywords.is_enabled(key):
|
|
377
544
|
is_negated = True
|
|
545
|
+
negated_keys.append(key)
|
|
378
546
|
negated = schema.setdefault("not", {})
|
|
379
547
|
negated[key] = value
|
|
380
548
|
if key in DEPENDENCIES:
|
|
381
549
|
# If this keyword has a dependency, then it should be also negated
|
|
382
550
|
dependency = DEPENDENCIES[key]
|
|
383
|
-
if dependency not in negated:
|
|
384
|
-
negated[dependency] = copied[dependency]
|
|
551
|
+
if dependency not in negated and dependency in copied:
|
|
552
|
+
negated[dependency] = copied[dependency]
|
|
385
553
|
else:
|
|
386
554
|
schema[key] = value
|
|
387
555
|
if is_negated:
|
|
388
|
-
|
|
389
|
-
|
|
556
|
+
# Build concise description from negated constraints
|
|
557
|
+
descriptions = []
|
|
558
|
+
for key in negated_keys:
|
|
559
|
+
value = copied[key]
|
|
560
|
+
# Special case: format required properties list nicely with quoted names
|
|
561
|
+
if key == "required" and isinstance(value, list) and len(value) <= 3:
|
|
562
|
+
props = ", ".join(f"`{prop}`" for prop in value)
|
|
563
|
+
descriptions.append(f"`{key}` ({props})")
|
|
564
|
+
else:
|
|
565
|
+
# Default: show `key` (value) for all constraints
|
|
566
|
+
descriptions.append(f"`{key}` ({value})")
|
|
567
|
+
|
|
568
|
+
constraint_desc = ", ".join(descriptions)
|
|
569
|
+
metadata = MutationMetadata(
|
|
570
|
+
parameter=None,
|
|
571
|
+
description=f"Violates {constraint_desc}",
|
|
572
|
+
location=None,
|
|
573
|
+
)
|
|
574
|
+
return MutationResult.SUCCESS, metadata
|
|
575
|
+
return MutationResult.FAILURE, None
|
|
390
576
|
|
|
391
577
|
|
|
392
578
|
DEPENDENCIES = {"exclusiveMaximum": "maximum", "exclusiveMinimum": "minimum"}
|
|
393
579
|
|
|
394
580
|
|
|
395
|
-
def get_mutations(draw: Draw, schema:
|
|
581
|
+
def get_mutations(draw: Draw, schema: JsonSchemaObject) -> tuple[Mutation, ...]:
|
|
396
582
|
"""Get mutations possible for a schema."""
|
|
397
583
|
types = get_type(schema)
|
|
398
584
|
# On the top-level of Open API schemas, types are always strings, but inside "schema" objects, they are the same as
|
|
399
585
|
# in JSON Schema, where it could be either a string or an array of strings.
|
|
400
|
-
options:
|
|
586
|
+
options: list[Mutation]
|
|
587
|
+
if list(schema) == ["type"]:
|
|
588
|
+
# When there is only `type` in schema then `negate_constraints` is not applicable
|
|
589
|
+
options = [change_type]
|
|
590
|
+
else:
|
|
591
|
+
options = [negate_constraints, change_type]
|
|
401
592
|
if "object" in types:
|
|
402
593
|
options.extend([change_properties, remove_required_property])
|
|
403
594
|
elif "array" in types:
|
|
@@ -409,7 +600,7 @@ def ident(x: T) -> T:
|
|
|
409
600
|
return x
|
|
410
601
|
|
|
411
602
|
|
|
412
|
-
def ordered(items: Sequence[T], unique_by: Callable[[T], Any] = ident) -> st.SearchStrategy[
|
|
603
|
+
def ordered(items: Sequence[T], unique_by: Callable[[T], Any] = ident) -> st.SearchStrategy[list[T]]:
|
|
413
604
|
"""Returns a strategy that generates randomly ordered lists of T.
|
|
414
605
|
|
|
415
606
|
NOTE. Items should be unique.
|
|
@@ -1,16 +1,7 @@
|
|
|
1
|
-
from typing import List
|
|
2
|
-
|
|
3
1
|
from hypothesis_jsonschema._canonicalise import canonicalish
|
|
4
2
|
|
|
5
3
|
from .types import Schema
|
|
6
4
|
|
|
7
5
|
|
|
8
|
-
def get_type(schema: Schema) -> List[str]:
|
|
9
|
-
type_ = schema.get("type", ["null", "boolean", "integer", "number", "string", "array", "object"])
|
|
10
|
-
if isinstance(type_, str):
|
|
11
|
-
return [type_]
|
|
12
|
-
return type_
|
|
13
|
-
|
|
14
|
-
|
|
15
6
|
def can_negate(schema: Schema) -> bool:
|
|
16
7
|
return canonicalish(schema) != {}
|