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
|
@@ -1 +0,0 @@
|
|
|
1
|
-
from .loaders import from_asgi, from_dict, from_file, from_path, from_url, from_wsgi
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
if TYPE_CHECKING:
|
|
6
|
+
from graphql import ValueNode
|
|
7
|
+
|
|
8
|
+
# Re-export `hypothesis_graphql` helpers
|
|
9
|
+
|
|
10
|
+
__all__ = [ # noqa: F822
|
|
11
|
+
"Boolean",
|
|
12
|
+
"Enum",
|
|
13
|
+
"Float",
|
|
14
|
+
"Int",
|
|
15
|
+
"List",
|
|
16
|
+
"Null",
|
|
17
|
+
"Object",
|
|
18
|
+
"String",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def __getattr__(name: str) -> ValueNode | None:
|
|
23
|
+
if name in __all__:
|
|
24
|
+
import hypothesis_graphql.nodes
|
|
25
|
+
|
|
26
|
+
return getattr(hypothesis_graphql.nodes, name)
|
|
27
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from functools import lru_cache
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
from schemathesis.core.errors import IncorrectUsage
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
import graphql
|
|
10
|
+
from hypothesis import strategies as st
|
|
11
|
+
|
|
12
|
+
CUSTOM_SCALARS: dict[str, st.SearchStrategy[graphql.ValueNode]] = {}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def scalar(name: str, strategy: st.SearchStrategy[graphql.ValueNode]) -> None:
|
|
16
|
+
r"""Register a custom Hypothesis strategy for generating GraphQL scalar values.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
name: Scalar name that matches your GraphQL schema scalar definition
|
|
20
|
+
strategy: Hypothesis strategy that generates GraphQL AST ValueNode objects
|
|
21
|
+
|
|
22
|
+
Example:
|
|
23
|
+
```python
|
|
24
|
+
import schemathesis
|
|
25
|
+
from hypothesis import strategies as st
|
|
26
|
+
from schemathesis.graphql import nodes
|
|
27
|
+
|
|
28
|
+
# Register email scalar
|
|
29
|
+
schemathesis.graphql.scalar("Email", st.emails().map(nodes.String))
|
|
30
|
+
|
|
31
|
+
# Register positive integer scalar
|
|
32
|
+
schemathesis.graphql.scalar(
|
|
33
|
+
"PositiveInt",
|
|
34
|
+
st.integers(min_value=1).map(nodes.Int)
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
# Register phone number scalar
|
|
38
|
+
schemathesis.graphql.scalar(
|
|
39
|
+
"Phone",
|
|
40
|
+
st.from_regex(r"\+1-\d{3}-\d{3}-\d{4}").map(nodes.String)
|
|
41
|
+
)
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Schema usage:
|
|
45
|
+
```graphql
|
|
46
|
+
scalar Email
|
|
47
|
+
scalar PositiveInt
|
|
48
|
+
|
|
49
|
+
type Query {
|
|
50
|
+
getUser(email: Email!, rating: PositiveInt!): User
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
"""
|
|
55
|
+
from hypothesis.strategies import SearchStrategy
|
|
56
|
+
|
|
57
|
+
if not isinstance(name, str):
|
|
58
|
+
raise IncorrectUsage(f"Scalar name {name!r} must be a string")
|
|
59
|
+
if not isinstance(strategy, SearchStrategy):
|
|
60
|
+
raise IncorrectUsage(
|
|
61
|
+
f"{strategy!r} must be a Hypothesis strategy which generates AST nodes matching this scalar"
|
|
62
|
+
)
|
|
63
|
+
CUSTOM_SCALARS[name] = strategy
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@lru_cache
|
|
67
|
+
def get_extra_scalar_strategies() -> dict[str, st.SearchStrategy]:
|
|
68
|
+
"""Get all extra GraphQL strategies."""
|
|
69
|
+
from hypothesis import strategies as st
|
|
70
|
+
|
|
71
|
+
from . import nodes
|
|
72
|
+
|
|
73
|
+
dates = st.dates().map(str)
|
|
74
|
+
times = st.times().map("%sZ".__mod__)
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
"Date": dates.map(nodes.String),
|
|
78
|
+
"Time": times.map(nodes.String),
|
|
79
|
+
"DateTime": st.tuples(dates, times).map("T".join).map(nodes.String),
|
|
80
|
+
"IP": st.ip_addresses().map(str).map(nodes.String),
|
|
81
|
+
"IPv4": st.ip_addresses(v=4).map(str).map(nodes.String),
|
|
82
|
+
"IPv6": st.ip_addresses(v=6).map(str).map(nodes.String),
|
|
83
|
+
"BigInt": st.integers().map(nodes.Int),
|
|
84
|
+
"Long": st.integers(min_value=-(2**63), max_value=2**63 - 1).map(nodes.Int),
|
|
85
|
+
"UUID": st.uuids().map(str).map(nodes.String),
|
|
86
|
+
}
|
|
@@ -1,166 +1,438 @@
|
|
|
1
|
-
from
|
|
2
|
-
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import enum
|
|
4
|
+
import time
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from difflib import get_close_matches
|
|
7
|
+
from enum import unique
|
|
8
|
+
from types import SimpleNamespace
|
|
9
|
+
from typing import (
|
|
10
|
+
TYPE_CHECKING,
|
|
11
|
+
Any,
|
|
12
|
+
Callable,
|
|
13
|
+
Generator,
|
|
14
|
+
Iterator,
|
|
15
|
+
Mapping,
|
|
16
|
+
NoReturn,
|
|
17
|
+
Union,
|
|
18
|
+
cast,
|
|
19
|
+
)
|
|
3
20
|
from urllib.parse import urlsplit
|
|
4
21
|
|
|
5
|
-
import attr
|
|
6
|
-
import graphql
|
|
7
|
-
import requests
|
|
8
22
|
from hypothesis import strategies as st
|
|
9
|
-
from hypothesis.strategies import SearchStrategy
|
|
10
|
-
from hypothesis_graphql import strategies as gql_st
|
|
11
23
|
from requests.structures import CaseInsensitiveDict
|
|
12
24
|
|
|
13
|
-
from
|
|
14
|
-
from
|
|
15
|
-
from
|
|
16
|
-
from
|
|
17
|
-
from
|
|
18
|
-
from
|
|
19
|
-
from
|
|
20
|
-
from
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
"path": self.operation.schema.get_full_path(self.formatted_path),
|
|
47
|
-
# Convert to a regular dictionary, as we use `CaseInsensitiveDict` which is not supported by Werkzeug
|
|
48
|
-
"headers": dict(final_headers),
|
|
49
|
-
"query_string": self.query,
|
|
50
|
-
"json": {"query": self.body},
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
def validate_response(
|
|
54
|
-
self,
|
|
55
|
-
response: GenericResponse,
|
|
56
|
-
checks: Tuple[CheckFunction, ...] = (),
|
|
57
|
-
additional_checks: Tuple[CheckFunction, ...] = (),
|
|
58
|
-
code_sample_style: Optional[str] = None,
|
|
59
|
-
) -> None:
|
|
60
|
-
checks = checks or (not_a_server_error,)
|
|
61
|
-
checks += additional_checks
|
|
62
|
-
return super().validate_response(response, checks, code_sample_style=code_sample_style)
|
|
63
|
-
|
|
64
|
-
def call_asgi(
|
|
65
|
-
self,
|
|
66
|
-
app: Any = None,
|
|
67
|
-
base_url: Optional[str] = None,
|
|
68
|
-
headers: Optional[Dict[str, str]] = None,
|
|
69
|
-
**kwargs: Any,
|
|
70
|
-
) -> requests.Response:
|
|
71
|
-
return super().call_asgi(app=app, base_url=base_url, headers=headers, **kwargs)
|
|
25
|
+
from schemathesis import auths
|
|
26
|
+
from schemathesis.core import NOT_SET, NotSet, Specification
|
|
27
|
+
from schemathesis.core.errors import InvalidSchema, OperationNotFound
|
|
28
|
+
from schemathesis.core.parameters import ParameterLocation
|
|
29
|
+
from schemathesis.core.result import Ok, Result
|
|
30
|
+
from schemathesis.generation import GenerationMode
|
|
31
|
+
from schemathesis.generation.case import Case
|
|
32
|
+
from schemathesis.generation.meta import (
|
|
33
|
+
CaseMetadata,
|
|
34
|
+
ComponentInfo,
|
|
35
|
+
ExamplesPhaseData,
|
|
36
|
+
FuzzingPhaseData,
|
|
37
|
+
GenerationInfo,
|
|
38
|
+
PhaseInfo,
|
|
39
|
+
TestPhase,
|
|
40
|
+
)
|
|
41
|
+
from schemathesis.hooks import HookContext, HookDispatcher, apply_to_all_dispatchers
|
|
42
|
+
from schemathesis.schemas import (
|
|
43
|
+
APIOperation,
|
|
44
|
+
APIOperationMap,
|
|
45
|
+
ApiStatistic,
|
|
46
|
+
BaseSchema,
|
|
47
|
+
OperationDefinition,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
from .scalars import CUSTOM_SCALARS, get_extra_scalar_strategies
|
|
51
|
+
|
|
52
|
+
if TYPE_CHECKING:
|
|
53
|
+
import graphql
|
|
54
|
+
from hypothesis.strategies import SearchStrategy
|
|
55
|
+
|
|
56
|
+
from schemathesis.auths import AuthStorage
|
|
57
|
+
|
|
72
58
|
|
|
59
|
+
@unique
|
|
60
|
+
class RootType(enum.Enum):
|
|
61
|
+
QUERY = enum.auto()
|
|
62
|
+
MUTATION = enum.auto()
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass(repr=False)
|
|
66
|
+
class GraphQLOperationDefinition(OperationDefinition):
|
|
67
|
+
field_name: str
|
|
68
|
+
type_: graphql.GraphQLType
|
|
69
|
+
root_type: RootType
|
|
70
|
+
|
|
71
|
+
__slots__ = ("raw", "field_name", "type_", "root_type")
|
|
72
|
+
|
|
73
|
+
def _repr_pretty_(self, *args: Any, **kwargs: Any) -> None: ...
|
|
74
|
+
|
|
75
|
+
@property
|
|
76
|
+
def is_query(self) -> bool:
|
|
77
|
+
return self.root_type == RootType.QUERY
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
def is_mutation(self) -> bool:
|
|
81
|
+
return self.root_type == RootType.MUTATION
|
|
73
82
|
|
|
74
|
-
C = TypeVar("C", bound=Case)
|
|
75
83
|
|
|
84
|
+
class GraphQLResponses:
|
|
85
|
+
def find_by_status_code(self, status_code: int) -> None:
|
|
86
|
+
return None # pragma: no cover
|
|
76
87
|
|
|
77
|
-
|
|
88
|
+
def add(self, status_code: str, definition: dict[str, Any]) -> None:
|
|
89
|
+
return None # pragma: no cover
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@dataclass
|
|
78
93
|
class GraphQLSchema(BaseSchema):
|
|
94
|
+
def __repr__(self) -> str:
|
|
95
|
+
return f"<{self.__class__.__name__}>"
|
|
96
|
+
|
|
97
|
+
def __iter__(self) -> Iterator[str]:
|
|
98
|
+
schema = self.client_schema
|
|
99
|
+
for operation_type in (
|
|
100
|
+
schema.query_type,
|
|
101
|
+
schema.mutation_type,
|
|
102
|
+
):
|
|
103
|
+
if operation_type is not None:
|
|
104
|
+
yield operation_type.name
|
|
105
|
+
|
|
106
|
+
def _get_operation_map(self, key: str) -> APIOperationMap:
|
|
107
|
+
schema = self.client_schema
|
|
108
|
+
for root_type, operation_type in (
|
|
109
|
+
(RootType.QUERY, schema.query_type),
|
|
110
|
+
(RootType.MUTATION, schema.mutation_type),
|
|
111
|
+
):
|
|
112
|
+
if operation_type and operation_type.name == key:
|
|
113
|
+
map = APIOperationMap(self, {})
|
|
114
|
+
map._data = FieldMap(map, root_type, operation_type)
|
|
115
|
+
return map
|
|
116
|
+
raise KeyError(key)
|
|
117
|
+
|
|
118
|
+
def find_operation_by_label(self, label: str) -> APIOperation | None:
|
|
119
|
+
if label.startswith(("Query.", "Mutation.")):
|
|
120
|
+
ty, field = label.split(".", maxsplit=1)
|
|
121
|
+
try:
|
|
122
|
+
return self[ty][field]
|
|
123
|
+
except KeyError:
|
|
124
|
+
return None
|
|
125
|
+
return None
|
|
126
|
+
|
|
127
|
+
def on_missing_operation(self, item: str, exc: KeyError) -> NoReturn:
|
|
128
|
+
raw_schema = self.raw_schema["__schema"]
|
|
129
|
+
type_names = [type_def["name"] for type_def in raw_schema.get("types", [])]
|
|
130
|
+
matches = get_close_matches(item, type_names)
|
|
131
|
+
message = f"`{item}` type not found"
|
|
132
|
+
if matches:
|
|
133
|
+
message += f". Did you mean `{matches[0]}`?"
|
|
134
|
+
raise OperationNotFound(message=message, item=item) from exc
|
|
135
|
+
|
|
79
136
|
def get_full_path(self, path: str) -> str:
|
|
80
137
|
return self.base_path
|
|
81
138
|
|
|
82
|
-
@property
|
|
83
|
-
def
|
|
84
|
-
return "
|
|
139
|
+
@property
|
|
140
|
+
def specification(self) -> Specification:
|
|
141
|
+
return Specification.graphql(version="")
|
|
85
142
|
|
|
86
143
|
@property
|
|
87
144
|
def client_schema(self) -> graphql.GraphQLSchema:
|
|
88
|
-
|
|
145
|
+
import graphql
|
|
146
|
+
|
|
147
|
+
if not hasattr(self, "_client_schema"):
|
|
148
|
+
self._client_schema = graphql.build_client_schema(self.raw_schema)
|
|
149
|
+
return self._client_schema
|
|
89
150
|
|
|
90
151
|
@property
|
|
91
152
|
def base_path(self) -> str:
|
|
92
|
-
if self.base_url:
|
|
93
|
-
return urlsplit(self.base_url).path
|
|
153
|
+
if self.config.base_url:
|
|
154
|
+
return urlsplit(self.config.base_url).path
|
|
94
155
|
return self._get_base_path()
|
|
95
156
|
|
|
96
157
|
def _get_base_path(self) -> str:
|
|
97
158
|
return cast(str, urlsplit(self.location).path)
|
|
98
159
|
|
|
99
|
-
|
|
100
|
-
|
|
160
|
+
def _measure_statistic(self) -> ApiStatistic:
|
|
161
|
+
statistic = ApiStatistic()
|
|
101
162
|
raw_schema = self.raw_schema["__schema"]
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
163
|
+
dummy_operation = APIOperation(
|
|
164
|
+
base_url=self.get_base_url(),
|
|
165
|
+
path=self.base_path,
|
|
166
|
+
label="",
|
|
167
|
+
method="POST",
|
|
168
|
+
schema=self,
|
|
169
|
+
responses=GraphQLResponses(),
|
|
170
|
+
security=None,
|
|
171
|
+
definition=None, # type: ignore[arg-type, var-annotated]
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
for type_name in ("queryType", "mutationType"):
|
|
175
|
+
type_def = raw_schema.get(type_name)
|
|
176
|
+
if type_def is not None:
|
|
177
|
+
query_type_name = type_def["name"]
|
|
178
|
+
for type_def in raw_schema.get("types", []):
|
|
179
|
+
if type_def["name"] == query_type_name:
|
|
180
|
+
for field in type_def["fields"]:
|
|
181
|
+
statistic.operations.total += 1
|
|
182
|
+
dummy_operation.label = f"{query_type_name}.{field['name']}"
|
|
183
|
+
if not self._should_skip(dummy_operation):
|
|
184
|
+
statistic.operations.selected += 1
|
|
185
|
+
return statistic
|
|
109
186
|
|
|
110
187
|
def get_all_operations(self) -> Generator[Result[APIOperation, InvalidSchema], None, None]:
|
|
111
188
|
schema = self.client_schema
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
189
|
+
for root_type, operation_type in (
|
|
190
|
+
(RootType.QUERY, schema.query_type),
|
|
191
|
+
(RootType.MUTATION, schema.mutation_type),
|
|
192
|
+
):
|
|
193
|
+
if operation_type is None:
|
|
194
|
+
continue
|
|
195
|
+
for field_name, field_ in operation_type.fields.items():
|
|
196
|
+
operation = self._build_operation(root_type, operation_type, field_name, field_)
|
|
197
|
+
if self._should_skip(operation):
|
|
198
|
+
continue
|
|
199
|
+
yield Ok(operation)
|
|
200
|
+
|
|
201
|
+
def _should_skip(
|
|
202
|
+
self,
|
|
203
|
+
operation: APIOperation,
|
|
204
|
+
_ctx_cache: SimpleNamespace = SimpleNamespace(operation=None),
|
|
205
|
+
) -> bool:
|
|
206
|
+
_ctx_cache.operation = operation
|
|
207
|
+
return not self.filter_set.match(_ctx_cache)
|
|
208
|
+
|
|
209
|
+
def _build_operation(
|
|
210
|
+
self,
|
|
211
|
+
root_type: RootType,
|
|
212
|
+
operation_type: graphql.GraphQLObjectType,
|
|
213
|
+
field_name: str,
|
|
214
|
+
field: graphql.GraphQlField,
|
|
215
|
+
) -> APIOperation:
|
|
216
|
+
return APIOperation(
|
|
217
|
+
base_url=self.get_base_url(),
|
|
218
|
+
path=self.base_path,
|
|
219
|
+
label=f"{operation_type.name}.{field_name}",
|
|
220
|
+
method="POST",
|
|
221
|
+
app=self.app,
|
|
222
|
+
schema=self,
|
|
223
|
+
responses=GraphQLResponses(),
|
|
224
|
+
security=None,
|
|
225
|
+
# Parameters are not yet supported
|
|
226
|
+
definition=GraphQLOperationDefinition(
|
|
227
|
+
raw=field,
|
|
228
|
+
type_=operation_type,
|
|
229
|
+
field_name=field_name,
|
|
230
|
+
root_type=root_type,
|
|
231
|
+
),
|
|
232
|
+
)
|
|
128
233
|
|
|
129
234
|
def get_case_strategy(
|
|
130
235
|
self,
|
|
131
236
|
operation: APIOperation,
|
|
132
|
-
hooks:
|
|
133
|
-
|
|
237
|
+
hooks: HookDispatcher | None = None,
|
|
238
|
+
auth_storage: AuthStorage | None = None,
|
|
239
|
+
generation_mode: GenerationMode = GenerationMode.POSITIVE,
|
|
240
|
+
**kwargs: Any,
|
|
134
241
|
) -> SearchStrategy:
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
242
|
+
return graphql_cases(
|
|
243
|
+
operation=operation,
|
|
244
|
+
hooks=hooks,
|
|
245
|
+
auth_storage=auth_storage,
|
|
246
|
+
generation_mode=generation_mode,
|
|
247
|
+
**kwargs,
|
|
248
|
+
)
|
|
140
249
|
|
|
141
|
-
def
|
|
142
|
-
self, response: GenericResponse, operation: APIOperation, stateful: Optional[Stateful]
|
|
143
|
-
) -> Sequence[StatefulTest]:
|
|
250
|
+
def get_strategies_from_examples(self, operation: APIOperation, **kwargs: Any) -> list[SearchStrategy[Case]]:
|
|
144
251
|
return []
|
|
145
252
|
|
|
146
253
|
def make_case(
|
|
147
254
|
self,
|
|
148
255
|
*,
|
|
149
|
-
case_cls: Type[C],
|
|
150
256
|
operation: APIOperation,
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
257
|
+
method: str | None = None,
|
|
258
|
+
path: str | None = None,
|
|
259
|
+
path_parameters: dict[str, Any] | None = None,
|
|
260
|
+
headers: dict[str, Any] | CaseInsensitiveDict | None = None,
|
|
261
|
+
cookies: dict[str, Any] | None = None,
|
|
262
|
+
query: dict[str, Any] | None = None,
|
|
263
|
+
body: list | dict[str, Any] | str | int | float | bool | bytes | NotSet = NOT_SET,
|
|
264
|
+
media_type: str | None = None,
|
|
265
|
+
meta: CaseMetadata | None = None,
|
|
266
|
+
) -> Case:
|
|
267
|
+
return Case(
|
|
159
268
|
operation=operation,
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
269
|
+
method=method or operation.method.upper(),
|
|
270
|
+
path=path or operation.path,
|
|
271
|
+
path_parameters=path_parameters or {},
|
|
272
|
+
headers=CaseInsensitiveDict() if headers is None else CaseInsensitiveDict(headers),
|
|
273
|
+
cookies=cookies or {},
|
|
274
|
+
query=query or {},
|
|
164
275
|
body=body,
|
|
165
|
-
media_type=media_type,
|
|
276
|
+
media_type=media_type or "application/json",
|
|
277
|
+
meta=meta,
|
|
166
278
|
)
|
|
279
|
+
|
|
280
|
+
def get_tags(self, operation: APIOperation) -> list[str] | None:
|
|
281
|
+
return None
|
|
282
|
+
|
|
283
|
+
def validate(self) -> None:
|
|
284
|
+
return None
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
@dataclass
|
|
288
|
+
class FieldMap(Mapping):
|
|
289
|
+
"""Container for accessing API operations.
|
|
290
|
+
|
|
291
|
+
Provides a more specific error message if API operation is not found.
|
|
292
|
+
"""
|
|
293
|
+
|
|
294
|
+
_parent: APIOperationMap
|
|
295
|
+
_root_type: RootType
|
|
296
|
+
_operation_type: graphql.GraphQLObjectType
|
|
297
|
+
|
|
298
|
+
__slots__ = ("_parent", "_root_type", "_operation_type")
|
|
299
|
+
|
|
300
|
+
def __len__(self) -> int:
|
|
301
|
+
return len(self._operation_type.fields)
|
|
302
|
+
|
|
303
|
+
def __iter__(self) -> Iterator[str]:
|
|
304
|
+
return iter(self._operation_type.fields)
|
|
305
|
+
|
|
306
|
+
def _init_operation(self, field_name: str) -> APIOperation:
|
|
307
|
+
schema = cast(GraphQLSchema, self._parent._schema)
|
|
308
|
+
operation_type = self._operation_type
|
|
309
|
+
field_ = operation_type.fields[field_name]
|
|
310
|
+
return schema._build_operation(self._root_type, operation_type, field_name, field_)
|
|
311
|
+
|
|
312
|
+
def __getitem__(self, item: str) -> APIOperation:
|
|
313
|
+
try:
|
|
314
|
+
return self._init_operation(item)
|
|
315
|
+
except KeyError as exc:
|
|
316
|
+
field_names = list(self._operation_type.fields)
|
|
317
|
+
matches = get_close_matches(item, field_names)
|
|
318
|
+
message = f"`{item}` field not found"
|
|
319
|
+
if matches:
|
|
320
|
+
message += f". Did you mean `{matches[0]}`?"
|
|
321
|
+
raise KeyError(message) from exc
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
@st.composite # type: ignore[misc]
|
|
325
|
+
def graphql_cases(
|
|
326
|
+
draw: Callable,
|
|
327
|
+
*,
|
|
328
|
+
operation: APIOperation,
|
|
329
|
+
hooks: HookDispatcher | None = None,
|
|
330
|
+
auth_storage: auths.AuthStorage | None = None,
|
|
331
|
+
generation_mode: GenerationMode = GenerationMode.POSITIVE,
|
|
332
|
+
path_parameters: NotSet | dict[str, Any] = NOT_SET,
|
|
333
|
+
headers: NotSet | dict[str, Any] = NOT_SET,
|
|
334
|
+
cookies: NotSet | dict[str, Any] = NOT_SET,
|
|
335
|
+
query: NotSet | dict[str, Any] = NOT_SET,
|
|
336
|
+
body: Any = NOT_SET,
|
|
337
|
+
media_type: str | None = None,
|
|
338
|
+
phase: TestPhase = TestPhase.FUZZING,
|
|
339
|
+
) -> Any:
|
|
340
|
+
import graphql
|
|
341
|
+
from hypothesis_graphql import strategies as gql_st
|
|
342
|
+
|
|
343
|
+
start = time.monotonic()
|
|
344
|
+
definition = cast(GraphQLOperationDefinition, operation.definition)
|
|
345
|
+
strategy_factory = {
|
|
346
|
+
RootType.QUERY: gql_st.queries,
|
|
347
|
+
RootType.MUTATION: gql_st.mutations,
|
|
348
|
+
}[definition.root_type]
|
|
349
|
+
hook_context = HookContext(operation=operation)
|
|
350
|
+
custom_scalars = {**get_extra_scalar_strategies(), **CUSTOM_SCALARS}
|
|
351
|
+
generation = operation.schema.config.generation_for(operation=operation, phase="fuzzing")
|
|
352
|
+
strategy = strategy_factory(
|
|
353
|
+
operation.schema.client_schema, # type: ignore[attr-defined]
|
|
354
|
+
fields=[definition.field_name],
|
|
355
|
+
custom_scalars=custom_scalars,
|
|
356
|
+
print_ast=_noop,
|
|
357
|
+
allow_x00=generation.allow_x00,
|
|
358
|
+
allow_null=generation.graphql_allow_null,
|
|
359
|
+
codec=generation.codec,
|
|
360
|
+
)
|
|
361
|
+
strategy = apply_to_all_dispatchers(operation, hook_context, hooks, strategy, "body").map(graphql.print_ast)
|
|
362
|
+
body = draw(strategy)
|
|
363
|
+
|
|
364
|
+
path_parameters_ = _generate_parameter(
|
|
365
|
+
ParameterLocation.PATH, path_parameters, draw, operation, hook_context, hooks
|
|
366
|
+
)
|
|
367
|
+
headers_ = _generate_parameter(ParameterLocation.HEADER, headers, draw, operation, hook_context, hooks)
|
|
368
|
+
cookies_ = _generate_parameter(ParameterLocation.COOKIE, cookies, draw, operation, hook_context, hooks)
|
|
369
|
+
query_ = _generate_parameter(ParameterLocation.QUERY, query, draw, operation, hook_context, hooks)
|
|
370
|
+
|
|
371
|
+
_phase_data = {
|
|
372
|
+
TestPhase.EXAMPLES: ExamplesPhaseData(
|
|
373
|
+
description="Positive test case",
|
|
374
|
+
parameter=None,
|
|
375
|
+
parameter_location=None,
|
|
376
|
+
location=None,
|
|
377
|
+
),
|
|
378
|
+
TestPhase.FUZZING: FuzzingPhaseData(
|
|
379
|
+
description="Positive test case",
|
|
380
|
+
parameter=None,
|
|
381
|
+
parameter_location=None,
|
|
382
|
+
location=None,
|
|
383
|
+
),
|
|
384
|
+
}[phase]
|
|
385
|
+
phase_data = cast(Union[ExamplesPhaseData, FuzzingPhaseData], _phase_data)
|
|
386
|
+
instance = operation.Case(
|
|
387
|
+
path_parameters=path_parameters_,
|
|
388
|
+
headers=headers_,
|
|
389
|
+
cookies=cookies_,
|
|
390
|
+
query=query_,
|
|
391
|
+
body=body,
|
|
392
|
+
_meta=CaseMetadata(
|
|
393
|
+
generation=GenerationInfo(
|
|
394
|
+
time=time.monotonic() - start,
|
|
395
|
+
mode=generation_mode,
|
|
396
|
+
),
|
|
397
|
+
phase=PhaseInfo(name=phase, data=phase_data),
|
|
398
|
+
components={
|
|
399
|
+
kind: ComponentInfo(mode=generation_mode)
|
|
400
|
+
for kind, value in [
|
|
401
|
+
(ParameterLocation.QUERY, query_),
|
|
402
|
+
(ParameterLocation.PATH, path_parameters_),
|
|
403
|
+
(ParameterLocation.HEADER, headers_),
|
|
404
|
+
(ParameterLocation.COOKIE, cookies_),
|
|
405
|
+
(ParameterLocation.BODY, body),
|
|
406
|
+
]
|
|
407
|
+
if value is not NOT_SET
|
|
408
|
+
},
|
|
409
|
+
),
|
|
410
|
+
media_type=media_type or "application/json",
|
|
411
|
+
)
|
|
412
|
+
context = auths.AuthContext(
|
|
413
|
+
operation=operation,
|
|
414
|
+
app=operation.app,
|
|
415
|
+
)
|
|
416
|
+
auths.set_on_case(instance, context, auth_storage)
|
|
417
|
+
return instance
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
def _generate_parameter(
|
|
421
|
+
location: ParameterLocation,
|
|
422
|
+
explicit: NotSet | dict[str, Any],
|
|
423
|
+
draw: Callable,
|
|
424
|
+
operation: APIOperation,
|
|
425
|
+
context: HookContext,
|
|
426
|
+
hooks: HookDispatcher | None,
|
|
427
|
+
) -> Any:
|
|
428
|
+
# Schemathesis does not generate anything but `body` for GraphQL, hence use `None`
|
|
429
|
+
container = location.container_name
|
|
430
|
+
if isinstance(explicit, NotSet):
|
|
431
|
+
strategy = apply_to_all_dispatchers(operation, context, hooks, st.none(), container)
|
|
432
|
+
else:
|
|
433
|
+
strategy = apply_to_all_dispatchers(operation, context, hooks, st.just(explicit), container)
|
|
434
|
+
return draw(strategy)
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
def _noop(node: graphql.Node) -> graphql.Node:
|
|
438
|
+
return node
|