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.
Files changed (70) hide show
  1. schemathesis/cli/commands/run/executor.py +1 -1
  2. schemathesis/cli/commands/run/handlers/base.py +28 -1
  3. schemathesis/cli/commands/run/handlers/cassettes.py +10 -12
  4. schemathesis/cli/commands/run/handlers/junitxml.py +5 -6
  5. schemathesis/cli/commands/run/handlers/output.py +7 -1
  6. schemathesis/cli/ext/fs.py +1 -1
  7. schemathesis/config/_diff_base.py +3 -1
  8. schemathesis/config/_operations.py +2 -0
  9. schemathesis/config/_phases.py +21 -4
  10. schemathesis/config/_projects.py +10 -2
  11. schemathesis/core/adapter.py +34 -0
  12. schemathesis/core/errors.py +29 -5
  13. schemathesis/core/jsonschema/__init__.py +13 -0
  14. schemathesis/core/jsonschema/bundler.py +163 -0
  15. schemathesis/{specs/openapi/constants.py → core/jsonschema/keywords.py} +0 -8
  16. schemathesis/core/jsonschema/references.py +122 -0
  17. schemathesis/core/jsonschema/types.py +41 -0
  18. schemathesis/core/media_types.py +6 -4
  19. schemathesis/core/parameters.py +37 -0
  20. schemathesis/core/transforms.py +25 -2
  21. schemathesis/core/validation.py +19 -0
  22. schemathesis/engine/context.py +1 -1
  23. schemathesis/engine/errors.py +11 -18
  24. schemathesis/engine/phases/stateful/_executor.py +1 -1
  25. schemathesis/engine/phases/unit/_executor.py +30 -13
  26. schemathesis/errors.py +4 -0
  27. schemathesis/filters.py +2 -2
  28. schemathesis/generation/coverage.py +87 -11
  29. schemathesis/generation/hypothesis/__init__.py +4 -1
  30. schemathesis/generation/hypothesis/builder.py +108 -70
  31. schemathesis/generation/meta.py +5 -14
  32. schemathesis/generation/overrides.py +17 -17
  33. schemathesis/pytest/lazy.py +1 -1
  34. schemathesis/pytest/plugin.py +1 -6
  35. schemathesis/schemas.py +22 -72
  36. schemathesis/specs/graphql/schemas.py +27 -16
  37. schemathesis/specs/openapi/_hypothesis.py +83 -68
  38. schemathesis/specs/openapi/adapter/__init__.py +10 -0
  39. schemathesis/specs/openapi/adapter/parameters.py +504 -0
  40. schemathesis/specs/openapi/adapter/protocol.py +57 -0
  41. schemathesis/specs/openapi/adapter/references.py +19 -0
  42. schemathesis/specs/openapi/adapter/responses.py +329 -0
  43. schemathesis/specs/openapi/adapter/security.py +141 -0
  44. schemathesis/specs/openapi/adapter/v2.py +28 -0
  45. schemathesis/specs/openapi/adapter/v3_0.py +28 -0
  46. schemathesis/specs/openapi/adapter/v3_1.py +28 -0
  47. schemathesis/specs/openapi/checks.py +99 -90
  48. schemathesis/specs/openapi/converter.py +114 -27
  49. schemathesis/specs/openapi/examples.py +210 -168
  50. schemathesis/specs/openapi/negative/__init__.py +12 -7
  51. schemathesis/specs/openapi/negative/mutations.py +68 -40
  52. schemathesis/specs/openapi/references.py +2 -175
  53. schemathesis/specs/openapi/schemas.py +142 -490
  54. schemathesis/specs/openapi/serialization.py +15 -7
  55. schemathesis/specs/openapi/stateful/__init__.py +17 -12
  56. schemathesis/specs/openapi/stateful/inference.py +13 -11
  57. schemathesis/specs/openapi/stateful/links.py +5 -20
  58. schemathesis/specs/openapi/types/__init__.py +3 -0
  59. schemathesis/specs/openapi/types/v3.py +68 -0
  60. schemathesis/specs/openapi/utils.py +1 -13
  61. schemathesis/transport/requests.py +3 -11
  62. schemathesis/transport/serialization.py +63 -27
  63. schemathesis/transport/wsgi.py +1 -8
  64. {schemathesis-4.1.4.dist-info → schemathesis-4.2.0.dist-info}/METADATA +2 -2
  65. {schemathesis-4.1.4.dist-info → schemathesis-4.2.0.dist-info}/RECORD +68 -53
  66. schemathesis/specs/openapi/parameters.py +0 -405
  67. schemathesis/specs/openapi/security.py +0 -162
  68. {schemathesis-4.1.4.dist-info → schemathesis-4.2.0.dist-info}/WHEEL +0 -0
  69. {schemathesis-4.1.4.dist-info → schemathesis-4.2.0.dist-info}/entry_points.txt +0 -0
  70. {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__
@@ -1,10 +1,12 @@
1
+ from __future__ import annotations
2
+
1
3
  from functools import lru_cache
2
- from typing import Generator, Tuple
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, None, None]:
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) -> Tuple[str, dict]:
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) -> Tuple[str, 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])
@@ -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
- {k2: deepclone(v2) for k2, v2 in v1.items()}
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)
@@ -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
 
@@ -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.definition.raw, entries)
98
+ injected += inferencer.inject_links(operation.responses, entries)
99
99
 
100
100
  return injected
101
101
 
@@ -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
- RECURSIVE_REFERENCE_ERROR_MESSAGE,
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", "UnsupportedRecursiveReference", "UnexpectedError"]
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.SCHEMA_UNSUPPORTED,
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, UnsupportedRecursiveReference):
372
- # Recursive references are not supported right now
373
- return RuntimeErrorKind.SCHEMA_UNSUPPORTED
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
- msg = "Unexpected error during testing of this API operation"
177
- exc_msg = str(exc)
178
- if exc_msg:
179
- msg += f": {exc_msg}"
180
- try:
181
- raise InternalError(msg) from exc
182
- except InternalError as exc:
183
- yield non_fatal_error(exc)
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.resolved
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.resolved
391
+ definition = ctx.operation.definition.raw
392
392
  resolved = resolve_pointer(definition, pointer)
393
393
  return resolved != value
394
394