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
schemathesis/cli/__init__.py
CHANGED
|
@@ -1,1017 +1,28 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
from
|
|
7
|
-
from
|
|
8
|
-
from
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
DataGenerationMethod,
|
|
27
|
-
)
|
|
28
|
-
from ..exceptions import HTTPError
|
|
29
|
-
from ..fixups import ALL_FIXUPS
|
|
30
|
-
from ..hooks import GLOBAL_HOOK_DISPATCHER, HookContext, HookDispatcher, HookScope
|
|
31
|
-
from ..models import Case, CheckFunction
|
|
32
|
-
from ..runner import events, prepare_hypothesis_settings
|
|
33
|
-
from ..schemas import BaseSchema
|
|
34
|
-
from ..specs.graphql import loaders as gql_loaders
|
|
35
|
-
from ..specs.graphql.schemas import GraphQLSchema
|
|
36
|
-
from ..specs.openapi import loaders as oas_loaders
|
|
37
|
-
from ..stateful import Stateful
|
|
38
|
-
from ..targets import Target
|
|
39
|
-
from ..types import Filter, RequestCert
|
|
40
|
-
from ..utils import GenericResponse, file_exists, get_requests_auth, import_app
|
|
41
|
-
from . import callbacks, cassettes, output
|
|
42
|
-
from .constants import DEFAULT_WORKERS, MAX_WORKERS, MIN_WORKERS
|
|
43
|
-
from .context import ExecutionContext, ServiceContext
|
|
44
|
-
from .debug import DebugOutputHandler
|
|
45
|
-
from .handlers import EventHandler
|
|
46
|
-
from .junitxml import JunitXMLHandler
|
|
47
|
-
from .options import CSVOption, CustomHelpMessageChoice, NotSet, OptionalInt
|
|
48
|
-
|
|
49
|
-
try:
|
|
50
|
-
from yaml import CSafeLoader as SafeLoader
|
|
51
|
-
except ImportError:
|
|
52
|
-
# pylint: disable=unused-import
|
|
53
|
-
from yaml import SafeLoader # type: ignore
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
def _get_callable_names(items: Tuple[Callable, ...]) -> Tuple[str, ...]:
|
|
57
|
-
return tuple(item.__name__ for item in items)
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
CONTEXT_SETTINGS = {"help_option_names": ["-h", "--help"]}
|
|
61
|
-
|
|
62
|
-
DEFAULT_CHECKS_NAMES = _get_callable_names(checks_module.DEFAULT_CHECKS)
|
|
63
|
-
ALL_CHECKS_NAMES = _get_callable_names(checks_module.ALL_CHECKS)
|
|
64
|
-
CHECKS_TYPE = click.Choice((*ALL_CHECKS_NAMES, "all"))
|
|
65
|
-
|
|
66
|
-
DEFAULT_TARGETS_NAMES = _get_callable_names(targets_module.DEFAULT_TARGETS)
|
|
67
|
-
ALL_TARGETS_NAMES = _get_callable_names(targets_module.ALL_TARGETS)
|
|
68
|
-
TARGETS_TYPE = click.Choice((*ALL_TARGETS_NAMES, "all"))
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
def register_target(function: Target) -> Target:
|
|
72
|
-
"""Register a new testing target for schemathesis CLI.
|
|
73
|
-
|
|
74
|
-
:param function: A function that will be called to calculate a metric passed to ``hypothesis.target``.
|
|
75
|
-
"""
|
|
76
|
-
targets_module.ALL_TARGETS += (function,)
|
|
77
|
-
TARGETS_TYPE.choices += (function.__name__,) # type: ignore
|
|
78
|
-
return function
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
def register_check(function: CheckFunction) -> CheckFunction:
|
|
82
|
-
"""Register a new check for schemathesis CLI.
|
|
83
|
-
|
|
84
|
-
:param function: A function to validate API responses.
|
|
85
|
-
|
|
86
|
-
.. code-block:: python
|
|
87
|
-
|
|
88
|
-
@schemathesis.register_check
|
|
89
|
-
def new_check(response, case):
|
|
90
|
-
# some awesome assertions!
|
|
91
|
-
...
|
|
92
|
-
"""
|
|
93
|
-
checks_module.ALL_CHECKS += (function,)
|
|
94
|
-
CHECKS_TYPE.choices += (function.__name__,) # type: ignore
|
|
95
|
-
return function
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
def reset_checks() -> None:
|
|
99
|
-
"""Get checks list to their default state."""
|
|
100
|
-
# Useful in tests
|
|
101
|
-
checks_module.ALL_CHECKS = checks_module.DEFAULT_CHECKS + checks_module.OPTIONAL_CHECKS
|
|
102
|
-
CHECKS_TYPE.choices = _get_callable_names(checks_module.ALL_CHECKS) + ("all",)
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
def reset_targets() -> None:
|
|
106
|
-
"""Get targets list to their default state."""
|
|
107
|
-
# Useful in tests
|
|
108
|
-
targets_module.ALL_TARGETS = targets_module.DEFAULT_TARGETS + targets_module.OPTIONAL_TARGETS
|
|
109
|
-
TARGETS_TYPE.choices = _get_callable_names(targets_module.ALL_TARGETS) + ("all",)
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
class DeprecatedOption(click.Option):
|
|
113
|
-
def __init__(self, *args: Any, removed_in: str, **kwargs: Any) -> None:
|
|
114
|
-
super().__init__(*args, **kwargs)
|
|
115
|
-
self.removed_in = removed_in
|
|
116
|
-
|
|
117
|
-
def handle_parse_result(self, ctx: click.Context, opts: Dict[str, Any], args: List[str]) -> Tuple[Any, List[str]]:
|
|
118
|
-
if self.name in opts:
|
|
119
|
-
opt_names = "/".join(f"`{name}`" for name in self.opts)
|
|
120
|
-
verb = "is" if len(self.opts) == 1 else "are"
|
|
121
|
-
click.secho(
|
|
122
|
-
f"\nWARNING: {opt_names} {verb} deprecated and will be removed in Schemathesis {self.removed_in}\n",
|
|
123
|
-
fg="yellow",
|
|
124
|
-
)
|
|
125
|
-
return super().handle_parse_result(ctx, opts, args)
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
@click.group(context_settings=CONTEXT_SETTINGS)
|
|
129
|
-
@click.option("--pre-run", help="A module to execute before the running the tests.", type=str)
|
|
130
|
-
@click.version_option()
|
|
131
|
-
def schemathesis(pre_run: Optional[str] = None) -> None:
|
|
132
|
-
"""Command line tool for testing your web application built with Open API / GraphQL specifications."""
|
|
133
|
-
if pre_run:
|
|
134
|
-
load_hook(pre_run)
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
class ParameterGroup(enum.Enum):
|
|
138
|
-
filtering = "Filtering", "These options define what parts of the API will be tested."
|
|
139
|
-
validation = "Validation", "Options, responsible for how responses & schemas will be checked."
|
|
140
|
-
hypothesis = "Hypothesis", "Configuration of the underlying Hypothesis engine."
|
|
141
|
-
generic = "Generic", None
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
class CommandWithCustomHelp(click.Command):
|
|
145
|
-
def format_options(self, ctx: click.Context, formatter: click.HelpFormatter) -> None:
|
|
146
|
-
# Group options first
|
|
147
|
-
groups = defaultdict(list)
|
|
148
|
-
for param in self.get_params(ctx):
|
|
149
|
-
rv = param.get_help_record(ctx)
|
|
150
|
-
if rv is not None:
|
|
151
|
-
if isinstance(param, GroupedOption):
|
|
152
|
-
group = param.group
|
|
153
|
-
else:
|
|
154
|
-
group = ParameterGroup.generic
|
|
155
|
-
groups[group].append(rv)
|
|
156
|
-
# Then display groups separately with optional description
|
|
157
|
-
for group in ParameterGroup:
|
|
158
|
-
opts = groups[group]
|
|
159
|
-
group_name, description = group.value
|
|
160
|
-
with formatter.section(f"{group_name} options"):
|
|
161
|
-
if description:
|
|
162
|
-
formatter.write_paragraph()
|
|
163
|
-
formatter.write_text(description)
|
|
164
|
-
formatter.write_paragraph()
|
|
165
|
-
formatter.write_dl(opts)
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
class GroupedOption(click.Option):
|
|
169
|
-
def __init__(self, *args: Any, group: ParameterGroup, **kwargs: Any):
|
|
170
|
-
super().__init__(*args, **kwargs)
|
|
171
|
-
self.group = group
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
@schemathesis.command(short_help="Perform schemathesis test.", cls=CommandWithCustomHelp)
|
|
175
|
-
@click.argument("schema", type=str, callback=callbacks.validate_schema)
|
|
176
|
-
@click.option(
|
|
177
|
-
"--checks",
|
|
178
|
-
"-c",
|
|
179
|
-
multiple=True,
|
|
180
|
-
help="List of checks to run.",
|
|
181
|
-
type=CHECKS_TYPE,
|
|
182
|
-
default=DEFAULT_CHECKS_NAMES,
|
|
183
|
-
cls=GroupedOption,
|
|
184
|
-
group=ParameterGroup.validation,
|
|
185
|
-
show_default=True,
|
|
186
|
-
)
|
|
187
|
-
@click.option(
|
|
188
|
-
"--data-generation-method",
|
|
189
|
-
"-D",
|
|
190
|
-
"data_generation_methods",
|
|
191
|
-
help="Defines how Schemathesis generates data for tests.",
|
|
192
|
-
type=click.Choice([item.name for item in DataGenerationMethod]),
|
|
193
|
-
default=DataGenerationMethod.default(),
|
|
194
|
-
callback=callbacks.convert_data_generation_method,
|
|
195
|
-
show_default=True,
|
|
196
|
-
)
|
|
197
|
-
@click.option(
|
|
198
|
-
"--max-response-time",
|
|
199
|
-
help="A custom check that will fail if the response time is greater than the specified one in milliseconds.",
|
|
200
|
-
type=click.IntRange(min=1),
|
|
201
|
-
cls=GroupedOption,
|
|
202
|
-
group=ParameterGroup.validation,
|
|
203
|
-
)
|
|
204
|
-
@click.option(
|
|
205
|
-
"--target",
|
|
206
|
-
"-t",
|
|
207
|
-
"targets",
|
|
208
|
-
multiple=True,
|
|
209
|
-
help="Targets for input generation.",
|
|
210
|
-
type=TARGETS_TYPE,
|
|
211
|
-
default=DEFAULT_TARGETS_NAMES,
|
|
212
|
-
show_default=True,
|
|
213
|
-
)
|
|
214
|
-
@click.option(
|
|
215
|
-
"-x",
|
|
216
|
-
"--exitfirst",
|
|
217
|
-
"exit_first",
|
|
218
|
-
is_flag=True,
|
|
219
|
-
default=False,
|
|
220
|
-
help="Exit instantly on first error or failed test.",
|
|
221
|
-
show_default=True,
|
|
222
|
-
)
|
|
223
|
-
@click.option(
|
|
224
|
-
"--dry-run",
|
|
225
|
-
"dry_run",
|
|
226
|
-
is_flag=True,
|
|
227
|
-
default=False,
|
|
228
|
-
help="Disable sending data to the application and checking responses. "
|
|
229
|
-
"Helpful to verify whether data is generated at all.",
|
|
230
|
-
)
|
|
231
|
-
@click.option(
|
|
232
|
-
"--auth", "-a", help="Server user and password. Example: USER:PASSWORD", type=str, callback=callbacks.validate_auth
|
|
233
|
-
)
|
|
234
|
-
@click.option(
|
|
235
|
-
"--auth-type",
|
|
236
|
-
"-A",
|
|
237
|
-
type=click.Choice(["basic", "digest"], case_sensitive=False),
|
|
238
|
-
default="basic",
|
|
239
|
-
help="The authentication mechanism to be used. Defaults to 'basic'.",
|
|
240
|
-
show_default=True,
|
|
241
|
-
)
|
|
242
|
-
@click.option(
|
|
243
|
-
"--header",
|
|
244
|
-
"-H",
|
|
245
|
-
"headers",
|
|
246
|
-
help=r"Custom header that will be used in all requests to the server. Example: Authorization: Bearer\ 123",
|
|
247
|
-
multiple=True,
|
|
248
|
-
type=str,
|
|
249
|
-
callback=callbacks.validate_headers,
|
|
250
|
-
)
|
|
251
|
-
@click.option(
|
|
252
|
-
"--endpoint",
|
|
253
|
-
"-E",
|
|
254
|
-
"endpoints",
|
|
255
|
-
type=str,
|
|
256
|
-
multiple=True,
|
|
257
|
-
help=r"Filter schemathesis tests by API operation path pattern. Example: users/\d+",
|
|
258
|
-
callback=callbacks.validate_regex,
|
|
259
|
-
cls=GroupedOption,
|
|
260
|
-
group=ParameterGroup.filtering,
|
|
261
|
-
)
|
|
262
|
-
@click.option(
|
|
263
|
-
"--method",
|
|
264
|
-
"-M",
|
|
265
|
-
"methods",
|
|
266
|
-
type=str,
|
|
267
|
-
multiple=True,
|
|
268
|
-
help="Filter schemathesis tests by HTTP method.",
|
|
269
|
-
callback=callbacks.validate_regex,
|
|
270
|
-
cls=GroupedOption,
|
|
271
|
-
group=ParameterGroup.filtering,
|
|
272
|
-
)
|
|
273
|
-
@click.option(
|
|
274
|
-
"--tag",
|
|
275
|
-
"-T",
|
|
276
|
-
"tags",
|
|
277
|
-
type=str,
|
|
278
|
-
multiple=True,
|
|
279
|
-
help="Filter schemathesis tests by schema tag pattern.",
|
|
280
|
-
callback=callbacks.validate_regex,
|
|
281
|
-
cls=GroupedOption,
|
|
282
|
-
group=ParameterGroup.filtering,
|
|
283
|
-
)
|
|
284
|
-
@click.option(
|
|
285
|
-
"--operation-id",
|
|
286
|
-
"-O",
|
|
287
|
-
"operation_ids",
|
|
288
|
-
type=str,
|
|
289
|
-
multiple=True,
|
|
290
|
-
help="Filter schemathesis tests by operationId pattern.",
|
|
291
|
-
callback=callbacks.validate_regex,
|
|
292
|
-
cls=GroupedOption,
|
|
293
|
-
group=ParameterGroup.filtering,
|
|
294
|
-
)
|
|
295
|
-
@click.option(
|
|
296
|
-
"--workers",
|
|
297
|
-
"-w",
|
|
298
|
-
"workers_num",
|
|
299
|
-
help="Number of workers to run tests.",
|
|
300
|
-
type=CustomHelpMessageChoice(
|
|
301
|
-
["auto"] + list(map(str, range(MIN_WORKERS, MAX_WORKERS + 1))),
|
|
302
|
-
choices_repr=f"[auto|{MIN_WORKERS}-{MAX_WORKERS}]",
|
|
303
|
-
),
|
|
304
|
-
default=str(DEFAULT_WORKERS),
|
|
305
|
-
show_default=True,
|
|
306
|
-
callback=callbacks.convert_workers,
|
|
307
|
-
)
|
|
308
|
-
@click.option(
|
|
309
|
-
"--base-url",
|
|
310
|
-
"-b",
|
|
311
|
-
help="Base URL address of the API, required for SCHEMA if specified by file.",
|
|
312
|
-
type=str,
|
|
313
|
-
callback=callbacks.validate_base_url,
|
|
314
|
-
)
|
|
315
|
-
@click.option("--app", help="WSGI/ASGI application to test.", type=str, callback=callbacks.validate_app)
|
|
316
|
-
@click.option(
|
|
317
|
-
"--request-timeout",
|
|
318
|
-
help="Timeout in milliseconds for network requests during the test run.",
|
|
319
|
-
type=click.IntRange(1),
|
|
320
|
-
default=DEFAULT_RESPONSE_TIMEOUT,
|
|
321
|
-
)
|
|
322
|
-
@click.option(
|
|
323
|
-
"--request-tls-verify",
|
|
324
|
-
help="Controls whether Schemathesis verifies the server's TLS certificate. "
|
|
325
|
-
"You can also pass the path to a CA_BUNDLE file for private certs.",
|
|
326
|
-
type=str,
|
|
327
|
-
default="true",
|
|
328
|
-
show_default=True,
|
|
329
|
-
callback=callbacks.convert_request_tls_verify,
|
|
330
|
-
)
|
|
331
|
-
@click.option(
|
|
332
|
-
"--request-cert",
|
|
333
|
-
help="File path of unencrypted client certificate for authentication. "
|
|
334
|
-
"The certificate can be bundled with a private key (e.g. PEM) or the private "
|
|
335
|
-
"key can be provided with the --request-cert-key argument.",
|
|
336
|
-
type=click.Path(exists=True),
|
|
337
|
-
default=None,
|
|
338
|
-
show_default=False,
|
|
339
|
-
)
|
|
340
|
-
@click.option(
|
|
341
|
-
"--request-cert-key",
|
|
342
|
-
help="File path of the private key of the client certificate.",
|
|
343
|
-
type=click.Path(exists=True),
|
|
344
|
-
default=None,
|
|
345
|
-
show_default=False,
|
|
346
|
-
callback=callbacks.validate_request_cert_key,
|
|
347
|
-
)
|
|
348
|
-
@click.option(
|
|
349
|
-
"--validate-schema",
|
|
350
|
-
help="Enable or disable validation of input schema.",
|
|
351
|
-
type=bool,
|
|
352
|
-
default=True,
|
|
353
|
-
show_default=True,
|
|
354
|
-
cls=GroupedOption,
|
|
355
|
-
group=ParameterGroup.validation,
|
|
356
|
-
)
|
|
357
|
-
@click.option(
|
|
358
|
-
"--skip-deprecated-operations",
|
|
359
|
-
help="Skip testing of deprecated API operations.",
|
|
360
|
-
is_flag=True,
|
|
361
|
-
is_eager=True,
|
|
362
|
-
default=False,
|
|
363
|
-
show_default=True,
|
|
364
|
-
cls=GroupedOption,
|
|
365
|
-
group=ParameterGroup.filtering,
|
|
366
|
-
)
|
|
367
|
-
@click.option(
|
|
368
|
-
"--junit-xml", help="Create junit-xml style report file at given path.", type=click.File("w", encoding="utf-8")
|
|
369
|
-
)
|
|
370
|
-
@click.option(
|
|
371
|
-
"--debug-output-file",
|
|
372
|
-
help="Save debug output as JSON lines in the given file.",
|
|
373
|
-
type=click.File("w", encoding="utf-8"),
|
|
374
|
-
)
|
|
375
|
-
@click.option(
|
|
376
|
-
"--show-errors-tracebacks",
|
|
377
|
-
help="Show full tracebacks for internal errors.",
|
|
378
|
-
is_flag=True,
|
|
379
|
-
is_eager=True,
|
|
380
|
-
default=False,
|
|
381
|
-
show_default=True,
|
|
382
|
-
)
|
|
383
|
-
@click.option(
|
|
384
|
-
"--code-sample-style",
|
|
385
|
-
help="Controls the style of code samples for failure reproduction.",
|
|
386
|
-
type=click.Choice([item.name for item in CodeSampleStyle]),
|
|
387
|
-
default=CodeSampleStyle.default().name,
|
|
388
|
-
callback=callbacks.convert_code_sample_style,
|
|
389
|
-
)
|
|
390
|
-
@click.option(
|
|
391
|
-
"--store-network-log", help="Store requests and responses into a file.", type=click.File("w", encoding="utf-8")
|
|
392
|
-
)
|
|
393
|
-
@click.option(
|
|
394
|
-
"--fixups",
|
|
395
|
-
help="Install specified compatibility fixups.",
|
|
396
|
-
multiple=True,
|
|
397
|
-
type=click.Choice(list(ALL_FIXUPS) + ["all"]),
|
|
398
|
-
)
|
|
399
|
-
@click.option(
|
|
400
|
-
"--stateful",
|
|
401
|
-
help="Utilize stateful testing capabilities.",
|
|
402
|
-
type=click.Choice([item.name for item in Stateful]),
|
|
403
|
-
callback=callbacks.convert_stateful,
|
|
404
|
-
)
|
|
405
|
-
@click.option(
|
|
406
|
-
"--stateful-recursion-limit",
|
|
407
|
-
help="Limit recursion depth for stateful testing.",
|
|
408
|
-
default=DEFAULT_STATEFUL_RECURSION_LIMIT,
|
|
409
|
-
show_default=True,
|
|
410
|
-
type=click.IntRange(1, 100),
|
|
411
|
-
cls=DeprecatedOption,
|
|
412
|
-
removed_in="4.0",
|
|
413
|
-
)
|
|
414
|
-
@click.option(
|
|
415
|
-
"--force-schema-version",
|
|
416
|
-
help="Force Schemathesis to parse the input schema with the specified spec version.",
|
|
417
|
-
type=click.Choice(["20", "30"]),
|
|
418
|
-
)
|
|
419
|
-
@click.option(
|
|
420
|
-
"--hypothesis-deadline",
|
|
421
|
-
help="Duration in milliseconds that each individual example with a test is not allowed to exceed.",
|
|
422
|
-
# max value to avoid overflow. It is the maximum amount of days in milliseconds
|
|
423
|
-
type=OptionalInt(1, 999999999 * 24 * 3600 * 1000),
|
|
424
|
-
cls=GroupedOption,
|
|
425
|
-
group=ParameterGroup.hypothesis,
|
|
426
|
-
)
|
|
427
|
-
@click.option(
|
|
428
|
-
"--hypothesis-derandomize",
|
|
429
|
-
help="Use Hypothesis's deterministic mode.",
|
|
430
|
-
is_flag=True,
|
|
431
|
-
default=None,
|
|
432
|
-
show_default=True,
|
|
433
|
-
cls=GroupedOption,
|
|
434
|
-
group=ParameterGroup.hypothesis,
|
|
435
|
-
)
|
|
436
|
-
@click.option(
|
|
437
|
-
"--hypothesis-max-examples",
|
|
438
|
-
help="Maximum number of generated examples per each method/path combination.",
|
|
439
|
-
type=click.IntRange(1),
|
|
440
|
-
cls=GroupedOption,
|
|
441
|
-
group=ParameterGroup.hypothesis,
|
|
442
|
-
)
|
|
443
|
-
@click.option(
|
|
444
|
-
"--hypothesis-phases",
|
|
445
|
-
help="Control which phases should be run.",
|
|
446
|
-
type=CSVOption(hypothesis.Phase),
|
|
447
|
-
cls=GroupedOption,
|
|
448
|
-
group=ParameterGroup.hypothesis,
|
|
449
|
-
)
|
|
450
|
-
@click.option(
|
|
451
|
-
"--hypothesis-report-multiple-bugs",
|
|
452
|
-
help="Raise only the exception with the smallest minimal example.",
|
|
453
|
-
type=bool,
|
|
454
|
-
cls=GroupedOption,
|
|
455
|
-
group=ParameterGroup.hypothesis,
|
|
456
|
-
)
|
|
457
|
-
@click.option(
|
|
458
|
-
"--hypothesis-seed",
|
|
459
|
-
help="Set a seed to use for all Hypothesis tests.",
|
|
460
|
-
type=int,
|
|
461
|
-
cls=GroupedOption,
|
|
462
|
-
group=ParameterGroup.hypothesis,
|
|
463
|
-
)
|
|
464
|
-
@click.option(
|
|
465
|
-
"--hypothesis-suppress-health-check",
|
|
466
|
-
help="Comma-separated list of health checks to disable.",
|
|
467
|
-
type=CSVOption(hypothesis.HealthCheck),
|
|
468
|
-
cls=GroupedOption,
|
|
469
|
-
group=ParameterGroup.hypothesis,
|
|
470
|
-
)
|
|
471
|
-
@click.option(
|
|
472
|
-
"--hypothesis-verbosity",
|
|
473
|
-
help="Verbosity level of Hypothesis messages.",
|
|
474
|
-
type=click.Choice([item.name for item in hypothesis.Verbosity]),
|
|
475
|
-
callback=callbacks.convert_verbosity,
|
|
476
|
-
cls=GroupedOption,
|
|
477
|
-
group=ParameterGroup.hypothesis,
|
|
478
|
-
)
|
|
479
|
-
@click.option("--no-color", help="Disable ANSI color escape codes.", type=bool, is_flag=True)
|
|
480
|
-
@click.option(
|
|
481
|
-
"--schemathesis-io-token",
|
|
482
|
-
help="Schemathesis.io authentication token. If present, test run results will be uploaded to Schemathesis.io",
|
|
483
|
-
type=str,
|
|
484
|
-
)
|
|
485
|
-
@click.option(
|
|
486
|
-
"--schemathesis-io-url",
|
|
487
|
-
help="Schemathesis.io base URL.",
|
|
488
|
-
default=service.DEFAULT_URL,
|
|
489
|
-
type=str,
|
|
490
|
-
)
|
|
491
|
-
@click.option("--verbosity", "-v", help="Reduce verbosity of error output.", count=True)
|
|
492
|
-
@click.pass_context
|
|
493
|
-
def run(
|
|
494
|
-
ctx: click.Context,
|
|
495
|
-
schema: str,
|
|
496
|
-
auth: Optional[Tuple[str, str]],
|
|
497
|
-
auth_type: str,
|
|
498
|
-
headers: Dict[str, str],
|
|
499
|
-
checks: Iterable[str] = DEFAULT_CHECKS_NAMES,
|
|
500
|
-
data_generation_methods: Tuple[DataGenerationMethod, ...] = DEFAULT_DATA_GENERATION_METHODS,
|
|
501
|
-
max_response_time: Optional[int] = None,
|
|
502
|
-
targets: Iterable[str] = DEFAULT_TARGETS_NAMES,
|
|
503
|
-
exit_first: bool = False,
|
|
504
|
-
dry_run: bool = False,
|
|
505
|
-
endpoints: Optional[Filter] = None,
|
|
506
|
-
methods: Optional[Filter] = None,
|
|
507
|
-
tags: Optional[Filter] = None,
|
|
508
|
-
operation_ids: Optional[Filter] = None,
|
|
509
|
-
workers_num: int = DEFAULT_WORKERS,
|
|
510
|
-
base_url: Optional[str] = None,
|
|
511
|
-
app: Optional[str] = None,
|
|
512
|
-
request_timeout: Optional[int] = None,
|
|
513
|
-
request_tls_verify: bool = True,
|
|
514
|
-
request_cert: Optional[str] = None,
|
|
515
|
-
request_cert_key: Optional[str] = None,
|
|
516
|
-
validate_schema: bool = True,
|
|
517
|
-
skip_deprecated_operations: bool = False,
|
|
518
|
-
junit_xml: Optional[click.utils.LazyFile] = None,
|
|
519
|
-
debug_output_file: Optional[click.utils.LazyFile] = None,
|
|
520
|
-
show_errors_tracebacks: bool = False,
|
|
521
|
-
code_sample_style: CodeSampleStyle = CodeSampleStyle.default(),
|
|
522
|
-
store_network_log: Optional[click.utils.LazyFile] = None,
|
|
523
|
-
fixups: Tuple[str] = (), # type: ignore
|
|
524
|
-
stateful: Optional[Stateful] = None,
|
|
525
|
-
stateful_recursion_limit: int = DEFAULT_STATEFUL_RECURSION_LIMIT,
|
|
526
|
-
force_schema_version: Optional[str] = None,
|
|
527
|
-
hypothesis_deadline: Optional[Union[int, NotSet]] = None,
|
|
528
|
-
hypothesis_derandomize: Optional[bool] = None,
|
|
529
|
-
hypothesis_max_examples: Optional[int] = None,
|
|
530
|
-
hypothesis_phases: Optional[List[hypothesis.Phase]] = None,
|
|
531
|
-
hypothesis_report_multiple_bugs: Optional[bool] = None,
|
|
532
|
-
hypothesis_suppress_health_check: Optional[List[hypothesis.HealthCheck]] = None,
|
|
533
|
-
hypothesis_seed: Optional[int] = None,
|
|
534
|
-
hypothesis_verbosity: Optional[hypothesis.Verbosity] = None,
|
|
535
|
-
verbosity: int = 0,
|
|
536
|
-
no_color: bool = False,
|
|
537
|
-
schemathesis_io_token: Optional[str] = None,
|
|
538
|
-
schemathesis_io_url: str = service.DEFAULT_URL,
|
|
539
|
-
) -> None:
|
|
540
|
-
"""Perform schemathesis test against an API specified by SCHEMA.
|
|
541
|
-
|
|
542
|
-
SCHEMA must be a valid URL or file path pointing to an Open API / GraphQL specification.
|
|
543
|
-
"""
|
|
544
|
-
# pylint: disable=too-many-locals
|
|
545
|
-
maybe_disable_color(ctx, no_color)
|
|
546
|
-
check_auth(auth, headers)
|
|
547
|
-
selected_targets = tuple(target for target in targets_module.ALL_TARGETS if target.__name__ in targets)
|
|
548
|
-
|
|
549
|
-
if "all" in checks:
|
|
550
|
-
selected_checks = checks_module.ALL_CHECKS
|
|
551
|
-
else:
|
|
552
|
-
selected_checks = tuple(check for check in checks_module.ALL_CHECKS if check.__name__ in checks)
|
|
553
|
-
|
|
554
|
-
if fixups:
|
|
555
|
-
if "all" in fixups:
|
|
556
|
-
_fixups.install()
|
|
557
|
-
else:
|
|
558
|
-
_fixups.install(fixups)
|
|
559
|
-
hypothesis_settings = prepare_hypothesis_settings(
|
|
560
|
-
deadline=hypothesis_deadline,
|
|
561
|
-
derandomize=hypothesis_derandomize,
|
|
562
|
-
max_examples=hypothesis_max_examples,
|
|
563
|
-
phases=hypothesis_phases,
|
|
564
|
-
report_multiple_bugs=hypothesis_report_multiple_bugs,
|
|
565
|
-
suppress_health_check=hypothesis_suppress_health_check,
|
|
566
|
-
verbosity=hypothesis_verbosity,
|
|
567
|
-
)
|
|
568
|
-
event_stream = into_event_stream(
|
|
569
|
-
schema,
|
|
570
|
-
app=app,
|
|
571
|
-
base_url=base_url,
|
|
572
|
-
validate_schema=validate_schema,
|
|
573
|
-
skip_deprecated_operations=skip_deprecated_operations,
|
|
574
|
-
data_generation_methods=data_generation_methods,
|
|
575
|
-
force_schema_version=force_schema_version,
|
|
576
|
-
request_tls_verify=request_tls_verify,
|
|
577
|
-
request_cert=prepare_request_cert(request_cert, request_cert_key),
|
|
578
|
-
auth=auth,
|
|
579
|
-
auth_type=auth_type,
|
|
580
|
-
headers=headers,
|
|
581
|
-
endpoint=endpoints or None,
|
|
582
|
-
method=methods or None,
|
|
583
|
-
tag=tags or None,
|
|
584
|
-
operation_id=operation_ids or None,
|
|
585
|
-
request_timeout=request_timeout,
|
|
586
|
-
seed=hypothesis_seed,
|
|
587
|
-
exit_first=exit_first,
|
|
588
|
-
dry_run=dry_run,
|
|
589
|
-
store_interactions=store_network_log is not None,
|
|
590
|
-
checks=selected_checks,
|
|
591
|
-
max_response_time=max_response_time,
|
|
592
|
-
targets=selected_targets,
|
|
593
|
-
workers_num=workers_num,
|
|
594
|
-
stateful=stateful,
|
|
595
|
-
stateful_recursion_limit=stateful_recursion_limit,
|
|
596
|
-
hypothesis_settings=hypothesis_settings,
|
|
597
|
-
)
|
|
598
|
-
execute(
|
|
599
|
-
event_stream,
|
|
600
|
-
workers_num,
|
|
601
|
-
show_errors_tracebacks,
|
|
602
|
-
validate_schema,
|
|
603
|
-
store_network_log,
|
|
604
|
-
junit_xml,
|
|
605
|
-
verbosity,
|
|
606
|
-
code_sample_style,
|
|
607
|
-
debug_output_file,
|
|
608
|
-
schemathesis_io_token,
|
|
609
|
-
schemathesis_io_url,
|
|
610
|
-
)
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
def prepare_request_cert(cert: Optional[str], key: Optional[str]) -> Optional[RequestCert]:
|
|
614
|
-
if cert is not None and key is not None:
|
|
615
|
-
return cert, key
|
|
616
|
-
return cert
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
@attr.s(slots=True)
|
|
620
|
-
class LoaderConfig:
|
|
621
|
-
"""Container for API loader parameters.
|
|
622
|
-
|
|
623
|
-
The main goal is to avoid too many parameters in function signatures.
|
|
624
|
-
"""
|
|
625
|
-
|
|
626
|
-
schema_location: str = attr.ib() # pragma: no mutate
|
|
627
|
-
app: Any = attr.ib() # pragma: no mutate
|
|
628
|
-
base_url: Optional[str] = attr.ib() # pragma: no mutate
|
|
629
|
-
validate_schema: bool = attr.ib() # pragma: no mutate
|
|
630
|
-
skip_deprecated_operations: bool = attr.ib() # pragma: no mutate
|
|
631
|
-
data_generation_methods: Tuple[DataGenerationMethod, ...] = attr.ib() # pragma: no mutate
|
|
632
|
-
force_schema_version: Optional[str] = attr.ib() # pragma: no mutate
|
|
633
|
-
request_tls_verify: Union[bool, str] = attr.ib() # pragma: no mutate
|
|
634
|
-
request_cert: Optional[RequestCert] = attr.ib() # pragma: no mutate
|
|
635
|
-
# Network request parameters
|
|
636
|
-
auth: Optional[Tuple[str, str]] = attr.ib() # pragma: no mutate
|
|
637
|
-
auth_type: Optional[str] = attr.ib() # pragma: no mutate
|
|
638
|
-
headers: Optional[Dict[str, str]] = attr.ib() # pragma: no mutate
|
|
639
|
-
# Schema filters
|
|
640
|
-
endpoint: Optional[Filter] = attr.ib() # pragma: no mutate
|
|
641
|
-
method: Optional[Filter] = attr.ib() # pragma: no mutate
|
|
642
|
-
tag: Optional[Filter] = attr.ib() # pragma: no mutate
|
|
643
|
-
operation_id: Optional[Filter] = attr.ib() # pragma: no mutate
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
def into_event_stream(
|
|
647
|
-
schema_location: str,
|
|
648
|
-
*,
|
|
649
|
-
app: Any,
|
|
650
|
-
base_url: Optional[str],
|
|
651
|
-
validate_schema: bool,
|
|
652
|
-
skip_deprecated_operations: bool,
|
|
653
|
-
data_generation_methods: Tuple[DataGenerationMethod, ...],
|
|
654
|
-
force_schema_version: Optional[str],
|
|
655
|
-
request_tls_verify: Union[bool, str],
|
|
656
|
-
request_cert: Optional[RequestCert],
|
|
657
|
-
# Network request parameters
|
|
658
|
-
auth: Optional[Tuple[str, str]],
|
|
659
|
-
auth_type: Optional[str],
|
|
660
|
-
headers: Optional[Dict[str, str]],
|
|
661
|
-
request_timeout: Optional[int],
|
|
662
|
-
# Schema filters
|
|
663
|
-
endpoint: Optional[Filter],
|
|
664
|
-
method: Optional[Filter],
|
|
665
|
-
tag: Optional[Filter],
|
|
666
|
-
operation_id: Optional[Filter],
|
|
667
|
-
# Runtime behavior
|
|
668
|
-
checks: Iterable[CheckFunction],
|
|
669
|
-
max_response_time: Optional[int],
|
|
670
|
-
targets: Iterable[Target],
|
|
671
|
-
workers_num: int,
|
|
672
|
-
hypothesis_settings: Optional[hypothesis.settings],
|
|
673
|
-
seed: Optional[int],
|
|
674
|
-
exit_first: bool,
|
|
675
|
-
dry_run: bool,
|
|
676
|
-
store_interactions: bool,
|
|
677
|
-
stateful: Optional[Stateful],
|
|
678
|
-
stateful_recursion_limit: int,
|
|
679
|
-
) -> Generator[events.ExecutionEvent, None, None]:
|
|
680
|
-
try:
|
|
681
|
-
if app is not None:
|
|
682
|
-
app = import_app(app)
|
|
683
|
-
config = LoaderConfig(
|
|
684
|
-
schema_location=schema_location,
|
|
685
|
-
app=app,
|
|
686
|
-
base_url=base_url,
|
|
687
|
-
validate_schema=validate_schema,
|
|
688
|
-
skip_deprecated_operations=skip_deprecated_operations,
|
|
689
|
-
data_generation_methods=data_generation_methods,
|
|
690
|
-
force_schema_version=force_schema_version,
|
|
691
|
-
request_tls_verify=request_tls_verify,
|
|
692
|
-
request_cert=request_cert,
|
|
693
|
-
auth=auth,
|
|
694
|
-
auth_type=auth_type,
|
|
695
|
-
headers=headers,
|
|
696
|
-
endpoint=endpoint or None,
|
|
697
|
-
method=method or None,
|
|
698
|
-
tag=tag or None,
|
|
699
|
-
operation_id=operation_id or None,
|
|
700
|
-
)
|
|
701
|
-
loaded_schema = load_schema(config)
|
|
702
|
-
yield from runner.from_schema(
|
|
703
|
-
loaded_schema,
|
|
704
|
-
auth=auth,
|
|
705
|
-
auth_type=auth_type,
|
|
706
|
-
headers=headers,
|
|
707
|
-
request_timeout=request_timeout,
|
|
708
|
-
request_tls_verify=request_tls_verify,
|
|
709
|
-
request_cert=request_cert,
|
|
710
|
-
seed=seed,
|
|
711
|
-
exit_first=exit_first,
|
|
712
|
-
dry_run=dry_run,
|
|
713
|
-
store_interactions=store_interactions,
|
|
714
|
-
checks=checks,
|
|
715
|
-
max_response_time=max_response_time,
|
|
716
|
-
targets=targets,
|
|
717
|
-
workers_num=workers_num,
|
|
718
|
-
stateful=stateful,
|
|
719
|
-
stateful_recursion_limit=stateful_recursion_limit,
|
|
720
|
-
hypothesis_settings=hypothesis_settings,
|
|
721
|
-
).execute()
|
|
722
|
-
except Exception as exc:
|
|
723
|
-
yield events.InternalError.from_exc(exc)
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
def load_schema(config: LoaderConfig) -> BaseSchema:
|
|
727
|
-
"""Automatically load API schema."""
|
|
728
|
-
first: Callable[[LoaderConfig], BaseSchema]
|
|
729
|
-
second: Callable[[LoaderConfig], BaseSchema]
|
|
730
|
-
if is_probably_graphql(config.schema_location):
|
|
731
|
-
# Try GraphQL first, then fallback to Open API
|
|
732
|
-
first, second = (_load_graphql_schema, _load_openapi_schema)
|
|
733
|
-
else:
|
|
734
|
-
# Try Open API first, then fallback to GraphQL
|
|
735
|
-
first, second = (_load_openapi_schema, _load_graphql_schema)
|
|
736
|
-
return _try_load_schema(config, first, second)
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
def _try_load_schema(
|
|
740
|
-
config: LoaderConfig, first: Callable[[LoaderConfig], BaseSchema], second: Callable[[LoaderConfig], BaseSchema]
|
|
741
|
-
) -> BaseSchema:
|
|
742
|
-
try:
|
|
743
|
-
return first(config)
|
|
744
|
-
except HTTPError as exc:
|
|
745
|
-
try:
|
|
746
|
-
return second(config)
|
|
747
|
-
except HTTPError:
|
|
748
|
-
# Raise the first loader's error
|
|
749
|
-
raise exc # pylint: disable=raise-missing-from
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
def _load_graphql_schema(config: LoaderConfig) -> GraphQLSchema:
|
|
753
|
-
loader = detect_loader(config.schema_location, config.app, is_openapi=False)
|
|
754
|
-
kwargs = get_graphql_loader_kwargs(loader, config)
|
|
755
|
-
return loader(config.schema_location, **kwargs)
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
def _load_openapi_schema(config: LoaderConfig) -> BaseSchema:
|
|
759
|
-
loader = detect_loader(config.schema_location, config.app, is_openapi=True)
|
|
760
|
-
kwargs = get_loader_kwargs(loader, config)
|
|
761
|
-
return loader(config.schema_location, **kwargs)
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
def detect_loader(schema_location: str, app: Any, is_openapi: bool) -> Callable:
|
|
765
|
-
"""Detect API schema loader."""
|
|
766
|
-
if file_exists(schema_location):
|
|
767
|
-
# If there is an existing file with the given name,
|
|
768
|
-
# then it is likely that the user wants to load API schema from there
|
|
769
|
-
return oas_loaders.from_path if is_openapi else gql_loaders.from_path # type: ignore
|
|
770
|
-
if app is not None and not urlparse(schema_location).netloc:
|
|
771
|
-
# App is passed & location is relative
|
|
772
|
-
return oas_loaders.get_loader_for_app(app) if is_openapi else gql_loaders.get_loader_for_app(app)
|
|
773
|
-
# Default behavior
|
|
774
|
-
return oas_loaders.from_uri if is_openapi else gql_loaders.from_url # type: ignore
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
def get_loader_kwargs(loader: Callable, config: LoaderConfig) -> Dict[str, Any]:
|
|
778
|
-
"""Detect the proper set of parameters for a loader."""
|
|
779
|
-
# These kwargs are shared by all loaders
|
|
780
|
-
kwargs = {
|
|
781
|
-
"app": config.app,
|
|
782
|
-
"base_url": config.base_url,
|
|
783
|
-
"method": config.method,
|
|
784
|
-
"endpoint": config.endpoint,
|
|
785
|
-
"tag": config.tag,
|
|
786
|
-
"operation_id": config.operation_id,
|
|
787
|
-
"skip_deprecated_operations": config.skip_deprecated_operations,
|
|
788
|
-
"validate_schema": config.validate_schema,
|
|
789
|
-
"force_schema_version": config.force_schema_version,
|
|
790
|
-
"data_generation_methods": config.data_generation_methods,
|
|
791
|
-
}
|
|
792
|
-
if loader is not oas_loaders.from_path:
|
|
793
|
-
kwargs["headers"] = config.headers
|
|
794
|
-
if loader in (oas_loaders.from_uri, oas_loaders.from_aiohttp):
|
|
795
|
-
_add_requests_kwargs(kwargs, config)
|
|
796
|
-
return kwargs
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
def get_graphql_loader_kwargs(
|
|
800
|
-
loader: Callable,
|
|
801
|
-
config: LoaderConfig,
|
|
802
|
-
) -> Dict[str, Any]:
|
|
803
|
-
"""Detect the proper set of parameters for a loader."""
|
|
804
|
-
# These kwargs are shared by all loaders
|
|
805
|
-
kwargs = {
|
|
806
|
-
"app": config.app,
|
|
807
|
-
"base_url": config.base_url,
|
|
808
|
-
"data_generation_methods": config.data_generation_methods,
|
|
809
|
-
}
|
|
810
|
-
if loader is not gql_loaders.from_path:
|
|
811
|
-
kwargs["headers"] = config.headers
|
|
812
|
-
if loader is gql_loaders.from_url:
|
|
813
|
-
_add_requests_kwargs(kwargs, config)
|
|
814
|
-
return kwargs
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
def _add_requests_kwargs(kwargs: Dict[str, Any], config: LoaderConfig) -> None:
|
|
818
|
-
kwargs["verify"] = config.request_tls_verify
|
|
819
|
-
if config.request_cert is not None:
|
|
820
|
-
kwargs["cert"] = config.request_cert
|
|
821
|
-
if config.auth is not None:
|
|
822
|
-
kwargs["auth"] = get_requests_auth(config.auth, config.auth_type)
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
def is_probably_graphql(location: str) -> bool:
|
|
826
|
-
"""Detect whether it is likely that the given location is a GraphQL endpoint."""
|
|
827
|
-
return location.endswith(("/graphql", "/graphql/"))
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
def check_auth(auth: Optional[Tuple[str, str]], headers: Dict[str, str]) -> None:
|
|
831
|
-
if auth is not None and "authorization" in {header.lower() for header in headers}:
|
|
832
|
-
raise click.BadParameter("Passing `--auth` together with `--header` that sets `Authorization` is not allowed.")
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
def get_output_handler(workers_num: int) -> EventHandler:
|
|
836
|
-
if workers_num > 1:
|
|
837
|
-
output_style = OutputStyle.short
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from schemathesis.cli.commands import Group, run, schemathesis
|
|
4
|
+
from schemathesis.cli.commands.run.context import ExecutionContext
|
|
5
|
+
from schemathesis.cli.commands.run.events import LoadingFinished, LoadingStarted
|
|
6
|
+
from schemathesis.cli.commands.run.executor import handler
|
|
7
|
+
from schemathesis.cli.commands.run.handlers import EventHandler
|
|
8
|
+
from schemathesis.cli.ext.groups import GROUPS, OptionGroup
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"schemathesis",
|
|
12
|
+
"run",
|
|
13
|
+
"EventHandler",
|
|
14
|
+
"ExecutionContext",
|
|
15
|
+
"LoadingStarted",
|
|
16
|
+
"LoadingFinished",
|
|
17
|
+
"add_group",
|
|
18
|
+
"handler",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def add_group(name: str, *, index: int | None = None) -> Group:
|
|
23
|
+
"""Add a custom options group to `st run`."""
|
|
24
|
+
if index is not None:
|
|
25
|
+
GROUPS[name] = OptionGroup(name=name, order=index)
|
|
838
26
|
else:
|
|
839
|
-
|
|
840
|
-
return
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
def load_hook(module_name: str) -> None:
|
|
844
|
-
"""Load the given hook by importing it."""
|
|
845
|
-
try:
|
|
846
|
-
sys.path.append(os.getcwd()) # fix ModuleNotFoundError module in cwd
|
|
847
|
-
__import__(module_name)
|
|
848
|
-
except Exception as exc:
|
|
849
|
-
click.secho("An exception happened during the hook loading:\n", fg="red")
|
|
850
|
-
message = traceback.format_exc()
|
|
851
|
-
click.secho(message, fg="red")
|
|
852
|
-
raise click.Abort() from exc
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
class OutputStyle(Enum):
|
|
856
|
-
"""Provide different output styles."""
|
|
857
|
-
|
|
858
|
-
default = output.default.DefaultOutputStyleHandler
|
|
859
|
-
short = output.short.ShortOutputStyleHandler
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
def execute(
|
|
863
|
-
event_stream: Generator[events.ExecutionEvent, None, None],
|
|
864
|
-
workers_num: int,
|
|
865
|
-
show_errors_tracebacks: bool,
|
|
866
|
-
validate_schema: bool,
|
|
867
|
-
store_network_log: Optional[click.utils.LazyFile],
|
|
868
|
-
junit_xml: Optional[click.utils.LazyFile],
|
|
869
|
-
verbosity: int,
|
|
870
|
-
code_sample_style: CodeSampleStyle,
|
|
871
|
-
debug_output_file: Optional[click.utils.LazyFile],
|
|
872
|
-
schemathesis_io_token: Optional[str],
|
|
873
|
-
schemathesis_io_url: str,
|
|
874
|
-
) -> None:
|
|
875
|
-
"""Execute a prepared runner by drawing events from it and passing to a proper handler."""
|
|
876
|
-
handlers: List[EventHandler] = []
|
|
877
|
-
if junit_xml is not None:
|
|
878
|
-
handlers.append(JunitXMLHandler(junit_xml))
|
|
879
|
-
if debug_output_file is not None:
|
|
880
|
-
handlers.append(DebugOutputHandler(debug_output_file))
|
|
881
|
-
service_context = None
|
|
882
|
-
if schemathesis_io_token is not None:
|
|
883
|
-
service_queue: Queue = Queue()
|
|
884
|
-
service_context = ServiceContext(url=schemathesis_io_url, queue=service_queue)
|
|
885
|
-
handlers.append(service.ServiceReporter(service_queue, schemathesis_io_token, schemathesis_io_url))
|
|
886
|
-
if store_network_log is not None:
|
|
887
|
-
# This handler should be first to have logs writing completed when the output handler will display statistic
|
|
888
|
-
handlers.append(cassettes.CassetteWriter(store_network_log))
|
|
889
|
-
handlers.append(get_output_handler(workers_num))
|
|
890
|
-
execution_context = ExecutionContext(
|
|
891
|
-
workers_num=workers_num,
|
|
892
|
-
show_errors_tracebacks=show_errors_tracebacks,
|
|
893
|
-
validate_schema=validate_schema,
|
|
894
|
-
cassette_file_name=store_network_log.name if store_network_log is not None else None,
|
|
895
|
-
junit_xml_file=junit_xml.name if junit_xml is not None else None,
|
|
896
|
-
verbosity=verbosity,
|
|
897
|
-
code_sample_style=code_sample_style,
|
|
898
|
-
service=service_context,
|
|
899
|
-
)
|
|
900
|
-
|
|
901
|
-
def shutdown() -> None:
|
|
902
|
-
for _handler in handlers:
|
|
903
|
-
_handler.shutdown()
|
|
904
|
-
|
|
905
|
-
GLOBAL_HOOK_DISPATCHER.dispatch("after_init_cli_run_handlers", HookContext(), handlers, execution_context)
|
|
906
|
-
event = None
|
|
907
|
-
try:
|
|
908
|
-
for event in event_stream:
|
|
909
|
-
for handler in handlers:
|
|
910
|
-
handler.handle_event(execution_context, event)
|
|
911
|
-
except Exception as exc:
|
|
912
|
-
if isinstance(exc, click.Abort):
|
|
913
|
-
# To avoid showing "Aborted!" message, which is the default behavior in Click
|
|
914
|
-
sys.exit(1)
|
|
915
|
-
raise
|
|
916
|
-
finally:
|
|
917
|
-
shutdown()
|
|
918
|
-
if event is not None and event.is_terminal:
|
|
919
|
-
exit_code = get_exit_code(event)
|
|
920
|
-
sys.exit(exit_code)
|
|
921
|
-
# Event stream did not finish with a terminal event. Only possible if the handler is broken
|
|
922
|
-
click.secho("Unexpected error", fg="red")
|
|
923
|
-
sys.exit(1)
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
def get_exit_code(event: events.ExecutionEvent) -> int:
|
|
927
|
-
if isinstance(event, events.Finished):
|
|
928
|
-
if event.has_failures or event.has_errors:
|
|
929
|
-
return 1
|
|
930
|
-
return 0
|
|
931
|
-
# Practically not possible. May occur only if the output handler is broken - in this case we still will have the
|
|
932
|
-
# right exit code.
|
|
933
|
-
return 1
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
@schemathesis.command(short_help="Replay requests from a saved cassette.")
|
|
937
|
-
@click.argument("cassette_path", type=click.Path(exists=True))
|
|
938
|
-
@click.option("--id", "id_", help="ID of interaction to replay.", type=str)
|
|
939
|
-
@click.option("--status", help="Status of interactions to replay.", type=str)
|
|
940
|
-
@click.option("--uri", help="A regexp that filters interactions by their request URI.", type=str)
|
|
941
|
-
@click.option("--method", help="A regexp that filters interactions by their request method.", type=str)
|
|
942
|
-
@click.option("--no-color", help="Disable ANSI color escape codes.", type=bool, is_flag=True)
|
|
943
|
-
@click.pass_context
|
|
944
|
-
def replay(
|
|
945
|
-
ctx: click.Context,
|
|
946
|
-
cassette_path: str,
|
|
947
|
-
id_: Optional[str],
|
|
948
|
-
status: Optional[str] = None,
|
|
949
|
-
uri: Optional[str] = None,
|
|
950
|
-
method: Optional[str] = None,
|
|
951
|
-
no_color: bool = False,
|
|
952
|
-
) -> None:
|
|
953
|
-
"""Replay a cassette.
|
|
954
|
-
|
|
955
|
-
Cassettes in VCR-compatible format can be replayed.
|
|
956
|
-
For example, ones that are recorded with ``store-network-log`` option of `schemathesis run` command.
|
|
957
|
-
"""
|
|
958
|
-
maybe_disable_color(ctx, no_color)
|
|
959
|
-
click.secho(f"{bold('Replaying cassette')}: {cassette_path}")
|
|
960
|
-
with open(cassette_path) as fd:
|
|
961
|
-
cassette = yaml.load(fd, Loader=SafeLoader)
|
|
962
|
-
click.secho(f"{bold('Total interactions')}: {len(cassette['http_interactions'])}\n")
|
|
963
|
-
for replayed in cassettes.replay(cassette, id_=id_, status=status, uri=uri, method=method):
|
|
964
|
-
click.secho(f" {bold('ID')} : {replayed.interaction['id']}")
|
|
965
|
-
click.secho(f" {bold('URI')} : {replayed.interaction['request']['uri']}")
|
|
966
|
-
click.secho(f" {bold('Old status code')} : {replayed.interaction['response']['status']['code']}")
|
|
967
|
-
click.secho(f" {bold('New status code')} : {replayed.response.status_code}\n")
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
def bold(message: str) -> str:
|
|
971
|
-
return click.style(message, bold=True)
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
def maybe_disable_color(ctx: click.Context, no_color: bool) -> None:
|
|
975
|
-
if no_color or "NO_COLOR" in os.environ:
|
|
976
|
-
ctx.color = False
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
@HookDispatcher.register_spec([HookScope.GLOBAL])
|
|
980
|
-
def after_init_cli_run_handlers(
|
|
981
|
-
context: HookContext, handlers: List[EventHandler], execution_context: ExecutionContext
|
|
982
|
-
) -> None:
|
|
983
|
-
"""Called after CLI hooks are initialized.
|
|
984
|
-
|
|
985
|
-
Might be used to add extra event handlers.
|
|
986
|
-
"""
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
@HookDispatcher.register_spec([HookScope.GLOBAL])
|
|
990
|
-
def before_call(context: HookContext, case: Case) -> None:
|
|
991
|
-
"""Called before every network call in CLI tests.
|
|
992
|
-
|
|
993
|
-
Use cases:
|
|
994
|
-
- Modification of `case`. For example, adding some pre-determined value to its query string.
|
|
995
|
-
- Logging
|
|
996
|
-
"""
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
@HookDispatcher.register_spec([HookScope.GLOBAL])
|
|
1000
|
-
def after_call(context: HookContext, case: Case, response: GenericResponse) -> None:
|
|
1001
|
-
"""Called after every network call in CLI tests.
|
|
1002
|
-
|
|
1003
|
-
Note that you need to modify the response in-place.
|
|
1004
|
-
|
|
1005
|
-
Use cases:
|
|
1006
|
-
- Response post-processing, like modifying its payload.
|
|
1007
|
-
- Logging
|
|
1008
|
-
"""
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
@HookDispatcher.register_spec([HookScope.GLOBAL])
|
|
1012
|
-
def process_call_kwargs(context: HookContext, case: Case, kwargs: Dict[str, Any]) -> None:
|
|
1013
|
-
"""Called before every network call in CLI tests.
|
|
1014
|
-
|
|
1015
|
-
Aims to modify the argument passed to `case.call` / `case.call_wsgi` / `case.call_asgi`.
|
|
1016
|
-
Note that you need to modify `kwargs` in-place.
|
|
1017
|
-
"""
|
|
27
|
+
GROUPS[name] = OptionGroup(name=name)
|
|
28
|
+
return Group(name)
|