schemathesis 3.13.0__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 -1016
- 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 +683 -247
- schemathesis/specs/graphql/__init__.py +0 -1
- schemathesis/specs/graphql/nodes.py +27 -0
- schemathesis/specs/graphql/scalars.py +86 -0
- schemathesis/specs/graphql/schemas.py +395 -123
- schemathesis/specs/graphql/validation.py +33 -0
- schemathesis/specs/openapi/__init__.py +9 -1
- schemathesis/specs/openapi/_hypothesis.py +578 -317
- 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 +753 -74
- 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 +117 -68
- schemathesis/specs/openapi/negative/mutations.py +294 -104
- schemathesis/specs/openapi/negative/utils.py +3 -6
- schemathesis/specs/openapi/patterns.py +458 -0
- schemathesis/specs/openapi/references.py +60 -81
- schemathesis/specs/openapi/schemas.py +648 -650
- schemathesis/specs/openapi/serialization.py +53 -30
- schemathesis/specs/openapi/stateful/__init__.py +404 -69
- 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.13.0.dist-info → schemathesis-4.4.2.dist-info}/WHEEL +1 -1
- schemathesis-4.4.2.dist-info/entry_points.txt +6 -0
- {schemathesis-3.13.0.dist-info → schemathesis-4.4.2.dist-info/licenses}/LICENSE +1 -1
- schemathesis/_compat.py +0 -41
- schemathesis/_hypothesis.py +0 -115
- schemathesis/cli/callbacks.py +0 -188
- schemathesis/cli/cassettes.py +0 -253
- schemathesis/cli/context.py +0 -36
- schemathesis/cli/debug.py +0 -21
- schemathesis/cli/handlers.py +0 -11
- schemathesis/cli/junitxml.py +0 -41
- schemathesis/cli/options.py +0 -51
- schemathesis/cli/output/__init__.py +0 -1
- schemathesis/cli/output/default.py +0 -508
- schemathesis/cli/output/short.py +0 -40
- schemathesis/constants.py +0 -79
- schemathesis/exceptions.py +0 -207
- schemathesis/extra/_aiohttp.py +0 -27
- schemathesis/extra/_flask.py +0 -10
- schemathesis/extra/_server.py +0 -16
- schemathesis/extra/pytest_plugin.py +0 -216
- schemathesis/failures.py +0 -131
- schemathesis/fixups/__init__.py +0 -29
- schemathesis/fixups/fast_api.py +0 -30
- schemathesis/lazy.py +0 -227
- schemathesis/models.py +0 -1041
- schemathesis/parameters.py +0 -88
- schemathesis/runner/__init__.py +0 -460
- schemathesis/runner/events.py +0 -240
- schemathesis/runner/impl/__init__.py +0 -3
- schemathesis/runner/impl/core.py +0 -755
- schemathesis/runner/impl/solo.py +0 -85
- schemathesis/runner/impl/threadpool.py +0 -367
- schemathesis/runner/serialization.py +0 -189
- schemathesis/serializers.py +0 -233
- schemathesis/service/__init__.py +0 -3
- schemathesis/service/client.py +0 -46
- schemathesis/service/constants.py +0 -12
- schemathesis/service/events.py +0 -39
- schemathesis/service/handler.py +0 -39
- schemathesis/service/models.py +0 -7
- schemathesis/service/serialization.py +0 -153
- schemathesis/service/worker.py +0 -40
- 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 -302
- schemathesis/specs/openapi/loaders.py +0 -453
- schemathesis/specs/openapi/parameters.py +0 -413
- schemathesis/specs/openapi/security.py +0 -129
- schemathesis/specs/openapi/validation.py +0 -24
- schemathesis/stateful.py +0 -349
- schemathesis/targets.py +0 -32
- schemathesis/types.py +0 -38
- schemathesis/utils.py +0 -436
- schemathesis-3.13.0.dist-info/METADATA +0 -202
- schemathesis-3.13.0.dist-info/RECORD +0 -91
- schemathesis-3.13.0.dist-info/entry_points.txt +0 -6
- /schemathesis/{extra → cli/ext}/__init__.py +0 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import random
|
|
4
|
+
|
|
5
|
+
from schemathesis.generation.modes import GenerationMode
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"GenerationMode",
|
|
9
|
+
"generate_random_case_id",
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
CASE_ID_ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
|
|
14
|
+
BASE = len(CASE_ID_ALPHABET)
|
|
15
|
+
# Separate `Random` as Hypothesis might interfere with the default one
|
|
16
|
+
RANDOM = random.Random()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def generate_random_case_id(length: int = 6) -> str:
|
|
20
|
+
number = RANDOM.randint(62 ** (length - 1), 62**length - 1)
|
|
21
|
+
output = ""
|
|
22
|
+
while number > 0:
|
|
23
|
+
number, rem = divmod(number, BASE)
|
|
24
|
+
output += CASE_ID_ALPHABET[rem]
|
|
25
|
+
return output
|
|
@@ -0,0 +1,478 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Mapping
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import TYPE_CHECKING, Any
|
|
6
|
+
|
|
7
|
+
from schemathesis import transport
|
|
8
|
+
from schemathesis.checks import CHECKS, CheckContext, CheckFunction, load_all_checks, run_checks
|
|
9
|
+
from schemathesis.core import NOT_SET, SCHEMATHESIS_TEST_CASE_HEADER, NotSet, curl
|
|
10
|
+
from schemathesis.core.failures import FailureGroup, failure_report_title, format_failures
|
|
11
|
+
from schemathesis.core.parameters import CONTAINER_TO_LOCATION, ParameterLocation
|
|
12
|
+
from schemathesis.core.transport import Response
|
|
13
|
+
from schemathesis.generation import GenerationMode, generate_random_case_id
|
|
14
|
+
from schemathesis.generation.meta import CaseMetadata, ComponentInfo
|
|
15
|
+
from schemathesis.generation.overrides import Override, store_components
|
|
16
|
+
from schemathesis.hooks import HookContext, dispatch
|
|
17
|
+
from schemathesis.transport.prepare import prepare_path, prepare_request
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
import httpx
|
|
21
|
+
import requests
|
|
22
|
+
import requests.auth
|
|
23
|
+
from requests.structures import CaseInsensitiveDict
|
|
24
|
+
from werkzeug.test import TestResponse
|
|
25
|
+
|
|
26
|
+
from schemathesis.schemas import APIOperation
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _default_headers() -> CaseInsensitiveDict:
|
|
30
|
+
from requests.structures import CaseInsensitiveDict
|
|
31
|
+
|
|
32
|
+
return CaseInsensitiveDict()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
_NOTSET_HASH = 0x7F3A9B2C
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class Case:
|
|
40
|
+
"""Generated test case data for a single API operation."""
|
|
41
|
+
|
|
42
|
+
operation: APIOperation
|
|
43
|
+
method: str
|
|
44
|
+
"""HTTP verb (`GET`, `POST`, etc.)"""
|
|
45
|
+
path: str
|
|
46
|
+
"""Path template from schema (e.g., `/users/{user_id}`)"""
|
|
47
|
+
id: str
|
|
48
|
+
"""Random ID sent in headers for log correlation"""
|
|
49
|
+
path_parameters: dict[str, Any]
|
|
50
|
+
"""Generated path variables (e.g., `{"user_id": "123"}`)"""
|
|
51
|
+
headers: CaseInsensitiveDict
|
|
52
|
+
"""Generated HTTP headers"""
|
|
53
|
+
cookies: dict[str, Any]
|
|
54
|
+
"""Generated cookies"""
|
|
55
|
+
query: dict[str, Any]
|
|
56
|
+
"""Generated query parameters"""
|
|
57
|
+
# By default, there is no body, but we can't use `None` as the default value because it clashes with `null`
|
|
58
|
+
# which is a valid payload.
|
|
59
|
+
body: list | dict[str, Any] | str | int | float | bool | bytes | NotSet
|
|
60
|
+
"""Generated request body"""
|
|
61
|
+
media_type: str | None
|
|
62
|
+
"""Media type from OpenAPI schema (e.g., "multipart/form-data")"""
|
|
63
|
+
|
|
64
|
+
_meta: CaseMetadata | None
|
|
65
|
+
|
|
66
|
+
_auth: requests.auth.AuthBase | None
|
|
67
|
+
_has_explicit_auth: bool
|
|
68
|
+
_components: dict
|
|
69
|
+
_freeze_metadata: bool
|
|
70
|
+
|
|
71
|
+
__slots__ = (
|
|
72
|
+
"operation",
|
|
73
|
+
"method",
|
|
74
|
+
"path",
|
|
75
|
+
"id",
|
|
76
|
+
"path_parameters",
|
|
77
|
+
"headers",
|
|
78
|
+
"cookies",
|
|
79
|
+
"query",
|
|
80
|
+
"body",
|
|
81
|
+
"media_type",
|
|
82
|
+
"_meta",
|
|
83
|
+
"_auth",
|
|
84
|
+
"_has_explicit_auth",
|
|
85
|
+
"_components",
|
|
86
|
+
"_freeze_metadata",
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
def __init__(
|
|
90
|
+
self,
|
|
91
|
+
operation: APIOperation,
|
|
92
|
+
method: str,
|
|
93
|
+
path: str,
|
|
94
|
+
*,
|
|
95
|
+
id: str | None = None,
|
|
96
|
+
path_parameters: dict[str, Any] | None = None,
|
|
97
|
+
headers: CaseInsensitiveDict | None = None,
|
|
98
|
+
cookies: dict[str, Any] | None = None,
|
|
99
|
+
query: dict[str, Any] | None = None,
|
|
100
|
+
body: list | dict[str, Any] | str | int | float | bool | bytes | "NotSet" = NOT_SET,
|
|
101
|
+
media_type: str | None = None,
|
|
102
|
+
meta: CaseMetadata | None = None,
|
|
103
|
+
_auth: requests.auth.AuthBase | None = None,
|
|
104
|
+
_has_explicit_auth: bool = False,
|
|
105
|
+
) -> None:
|
|
106
|
+
# Use object.__setattr__ to bypass __setattr__ tracking during initialization
|
|
107
|
+
object.__setattr__(self, "operation", operation)
|
|
108
|
+
object.__setattr__(self, "method", method)
|
|
109
|
+
object.__setattr__(self, "path", path)
|
|
110
|
+
object.__setattr__(self, "id", id if id is not None else generate_random_case_id())
|
|
111
|
+
object.__setattr__(self, "path_parameters", path_parameters if path_parameters is not None else {})
|
|
112
|
+
object.__setattr__(self, "headers", headers if headers is not None else _default_headers())
|
|
113
|
+
object.__setattr__(self, "cookies", cookies if cookies is not None else {})
|
|
114
|
+
object.__setattr__(self, "query", query if query is not None else {})
|
|
115
|
+
object.__setattr__(self, "body", body)
|
|
116
|
+
object.__setattr__(self, "media_type", media_type)
|
|
117
|
+
object.__setattr__(self, "_meta", meta)
|
|
118
|
+
object.__setattr__(self, "_auth", _auth)
|
|
119
|
+
object.__setattr__(self, "_has_explicit_auth", _has_explicit_auth)
|
|
120
|
+
object.__setattr__(self, "_components", store_components(self))
|
|
121
|
+
object.__setattr__(self, "_freeze_metadata", False)
|
|
122
|
+
|
|
123
|
+
# Initialize hash tracking if we have metadata
|
|
124
|
+
if self._meta is not None:
|
|
125
|
+
self._init_hashes()
|
|
126
|
+
|
|
127
|
+
def __eq__(self, other: object) -> bool:
|
|
128
|
+
if not isinstance(other, Case):
|
|
129
|
+
return NotImplemented
|
|
130
|
+
|
|
131
|
+
return (
|
|
132
|
+
self.operation == other.operation
|
|
133
|
+
and self.method == other.method
|
|
134
|
+
and self.path == other.path
|
|
135
|
+
and self.path_parameters == other.path_parameters
|
|
136
|
+
and self.headers == other.headers
|
|
137
|
+
and self.cookies == other.cookies
|
|
138
|
+
and self.query == other.query
|
|
139
|
+
and self.body == other.body
|
|
140
|
+
and self.media_type == other.media_type
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
def __setattr__(self, name: str, value: Any) -> None:
|
|
144
|
+
"""Track modifications to containers for metadata revalidation."""
|
|
145
|
+
# Set the value
|
|
146
|
+
object.__setattr__(self, name, value)
|
|
147
|
+
|
|
148
|
+
# Mark as dirty if we modified a tracked container and have metadata
|
|
149
|
+
if name in CONTAINER_TO_LOCATION and self._meta is not None:
|
|
150
|
+
location = CONTAINER_TO_LOCATION[name]
|
|
151
|
+
self._meta.mark_dirty(location)
|
|
152
|
+
# Update hash immediately so future in-place modifications can be detected
|
|
153
|
+
self._meta.update_validated_hash(location, self._hash_container(value))
|
|
154
|
+
|
|
155
|
+
@property
|
|
156
|
+
def _override(self) -> Override:
|
|
157
|
+
return Override.from_components(self._components, self)
|
|
158
|
+
|
|
159
|
+
def __repr__(self) -> str:
|
|
160
|
+
output = f"{self.__class__.__name__}("
|
|
161
|
+
first = True
|
|
162
|
+
for name in ("path_parameters", "headers", "cookies", "query", "body"):
|
|
163
|
+
value = getattr(self, name)
|
|
164
|
+
if name != "body" and not value:
|
|
165
|
+
continue
|
|
166
|
+
if value is not None and not isinstance(value, NotSet):
|
|
167
|
+
if first:
|
|
168
|
+
first = False
|
|
169
|
+
else:
|
|
170
|
+
output += ", "
|
|
171
|
+
output += f"{name}={value!r}"
|
|
172
|
+
return f"{output})"
|
|
173
|
+
|
|
174
|
+
def __hash__(self) -> int:
|
|
175
|
+
return hash(self.as_curl_command({SCHEMATHESIS_TEST_CASE_HEADER: "0"}))
|
|
176
|
+
|
|
177
|
+
def _repr_pretty_(self, *args: Any, **kwargs: Any) -> None: ...
|
|
178
|
+
|
|
179
|
+
def _init_hashes(self) -> None:
|
|
180
|
+
"""Initialize hash tracking in metadata for generated components only."""
|
|
181
|
+
assert self._meta is not None
|
|
182
|
+
# Only track components that were actually generated
|
|
183
|
+
for location in self._meta.components.keys():
|
|
184
|
+
value = getattr(self, location.container_name)
|
|
185
|
+
hash_value = self._hash_container(value)
|
|
186
|
+
self._meta.update_validated_hash(location, hash_value)
|
|
187
|
+
|
|
188
|
+
def _check_modifications(self) -> None:
|
|
189
|
+
"""Detect in-place modifications by comparing container hashes."""
|
|
190
|
+
if self._meta is None:
|
|
191
|
+
return
|
|
192
|
+
|
|
193
|
+
# Only check components that were actually generated
|
|
194
|
+
for location in self._meta.components.keys():
|
|
195
|
+
last_hash = self._meta._last_validated_hashes[location]
|
|
196
|
+
value = getattr(self, location.container_name)
|
|
197
|
+
current_hash = self._hash_container(value)
|
|
198
|
+
|
|
199
|
+
if current_hash != last_hash:
|
|
200
|
+
# Container was modified in-place
|
|
201
|
+
self._meta.mark_dirty(location)
|
|
202
|
+
|
|
203
|
+
def _revalidate_metadata(self) -> None:
|
|
204
|
+
"""Revalidate dirty components and update metadata."""
|
|
205
|
+
assert self._meta and self._meta.is_dirty()
|
|
206
|
+
|
|
207
|
+
from schemathesis.specs.openapi.schemas import BaseOpenAPISchema
|
|
208
|
+
|
|
209
|
+
# Only works for OpenAPI schemas
|
|
210
|
+
if not isinstance(self.operation.schema, BaseOpenAPISchema):
|
|
211
|
+
# Can't validate, just clear dirty flags
|
|
212
|
+
for location in list(self._meta._dirty):
|
|
213
|
+
self._meta.clear_dirty(location)
|
|
214
|
+
return
|
|
215
|
+
|
|
216
|
+
validator_cls = self.operation.schema.adapter.jsonschema_validator_cls
|
|
217
|
+
|
|
218
|
+
for location in list(self._meta._dirty):
|
|
219
|
+
# Get current value
|
|
220
|
+
value = getattr(self, location.container_name)
|
|
221
|
+
|
|
222
|
+
# Validate against schema
|
|
223
|
+
is_valid = self._validate_component(location, value, validator_cls)
|
|
224
|
+
|
|
225
|
+
# Update component metadata
|
|
226
|
+
if location in self._meta.components:
|
|
227
|
+
new_mode = GenerationMode.POSITIVE if is_valid else GenerationMode.NEGATIVE
|
|
228
|
+
self._meta.components[location] = ComponentInfo(mode=new_mode)
|
|
229
|
+
|
|
230
|
+
# Update hash and clear dirty flag
|
|
231
|
+
self._meta.update_validated_hash(location, self._hash_container(value))
|
|
232
|
+
self._meta.clear_dirty(location)
|
|
233
|
+
|
|
234
|
+
# Recompute overall generation mode
|
|
235
|
+
if self._meta.components:
|
|
236
|
+
if all(info.mode.is_positive for info in self._meta.components.values()):
|
|
237
|
+
self._meta.generation.mode = GenerationMode.POSITIVE
|
|
238
|
+
else:
|
|
239
|
+
self._meta.generation.mode = GenerationMode.NEGATIVE
|
|
240
|
+
|
|
241
|
+
def _validate_component(
|
|
242
|
+
self,
|
|
243
|
+
location: ParameterLocation,
|
|
244
|
+
value: Any,
|
|
245
|
+
validator_cls: type,
|
|
246
|
+
) -> bool:
|
|
247
|
+
"""Validate a component value against its schema."""
|
|
248
|
+
if location == ParameterLocation.BODY:
|
|
249
|
+
# Validate body against media type schema
|
|
250
|
+
if isinstance(value, NotSet) or value is None:
|
|
251
|
+
return False
|
|
252
|
+
for alternative in self.operation.body:
|
|
253
|
+
if alternative.media_type == self.media_type:
|
|
254
|
+
return validator_cls(alternative.optimized_schema).is_valid(value)
|
|
255
|
+
# Validate other locations against container schema
|
|
256
|
+
container = getattr(self.operation, location.container_name)
|
|
257
|
+
return validator_cls(container.schema).is_valid(value)
|
|
258
|
+
|
|
259
|
+
def _hash_container(self, value: Any) -> int:
|
|
260
|
+
"""Create a hash representing the current state of a container.
|
|
261
|
+
|
|
262
|
+
Recursively hashes nested dicts/lists/tuples and primitives to detect modifications.
|
|
263
|
+
"""
|
|
264
|
+
if isinstance(value, Mapping):
|
|
265
|
+
return hash((type(value), tuple(sorted((k, self._hash_container(v)) for k, v in value.items()))))
|
|
266
|
+
elif isinstance(value, (list, tuple)):
|
|
267
|
+
return hash((type(value), tuple(self._hash_container(item) for item in value)))
|
|
268
|
+
elif isinstance(value, NotSet):
|
|
269
|
+
return _NOTSET_HASH
|
|
270
|
+
return hash((type(value), value))
|
|
271
|
+
|
|
272
|
+
@property
|
|
273
|
+
def meta(self) -> CaseMetadata | None:
|
|
274
|
+
"""Get metadata, revalidating if components were modified."""
|
|
275
|
+
# Skip revalidation if metadata is frozen (e.g., during request preparation)
|
|
276
|
+
if not self._freeze_metadata:
|
|
277
|
+
self._check_modifications()
|
|
278
|
+
if self._meta and self._meta.is_dirty():
|
|
279
|
+
self._revalidate_metadata()
|
|
280
|
+
return self._meta
|
|
281
|
+
|
|
282
|
+
@property
|
|
283
|
+
def formatted_path(self) -> str:
|
|
284
|
+
"""Path template with variables substituted (e.g., /users/{user_id} → /users/123)."""
|
|
285
|
+
return prepare_path(self.path, self.path_parameters)
|
|
286
|
+
|
|
287
|
+
def as_curl_command(self, headers: Mapping[str, Any] | None = None, verify: bool = True) -> str:
|
|
288
|
+
"""Generate a curl command that reproduces this test case.
|
|
289
|
+
|
|
290
|
+
Args:
|
|
291
|
+
headers: Additional headers to include in the command.
|
|
292
|
+
verify: When False, adds `--insecure` flag to curl command.
|
|
293
|
+
|
|
294
|
+
"""
|
|
295
|
+
request_data = prepare_request(self, headers, config=self.operation.schema.config.output.sanitization)
|
|
296
|
+
result = curl.generate(
|
|
297
|
+
method=str(request_data.method),
|
|
298
|
+
url=str(request_data.url),
|
|
299
|
+
body=request_data.body,
|
|
300
|
+
verify=verify,
|
|
301
|
+
headers=dict(request_data.headers),
|
|
302
|
+
known_generated_headers=dict(self.headers or {}),
|
|
303
|
+
)
|
|
304
|
+
# Include warnings if any exist
|
|
305
|
+
if result.warnings:
|
|
306
|
+
warnings_text = "\n\n".join(f"⚠️ {warning}" for warning in result.warnings)
|
|
307
|
+
return f"{result.command}\n\n{warnings_text}"
|
|
308
|
+
return result.command
|
|
309
|
+
|
|
310
|
+
def as_transport_kwargs(self, base_url: str | None = None, headers: dict[str, str] | None = None) -> dict[str, Any]:
|
|
311
|
+
return self.operation.schema.transport.serialize_case(self, base_url=base_url, headers=headers)
|
|
312
|
+
|
|
313
|
+
def call(
|
|
314
|
+
self,
|
|
315
|
+
base_url: str | None = None,
|
|
316
|
+
session: requests.Session | None = None,
|
|
317
|
+
headers: dict[str, Any] | None = None,
|
|
318
|
+
params: dict[str, Any] | None = None,
|
|
319
|
+
cookies: dict[str, Any] | None = None,
|
|
320
|
+
**kwargs: Any,
|
|
321
|
+
) -> Response:
|
|
322
|
+
"""Make an HTTP request using this test case's data without validation.
|
|
323
|
+
|
|
324
|
+
Use when you need to validate response separately
|
|
325
|
+
|
|
326
|
+
Args:
|
|
327
|
+
base_url: Override the schema's base URL.
|
|
328
|
+
session: Reuse an existing requests session.
|
|
329
|
+
headers: Additional headers.
|
|
330
|
+
params: Additional query parameters.
|
|
331
|
+
cookies: Additional cookies.
|
|
332
|
+
**kwargs: Additional transport-level arguments.
|
|
333
|
+
|
|
334
|
+
"""
|
|
335
|
+
hook_context = HookContext(operation=self.operation)
|
|
336
|
+
dispatch("before_call", hook_context, self, _with_dual_style_kwargs=True, **kwargs)
|
|
337
|
+
|
|
338
|
+
# Revalidate metadata if dirty before freezing (captures user modifications)
|
|
339
|
+
if self._meta and self._meta.is_dirty():
|
|
340
|
+
self._check_modifications()
|
|
341
|
+
self._revalidate_metadata()
|
|
342
|
+
|
|
343
|
+
# Freeze metadata to prevent revalidation after request preparation transforms the body
|
|
344
|
+
object.__setattr__(self, "_freeze_metadata", True)
|
|
345
|
+
|
|
346
|
+
if self.operation.app is not None:
|
|
347
|
+
kwargs.setdefault("app", self.operation.app)
|
|
348
|
+
if "app" in kwargs:
|
|
349
|
+
transport_ = transport.get(kwargs["app"])
|
|
350
|
+
else:
|
|
351
|
+
transport_ = self.operation.schema.transport
|
|
352
|
+
try:
|
|
353
|
+
response = transport_.send(
|
|
354
|
+
self,
|
|
355
|
+
session=session,
|
|
356
|
+
base_url=base_url,
|
|
357
|
+
headers=headers,
|
|
358
|
+
params=params,
|
|
359
|
+
cookies=cookies,
|
|
360
|
+
**kwargs,
|
|
361
|
+
)
|
|
362
|
+
except Exception as exc:
|
|
363
|
+
# May happen in ASGI / WSGI apps
|
|
364
|
+
if not hasattr(exc, "__notes__"):
|
|
365
|
+
exc.__notes__ = [] # type: ignore[attr-defined]
|
|
366
|
+
verify = kwargs.get("verify", True)
|
|
367
|
+
curl = self.as_curl_command(headers=headers, verify=verify)
|
|
368
|
+
exc.__notes__.append(f"\nReproduce with: \n\n {curl}") # type: ignore[attr-defined]
|
|
369
|
+
raise
|
|
370
|
+
dispatch("after_call", hook_context, self, response)
|
|
371
|
+
return response
|
|
372
|
+
|
|
373
|
+
def validate_response(
|
|
374
|
+
self,
|
|
375
|
+
response: Response | httpx.Response | requests.Response | TestResponse,
|
|
376
|
+
checks: list[CheckFunction] | None = None,
|
|
377
|
+
additional_checks: list[CheckFunction] | None = None,
|
|
378
|
+
excluded_checks: list[CheckFunction] | None = None,
|
|
379
|
+
headers: dict[str, Any] | None = None,
|
|
380
|
+
transport_kwargs: dict[str, Any] | None = None,
|
|
381
|
+
) -> None:
|
|
382
|
+
"""Validate a response against the API schema and built-in checks.
|
|
383
|
+
|
|
384
|
+
Args:
|
|
385
|
+
response: Response to validate.
|
|
386
|
+
checks: Explicit set of checks to run.
|
|
387
|
+
additional_checks: Additional custom checks to run.
|
|
388
|
+
excluded_checks: Built-in checks to skip.
|
|
389
|
+
headers: Headers used in the original request.
|
|
390
|
+
transport_kwargs: Transport arguments used in the original request.
|
|
391
|
+
|
|
392
|
+
"""
|
|
393
|
+
__tracebackhide__ = True
|
|
394
|
+
from requests.structures import CaseInsensitiveDict
|
|
395
|
+
|
|
396
|
+
# In some cases checks may not be loaded.
|
|
397
|
+
# For example - non-Schemathesis tests that manually construct `Case` instances
|
|
398
|
+
load_all_checks()
|
|
399
|
+
|
|
400
|
+
response = Response.from_any(response)
|
|
401
|
+
|
|
402
|
+
config = self.operation.schema.config.checks_config_for(
|
|
403
|
+
operation=self.operation, phase=self.meta.phase.name.value if self.meta is not None else None
|
|
404
|
+
)
|
|
405
|
+
if not checks:
|
|
406
|
+
# Checks are not specified explicitly, derive from the config
|
|
407
|
+
checks = []
|
|
408
|
+
for check in CHECKS.get_all():
|
|
409
|
+
name = check.__name__
|
|
410
|
+
if config.get_by_name(name=name).enabled:
|
|
411
|
+
checks.append(check)
|
|
412
|
+
checks = [
|
|
413
|
+
check for check in list(checks) + list(additional_checks or []) if check not in set(excluded_checks or [])
|
|
414
|
+
]
|
|
415
|
+
|
|
416
|
+
ctx = CheckContext(
|
|
417
|
+
override=self._override,
|
|
418
|
+
auth=None,
|
|
419
|
+
headers=CaseInsensitiveDict(headers) if headers else None,
|
|
420
|
+
config=config,
|
|
421
|
+
transport_kwargs=transport_kwargs,
|
|
422
|
+
recorder=None,
|
|
423
|
+
)
|
|
424
|
+
failures = run_checks(
|
|
425
|
+
case=self,
|
|
426
|
+
response=response,
|
|
427
|
+
ctx=ctx,
|
|
428
|
+
checks=checks,
|
|
429
|
+
on_failure=lambda _, collected, failure: collected.add(failure),
|
|
430
|
+
)
|
|
431
|
+
if failures:
|
|
432
|
+
_failures = list(failures)
|
|
433
|
+
message = failure_report_title(_failures) + "\n"
|
|
434
|
+
verify = getattr(response, "verify", True)
|
|
435
|
+
curl = self.as_curl_command(headers=dict(response.request.headers), verify=verify)
|
|
436
|
+
message += format_failures(
|
|
437
|
+
case_id=None,
|
|
438
|
+
response=response,
|
|
439
|
+
failures=_failures,
|
|
440
|
+
curl=curl,
|
|
441
|
+
config=self.operation.schema.config.output,
|
|
442
|
+
)
|
|
443
|
+
message += "\n\n"
|
|
444
|
+
raise FailureGroup(_failures, message) from None
|
|
445
|
+
|
|
446
|
+
def call_and_validate(
|
|
447
|
+
self,
|
|
448
|
+
base_url: str | None = None,
|
|
449
|
+
session: requests.Session | None = None,
|
|
450
|
+
headers: dict[str, Any] | None = None,
|
|
451
|
+
checks: list[CheckFunction] | None = None,
|
|
452
|
+
additional_checks: list[CheckFunction] | None = None,
|
|
453
|
+
excluded_checks: list[CheckFunction] | None = None,
|
|
454
|
+
**kwargs: Any,
|
|
455
|
+
) -> Response:
|
|
456
|
+
"""Make an HTTP request and validates the response automatically.
|
|
457
|
+
|
|
458
|
+
Args:
|
|
459
|
+
base_url: Override the schema's base URL.
|
|
460
|
+
session: Reuse an existing requests session.
|
|
461
|
+
headers: Additional headers to send.
|
|
462
|
+
checks: Explicit set of checks to run.
|
|
463
|
+
additional_checks: Additional custom checks to run.
|
|
464
|
+
excluded_checks: Built-in checks to skip.
|
|
465
|
+
**kwargs: Additional transport-level arguments.
|
|
466
|
+
|
|
467
|
+
"""
|
|
468
|
+
__tracebackhide__ = True
|
|
469
|
+
response = self.call(base_url, session, headers, **kwargs)
|
|
470
|
+
self.validate_response(
|
|
471
|
+
response,
|
|
472
|
+
checks,
|
|
473
|
+
headers=headers,
|
|
474
|
+
additional_checks=additional_checks,
|
|
475
|
+
excluded_checks=excluded_checks,
|
|
476
|
+
transport_kwargs=kwargs,
|
|
477
|
+
)
|
|
478
|
+
return response
|