schemathesis 3.15.4__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 -1219
- 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 +682 -257
- schemathesis/specs/graphql/__init__.py +0 -1
- schemathesis/specs/graphql/nodes.py +26 -2
- schemathesis/specs/graphql/scalars.py +77 -12
- schemathesis/specs/graphql/schemas.py +367 -148
- schemathesis/specs/graphql/validation.py +33 -0
- schemathesis/specs/openapi/__init__.py +9 -1
- schemathesis/specs/openapi/_hypothesis.py +555 -318
- 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 +748 -82
- 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 +93 -73
- schemathesis/specs/openapi/negative/mutations.py +294 -103
- schemathesis/specs/openapi/negative/utils.py +0 -9
- schemathesis/specs/openapi/patterns.py +458 -0
- schemathesis/specs/openapi/references.py +60 -81
- schemathesis/specs/openapi/schemas.py +647 -666
- schemathesis/specs/openapi/serialization.py +53 -30
- schemathesis/specs/openapi/stateful/__init__.py +403 -68
- 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.15.4.dist-info → schemathesis-4.4.2.dist-info}/WHEEL +1 -1
- schemathesis-4.4.2.dist-info/entry_points.txt +6 -0
- {schemathesis-3.15.4.dist-info → schemathesis-4.4.2.dist-info/licenses}/LICENSE +1 -1
- schemathesis/_compat.py +0 -57
- schemathesis/_hypothesis.py +0 -123
- schemathesis/auth.py +0 -214
- schemathesis/cli/callbacks.py +0 -240
- schemathesis/cli/cassettes.py +0 -351
- schemathesis/cli/context.py +0 -38
- schemathesis/cli/debug.py +0 -21
- schemathesis/cli/handlers.py +0 -11
- schemathesis/cli/junitxml.py +0 -41
- schemathesis/cli/options.py +0 -70
- schemathesis/cli/output/__init__.py +0 -1
- schemathesis/cli/output/default.py +0 -521
- schemathesis/cli/output/short.py +0 -40
- schemathesis/constants.py +0 -88
- schemathesis/exceptions.py +0 -257
- schemathesis/extra/_aiohttp.py +0 -27
- schemathesis/extra/_flask.py +0 -10
- schemathesis/extra/_server.py +0 -16
- schemathesis/extra/pytest_plugin.py +0 -251
- schemathesis/failures.py +0 -145
- schemathesis/fixups/__init__.py +0 -29
- schemathesis/fixups/fast_api.py +0 -30
- schemathesis/graphql.py +0 -5
- schemathesis/internal.py +0 -6
- schemathesis/lazy.py +0 -301
- schemathesis/models.py +0 -1113
- schemathesis/parameters.py +0 -91
- schemathesis/runner/__init__.py +0 -470
- schemathesis/runner/events.py +0 -242
- schemathesis/runner/impl/__init__.py +0 -3
- schemathesis/runner/impl/core.py +0 -791
- schemathesis/runner/impl/solo.py +0 -85
- schemathesis/runner/impl/threadpool.py +0 -367
- schemathesis/runner/serialization.py +0 -206
- schemathesis/serializers.py +0 -253
- schemathesis/service/__init__.py +0 -18
- schemathesis/service/auth.py +0 -10
- schemathesis/service/client.py +0 -62
- schemathesis/service/constants.py +0 -25
- schemathesis/service/events.py +0 -39
- schemathesis/service/handler.py +0 -46
- schemathesis/service/hosts.py +0 -74
- schemathesis/service/metadata.py +0 -42
- schemathesis/service/models.py +0 -21
- schemathesis/service/serialization.py +0 -184
- schemathesis/service/worker.py +0 -39
- 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 -303
- schemathesis/specs/openapi/loaders.py +0 -453
- schemathesis/specs/openapi/parameters.py +0 -430
- schemathesis/specs/openapi/security.py +0 -129
- schemathesis/specs/openapi/validation.py +0 -24
- schemathesis/stateful.py +0 -358
- schemathesis/targets.py +0 -32
- schemathesis/types.py +0 -38
- schemathesis/utils.py +0 -475
- schemathesis-3.15.4.dist-info/METADATA +0 -202
- schemathesis-3.15.4.dist-info/RECORD +0 -99
- schemathesis-3.15.4.dist-info/entry_points.txt +0 -7
- /schemathesis/{extra → cli/ext}/__init__.py +0 -0
|
@@ -1,219 +1,438 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
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
|
+
)
|
|
4
20
|
from urllib.parse import urlsplit
|
|
5
21
|
|
|
6
|
-
import attr
|
|
7
|
-
import graphql
|
|
8
|
-
import requests
|
|
9
22
|
from hypothesis import strategies as st
|
|
10
|
-
from hypothesis.strategies import SearchStrategy
|
|
11
|
-
from hypothesis_graphql import strategies as gql_st
|
|
12
23
|
from requests.structures import CaseInsensitiveDict
|
|
13
24
|
|
|
14
|
-
from
|
|
15
|
-
from
|
|
16
|
-
from
|
|
17
|
-
from
|
|
18
|
-
from
|
|
19
|
-
from
|
|
20
|
-
from
|
|
21
|
-
from
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
if isinstance(self.body, bytes):
|
|
39
|
-
kwargs["data"] = self.body
|
|
40
|
-
# Assume that the payload is JSON, not raw GraphQL queries
|
|
41
|
-
kwargs["headers"].setdefault("Content-Type", "application/json")
|
|
42
|
-
else:
|
|
43
|
-
kwargs["json"] = {"query": self.body}
|
|
44
|
-
return kwargs
|
|
45
|
-
|
|
46
|
-
def as_werkzeug_kwargs(self, headers: Optional[Dict[str, str]] = None) -> Dict[str, Any]:
|
|
47
|
-
final_headers = self._get_headers(headers)
|
|
48
|
-
return {
|
|
49
|
-
"method": self.method,
|
|
50
|
-
"path": self.operation.schema.get_full_path(self.formatted_path),
|
|
51
|
-
# Convert to a regular dictionary, as we use `CaseInsensitiveDict` which is not supported by Werkzeug
|
|
52
|
-
"headers": dict(final_headers),
|
|
53
|
-
"query_string": self.query,
|
|
54
|
-
"json": {"query": self.body},
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
def validate_response(
|
|
58
|
-
self,
|
|
59
|
-
response: GenericResponse,
|
|
60
|
-
checks: Tuple[CheckFunction, ...] = (),
|
|
61
|
-
additional_checks: Tuple[CheckFunction, ...] = (),
|
|
62
|
-
code_sample_style: Optional[str] = None,
|
|
63
|
-
) -> None:
|
|
64
|
-
checks = checks or (not_a_server_error,)
|
|
65
|
-
checks += additional_checks
|
|
66
|
-
return super().validate_response(response, checks, code_sample_style=code_sample_style)
|
|
67
|
-
|
|
68
|
-
def call_asgi(
|
|
69
|
-
self,
|
|
70
|
-
app: Any = None,
|
|
71
|
-
base_url: Optional[str] = None,
|
|
72
|
-
headers: Optional[Dict[str, str]] = None,
|
|
73
|
-
**kwargs: Any,
|
|
74
|
-
) -> requests.Response:
|
|
75
|
-
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
|
+
)
|
|
76
49
|
|
|
50
|
+
from .scalars import CUSTOM_SCALARS, get_extra_scalar_strategies
|
|
77
51
|
|
|
78
|
-
|
|
52
|
+
if TYPE_CHECKING:
|
|
53
|
+
import graphql
|
|
54
|
+
from hypothesis.strategies import SearchStrategy
|
|
79
55
|
|
|
56
|
+
from schemathesis.auths import AuthStorage
|
|
80
57
|
|
|
81
|
-
|
|
58
|
+
|
|
59
|
+
@unique
|
|
60
|
+
class RootType(enum.Enum):
|
|
61
|
+
QUERY = enum.auto()
|
|
62
|
+
MUTATION = enum.auto()
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass(repr=False)
|
|
82
66
|
class GraphQLOperationDefinition(OperationDefinition):
|
|
83
|
-
field_name: str
|
|
84
|
-
type_: graphql.GraphQLType
|
|
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
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class GraphQLResponses:
|
|
85
|
+
def find_by_status_code(self, status_code: int) -> None:
|
|
86
|
+
return None # pragma: no cover
|
|
87
|
+
|
|
88
|
+
def add(self, status_code: str, definition: dict[str, Any]) -> None:
|
|
89
|
+
return None # pragma: no cover
|
|
85
90
|
|
|
86
91
|
|
|
87
|
-
@
|
|
92
|
+
@dataclass
|
|
88
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
|
+
|
|
89
136
|
def get_full_path(self, path: str) -> str:
|
|
90
137
|
return self.base_path
|
|
91
138
|
|
|
92
|
-
@property
|
|
93
|
-
def
|
|
94
|
-
return "
|
|
139
|
+
@property
|
|
140
|
+
def specification(self) -> Specification:
|
|
141
|
+
return Specification.graphql(version="")
|
|
95
142
|
|
|
96
143
|
@property
|
|
97
144
|
def client_schema(self) -> graphql.GraphQLSchema:
|
|
145
|
+
import graphql
|
|
146
|
+
|
|
98
147
|
if not hasattr(self, "_client_schema"):
|
|
99
|
-
# pylint: disable=attribute-defined-outside-init
|
|
100
148
|
self._client_schema = graphql.build_client_schema(self.raw_schema)
|
|
101
149
|
return self._client_schema
|
|
102
150
|
|
|
103
151
|
@property
|
|
104
152
|
def base_path(self) -> str:
|
|
105
|
-
if self.base_url:
|
|
106
|
-
return urlsplit(self.base_url).path
|
|
153
|
+
if self.config.base_url:
|
|
154
|
+
return urlsplit(self.config.base_url).path
|
|
107
155
|
return self._get_base_path()
|
|
108
156
|
|
|
109
157
|
def _get_base_path(self) -> str:
|
|
110
158
|
return cast(str, urlsplit(self.location).path)
|
|
111
159
|
|
|
112
|
-
|
|
113
|
-
|
|
160
|
+
def _measure_statistic(self) -> ApiStatistic:
|
|
161
|
+
statistic = ApiStatistic()
|
|
114
162
|
raw_schema = self.raw_schema["__schema"]
|
|
115
|
-
|
|
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
|
+
|
|
116
174
|
for type_name in ("queryType", "mutationType"):
|
|
117
175
|
type_def = raw_schema.get(type_name)
|
|
118
176
|
if type_def is not None:
|
|
119
177
|
query_type_name = type_def["name"]
|
|
120
178
|
for type_def in raw_schema.get("types", []):
|
|
121
179
|
if type_def["name"] == query_type_name:
|
|
122
|
-
|
|
123
|
-
|
|
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
|
|
124
186
|
|
|
125
187
|
def get_all_operations(self) -> Generator[Result[APIOperation, InvalidSchema], None, None]:
|
|
126
188
|
schema = self.client_schema
|
|
127
|
-
for operation_type in (
|
|
189
|
+
for root_type, operation_type in (
|
|
190
|
+
(RootType.QUERY, schema.query_type),
|
|
191
|
+
(RootType.MUTATION, schema.mutation_type),
|
|
192
|
+
):
|
|
128
193
|
if operation_type is None:
|
|
129
194
|
continue
|
|
130
|
-
for field_name,
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
+
)
|
|
151
233
|
|
|
152
234
|
def get_case_strategy(
|
|
153
235
|
self,
|
|
154
236
|
operation: APIOperation,
|
|
155
|
-
hooks:
|
|
156
|
-
auth_storage:
|
|
157
|
-
|
|
237
|
+
hooks: HookDispatcher | None = None,
|
|
238
|
+
auth_storage: AuthStorage | None = None,
|
|
239
|
+
generation_mode: GenerationMode = GenerationMode.POSITIVE,
|
|
240
|
+
**kwargs: Any,
|
|
158
241
|
) -> SearchStrategy:
|
|
159
|
-
return
|
|
242
|
+
return graphql_cases(
|
|
160
243
|
operation=operation,
|
|
161
|
-
client_schema=self.client_schema,
|
|
162
244
|
hooks=hooks,
|
|
163
245
|
auth_storage=auth_storage,
|
|
164
|
-
|
|
246
|
+
generation_mode=generation_mode,
|
|
247
|
+
**kwargs,
|
|
165
248
|
)
|
|
166
249
|
|
|
167
|
-
def get_strategies_from_examples(self, operation: APIOperation) ->
|
|
168
|
-
return []
|
|
169
|
-
|
|
170
|
-
def get_stateful_tests(
|
|
171
|
-
self, response: GenericResponse, operation: APIOperation, stateful: Optional[Stateful]
|
|
172
|
-
) -> Sequence[StatefulTest]:
|
|
250
|
+
def get_strategies_from_examples(self, operation: APIOperation, **kwargs: Any) -> list[SearchStrategy[Case]]:
|
|
173
251
|
return []
|
|
174
252
|
|
|
175
253
|
def make_case(
|
|
176
254
|
self,
|
|
177
255
|
*,
|
|
178
|
-
case_cls: Type[C],
|
|
179
256
|
operation: APIOperation,
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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(
|
|
188
268
|
operation=operation,
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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 {},
|
|
193
275
|
body=body,
|
|
194
|
-
media_type=media_type,
|
|
276
|
+
media_type=media_type or "application/json",
|
|
277
|
+
meta=meta,
|
|
195
278
|
)
|
|
196
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_)
|
|
197
311
|
|
|
198
|
-
|
|
199
|
-
|
|
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(
|
|
200
326
|
draw: Callable,
|
|
327
|
+
*,
|
|
201
328
|
operation: APIOperation,
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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,
|
|
206
339
|
) -> Any:
|
|
340
|
+
import graphql
|
|
341
|
+
from hypothesis_graphql import strategies as gql_st
|
|
342
|
+
|
|
343
|
+
start = time.monotonic()
|
|
207
344
|
definition = cast(GraphQLOperationDefinition, operation.definition)
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
}[definition.
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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(
|
|
215
413
|
operation=operation,
|
|
216
414
|
app=operation.app,
|
|
217
415
|
)
|
|
218
|
-
|
|
416
|
+
auths.set_on_case(instance, context, auth_storage)
|
|
219
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
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, List, cast
|
|
4
|
+
|
|
5
|
+
from schemathesis.generation.case import Case
|
|
6
|
+
from schemathesis.graphql.checks import GraphQLClientError, GraphQLServerError, UnexpectedGraphQLResponse
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def validate_graphql_response(case: Case, payload: Any) -> None:
|
|
10
|
+
"""Validate GraphQL response.
|
|
11
|
+
|
|
12
|
+
Semantically valid GraphQL responses are JSON objects and may contain `data` or `errors` keys.
|
|
13
|
+
"""
|
|
14
|
+
from graphql.error import GraphQLFormattedError
|
|
15
|
+
|
|
16
|
+
if not isinstance(payload, dict):
|
|
17
|
+
raise UnexpectedGraphQLResponse(
|
|
18
|
+
operation=case.operation.label,
|
|
19
|
+
message="GraphQL response is not a JSON object",
|
|
20
|
+
type_name=str(type(payload)),
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
errors = cast(List[GraphQLFormattedError], payload.get("errors"))
|
|
24
|
+
if errors is not None and len(errors) > 0:
|
|
25
|
+
data = payload.get("data")
|
|
26
|
+
# There is no `path` pointing to some part of the input query, assuming client error
|
|
27
|
+
if data is None and "path" not in errors[0]:
|
|
28
|
+
raise GraphQLClientError(operation=case.operation.label, message=errors[0]["message"], errors=errors)
|
|
29
|
+
if len(errors) > 1:
|
|
30
|
+
message = "\n\n".join([f"{idx}. {error['message']}" for idx, error in enumerate(errors, 1)])
|
|
31
|
+
else:
|
|
32
|
+
message = errors[0]["message"]
|
|
33
|
+
raise GraphQLServerError(operation=case.operation.label, message=message, errors=errors)
|
|
@@ -1 +1,9 @@
|
|
|
1
|
-
from .
|
|
1
|
+
from .formats import register_string_format as format
|
|
2
|
+
from .formats import unregister_string_format
|
|
3
|
+
from .media_types import register_media_type as media_type
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"format",
|
|
7
|
+
"unregister_string_format",
|
|
8
|
+
"media_type",
|
|
9
|
+
]
|