schemathesis 3.39.15__py3-none-any.whl → 4.0.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/__init__.py +41 -79
- schemathesis/auths.py +111 -122
- schemathesis/checks.py +169 -60
- schemathesis/cli/__init__.py +15 -2117
- schemathesis/cli/commands/__init__.py +85 -0
- schemathesis/cli/commands/data.py +10 -0
- schemathesis/cli/commands/run/__init__.py +590 -0
- schemathesis/cli/commands/run/context.py +204 -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 +18 -0
- schemathesis/cli/commands/run/handlers/cassettes.py +474 -0
- schemathesis/cli/commands/run/handlers/junitxml.py +55 -0
- schemathesis/cli/commands/run/handlers/output.py +1628 -0
- schemathesis/cli/commands/run/loaders.py +114 -0
- schemathesis/cli/commands/run/validation.py +246 -0
- schemathesis/cli/constants.py +5 -58
- schemathesis/cli/core.py +19 -0
- schemathesis/cli/ext/fs.py +16 -0
- schemathesis/cli/ext/groups.py +84 -0
- schemathesis/cli/{options.py → ext/options.py} +36 -34
- schemathesis/config/__init__.py +189 -0
- schemathesis/config/_auth.py +51 -0
- schemathesis/config/_checks.py +268 -0
- schemathesis/config/_diff_base.py +99 -0
- schemathesis/config/_env.py +21 -0
- schemathesis/config/_error.py +156 -0
- schemathesis/config/_generation.py +149 -0
- schemathesis/config/_health_check.py +24 -0
- schemathesis/config/_operations.py +327 -0
- schemathesis/config/_output.py +171 -0
- schemathesis/config/_parameters.py +19 -0
- schemathesis/config/_phases.py +187 -0
- schemathesis/config/_projects.py +527 -0
- schemathesis/config/_rate_limit.py +17 -0
- schemathesis/config/_report.py +120 -0
- schemathesis/config/_validator.py +9 -0
- schemathesis/config/_warnings.py +25 -0
- schemathesis/config/schema.json +885 -0
- schemathesis/core/__init__.py +67 -0
- schemathesis/core/compat.py +32 -0
- schemathesis/core/control.py +2 -0
- schemathesis/core/curl.py +58 -0
- schemathesis/core/deserialization.py +65 -0
- schemathesis/core/errors.py +459 -0
- schemathesis/core/failures.py +315 -0
- schemathesis/core/fs.py +19 -0
- schemathesis/core/hooks.py +20 -0
- schemathesis/core/loaders.py +104 -0
- schemathesis/core/marks.py +66 -0
- schemathesis/{transports/content_types.py → core/media_types.py} +14 -12
- schemathesis/core/output/__init__.py +46 -0
- schemathesis/core/output/sanitization.py +54 -0
- schemathesis/{throttling.py → core/rate_limit.py} +16 -17
- schemathesis/core/registries.py +31 -0
- schemathesis/core/transforms.py +113 -0
- schemathesis/core/transport.py +223 -0
- schemathesis/core/validation.py +54 -0
- schemathesis/core/version.py +7 -0
- schemathesis/engine/__init__.py +28 -0
- schemathesis/engine/context.py +118 -0
- schemathesis/engine/control.py +36 -0
- schemathesis/engine/core.py +169 -0
- schemathesis/engine/errors.py +464 -0
- schemathesis/engine/events.py +258 -0
- schemathesis/engine/phases/__init__.py +88 -0
- schemathesis/{runner → engine/phases}/probes.py +52 -68
- schemathesis/engine/phases/stateful/__init__.py +68 -0
- schemathesis/engine/phases/stateful/_executor.py +356 -0
- schemathesis/engine/phases/stateful/context.py +85 -0
- schemathesis/engine/phases/unit/__init__.py +212 -0
- schemathesis/engine/phases/unit/_executor.py +416 -0
- schemathesis/engine/phases/unit/_pool.py +82 -0
- schemathesis/engine/recorder.py +247 -0
- schemathesis/errors.py +43 -0
- schemathesis/filters.py +17 -98
- schemathesis/generation/__init__.py +5 -33
- schemathesis/generation/case.py +317 -0
- schemathesis/generation/coverage.py +282 -175
- schemathesis/generation/hypothesis/__init__.py +36 -0
- schemathesis/generation/hypothesis/builder.py +800 -0
- schemathesis/generation/{_hypothesis.py → hypothesis/examples.py} +2 -11
- schemathesis/generation/hypothesis/given.py +66 -0
- schemathesis/generation/hypothesis/reporting.py +14 -0
- schemathesis/generation/hypothesis/strategies.py +16 -0
- schemathesis/generation/meta.py +115 -0
- schemathesis/generation/metrics.py +93 -0
- schemathesis/generation/modes.py +20 -0
- schemathesis/generation/overrides.py +116 -0
- schemathesis/generation/stateful/__init__.py +37 -0
- schemathesis/generation/stateful/state_machine.py +278 -0
- schemathesis/graphql/__init__.py +15 -0
- schemathesis/graphql/checks.py +109 -0
- schemathesis/graphql/loaders.py +284 -0
- schemathesis/hooks.py +80 -101
- schemathesis/openapi/__init__.py +13 -0
- schemathesis/openapi/checks.py +455 -0
- schemathesis/openapi/generation/__init__.py +0 -0
- schemathesis/openapi/generation/filters.py +72 -0
- schemathesis/openapi/loaders.py +313 -0
- schemathesis/pytest/__init__.py +5 -0
- schemathesis/pytest/control_flow.py +7 -0
- schemathesis/pytest/lazy.py +281 -0
- schemathesis/pytest/loaders.py +36 -0
- schemathesis/{extra/pytest_plugin.py → pytest/plugin.py} +128 -108
- schemathesis/python/__init__.py +0 -0
- schemathesis/python/asgi.py +12 -0
- schemathesis/python/wsgi.py +12 -0
- schemathesis/schemas.py +537 -273
- schemathesis/specs/graphql/__init__.py +0 -1
- schemathesis/specs/graphql/_cache.py +1 -2
- schemathesis/specs/graphql/scalars.py +42 -6
- schemathesis/specs/graphql/schemas.py +141 -137
- schemathesis/specs/graphql/validation.py +11 -17
- schemathesis/specs/openapi/__init__.py +6 -1
- schemathesis/specs/openapi/_cache.py +1 -2
- schemathesis/specs/openapi/_hypothesis.py +142 -156
- schemathesis/specs/openapi/checks.py +368 -257
- schemathesis/specs/openapi/converter.py +4 -4
- schemathesis/specs/openapi/definitions.py +1 -1
- schemathesis/specs/openapi/examples.py +23 -21
- schemathesis/specs/openapi/expressions/__init__.py +31 -19
- schemathesis/specs/openapi/expressions/extractors.py +1 -4
- schemathesis/specs/openapi/expressions/lexer.py +1 -1
- schemathesis/specs/openapi/expressions/nodes.py +36 -41
- schemathesis/specs/openapi/expressions/parser.py +1 -1
- schemathesis/specs/openapi/formats.py +35 -7
- schemathesis/specs/openapi/media_types.py +53 -12
- schemathesis/specs/openapi/negative/__init__.py +7 -4
- schemathesis/specs/openapi/negative/mutations.py +6 -5
- schemathesis/specs/openapi/parameters.py +7 -10
- schemathesis/specs/openapi/patterns.py +94 -31
- schemathesis/specs/openapi/references.py +12 -53
- schemathesis/specs/openapi/schemas.py +238 -308
- schemathesis/specs/openapi/security.py +1 -1
- schemathesis/specs/openapi/serialization.py +12 -6
- schemathesis/specs/openapi/stateful/__init__.py +268 -133
- schemathesis/specs/openapi/stateful/control.py +87 -0
- schemathesis/specs/openapi/stateful/links.py +209 -0
- schemathesis/transport/__init__.py +142 -0
- schemathesis/transport/asgi.py +26 -0
- schemathesis/transport/prepare.py +124 -0
- schemathesis/transport/requests.py +244 -0
- schemathesis/{_xml.py → transport/serialization.py} +69 -11
- schemathesis/transport/wsgi.py +171 -0
- schemathesis-4.0.0.dist-info/METADATA +204 -0
- schemathesis-4.0.0.dist-info/RECORD +164 -0
- {schemathesis-3.39.15.dist-info → schemathesis-4.0.0.dist-info}/entry_points.txt +1 -1
- {schemathesis-3.39.15.dist-info → schemathesis-4.0.0.dist-info}/licenses/LICENSE +1 -1
- schemathesis/_compat.py +0 -74
- schemathesis/_dependency_versions.py +0 -19
- schemathesis/_hypothesis.py +0 -712
- schemathesis/_override.py +0 -50
- schemathesis/_patches.py +0 -21
- schemathesis/_rate_limiter.py +0 -7
- schemathesis/cli/callbacks.py +0 -466
- schemathesis/cli/cassettes.py +0 -561
- schemathesis/cli/context.py +0 -75
- schemathesis/cli/debug.py +0 -27
- schemathesis/cli/handlers.py +0 -19
- schemathesis/cli/junitxml.py +0 -124
- schemathesis/cli/output/__init__.py +0 -1
- schemathesis/cli/output/default.py +0 -920
- schemathesis/cli/output/short.py +0 -59
- schemathesis/cli/reporting.py +0 -79
- schemathesis/cli/sanitization.py +0 -26
- schemathesis/code_samples.py +0 -151
- schemathesis/constants.py +0 -54
- schemathesis/contrib/__init__.py +0 -11
- schemathesis/contrib/openapi/__init__.py +0 -11
- schemathesis/contrib/openapi/fill_missing_examples.py +0 -24
- schemathesis/contrib/openapi/formats/__init__.py +0 -9
- schemathesis/contrib/openapi/formats/uuid.py +0 -16
- schemathesis/contrib/unique_data.py +0 -41
- schemathesis/exceptions.py +0 -571
- schemathesis/experimental/__init__.py +0 -109
- schemathesis/extra/_aiohttp.py +0 -28
- schemathesis/extra/_flask.py +0 -13
- schemathesis/extra/_server.py +0 -18
- schemathesis/failures.py +0 -284
- schemathesis/fixups/__init__.py +0 -37
- schemathesis/fixups/fast_api.py +0 -41
- schemathesis/fixups/utf8_bom.py +0 -28
- schemathesis/generation/_methods.py +0 -44
- schemathesis/graphql.py +0 -3
- schemathesis/internal/__init__.py +0 -7
- schemathesis/internal/checks.py +0 -86
- schemathesis/internal/copy.py +0 -32
- schemathesis/internal/datetime.py +0 -5
- schemathesis/internal/deprecation.py +0 -37
- schemathesis/internal/diff.py +0 -15
- schemathesis/internal/extensions.py +0 -27
- schemathesis/internal/jsonschema.py +0 -36
- schemathesis/internal/output.py +0 -68
- schemathesis/internal/transformation.py +0 -26
- schemathesis/internal/validation.py +0 -34
- schemathesis/lazy.py +0 -474
- schemathesis/loaders.py +0 -122
- schemathesis/models.py +0 -1341
- schemathesis/parameters.py +0 -90
- schemathesis/runner/__init__.py +0 -605
- schemathesis/runner/events.py +0 -389
- schemathesis/runner/impl/__init__.py +0 -3
- schemathesis/runner/impl/context.py +0 -88
- schemathesis/runner/impl/core.py +0 -1280
- schemathesis/runner/impl/solo.py +0 -80
- schemathesis/runner/impl/threadpool.py +0 -391
- schemathesis/runner/serialization.py +0 -544
- schemathesis/sanitization.py +0 -252
- schemathesis/serializers.py +0 -328
- schemathesis/service/__init__.py +0 -18
- schemathesis/service/auth.py +0 -11
- schemathesis/service/ci.py +0 -202
- schemathesis/service/client.py +0 -133
- schemathesis/service/constants.py +0 -38
- schemathesis/service/events.py +0 -61
- schemathesis/service/extensions.py +0 -224
- schemathesis/service/hosts.py +0 -111
- schemathesis/service/metadata.py +0 -71
- schemathesis/service/models.py +0 -258
- schemathesis/service/report.py +0 -255
- schemathesis/service/serialization.py +0 -173
- schemathesis/service/usage.py +0 -66
- schemathesis/specs/graphql/loaders.py +0 -364
- schemathesis/specs/openapi/expressions/context.py +0 -16
- schemathesis/specs/openapi/links.py +0 -389
- schemathesis/specs/openapi/loaders.py +0 -707
- schemathesis/specs/openapi/stateful/statistic.py +0 -198
- schemathesis/specs/openapi/stateful/types.py +0 -14
- schemathesis/specs/openapi/validation.py +0 -26
- schemathesis/stateful/__init__.py +0 -147
- schemathesis/stateful/config.py +0 -97
- schemathesis/stateful/context.py +0 -135
- schemathesis/stateful/events.py +0 -274
- schemathesis/stateful/runner.py +0 -309
- schemathesis/stateful/sink.py +0 -68
- schemathesis/stateful/state_machine.py +0 -328
- schemathesis/stateful/statistic.py +0 -22
- schemathesis/stateful/validation.py +0 -100
- schemathesis/targets.py +0 -77
- schemathesis/transports/__init__.py +0 -369
- schemathesis/transports/asgi.py +0 -7
- schemathesis/transports/auth.py +0 -38
- schemathesis/transports/headers.py +0 -36
- schemathesis/transports/responses.py +0 -57
- schemathesis/types.py +0 -44
- schemathesis/utils.py +0 -164
- schemathesis-3.39.15.dist-info/METADATA +0 -293
- schemathesis-3.39.15.dist-info/RECORD +0 -160
- /schemathesis/{extra → cli/ext}/__init__.py +0 -0
- /schemathesis/{_lazy_import.py → core/lazy_import.py} +0 -0
- /schemathesis/{internal → core}/result.py +0 -0
- {schemathesis-3.39.15.dist-info → schemathesis-4.0.0.dist-info}/WHEEL +0 -0
@@ -1,43 +1,54 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
3
|
import enum
|
4
|
+
import http.client
|
4
5
|
from dataclasses import dataclass
|
5
6
|
from http.cookies import SimpleCookie
|
6
7
|
from typing import TYPE_CHECKING, Any, Dict, Generator, NoReturn, cast
|
7
8
|
from urllib.parse import parse_qs, urlparse
|
8
9
|
|
9
|
-
|
10
|
-
from
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
10
|
+
import schemathesis
|
11
|
+
from schemathesis.checks import CheckContext
|
12
|
+
from schemathesis.core import media_types, string_to_boolean
|
13
|
+
from schemathesis.core.failures import Failure
|
14
|
+
from schemathesis.core.transport import Response
|
15
|
+
from schemathesis.generation.case import Case
|
16
|
+
from schemathesis.generation.meta import ComponentKind, CoveragePhaseData
|
17
|
+
from schemathesis.openapi.checks import (
|
18
|
+
AcceptedNegativeData,
|
19
|
+
EnsureResourceAvailability,
|
20
|
+
IgnoredAuth,
|
21
|
+
JsonSchemaError,
|
22
|
+
MalformedMediaType,
|
23
|
+
MissingContentType,
|
24
|
+
MissingHeaderNotRejected,
|
25
|
+
MissingHeaders,
|
26
|
+
RejectedPositiveData,
|
27
|
+
UndefinedContentType,
|
28
|
+
UndefinedStatusCode,
|
29
|
+
UnsupportedMethodResponse,
|
30
|
+
UseAfterFree,
|
22
31
|
)
|
23
|
-
from
|
24
|
-
from
|
32
|
+
from schemathesis.specs.openapi.constants import LOCATION_TO_CONTAINER
|
33
|
+
from schemathesis.transport.prepare import prepare_path
|
34
|
+
|
25
35
|
from .utils import expand_status_code, expand_status_codes
|
26
36
|
|
27
37
|
if TYPE_CHECKING:
|
28
|
-
from
|
29
|
-
|
30
|
-
from ...internal.checks import CheckContext
|
31
|
-
from ...models import APIOperation, Case
|
32
|
-
from ...transports.responses import GenericResponse
|
38
|
+
from schemathesis.schemas import APIOperation
|
33
39
|
|
34
40
|
|
35
41
|
def is_unexpected_http_status_case(case: Case) -> bool:
|
36
42
|
# Skip checks for requests using HTTP methods not defined in the API spec
|
37
|
-
return bool(
|
43
|
+
return bool(
|
44
|
+
case.meta
|
45
|
+
and isinstance(case.meta.phase.data, CoveragePhaseData)
|
46
|
+
and case.meta.phase.data.description.startswith("Unspecified HTTP method")
|
47
|
+
)
|
38
48
|
|
39
49
|
|
40
|
-
|
50
|
+
@schemathesis.check
|
51
|
+
def status_code_conformance(ctx: CheckContext, response: Response, case: Case) -> bool | None:
|
41
52
|
from .schemas import BaseOpenAPISchema
|
42
53
|
|
43
54
|
if not isinstance(case.operation.schema, BaseOpenAPISchema) or is_unexpected_http_status_case(case):
|
@@ -50,15 +61,12 @@ def status_code_conformance(ctx: CheckContext, response: GenericResponse, case:
|
|
50
61
|
if response.status_code not in allowed_status_codes:
|
51
62
|
defined_status_codes = list(map(str, responses))
|
52
63
|
responses_list = ", ".join(defined_status_codes)
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
defined_status_codes=defined_status_codes,
|
60
|
-
allowed_status_codes=allowed_status_codes,
|
61
|
-
),
|
64
|
+
raise UndefinedStatusCode(
|
65
|
+
operation=case.operation.label,
|
66
|
+
status_code=response.status_code,
|
67
|
+
defined_status_codes=defined_status_codes,
|
68
|
+
allowed_status_codes=allowed_status_codes,
|
69
|
+
message=f"Received: {response.status_code}\nDocumented: {responses_list}",
|
62
70
|
)
|
63
71
|
return None # explicitly return None for mypy
|
64
72
|
|
@@ -68,7 +76,8 @@ def _expand_responses(responses: dict[str | int, Any]) -> Generator[int, None, N
|
|
68
76
|
yield from expand_status_code(code)
|
69
77
|
|
70
78
|
|
71
|
-
|
79
|
+
@schemathesis.check
|
80
|
+
def content_type_conformance(ctx: CheckContext, response: Response, case: Case) -> bool | None:
|
72
81
|
from .schemas import BaseOpenAPISchema
|
73
82
|
|
74
83
|
if not isinstance(case.operation.schema, BaseOpenAPISchema) or is_unexpected_http_status_case(case):
|
@@ -76,25 +85,24 @@ def content_type_conformance(ctx: CheckContext, response: GenericResponse, case:
|
|
76
85
|
documented_content_types = case.operation.schema.get_content_types(case.operation, response)
|
77
86
|
if not documented_content_types:
|
78
87
|
return None
|
79
|
-
|
80
|
-
if not
|
81
|
-
|
82
|
-
raise
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
media_types=documented_content_types,
|
87
|
-
),
|
88
|
+
content_types = response.headers.get("content-type")
|
89
|
+
if not content_types:
|
90
|
+
all_media_types = [f"\n- `{content_type}`" for content_type in documented_content_types]
|
91
|
+
raise MissingContentType(
|
92
|
+
operation=case.operation.label,
|
93
|
+
message=f"The following media types are documented in the schema:{''.join(all_media_types)}",
|
94
|
+
media_types=documented_content_types,
|
88
95
|
)
|
96
|
+
content_type = content_types[0]
|
89
97
|
for option in documented_content_types:
|
90
98
|
try:
|
91
|
-
expected_main, expected_sub =
|
92
|
-
except ValueError
|
93
|
-
_reraise_malformed_media_type(case,
|
99
|
+
expected_main, expected_sub = media_types.parse(option)
|
100
|
+
except ValueError:
|
101
|
+
_reraise_malformed_media_type(case, "Schema", option, option)
|
94
102
|
try:
|
95
|
-
received_main, received_sub =
|
96
|
-
except ValueError
|
97
|
-
_reraise_malformed_media_type(case,
|
103
|
+
received_main, received_sub = media_types.parse(content_type)
|
104
|
+
except ValueError:
|
105
|
+
_reraise_malformed_media_type(case, "Response", content_type, option)
|
98
106
|
if (
|
99
107
|
(expected_main == "*" and expected_sub == "*")
|
100
108
|
or (expected_main == received_main and expected_sub == "*")
|
@@ -102,28 +110,25 @@ def content_type_conformance(ctx: CheckContext, response: GenericResponse, case:
|
|
102
110
|
or (expected_main == received_main and expected_sub == received_sub)
|
103
111
|
):
|
104
112
|
return None
|
105
|
-
|
106
|
-
case.operation.
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
context=failures.UndefinedContentType(
|
111
|
-
message=f"Received: {content_type}\nDocumented: {', '.join(documented_content_types)}",
|
112
|
-
content_type=content_type,
|
113
|
-
defined_content_types=documented_content_types,
|
114
|
-
),
|
113
|
+
raise UndefinedContentType(
|
114
|
+
operation=case.operation.label,
|
115
|
+
message=f"Received: {content_type}\nDocumented: {', '.join(documented_content_types)}",
|
116
|
+
content_type=content_type,
|
117
|
+
defined_content_types=documented_content_types,
|
115
118
|
)
|
116
119
|
|
117
120
|
|
118
|
-
def _reraise_malformed_media_type(case: Case,
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
121
|
+
def _reraise_malformed_media_type(case: Case, location: str, actual: str, defined: str) -> NoReturn:
|
122
|
+
raise MalformedMediaType(
|
123
|
+
operation=case.operation.label,
|
124
|
+
message=f"Media type for {location} is incorrect\n\nReceived: {actual}\nDocumented: {defined}",
|
125
|
+
actual=actual,
|
126
|
+
defined=defined,
|
127
|
+
)
|
124
128
|
|
125
129
|
|
126
|
-
|
130
|
+
@schemathesis.check
|
131
|
+
def response_headers_conformance(ctx: CheckContext, response: Response, case: Case) -> bool | None:
|
127
132
|
import jsonschema
|
128
133
|
|
129
134
|
from .parameters import OpenAPI20Parameter, OpenAPI30Parameter
|
@@ -141,23 +146,17 @@ def response_headers_conformance(ctx: CheckContext, response: GenericResponse, c
|
|
141
146
|
missing_headers = [
|
142
147
|
header
|
143
148
|
for header, definition in defined_headers.items()
|
144
|
-
if header not in response.headers and definition.get(case.operation.schema.header_required_field, False)
|
149
|
+
if header.lower() not in response.headers and definition.get(case.operation.schema.header_required_field, False)
|
145
150
|
]
|
146
|
-
errors = []
|
151
|
+
errors: list[Failure] = []
|
147
152
|
if missing_headers:
|
148
153
|
formatted_headers = [f"\n- `{header}`" for header in missing_headers]
|
149
154
|
message = f"The following required headers are missing from the response:{''.join(formatted_headers)}"
|
150
|
-
|
151
|
-
try:
|
152
|
-
raise exc_class(
|
153
|
-
failures.MissingHeaders.title,
|
154
|
-
context=failures.MissingHeaders(message=message, missing_headers=missing_headers),
|
155
|
-
)
|
156
|
-
except Exception as exc:
|
157
|
-
errors.append(exc)
|
155
|
+
errors.append(MissingHeaders(operation=case.operation.label, message=message, missing_headers=missing_headers))
|
158
156
|
for name, definition in defined_headers.items():
|
159
|
-
|
160
|
-
if
|
157
|
+
values = response.headers.get(name.lower())
|
158
|
+
if values is not None:
|
159
|
+
value = values[0]
|
161
160
|
with case.operation.schema._validating_response(scopes) as resolver:
|
162
161
|
if "$ref" in definition:
|
163
162
|
_, definition = resolver.resolve(definition["$ref"])
|
@@ -178,14 +177,14 @@ def response_headers_conformance(ctx: CheckContext, response: GenericResponse, c
|
|
178
177
|
format_checker=jsonschema.Draft202012Validator.FORMAT_CHECKER,
|
179
178
|
)
|
180
179
|
except jsonschema.ValidationError as exc:
|
181
|
-
|
182
|
-
|
183
|
-
|
180
|
+
errors.append(
|
181
|
+
JsonSchemaError.from_exception(
|
182
|
+
title="Response header does not conform to the schema",
|
183
|
+
operation=case.operation.label,
|
184
|
+
exc=exc,
|
185
|
+
config=case.operation.schema.config.output,
|
186
|
+
)
|
184
187
|
)
|
185
|
-
try:
|
186
|
-
raise exc_class("Response header does not conform to the schema", context=error_ctx) from exc
|
187
|
-
except Exception as exc:
|
188
|
-
errors.append(exc)
|
189
188
|
return _maybe_raise_one_or_more(errors) # type: ignore[func-returns-value]
|
190
189
|
|
191
190
|
|
@@ -207,11 +206,12 @@ def _coerce_header_value(value: str, schema: dict[str, Any]) -> str | int | floa
|
|
207
206
|
if schema_type == "null" and value.lower() == "null":
|
208
207
|
return None
|
209
208
|
if schema_type == "boolean":
|
210
|
-
return
|
209
|
+
return string_to_boolean(value)
|
211
210
|
return value
|
212
211
|
|
213
212
|
|
214
|
-
|
213
|
+
@schemathesis.check
|
214
|
+
def response_schema_conformance(ctx: CheckContext, response: Response, case: Case) -> bool | None:
|
215
215
|
from .schemas import BaseOpenAPISchema
|
216
216
|
|
217
217
|
if not isinstance(case.operation.schema, BaseOpenAPISchema) or is_unexpected_http_status_case(case):
|
@@ -219,95 +219,113 @@ def response_schema_conformance(ctx: CheckContext, response: GenericResponse, ca
|
|
219
219
|
return case.operation.validate_response(response)
|
220
220
|
|
221
221
|
|
222
|
-
|
222
|
+
@schemathesis.check
|
223
|
+
def negative_data_rejection(ctx: CheckContext, response: Response, case: Case) -> bool | None:
|
223
224
|
from .schemas import BaseOpenAPISchema
|
224
225
|
|
225
|
-
if
|
226
|
+
if (
|
227
|
+
not isinstance(case.operation.schema, BaseOpenAPISchema)
|
228
|
+
or case.meta is None
|
229
|
+
or is_unexpected_http_status_case(case)
|
230
|
+
):
|
226
231
|
return True
|
227
232
|
|
228
233
|
config = ctx.config.negative_data_rejection
|
229
|
-
allowed_statuses = expand_status_codes(config.
|
234
|
+
allowed_statuses = expand_status_codes(config.expected_statuses or [])
|
230
235
|
|
231
236
|
if (
|
232
|
-
case.
|
233
|
-
and case.data_generation_method.is_negative
|
237
|
+
case.meta.generation.mode.is_negative
|
234
238
|
and response.status_code not in allowed_statuses
|
235
239
|
and not has_only_additional_properties_in_non_body_parameters(case)
|
236
240
|
):
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
message=message,
|
243
|
-
status_code=response.status_code,
|
244
|
-
allowed_statuses=config.allowed_statuses,
|
245
|
-
),
|
241
|
+
raise AcceptedNegativeData(
|
242
|
+
operation=case.operation.label,
|
243
|
+
message=f"Allowed statuses: {', '.join(config.expected_statuses)}",
|
244
|
+
status_code=response.status_code,
|
245
|
+
expected_statuses=config.expected_statuses,
|
246
246
|
)
|
247
247
|
return None
|
248
248
|
|
249
249
|
|
250
|
-
|
250
|
+
@schemathesis.check
|
251
|
+
def positive_data_acceptance(ctx: CheckContext, response: Response, case: Case) -> bool | None:
|
251
252
|
from .schemas import BaseOpenAPISchema
|
252
253
|
|
253
|
-
if
|
254
|
+
if (
|
255
|
+
not isinstance(case.operation.schema, BaseOpenAPISchema)
|
256
|
+
or case.meta is None
|
257
|
+
or is_unexpected_http_status_case(case)
|
258
|
+
):
|
254
259
|
return True
|
255
260
|
|
256
261
|
config = ctx.config.positive_data_acceptance
|
257
|
-
allowed_statuses = expand_status_codes(config.
|
258
|
-
|
259
|
-
if
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
exc_class = get_positive_acceptance_error(case.operation.verbose_name, response.status_code)
|
266
|
-
raise exc_class(
|
267
|
-
failures.RejectedPositiveData.title,
|
268
|
-
context=failures.RejectedPositiveData(
|
269
|
-
message=message,
|
270
|
-
status_code=response.status_code,
|
271
|
-
allowed_statuses=config.allowed_statuses,
|
272
|
-
),
|
262
|
+
allowed_statuses = expand_status_codes(config.expected_statuses or [])
|
263
|
+
|
264
|
+
if case.meta.generation.mode.is_positive and response.status_code not in allowed_statuses:
|
265
|
+
raise RejectedPositiveData(
|
266
|
+
operation=case.operation.label,
|
267
|
+
message=f"Allowed statuses: {', '.join(config.expected_statuses)}",
|
268
|
+
status_code=response.status_code,
|
269
|
+
allowed_statuses=config.expected_statuses,
|
273
270
|
)
|
274
271
|
return None
|
275
272
|
|
276
273
|
|
277
|
-
|
274
|
+
@schemathesis.check
|
275
|
+
def missing_required_header(ctx: CheckContext, response: Response, case: Case) -> bool | None:
|
276
|
+
meta = case.meta
|
277
|
+
if meta is None or not isinstance(meta.phase.data, CoveragePhaseData) or is_unexpected_http_status_case(case):
|
278
|
+
return None
|
279
|
+
data = meta.phase.data
|
278
280
|
if (
|
279
|
-
|
280
|
-
and
|
281
|
-
and
|
282
|
-
and
|
283
|
-
and case.meta.description.startswith("Missing ")
|
281
|
+
data.parameter
|
282
|
+
and data.parameter_location == "header"
|
283
|
+
and data.description
|
284
|
+
and data.description.startswith("Missing ")
|
284
285
|
):
|
285
|
-
if
|
286
|
-
|
286
|
+
if data.parameter.lower() == "authorization":
|
287
|
+
expected_statuses = {401}
|
287
288
|
else:
|
288
289
|
config = ctx.config.missing_required_header
|
289
|
-
|
290
|
-
if response.status_code not in
|
291
|
-
allowed =
|
292
|
-
raise
|
290
|
+
expected_statuses = expand_status_codes(config.expected_statuses or [])
|
291
|
+
if response.status_code not in expected_statuses:
|
292
|
+
allowed = ", ".join(map(str, expected_statuses))
|
293
|
+
raise MissingHeaderNotRejected(
|
294
|
+
operation=f"{case.method} {case.path}",
|
295
|
+
header_name=data.parameter,
|
296
|
+
status_code=response.status_code,
|
297
|
+
expected_statuses=list(expected_statuses),
|
298
|
+
message=f"Missing header not rejected (got {response.status_code}, expected {allowed})",
|
299
|
+
)
|
293
300
|
return None
|
294
301
|
|
295
302
|
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
):
|
303
|
+
@schemathesis.check
|
304
|
+
def unsupported_method(ctx: CheckContext, response: Response, case: Case) -> bool | None:
|
305
|
+
meta = case.meta
|
306
|
+
if meta is None or not isinstance(meta.phase.data, CoveragePhaseData) or response.request.method == "OPTIONS":
|
307
|
+
return None
|
308
|
+
data = meta.phase.data
|
309
|
+
if data.description and data.description.startswith("Unspecified HTTP method:"):
|
303
310
|
if response.status_code != 405:
|
304
|
-
raise
|
305
|
-
|
311
|
+
raise UnsupportedMethodResponse(
|
312
|
+
operation=case.operation.label,
|
313
|
+
method=cast(str, response.request.method),
|
314
|
+
status_code=response.status_code,
|
315
|
+
failure_reason="wrong_status",
|
316
|
+
message=f"Wrong status for unsupported method {response.request.method} (got {response.status_code}, expected 405)",
|
306
317
|
)
|
307
318
|
|
308
|
-
allow_header = response.headers.get("
|
319
|
+
allow_header = response.headers.get("allow")
|
309
320
|
if not allow_header:
|
310
|
-
raise
|
321
|
+
raise UnsupportedMethodResponse(
|
322
|
+
operation=case.operation.label,
|
323
|
+
method=cast(str, response.request.method),
|
324
|
+
status_code=response.status_code,
|
325
|
+
allow_header_present=False,
|
326
|
+
failure_reason="missing_allow_header",
|
327
|
+
message=f"Missing Allow header for unsupported method {response.request.method}",
|
328
|
+
)
|
311
329
|
return None
|
312
330
|
|
313
331
|
|
@@ -321,14 +339,17 @@ def has_only_additional_properties_in_non_body_parameters(case: Case) -> bool:
|
|
321
339
|
if meta is None:
|
322
340
|
# Ignore manually created cases
|
323
341
|
return False
|
324
|
-
if (meta.
|
342
|
+
if (ComponentKind.BODY in meta.components and meta.components[ComponentKind.BODY].mode.is_negative) or (
|
343
|
+
ComponentKind.PATH_PARAMETERS in meta.components
|
344
|
+
and meta.components[ComponentKind.PATH_PARAMETERS].mode.is_negative
|
345
|
+
):
|
325
346
|
# Body or path negations always imply other negations
|
326
347
|
return False
|
327
348
|
validator_cls = case.operation.schema.validator_cls # type: ignore[attr-defined]
|
328
|
-
for container in (
|
329
|
-
meta_for_location =
|
330
|
-
value = getattr(case, container)
|
331
|
-
if value is not None and meta_for_location is not None and meta_for_location.is_negative:
|
349
|
+
for container in (ComponentKind.QUERY, ComponentKind.HEADERS, ComponentKind.COOKIES):
|
350
|
+
meta_for_location = meta.components.get(container)
|
351
|
+
value = getattr(case, container.value)
|
352
|
+
if value is not None and meta_for_location is not None and meta_for_location.mode.is_negative:
|
332
353
|
parameters = getattr(case.operation, container)
|
333
354
|
value_without_additional_properties = {k: v for k, v in value.items() if k in parameters}
|
334
355
|
schema = get_schema_for_location(case.operation, container, parameters)
|
@@ -339,90 +360,134 @@ def has_only_additional_properties_in_non_body_parameters(case: Case) -> bool:
|
|
339
360
|
return True
|
340
361
|
|
341
362
|
|
342
|
-
|
343
|
-
|
363
|
+
@schemathesis.check
|
364
|
+
def use_after_free(ctx: CheckContext, response: Response, case: Case) -> bool | None:
|
344
365
|
from .schemas import BaseOpenAPISchema
|
345
366
|
|
346
|
-
if not isinstance(
|
367
|
+
if not isinstance(case.operation.schema, BaseOpenAPISchema) or is_unexpected_http_status_case(case):
|
347
368
|
return True
|
348
|
-
if response.status_code == 404 or
|
369
|
+
if response.status_code == 404 or response.status_code >= 500:
|
349
370
|
return None
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
371
|
+
|
372
|
+
for related_case in ctx._find_related(case_id=case.id):
|
373
|
+
parent = ctx._find_parent(case_id=related_case.id)
|
374
|
+
if not parent:
|
375
|
+
continue
|
376
|
+
|
377
|
+
parent_response = ctx._find_response(case_id=parent.id)
|
378
|
+
|
379
|
+
if (
|
380
|
+
related_case.operation.method.lower() == "delete"
|
381
|
+
and parent_response is not None
|
382
|
+
and 200 <= parent_response.status_code < 300
|
383
|
+
):
|
355
384
|
if _is_prefix_operation(
|
385
|
+
ResourcePath(related_case.path, related_case.path_parameters or {}),
|
356
386
|
ResourcePath(case.path, case.path_parameters or {}),
|
357
|
-
ResourcePath(original.path, original.path_parameters or {}),
|
358
387
|
):
|
359
|
-
free = f"{
|
360
|
-
usage = f"{
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
raise exc_class(
|
368
|
-
failures.UseAfterFree.title,
|
369
|
-
context=failures.UseAfterFree(
|
370
|
-
message=message,
|
371
|
-
free=free,
|
372
|
-
usage=usage,
|
388
|
+
free = f"{related_case.operation.method.upper()} {prepare_path(related_case.path, related_case.path_parameters)}"
|
389
|
+
usage = f"{case.operation.method.upper()} {prepare_path(case.path, case.path_parameters)}"
|
390
|
+
reason = http.client.responses.get(response.status_code, "Unknown")
|
391
|
+
raise UseAfterFree(
|
392
|
+
operation=related_case.operation.label,
|
393
|
+
message=(
|
394
|
+
"The API did not return a `HTTP 404 Not Found` response "
|
395
|
+
f"(got `HTTP {response.status_code} {reason}`) for a resource that was previously deleted.\n\nThe resource was deleted with `{free}`"
|
373
396
|
),
|
397
|
+
free=free,
|
398
|
+
usage=usage,
|
374
399
|
)
|
375
|
-
|
376
|
-
break
|
377
|
-
response = case.source.response
|
378
|
-
case = case.source.case
|
400
|
+
|
379
401
|
return None
|
380
402
|
|
381
403
|
|
382
|
-
|
383
|
-
|
404
|
+
@schemathesis.check
|
405
|
+
def ensure_resource_availability(ctx: CheckContext, response: Response, case: Case) -> bool | None:
|
384
406
|
from .schemas import BaseOpenAPISchema
|
385
407
|
|
386
|
-
if not isinstance(
|
408
|
+
if not isinstance(case.operation.schema, BaseOpenAPISchema) or is_unexpected_http_status_case(case):
|
387
409
|
return True
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
|
410
|
+
|
411
|
+
# First, check if this is a 4XX response
|
412
|
+
if not (400 <= response.status_code < 500):
|
413
|
+
return None
|
414
|
+
|
415
|
+
parent = ctx._find_parent(case_id=case.id)
|
416
|
+
if parent is None:
|
417
|
+
return None
|
418
|
+
parent_response = ctx._find_response(case_id=parent.id)
|
419
|
+
if parent_response is None:
|
420
|
+
return None
|
421
|
+
|
422
|
+
if not (
|
423
|
+
parent.operation.method.upper() == "POST"
|
424
|
+
and 200 <= parent_response.status_code < 400
|
397
425
|
and _is_prefix_operation(
|
398
|
-
ResourcePath(
|
399
|
-
ResourcePath(
|
426
|
+
ResourcePath(parent.path, parent.path_parameters or {}),
|
427
|
+
ResourcePath(case.path, case.path_parameters or {}),
|
400
428
|
)
|
401
429
|
):
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
|
430
|
+
return None
|
431
|
+
|
432
|
+
# Check if all parameters come from links
|
433
|
+
overrides = case._override
|
434
|
+
overrides_all_parameters = True
|
435
|
+
for parameter in case.operation.iter_parameters():
|
436
|
+
container = LOCATION_TO_CONTAINER[parameter.location]
|
437
|
+
if parameter.name not in getattr(overrides, container, {}):
|
438
|
+
overrides_all_parameters = False
|
439
|
+
break
|
440
|
+
if not overrides_all_parameters:
|
441
|
+
return None
|
442
|
+
|
443
|
+
# Look for any successful DELETE operations on this resource
|
444
|
+
for related_case in ctx._find_related(case_id=case.id):
|
445
|
+
related_response = ctx._find_response(case_id=related_case.id)
|
446
|
+
if (
|
447
|
+
related_case.operation.method.upper() == "DELETE"
|
448
|
+
and related_response is not None
|
449
|
+
and 200 <= related_response.status_code < 300
|
450
|
+
and _is_prefix_operation(
|
451
|
+
ResourcePath(related_case.path, related_case.path_parameters or {}),
|
452
|
+
ResourcePath(case.path, case.path_parameters or {}),
|
453
|
+
)
|
454
|
+
):
|
455
|
+
# Resource was properly deleted, 404 is expected
|
456
|
+
return None
|
457
|
+
|
458
|
+
# If we got here:
|
459
|
+
# 1. Resource was created successfully
|
460
|
+
# 2. Current operation returned 4XX
|
461
|
+
# 3. All parameters come from links
|
462
|
+
# 4. No successful DELETE operations found
|
463
|
+
created_with = parent.operation.label
|
464
|
+
not_available_with = case.operation.label
|
465
|
+
reason = http.client.responses.get(response.status_code, "Unknown")
|
466
|
+
raise EnsureResourceAvailability(
|
467
|
+
operation=created_with,
|
468
|
+
message=(
|
407
469
|
f"The API returned `{response.status_code} {reason}` for a resource that was just created.\n\n"
|
408
470
|
f"Created with : `{created_with}`\n"
|
409
471
|
f"Not available with: `{not_available_with}`"
|
410
|
-
)
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
|
472
|
+
),
|
473
|
+
created_with=created_with,
|
474
|
+
not_available_with=not_available_with,
|
475
|
+
)
|
476
|
+
|
477
|
+
|
478
|
+
class AuthScenario(str, enum.Enum):
|
479
|
+
NO_AUTH = "no_auth"
|
480
|
+
INVALID_AUTH = "invalid_auth"
|
481
|
+
GENERATED_AUTH = "generated_auth"
|
418
482
|
|
419
483
|
|
420
|
-
class AuthKind(enum.Enum):
|
484
|
+
class AuthKind(str, enum.Enum):
|
421
485
|
EXPLICIT = "explicit"
|
422
486
|
GENERATED = "generated"
|
423
487
|
|
424
488
|
|
425
|
-
|
489
|
+
@schemathesis.check
|
490
|
+
def ignored_auth(ctx: CheckContext, response: Response, case: Case) -> bool | None:
|
426
491
|
"""Check if an operation declares authentication as a requirement but does not actually enforce it."""
|
427
492
|
from .schemas import BaseOpenAPISchema
|
428
493
|
|
@@ -431,58 +496,69 @@ def ignored_auth(ctx: CheckContext, response: GenericResponse, case: Case) -> bo
|
|
431
496
|
security_parameters = _get_security_parameters(case.operation)
|
432
497
|
# Authentication is required for this API operation and response is successful
|
433
498
|
if security_parameters and 200 <= response.status_code < 300:
|
434
|
-
auth = _contains_auth(ctx, case, response
|
499
|
+
auth = _contains_auth(ctx, case, response, security_parameters)
|
435
500
|
if auth == AuthKind.EXPLICIT:
|
436
501
|
# Auth is explicitly set, it is expected to be valid
|
437
502
|
# Check if invalid auth will give an error
|
438
|
-
|
439
|
-
kwargs = ctx.
|
503
|
+
no_auth_case = remove_auth(case, security_parameters)
|
504
|
+
kwargs = ctx._transport_kwargs or {}
|
440
505
|
kwargs.copy()
|
441
|
-
|
442
|
-
|
443
|
-
|
444
|
-
|
506
|
+
for location, container_name in (
|
507
|
+
("header", "headers"),
|
508
|
+
("cookie", "cookies"),
|
509
|
+
("query", "query"),
|
510
|
+
):
|
511
|
+
if container_name in kwargs:
|
512
|
+
container = kwargs[container_name].copy()
|
513
|
+
_remove_auth_from_container(container, security_parameters, location=location)
|
514
|
+
kwargs[container_name] = container
|
445
515
|
kwargs.pop("session", None)
|
446
|
-
|
447
|
-
|
448
|
-
|
449
|
-
|
516
|
+
ctx._record_case(parent_id=case.id, case=no_auth_case)
|
517
|
+
no_auth_response = case.operation.schema.transport.send(no_auth_case, **kwargs)
|
518
|
+
ctx._record_response(case_id=no_auth_case.id, response=no_auth_response)
|
519
|
+
if no_auth_response.status_code != 401:
|
520
|
+
_raise_no_auth_error(no_auth_response, no_auth_case, AuthScenario.NO_AUTH)
|
450
521
|
# Try to set invalid auth and check if it succeeds
|
451
522
|
for parameter in security_parameters:
|
452
|
-
|
453
|
-
|
454
|
-
|
455
|
-
|
456
|
-
|
457
|
-
|
523
|
+
invalid_auth_case = remove_auth(case, security_parameters)
|
524
|
+
_set_auth_for_case(invalid_auth_case, parameter)
|
525
|
+
ctx._record_case(parent_id=case.id, case=invalid_auth_case)
|
526
|
+
invalid_auth_response = case.operation.schema.transport.send(invalid_auth_case, **kwargs)
|
527
|
+
ctx._record_response(case_id=invalid_auth_case.id, response=invalid_auth_response)
|
528
|
+
if invalid_auth_response.status_code != 401:
|
529
|
+
_raise_no_auth_error(invalid_auth_response, invalid_auth_case, AuthScenario.INVALID_AUTH)
|
458
530
|
elif auth == AuthKind.GENERATED:
|
459
531
|
# If this auth is generated which means it is likely invalid, then
|
460
532
|
# this request should have been an error
|
461
|
-
_raise_no_auth_error(response, case
|
533
|
+
_raise_no_auth_error(response, case, AuthScenario.GENERATED_AUTH)
|
462
534
|
else:
|
463
535
|
# Successful response when there is no auth
|
464
|
-
_raise_no_auth_error(response, case
|
536
|
+
_raise_no_auth_error(response, case, AuthScenario.NO_AUTH)
|
465
537
|
return None
|
466
538
|
|
467
539
|
|
468
|
-
def
|
469
|
-
|
470
|
-
if hasattr(old, "__attrs__"):
|
471
|
-
for attribute in new.__attrs__:
|
472
|
-
setattr(old, attribute, getattr(new, attribute))
|
473
|
-
else:
|
474
|
-
old.__dict__.update(new.__dict__)
|
540
|
+
def _raise_no_auth_error(response: Response, case: Case, auth: AuthScenario) -> NoReturn:
|
541
|
+
reason = http.client.responses.get(response.status_code, "Unknown")
|
475
542
|
|
476
|
-
|
477
|
-
|
478
|
-
|
479
|
-
|
480
|
-
|
481
|
-
|
482
|
-
|
483
|
-
|
484
|
-
|
485
|
-
|
543
|
+
if auth == AuthScenario.NO_AUTH:
|
544
|
+
title = "API accepts requests without authentication"
|
545
|
+
detail = None
|
546
|
+
elif auth == AuthScenario.INVALID_AUTH:
|
547
|
+
title = "API accepts invalid authentication"
|
548
|
+
detail = "invalid credentials provided"
|
549
|
+
else:
|
550
|
+
title = "API accepts invalid authentication"
|
551
|
+
detail = "generated auth likely invalid"
|
552
|
+
|
553
|
+
message = f"Expected 401, got `{response.status_code} {reason}` for `{case.operation.label}`"
|
554
|
+
if detail is not None:
|
555
|
+
message = f"{message} ({detail})"
|
556
|
+
|
557
|
+
raise IgnoredAuth(
|
558
|
+
operation=case.operation.label,
|
559
|
+
message=message,
|
560
|
+
title=title,
|
561
|
+
case_id=case.id,
|
486
562
|
)
|
487
563
|
|
488
564
|
|
@@ -502,14 +578,15 @@ def _get_security_parameters(operation: APIOperation) -> list[SecurityParameter]
|
|
502
578
|
|
503
579
|
|
504
580
|
def _contains_auth(
|
505
|
-
ctx: CheckContext, case: Case,
|
581
|
+
ctx: CheckContext, case: Case, response: Response, security_parameters: list[SecurityParameter]
|
506
582
|
) -> AuthKind | None:
|
507
583
|
"""Whether a request has authentication declared in the schema."""
|
508
584
|
from requests.cookies import RequestsCookieJar
|
509
585
|
|
510
586
|
# If auth comes from explicit `auth` option or a custom auth, it is always explicit
|
511
|
-
if ctx.
|
587
|
+
if ctx._auth is not None or case._has_explicit_auth:
|
512
588
|
return AuthKind.EXPLICIT
|
589
|
+
request = response.request
|
513
590
|
parsed = urlparse(request.url)
|
514
591
|
query = parse_qs(parsed.query) # type: ignore
|
515
592
|
# Load the `Cookie` header separately, because it is possible that `request._cookies` and the header are out of sync
|
@@ -531,45 +608,76 @@ def _contains_auth(
|
|
531
608
|
for parameter in security_parameters:
|
532
609
|
name = parameter["name"]
|
533
610
|
if has_header(parameter):
|
534
|
-
if (
|
611
|
+
if (
|
612
|
+
# Explicit CLI headers
|
613
|
+
(ctx._headers is not None and name in ctx._headers)
|
614
|
+
# Other kinds of overrides
|
615
|
+
or (ctx._override and name in ctx._override.headers)
|
616
|
+
or (response._override and name in response._override.headers)
|
617
|
+
):
|
535
618
|
return AuthKind.EXPLICIT
|
536
619
|
return AuthKind.GENERATED
|
537
620
|
if has_cookie(parameter):
|
538
|
-
|
539
|
-
|
540
|
-
if
|
541
|
-
|
542
|
-
|
621
|
+
for headers in [
|
622
|
+
ctx._headers,
|
623
|
+
(ctx._override.headers if ctx._override else None),
|
624
|
+
(response._override.headers if response._override else None),
|
625
|
+
]:
|
626
|
+
if headers is not None and "Cookie" in headers:
|
627
|
+
jar = cast(RequestsCookieJar, headers["Cookie"])
|
628
|
+
if name in jar:
|
629
|
+
return AuthKind.EXPLICIT
|
630
|
+
|
631
|
+
if (ctx._override and name in ctx._override.cookies) or (
|
632
|
+
response._override and name in response._override.cookies
|
633
|
+
):
|
543
634
|
return AuthKind.EXPLICIT
|
544
635
|
return AuthKind.GENERATED
|
545
636
|
if has_query(parameter):
|
546
|
-
if ctx.
|
637
|
+
if (ctx._override and name in ctx._override.query) or (
|
638
|
+
response._override and name in response._override.query
|
639
|
+
):
|
547
640
|
return AuthKind.EXPLICIT
|
548
641
|
return AuthKind.GENERATED
|
549
642
|
|
550
643
|
return None
|
551
644
|
|
552
645
|
|
553
|
-
def
|
646
|
+
def remove_auth(case: Case, security_parameters: list[SecurityParameter]) -> Case:
|
554
647
|
"""Remove security parameters from a generated case.
|
555
648
|
|
556
649
|
It mutates `case` in place.
|
557
650
|
"""
|
651
|
+
headers = case.headers.copy()
|
652
|
+
query = case.query.copy()
|
653
|
+
cookies = case.cookies.copy()
|
558
654
|
for parameter in security_parameters:
|
559
655
|
name = parameter["name"]
|
560
|
-
if parameter["in"] == "header" and
|
561
|
-
|
562
|
-
if parameter["in"] == "query" and
|
563
|
-
|
564
|
-
if parameter["in"] == "cookie" and
|
565
|
-
|
656
|
+
if parameter["in"] == "header" and headers:
|
657
|
+
headers.pop(name, None)
|
658
|
+
if parameter["in"] == "query" and query:
|
659
|
+
query.pop(name, None)
|
660
|
+
if parameter["in"] == "cookie" and cookies:
|
661
|
+
cookies.pop(name, None)
|
662
|
+
return Case(
|
663
|
+
operation=case.operation,
|
664
|
+
method=case.method,
|
665
|
+
path=case.path,
|
666
|
+
path_parameters=case.path_parameters.copy(),
|
667
|
+
headers=headers,
|
668
|
+
cookies=cookies,
|
669
|
+
query=query,
|
670
|
+
body=case.body.copy() if isinstance(case.body, (list, dict)) else case.body,
|
671
|
+
media_type=case.media_type,
|
672
|
+
meta=case.meta,
|
673
|
+
)
|
566
674
|
|
567
675
|
|
568
|
-
def
|
676
|
+
def _remove_auth_from_container(container: dict, security_parameters: list[SecurityParameter], location: str) -> None:
|
569
677
|
for parameter in security_parameters:
|
570
678
|
name = parameter["name"]
|
571
|
-
if parameter["in"] ==
|
572
|
-
|
679
|
+
if parameter["in"] == location:
|
680
|
+
container.pop(name, None)
|
573
681
|
|
574
682
|
|
575
683
|
def _set_auth_for_case(case: Case, parameter: SecurityParameter) -> None:
|
@@ -581,6 +689,9 @@ def _set_auth_for_case(case: Case, parameter: SecurityParameter) -> None:
|
|
581
689
|
):
|
582
690
|
if parameter["in"] == location:
|
583
691
|
container = getattr(case, attr_name, {})
|
692
|
+
# Could happen in the negative testing mode
|
693
|
+
if not isinstance(container, dict):
|
694
|
+
container = {}
|
584
695
|
container[name] = "SCHEMATHESIS-INVALID-VALUE"
|
585
696
|
setattr(case, attr_name, container)
|
586
697
|
|