schemathesis 3.39.15__py3-none-any.whl → 4.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- schemathesis/__init__.py +41 -79
- schemathesis/auths.py +111 -122
- schemathesis/checks.py +169 -60
- schemathesis/cli/__init__.py +15 -2117
- schemathesis/cli/commands/__init__.py +85 -0
- schemathesis/cli/commands/data.py +10 -0
- schemathesis/cli/commands/run/__init__.py +590 -0
- schemathesis/cli/commands/run/context.py +204 -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 +18 -0
- schemathesis/cli/commands/run/handlers/cassettes.py +474 -0
- schemathesis/cli/commands/run/handlers/junitxml.py +55 -0
- schemathesis/cli/commands/run/handlers/output.py +1628 -0
- schemathesis/cli/commands/run/loaders.py +114 -0
- schemathesis/cli/commands/run/validation.py +246 -0
- schemathesis/cli/constants.py +5 -58
- schemathesis/cli/core.py +19 -0
- schemathesis/cli/ext/fs.py +16 -0
- schemathesis/cli/ext/groups.py +84 -0
- schemathesis/cli/{options.py → ext/options.py} +36 -34
- schemathesis/config/__init__.py +189 -0
- schemathesis/config/_auth.py +51 -0
- schemathesis/config/_checks.py +268 -0
- schemathesis/config/_diff_base.py +99 -0
- schemathesis/config/_env.py +21 -0
- schemathesis/config/_error.py +156 -0
- schemathesis/config/_generation.py +149 -0
- schemathesis/config/_health_check.py +24 -0
- schemathesis/config/_operations.py +327 -0
- schemathesis/config/_output.py +171 -0
- schemathesis/config/_parameters.py +19 -0
- schemathesis/config/_phases.py +187 -0
- schemathesis/config/_projects.py +527 -0
- schemathesis/config/_rate_limit.py +17 -0
- schemathesis/config/_report.py +120 -0
- schemathesis/config/_validator.py +9 -0
- schemathesis/config/_warnings.py +25 -0
- schemathesis/config/schema.json +885 -0
- schemathesis/core/__init__.py +67 -0
- schemathesis/core/compat.py +32 -0
- schemathesis/core/control.py +2 -0
- schemathesis/core/curl.py +58 -0
- schemathesis/core/deserialization.py +65 -0
- schemathesis/core/errors.py +459 -0
- schemathesis/core/failures.py +315 -0
- schemathesis/core/fs.py +19 -0
- schemathesis/core/hooks.py +20 -0
- schemathesis/core/loaders.py +104 -0
- schemathesis/core/marks.py +66 -0
- schemathesis/{transports/content_types.py → core/media_types.py} +14 -12
- schemathesis/core/output/__init__.py +46 -0
- schemathesis/core/output/sanitization.py +54 -0
- schemathesis/{throttling.py → core/rate_limit.py} +16 -17
- schemathesis/core/registries.py +31 -0
- schemathesis/core/transforms.py +113 -0
- schemathesis/core/transport.py +223 -0
- schemathesis/core/validation.py +54 -0
- schemathesis/core/version.py +7 -0
- schemathesis/engine/__init__.py +28 -0
- schemathesis/engine/context.py +118 -0
- schemathesis/engine/control.py +36 -0
- schemathesis/engine/core.py +169 -0
- schemathesis/engine/errors.py +464 -0
- schemathesis/engine/events.py +258 -0
- schemathesis/engine/phases/__init__.py +88 -0
- schemathesis/{runner → engine/phases}/probes.py +52 -68
- schemathesis/engine/phases/stateful/__init__.py +68 -0
- schemathesis/engine/phases/stateful/_executor.py +356 -0
- schemathesis/engine/phases/stateful/context.py +85 -0
- schemathesis/engine/phases/unit/__init__.py +212 -0
- schemathesis/engine/phases/unit/_executor.py +416 -0
- schemathesis/engine/phases/unit/_pool.py +82 -0
- schemathesis/engine/recorder.py +247 -0
- schemathesis/errors.py +43 -0
- schemathesis/filters.py +17 -98
- schemathesis/generation/__init__.py +5 -33
- schemathesis/generation/case.py +317 -0
- schemathesis/generation/coverage.py +282 -175
- schemathesis/generation/hypothesis/__init__.py +36 -0
- schemathesis/generation/hypothesis/builder.py +800 -0
- schemathesis/generation/{_hypothesis.py → hypothesis/examples.py} +2 -11
- schemathesis/generation/hypothesis/given.py +66 -0
- schemathesis/generation/hypothesis/reporting.py +14 -0
- schemathesis/generation/hypothesis/strategies.py +16 -0
- schemathesis/generation/meta.py +115 -0
- schemathesis/generation/metrics.py +93 -0
- schemathesis/generation/modes.py +20 -0
- schemathesis/generation/overrides.py +116 -0
- schemathesis/generation/stateful/__init__.py +37 -0
- schemathesis/generation/stateful/state_machine.py +278 -0
- schemathesis/graphql/__init__.py +15 -0
- schemathesis/graphql/checks.py +109 -0
- schemathesis/graphql/loaders.py +284 -0
- schemathesis/hooks.py +80 -101
- schemathesis/openapi/__init__.py +13 -0
- schemathesis/openapi/checks.py +455 -0
- schemathesis/openapi/generation/__init__.py +0 -0
- schemathesis/openapi/generation/filters.py +72 -0
- schemathesis/openapi/loaders.py +313 -0
- schemathesis/pytest/__init__.py +5 -0
- schemathesis/pytest/control_flow.py +7 -0
- schemathesis/pytest/lazy.py +281 -0
- schemathesis/pytest/loaders.py +36 -0
- schemathesis/{extra/pytest_plugin.py → pytest/plugin.py} +128 -108
- schemathesis/python/__init__.py +0 -0
- schemathesis/python/asgi.py +12 -0
- schemathesis/python/wsgi.py +12 -0
- schemathesis/schemas.py +537 -273
- schemathesis/specs/graphql/__init__.py +0 -1
- schemathesis/specs/graphql/_cache.py +1 -2
- schemathesis/specs/graphql/scalars.py +42 -6
- schemathesis/specs/graphql/schemas.py +141 -137
- schemathesis/specs/graphql/validation.py +11 -17
- schemathesis/specs/openapi/__init__.py +6 -1
- schemathesis/specs/openapi/_cache.py +1 -2
- schemathesis/specs/openapi/_hypothesis.py +142 -156
- schemathesis/specs/openapi/checks.py +368 -257
- schemathesis/specs/openapi/converter.py +4 -4
- schemathesis/specs/openapi/definitions.py +1 -1
- schemathesis/specs/openapi/examples.py +23 -21
- schemathesis/specs/openapi/expressions/__init__.py +31 -19
- schemathesis/specs/openapi/expressions/extractors.py +1 -4
- schemathesis/specs/openapi/expressions/lexer.py +1 -1
- schemathesis/specs/openapi/expressions/nodes.py +36 -41
- schemathesis/specs/openapi/expressions/parser.py +1 -1
- schemathesis/specs/openapi/formats.py +35 -7
- schemathesis/specs/openapi/media_types.py +53 -12
- schemathesis/specs/openapi/negative/__init__.py +7 -4
- schemathesis/specs/openapi/negative/mutations.py +6 -5
- schemathesis/specs/openapi/parameters.py +7 -10
- schemathesis/specs/openapi/patterns.py +94 -31
- schemathesis/specs/openapi/references.py +12 -53
- schemathesis/specs/openapi/schemas.py +238 -308
- schemathesis/specs/openapi/security.py +1 -1
- schemathesis/specs/openapi/serialization.py +12 -6
- schemathesis/specs/openapi/stateful/__init__.py +268 -133
- schemathesis/specs/openapi/stateful/control.py +87 -0
- schemathesis/specs/openapi/stateful/links.py +209 -0
- schemathesis/transport/__init__.py +142 -0
- schemathesis/transport/asgi.py +26 -0
- schemathesis/transport/prepare.py +124 -0
- schemathesis/transport/requests.py +244 -0
- schemathesis/{_xml.py → transport/serialization.py} +69 -11
- schemathesis/transport/wsgi.py +171 -0
- schemathesis-4.0.0.dist-info/METADATA +204 -0
- schemathesis-4.0.0.dist-info/RECORD +164 -0
- {schemathesis-3.39.15.dist-info → schemathesis-4.0.0.dist-info}/entry_points.txt +1 -1
- {schemathesis-3.39.15.dist-info → schemathesis-4.0.0.dist-info}/licenses/LICENSE +1 -1
- schemathesis/_compat.py +0 -74
- schemathesis/_dependency_versions.py +0 -19
- schemathesis/_hypothesis.py +0 -712
- schemathesis/_override.py +0 -50
- schemathesis/_patches.py +0 -21
- schemathesis/_rate_limiter.py +0 -7
- schemathesis/cli/callbacks.py +0 -466
- schemathesis/cli/cassettes.py +0 -561
- schemathesis/cli/context.py +0 -75
- schemathesis/cli/debug.py +0 -27
- schemathesis/cli/handlers.py +0 -19
- schemathesis/cli/junitxml.py +0 -124
- schemathesis/cli/output/__init__.py +0 -1
- schemathesis/cli/output/default.py +0 -920
- schemathesis/cli/output/short.py +0 -59
- schemathesis/cli/reporting.py +0 -79
- schemathesis/cli/sanitization.py +0 -26
- schemathesis/code_samples.py +0 -151
- schemathesis/constants.py +0 -54
- schemathesis/contrib/__init__.py +0 -11
- schemathesis/contrib/openapi/__init__.py +0 -11
- schemathesis/contrib/openapi/fill_missing_examples.py +0 -24
- schemathesis/contrib/openapi/formats/__init__.py +0 -9
- schemathesis/contrib/openapi/formats/uuid.py +0 -16
- schemathesis/contrib/unique_data.py +0 -41
- schemathesis/exceptions.py +0 -571
- schemathesis/experimental/__init__.py +0 -109
- schemathesis/extra/_aiohttp.py +0 -28
- schemathesis/extra/_flask.py +0 -13
- schemathesis/extra/_server.py +0 -18
- schemathesis/failures.py +0 -284
- schemathesis/fixups/__init__.py +0 -37
- schemathesis/fixups/fast_api.py +0 -41
- schemathesis/fixups/utf8_bom.py +0 -28
- schemathesis/generation/_methods.py +0 -44
- schemathesis/graphql.py +0 -3
- schemathesis/internal/__init__.py +0 -7
- schemathesis/internal/checks.py +0 -86
- schemathesis/internal/copy.py +0 -32
- schemathesis/internal/datetime.py +0 -5
- schemathesis/internal/deprecation.py +0 -37
- schemathesis/internal/diff.py +0 -15
- schemathesis/internal/extensions.py +0 -27
- schemathesis/internal/jsonschema.py +0 -36
- schemathesis/internal/output.py +0 -68
- schemathesis/internal/transformation.py +0 -26
- schemathesis/internal/validation.py +0 -34
- schemathesis/lazy.py +0 -474
- schemathesis/loaders.py +0 -122
- schemathesis/models.py +0 -1341
- schemathesis/parameters.py +0 -90
- schemathesis/runner/__init__.py +0 -605
- schemathesis/runner/events.py +0 -389
- schemathesis/runner/impl/__init__.py +0 -3
- schemathesis/runner/impl/context.py +0 -88
- schemathesis/runner/impl/core.py +0 -1280
- schemathesis/runner/impl/solo.py +0 -80
- schemathesis/runner/impl/threadpool.py +0 -391
- schemathesis/runner/serialization.py +0 -544
- schemathesis/sanitization.py +0 -252
- schemathesis/serializers.py +0 -328
- schemathesis/service/__init__.py +0 -18
- schemathesis/service/auth.py +0 -11
- schemathesis/service/ci.py +0 -202
- schemathesis/service/client.py +0 -133
- schemathesis/service/constants.py +0 -38
- schemathesis/service/events.py +0 -61
- schemathesis/service/extensions.py +0 -224
- schemathesis/service/hosts.py +0 -111
- schemathesis/service/metadata.py +0 -71
- schemathesis/service/models.py +0 -258
- schemathesis/service/report.py +0 -255
- schemathesis/service/serialization.py +0 -173
- schemathesis/service/usage.py +0 -66
- schemathesis/specs/graphql/loaders.py +0 -364
- schemathesis/specs/openapi/expressions/context.py +0 -16
- schemathesis/specs/openapi/links.py +0 -389
- schemathesis/specs/openapi/loaders.py +0 -707
- schemathesis/specs/openapi/stateful/statistic.py +0 -198
- schemathesis/specs/openapi/stateful/types.py +0 -14
- schemathesis/specs/openapi/validation.py +0 -26
- schemathesis/stateful/__init__.py +0 -147
- schemathesis/stateful/config.py +0 -97
- schemathesis/stateful/context.py +0 -135
- schemathesis/stateful/events.py +0 -274
- schemathesis/stateful/runner.py +0 -309
- schemathesis/stateful/sink.py +0 -68
- schemathesis/stateful/state_machine.py +0 -328
- schemathesis/stateful/statistic.py +0 -22
- schemathesis/stateful/validation.py +0 -100
- schemathesis/targets.py +0 -77
- schemathesis/transports/__init__.py +0 -369
- schemathesis/transports/asgi.py +0 -7
- schemathesis/transports/auth.py +0 -38
- schemathesis/transports/headers.py +0 -36
- schemathesis/transports/responses.py +0 -57
- schemathesis/types.py +0 -44
- schemathesis/utils.py +0 -164
- schemathesis-3.39.15.dist-info/METADATA +0 -293
- schemathesis-3.39.15.dist-info/RECORD +0 -160
- /schemathesis/{extra → cli/ext}/__init__.py +0 -0
- /schemathesis/{_lazy_import.py → core/lazy_import.py} +0 -0
- /schemathesis/{internal → core}/result.py +0 -0
- {schemathesis-3.39.15.dist-info → schemathesis-4.0.0.dist-info}/WHEEL +0 -0
@@ -1 +0,0 @@
|
|
1
|
-
from .loaders import from_asgi, from_dict, from_file, from_path, from_url, from_wsgi
|
@@ -3,7 +3,7 @@ from __future__ import annotations
|
|
3
3
|
from functools import lru_cache
|
4
4
|
from typing import TYPE_CHECKING
|
5
5
|
|
6
|
-
from
|
6
|
+
from schemathesis.core.errors import IncorrectUsage
|
7
7
|
|
8
8
|
if TYPE_CHECKING:
|
9
9
|
import graphql
|
@@ -13,17 +13,53 @@ CUSTOM_SCALARS: dict[str, st.SearchStrategy[graphql.ValueNode]] = {}
|
|
13
13
|
|
14
14
|
|
15
15
|
def scalar(name: str, strategy: st.SearchStrategy[graphql.ValueNode]) -> None:
|
16
|
-
"""Register a
|
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
|
+
```
|
17
53
|
|
18
|
-
:param str name: Scalar name. It should correspond the one used in the schema.
|
19
|
-
:param strategy: Hypothesis strategy you'd like to use to generate values for this scalar.
|
20
54
|
"""
|
21
55
|
from hypothesis.strategies import SearchStrategy
|
22
56
|
|
23
57
|
if not isinstance(name, str):
|
24
|
-
raise
|
58
|
+
raise IncorrectUsage(f"Scalar name {name!r} must be a string")
|
25
59
|
if not isinstance(strategy, SearchStrategy):
|
26
|
-
raise
|
60
|
+
raise IncorrectUsage(
|
61
|
+
f"{strategy!r} must be a Hypothesis strategy which generates AST nodes matching this scalar"
|
62
|
+
)
|
27
63
|
CUSTOM_SCALARS[name] = strategy
|
28
64
|
|
29
65
|
|
@@ -14,44 +14,49 @@ from typing import (
|
|
14
14
|
Iterator,
|
15
15
|
Mapping,
|
16
16
|
NoReturn,
|
17
|
-
|
18
|
-
TypeVar,
|
17
|
+
Union,
|
19
18
|
cast,
|
20
19
|
)
|
21
|
-
from urllib.parse import urlsplit
|
20
|
+
from urllib.parse import urlsplit
|
22
21
|
|
23
22
|
import graphql
|
24
23
|
from hypothesis import strategies as st
|
25
24
|
from hypothesis_graphql import strategies as gql_st
|
26
25
|
from requests.structures import CaseInsensitiveDict
|
27
26
|
|
28
|
-
from
|
29
|
-
from
|
30
|
-
from
|
31
|
-
from
|
32
|
-
from
|
33
|
-
from
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
27
|
+
from schemathesis import auths
|
28
|
+
from schemathesis.core import NOT_SET, NotSet, Specification
|
29
|
+
from schemathesis.core.errors import InvalidSchema, OperationNotFound
|
30
|
+
from schemathesis.core.result import Ok, Result
|
31
|
+
from schemathesis.generation import GenerationMode
|
32
|
+
from schemathesis.generation.case import Case
|
33
|
+
from schemathesis.generation.meta import (
|
34
|
+
CaseMetadata,
|
35
|
+
ComponentInfo,
|
36
|
+
ComponentKind,
|
37
|
+
ExplicitPhaseData,
|
38
|
+
GeneratePhaseData,
|
39
|
+
GenerationInfo,
|
40
|
+
PhaseInfo,
|
41
|
+
TestPhase,
|
39
42
|
)
|
40
|
-
from
|
41
|
-
from
|
42
|
-
|
43
|
-
|
44
|
-
|
43
|
+
from schemathesis.hooks import HookContext, HookDispatcher, apply_to_all_dispatchers
|
44
|
+
from schemathesis.schemas import (
|
45
|
+
APIOperation,
|
46
|
+
APIOperationMap,
|
47
|
+
ApiStatistic,
|
48
|
+
BaseSchema,
|
49
|
+
OperationDefinition,
|
50
|
+
)
|
51
|
+
from schemathesis.specs.openapi.constants import LOCATION_TO_CONTAINER
|
52
|
+
|
45
53
|
from ._cache import OperationCache
|
46
54
|
from .scalars import CUSTOM_SCALARS, get_extra_scalar_strategies
|
47
55
|
|
48
56
|
if TYPE_CHECKING:
|
49
57
|
from hypothesis.strategies import SearchStrategy
|
50
58
|
|
51
|
-
from
|
52
|
-
from ...internal.checks import CheckFunction
|
53
|
-
from ...stateful import Stateful, StatefulTest
|
54
|
-
from ...transports.responses import GenericResponse
|
59
|
+
from schemathesis.auths import AuthStorage
|
55
60
|
|
56
61
|
|
57
62
|
@unique
|
@@ -61,47 +66,13 @@ class RootType(enum.Enum):
|
|
61
66
|
|
62
67
|
|
63
68
|
@dataclass(repr=False)
|
64
|
-
class GraphQLCase(Case):
|
65
|
-
def __hash__(self) -> int:
|
66
|
-
return hash(self.as_curl_command({SCHEMATHESIS_TEST_CASE_HEADER: "0"}))
|
67
|
-
|
68
|
-
def _get_url(self, base_url: str | None) -> str:
|
69
|
-
base_url = self._get_base_url(base_url)
|
70
|
-
# Replace the path, in case if the user provided any path parameters via hooks
|
71
|
-
parts = list(urlsplit(base_url))
|
72
|
-
parts[2] = self.formatted_path
|
73
|
-
return urlunsplit(parts)
|
74
|
-
|
75
|
-
def _get_body(self) -> Body | NotSet:
|
76
|
-
return self.body if isinstance(self.body, (NotSet, bytes)) else {"query": self.body}
|
77
|
-
|
78
|
-
def validate_response(
|
79
|
-
self,
|
80
|
-
response: GenericResponse,
|
81
|
-
checks: tuple[CheckFunction, ...] = (),
|
82
|
-
additional_checks: tuple[CheckFunction, ...] = (),
|
83
|
-
excluded_checks: tuple[CheckFunction, ...] = (),
|
84
|
-
code_sample_style: str | None = None,
|
85
|
-
headers: dict[str, Any] | None = None,
|
86
|
-
transport_kwargs: dict[str, Any] | None = None,
|
87
|
-
) -> None:
|
88
|
-
checks = checks or (not_a_server_error,)
|
89
|
-
checks += additional_checks
|
90
|
-
checks = tuple(check for check in checks if check not in excluded_checks)
|
91
|
-
return super().validate_response(
|
92
|
-
response, checks, code_sample_style=code_sample_style, headers=headers, transport_kwargs=transport_kwargs
|
93
|
-
)
|
94
|
-
|
95
|
-
|
96
|
-
C = TypeVar("C", bound=Case)
|
97
|
-
|
98
|
-
|
99
|
-
@dataclass
|
100
69
|
class GraphQLOperationDefinition(OperationDefinition):
|
101
70
|
field_name: str
|
102
71
|
type_: graphql.GraphQLType
|
103
72
|
root_type: RootType
|
104
73
|
|
74
|
+
def _repr_pretty_(self, *args: Any, **kwargs: Any) -> None: ...
|
75
|
+
|
105
76
|
@property
|
106
77
|
def is_query(self) -> bool:
|
107
78
|
return self.root_type == RootType.QUERY
|
@@ -144,6 +115,15 @@ class GraphQLSchema(BaseSchema):
|
|
144
115
|
return map
|
145
116
|
raise KeyError(key)
|
146
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
|
+
|
147
127
|
def on_missing_operation(self, item: str, exc: KeyError) -> NoReturn:
|
148
128
|
raw_schema = self.raw_schema["__schema"]
|
149
129
|
type_names = [type_def["name"] for type_def in raw_schema.get("types", [])]
|
@@ -157,8 +137,8 @@ class GraphQLSchema(BaseSchema):
|
|
157
137
|
return self.base_path
|
158
138
|
|
159
139
|
@property
|
160
|
-
def
|
161
|
-
return "
|
140
|
+
def specification(self) -> Specification:
|
141
|
+
return Specification.graphql(version="")
|
162
142
|
|
163
143
|
@property
|
164
144
|
def client_schema(self) -> graphql.GraphQLSchema:
|
@@ -168,34 +148,39 @@ class GraphQLSchema(BaseSchema):
|
|
168
148
|
|
169
149
|
@property
|
170
150
|
def base_path(self) -> str:
|
171
|
-
if self.base_url:
|
172
|
-
return urlsplit(self.base_url).path
|
151
|
+
if self.config.base_url:
|
152
|
+
return urlsplit(self.config.base_url).path
|
173
153
|
return self._get_base_path()
|
174
154
|
|
175
155
|
def _get_base_path(self) -> str:
|
176
156
|
return cast(str, urlsplit(self.location).path)
|
177
157
|
|
178
|
-
|
179
|
-
|
158
|
+
def _measure_statistic(self) -> ApiStatistic:
|
159
|
+
statistic = ApiStatistic()
|
180
160
|
raw_schema = self.raw_schema["__schema"]
|
181
|
-
|
161
|
+
dummy_operation = APIOperation(
|
162
|
+
base_url=self.get_base_url(),
|
163
|
+
path=self.base_path,
|
164
|
+
label="",
|
165
|
+
method="POST",
|
166
|
+
schema=self,
|
167
|
+
definition=None, # type: ignore
|
168
|
+
)
|
169
|
+
|
182
170
|
for type_name in ("queryType", "mutationType"):
|
183
171
|
type_def = raw_schema.get(type_name)
|
184
172
|
if type_def is not None:
|
185
173
|
query_type_name = type_def["name"]
|
186
174
|
for type_def in raw_schema.get("types", []):
|
187
175
|
if type_def["name"] == query_type_name:
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
def get_all_operations(
|
197
|
-
self, hooks: HookDispatcher | None = None, generation_config: GenerationConfig | None = None
|
198
|
-
) -> Generator[Result[APIOperation, OperationSchemaError], None, None]:
|
176
|
+
for field in type_def["fields"]:
|
177
|
+
statistic.operations.total += 1
|
178
|
+
dummy_operation.label = f"{query_type_name}.{field['name']}"
|
179
|
+
if not self._should_skip(dummy_operation):
|
180
|
+
statistic.operations.selected += 1
|
181
|
+
return statistic
|
182
|
+
|
183
|
+
def get_all_operations(self) -> Generator[Result[APIOperation, InvalidSchema], None, None]:
|
199
184
|
schema = self.client_schema
|
200
185
|
for root_type, operation_type in (
|
201
186
|
(RootType.QUERY, schema.query_type),
|
@@ -207,13 +192,6 @@ class GraphQLSchema(BaseSchema):
|
|
207
192
|
operation = self._build_operation(root_type, operation_type, field_name, field_)
|
208
193
|
if self._should_skip(operation):
|
209
194
|
continue
|
210
|
-
context = HookContext(operation=operation)
|
211
|
-
if (
|
212
|
-
should_skip_operation(GLOBAL_HOOK_DISPATCHER, context)
|
213
|
-
or should_skip_operation(self.hooks, context)
|
214
|
-
or (hooks and should_skip_operation(hooks, context))
|
215
|
-
):
|
216
|
-
continue
|
217
195
|
yield Ok(operation)
|
218
196
|
|
219
197
|
def _should_skip(
|
@@ -234,7 +212,7 @@ class GraphQLSchema(BaseSchema):
|
|
234
212
|
return APIOperation(
|
235
213
|
base_url=self.get_base_url(),
|
236
214
|
path=self.base_path,
|
237
|
-
|
215
|
+
label=f"{operation_type.name}.{field_name}",
|
238
216
|
method="POST",
|
239
217
|
app=self.app,
|
240
218
|
schema=self,
|
@@ -247,7 +225,6 @@ class GraphQLSchema(BaseSchema):
|
|
247
225
|
field_name=field_name,
|
248
226
|
root_type=root_type,
|
249
227
|
),
|
250
|
-
case_cls=GraphQLCase,
|
251
228
|
)
|
252
229
|
|
253
230
|
def get_case_strategy(
|
@@ -255,51 +232,45 @@ class GraphQLSchema(BaseSchema):
|
|
255
232
|
operation: APIOperation,
|
256
233
|
hooks: HookDispatcher | None = None,
|
257
234
|
auth_storage: AuthStorage | None = None,
|
258
|
-
|
259
|
-
generation_config: GenerationConfig | None = None,
|
235
|
+
generation_mode: GenerationMode = GenerationMode.POSITIVE,
|
260
236
|
**kwargs: Any,
|
261
237
|
) -> SearchStrategy:
|
262
|
-
return
|
238
|
+
return graphql_cases(
|
263
239
|
operation=operation,
|
264
|
-
client_schema=self.client_schema,
|
265
240
|
hooks=hooks,
|
266
241
|
auth_storage=auth_storage,
|
267
|
-
|
268
|
-
generation_config=generation_config or self.generation_config,
|
242
|
+
generation_mode=generation_mode,
|
269
243
|
**kwargs,
|
270
244
|
)
|
271
245
|
|
272
|
-
def get_strategies_from_examples(
|
273
|
-
self, operation: APIOperation, as_strategy_kwargs: dict[str, Any] | None = None
|
274
|
-
) -> list[SearchStrategy[Case]]:
|
275
|
-
return []
|
276
|
-
|
277
|
-
def get_stateful_tests(
|
278
|
-
self, response: GenericResponse, operation: APIOperation, stateful: Stateful | None
|
279
|
-
) -> Sequence[StatefulTest]:
|
246
|
+
def get_strategies_from_examples(self, operation: APIOperation, **kwargs: Any) -> list[SearchStrategy[Case]]:
|
280
247
|
return []
|
281
248
|
|
282
249
|
def make_case(
|
283
250
|
self,
|
284
251
|
*,
|
285
|
-
case_cls: type[C],
|
286
252
|
operation: APIOperation,
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
253
|
+
method: str | None = None,
|
254
|
+
path: str | None = None,
|
255
|
+
path_parameters: dict[str, Any] | None = None,
|
256
|
+
headers: dict[str, Any] | CaseInsensitiveDict | None = None,
|
257
|
+
cookies: dict[str, Any] | None = None,
|
258
|
+
query: dict[str, Any] | None = None,
|
259
|
+
body: list | dict[str, Any] | str | int | float | bool | bytes | NotSet = NOT_SET,
|
292
260
|
media_type: str | None = None,
|
293
|
-
|
294
|
-
|
261
|
+
meta: CaseMetadata | None = None,
|
262
|
+
) -> Case:
|
263
|
+
return Case(
|
295
264
|
operation=operation,
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
265
|
+
method=method or operation.method.upper(),
|
266
|
+
path=path or operation.path,
|
267
|
+
path_parameters=path_parameters or {},
|
268
|
+
headers=CaseInsensitiveDict() if headers is None else CaseInsensitiveDict(headers),
|
269
|
+
cookies=cookies or {},
|
270
|
+
query=query or {},
|
300
271
|
body=body,
|
301
272
|
media_type=media_type or "application/json",
|
302
|
-
|
273
|
+
meta=meta,
|
303
274
|
)
|
304
275
|
|
305
276
|
def get_tags(self, operation: APIOperation) -> list[str] | None:
|
@@ -353,15 +324,20 @@ class FieldMap(Mapping):
|
|
353
324
|
|
354
325
|
|
355
326
|
@st.composite # type: ignore
|
356
|
-
def
|
327
|
+
def graphql_cases(
|
357
328
|
draw: Callable,
|
329
|
+
*,
|
358
330
|
operation: APIOperation,
|
359
|
-
client_schema: graphql.GraphQLSchema,
|
360
331
|
hooks: HookDispatcher | None = None,
|
361
|
-
auth_storage: AuthStorage | None = None,
|
362
|
-
|
363
|
-
|
364
|
-
|
332
|
+
auth_storage: auths.AuthStorage | None = None,
|
333
|
+
generation_mode: GenerationMode = GenerationMode.POSITIVE,
|
334
|
+
path_parameters: NotSet | dict[str, Any] = NOT_SET,
|
335
|
+
headers: NotSet | dict[str, Any] = NOT_SET,
|
336
|
+
cookies: NotSet | dict[str, Any] = NOT_SET,
|
337
|
+
query: NotSet | dict[str, Any] = NOT_SET,
|
338
|
+
body: Any = NOT_SET,
|
339
|
+
media_type: str | None = None,
|
340
|
+
phase: TestPhase = TestPhase.FUZZING,
|
365
341
|
) -> Any:
|
366
342
|
start = time.monotonic()
|
367
343
|
definition = cast(GraphQLOperationDefinition, operation.definition)
|
@@ -369,36 +345,56 @@ def get_case_strategy(
|
|
369
345
|
RootType.QUERY: gql_st.queries,
|
370
346
|
RootType.MUTATION: gql_st.mutations,
|
371
347
|
}[definition.root_type]
|
372
|
-
hook_context = HookContext(operation)
|
373
|
-
generation_config = generation_config or GenerationConfig()
|
348
|
+
hook_context = HookContext(operation=operation)
|
374
349
|
custom_scalars = {**get_extra_scalar_strategies(), **CUSTOM_SCALARS}
|
350
|
+
generation = operation.schema.config.generation_for(operation=operation, phase="fuzzing")
|
375
351
|
strategy = strategy_factory(
|
376
|
-
client_schema,
|
352
|
+
operation.schema.client_schema, # type: ignore[attr-defined]
|
377
353
|
fields=[definition.field_name],
|
378
354
|
custom_scalars=custom_scalars,
|
379
355
|
print_ast=_noop, # type: ignore
|
380
|
-
allow_x00=
|
381
|
-
allow_null=
|
382
|
-
codec=
|
356
|
+
allow_x00=generation.allow_x00,
|
357
|
+
allow_null=generation.graphql_allow_null,
|
358
|
+
codec=generation.codec,
|
383
359
|
)
|
384
360
|
strategy = apply_to_all_dispatchers(operation, hook_context, hooks, strategy, "body").map(graphql.print_ast)
|
385
361
|
body = draw(strategy)
|
386
362
|
|
387
|
-
path_parameters_ = _generate_parameter("path", draw, operation, hook_context, hooks)
|
388
|
-
headers_ = _generate_parameter("header", draw, operation, hook_context, hooks)
|
389
|
-
cookies_ = _generate_parameter("cookie", draw, operation, hook_context, hooks)
|
390
|
-
query_ = _generate_parameter("query", draw, operation, hook_context, hooks)
|
391
|
-
|
392
|
-
|
363
|
+
path_parameters_ = _generate_parameter("path", path_parameters, draw, operation, hook_context, hooks)
|
364
|
+
headers_ = _generate_parameter("header", headers, draw, operation, hook_context, hooks)
|
365
|
+
cookies_ = _generate_parameter("cookie", cookies, draw, operation, hook_context, hooks)
|
366
|
+
query_ = _generate_parameter("query", query, draw, operation, hook_context, hooks)
|
367
|
+
|
368
|
+
_phase_data = {
|
369
|
+
TestPhase.EXAMPLES: ExplicitPhaseData(),
|
370
|
+
TestPhase.FUZZING: GeneratePhaseData(),
|
371
|
+
}[phase]
|
372
|
+
phase_data = cast(Union[ExplicitPhaseData, GeneratePhaseData], _phase_data)
|
373
|
+
instance = operation.Case(
|
393
374
|
path_parameters=path_parameters_,
|
394
375
|
headers=headers_,
|
395
376
|
cookies=cookies_,
|
396
377
|
query=query_,
|
397
378
|
body=body,
|
398
|
-
|
399
|
-
|
400
|
-
|
401
|
-
|
379
|
+
_meta=CaseMetadata(
|
380
|
+
generation=GenerationInfo(
|
381
|
+
time=time.monotonic() - start,
|
382
|
+
mode=generation_mode,
|
383
|
+
),
|
384
|
+
phase=PhaseInfo(name=phase, data=phase_data),
|
385
|
+
components={
|
386
|
+
kind: ComponentInfo(mode=generation_mode)
|
387
|
+
for kind, value in [
|
388
|
+
(ComponentKind.QUERY, query_),
|
389
|
+
(ComponentKind.PATH_PARAMETERS, path_parameters_),
|
390
|
+
(ComponentKind.HEADERS, headers_),
|
391
|
+
(ComponentKind.COOKIES, cookies_),
|
392
|
+
(ComponentKind.BODY, body),
|
393
|
+
]
|
394
|
+
if value is not NOT_SET
|
395
|
+
},
|
396
|
+
),
|
397
|
+
media_type=media_type or "application/json",
|
402
398
|
) # type: ignore
|
403
399
|
context = auths.AuthContext(
|
404
400
|
operation=operation,
|
@@ -409,11 +405,19 @@ def get_case_strategy(
|
|
409
405
|
|
410
406
|
|
411
407
|
def _generate_parameter(
|
412
|
-
location: str,
|
408
|
+
location: str,
|
409
|
+
explicit: NotSet | dict[str, Any],
|
410
|
+
draw: Callable,
|
411
|
+
operation: APIOperation,
|
412
|
+
context: HookContext,
|
413
|
+
hooks: HookDispatcher | None,
|
413
414
|
) -> Any:
|
414
415
|
# Schemathesis does not generate anything but `body` for GraphQL, hence use `None`
|
415
416
|
container = LOCATION_TO_CONTAINER[location]
|
416
|
-
|
417
|
+
if isinstance(explicit, NotSet):
|
418
|
+
strategy = apply_to_all_dispatchers(operation, context, hooks, st.none(), container)
|
419
|
+
else:
|
420
|
+
strategy = apply_to_all_dispatchers(operation, context, hooks, st.just(explicit), container)
|
417
421
|
return draw(strategy)
|
418
422
|
|
419
423
|
|
@@ -1,10 +1,12 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
1
3
|
from typing import Any, List, cast
|
2
4
|
|
3
|
-
from
|
4
|
-
from
|
5
|
+
from schemathesis.generation.case import Case
|
6
|
+
from schemathesis.graphql.checks import GraphQLClientError, GraphQLServerError, UnexpectedGraphQLResponse
|
5
7
|
|
6
8
|
|
7
|
-
def validate_graphql_response(payload: Any) -> None:
|
9
|
+
def validate_graphql_response(case: Case, payload: Any) -> None:
|
8
10
|
"""Validate GraphQL response.
|
9
11
|
|
10
12
|
Semantically valid GraphQL responses are JSON objects and may contain `data` or `errors` keys.
|
@@ -12,28 +14,20 @@ def validate_graphql_response(payload: Any) -> None:
|
|
12
14
|
from graphql.error import GraphQLFormattedError
|
13
15
|
|
14
16
|
if not isinstance(payload, dict):
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
17
|
+
raise UnexpectedGraphQLResponse(
|
18
|
+
operation=case.operation.label,
|
19
|
+
message="GraphQL response is not a JSON object",
|
20
|
+
type_name=str(type(payload)),
|
19
21
|
)
|
20
22
|
|
21
23
|
errors = cast(List[GraphQLFormattedError], payload.get("errors"))
|
22
24
|
if errors is not None and len(errors) > 0:
|
23
|
-
exc_class = get_grouped_graphql_error(errors)
|
24
25
|
data = payload.get("data")
|
25
26
|
# There is no `path` pointing to some part of the input query, assuming client error
|
26
27
|
if data is None and "path" not in errors[0]:
|
27
|
-
|
28
|
-
raise exc_class(
|
29
|
-
failures.GraphQLClientError.title,
|
30
|
-
context=failures.GraphQLClientError(message=message, errors=errors),
|
31
|
-
)
|
28
|
+
raise GraphQLClientError(operation=case.operation.label, message=errors[0]["message"], errors=errors)
|
32
29
|
if len(errors) > 1:
|
33
30
|
message = "\n\n".join([f"{idx}. {error['message']}" for idx, error in enumerate(errors, 1)])
|
34
31
|
else:
|
35
32
|
message = errors[0]["message"]
|
36
|
-
raise
|
37
|
-
failures.GraphQLServerError.title,
|
38
|
-
context=failures.GraphQLServerError(message=message, errors=errors),
|
39
|
-
)
|
33
|
+
raise GraphQLServerError(operation=case.operation.label, message=message, errors=errors)
|
@@ -1,4 +1,9 @@
|
|
1
1
|
from .formats import register_string_format as format
|
2
2
|
from .formats import unregister_string_format
|
3
|
-
from .loaders import from_aiohttp, from_asgi, from_dict, from_file, from_path, from_pytest_fixture, from_uri, from_wsgi
|
4
3
|
from .media_types import register_media_type as media_type
|
4
|
+
|
5
|
+
__all__ = [
|
6
|
+
"format",
|
7
|
+
"unregister_string_format",
|
8
|
+
"media_type",
|
9
|
+
]
|