schemathesis 3.13.0__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 -1016
- 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 +683 -247
- schemathesis/specs/graphql/__init__.py +0 -1
- schemathesis/specs/graphql/nodes.py +27 -0
- schemathesis/specs/graphql/scalars.py +86 -0
- schemathesis/specs/graphql/schemas.py +395 -123
- schemathesis/specs/graphql/validation.py +33 -0
- schemathesis/specs/openapi/__init__.py +9 -1
- schemathesis/specs/openapi/_hypothesis.py +578 -317
- 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 +753 -74
- 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 +117 -68
- schemathesis/specs/openapi/negative/mutations.py +294 -104
- schemathesis/specs/openapi/negative/utils.py +3 -6
- schemathesis/specs/openapi/patterns.py +458 -0
- schemathesis/specs/openapi/references.py +60 -81
- schemathesis/specs/openapi/schemas.py +648 -650
- schemathesis/specs/openapi/serialization.py +53 -30
- schemathesis/specs/openapi/stateful/__init__.py +404 -69
- 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.13.0.dist-info → schemathesis-4.4.2.dist-info}/WHEEL +1 -1
- schemathesis-4.4.2.dist-info/entry_points.txt +6 -0
- {schemathesis-3.13.0.dist-info → schemathesis-4.4.2.dist-info/licenses}/LICENSE +1 -1
- schemathesis/_compat.py +0 -41
- schemathesis/_hypothesis.py +0 -115
- schemathesis/cli/callbacks.py +0 -188
- schemathesis/cli/cassettes.py +0 -253
- schemathesis/cli/context.py +0 -36
- schemathesis/cli/debug.py +0 -21
- schemathesis/cli/handlers.py +0 -11
- schemathesis/cli/junitxml.py +0 -41
- schemathesis/cli/options.py +0 -51
- schemathesis/cli/output/__init__.py +0 -1
- schemathesis/cli/output/default.py +0 -508
- schemathesis/cli/output/short.py +0 -40
- schemathesis/constants.py +0 -79
- schemathesis/exceptions.py +0 -207
- schemathesis/extra/_aiohttp.py +0 -27
- schemathesis/extra/_flask.py +0 -10
- schemathesis/extra/_server.py +0 -16
- schemathesis/extra/pytest_plugin.py +0 -216
- schemathesis/failures.py +0 -131
- schemathesis/fixups/__init__.py +0 -29
- schemathesis/fixups/fast_api.py +0 -30
- schemathesis/lazy.py +0 -227
- schemathesis/models.py +0 -1041
- schemathesis/parameters.py +0 -88
- schemathesis/runner/__init__.py +0 -460
- schemathesis/runner/events.py +0 -240
- schemathesis/runner/impl/__init__.py +0 -3
- schemathesis/runner/impl/core.py +0 -755
- schemathesis/runner/impl/solo.py +0 -85
- schemathesis/runner/impl/threadpool.py +0 -367
- schemathesis/runner/serialization.py +0 -189
- schemathesis/serializers.py +0 -233
- schemathesis/service/__init__.py +0 -3
- schemathesis/service/client.py +0 -46
- schemathesis/service/constants.py +0 -12
- schemathesis/service/events.py +0 -39
- schemathesis/service/handler.py +0 -39
- schemathesis/service/models.py +0 -7
- schemathesis/service/serialization.py +0 -153
- schemathesis/service/worker.py +0 -40
- 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 -302
- schemathesis/specs/openapi/loaders.py +0 -453
- schemathesis/specs/openapi/parameters.py +0 -413
- schemathesis/specs/openapi/security.py +0 -129
- schemathesis/specs/openapi/validation.py +0 -24
- schemathesis/stateful.py +0 -349
- schemathesis/targets.py +0 -32
- schemathesis/types.py +0 -38
- schemathesis/utils.py +0 -436
- schemathesis-3.13.0.dist-info/METADATA +0 -202
- schemathesis-3.13.0.dist-info/RECORD +0 -91
- schemathesis-3.13.0.dist-info/entry_points.txt +0 -6
- /schemathesis/{extra → cli/ext}/__init__.py +0 -0
|
@@ -0,0 +1,992 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import inspect
|
|
5
|
+
import warnings
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from enum import Enum
|
|
8
|
+
from functools import wraps
|
|
9
|
+
from itertools import combinations
|
|
10
|
+
from time import perf_counter
|
|
11
|
+
from typing import Any, Callable, Generator, Mapping
|
|
12
|
+
|
|
13
|
+
import hypothesis
|
|
14
|
+
from hypothesis import Phase, Verbosity
|
|
15
|
+
from hypothesis import strategies as st
|
|
16
|
+
from hypothesis._settings import all_settings
|
|
17
|
+
from hypothesis.errors import Unsatisfiable
|
|
18
|
+
from jsonschema.exceptions import SchemaError
|
|
19
|
+
from requests.models import CaseInsensitiveDict
|
|
20
|
+
|
|
21
|
+
from schemathesis import auths
|
|
22
|
+
from schemathesis.auths import AuthStorage, AuthStorageMark
|
|
23
|
+
from schemathesis.config import GenerationConfig, ProjectConfig
|
|
24
|
+
from schemathesis.core import INJECTED_PATH_PARAMETER_KEY, NOT_SET, NotSet, SpecificationFeature, media_types
|
|
25
|
+
from schemathesis.core.errors import (
|
|
26
|
+
InfiniteRecursiveReference,
|
|
27
|
+
InvalidSchema,
|
|
28
|
+
MalformedMediaType,
|
|
29
|
+
SerializationNotPossible,
|
|
30
|
+
UnresolvableReference,
|
|
31
|
+
)
|
|
32
|
+
from schemathesis.core.marks import Mark
|
|
33
|
+
from schemathesis.core.parameters import LOCATION_TO_CONTAINER, ParameterLocation
|
|
34
|
+
from schemathesis.core.transforms import deepclone
|
|
35
|
+
from schemathesis.core.transport import prepare_urlencoded
|
|
36
|
+
from schemathesis.core.validation import has_invalid_characters, is_latin_1_encodable
|
|
37
|
+
from schemathesis.generation import GenerationMode, coverage
|
|
38
|
+
from schemathesis.generation.case import Case
|
|
39
|
+
from schemathesis.generation.hypothesis import examples, setup
|
|
40
|
+
from schemathesis.generation.hypothesis.examples import add_single_example
|
|
41
|
+
from schemathesis.generation.hypothesis.given import GivenInput
|
|
42
|
+
from schemathesis.generation.meta import (
|
|
43
|
+
CaseMetadata,
|
|
44
|
+
ComponentInfo,
|
|
45
|
+
CoveragePhaseData,
|
|
46
|
+
CoverageScenario,
|
|
47
|
+
GenerationInfo,
|
|
48
|
+
PhaseInfo,
|
|
49
|
+
)
|
|
50
|
+
from schemathesis.hooks import GLOBAL_HOOK_DISPATCHER, HookContext, HookDispatcher, HookDispatcherMark
|
|
51
|
+
from schemathesis.schemas import APIOperation, ParameterSet
|
|
52
|
+
|
|
53
|
+
setup()
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class HypothesisTestMode(str, Enum):
|
|
57
|
+
EXAMPLES = "examples"
|
|
58
|
+
COVERAGE = "coverage"
|
|
59
|
+
FUZZING = "fuzzing"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass
|
|
63
|
+
class HypothesisTestConfig:
|
|
64
|
+
project: ProjectConfig
|
|
65
|
+
modes: list[HypothesisTestMode]
|
|
66
|
+
settings: hypothesis.settings | None
|
|
67
|
+
seed: int | None
|
|
68
|
+
as_strategy_kwargs: dict[str, Any]
|
|
69
|
+
given_args: tuple[GivenInput, ...]
|
|
70
|
+
given_kwargs: dict[str, GivenInput]
|
|
71
|
+
|
|
72
|
+
__slots__ = (
|
|
73
|
+
"project",
|
|
74
|
+
"modes",
|
|
75
|
+
"settings",
|
|
76
|
+
"seed",
|
|
77
|
+
"as_strategy_kwargs",
|
|
78
|
+
"given_args",
|
|
79
|
+
"given_kwargs",
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
def __init__(
|
|
83
|
+
self,
|
|
84
|
+
project: ProjectConfig,
|
|
85
|
+
modes: list[HypothesisTestMode],
|
|
86
|
+
settings: hypothesis.settings | None = None,
|
|
87
|
+
seed: int | None = None,
|
|
88
|
+
as_strategy_kwargs: dict[str, Any] | None = None,
|
|
89
|
+
given_args: tuple[GivenInput, ...] = (),
|
|
90
|
+
given_kwargs: dict[str, GivenInput] | None = None,
|
|
91
|
+
) -> None:
|
|
92
|
+
self.project = project
|
|
93
|
+
self.modes = modes
|
|
94
|
+
self.settings = settings
|
|
95
|
+
self.seed = seed
|
|
96
|
+
self.as_strategy_kwargs = as_strategy_kwargs or {}
|
|
97
|
+
self.given_args = given_args
|
|
98
|
+
self.given_kwargs = given_kwargs or {}
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def create_test(
|
|
102
|
+
*,
|
|
103
|
+
operation: APIOperation,
|
|
104
|
+
test_func: Callable,
|
|
105
|
+
config: HypothesisTestConfig,
|
|
106
|
+
) -> Callable:
|
|
107
|
+
"""Create a Hypothesis test."""
|
|
108
|
+
hook_dispatcher = HookDispatcherMark.get(test_func)
|
|
109
|
+
auth_storage = AuthStorageMark.get(test_func)
|
|
110
|
+
|
|
111
|
+
strategy_kwargs = {
|
|
112
|
+
"hooks": hook_dispatcher,
|
|
113
|
+
"auth_storage": auth_storage,
|
|
114
|
+
**config.as_strategy_kwargs,
|
|
115
|
+
}
|
|
116
|
+
generation = config.project.generation_for(operation=operation)
|
|
117
|
+
strategy = st.one_of(operation.as_strategy(generation_mode=mode, **strategy_kwargs) for mode in generation.modes)
|
|
118
|
+
|
|
119
|
+
hypothesis_test = create_base_test(
|
|
120
|
+
test_function=test_func,
|
|
121
|
+
strategy=strategy,
|
|
122
|
+
args=config.given_args,
|
|
123
|
+
kwargs=config.given_kwargs,
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
ApiOperationMark.set(hypothesis_test, operation)
|
|
127
|
+
|
|
128
|
+
if config.seed is not None:
|
|
129
|
+
hypothesis_test = hypothesis.seed(config.seed)(hypothesis_test)
|
|
130
|
+
|
|
131
|
+
default = hypothesis.settings.default
|
|
132
|
+
settings = getattr(hypothesis_test, SETTINGS_ATTRIBUTE_NAME, None)
|
|
133
|
+
assert settings is not None
|
|
134
|
+
|
|
135
|
+
if settings.verbosity == default.verbosity:
|
|
136
|
+
settings = hypothesis.settings(settings, verbosity=Verbosity.quiet)
|
|
137
|
+
|
|
138
|
+
if config.settings is not None:
|
|
139
|
+
# Merge the user-provided settings with the current ones
|
|
140
|
+
settings = hypothesis.settings(
|
|
141
|
+
config.settings,
|
|
142
|
+
**{
|
|
143
|
+
item: getattr(settings, item)
|
|
144
|
+
for item in all_settings
|
|
145
|
+
if getattr(settings, item) != getattr(default, item)
|
|
146
|
+
},
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
if Phase.explain in settings.phases:
|
|
150
|
+
phases = tuple(phase for phase in settings.phases if phase != Phase.explain)
|
|
151
|
+
settings = hypothesis.settings(settings, phases=phases)
|
|
152
|
+
|
|
153
|
+
# Remove `reuse` & `generate` phases to avoid yielding any test cases if we don't do fuzzing
|
|
154
|
+
if HypothesisTestMode.FUZZING not in config.modes and (
|
|
155
|
+
Phase.generate in settings.phases or Phase.reuse in settings.phases
|
|
156
|
+
):
|
|
157
|
+
phases = tuple(phase for phase in settings.phases if phase not in (Phase.reuse, Phase.generate))
|
|
158
|
+
settings = hypothesis.settings(settings, phases=phases)
|
|
159
|
+
|
|
160
|
+
specification = operation.schema.specification
|
|
161
|
+
|
|
162
|
+
# Add examples if explicit phase is enabled
|
|
163
|
+
if (
|
|
164
|
+
HypothesisTestMode.EXAMPLES in config.modes
|
|
165
|
+
and Phase.explicit in settings.phases
|
|
166
|
+
and specification.supports_feature(SpecificationFeature.EXAMPLES)
|
|
167
|
+
):
|
|
168
|
+
phases_config = config.project.phases_for(operation=operation)
|
|
169
|
+
hypothesis_test = add_examples(
|
|
170
|
+
hypothesis_test,
|
|
171
|
+
operation,
|
|
172
|
+
fill_missing=phases_config.examples.fill_missing,
|
|
173
|
+
hook_dispatcher=hook_dispatcher,
|
|
174
|
+
**strategy_kwargs,
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
if (
|
|
178
|
+
HypothesisTestMode.COVERAGE in config.modes
|
|
179
|
+
and Phase.explicit in settings.phases
|
|
180
|
+
and specification.supports_feature(SpecificationFeature.COVERAGE)
|
|
181
|
+
and not config.given_args
|
|
182
|
+
and not config.given_kwargs
|
|
183
|
+
):
|
|
184
|
+
phases_config = config.project.phases_for(operation=operation)
|
|
185
|
+
hypothesis_test = add_coverage(
|
|
186
|
+
hypothesis_test,
|
|
187
|
+
operation,
|
|
188
|
+
generation.modes,
|
|
189
|
+
auth_storage,
|
|
190
|
+
config.as_strategy_kwargs,
|
|
191
|
+
generate_duplicate_query_parameters=phases_config.coverage.generate_duplicate_query_parameters,
|
|
192
|
+
unexpected_methods=phases_config.coverage.unexpected_methods,
|
|
193
|
+
generation_config=generation,
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
injected_path_parameter_names = [
|
|
197
|
+
parameter.name
|
|
198
|
+
for parameter in operation.path_parameters
|
|
199
|
+
if parameter.definition.get(INJECTED_PATH_PARAMETER_KEY)
|
|
200
|
+
]
|
|
201
|
+
if injected_path_parameter_names:
|
|
202
|
+
names = ", ".join(f"'{name}'" for name in injected_path_parameter_names)
|
|
203
|
+
plural = "s" if len(injected_path_parameter_names) > 1 else ""
|
|
204
|
+
verb = "are" if len(injected_path_parameter_names) > 1 else "is"
|
|
205
|
+
error = InvalidSchema(f"Path parameter{plural} {names} {verb} not defined")
|
|
206
|
+
MissingPathParameters.set(hypothesis_test, error)
|
|
207
|
+
|
|
208
|
+
setattr(hypothesis_test, SETTINGS_ATTRIBUTE_NAME, settings)
|
|
209
|
+
|
|
210
|
+
return hypothesis_test
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
SETTINGS_ATTRIBUTE_NAME = "_hypothesis_internal_use_settings"
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def create_base_test(
|
|
217
|
+
*,
|
|
218
|
+
test_function: Callable,
|
|
219
|
+
strategy: st.SearchStrategy,
|
|
220
|
+
args: tuple[GivenInput, ...],
|
|
221
|
+
kwargs: dict[str, GivenInput],
|
|
222
|
+
) -> Callable:
|
|
223
|
+
"""Create the basic Hypothesis test with the given strategy."""
|
|
224
|
+
|
|
225
|
+
@wraps(test_function)
|
|
226
|
+
def test_wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
227
|
+
__tracebackhide__ = True
|
|
228
|
+
return test_function(*args, **kwargs)
|
|
229
|
+
|
|
230
|
+
funcobj = hypothesis.given(*args, **{**kwargs, "case": strategy})(test_wrapper)
|
|
231
|
+
|
|
232
|
+
if inspect.iscoroutinefunction(test_function):
|
|
233
|
+
funcobj.hypothesis.inner_test = make_async_test(test_function)
|
|
234
|
+
return funcobj
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def make_async_test(test: Callable) -> Callable:
|
|
238
|
+
def async_run(*args: Any, **kwargs: Any) -> None:
|
|
239
|
+
try:
|
|
240
|
+
loop = asyncio.get_event_loop()
|
|
241
|
+
except RuntimeError:
|
|
242
|
+
loop = asyncio.new_event_loop()
|
|
243
|
+
coro = test(*args, **kwargs)
|
|
244
|
+
future = asyncio.ensure_future(coro, loop=loop)
|
|
245
|
+
loop.run_until_complete(future)
|
|
246
|
+
|
|
247
|
+
return async_run
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def add_examples(
|
|
251
|
+
test: Callable,
|
|
252
|
+
operation: APIOperation,
|
|
253
|
+
fill_missing: bool,
|
|
254
|
+
hook_dispatcher: HookDispatcher | None = None,
|
|
255
|
+
**kwargs: Any,
|
|
256
|
+
) -> Callable:
|
|
257
|
+
for example in generate_example_cases(
|
|
258
|
+
test=test, operation=operation, fill_missing=fill_missing, hook_dispatcher=hook_dispatcher, **kwargs
|
|
259
|
+
):
|
|
260
|
+
test = hypothesis.example(case=example)(test)
|
|
261
|
+
|
|
262
|
+
return test
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def generate_example_cases(
|
|
266
|
+
*,
|
|
267
|
+
test: Callable,
|
|
268
|
+
operation: APIOperation,
|
|
269
|
+
fill_missing: bool,
|
|
270
|
+
hook_dispatcher: HookDispatcher | None = None,
|
|
271
|
+
**kwargs: Any,
|
|
272
|
+
) -> Generator[Case]:
|
|
273
|
+
"""Add examples to the Hypothesis test, if they are specified in the schema."""
|
|
274
|
+
try:
|
|
275
|
+
result: list[Case] = [
|
|
276
|
+
examples.generate_one(strategy) for strategy in operation.get_strategies_from_examples(**kwargs)
|
|
277
|
+
]
|
|
278
|
+
except (
|
|
279
|
+
InvalidSchema,
|
|
280
|
+
InfiniteRecursiveReference,
|
|
281
|
+
Unsatisfiable,
|
|
282
|
+
UnresolvableReference,
|
|
283
|
+
SerializationNotPossible,
|
|
284
|
+
SchemaError,
|
|
285
|
+
) as exc:
|
|
286
|
+
result = []
|
|
287
|
+
if isinstance(exc, Unsatisfiable):
|
|
288
|
+
UnsatisfiableExampleMark.set(test, exc)
|
|
289
|
+
if isinstance(exc, SerializationNotPossible):
|
|
290
|
+
NonSerializableMark.set(test, exc)
|
|
291
|
+
if isinstance(exc, SchemaError):
|
|
292
|
+
InvalidRegexMark.set(test, exc)
|
|
293
|
+
if isinstance(exc, InfiniteRecursiveReference):
|
|
294
|
+
InfiniteRecursiveReferenceMark.set(test, exc)
|
|
295
|
+
if isinstance(exc, UnresolvableReference):
|
|
296
|
+
UnresolvableReferenceMark.set(test, exc)
|
|
297
|
+
|
|
298
|
+
if fill_missing and not result:
|
|
299
|
+
strategy = operation.as_strategy()
|
|
300
|
+
add_single_example(strategy, result)
|
|
301
|
+
|
|
302
|
+
context = HookContext(operation=operation) # context should be passed here instead
|
|
303
|
+
GLOBAL_HOOK_DISPATCHER.dispatch("before_add_examples", context, result)
|
|
304
|
+
operation.schema.hooks.dispatch("before_add_examples", context, result)
|
|
305
|
+
if hook_dispatcher:
|
|
306
|
+
hook_dispatcher.dispatch("before_add_examples", context, result)
|
|
307
|
+
original_test = test
|
|
308
|
+
for example in result:
|
|
309
|
+
if example.headers is not None:
|
|
310
|
+
invalid_headers = dict(find_invalid_headers(example.headers))
|
|
311
|
+
if invalid_headers:
|
|
312
|
+
InvalidHeadersExampleMark.set(original_test, invalid_headers)
|
|
313
|
+
continue
|
|
314
|
+
adjust_urlencoded_payload(example)
|
|
315
|
+
yield example
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def adjust_urlencoded_payload(case: Case) -> None:
|
|
319
|
+
if case.media_type is not None:
|
|
320
|
+
try:
|
|
321
|
+
media_type = media_types.parse(case.media_type)
|
|
322
|
+
if media_type == ("application", "x-www-form-urlencoded"):
|
|
323
|
+
case.body = prepare_urlencoded(case.body)
|
|
324
|
+
except ValueError:
|
|
325
|
+
pass
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def add_coverage(
|
|
329
|
+
test: Callable,
|
|
330
|
+
operation: APIOperation,
|
|
331
|
+
generation_modes: list[GenerationMode],
|
|
332
|
+
auth_storage: AuthStorage | None,
|
|
333
|
+
as_strategy_kwargs: dict[str, Any],
|
|
334
|
+
generate_duplicate_query_parameters: bool,
|
|
335
|
+
unexpected_methods: set[str],
|
|
336
|
+
generation_config: GenerationConfig,
|
|
337
|
+
) -> Callable:
|
|
338
|
+
for case in generate_coverage_cases(
|
|
339
|
+
operation=operation,
|
|
340
|
+
generation_modes=generation_modes,
|
|
341
|
+
auth_storage=auth_storage,
|
|
342
|
+
as_strategy_kwargs=as_strategy_kwargs,
|
|
343
|
+
generate_duplicate_query_parameters=generate_duplicate_query_parameters,
|
|
344
|
+
unexpected_methods=unexpected_methods,
|
|
345
|
+
generation_config=generation_config,
|
|
346
|
+
):
|
|
347
|
+
test = hypothesis.example(case=case)(test)
|
|
348
|
+
return test
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def generate_coverage_cases(
|
|
352
|
+
*,
|
|
353
|
+
operation: APIOperation,
|
|
354
|
+
generation_modes: list[GenerationMode],
|
|
355
|
+
auth_storage: AuthStorage | None,
|
|
356
|
+
as_strategy_kwargs: dict[str, Any],
|
|
357
|
+
generate_duplicate_query_parameters: bool,
|
|
358
|
+
unexpected_methods: set[str],
|
|
359
|
+
generation_config: GenerationConfig,
|
|
360
|
+
) -> Generator[Case]:
|
|
361
|
+
from schemathesis.core.parameters import LOCATION_TO_CONTAINER
|
|
362
|
+
|
|
363
|
+
auth_context = auths.AuthContext(
|
|
364
|
+
operation=operation,
|
|
365
|
+
app=operation.app,
|
|
366
|
+
)
|
|
367
|
+
overrides = {
|
|
368
|
+
container: as_strategy_kwargs[container]
|
|
369
|
+
for container in LOCATION_TO_CONTAINER.values()
|
|
370
|
+
if container in as_strategy_kwargs
|
|
371
|
+
}
|
|
372
|
+
with warnings.catch_warnings():
|
|
373
|
+
warnings.filterwarnings(
|
|
374
|
+
"ignore", message=".*but this is not valid syntax for a Python regular expression.*", category=UserWarning
|
|
375
|
+
)
|
|
376
|
+
for case in _iter_coverage_cases(
|
|
377
|
+
operation=operation,
|
|
378
|
+
generation_modes=generation_modes,
|
|
379
|
+
generate_duplicate_query_parameters=generate_duplicate_query_parameters,
|
|
380
|
+
unexpected_methods=unexpected_methods,
|
|
381
|
+
generation_config=generation_config,
|
|
382
|
+
):
|
|
383
|
+
if case.media_type and operation.schema.transport.get_first_matching_media_type(case.media_type) is None:
|
|
384
|
+
continue
|
|
385
|
+
adjust_urlencoded_payload(case)
|
|
386
|
+
auths.set_on_case(case, auth_context, auth_storage)
|
|
387
|
+
for container_name, value in overrides.items():
|
|
388
|
+
container = getattr(case, container_name)
|
|
389
|
+
if container is None:
|
|
390
|
+
setattr(case, container_name, value)
|
|
391
|
+
else:
|
|
392
|
+
container.update(value)
|
|
393
|
+
yield case
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
class Instant:
|
|
397
|
+
__slots__ = ("start",)
|
|
398
|
+
|
|
399
|
+
def __init__(self) -> None:
|
|
400
|
+
self.start = perf_counter()
|
|
401
|
+
|
|
402
|
+
@property
|
|
403
|
+
def elapsed(self) -> float:
|
|
404
|
+
return perf_counter() - self.start
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
class Template:
|
|
408
|
+
__slots__ = ("_components", "_template", "_serializers")
|
|
409
|
+
|
|
410
|
+
def __init__(self, serializers: dict[str, Callable]) -> None:
|
|
411
|
+
self._components: dict[ParameterLocation, ComponentInfo] = {}
|
|
412
|
+
self._template: dict[str, Any] = {}
|
|
413
|
+
self._serializers = serializers
|
|
414
|
+
|
|
415
|
+
def __contains__(self, key: str) -> bool:
|
|
416
|
+
return key in self._template
|
|
417
|
+
|
|
418
|
+
def __getitem__(self, key: str) -> dict:
|
|
419
|
+
return self._template[key]
|
|
420
|
+
|
|
421
|
+
def get(self, key: str, default: Any = None) -> dict:
|
|
422
|
+
return self._template.get(key, default)
|
|
423
|
+
|
|
424
|
+
def add_parameter(self, location: ParameterLocation, name: str, value: coverage.GeneratedValue) -> None:
|
|
425
|
+
info = self._components.get(location)
|
|
426
|
+
if info is None:
|
|
427
|
+
self._components[location] = ComponentInfo(mode=value.generation_mode)
|
|
428
|
+
elif value.generation_mode == GenerationMode.NEGATIVE:
|
|
429
|
+
info.mode = GenerationMode.NEGATIVE
|
|
430
|
+
|
|
431
|
+
container = self._template.setdefault(location.container_name, {})
|
|
432
|
+
container[name] = value.value
|
|
433
|
+
|
|
434
|
+
def set_body(self, body: coverage.GeneratedValue, media_type: str) -> None:
|
|
435
|
+
self._template["body"] = body.value
|
|
436
|
+
self._template["media_type"] = media_type
|
|
437
|
+
self._components[ParameterLocation.BODY] = ComponentInfo(mode=body.generation_mode)
|
|
438
|
+
|
|
439
|
+
def _serialize(self, kwargs: dict[str, Any]) -> dict[str, Any]:
|
|
440
|
+
from schemathesis.specs.openapi._hypothesis import quote_all
|
|
441
|
+
|
|
442
|
+
output = {}
|
|
443
|
+
for container_name, value in kwargs.items():
|
|
444
|
+
serializer = self._serializers.get(container_name)
|
|
445
|
+
if container_name in ("headers", "cookies") and isinstance(value, dict):
|
|
446
|
+
value = _stringify_value(value, container_name)
|
|
447
|
+
if serializer is not None:
|
|
448
|
+
value = serializer(value)
|
|
449
|
+
if container_name == "query" and isinstance(value, dict):
|
|
450
|
+
value = _stringify_value(value, container_name)
|
|
451
|
+
if container_name == "path_parameters" and isinstance(value, dict):
|
|
452
|
+
value = _stringify_value(quote_all(value), container_name)
|
|
453
|
+
output[container_name] = value
|
|
454
|
+
return output
|
|
455
|
+
|
|
456
|
+
def unmodified(self) -> TemplateValue:
|
|
457
|
+
kwargs = deepclone(self._template)
|
|
458
|
+
kwargs = self._serialize(kwargs)
|
|
459
|
+
return TemplateValue(kwargs=kwargs, components=self._components.copy())
|
|
460
|
+
|
|
461
|
+
def with_body(self, *, media_type: str, value: coverage.GeneratedValue) -> TemplateValue:
|
|
462
|
+
kwargs = {**self._template, "media_type": media_type, "body": value.value}
|
|
463
|
+
kwargs = self._serialize(kwargs)
|
|
464
|
+
components = {**self._components, ParameterLocation.BODY: ComponentInfo(mode=value.generation_mode)}
|
|
465
|
+
return TemplateValue(kwargs=kwargs, components=components)
|
|
466
|
+
|
|
467
|
+
def with_parameter(
|
|
468
|
+
self, *, location: ParameterLocation, name: str, value: coverage.GeneratedValue
|
|
469
|
+
) -> TemplateValue:
|
|
470
|
+
container = self._template[location.container_name]
|
|
471
|
+
return self.with_location(
|
|
472
|
+
location=location,
|
|
473
|
+
value={**container, name: value.value},
|
|
474
|
+
generation_mode=value.generation_mode,
|
|
475
|
+
)
|
|
476
|
+
|
|
477
|
+
def with_location(
|
|
478
|
+
self, *, location: ParameterLocation, value: Any, generation_mode: GenerationMode
|
|
479
|
+
) -> TemplateValue:
|
|
480
|
+
kwargs = {**self._template, location.container_name: value}
|
|
481
|
+
components = {**self._components, location: ComponentInfo(mode=generation_mode)}
|
|
482
|
+
kwargs = self._serialize(kwargs)
|
|
483
|
+
return TemplateValue(kwargs=kwargs, components=components)
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
@dataclass
|
|
487
|
+
class TemplateValue:
|
|
488
|
+
kwargs: dict[str, Any]
|
|
489
|
+
components: dict[ParameterLocation, ComponentInfo]
|
|
490
|
+
|
|
491
|
+
__slots__ = ("kwargs", "components")
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
def _stringify_value(val: Any, container_name: str) -> Any:
|
|
495
|
+
if val is None:
|
|
496
|
+
return "null"
|
|
497
|
+
if val is True:
|
|
498
|
+
return "true"
|
|
499
|
+
if val is False:
|
|
500
|
+
return "false"
|
|
501
|
+
if isinstance(val, (int, float)):
|
|
502
|
+
return str(val)
|
|
503
|
+
if isinstance(val, list):
|
|
504
|
+
if container_name == "query":
|
|
505
|
+
# Having a list here ensures there will be multiple query parameters wit the same name
|
|
506
|
+
return [_stringify_value(item, container_name) for item in val]
|
|
507
|
+
# use comma-separated values style for arrays
|
|
508
|
+
return ",".join(str(_stringify_value(sub, container_name)) for sub in val)
|
|
509
|
+
if isinstance(val, dict):
|
|
510
|
+
return {key: _stringify_value(sub, container_name) for key, sub in val.items()}
|
|
511
|
+
return val
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
def _iter_coverage_cases(
|
|
515
|
+
*,
|
|
516
|
+
operation: APIOperation,
|
|
517
|
+
generation_modes: list[GenerationMode],
|
|
518
|
+
generate_duplicate_query_parameters: bool,
|
|
519
|
+
unexpected_methods: set[str],
|
|
520
|
+
generation_config: GenerationConfig,
|
|
521
|
+
) -> Generator[Case, None, None]:
|
|
522
|
+
from schemathesis.specs.openapi._hypothesis import _build_custom_formats
|
|
523
|
+
from schemathesis.specs.openapi.examples import find_matching_in_responses
|
|
524
|
+
from schemathesis.specs.openapi.media_types import MEDIA_TYPES
|
|
525
|
+
from schemathesis.specs.openapi.schemas import BaseOpenAPISchema
|
|
526
|
+
from schemathesis.specs.openapi.serialization import get_serializers_for_operation
|
|
527
|
+
|
|
528
|
+
generators: dict[tuple[ParameterLocation, str], Generator[coverage.GeneratedValue, None, None]] = {}
|
|
529
|
+
serializers = get_serializers_for_operation(operation)
|
|
530
|
+
template = Template(serializers)
|
|
531
|
+
|
|
532
|
+
instant = Instant()
|
|
533
|
+
responses = list(operation.responses.iter_examples())
|
|
534
|
+
custom_formats = _build_custom_formats(generation_config)
|
|
535
|
+
|
|
536
|
+
seen_negative = coverage.HashSet()
|
|
537
|
+
seen_positive = coverage.HashSet()
|
|
538
|
+
assert isinstance(operation.schema, BaseOpenAPISchema)
|
|
539
|
+
validator_cls = operation.schema.adapter.jsonschema_validator_cls
|
|
540
|
+
|
|
541
|
+
for parameter in operation.iter_parameters():
|
|
542
|
+
location = parameter.location
|
|
543
|
+
name = parameter.name
|
|
544
|
+
schema = parameter.unoptimized_schema
|
|
545
|
+
examples = parameter.examples
|
|
546
|
+
if examples:
|
|
547
|
+
schema = dict(schema)
|
|
548
|
+
schema["examples"] = examples
|
|
549
|
+
for value in find_matching_in_responses(responses, parameter.name):
|
|
550
|
+
schema.setdefault("examples", []).append(value)
|
|
551
|
+
gen = coverage.cover_schema_iter(
|
|
552
|
+
coverage.CoverageContext(
|
|
553
|
+
root_schema=schema,
|
|
554
|
+
location=location,
|
|
555
|
+
media_type=None,
|
|
556
|
+
generation_modes=generation_modes,
|
|
557
|
+
is_required=parameter.is_required,
|
|
558
|
+
custom_formats=custom_formats,
|
|
559
|
+
validator_cls=validator_cls,
|
|
560
|
+
allow_extra_parameters=generation_config.allow_extra_parameters,
|
|
561
|
+
),
|
|
562
|
+
schema,
|
|
563
|
+
)
|
|
564
|
+
value = next(gen, NOT_SET)
|
|
565
|
+
if isinstance(value, NotSet):
|
|
566
|
+
if location == ParameterLocation.PATH:
|
|
567
|
+
# Can't skip path parameters - they should be filled
|
|
568
|
+
schema = dict(schema)
|
|
569
|
+
schema.setdefault("type", "string")
|
|
570
|
+
schema.setdefault("minLength", 1)
|
|
571
|
+
gen = coverage.cover_schema_iter(
|
|
572
|
+
coverage.CoverageContext(
|
|
573
|
+
root_schema=schema,
|
|
574
|
+
location=location,
|
|
575
|
+
media_type=None,
|
|
576
|
+
generation_modes=[GenerationMode.POSITIVE],
|
|
577
|
+
is_required=parameter.is_required,
|
|
578
|
+
custom_formats=custom_formats,
|
|
579
|
+
validator_cls=validator_cls,
|
|
580
|
+
allow_extra_parameters=generation_config.allow_extra_parameters,
|
|
581
|
+
),
|
|
582
|
+
schema,
|
|
583
|
+
)
|
|
584
|
+
value = next(
|
|
585
|
+
gen,
|
|
586
|
+
coverage.GeneratedValue(
|
|
587
|
+
"value",
|
|
588
|
+
generation_mode=GenerationMode.NEGATIVE,
|
|
589
|
+
scenario=CoverageScenario.UNSUPPORTED_PATH_PATTERN,
|
|
590
|
+
description="Sample value for unsupported path parameter pattern",
|
|
591
|
+
parameter=name,
|
|
592
|
+
location="/",
|
|
593
|
+
),
|
|
594
|
+
)
|
|
595
|
+
template.add_parameter(location, name, value)
|
|
596
|
+
continue
|
|
597
|
+
continue
|
|
598
|
+
template.add_parameter(location, name, value)
|
|
599
|
+
generators[(location, name)] = gen
|
|
600
|
+
template_time = instant.elapsed
|
|
601
|
+
if operation.body:
|
|
602
|
+
for body in operation.body:
|
|
603
|
+
instant = Instant()
|
|
604
|
+
schema = body.unoptimized_schema
|
|
605
|
+
examples = body.examples
|
|
606
|
+
if examples:
|
|
607
|
+
schema = dict(schema)
|
|
608
|
+
# User-registered media types should only handle text / binary data
|
|
609
|
+
if body.media_type in MEDIA_TYPES:
|
|
610
|
+
schema["examples"] = [example for example in examples if isinstance(example, (str, bytes))]
|
|
611
|
+
else:
|
|
612
|
+
schema["examples"] = examples
|
|
613
|
+
try:
|
|
614
|
+
media_type = media_types.parse(body.media_type)
|
|
615
|
+
except MalformedMediaType:
|
|
616
|
+
media_type = None
|
|
617
|
+
gen = coverage.cover_schema_iter(
|
|
618
|
+
coverage.CoverageContext(
|
|
619
|
+
root_schema=schema,
|
|
620
|
+
location=ParameterLocation.BODY,
|
|
621
|
+
media_type=media_type,
|
|
622
|
+
generation_modes=generation_modes,
|
|
623
|
+
is_required=body.is_required,
|
|
624
|
+
custom_formats=custom_formats,
|
|
625
|
+
validator_cls=validator_cls,
|
|
626
|
+
allow_extra_parameters=generation_config.allow_extra_parameters,
|
|
627
|
+
),
|
|
628
|
+
schema,
|
|
629
|
+
)
|
|
630
|
+
value = next(gen, NOT_SET)
|
|
631
|
+
if isinstance(value, NotSet) or (
|
|
632
|
+
body.media_type in MEDIA_TYPES and not isinstance(value.value, (str, bytes))
|
|
633
|
+
):
|
|
634
|
+
continue
|
|
635
|
+
elapsed = instant.elapsed
|
|
636
|
+
if "body" not in template:
|
|
637
|
+
template_time += elapsed
|
|
638
|
+
template.set_body(value, body.media_type)
|
|
639
|
+
data = template.with_body(value=value, media_type=body.media_type)
|
|
640
|
+
yield operation.Case(
|
|
641
|
+
**data.kwargs,
|
|
642
|
+
_meta=CaseMetadata(
|
|
643
|
+
generation=GenerationInfo(
|
|
644
|
+
time=elapsed,
|
|
645
|
+
mode=value.generation_mode,
|
|
646
|
+
),
|
|
647
|
+
components=data.components,
|
|
648
|
+
phase=PhaseInfo.coverage(
|
|
649
|
+
scenario=value.scenario,
|
|
650
|
+
description=value.description,
|
|
651
|
+
location=value.location,
|
|
652
|
+
parameter=body.media_type,
|
|
653
|
+
parameter_location=ParameterLocation.BODY,
|
|
654
|
+
),
|
|
655
|
+
),
|
|
656
|
+
)
|
|
657
|
+
iterator = iter(gen)
|
|
658
|
+
while True:
|
|
659
|
+
instant = Instant()
|
|
660
|
+
try:
|
|
661
|
+
next_value = next(iterator)
|
|
662
|
+
if body.media_type in MEDIA_TYPES and not isinstance(next_value.value, (str, bytes)):
|
|
663
|
+
continue
|
|
664
|
+
|
|
665
|
+
data = template.with_body(value=next_value, media_type=body.media_type)
|
|
666
|
+
yield operation.Case(
|
|
667
|
+
**data.kwargs,
|
|
668
|
+
_meta=CaseMetadata(
|
|
669
|
+
generation=GenerationInfo(
|
|
670
|
+
time=instant.elapsed,
|
|
671
|
+
mode=next_value.generation_mode,
|
|
672
|
+
),
|
|
673
|
+
components=data.components,
|
|
674
|
+
phase=PhaseInfo.coverage(
|
|
675
|
+
scenario=next_value.scenario,
|
|
676
|
+
description=next_value.description,
|
|
677
|
+
location=next_value.location,
|
|
678
|
+
parameter=body.media_type,
|
|
679
|
+
parameter_location=ParameterLocation.BODY,
|
|
680
|
+
),
|
|
681
|
+
),
|
|
682
|
+
)
|
|
683
|
+
except StopIteration:
|
|
684
|
+
break
|
|
685
|
+
elif GenerationMode.POSITIVE in generation_modes:
|
|
686
|
+
data = template.unmodified()
|
|
687
|
+
seen_positive.insert(data.kwargs)
|
|
688
|
+
yield operation.Case(
|
|
689
|
+
**data.kwargs,
|
|
690
|
+
_meta=CaseMetadata(
|
|
691
|
+
generation=GenerationInfo(
|
|
692
|
+
time=template_time,
|
|
693
|
+
mode=GenerationMode.POSITIVE,
|
|
694
|
+
),
|
|
695
|
+
components=data.components,
|
|
696
|
+
phase=PhaseInfo.coverage(
|
|
697
|
+
scenario=CoverageScenario.DEFAULT_POSITIVE_TEST, description="Default positive test case"
|
|
698
|
+
),
|
|
699
|
+
),
|
|
700
|
+
)
|
|
701
|
+
|
|
702
|
+
for (location, name), gen in generators.items():
|
|
703
|
+
iterator = iter(gen)
|
|
704
|
+
while True:
|
|
705
|
+
instant = Instant()
|
|
706
|
+
try:
|
|
707
|
+
value = next(iterator)
|
|
708
|
+
data = template.with_parameter(location=location, name=name, value=value)
|
|
709
|
+
except StopIteration:
|
|
710
|
+
break
|
|
711
|
+
|
|
712
|
+
if value.generation_mode == GenerationMode.NEGATIVE:
|
|
713
|
+
seen_negative.insert(data.kwargs)
|
|
714
|
+
elif value.generation_mode == GenerationMode.POSITIVE and not seen_positive.insert(data.kwargs):
|
|
715
|
+
# Was already generated before
|
|
716
|
+
continue
|
|
717
|
+
|
|
718
|
+
yield operation.Case(
|
|
719
|
+
**data.kwargs,
|
|
720
|
+
_meta=CaseMetadata(
|
|
721
|
+
generation=GenerationInfo(time=instant.elapsed, mode=value.generation_mode),
|
|
722
|
+
components=data.components,
|
|
723
|
+
phase=PhaseInfo.coverage(
|
|
724
|
+
scenario=value.scenario,
|
|
725
|
+
description=value.description,
|
|
726
|
+
location=value.location,
|
|
727
|
+
parameter=name,
|
|
728
|
+
parameter_location=location,
|
|
729
|
+
),
|
|
730
|
+
),
|
|
731
|
+
)
|
|
732
|
+
if GenerationMode.NEGATIVE in generation_modes:
|
|
733
|
+
# Generate HTTP methods that are not specified in the spec
|
|
734
|
+
methods = unexpected_methods - set(operation.schema[operation.path])
|
|
735
|
+
for method in sorted(methods):
|
|
736
|
+
instant = Instant()
|
|
737
|
+
data = template.unmodified()
|
|
738
|
+
yield operation.Case(
|
|
739
|
+
**data.kwargs,
|
|
740
|
+
method=method.upper(),
|
|
741
|
+
_meta=CaseMetadata(
|
|
742
|
+
generation=GenerationInfo(time=instant.elapsed, mode=GenerationMode.NEGATIVE),
|
|
743
|
+
components=data.components,
|
|
744
|
+
phase=PhaseInfo.coverage(
|
|
745
|
+
scenario=CoverageScenario.UNSPECIFIED_HTTP_METHOD,
|
|
746
|
+
description=f"Unspecified HTTP method: {method.upper()}",
|
|
747
|
+
),
|
|
748
|
+
),
|
|
749
|
+
)
|
|
750
|
+
# Generate duplicate query parameters
|
|
751
|
+
# NOTE: if the query schema has no constraints, then we may have no negative test cases at all
|
|
752
|
+
# as they all will match the original schema and therefore will be considered as positive ones
|
|
753
|
+
if generate_duplicate_query_parameters and operation.query and "query" in template:
|
|
754
|
+
container = template["query"]
|
|
755
|
+
for parameter in operation.query:
|
|
756
|
+
instant = Instant()
|
|
757
|
+
# Could be absent if value schema can't be negated
|
|
758
|
+
# I.e. contains just `default` value without any other keywords
|
|
759
|
+
value = container.get(parameter.name, NOT_SET)
|
|
760
|
+
if value is not NOT_SET:
|
|
761
|
+
data = template.with_location(
|
|
762
|
+
location=ParameterLocation.QUERY,
|
|
763
|
+
value={**container, parameter.name: [value, value]},
|
|
764
|
+
generation_mode=GenerationMode.NEGATIVE,
|
|
765
|
+
)
|
|
766
|
+
yield operation.Case(
|
|
767
|
+
**data.kwargs,
|
|
768
|
+
_meta=CaseMetadata(
|
|
769
|
+
generation=GenerationInfo(time=instant.elapsed, mode=GenerationMode.NEGATIVE),
|
|
770
|
+
components=data.components,
|
|
771
|
+
phase=PhaseInfo.coverage(
|
|
772
|
+
scenario=CoverageScenario.DUPLICATE_PARAMETER,
|
|
773
|
+
description=f"Duplicate `{parameter.name}` query parameter",
|
|
774
|
+
parameter=parameter.name,
|
|
775
|
+
parameter_location=ParameterLocation.QUERY,
|
|
776
|
+
),
|
|
777
|
+
),
|
|
778
|
+
)
|
|
779
|
+
# Generate missing required parameters
|
|
780
|
+
for parameter in operation.iter_parameters():
|
|
781
|
+
if parameter.is_required and parameter.location != ParameterLocation.PATH:
|
|
782
|
+
instant = Instant()
|
|
783
|
+
name = parameter.name
|
|
784
|
+
location = parameter.location
|
|
785
|
+
container = template.get(location.container_name, {})
|
|
786
|
+
data = template.with_location(
|
|
787
|
+
location=location,
|
|
788
|
+
value={k: v for k, v in container.items() if k != name},
|
|
789
|
+
generation_mode=GenerationMode.NEGATIVE,
|
|
790
|
+
)
|
|
791
|
+
|
|
792
|
+
if seen_negative.insert(data.kwargs):
|
|
793
|
+
yield operation.Case(
|
|
794
|
+
**data.kwargs,
|
|
795
|
+
_meta=CaseMetadata(
|
|
796
|
+
generation=GenerationInfo(time=instant.elapsed, mode=GenerationMode.NEGATIVE),
|
|
797
|
+
components=data.components,
|
|
798
|
+
phase=PhaseInfo.coverage(
|
|
799
|
+
scenario=CoverageScenario.MISSING_PARAMETER,
|
|
800
|
+
description=f"Missing `{name}` at {location.value}",
|
|
801
|
+
parameter=name,
|
|
802
|
+
parameter_location=location,
|
|
803
|
+
),
|
|
804
|
+
),
|
|
805
|
+
)
|
|
806
|
+
# Generate combinations for each location
|
|
807
|
+
for location, parameter_set in [
|
|
808
|
+
(ParameterLocation.QUERY, operation.query),
|
|
809
|
+
(ParameterLocation.HEADER, operation.headers),
|
|
810
|
+
(ParameterLocation.COOKIE, operation.cookies),
|
|
811
|
+
]:
|
|
812
|
+
if not parameter_set:
|
|
813
|
+
continue
|
|
814
|
+
|
|
815
|
+
container_name = location.container_name
|
|
816
|
+
base_container = template.get(container_name, {})
|
|
817
|
+
|
|
818
|
+
# Get required and optional parameters
|
|
819
|
+
required = {p.name for p in parameter_set if p.is_required}
|
|
820
|
+
all_params = {p.name for p in parameter_set}
|
|
821
|
+
optional = sorted(all_params - required)
|
|
822
|
+
|
|
823
|
+
# Helper function to create and yield a case
|
|
824
|
+
def make_case(
|
|
825
|
+
container_values: dict,
|
|
826
|
+
scenario: CoverageScenario,
|
|
827
|
+
description: str,
|
|
828
|
+
_location: ParameterLocation,
|
|
829
|
+
_parameter: str | None,
|
|
830
|
+
_generation_mode: GenerationMode,
|
|
831
|
+
_instant: Instant,
|
|
832
|
+
) -> Case:
|
|
833
|
+
data = template.with_location(location=_location, value=container_values, generation_mode=_generation_mode)
|
|
834
|
+
return operation.Case(
|
|
835
|
+
**data.kwargs,
|
|
836
|
+
_meta=CaseMetadata(
|
|
837
|
+
generation=GenerationInfo(
|
|
838
|
+
time=_instant.elapsed,
|
|
839
|
+
mode=_generation_mode,
|
|
840
|
+
),
|
|
841
|
+
components=data.components,
|
|
842
|
+
phase=PhaseInfo.coverage(
|
|
843
|
+
scenario=scenario,
|
|
844
|
+
description=description,
|
|
845
|
+
parameter=_parameter,
|
|
846
|
+
parameter_location=_location,
|
|
847
|
+
),
|
|
848
|
+
),
|
|
849
|
+
)
|
|
850
|
+
|
|
851
|
+
def _combination_schema(
|
|
852
|
+
combination: dict[str, Any], _required: set[str], _parameter_set: ParameterSet
|
|
853
|
+
) -> dict[str, Any]:
|
|
854
|
+
return {
|
|
855
|
+
"properties": {
|
|
856
|
+
parameter.name: parameter.optimized_schema
|
|
857
|
+
for parameter in _parameter_set
|
|
858
|
+
if parameter.name in combination
|
|
859
|
+
},
|
|
860
|
+
"required": list(_required),
|
|
861
|
+
"additionalProperties": False,
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
def _yield_negative(
|
|
865
|
+
subschema: dict[str, Any], _location: ParameterLocation, is_required: bool
|
|
866
|
+
) -> Generator[Case, None, None]:
|
|
867
|
+
iterator = iter(
|
|
868
|
+
coverage.cover_schema_iter(
|
|
869
|
+
coverage.CoverageContext(
|
|
870
|
+
root_schema=subschema,
|
|
871
|
+
location=_location,
|
|
872
|
+
media_type=None,
|
|
873
|
+
generation_modes=[GenerationMode.NEGATIVE],
|
|
874
|
+
is_required=is_required,
|
|
875
|
+
custom_formats=custom_formats,
|
|
876
|
+
validator_cls=validator_cls,
|
|
877
|
+
allow_extra_parameters=generation_config.allow_extra_parameters,
|
|
878
|
+
),
|
|
879
|
+
subschema,
|
|
880
|
+
)
|
|
881
|
+
)
|
|
882
|
+
while True:
|
|
883
|
+
instant = Instant()
|
|
884
|
+
try:
|
|
885
|
+
more = next(iterator)
|
|
886
|
+
yield make_case(
|
|
887
|
+
more.value,
|
|
888
|
+
more.scenario,
|
|
889
|
+
more.description,
|
|
890
|
+
_location,
|
|
891
|
+
more.parameter,
|
|
892
|
+
GenerationMode.NEGATIVE,
|
|
893
|
+
instant,
|
|
894
|
+
)
|
|
895
|
+
except StopIteration:
|
|
896
|
+
break
|
|
897
|
+
|
|
898
|
+
# 1. Generate only required properties
|
|
899
|
+
if required and all_params != required:
|
|
900
|
+
only_required = {k: v for k, v in base_container.items() if k in required}
|
|
901
|
+
if GenerationMode.POSITIVE in generation_modes:
|
|
902
|
+
yield make_case(
|
|
903
|
+
only_required,
|
|
904
|
+
CoverageScenario.OBJECT_ONLY_REQUIRED,
|
|
905
|
+
"Only required properties",
|
|
906
|
+
location,
|
|
907
|
+
None,
|
|
908
|
+
GenerationMode.POSITIVE,
|
|
909
|
+
Instant(),
|
|
910
|
+
)
|
|
911
|
+
if GenerationMode.NEGATIVE in generation_modes:
|
|
912
|
+
subschema = _combination_schema(only_required, required, parameter_set)
|
|
913
|
+
for case in _yield_negative(subschema, location, is_required=bool(required)):
|
|
914
|
+
kwargs = _case_to_kwargs(case)
|
|
915
|
+
if not seen_negative.insert(kwargs):
|
|
916
|
+
continue
|
|
917
|
+
assert case.meta is not None
|
|
918
|
+
assert isinstance(case.meta.phase.data, CoveragePhaseData)
|
|
919
|
+
# Already generated in one of the blocks above
|
|
920
|
+
if (
|
|
921
|
+
location != "path"
|
|
922
|
+
and case.meta.phase.data.scenario != CoverageScenario.OBJECT_MISSING_REQUIRED_PROPERTY
|
|
923
|
+
):
|
|
924
|
+
yield case
|
|
925
|
+
|
|
926
|
+
# 2. Generate combinations with required properties and one optional property
|
|
927
|
+
for opt_param in optional:
|
|
928
|
+
combo = {k: v for k, v in base_container.items() if k in required or k == opt_param}
|
|
929
|
+
if combo != base_container and GenerationMode.POSITIVE in generation_modes:
|
|
930
|
+
yield make_case(
|
|
931
|
+
combo,
|
|
932
|
+
CoverageScenario.OBJECT_REQUIRED_AND_OPTIONAL,
|
|
933
|
+
f"All required properties and optional '{opt_param}'",
|
|
934
|
+
location,
|
|
935
|
+
None,
|
|
936
|
+
GenerationMode.POSITIVE,
|
|
937
|
+
Instant(),
|
|
938
|
+
)
|
|
939
|
+
if GenerationMode.NEGATIVE in generation_modes:
|
|
940
|
+
subschema = _combination_schema(combo, required, parameter_set)
|
|
941
|
+
for case in _yield_negative(subschema, location, is_required=bool(required)):
|
|
942
|
+
assert case.meta is not None
|
|
943
|
+
assert isinstance(case.meta.phase.data, CoveragePhaseData)
|
|
944
|
+
# Already generated in one of the blocks above
|
|
945
|
+
if (
|
|
946
|
+
location != "path"
|
|
947
|
+
and case.meta.phase.data.scenario != CoverageScenario.OBJECT_MISSING_REQUIRED_PROPERTY
|
|
948
|
+
):
|
|
949
|
+
yield case
|
|
950
|
+
|
|
951
|
+
# 3. Generate one combination for each size from 2 to N-1 of optional parameters
|
|
952
|
+
if len(optional) > 1 and GenerationMode.POSITIVE in generation_modes:
|
|
953
|
+
for size in range(2, len(optional)):
|
|
954
|
+
for combination in combinations(optional, size):
|
|
955
|
+
combo = {k: v for k, v in base_container.items() if k in required or k in combination}
|
|
956
|
+
if combo != base_container:
|
|
957
|
+
yield make_case(
|
|
958
|
+
combo,
|
|
959
|
+
CoverageScenario.OBJECT_REQUIRED_AND_OPTIONAL,
|
|
960
|
+
f"All required and {size} optional properties",
|
|
961
|
+
location,
|
|
962
|
+
None,
|
|
963
|
+
GenerationMode.POSITIVE,
|
|
964
|
+
Instant(),
|
|
965
|
+
)
|
|
966
|
+
|
|
967
|
+
|
|
968
|
+
def _case_to_kwargs(case: Case) -> dict:
|
|
969
|
+
kwargs = {}
|
|
970
|
+
for container_name in LOCATION_TO_CONTAINER.values():
|
|
971
|
+
value = getattr(case, container_name)
|
|
972
|
+
if isinstance(value, CaseInsensitiveDict) and value:
|
|
973
|
+
kwargs[container_name] = dict(value)
|
|
974
|
+
elif value and value is not NOT_SET:
|
|
975
|
+
kwargs[container_name] = value
|
|
976
|
+
return kwargs
|
|
977
|
+
|
|
978
|
+
|
|
979
|
+
def find_invalid_headers(headers: Mapping) -> Generator[tuple[str, str], None, None]:
|
|
980
|
+
for name, value in headers.items():
|
|
981
|
+
if not is_latin_1_encodable(value) or has_invalid_characters(name, value):
|
|
982
|
+
yield name, value
|
|
983
|
+
|
|
984
|
+
|
|
985
|
+
UnsatisfiableExampleMark = Mark[Unsatisfiable](attr_name="unsatisfiable_example")
|
|
986
|
+
NonSerializableMark = Mark[SerializationNotPossible](attr_name="non_serializable")
|
|
987
|
+
InvalidRegexMark = Mark[SchemaError](attr_name="invalid_regex")
|
|
988
|
+
InvalidHeadersExampleMark = Mark[dict[str, str]](attr_name="invalid_example_header")
|
|
989
|
+
MissingPathParameters = Mark[InvalidSchema](attr_name="missing_path_parameters")
|
|
990
|
+
InfiniteRecursiveReferenceMark = Mark[InfiniteRecursiveReference](attr_name="infinite_recursive_reference")
|
|
991
|
+
UnresolvableReferenceMark = Mark[UnresolvableReference](attr_name="unresolvable_reference")
|
|
992
|
+
ApiOperationMark = Mark[APIOperation](attr_name="api_operation")
|