schemathesis 3.35.4__py3-none-any.whl → 3.35.5__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 +5 -5
- schemathesis/_hypothesis.py +12 -6
- schemathesis/_override.py +4 -4
- schemathesis/auths.py +1 -1
- schemathesis/cli/__init__.py +19 -13
- schemathesis/cli/callbacks.py +6 -4
- schemathesis/cli/cassettes.py +67 -41
- schemathesis/cli/context.py +7 -6
- schemathesis/cli/junitxml.py +1 -1
- schemathesis/cli/options.py +7 -4
- schemathesis/cli/output/default.py +5 -5
- schemathesis/cli/reporting.py +4 -2
- schemathesis/code_samples.py +4 -3
- schemathesis/exceptions.py +4 -3
- schemathesis/extra/_flask.py +4 -1
- schemathesis/extra/pytest_plugin.py +6 -3
- schemathesis/failures.py +2 -1
- schemathesis/filters.py +2 -2
- schemathesis/generation/__init__.py +2 -2
- schemathesis/generation/_hypothesis.py +1 -1
- schemathesis/generation/coverage.py +5 -5
- schemathesis/graphql.py +0 -1
- schemathesis/hooks.py +3 -3
- schemathesis/lazy.py +10 -7
- schemathesis/loaders.py +3 -3
- schemathesis/models.py +39 -15
- schemathesis/runner/__init__.py +5 -5
- schemathesis/runner/events.py +1 -1
- schemathesis/runner/impl/context.py +58 -0
- schemathesis/runner/impl/core.py +54 -61
- schemathesis/runner/impl/solo.py +17 -20
- schemathesis/runner/impl/threadpool.py +65 -71
- schemathesis/runner/serialization.py +4 -3
- schemathesis/sanitization.py +2 -1
- schemathesis/schemas.py +18 -20
- schemathesis/serializers.py +2 -0
- schemathesis/service/client.py +1 -1
- schemathesis/service/events.py +4 -1
- schemathesis/service/extensions.py +2 -2
- schemathesis/service/hosts.py +4 -2
- schemathesis/service/models.py +3 -3
- schemathesis/service/report.py +3 -3
- schemathesis/service/serialization.py +4 -2
- schemathesis/specs/graphql/loaders.py +4 -3
- schemathesis/specs/graphql/schemas.py +4 -3
- schemathesis/specs/openapi/definitions.py +1 -5
- schemathesis/specs/openapi/examples.py +92 -2
- schemathesis/specs/openapi/expressions/__init__.py +7 -0
- schemathesis/specs/openapi/expressions/extractors.py +4 -1
- schemathesis/specs/openapi/expressions/nodes.py +5 -3
- schemathesis/specs/openapi/links.py +4 -4
- schemathesis/specs/openapi/loaders.py +5 -4
- schemathesis/specs/openapi/negative/__init__.py +5 -3
- schemathesis/specs/openapi/negative/mutations.py +5 -4
- schemathesis/specs/openapi/parameters.py +4 -2
- schemathesis/specs/openapi/schemas.py +9 -10
- schemathesis/specs/openapi/security.py +6 -4
- schemathesis/specs/openapi/stateful/__init__.py +2 -2
- schemathesis/specs/openapi/stateful/statistic.py +3 -3
- schemathesis/specs/openapi/stateful/types.py +3 -2
- schemathesis/stateful/__init__.py +3 -3
- schemathesis/stateful/config.py +1 -1
- schemathesis/stateful/context.py +3 -3
- schemathesis/stateful/events.py +3 -3
- schemathesis/stateful/runner.py +5 -4
- schemathesis/stateful/sink.py +1 -1
- schemathesis/stateful/state_machine.py +5 -5
- schemathesis/stateful/statistic.py +3 -1
- schemathesis/stateful/validation.py +1 -1
- schemathesis/transports/__init__.py +2 -2
- schemathesis/transports/asgi.py +7 -0
- schemathesis/transports/auth.py +2 -1
- schemathesis/transports/content_types.py +1 -1
- schemathesis/transports/responses.py +2 -1
- schemathesis/utils.py +4 -2
- {schemathesis-3.35.4.dist-info → schemathesis-3.35.5.dist-info}/METADATA +1 -1
- schemathesis-3.35.5.dist-info/RECORD +156 -0
- schemathesis-3.35.4.dist-info/RECORD +0 -154
- {schemathesis-3.35.4.dist-info → schemathesis-3.35.5.dist-info}/WHEEL +0 -0
- {schemathesis-3.35.4.dist-info → schemathesis-3.35.5.dist-info}/entry_points.txt +0 -0
- {schemathesis-3.35.4.dist-info → schemathesis-3.35.5.dist-info}/licenses/LICENSE +0 -0
|
@@ -4,10 +4,9 @@ from contextlib import suppress
|
|
|
4
4
|
from dataclasses import dataclass
|
|
5
5
|
from functools import lru_cache
|
|
6
6
|
from itertools import chain, cycle, islice
|
|
7
|
-
from typing import TYPE_CHECKING, Any, Generator, Union, cast
|
|
7
|
+
from typing import TYPE_CHECKING, Any, Generator, Iterator, Union, cast
|
|
8
8
|
|
|
9
9
|
import requests
|
|
10
|
-
from hypothesis.strategies import SearchStrategy
|
|
11
10
|
from hypothesis_jsonschema import from_schema
|
|
12
11
|
|
|
13
12
|
from ...constants import DEFAULT_RESPONSE_TIMEOUT
|
|
@@ -20,6 +19,8 @@ from .formats import STRING_FORMATS
|
|
|
20
19
|
from .parameters import OpenAPIBody, OpenAPIParameter
|
|
21
20
|
|
|
22
21
|
if TYPE_CHECKING:
|
|
22
|
+
from hypothesis.strategies import SearchStrategy
|
|
23
|
+
|
|
23
24
|
from ...generation import GenerationConfig
|
|
24
25
|
|
|
25
26
|
|
|
@@ -77,6 +78,7 @@ def get_strategies_from_examples(
|
|
|
77
78
|
|
|
78
79
|
def extract_top_level(operation: APIOperation[OpenAPIParameter, Case]) -> Generator[Example, None, None]:
|
|
79
80
|
"""Extract top-level parameter examples from `examples` & `example` fields."""
|
|
81
|
+
responses = find_in_responses(operation)
|
|
80
82
|
for parameter in operation.iter_parameters():
|
|
81
83
|
if "schema" in parameter.definition:
|
|
82
84
|
definitions = [parameter.definition, *_expand_subschemas(parameter.definition["schema"])]
|
|
@@ -106,6 +108,10 @@ def extract_top_level(operation: APIOperation[OpenAPIParameter, Case]) -> Genera
|
|
|
106
108
|
yield ParameterExample(
|
|
107
109
|
container=LOCATION_TO_CONTAINER[parameter.location], name=parameter.name, value=value
|
|
108
110
|
)
|
|
111
|
+
for value in find_matching_in_responses(responses, parameter.name):
|
|
112
|
+
yield ParameterExample(
|
|
113
|
+
container=LOCATION_TO_CONTAINER[parameter.location], name=parameter.name, value=value
|
|
114
|
+
)
|
|
109
115
|
for alternative in operation.body:
|
|
110
116
|
alternative = cast(OpenAPIBody, alternative)
|
|
111
117
|
if "schema" in alternative.definition:
|
|
@@ -349,3 +355,87 @@ def _produce_parameter_combinations(parameters: dict[str, dict[str, list]]) -> G
|
|
|
349
355
|
}
|
|
350
356
|
for container, variants in parameters.items()
|
|
351
357
|
}
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def find_in_responses(operation: APIOperation) -> dict[str, list[dict[str, Any]]]:
|
|
361
|
+
"""Find schema examples in responses."""
|
|
362
|
+
output: dict[str, list[dict[str, Any]]] = {}
|
|
363
|
+
for status_code, response in operation.definition.raw.get("responses", {}).items():
|
|
364
|
+
if not str(status_code).startswith("2"):
|
|
365
|
+
# Check only 2xx responses
|
|
366
|
+
continue
|
|
367
|
+
if isinstance(response, dict) and "$ref" in response:
|
|
368
|
+
_, response = operation.schema.resolver.resolve_in_scope(response, operation.definition.scope) # type:ignore[attr-defined]
|
|
369
|
+
for media_type, definition in response.get("content", {}).items():
|
|
370
|
+
schema_ref = definition.get("schema", {}).get("$ref")
|
|
371
|
+
if schema_ref:
|
|
372
|
+
name = schema_ref.split("/")[-1]
|
|
373
|
+
else:
|
|
374
|
+
name = f"{status_code}/{media_type}"
|
|
375
|
+
for examples_field, example_field in (
|
|
376
|
+
("examples", "example"),
|
|
377
|
+
("x-examples", "x-example"),
|
|
378
|
+
):
|
|
379
|
+
examples = definition.get(examples_field, {})
|
|
380
|
+
for example in examples.values():
|
|
381
|
+
if "value" in example:
|
|
382
|
+
output.setdefault(name, []).append(example["value"])
|
|
383
|
+
if example_field in definition:
|
|
384
|
+
output.setdefault(name, []).append(definition[example_field])
|
|
385
|
+
return output
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
NOT_FOUND = object()
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def find_matching_in_responses(examples: dict[str, list], param: str) -> Iterator[Any]:
|
|
392
|
+
"""Find matching parameter examples."""
|
|
393
|
+
normalized = param.lower()
|
|
394
|
+
is_id_param = normalized.endswith("id")
|
|
395
|
+
# Extract values from response examples that match input parameters.
|
|
396
|
+
# E.g., for `GET /orders/{id}/`, use "id" or "orderId" from `Order` response
|
|
397
|
+
# as examples for the "id" path parameter.
|
|
398
|
+
for schema_name, schema_examples in examples.items():
|
|
399
|
+
for example in schema_examples:
|
|
400
|
+
if not isinstance(example, dict):
|
|
401
|
+
continue
|
|
402
|
+
# Unwrapping example from `{"item": [{...}]}`
|
|
403
|
+
if isinstance(example, dict) and len(example) == 1 and list(example)[0].lower() == schema_name.lower():
|
|
404
|
+
inner = list(example.values())[0]
|
|
405
|
+
if isinstance(inner, list):
|
|
406
|
+
for sub_example in inner:
|
|
407
|
+
found = _find_matching_in_responses(sub_example, schema_name, param, normalized, is_id_param)
|
|
408
|
+
if found is not NOT_FOUND:
|
|
409
|
+
yield found
|
|
410
|
+
continue
|
|
411
|
+
example = inner
|
|
412
|
+
found = _find_matching_in_responses(example, schema_name, param, normalized, is_id_param)
|
|
413
|
+
if found is not NOT_FOUND:
|
|
414
|
+
yield found
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
def _find_matching_in_responses(
|
|
418
|
+
example: dict[str, Any], schema_name: str, param: str, normalized: str, is_id_param: bool
|
|
419
|
+
) -> Any:
|
|
420
|
+
# Check for exact match
|
|
421
|
+
if param in example:
|
|
422
|
+
return example[param]
|
|
423
|
+
|
|
424
|
+
# Check for case-insensitive match
|
|
425
|
+
for key in example:
|
|
426
|
+
if key.lower() == normalized:
|
|
427
|
+
return example[key]
|
|
428
|
+
else:
|
|
429
|
+
# If no match found and it's an ID parameter, try additional checks
|
|
430
|
+
if is_id_param:
|
|
431
|
+
# Check for 'id' if parameter is '{something}Id'
|
|
432
|
+
if "id" in example:
|
|
433
|
+
return example["id"]
|
|
434
|
+
# Check for '{schemaName}Id' or '{schemaName}_id'
|
|
435
|
+
if normalized == "id" or normalized.startswith(schema_name.lower()):
|
|
436
|
+
for key in (schema_name, schema_name.lower()):
|
|
437
|
+
for suffix in ("_id", "Id"):
|
|
438
|
+
with_suffix = f"{key}{suffix}"
|
|
439
|
+
if with_suffix in example:
|
|
440
|
+
return example[with_suffix]
|
|
441
|
+
return NOT_FOUND
|
|
@@ -11,6 +11,13 @@ from typing import Any
|
|
|
11
11
|
from . import lexer, nodes, parser
|
|
12
12
|
from .context import ExpressionContext
|
|
13
13
|
|
|
14
|
+
__all__ = [
|
|
15
|
+
"lexer",
|
|
16
|
+
"nodes",
|
|
17
|
+
"parser",
|
|
18
|
+
"ExpressionContext",
|
|
19
|
+
]
|
|
20
|
+
|
|
14
21
|
|
|
15
22
|
def evaluate(expr: Any, context: ExpressionContext, evaluate_nested: bool = False) -> Any:
|
|
16
23
|
"""Evaluate runtime expression in context."""
|
|
@@ -4,13 +4,15 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
from dataclasses import dataclass
|
|
6
6
|
from enum import Enum, unique
|
|
7
|
-
from typing import Any
|
|
7
|
+
from typing import TYPE_CHECKING, Any
|
|
8
8
|
|
|
9
9
|
from requests.structures import CaseInsensitiveDict
|
|
10
10
|
|
|
11
11
|
from .. import references
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from .context import ExpressionContext
|
|
15
|
+
from .extractors import Extractor
|
|
14
16
|
|
|
15
17
|
|
|
16
18
|
@dataclass
|
|
@@ -10,22 +10,22 @@ from difflib import get_close_matches
|
|
|
10
10
|
from types import SimpleNamespace
|
|
11
11
|
from typing import TYPE_CHECKING, Any, Generator, Literal, NoReturn, Sequence, TypedDict, Union, cast
|
|
12
12
|
|
|
13
|
-
from jsonschema import RefResolver
|
|
14
|
-
|
|
15
13
|
from ...constants import NOT_SET
|
|
16
14
|
from ...internal.copy import fast_deepcopy
|
|
17
15
|
from ...models import APIOperation, Case, TransitionId
|
|
18
|
-
from ...parameters import ParameterSet
|
|
19
16
|
from ...stateful import ParsedData, StatefulTest, UnresolvableLink
|
|
20
17
|
from ...stateful.state_machine import Direction
|
|
21
|
-
from ...types import NotSet
|
|
22
18
|
from . import expressions
|
|
23
19
|
from .constants import LOCATION_TO_CONTAINER
|
|
24
20
|
from .parameters import OpenAPI20Body, OpenAPI30Body, OpenAPIParameter
|
|
25
21
|
from .references import RECURSION_DEPTH_LIMIT, Unresolvable
|
|
26
22
|
|
|
27
23
|
if TYPE_CHECKING:
|
|
24
|
+
from jsonschema import RefResolver
|
|
25
|
+
|
|
26
|
+
from ...parameters import ParameterSet
|
|
28
27
|
from ...transports.responses import GenericResponse
|
|
28
|
+
from ...types import NotSet
|
|
29
29
|
|
|
30
30
|
|
|
31
31
|
@dataclass(repr=False)
|
|
@@ -9,7 +9,7 @@ from urllib.parse import urljoin
|
|
|
9
9
|
|
|
10
10
|
from ... import experimental, fixups
|
|
11
11
|
from ...code_samples import CodeSampleStyle
|
|
12
|
-
from ...constants import NOT_SET, WAIT_FOR_SCHEMA_INTERVAL
|
|
12
|
+
from ...constants import DEFAULT_RESPONSE_TIMEOUT, NOT_SET, WAIT_FOR_SCHEMA_INTERVAL
|
|
13
13
|
from ...exceptions import SchemaError, SchemaErrorType
|
|
14
14
|
from ...filters import filter_set_from_components
|
|
15
15
|
from ...generation import (
|
|
@@ -163,6 +163,7 @@ def from_uri(
|
|
|
163
163
|
interval=WAIT_FOR_SCHEMA_INTERVAL,
|
|
164
164
|
)
|
|
165
165
|
def _load_schema(_uri: str, **_kwargs: Any) -> requests.Response:
|
|
166
|
+
_kwargs.setdefault("timeout", DEFAULT_RESPONSE_TIMEOUT / 1000)
|
|
166
167
|
return requests.get(_uri, **kwargs)
|
|
167
168
|
|
|
168
169
|
else:
|
|
@@ -441,7 +442,7 @@ def _format_status_codes(status_codes: list[tuple[int, list[str | int]]]) -> str
|
|
|
441
442
|
for status_code, path in status_codes:
|
|
442
443
|
buffer.write(f" - {status_code} at schema['paths']")
|
|
443
444
|
for chunk in path:
|
|
444
|
-
buffer.write(f"[{
|
|
445
|
+
buffer.write(f"[{chunk!r}]")
|
|
445
446
|
buffer.write("['responses']\n")
|
|
446
447
|
return buffer.getvalue().rstrip()
|
|
447
448
|
|
|
@@ -595,9 +596,9 @@ def from_wsgi(
|
|
|
595
596
|
|
|
596
597
|
|
|
597
598
|
def get_loader_for_app(app: Any) -> Callable:
|
|
598
|
-
from
|
|
599
|
+
from ...transports.asgi import is_asgi_app
|
|
599
600
|
|
|
600
|
-
if
|
|
601
|
+
if is_asgi_app(app):
|
|
601
602
|
return from_asgi
|
|
602
603
|
if app.__class__.__module__.startswith("aiohttp."):
|
|
603
604
|
return from_aiohttp
|
|
@@ -2,17 +2,19 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
from dataclasses import dataclass
|
|
4
4
|
from functools import lru_cache
|
|
5
|
-
from typing import Any
|
|
5
|
+
from typing import TYPE_CHECKING, Any
|
|
6
6
|
from urllib.parse import urlencode
|
|
7
7
|
|
|
8
8
|
import jsonschema
|
|
9
9
|
from hypothesis import strategies as st
|
|
10
10
|
from hypothesis_jsonschema import from_schema
|
|
11
11
|
|
|
12
|
-
from ....generation import GenerationConfig
|
|
13
12
|
from ..constants import ALL_KEYWORDS
|
|
14
13
|
from .mutations import MutationContext
|
|
15
|
-
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from ....generation import GenerationConfig
|
|
17
|
+
from .types import Draw, Schema
|
|
16
18
|
|
|
17
19
|
|
|
18
20
|
@dataclass
|
|
@@ -179,7 +179,7 @@ def remove_required_property(context: MutationContext, draw: Draw, schema: Schem
|
|
|
179
179
|
else:
|
|
180
180
|
candidate = draw(st.sampled_from(sorted(required)))
|
|
181
181
|
enabled_properties = draw(st.shared(FeatureStrategy(), key="properties")) # type: ignore
|
|
182
|
-
candidates = [candidate
|
|
182
|
+
candidates = [candidate, *sorted([prop for prop in required if enabled_properties.is_enabled(prop)])]
|
|
183
183
|
property_name = draw(st.sampled_from(candidates))
|
|
184
184
|
required.remove(property_name)
|
|
185
185
|
if not required:
|
|
@@ -226,9 +226,10 @@ def change_type(context: MutationContext, draw: Draw, schema: Schema) -> Mutatio
|
|
|
226
226
|
candidate = draw(st.sampled_from(sorted(candidates)))
|
|
227
227
|
candidates.remove(candidate)
|
|
228
228
|
enabled_types = draw(st.shared(FeatureStrategy(), key="types")) # type: ignore
|
|
229
|
-
remaining_candidates = [
|
|
230
|
-
|
|
231
|
-
|
|
229
|
+
remaining_candidates = [
|
|
230
|
+
candidate,
|
|
231
|
+
*sorted([candidate for candidate in candidates if enabled_types.is_enabled(candidate)]),
|
|
232
|
+
]
|
|
232
233
|
new_type = draw(st.sampled_from(remaining_candidates))
|
|
233
234
|
schema["type"] = new_type
|
|
234
235
|
prevent_unsatisfiable_schema(schema, new_type)
|
|
@@ -2,13 +2,15 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
4
|
from dataclasses import dataclass
|
|
5
|
-
from typing import Any, ClassVar, Iterable
|
|
5
|
+
from typing import TYPE_CHECKING, Any, ClassVar, Iterable
|
|
6
6
|
|
|
7
7
|
from ...exceptions import OperationSchemaError
|
|
8
|
-
from ...models import APIOperation
|
|
9
8
|
from ...parameters import Parameter
|
|
10
9
|
from .converter import to_json_schema_recursive
|
|
11
10
|
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from ...models import APIOperation
|
|
13
|
+
|
|
12
14
|
|
|
13
15
|
@dataclass(eq=False)
|
|
14
16
|
class OpenAPIParameter(Parameter):
|
|
@@ -20,21 +20,18 @@ from typing import (
|
|
|
20
20
|
Mapping,
|
|
21
21
|
NoReturn,
|
|
22
22
|
Sequence,
|
|
23
|
-
Type,
|
|
24
23
|
TypeVar,
|
|
25
24
|
cast,
|
|
26
25
|
)
|
|
27
26
|
from urllib.parse import urlsplit
|
|
28
27
|
|
|
29
28
|
import jsonschema
|
|
30
|
-
from hypothesis.strategies import SearchStrategy
|
|
31
29
|
from packaging import version
|
|
32
30
|
from requests.structures import CaseInsensitiveDict
|
|
33
31
|
|
|
34
32
|
from ... import experimental, failures
|
|
35
33
|
from ..._compat import MultipleFailures
|
|
36
34
|
from ..._override import CaseOverride, check_no_override_mark, set_override_mark
|
|
37
|
-
from ...auths import AuthStorage
|
|
38
35
|
from ...constants import HTTP_METHODS, NOT_SET
|
|
39
36
|
from ...exceptions import (
|
|
40
37
|
InternalError,
|
|
@@ -54,10 +51,8 @@ from ...internal.result import Err, Ok, Result
|
|
|
54
51
|
from ...models import APIOperation, Case, OperationDefinition
|
|
55
52
|
from ...schemas import APIOperationMap, BaseSchema
|
|
56
53
|
from ...stateful import Stateful, StatefulTest
|
|
57
|
-
from ...stateful.state_machine import APIStateMachine
|
|
58
54
|
from ...transports.content_types import is_json_media_type, parse_content_type
|
|
59
55
|
from ...transports.responses import get_json
|
|
60
|
-
from ...types import Body, Cookies, FormData, GenericTest, Headers, NotSet, PathParameters, Query
|
|
61
56
|
from . import links, serialization
|
|
62
57
|
from ._cache import OperationCache
|
|
63
58
|
from ._hypothesis import get_case_strategy
|
|
@@ -83,7 +78,12 @@ from .security import BaseSecurityProcessor, OpenAPISecurityProcessor, SwaggerSe
|
|
|
83
78
|
from .stateful import create_state_machine
|
|
84
79
|
|
|
85
80
|
if TYPE_CHECKING:
|
|
81
|
+
from hypothesis.strategies import SearchStrategy
|
|
82
|
+
|
|
83
|
+
from ...auths import AuthStorage
|
|
84
|
+
from ...stateful.state_machine import APIStateMachine
|
|
86
85
|
from ...transports.responses import GenericResponse
|
|
86
|
+
from ...types import Body, Cookies, FormData, GenericTest, Headers, NotSet, PathParameters, Query
|
|
87
87
|
|
|
88
88
|
SCHEMA_ERROR_MESSAGE = "Ensure that the definition complies with the OpenAPI specification"
|
|
89
89
|
SCHEMA_PARSING_ERRORS = (KeyError, AttributeError, jsonschema.exceptions.RefResolutionError)
|
|
@@ -626,7 +626,7 @@ class BaseOpenAPISchema(BaseSchema):
|
|
|
626
626
|
return operation.definition.raw.get("tags")
|
|
627
627
|
|
|
628
628
|
@property
|
|
629
|
-
def validator_cls(self) ->
|
|
629
|
+
def validator_cls(self) -> type[jsonschema.Validator]:
|
|
630
630
|
if self.spec_version.startswith("3.1") and experimental.OPEN_API_3_1.is_enabled:
|
|
631
631
|
return jsonschema.Draft202012Validator
|
|
632
632
|
return jsonschema.Draft4Validator
|
|
@@ -791,11 +791,10 @@ class BaseOpenAPISchema(BaseSchema):
|
|
|
791
791
|
|
|
792
792
|
def _maybe_raise_one_or_more(errors: list[Exception]) -> None:
|
|
793
793
|
if not errors:
|
|
794
|
-
return
|
|
795
|
-
|
|
794
|
+
return
|
|
795
|
+
if len(errors) == 1:
|
|
796
796
|
raise errors[0]
|
|
797
|
-
|
|
798
|
-
raise MultipleFailures("\n\n".join(str(error) for error in errors), errors)
|
|
797
|
+
raise MultipleFailures("\n\n".join(str(error) for error in errors), errors)
|
|
799
798
|
|
|
800
799
|
|
|
801
800
|
def _make_reference_key(scopes: list[str], reference: str) -> str:
|
|
@@ -3,13 +3,15 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
from dataclasses import dataclass
|
|
6
|
-
from typing import Any, ClassVar, Generator
|
|
6
|
+
from typing import TYPE_CHECKING, Any, ClassVar, Generator
|
|
7
7
|
|
|
8
|
-
from jsonschema import RefResolver
|
|
9
|
-
|
|
10
|
-
from ...models import APIOperation
|
|
11
8
|
from .parameters import OpenAPI20Parameter, OpenAPI30Parameter, OpenAPIParameter
|
|
12
9
|
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from jsonschema import RefResolver
|
|
12
|
+
|
|
13
|
+
from ...models import APIOperation
|
|
14
|
+
|
|
13
15
|
|
|
14
16
|
@dataclass
|
|
15
17
|
class BaseSecurityProcessor:
|
|
@@ -16,11 +16,11 @@ from .. import expressions
|
|
|
16
16
|
from ..links import get_all_links
|
|
17
17
|
from ..utils import expand_status_code
|
|
18
18
|
from .statistic import OpenAPILinkStats
|
|
19
|
-
from .types import FilterFunction, LinkName, StatusCode, TargetName
|
|
20
19
|
|
|
21
20
|
if TYPE_CHECKING:
|
|
22
21
|
from ....models import Case
|
|
23
22
|
from ..schemas import BaseOpenAPISchema
|
|
23
|
+
from .types import FilterFunction, LinkName, StatusCode, TargetName
|
|
24
24
|
|
|
25
25
|
|
|
26
26
|
class OpenAPIStateMachine(APIStateMachine):
|
|
@@ -193,7 +193,7 @@ def make_response_matcher(matchers: list[tuple[str, FilterFunction]]) -> Callabl
|
|
|
193
193
|
return compare
|
|
194
194
|
|
|
195
195
|
|
|
196
|
-
@lru_cache
|
|
196
|
+
@lru_cache
|
|
197
197
|
def make_response_filter(status_code: str, all_status_codes: Iterator[str]) -> FilterFunction:
|
|
198
198
|
"""Create a filter for stored responses.
|
|
199
199
|
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from dataclasses import dataclass, field
|
|
4
|
-
from typing import TYPE_CHECKING, Iterator,
|
|
4
|
+
from typing import TYPE_CHECKING, Iterator, Union
|
|
5
5
|
|
|
6
6
|
from ....internal.copy import fast_deepcopy
|
|
7
7
|
from ....stateful.statistic import TransitionStats
|
|
8
|
-
from .types import AggregatedResponseCounter, LinkName, ResponseCounter, SourceName, StatusCode, TargetName
|
|
9
8
|
|
|
10
9
|
if TYPE_CHECKING:
|
|
11
10
|
from ....stateful import events
|
|
11
|
+
from .types import AggregatedResponseCounter, LinkName, ResponseCounter, SourceName, StatusCode, TargetName
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
@dataclass
|
|
@@ -136,7 +136,7 @@ class OpenAPILinkStats(TransitionStats):
|
|
|
136
136
|
def to_formatted_table(self, width: int) -> str:
|
|
137
137
|
"""Format the statistic as a table."""
|
|
138
138
|
entries = list(self.iter_with_format())
|
|
139
|
-
lines:
|
|
139
|
+
lines: list[str | list[str]] = [HEADER, ""]
|
|
140
140
|
column_widths = [len(column) for column in HEADER]
|
|
141
141
|
for entry in entries:
|
|
142
142
|
if isinstance(entry.entry, Link):
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from typing import Callable, Dict, TypedDict, Union
|
|
3
|
+
from typing import TYPE_CHECKING, Callable, Dict, TypedDict, Union
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
if TYPE_CHECKING:
|
|
6
|
+
from ....stateful.state_machine import StepResult
|
|
6
7
|
|
|
7
8
|
StatusCode = str
|
|
8
9
|
LinkName = str
|
|
@@ -5,15 +5,15 @@ import json
|
|
|
5
5
|
from dataclasses import dataclass, field
|
|
6
6
|
from typing import TYPE_CHECKING, Any, Callable, Generator
|
|
7
7
|
|
|
8
|
-
from .. import GenerationConfig
|
|
9
8
|
from ..constants import NOT_SET
|
|
10
|
-
from ..exceptions import OperationSchemaError
|
|
11
9
|
from ..internal.result import Ok, Result
|
|
12
|
-
from ..models import APIOperation, Case
|
|
13
10
|
|
|
14
11
|
if TYPE_CHECKING:
|
|
15
12
|
import hypothesis
|
|
16
13
|
|
|
14
|
+
from .. import GenerationConfig
|
|
15
|
+
from ..exceptions import OperationSchemaError
|
|
16
|
+
from ..models import APIOperation, Case
|
|
17
17
|
from ..transports.responses import GenericResponse
|
|
18
18
|
from .state_machine import APIStateMachine
|
|
19
19
|
|
schemathesis/stateful/config.py
CHANGED
|
@@ -21,7 +21,7 @@ def _default_checks_factory() -> tuple[CheckFunction, ...]:
|
|
|
21
21
|
from ..checks import ALL_CHECKS
|
|
22
22
|
from ..specs.openapi.checks import ensure_resource_availability, use_after_free
|
|
23
23
|
|
|
24
|
-
return ALL_CHECKS
|
|
24
|
+
return (*ALL_CHECKS, use_after_free, ensure_resource_availability)
|
|
25
25
|
|
|
26
26
|
|
|
27
27
|
def _get_default_hypothesis_settings_kwargs() -> dict[str, Any]:
|
schemathesis/stateful/context.py
CHANGED
|
@@ -57,11 +57,11 @@ class RunnerContext:
|
|
|
57
57
|
def current_scenario_status(self) -> events.ScenarioStatus:
|
|
58
58
|
if self.current_step_status == events.StepStatus.SUCCESS:
|
|
59
59
|
return events.ScenarioStatus.SUCCESS
|
|
60
|
-
|
|
60
|
+
if self.current_step_status == events.StepStatus.FAILURE:
|
|
61
61
|
return events.ScenarioStatus.FAILURE
|
|
62
|
-
|
|
62
|
+
if self.current_step_status == events.StepStatus.ERROR:
|
|
63
63
|
return events.ScenarioStatus.ERROR
|
|
64
|
-
|
|
64
|
+
if self.current_step_status == events.StepStatus.INTERRUPTED:
|
|
65
65
|
return events.ScenarioStatus.INTERRUPTED
|
|
66
66
|
return events.ScenarioStatus.REJECTED
|
|
67
67
|
|
schemathesis/stateful/events.py
CHANGED
|
@@ -4,7 +4,7 @@ import time
|
|
|
4
4
|
from dataclasses import asdict as _asdict
|
|
5
5
|
from dataclasses import dataclass
|
|
6
6
|
from enum import Enum
|
|
7
|
-
from typing import TYPE_CHECKING, Any
|
|
7
|
+
from typing import TYPE_CHECKING, Any
|
|
8
8
|
|
|
9
9
|
from ..exceptions import format_exception
|
|
10
10
|
|
|
@@ -40,11 +40,11 @@ class RunStarted(StatefulEvent):
|
|
|
40
40
|
"""Before executing all scenarios."""
|
|
41
41
|
|
|
42
42
|
started_at: float
|
|
43
|
-
state_machine:
|
|
43
|
+
state_machine: type[APIStateMachine]
|
|
44
44
|
|
|
45
45
|
__slots__ = ("state_machine", "timestamp", "started_at")
|
|
46
46
|
|
|
47
|
-
def __init__(self, *, state_machine:
|
|
47
|
+
def __init__(self, *, state_machine: type[APIStateMachine]) -> None:
|
|
48
48
|
self.state_machine = state_machine
|
|
49
49
|
self.started_at = time.time()
|
|
50
50
|
self.timestamp = time.monotonic()
|
schemathesis/stateful/runner.py
CHANGED
|
@@ -4,13 +4,12 @@ import queue
|
|
|
4
4
|
import threading
|
|
5
5
|
from contextlib import contextmanager
|
|
6
6
|
from dataclasses import dataclass, field
|
|
7
|
-
from typing import TYPE_CHECKING, Any, Generator, Iterator
|
|
7
|
+
from typing import TYPE_CHECKING, Any, Generator, Iterator
|
|
8
8
|
|
|
9
9
|
import hypothesis
|
|
10
10
|
import requests
|
|
11
11
|
from hypothesis.control import current_build_context
|
|
12
12
|
from hypothesis.errors import Flaky, Unsatisfiable
|
|
13
|
-
from hypothesis.stateful import Rule
|
|
14
13
|
|
|
15
14
|
from ..exceptions import CheckFailed
|
|
16
15
|
from ..targets import TargetMetricCollector
|
|
@@ -20,6 +19,8 @@ from .context import RunnerContext
|
|
|
20
19
|
from .validation import validate_response
|
|
21
20
|
|
|
22
21
|
if TYPE_CHECKING:
|
|
22
|
+
from hypothesis.stateful import Rule
|
|
23
|
+
|
|
23
24
|
from ..models import Case, CheckFunction
|
|
24
25
|
from ..transports.responses import GenericResponse
|
|
25
26
|
from .state_machine import APIStateMachine, Direction, StepResult
|
|
@@ -36,7 +37,7 @@ class StatefulTestRunner:
|
|
|
36
37
|
"""
|
|
37
38
|
|
|
38
39
|
# State machine class to use
|
|
39
|
-
state_machine:
|
|
40
|
+
state_machine: type[APIStateMachine]
|
|
40
41
|
# Test runner configuration that defines the runtime behavior
|
|
41
42
|
config: StatefulTestRunnerConfig = field(default_factory=StatefulTestRunnerConfig)
|
|
42
43
|
# Event to stop the execution
|
|
@@ -105,7 +106,7 @@ def thread_manager(thread: threading.Thread) -> Generator[None, None, None]:
|
|
|
105
106
|
|
|
106
107
|
def _execute_state_machine_loop(
|
|
107
108
|
*,
|
|
108
|
-
state_machine:
|
|
109
|
+
state_machine: type[APIStateMachine],
|
|
109
110
|
event_queue: queue.Queue,
|
|
110
111
|
config: StatefulTestRunnerConfig,
|
|
111
112
|
stop_event: threading.Event,
|
schemathesis/stateful/sink.py
CHANGED
|
@@ -4,7 +4,7 @@ import re
|
|
|
4
4
|
import time
|
|
5
5
|
from dataclasses import dataclass
|
|
6
6
|
from functools import lru_cache
|
|
7
|
-
from typing import TYPE_CHECKING, Any, ClassVar
|
|
7
|
+
from typing import TYPE_CHECKING, Any, ClassVar
|
|
8
8
|
|
|
9
9
|
from hypothesis.errors import InvalidDefinition
|
|
10
10
|
from hypothesis.stateful import RuleBasedStateMachine
|
|
@@ -16,7 +16,6 @@ from ..models import APIOperation, Case, CheckFunction
|
|
|
16
16
|
from .config import _default_hypothesis_settings_factory
|
|
17
17
|
from .runner import StatefulTestRunner, StatefulTestRunnerConfig
|
|
18
18
|
from .sink import StateMachineSink
|
|
19
|
-
from .statistic import TransitionStats
|
|
20
19
|
|
|
21
20
|
if TYPE_CHECKING:
|
|
22
21
|
import hypothesis
|
|
@@ -24,6 +23,7 @@ if TYPE_CHECKING:
|
|
|
24
23
|
|
|
25
24
|
from ..schemas import BaseSchema
|
|
26
25
|
from ..transports.responses import GenericResponse
|
|
26
|
+
from .statistic import TransitionStats
|
|
27
27
|
|
|
28
28
|
|
|
29
29
|
@dataclass
|
|
@@ -64,7 +64,7 @@ class APIStateMachine(RuleBasedStateMachine):
|
|
|
64
64
|
|
|
65
65
|
@classmethod
|
|
66
66
|
@lru_cache
|
|
67
|
-
def _to_test_case(cls) ->
|
|
67
|
+
def _to_test_case(cls) -> type:
|
|
68
68
|
from . import run_state_machine_as_test
|
|
69
69
|
|
|
70
70
|
class StateMachineTestCase(RuleBasedStateMachine.TestCase):
|
|
@@ -97,7 +97,7 @@ class APIStateMachine(RuleBasedStateMachine):
|
|
|
97
97
|
|
|
98
98
|
def _add_result_to_targets(self, targets: tuple[str, ...], result: StepResult | None) -> None:
|
|
99
99
|
if result is None:
|
|
100
|
-
return
|
|
100
|
+
return
|
|
101
101
|
target = self._get_target_for_result(result)
|
|
102
102
|
if target is not None:
|
|
103
103
|
super()._add_result_to_targets((target,), result)
|
|
@@ -310,7 +310,7 @@ def _print_case(case: Case, kwargs: dict[str, Any]) -> str:
|
|
|
310
310
|
headers.update(kwargs.get("headers", {}))
|
|
311
311
|
case.headers = headers
|
|
312
312
|
data = [
|
|
313
|
-
f"{name}={
|
|
313
|
+
f"{name}={getattr(case, name)!r}"
|
|
314
314
|
for name in ("path_parameters", "headers", "cookies", "query", "body", "media_type")
|
|
315
315
|
if getattr(case, name) not in (None, NOT_SET)
|
|
316
316
|
]
|
|
@@ -3,12 +3,12 @@ from __future__ import annotations
|
|
|
3
3
|
from typing import TYPE_CHECKING
|
|
4
4
|
|
|
5
5
|
from ..exceptions import CheckFailed, get_grouped_exception
|
|
6
|
-
from .context import RunnerContext
|
|
7
6
|
|
|
8
7
|
if TYPE_CHECKING:
|
|
9
8
|
from ..failures import FailureContext
|
|
10
9
|
from ..models import Case, CheckFunction
|
|
11
10
|
from ..transports.responses import GenericResponse
|
|
11
|
+
from .context import RunnerContext
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
def validate_response(
|