schemathesis 4.1.4__py3-none-any.whl → 4.2.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/cli/commands/run/executor.py +1 -1
- schemathesis/cli/commands/run/handlers/base.py +28 -1
- schemathesis/cli/commands/run/handlers/cassettes.py +10 -12
- schemathesis/cli/commands/run/handlers/junitxml.py +5 -6
- schemathesis/cli/commands/run/handlers/output.py +7 -1
- schemathesis/cli/ext/fs.py +1 -1
- schemathesis/config/_diff_base.py +3 -1
- schemathesis/config/_operations.py +2 -0
- schemathesis/config/_phases.py +21 -4
- schemathesis/config/_projects.py +10 -2
- schemathesis/core/adapter.py +34 -0
- schemathesis/core/errors.py +29 -5
- schemathesis/core/jsonschema/__init__.py +13 -0
- schemathesis/core/jsonschema/bundler.py +163 -0
- schemathesis/{specs/openapi/constants.py → core/jsonschema/keywords.py} +0 -8
- schemathesis/core/jsonschema/references.py +122 -0
- schemathesis/core/jsonschema/types.py +41 -0
- schemathesis/core/media_types.py +6 -4
- schemathesis/core/parameters.py +37 -0
- schemathesis/core/transforms.py +25 -2
- schemathesis/core/validation.py +19 -0
- schemathesis/engine/context.py +1 -1
- schemathesis/engine/errors.py +11 -18
- schemathesis/engine/phases/stateful/_executor.py +1 -1
- schemathesis/engine/phases/unit/_executor.py +30 -13
- schemathesis/errors.py +4 -0
- schemathesis/filters.py +2 -2
- schemathesis/generation/coverage.py +87 -11
- schemathesis/generation/hypothesis/__init__.py +4 -1
- schemathesis/generation/hypothesis/builder.py +108 -70
- schemathesis/generation/meta.py +5 -14
- schemathesis/generation/overrides.py +17 -17
- schemathesis/pytest/lazy.py +1 -1
- schemathesis/pytest/plugin.py +1 -6
- schemathesis/schemas.py +22 -72
- schemathesis/specs/graphql/schemas.py +27 -16
- schemathesis/specs/openapi/_hypothesis.py +83 -68
- schemathesis/specs/openapi/adapter/__init__.py +10 -0
- schemathesis/specs/openapi/adapter/parameters.py +504 -0
- schemathesis/specs/openapi/adapter/protocol.py +57 -0
- schemathesis/specs/openapi/adapter/references.py +19 -0
- schemathesis/specs/openapi/adapter/responses.py +329 -0
- schemathesis/specs/openapi/adapter/security.py +141 -0
- schemathesis/specs/openapi/adapter/v2.py +28 -0
- schemathesis/specs/openapi/adapter/v3_0.py +28 -0
- schemathesis/specs/openapi/adapter/v3_1.py +28 -0
- schemathesis/specs/openapi/checks.py +99 -90
- schemathesis/specs/openapi/converter.py +114 -27
- schemathesis/specs/openapi/examples.py +210 -168
- schemathesis/specs/openapi/negative/__init__.py +12 -7
- schemathesis/specs/openapi/negative/mutations.py +68 -40
- schemathesis/specs/openapi/references.py +2 -175
- schemathesis/specs/openapi/schemas.py +142 -490
- schemathesis/specs/openapi/serialization.py +15 -7
- schemathesis/specs/openapi/stateful/__init__.py +17 -12
- schemathesis/specs/openapi/stateful/inference.py +13 -11
- schemathesis/specs/openapi/stateful/links.py +5 -20
- schemathesis/specs/openapi/types/__init__.py +3 -0
- schemathesis/specs/openapi/types/v3.py +68 -0
- schemathesis/specs/openapi/utils.py +1 -13
- schemathesis/transport/requests.py +3 -11
- schemathesis/transport/serialization.py +63 -27
- schemathesis/transport/wsgi.py +1 -8
- {schemathesis-4.1.4.dist-info → schemathesis-4.2.0.dist-info}/METADATA +2 -2
- {schemathesis-4.1.4.dist-info → schemathesis-4.2.0.dist-info}/RECORD +68 -53
- schemathesis/specs/openapi/parameters.py +0 -405
- schemathesis/specs/openapi/security.py +0 -162
- {schemathesis-4.1.4.dist-info → schemathesis-4.2.0.dist-info}/WHEEL +0 -0
- {schemathesis-4.1.4.dist-info → schemathesis-4.2.0.dist-info}/entry_points.txt +0 -0
- {schemathesis-4.1.4.dist-info → schemathesis-4.2.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,122 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from schemathesis.core.jsonschema.keywords import ALL_KEYWORDS
|
4
|
+
from schemathesis.core.jsonschema.types import JsonSchema, JsonSchemaObject, get_type
|
5
|
+
|
6
|
+
|
7
|
+
def sanitize(schema: JsonSchema) -> set[str]:
|
8
|
+
"""Remove optional parts of the schema that contain references.
|
9
|
+
|
10
|
+
It covers only the most popular cases, as removing all optional parts is complicated.
|
11
|
+
We might fall back to filtering out invalid cases in the future.
|
12
|
+
"""
|
13
|
+
if isinstance(schema, bool):
|
14
|
+
return set()
|
15
|
+
|
16
|
+
stack = [schema]
|
17
|
+
while stack:
|
18
|
+
current = stack.pop()
|
19
|
+
if isinstance(current, dict):
|
20
|
+
# Optional properties
|
21
|
+
if "properties" in current:
|
22
|
+
properties = current["properties"]
|
23
|
+
required = current.get("required", [])
|
24
|
+
for name, value in list(properties.items()):
|
25
|
+
if isinstance(value, dict):
|
26
|
+
if name not in required and _has_references(value):
|
27
|
+
del properties[name]
|
28
|
+
elif _find_single_reference_combinators(value):
|
29
|
+
properties.pop(name, None)
|
30
|
+
else:
|
31
|
+
stack.append(value)
|
32
|
+
|
33
|
+
# Optional items
|
34
|
+
if "items" in current:
|
35
|
+
_sanitize_items(current)
|
36
|
+
# Not required additional properties
|
37
|
+
if "additionalProperties" in current:
|
38
|
+
_sanitize_additional_properties(current)
|
39
|
+
for k in _find_single_reference_combinators(current):
|
40
|
+
del current[k]
|
41
|
+
|
42
|
+
remaining: set[str] = set()
|
43
|
+
_collect_all_references(schema, remaining)
|
44
|
+
return remaining
|
45
|
+
|
46
|
+
|
47
|
+
def _collect_all_references(schema: JsonSchema | list[JsonSchema], remaining: set[str]) -> None:
|
48
|
+
"""Recursively collect all $ref present in the schema."""
|
49
|
+
if isinstance(schema, dict):
|
50
|
+
reference = schema.get("$ref")
|
51
|
+
if isinstance(reference, str):
|
52
|
+
remaining.add(reference)
|
53
|
+
for value in schema.values():
|
54
|
+
_collect_all_references(value, remaining)
|
55
|
+
elif isinstance(schema, list):
|
56
|
+
for item in schema:
|
57
|
+
_collect_all_references(item, remaining)
|
58
|
+
|
59
|
+
|
60
|
+
def _has_references_in_items(items: list[JsonSchema]) -> bool:
|
61
|
+
return any("$ref" in item for item in items if isinstance(item, dict))
|
62
|
+
|
63
|
+
|
64
|
+
def _has_references(schema: JsonSchemaObject) -> bool:
|
65
|
+
if "$ref" in schema:
|
66
|
+
return True
|
67
|
+
items = schema.get("items")
|
68
|
+
return (isinstance(items, dict) and "$ref" in items) or isinstance(items, list) and _has_references_in_items(items)
|
69
|
+
|
70
|
+
|
71
|
+
def _is_optional_schema(schema: JsonSchema) -> bool:
|
72
|
+
# Whether this schema could be dropped from a list of schemas
|
73
|
+
if isinstance(schema, bool):
|
74
|
+
return True
|
75
|
+
type_ = get_type(schema)
|
76
|
+
if type_ == ["object"]:
|
77
|
+
# Empty object is valid for this schema -> could be dropped
|
78
|
+
return schema.get("required", []) == [] and schema.get("minProperties", 0) == 0
|
79
|
+
# Has at least one keyword -> should not be removed
|
80
|
+
return not any(k in ALL_KEYWORDS for k in schema)
|
81
|
+
|
82
|
+
|
83
|
+
def _find_single_reference_combinators(schema: JsonSchemaObject) -> list[str]:
|
84
|
+
# Schema example:
|
85
|
+
# {
|
86
|
+
# "type": "object",
|
87
|
+
# "properties": {
|
88
|
+
# "parent": {
|
89
|
+
# "allOf": [{"$ref": "#/components/schemas/User"}]
|
90
|
+
# }
|
91
|
+
# }
|
92
|
+
# }
|
93
|
+
found = []
|
94
|
+
for keyword in ("allOf", "oneOf", "anyOf"):
|
95
|
+
combinator = schema.get(keyword)
|
96
|
+
if combinator is not None:
|
97
|
+
optionals = [subschema for subschema in combinator if not _is_optional_schema(subschema)]
|
98
|
+
# NOTE: The first schema is not bool, hence it is safe to pass it to `_has_references`
|
99
|
+
if len(optionals) == 1 and _has_references(optionals[0]):
|
100
|
+
found.append(keyword)
|
101
|
+
return found
|
102
|
+
|
103
|
+
|
104
|
+
def _sanitize_items(schema: JsonSchemaObject) -> None:
|
105
|
+
items = schema["items"]
|
106
|
+
min_items = schema.get("minItems", 0)
|
107
|
+
if not min_items:
|
108
|
+
if isinstance(items, dict) and ("$ref" in items or _find_single_reference_combinators(items)):
|
109
|
+
_convert_to_empty_array(schema)
|
110
|
+
if isinstance(items, list) and _has_references_in_items(items):
|
111
|
+
_convert_to_empty_array(schema)
|
112
|
+
|
113
|
+
|
114
|
+
def _convert_to_empty_array(schema: JsonSchemaObject) -> None:
|
115
|
+
del schema["items"]
|
116
|
+
schema["maxItems"] = 0
|
117
|
+
|
118
|
+
|
119
|
+
def _sanitize_additional_properties(schema: JsonSchemaObject) -> None:
|
120
|
+
additional_properties = schema["additionalProperties"]
|
121
|
+
if isinstance(additional_properties, dict) and "$ref" in additional_properties:
|
122
|
+
schema["additionalProperties"] = False
|
@@ -0,0 +1,41 @@
|
|
1
|
+
from typing import Any, Union
|
2
|
+
|
3
|
+
JsonSchemaObject = dict[str, Any]
|
4
|
+
JsonSchema = Union[JsonSchemaObject, bool]
|
5
|
+
|
6
|
+
ANY_TYPE = ["null", "boolean", "number", "string", "array", "object"]
|
7
|
+
ALL_TYPES = ["null", "boolean", "integer", "number", "string", "array", "object"]
|
8
|
+
|
9
|
+
|
10
|
+
def get_type(schema: JsonSchema, *, _check_type: bool = False) -> list[str]:
|
11
|
+
if isinstance(schema, bool):
|
12
|
+
return ANY_TYPE
|
13
|
+
ty = schema.get("type", ANY_TYPE)
|
14
|
+
if isinstance(ty, str):
|
15
|
+
if _check_type and ty not in ALL_TYPES:
|
16
|
+
raise AssertionError(f"Unknown type: `{ty}`. Should be one of {', '.join(ALL_TYPES)}")
|
17
|
+
return [ty]
|
18
|
+
if ty is ANY_TYPE:
|
19
|
+
return list(ty)
|
20
|
+
return list(ty)
|
21
|
+
|
22
|
+
|
23
|
+
def _get_type(schema: JsonSchema) -> list[str]:
|
24
|
+
# Special version to patch `hypothesis-jsonschema`
|
25
|
+
return get_type(schema, _check_type=True)
|
26
|
+
|
27
|
+
|
28
|
+
def to_json_type_name(v: Any) -> str:
|
29
|
+
if v is None:
|
30
|
+
return "null"
|
31
|
+
if isinstance(v, bool):
|
32
|
+
return "boolean"
|
33
|
+
if isinstance(v, dict):
|
34
|
+
return "object"
|
35
|
+
if isinstance(v, list):
|
36
|
+
return "array"
|
37
|
+
if isinstance(v, (int, float)):
|
38
|
+
return "number"
|
39
|
+
if isinstance(v, str):
|
40
|
+
return "string"
|
41
|
+
return type(v).__name__
|
schemathesis/core/media_types.py
CHANGED
@@ -1,10 +1,12 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
1
3
|
from functools import lru_cache
|
2
|
-
from typing import Generator
|
4
|
+
from typing import Generator
|
3
5
|
|
4
6
|
from schemathesis.core.errors import MalformedMediaType
|
5
7
|
|
6
8
|
|
7
|
-
def _parseparam(s: str) -> Generator[str
|
9
|
+
def _parseparam(s: str) -> Generator[str]:
|
8
10
|
while s[:1] == ";":
|
9
11
|
s = s[1:]
|
10
12
|
end = s.find(";")
|
@@ -17,7 +19,7 @@ def _parseparam(s: str) -> Generator[str, None, None]:
|
|
17
19
|
s = s[end:]
|
18
20
|
|
19
21
|
|
20
|
-
def _parse_header(line: str) ->
|
22
|
+
def _parse_header(line: str) -> tuple[str, dict]:
|
21
23
|
parts = _parseparam(";" + line)
|
22
24
|
key = parts.__next__()
|
23
25
|
pdict = {}
|
@@ -34,7 +36,7 @@ def _parse_header(line: str) -> Tuple[str, dict]:
|
|
34
36
|
|
35
37
|
|
36
38
|
@lru_cache
|
37
|
-
def parse(media_type: str) ->
|
39
|
+
def parse(media_type: str) -> tuple[str, str]:
|
38
40
|
"""Parse Content Type and return main type and subtype."""
|
39
41
|
try:
|
40
42
|
media_type, _ = _parse_header(media_type)
|
@@ -0,0 +1,37 @@
|
|
1
|
+
from enum import Enum
|
2
|
+
|
3
|
+
LOCATION_TO_CONTAINER = {
|
4
|
+
"path": "path_parameters",
|
5
|
+
"query": "query",
|
6
|
+
"header": "headers",
|
7
|
+
"cookie": "cookies",
|
8
|
+
"body": "body",
|
9
|
+
}
|
10
|
+
|
11
|
+
|
12
|
+
class ParameterLocation(str, Enum):
|
13
|
+
"""API parameter location."""
|
14
|
+
|
15
|
+
QUERY = "query"
|
16
|
+
HEADER = "header"
|
17
|
+
PATH = "path"
|
18
|
+
COOKIE = "cookie"
|
19
|
+
BODY = "body"
|
20
|
+
UNKNOWN = None
|
21
|
+
|
22
|
+
@property
|
23
|
+
def container_name(self) -> str:
|
24
|
+
return {
|
25
|
+
"path": "path_parameters",
|
26
|
+
"query": "query",
|
27
|
+
"header": "headers",
|
28
|
+
"cookie": "cookies",
|
29
|
+
"body": "body",
|
30
|
+
}[self]
|
31
|
+
|
32
|
+
@property
|
33
|
+
def is_in_header(self) -> bool:
|
34
|
+
return self in HEADER_LOCATIONS
|
35
|
+
|
36
|
+
|
37
|
+
HEADER_LOCATIONS = frozenset([ParameterLocation.HEADER, ParameterLocation.COOKIE])
|
schemathesis/core/transforms.py
CHANGED
@@ -1,6 +1,20 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
|
-
from typing import Any, Callable, Dict, List, Mapping, Union, overload
|
3
|
+
from typing import Any, Callable, Dict, List, Mapping, TypeVar, Union, overload
|
4
|
+
|
5
|
+
T = TypeVar("T")
|
6
|
+
|
7
|
+
|
8
|
+
@overload
|
9
|
+
def deepclone(value: dict) -> dict: ... # pragma: no cover
|
10
|
+
|
11
|
+
|
12
|
+
@overload
|
13
|
+
def deepclone(value: list) -> list: ... # pragma: no cover
|
14
|
+
|
15
|
+
|
16
|
+
@overload
|
17
|
+
def deepclone(value: T) -> T: ... # pragma: no cover
|
4
18
|
|
5
19
|
|
6
20
|
def deepclone(value: Any) -> Any:
|
@@ -11,7 +25,16 @@ def deepclone(value: Any) -> Any:
|
|
11
25
|
if isinstance(value, dict):
|
12
26
|
return {
|
13
27
|
k1: (
|
14
|
-
{
|
28
|
+
{
|
29
|
+
k2: (
|
30
|
+
{k3: deepclone(v3) for k3, v3 in v2.items()}
|
31
|
+
if isinstance(v2, dict)
|
32
|
+
else [deepclone(v3) for v3 in v2]
|
33
|
+
if isinstance(v2, list)
|
34
|
+
else v2
|
35
|
+
)
|
36
|
+
for k2, v2 in v1.items()
|
37
|
+
}
|
15
38
|
if isinstance(v1, dict)
|
16
39
|
else [deepclone(v2) for v2 in v1]
|
17
40
|
if isinstance(v1, list)
|
schemathesis/core/validation.py
CHANGED
@@ -1,6 +1,8 @@
|
|
1
1
|
import re
|
2
2
|
from urllib.parse import urlparse
|
3
3
|
|
4
|
+
from schemathesis.core.errors import InvalidSchema
|
5
|
+
|
4
6
|
# Adapted from http.client._is_illegal_header_value
|
5
7
|
INVALID_HEADER_RE = re.compile(r"\n(?![ \t])|\r(?![ \t\n])")
|
6
8
|
|
@@ -29,6 +31,23 @@ def is_latin_1_encodable(value: object) -> bool:
|
|
29
31
|
return False
|
30
32
|
|
31
33
|
|
34
|
+
def check_header_name(name: str) -> None:
|
35
|
+
from requests.exceptions import InvalidHeader
|
36
|
+
from requests.utils import check_header_validity
|
37
|
+
|
38
|
+
if not name:
|
39
|
+
raise InvalidSchema("Header name should not be empty")
|
40
|
+
if not name.isascii():
|
41
|
+
# `urllib3` encodes header names to ASCII
|
42
|
+
raise InvalidSchema(f"Header name should be ASCII: {name}")
|
43
|
+
try:
|
44
|
+
check_header_validity((name, ""))
|
45
|
+
except InvalidHeader as exc:
|
46
|
+
raise InvalidSchema(str(exc)) from None
|
47
|
+
if bool(INVALID_HEADER_RE.search(name)):
|
48
|
+
raise InvalidSchema(f"Invalid header name: {name}")
|
49
|
+
|
50
|
+
|
32
51
|
SURROGATE_PAIR_RE = re.compile(r"[\ud800-\udfff]")
|
33
52
|
_contains_surrogate_pair = SURROGATE_PAIR_RE.search
|
34
53
|
|
schemathesis/engine/context.py
CHANGED
@@ -95,7 +95,7 @@ class EngineContext:
|
|
95
95
|
# Generate links from collected Location headers
|
96
96
|
inferencer = LinkInferencer.from_schema(self.schema)
|
97
97
|
for operation, entries in self.observations.location_headers.items():
|
98
|
-
injected += inferencer.inject_links(operation.
|
98
|
+
injected += inferencer.inject_links(operation.responses, entries)
|
99
99
|
|
100
100
|
return injected
|
101
101
|
|
schemathesis/engine/errors.py
CHANGED
@@ -14,9 +14,10 @@ from typing import TYPE_CHECKING, Callable, Iterator, Sequence, cast
|
|
14
14
|
|
15
15
|
from schemathesis import errors
|
16
16
|
from schemathesis.core.errors import (
|
17
|
-
|
17
|
+
InfiniteRecursiveReference,
|
18
18
|
InvalidTransition,
|
19
19
|
SerializationNotPossible,
|
20
|
+
UnresolvableReference,
|
20
21
|
format_exception,
|
21
22
|
get_request_error_extras,
|
22
23
|
get_request_error_message,
|
@@ -28,7 +29,7 @@ if TYPE_CHECKING:
|
|
28
29
|
import requests
|
29
30
|
from requests.exceptions import ChunkedEncodingError
|
30
31
|
|
31
|
-
__all__ = ["EngineErrorInfo", "DeadlineExceeded", "
|
32
|
+
__all__ = ["EngineErrorInfo", "DeadlineExceeded", "UnexpectedError"]
|
32
33
|
|
33
34
|
|
34
35
|
class DeadlineExceeded(errors.SchemathesisError):
|
@@ -43,13 +44,6 @@ class DeadlineExceeded(errors.SchemathesisError):
|
|
43
44
|
)
|
44
45
|
|
45
46
|
|
46
|
-
class UnsupportedRecursiveReference(errors.SchemathesisError):
|
47
|
-
"""Recursive reference is impossible to resolve due to current limitations."""
|
48
|
-
|
49
|
-
def __init__(self) -> None:
|
50
|
-
super().__init__(RECURSIVE_REFERENCE_ERROR_MESSAGE)
|
51
|
-
|
52
|
-
|
53
47
|
class UnexpectedError(errors.SchemathesisError):
|
54
48
|
"""An unexpected error during the engine execution.
|
55
49
|
|
@@ -103,7 +97,6 @@ class EngineErrorInfo:
|
|
103
97
|
return "Schema Error"
|
104
98
|
|
105
99
|
return {
|
106
|
-
RuntimeErrorKind.SCHEMA_UNSUPPORTED: "Unsupported Schema",
|
107
100
|
RuntimeErrorKind.SCHEMA_NO_LINKS_FOUND: "Missing Open API links",
|
108
101
|
RuntimeErrorKind.SCHEMA_INVALID_STATE_MACHINE: "Invalid OpenAPI Links Definition",
|
109
102
|
RuntimeErrorKind.HYPOTHESIS_UNSUPPORTED_GRAPHQL_SCALAR: "Unknown GraphQL Scalar",
|
@@ -120,9 +113,6 @@ class EngineErrorInfo:
|
|
120
113
|
if isinstance(self._error, requests.RequestException):
|
121
114
|
return get_request_error_message(self._error)
|
122
115
|
|
123
|
-
if self._kind == RuntimeErrorKind.SCHEMA_UNSUPPORTED:
|
124
|
-
return str(self._error).strip()
|
125
|
-
|
126
116
|
if self._kind == RuntimeErrorKind.HYPOTHESIS_UNSUPPORTED_GRAPHQL_SCALAR and isinstance(
|
127
117
|
self._error, hypothesis.errors.InvalidArgument
|
128
118
|
):
|
@@ -175,7 +165,8 @@ class EngineErrorInfo:
|
|
175
165
|
return self._kind not in (
|
176
166
|
RuntimeErrorKind.SCHEMA_INVALID_REGULAR_EXPRESSION,
|
177
167
|
RuntimeErrorKind.SCHEMA_INVALID_STATE_MACHINE,
|
178
|
-
RuntimeErrorKind.
|
168
|
+
RuntimeErrorKind.SCHEMA_INVALID_UNRESOLVABLE_REFERENCE,
|
169
|
+
RuntimeErrorKind.SCHEMA_INVALID_INFINITE_RECURSION,
|
179
170
|
RuntimeErrorKind.SCHEMA_GENERIC,
|
180
171
|
RuntimeErrorKind.SCHEMA_NO_LINKS_FOUND,
|
181
172
|
RuntimeErrorKind.SERIALIZATION_NOT_POSSIBLE,
|
@@ -312,8 +303,9 @@ class RuntimeErrorKind(str, enum.Enum):
|
|
312
303
|
|
313
304
|
SCHEMA_INVALID_REGULAR_EXPRESSION = "schema_invalid_regular_expression"
|
314
305
|
SCHEMA_INVALID_STATE_MACHINE = "schema_invalid_state_machine"
|
306
|
+
SCHEMA_INVALID_INFINITE_RECURSION = "schema_invalid_infinite_recursion"
|
307
|
+
SCHEMA_INVALID_UNRESOLVABLE_REFERENCE = "schema_invalid_unresolvable_reference"
|
315
308
|
SCHEMA_NO_LINKS_FOUND = "schema_no_links_found"
|
316
|
-
SCHEMA_UNSUPPORTED = "schema_unsupported"
|
317
309
|
SCHEMA_GENERIC = "schema_generic"
|
318
310
|
|
319
311
|
SERIALIZATION_NOT_POSSIBLE = "serialization_not_possible"
|
@@ -368,9 +360,10 @@ def _classify(*, error: Exception) -> RuntimeErrorKind:
|
|
368
360
|
return RuntimeErrorKind.SCHEMA_INVALID_STATE_MACHINE
|
369
361
|
if isinstance(error, errors.NoLinksFound):
|
370
362
|
return RuntimeErrorKind.SCHEMA_NO_LINKS_FOUND
|
371
|
-
if isinstance(error,
|
372
|
-
|
373
|
-
|
363
|
+
if isinstance(error, InfiniteRecursiveReference):
|
364
|
+
return RuntimeErrorKind.SCHEMA_INVALID_INFINITE_RECURSION
|
365
|
+
if isinstance(error, UnresolvableReference):
|
366
|
+
return RuntimeErrorKind.SCHEMA_INVALID_UNRESOLVABLE_REFERENCE
|
374
367
|
if isinstance(error, errors.SerializationError):
|
375
368
|
if isinstance(error, errors.UnboundPrefix):
|
376
369
|
return RuntimeErrorKind.SERIALIZATION_UNBOUNDED_PREFIX
|
@@ -46,7 +46,7 @@ from schemathesis.generation.metrics import MetricCollector
|
|
46
46
|
def _get_hypothesis_settings_kwargs_override(settings: hypothesis.settings) -> dict[str, Any]:
|
47
47
|
"""Get the settings that should be overridden to match the defaults for API state machines."""
|
48
48
|
kwargs = {}
|
49
|
-
hypothesis_default = hypothesis.settings()
|
49
|
+
hypothesis_default = hypothesis.settings.get_profile("default")
|
50
50
|
if settings.phases == hypothesis_default.phases:
|
51
51
|
kwargs["phases"] = DEFAULT_STATE_MACHINE_SETTINGS.phases
|
52
52
|
if settings.stateful_step_count == hypothesis_default.stateful_step_count:
|
@@ -8,7 +8,6 @@ from warnings import WarningMessage, catch_warnings
|
|
8
8
|
|
9
9
|
import requests
|
10
10
|
from hypothesis.errors import InvalidArgument
|
11
|
-
from hypothesis_jsonschema._canonicalise import HypothesisRefResolutionError
|
12
11
|
from jsonschema.exceptions import SchemaError as JsonSchemaError
|
13
12
|
from jsonschema.exceptions import ValidationError
|
14
13
|
from requests.exceptions import ChunkedEncodingError
|
@@ -37,7 +36,6 @@ from schemathesis.engine.errors import (
|
|
37
36
|
TestingState,
|
38
37
|
UnexpectedError,
|
39
38
|
UnrecoverableNetworkError,
|
40
|
-
UnsupportedRecursiveReference,
|
41
39
|
clear_hypothesis_notes,
|
42
40
|
deduplicate_errors,
|
43
41
|
is_unrecoverable_network_error,
|
@@ -47,10 +45,12 @@ from schemathesis.engine.recorder import ScenarioRecorder
|
|
47
45
|
from schemathesis.generation import metrics, overrides
|
48
46
|
from schemathesis.generation.case import Case
|
49
47
|
from schemathesis.generation.hypothesis.builder import (
|
48
|
+
InfiniteRecursiveReferenceMark,
|
50
49
|
InvalidHeadersExampleMark,
|
51
50
|
InvalidRegexMark,
|
52
51
|
MissingPathParameters,
|
53
52
|
NonSerializableMark,
|
53
|
+
UnresolvableReferenceMark,
|
54
54
|
UnsatisfiableExampleMark,
|
55
55
|
)
|
56
56
|
from schemathesis.generation.hypothesis.reporting import ignore_hypothesis_output
|
@@ -173,14 +173,24 @@ def run_test(
|
|
173
173
|
status = Status.ERROR
|
174
174
|
try:
|
175
175
|
operation.schema.validate()
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
176
|
+
# JSON Schema validation can miss it if there is `$ref` adjacent to `type` on older specifications
|
177
|
+
if str(exc).startswith("Unknown type"):
|
178
|
+
yield non_fatal_error(
|
179
|
+
InvalidSchema(
|
180
|
+
message=str(exc),
|
181
|
+
path=operation.path,
|
182
|
+
method=operation.method,
|
183
|
+
)
|
184
|
+
)
|
185
|
+
else:
|
186
|
+
msg = "Unexpected error during testing of this API operation"
|
187
|
+
exc_msg = str(exc)
|
188
|
+
if exc_msg:
|
189
|
+
msg += f": {exc_msg}"
|
190
|
+
try:
|
191
|
+
raise InternalError(msg) from exc
|
192
|
+
except InternalError as exc:
|
193
|
+
yield non_fatal_error(exc)
|
184
194
|
except ValidationError as exc:
|
185
195
|
yield non_fatal_error(
|
186
196
|
InvalidSchema.from_jsonschema_error(
|
@@ -190,9 +200,6 @@ def run_test(
|
|
190
200
|
config=ctx.config.output,
|
191
201
|
)
|
192
202
|
)
|
193
|
-
except HypothesisRefResolutionError:
|
194
|
-
status = Status.ERROR
|
195
|
-
yield non_fatal_error(UnsupportedRecursiveReference())
|
196
203
|
except InvalidArgument as exc:
|
197
204
|
status = Status.ERROR
|
198
205
|
message = get_invalid_regular_expression_message(warnings)
|
@@ -265,6 +272,16 @@ def run_test(
|
|
265
272
|
status = Status.ERROR
|
266
273
|
yield non_fatal_error(missing_path_parameters)
|
267
274
|
|
275
|
+
infinite_recursive_reference = InfiniteRecursiveReferenceMark.get(test_function)
|
276
|
+
if infinite_recursive_reference:
|
277
|
+
status = Status.ERROR
|
278
|
+
yield non_fatal_error(infinite_recursive_reference)
|
279
|
+
|
280
|
+
unresolvable_reference = UnresolvableReferenceMark.get(test_function)
|
281
|
+
if unresolvable_reference:
|
282
|
+
status = Status.ERROR
|
283
|
+
yield non_fatal_error(unresolvable_reference)
|
284
|
+
|
268
285
|
for error in deduplicate_errors(errors):
|
269
286
|
yield non_fatal_error(error)
|
270
287
|
|
schemathesis/errors.py
CHANGED
@@ -3,6 +3,7 @@
|
|
3
3
|
from schemathesis.core.errors import (
|
4
4
|
HookError,
|
5
5
|
IncorrectUsage,
|
6
|
+
InfiniteRecursiveReference,
|
6
7
|
InternalError,
|
7
8
|
InvalidHeadersExample,
|
8
9
|
InvalidRateLimit,
|
@@ -19,11 +20,13 @@ from schemathesis.core.errors import (
|
|
19
20
|
SerializationNotPossible,
|
20
21
|
TransitionValidationError,
|
21
22
|
UnboundPrefix,
|
23
|
+
UnresolvableReference,
|
22
24
|
)
|
23
25
|
|
24
26
|
__all__ = [
|
25
27
|
"HookError",
|
26
28
|
"IncorrectUsage",
|
29
|
+
"InfiniteRecursiveReference",
|
27
30
|
"InternalError",
|
28
31
|
"InvalidHeadersExample",
|
29
32
|
"InvalidRateLimit",
|
@@ -40,4 +43,5 @@ __all__ = [
|
|
40
43
|
"SerializationNotPossible",
|
41
44
|
"TransitionValidationError",
|
42
45
|
"UnboundPrefix",
|
46
|
+
"UnresolvableReference",
|
43
47
|
]
|
schemathesis/filters.py
CHANGED
@@ -382,13 +382,13 @@ def expression_to_filter_function(expression: str) -> Callable[[HasAPIOperation]
|
|
382
382
|
if op == "==":
|
383
383
|
|
384
384
|
def filter_function(ctx: HasAPIOperation) -> bool:
|
385
|
-
definition = ctx.operation.definition.
|
385
|
+
definition = ctx.operation.definition.raw
|
386
386
|
resolved = resolve_pointer(definition, pointer)
|
387
387
|
return resolved == value
|
388
388
|
else:
|
389
389
|
|
390
390
|
def filter_function(ctx: HasAPIOperation) -> bool:
|
391
|
-
definition = ctx.operation.definition.
|
391
|
+
definition = ctx.operation.definition.raw
|
392
392
|
resolved = resolve_pointer(definition, pointer)
|
393
393
|
return resolved != value
|
394
394
|
|