schemathesis 3.39.7__py3-none-any.whl → 4.0.0a2__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 +27 -65
- schemathesis/auths.py +26 -68
- schemathesis/checks.py +130 -60
- schemathesis/cli/__init__.py +5 -2105
- schemathesis/cli/commands/__init__.py +37 -0
- schemathesis/cli/commands/run/__init__.py +662 -0
- schemathesis/cli/commands/run/checks.py +80 -0
- schemathesis/cli/commands/run/context.py +117 -0
- schemathesis/cli/commands/run/events.py +30 -0
- schemathesis/cli/commands/run/executor.py +141 -0
- schemathesis/cli/commands/run/filters.py +202 -0
- schemathesis/cli/commands/run/handlers/__init__.py +46 -0
- schemathesis/cli/commands/run/handlers/base.py +18 -0
- schemathesis/cli/{cassettes.py → commands/run/handlers/cassettes.py} +178 -247
- schemathesis/cli/commands/run/handlers/junitxml.py +54 -0
- schemathesis/cli/commands/run/handlers/output.py +1368 -0
- schemathesis/cli/commands/run/hypothesis.py +105 -0
- schemathesis/cli/commands/run/loaders.py +129 -0
- schemathesis/cli/{callbacks.py → commands/run/validation.py} +59 -175
- schemathesis/cli/constants.py +5 -58
- schemathesis/cli/core.py +17 -0
- schemathesis/cli/ext/fs.py +14 -0
- schemathesis/cli/ext/groups.py +55 -0
- schemathesis/cli/{options.py → ext/options.py} +37 -16
- schemathesis/cli/hooks.py +36 -0
- schemathesis/contrib/__init__.py +1 -3
- schemathesis/contrib/openapi/__init__.py +1 -3
- schemathesis/contrib/openapi/fill_missing_examples.py +3 -7
- schemathesis/core/__init__.py +58 -0
- schemathesis/core/compat.py +25 -0
- schemathesis/core/control.py +2 -0
- schemathesis/core/curl.py +58 -0
- schemathesis/core/deserialization.py +65 -0
- schemathesis/core/errors.py +370 -0
- schemathesis/core/failures.py +315 -0
- schemathesis/core/fs.py +19 -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/{internal/output.py → core/output/__init__.py} +1 -0
- schemathesis/core/output/sanitization.py +197 -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 +108 -0
- schemathesis/core/validation.py +38 -0
- schemathesis/core/version.py +7 -0
- schemathesis/engine/__init__.py +30 -0
- schemathesis/engine/config.py +59 -0
- schemathesis/engine/context.py +119 -0
- schemathesis/engine/control.py +36 -0
- schemathesis/engine/core.py +157 -0
- schemathesis/engine/errors.py +394 -0
- schemathesis/engine/events.py +243 -0
- schemathesis/engine/phases/__init__.py +66 -0
- schemathesis/{runner → engine/phases}/probes.py +49 -68
- schemathesis/engine/phases/stateful/__init__.py +66 -0
- schemathesis/engine/phases/stateful/_executor.py +301 -0
- schemathesis/engine/phases/stateful/context.py +85 -0
- schemathesis/engine/phases/unit/__init__.py +175 -0
- schemathesis/engine/phases/unit/_executor.py +322 -0
- schemathesis/engine/phases/unit/_pool.py +74 -0
- schemathesis/engine/recorder.py +246 -0
- schemathesis/errors.py +31 -0
- schemathesis/experimental/__init__.py +9 -40
- schemathesis/filters.py +7 -95
- schemathesis/generation/__init__.py +3 -3
- schemathesis/generation/case.py +190 -0
- schemathesis/generation/coverage.py +22 -22
- schemathesis/{_patches.py → generation/hypothesis/__init__.py} +15 -6
- schemathesis/generation/hypothesis/builder.py +585 -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/modes.py +28 -0
- schemathesis/generation/overrides.py +96 -0
- schemathesis/generation/stateful/__init__.py +20 -0
- schemathesis/{stateful → generation/stateful}/state_machine.py +84 -109
- schemathesis/generation/targets.py +69 -0
- schemathesis/graphql/__init__.py +15 -0
- schemathesis/graphql/checks.py +109 -0
- schemathesis/graphql/loaders.py +131 -0
- schemathesis/hooks.py +17 -62
- schemathesis/openapi/__init__.py +13 -0
- schemathesis/openapi/checks.py +387 -0
- schemathesis/openapi/generation/__init__.py +0 -0
- schemathesis/openapi/generation/filters.py +63 -0
- schemathesis/openapi/loaders.py +178 -0
- schemathesis/pytest/__init__.py +5 -0
- schemathesis/pytest/control_flow.py +7 -0
- schemathesis/pytest/lazy.py +273 -0
- schemathesis/pytest/loaders.py +12 -0
- schemathesis/{extra/pytest_plugin.py → pytest/plugin.py} +94 -107
- schemathesis/python/__init__.py +0 -0
- schemathesis/python/asgi.py +12 -0
- schemathesis/python/wsgi.py +12 -0
- schemathesis/schemas.py +456 -228
- schemathesis/specs/graphql/__init__.py +0 -1
- schemathesis/specs/graphql/_cache.py +1 -2
- schemathesis/specs/graphql/scalars.py +5 -3
- schemathesis/specs/graphql/schemas.py +122 -123
- 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 +97 -134
- schemathesis/specs/openapi/checks.py +238 -219
- schemathesis/specs/openapi/converter.py +4 -4
- schemathesis/specs/openapi/definitions.py +1 -1
- schemathesis/specs/openapi/examples.py +22 -20
- schemathesis/specs/openapi/expressions/__init__.py +11 -15
- schemathesis/specs/openapi/expressions/extractors.py +1 -4
- schemathesis/specs/openapi/expressions/nodes.py +33 -32
- schemathesis/specs/openapi/formats.py +3 -2
- schemathesis/specs/openapi/links.py +123 -299
- schemathesis/specs/openapi/media_types.py +10 -12
- schemathesis/specs/openapi/negative/__init__.py +2 -1
- schemathesis/specs/openapi/negative/mutations.py +3 -2
- schemathesis/specs/openapi/parameters.py +8 -6
- schemathesis/specs/openapi/patterns.py +1 -1
- schemathesis/specs/openapi/references.py +11 -51
- schemathesis/specs/openapi/schemas.py +177 -191
- schemathesis/specs/openapi/security.py +1 -1
- schemathesis/specs/openapi/serialization.py +10 -6
- schemathesis/specs/openapi/stateful/__init__.py +97 -91
- schemathesis/transport/__init__.py +104 -0
- schemathesis/transport/asgi.py +26 -0
- schemathesis/transport/prepare.py +99 -0
- schemathesis/transport/requests.py +221 -0
- schemathesis/{_xml.py → transport/serialization.py} +69 -7
- schemathesis/transport/wsgi.py +165 -0
- {schemathesis-3.39.7.dist-info → schemathesis-4.0.0a2.dist-info}/METADATA +18 -14
- schemathesis-4.0.0a2.dist-info/RECORD +151 -0
- {schemathesis-3.39.7.dist-info → schemathesis-4.0.0a2.dist-info}/entry_points.txt +1 -1
- schemathesis/_compat.py +0 -74
- schemathesis/_dependency_versions.py +0 -19
- schemathesis/_hypothesis.py +0 -559
- schemathesis/_override.py +0 -50
- schemathesis/_rate_limiter.py +0 -7
- 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 -936
- 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 -56
- 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/extra/_aiohttp.py +0 -28
- schemathesis/extra/_flask.py +0 -13
- schemathesis/extra/_server.py +0 -18
- schemathesis/failures.py +0 -277
- 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 -84
- schemathesis/internal/copy.py +0 -32
- schemathesis/internal/datetime.py +0 -5
- schemathesis/internal/deprecation.py +0 -38
- schemathesis/internal/diff.py +0 -15
- schemathesis/internal/extensions.py +0 -27
- schemathesis/internal/jsonschema.py +0 -36
- 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 -104
- schemathesis/runner/impl/core.py +0 -1246
- 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/loaders.py +0 -708
- 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/statistic.py +0 -22
- schemathesis/stateful/validation.py +0 -100
- schemathesis/targets.py +0 -77
- schemathesis/transports/__init__.py +0 -359
- 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.7.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.7.dist-info → schemathesis-4.0.0a2.dist-info}/WHEEL +0 -0
- {schemathesis-3.39.7.dist-info → schemathesis-4.0.0a2.dist-info}/licenses/LICENSE +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
|
@@ -21,9 +21,11 @@ def scalar(name: str, strategy: st.SearchStrategy[graphql.ValueNode]) -> None:
|
|
21
21
|
from hypothesis.strategies import SearchStrategy
|
22
22
|
|
23
23
|
if not isinstance(name, str):
|
24
|
-
raise
|
24
|
+
raise IncorrectUsage(f"Scalar name {name!r} must be a string")
|
25
25
|
if not isinstance(strategy, SearchStrategy):
|
26
|
-
raise
|
26
|
+
raise IncorrectUsage(
|
27
|
+
f"{strategy!r} must be a Hypothesis strategy which generates AST nodes matching this scalar"
|
28
|
+
)
|
27
29
|
CUSTOM_SCALARS[name] = strategy
|
28
30
|
|
29
31
|
|
@@ -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 GenerationConfig, 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
|
@@ -157,8 +128,8 @@ class GraphQLSchema(BaseSchema):
|
|
157
128
|
return self.base_path
|
158
129
|
|
159
130
|
@property
|
160
|
-
def
|
161
|
-
return "
|
131
|
+
def specification(self) -> Specification:
|
132
|
+
return Specification.graphql(version="")
|
162
133
|
|
163
134
|
@property
|
164
135
|
def client_schema(self) -> graphql.GraphQLSchema:
|
@@ -175,27 +146,34 @@ class GraphQLSchema(BaseSchema):
|
|
175
146
|
def _get_base_path(self) -> str:
|
176
147
|
return cast(str, urlsplit(self.location).path)
|
177
148
|
|
178
|
-
|
179
|
-
|
149
|
+
def _measure_statistic(self) -> ApiStatistic:
|
150
|
+
statistic = ApiStatistic()
|
180
151
|
raw_schema = self.raw_schema["__schema"]
|
181
|
-
|
152
|
+
dummy_operation = APIOperation(
|
153
|
+
base_url=self.get_base_url(),
|
154
|
+
path=self.base_path,
|
155
|
+
label="",
|
156
|
+
method="POST",
|
157
|
+
schema=self,
|
158
|
+
definition=None, # type: ignore
|
159
|
+
)
|
160
|
+
|
182
161
|
for type_name in ("queryType", "mutationType"):
|
183
162
|
type_def = raw_schema.get(type_name)
|
184
163
|
if type_def is not None:
|
185
164
|
query_type_name = type_def["name"]
|
186
165
|
for type_def in raw_schema.get("types", []):
|
187
166
|
if type_def["name"] == query_type_name:
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
return 0
|
167
|
+
for field in type_def["fields"]:
|
168
|
+
statistic.operations.total += 1
|
169
|
+
dummy_operation.label = f"{query_type_name}.{field['name']}"
|
170
|
+
if not self._should_skip(dummy_operation):
|
171
|
+
statistic.operations.selected += 1
|
172
|
+
return statistic
|
195
173
|
|
196
174
|
def get_all_operations(
|
197
|
-
self,
|
198
|
-
) -> Generator[Result[APIOperation,
|
175
|
+
self, generation_config: GenerationConfig | None = None
|
176
|
+
) -> Generator[Result[APIOperation, InvalidSchema], None, None]:
|
199
177
|
schema = self.client_schema
|
200
178
|
for root_type, operation_type in (
|
201
179
|
(RootType.QUERY, schema.query_type),
|
@@ -207,13 +185,6 @@ class GraphQLSchema(BaseSchema):
|
|
207
185
|
operation = self._build_operation(root_type, operation_type, field_name, field_)
|
208
186
|
if self._should_skip(operation):
|
209
187
|
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
188
|
yield Ok(operation)
|
218
189
|
|
219
190
|
def _should_skip(
|
@@ -234,7 +205,7 @@ class GraphQLSchema(BaseSchema):
|
|
234
205
|
return APIOperation(
|
235
206
|
base_url=self.get_base_url(),
|
236
207
|
path=self.base_path,
|
237
|
-
|
208
|
+
label=f"{operation_type.name}.{field_name}",
|
238
209
|
method="POST",
|
239
210
|
app=self.app,
|
240
211
|
schema=self,
|
@@ -247,7 +218,6 @@ class GraphQLSchema(BaseSchema):
|
|
247
218
|
field_name=field_name,
|
248
219
|
root_type=root_type,
|
249
220
|
),
|
250
|
-
case_cls=GraphQLCase,
|
251
221
|
)
|
252
222
|
|
253
223
|
def get_case_strategy(
|
@@ -255,51 +225,47 @@ class GraphQLSchema(BaseSchema):
|
|
255
225
|
operation: APIOperation,
|
256
226
|
hooks: HookDispatcher | None = None,
|
257
227
|
auth_storage: AuthStorage | None = None,
|
258
|
-
|
228
|
+
generation_mode: GenerationMode = GenerationMode.default(),
|
259
229
|
generation_config: GenerationConfig | None = None,
|
260
230
|
**kwargs: Any,
|
261
231
|
) -> SearchStrategy:
|
262
|
-
return
|
232
|
+
return graphql_cases(
|
263
233
|
operation=operation,
|
264
|
-
client_schema=self.client_schema,
|
265
234
|
hooks=hooks,
|
266
235
|
auth_storage=auth_storage,
|
267
|
-
|
236
|
+
generation_mode=generation_mode,
|
268
237
|
generation_config=generation_config or self.generation_config,
|
269
238
|
**kwargs,
|
270
239
|
)
|
271
240
|
|
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]:
|
241
|
+
def get_strategies_from_examples(self, operation: APIOperation, **kwargs: Any) -> list[SearchStrategy[Case]]:
|
280
242
|
return []
|
281
243
|
|
282
244
|
def make_case(
|
283
245
|
self,
|
284
246
|
*,
|
285
|
-
case_cls: type[C],
|
286
247
|
operation: APIOperation,
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
248
|
+
method: str | None = None,
|
249
|
+
path: str | None = None,
|
250
|
+
path_parameters: dict[str, Any] | None = None,
|
251
|
+
headers: dict[str, Any] | None = None,
|
252
|
+
cookies: dict[str, Any] | None = None,
|
253
|
+
query: dict[str, Any] | None = None,
|
254
|
+
body: list | dict[str, Any] | str | int | float | bool | bytes | NotSet = NOT_SET,
|
292
255
|
media_type: str | None = None,
|
293
|
-
|
294
|
-
|
256
|
+
meta: CaseMetadata | None = None,
|
257
|
+
) -> Case:
|
258
|
+
return Case(
|
295
259
|
operation=operation,
|
260
|
+
method=method or operation.method.upper(),
|
261
|
+
path=path or operation.path,
|
296
262
|
path_parameters=path_parameters,
|
297
263
|
headers=CaseInsensitiveDict(headers) if headers is not None else headers,
|
298
264
|
cookies=cookies,
|
299
265
|
query=query,
|
300
266
|
body=body,
|
301
267
|
media_type=media_type or "application/json",
|
302
|
-
|
268
|
+
meta=meta,
|
303
269
|
)
|
304
270
|
|
305
271
|
def get_tags(self, operation: APIOperation) -> list[str] | None:
|
@@ -353,15 +319,21 @@ class FieldMap(Mapping):
|
|
353
319
|
|
354
320
|
|
355
321
|
@st.composite # type: ignore
|
356
|
-
def
|
322
|
+
def graphql_cases(
|
357
323
|
draw: Callable,
|
324
|
+
*,
|
358
325
|
operation: APIOperation,
|
359
|
-
client_schema: graphql.GraphQLSchema,
|
360
326
|
hooks: HookDispatcher | None = None,
|
361
|
-
auth_storage: AuthStorage | None = None,
|
362
|
-
|
363
|
-
generation_config: GenerationConfig
|
364
|
-
|
327
|
+
auth_storage: auths.AuthStorage | None = None,
|
328
|
+
generation_mode: GenerationMode = GenerationMode.default(),
|
329
|
+
generation_config: GenerationConfig,
|
330
|
+
path_parameters: NotSet | dict[str, Any] = NOT_SET,
|
331
|
+
headers: NotSet | dict[str, Any] = NOT_SET,
|
332
|
+
cookies: NotSet | dict[str, Any] = NOT_SET,
|
333
|
+
query: NotSet | dict[str, Any] = NOT_SET,
|
334
|
+
body: Any = NOT_SET,
|
335
|
+
media_type: str | None = None,
|
336
|
+
phase: TestPhase = TestPhase.GENERATE,
|
365
337
|
) -> Any:
|
366
338
|
start = time.monotonic()
|
367
339
|
definition = cast(GraphQLOperationDefinition, operation.definition)
|
@@ -370,10 +342,9 @@ def get_case_strategy(
|
|
370
342
|
RootType.MUTATION: gql_st.mutations,
|
371
343
|
}[definition.root_type]
|
372
344
|
hook_context = HookContext(operation)
|
373
|
-
generation_config = generation_config or GenerationConfig()
|
374
345
|
custom_scalars = {**get_extra_scalar_strategies(), **CUSTOM_SCALARS}
|
375
346
|
strategy = strategy_factory(
|
376
|
-
client_schema,
|
347
|
+
operation.schema.client_schema, # type: ignore[attr-defined]
|
377
348
|
fields=[definition.field_name],
|
378
349
|
custom_scalars=custom_scalars,
|
379
350
|
print_ast=_noop, # type: ignore
|
@@ -384,21 +355,41 @@ def get_case_strategy(
|
|
384
355
|
strategy = apply_to_all_dispatchers(operation, hook_context, hooks, strategy, "body").map(graphql.print_ast)
|
385
356
|
body = draw(strategy)
|
386
357
|
|
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
|
-
|
358
|
+
path_parameters_ = _generate_parameter("path", path_parameters, draw, operation, hook_context, hooks)
|
359
|
+
headers_ = _generate_parameter("header", headers, draw, operation, hook_context, hooks)
|
360
|
+
cookies_ = _generate_parameter("cookie", cookies, draw, operation, hook_context, hooks)
|
361
|
+
query_ = _generate_parameter("query", query, draw, operation, hook_context, hooks)
|
362
|
+
|
363
|
+
_phase_data = {
|
364
|
+
TestPhase.EXPLICIT: ExplicitPhaseData(),
|
365
|
+
TestPhase.GENERATE: GeneratePhaseData(),
|
366
|
+
}[phase]
|
367
|
+
phase_data = cast(Union[ExplicitPhaseData, GeneratePhaseData], _phase_data)
|
368
|
+
instance = operation.Case(
|
393
369
|
path_parameters=path_parameters_,
|
394
370
|
headers=headers_,
|
395
371
|
cookies=cookies_,
|
396
372
|
query=query_,
|
397
373
|
body=body,
|
398
|
-
|
399
|
-
|
400
|
-
|
401
|
-
|
374
|
+
meta=CaseMetadata(
|
375
|
+
generation=GenerationInfo(
|
376
|
+
time=time.monotonic() - start,
|
377
|
+
mode=generation_mode,
|
378
|
+
),
|
379
|
+
phase=PhaseInfo(name=phase, data=phase_data),
|
380
|
+
components={
|
381
|
+
kind: ComponentInfo(mode=generation_mode)
|
382
|
+
for kind, value in [
|
383
|
+
(ComponentKind.QUERY, query_),
|
384
|
+
(ComponentKind.PATH_PARAMETERS, path_parameters_),
|
385
|
+
(ComponentKind.HEADERS, headers_),
|
386
|
+
(ComponentKind.COOKIES, cookies_),
|
387
|
+
(ComponentKind.BODY, body),
|
388
|
+
]
|
389
|
+
if value is not NOT_SET
|
390
|
+
},
|
391
|
+
),
|
392
|
+
media_type=media_type or "application/json",
|
402
393
|
) # type: ignore
|
403
394
|
context = auths.AuthContext(
|
404
395
|
operation=operation,
|
@@ -409,11 +400,19 @@ def get_case_strategy(
|
|
409
400
|
|
410
401
|
|
411
402
|
def _generate_parameter(
|
412
|
-
location: str,
|
403
|
+
location: str,
|
404
|
+
explicit: NotSet | dict[str, Any],
|
405
|
+
draw: Callable,
|
406
|
+
operation: APIOperation,
|
407
|
+
context: HookContext,
|
408
|
+
hooks: HookDispatcher | None,
|
413
409
|
) -> Any:
|
414
410
|
# Schemathesis does not generate anything but `body` for GraphQL, hence use `None`
|
415
411
|
container = LOCATION_TO_CONTAINER[location]
|
416
|
-
|
412
|
+
if isinstance(explicit, NotSet):
|
413
|
+
strategy = apply_to_all_dispatchers(operation, context, hooks, st.none(), container)
|
414
|
+
else:
|
415
|
+
strategy = apply_to_all_dispatchers(operation, context, hooks, st.just(explicit), container)
|
417
416
|
return draw(strategy)
|
418
417
|
|
419
418
|
|
@@ -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
|
+
]
|