schemathesis 3.31.0__py3-none-any.whl → 3.31.1__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/models.py +15 -0
- schemathesis/specs/openapi/_hypothesis.py +26 -13
- schemathesis/specs/openapi/checks.py +34 -1
- schemathesis/specs/openapi/negative/mutations.py +9 -2
- schemathesis/specs/openapi/schemas.py +8 -5
- {schemathesis-3.31.0.dist-info → schemathesis-3.31.1.dist-info}/METADATA +1 -1
- {schemathesis-3.31.0.dist-info → schemathesis-3.31.1.dist-info}/RECORD +10 -10
- {schemathesis-3.31.0.dist-info → schemathesis-3.31.1.dist-info}/WHEEL +0 -0
- {schemathesis-3.31.0.dist-info → schemathesis-3.31.1.dist-info}/entry_points.txt +0 -0
- {schemathesis-3.31.0.dist-info → schemathesis-3.31.1.dist-info}/licenses/LICENSE +0 -0
schemathesis/models.py
CHANGED
|
@@ -119,6 +119,19 @@ def prepare_request_data(kwargs: dict[str, Any]) -> PreparedRequestData:
|
|
|
119
119
|
)
|
|
120
120
|
|
|
121
121
|
|
|
122
|
+
@dataclass
|
|
123
|
+
class GenerationMetadata:
|
|
124
|
+
"""Stores various information about how data is generated."""
|
|
125
|
+
|
|
126
|
+
query: DataGenerationMethod | None
|
|
127
|
+
path_parameters: DataGenerationMethod | None
|
|
128
|
+
headers: DataGenerationMethod | None
|
|
129
|
+
cookies: DataGenerationMethod | None
|
|
130
|
+
body: DataGenerationMethod | None
|
|
131
|
+
|
|
132
|
+
__slots__ = ("query", "path_parameters", "headers", "cookies", "body")
|
|
133
|
+
|
|
134
|
+
|
|
122
135
|
@dataclass(repr=False)
|
|
123
136
|
class Case:
|
|
124
137
|
"""A single test case parameters."""
|
|
@@ -139,6 +152,8 @@ class Case:
|
|
|
139
152
|
media_type: str | None = None
|
|
140
153
|
source: CaseSource | None = None
|
|
141
154
|
|
|
155
|
+
meta: GenerationMetadata | None = None
|
|
156
|
+
|
|
142
157
|
# The way the case was generated (None for manually crafted ones)
|
|
143
158
|
data_generation_method: DataGenerationMethod | None = None
|
|
144
159
|
_auth: requests.auth.AuthBase | None = None
|
|
@@ -25,7 +25,7 @@ from ...generation import DataGenerationMethod, GenerationConfig
|
|
|
25
25
|
from ...hooks import HookContext, HookDispatcher, apply_to_all_dispatchers
|
|
26
26
|
from ...internal.copy import fast_deepcopy
|
|
27
27
|
from ...internal.validation import is_illegal_surrogate
|
|
28
|
-
from ...models import APIOperation, Case, cant_serialize
|
|
28
|
+
from ...models import APIOperation, Case, GenerationMetadata, cant_serialize
|
|
29
29
|
from ...serializers import Binary
|
|
30
30
|
from ...transports.content_types import parse_content_type
|
|
31
31
|
from ...transports.headers import has_invalid_characters, is_latin_1_encodable
|
|
@@ -36,7 +36,7 @@ from .formats import STRING_FORMATS
|
|
|
36
36
|
from .media_types import MEDIA_TYPES
|
|
37
37
|
from .negative import negative_schema
|
|
38
38
|
from .negative.utils import can_negate
|
|
39
|
-
from .parameters import OpenAPIBody, parameters_to_json_schema
|
|
39
|
+
from .parameters import OpenAPIBody, OpenAPIParameter, parameters_to_json_schema
|
|
40
40
|
from .utils import is_header_location
|
|
41
41
|
|
|
42
42
|
HEADER_FORMAT = "_header_value"
|
|
@@ -207,6 +207,13 @@ def get_case_strategy(
|
|
|
207
207
|
query=query_.value,
|
|
208
208
|
body=body_.value,
|
|
209
209
|
data_generation_method=generator,
|
|
210
|
+
meta=GenerationMetadata(
|
|
211
|
+
query=query_.generator,
|
|
212
|
+
path_parameters=path_parameters_.generator,
|
|
213
|
+
headers=headers_.generator,
|
|
214
|
+
cookies=cookies_.generator,
|
|
215
|
+
body=body_.generator,
|
|
216
|
+
),
|
|
210
217
|
)
|
|
211
218
|
auth_context = auths.AuthContext(
|
|
212
219
|
operation=operation,
|
|
@@ -347,6 +354,22 @@ def can_negate_headers(operation: APIOperation, location: str) -> bool:
|
|
|
347
354
|
return any(header != {"type": "string"} for header in headers.values())
|
|
348
355
|
|
|
349
356
|
|
|
357
|
+
def get_schema_for_location(
|
|
358
|
+
operation: APIOperation, location: str, parameters: Iterable[OpenAPIParameter]
|
|
359
|
+
) -> dict[str, Any]:
|
|
360
|
+
schema = parameters_to_json_schema(operation, parameters)
|
|
361
|
+
if location == "path":
|
|
362
|
+
if not operation.schema.validate_schema:
|
|
363
|
+
# If schema validation is disabled, we try to generate data even if the parameter definition
|
|
364
|
+
# contains errors.
|
|
365
|
+
# In this case, we know that the `required` keyword should always be `True`.
|
|
366
|
+
schema["required"] = list(schema["properties"])
|
|
367
|
+
for prop in schema.get("properties", {}).values():
|
|
368
|
+
if prop.get("type") == "string":
|
|
369
|
+
prop.setdefault("minLength", 1)
|
|
370
|
+
return operation.schema.prepare_schema(schema)
|
|
371
|
+
|
|
372
|
+
|
|
350
373
|
def get_parameters_strategy(
|
|
351
374
|
operation: APIOperation,
|
|
352
375
|
strategy_factory: StrategyFactory,
|
|
@@ -361,17 +384,7 @@ def get_parameters_strategy(
|
|
|
361
384
|
nested_cache_key = (strategy_factory, location, tuple(sorted(exclude)))
|
|
362
385
|
if operation in _PARAMETER_STRATEGIES_CACHE and nested_cache_key in _PARAMETER_STRATEGIES_CACHE[operation]:
|
|
363
386
|
return _PARAMETER_STRATEGIES_CACHE[operation][nested_cache_key]
|
|
364
|
-
schema =
|
|
365
|
-
if location == "path":
|
|
366
|
-
if not operation.schema.validate_schema:
|
|
367
|
-
# If schema validation is disabled, we try to generate data even if the parameter definition
|
|
368
|
-
# contains errors.
|
|
369
|
-
# In this case, we know that the `required` keyword should always be `True`.
|
|
370
|
-
schema["required"] = list(schema["properties"])
|
|
371
|
-
for prop in schema.get("properties", {}).values():
|
|
372
|
-
if prop.get("type") == "string":
|
|
373
|
-
prop.setdefault("minLength", 1)
|
|
374
|
-
schema = operation.schema.prepare_schema(schema)
|
|
387
|
+
schema = get_schema_for_location(operation, location, parameters)
|
|
375
388
|
for name in exclude:
|
|
376
389
|
# Values from `exclude` are not necessarily valid for the schema - they come from user-defined examples
|
|
377
390
|
# that may be invalid
|
|
@@ -145,7 +145,12 @@ def negative_data_rejection(response: GenericResponse, case: Case) -> bool | Non
|
|
|
145
145
|
|
|
146
146
|
if not isinstance(case.operation.schema, BaseOpenAPISchema):
|
|
147
147
|
return True
|
|
148
|
-
if
|
|
148
|
+
if (
|
|
149
|
+
case.data_generation_method
|
|
150
|
+
and case.data_generation_method.is_negative
|
|
151
|
+
and 200 <= response.status_code < 300
|
|
152
|
+
and not has_only_additional_properties_in_non_body_parameters(case)
|
|
153
|
+
):
|
|
149
154
|
exc_class = get_negative_rejection_error(case.operation.verbose_name, response.status_code)
|
|
150
155
|
raise exc_class(
|
|
151
156
|
failures.AcceptedNegativeData.title,
|
|
@@ -154,6 +159,34 @@ def negative_data_rejection(response: GenericResponse, case: Case) -> bool | Non
|
|
|
154
159
|
return None
|
|
155
160
|
|
|
156
161
|
|
|
162
|
+
def has_only_additional_properties_in_non_body_parameters(case: Case) -> bool:
|
|
163
|
+
# Check if the case contains only additional properties in query, headers, or cookies.
|
|
164
|
+
# This function is used to determine if negation is solely in the form of extra properties,
|
|
165
|
+
# which are often ignored for backward-compatibility by the tested apps
|
|
166
|
+
from ._hypothesis import get_schema_for_location
|
|
167
|
+
|
|
168
|
+
meta = case.meta
|
|
169
|
+
if meta is None:
|
|
170
|
+
# Ignore manually created cases
|
|
171
|
+
return False
|
|
172
|
+
if (meta.body and meta.body.is_negative) or (meta.path_parameters and meta.path_parameters.is_negative):
|
|
173
|
+
# Body or path negations always imply other negations
|
|
174
|
+
return False
|
|
175
|
+
validator_cls = case.operation.schema.validator_cls # type: ignore[attr-defined]
|
|
176
|
+
for container in ("query", "headers", "cookies"):
|
|
177
|
+
meta_for_location = getattr(meta, container)
|
|
178
|
+
value = getattr(case, container)
|
|
179
|
+
if value is not None and meta_for_location is not None and meta_for_location.is_negative:
|
|
180
|
+
parameters = getattr(case.operation, container)
|
|
181
|
+
value_without_additional_properties = {k: v for k, v in value.items() if k in parameters}
|
|
182
|
+
schema = get_schema_for_location(case.operation, container, parameters)
|
|
183
|
+
if not validator_cls(schema).is_valid(value_without_additional_properties):
|
|
184
|
+
# Other types of negation found
|
|
185
|
+
return False
|
|
186
|
+
# Only additional properties are added
|
|
187
|
+
return True
|
|
188
|
+
|
|
189
|
+
|
|
157
190
|
def use_after_free(response: GenericResponse, original: Case) -> bool | None:
|
|
158
191
|
from ...transports.responses import get_reason
|
|
159
192
|
from .schemas import BaseOpenAPISchema
|
|
@@ -81,6 +81,10 @@ class MutationContext:
|
|
|
81
81
|
def is_path_location(self) -> bool:
|
|
82
82
|
return self.location == "path"
|
|
83
83
|
|
|
84
|
+
@property
|
|
85
|
+
def is_query_location(self) -> bool:
|
|
86
|
+
return self.location == "query"
|
|
87
|
+
|
|
84
88
|
def mutate(self, draw: Draw) -> Schema:
|
|
85
89
|
# On the top level, Schemathesis creates "object" schemas for all parameter "in" values except "body", which is
|
|
86
90
|
# taken as-is. Therefore, we can only apply mutations that won't change the Open API semantics of the schema.
|
|
@@ -203,8 +207,11 @@ def change_type(context: MutationContext, draw: Draw, schema: Schema) -> Mutatio
|
|
|
203
207
|
if context.media_type == "application/x-www-form-urlencoded":
|
|
204
208
|
# Form data should be an object, do not change it
|
|
205
209
|
return MutationResult.FAILURE
|
|
206
|
-
#
|
|
207
|
-
|
|
210
|
+
# For headers, query and path parameters, if the current type is string, then it already
|
|
211
|
+
# includes all possible values as those parameters will be stringified before sending,
|
|
212
|
+
# therefore it can't be negated.
|
|
213
|
+
types = get_type(schema)
|
|
214
|
+
if "string" in types and (context.is_header_location or context.is_path_location or context.is_query_location):
|
|
208
215
|
return MutationResult.FAILURE
|
|
209
216
|
candidates = _get_type_candidates(context, schema)
|
|
210
217
|
if not candidates:
|
|
@@ -19,6 +19,7 @@ from typing import (
|
|
|
19
19
|
Mapping,
|
|
20
20
|
NoReturn,
|
|
21
21
|
Sequence,
|
|
22
|
+
Type,
|
|
22
23
|
TypeVar,
|
|
23
24
|
cast,
|
|
24
25
|
)
|
|
@@ -615,6 +616,12 @@ class BaseOpenAPISchema(BaseSchema):
|
|
|
615
616
|
def get_tags(self, operation: APIOperation) -> list[str] | None:
|
|
616
617
|
return operation.definition.raw.get("tags")
|
|
617
618
|
|
|
619
|
+
@property
|
|
620
|
+
def validator_cls(self) -> Type[jsonschema.Validator]:
|
|
621
|
+
if self.spec_version.startswith("3.1") and experimental.OPEN_API_3_1.is_enabled:
|
|
622
|
+
return jsonschema.Draft202012Validator
|
|
623
|
+
return jsonschema.Draft4Validator
|
|
624
|
+
|
|
618
625
|
def validate_response(self, operation: APIOperation, response: GenericResponse) -> bool | None:
|
|
619
626
|
responses = {str(key): value for key, value in operation.definition.raw.get("responses", {}).items()}
|
|
620
627
|
status_code = str(response.status_code)
|
|
@@ -658,13 +665,9 @@ class BaseOpenAPISchema(BaseSchema):
|
|
|
658
665
|
resolver = ConvertingResolver(
|
|
659
666
|
self.location or "", self.raw_schema, nullable_name=self.nullable_name, is_response_schema=True
|
|
660
667
|
)
|
|
661
|
-
if self.spec_version.startswith("3.1") and experimental.OPEN_API_3_1.is_enabled:
|
|
662
|
-
cls = jsonschema.Draft202012Validator
|
|
663
|
-
else:
|
|
664
|
-
cls = jsonschema.Draft4Validator
|
|
665
668
|
with in_scopes(resolver, scopes):
|
|
666
669
|
try:
|
|
667
|
-
jsonschema.validate(data, schema, cls=
|
|
670
|
+
jsonschema.validate(data, schema, cls=self.validator_cls, resolver=resolver)
|
|
668
671
|
except jsonschema.ValidationError as exc:
|
|
669
672
|
exc_class = get_schema_validation_error(operation.verbose_name, exc)
|
|
670
673
|
ctx = failures.ValidationErrorContext.from_exception(exc, output_config=operation.schema.output_config)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: schemathesis
|
|
3
|
-
Version: 3.31.
|
|
3
|
+
Version: 3.31.1
|
|
4
4
|
Summary: Property-based testing framework for Open API and GraphQL based apps
|
|
5
5
|
Project-URL: Documentation, https://schemathesis.readthedocs.io/en/stable/
|
|
6
6
|
Project-URL: Changelog, https://schemathesis.readthedocs.io/en/stable/changelog.html
|
|
@@ -17,7 +17,7 @@ schemathesis/graphql.py,sha256=YkoKWY5K8lxp7H3ikAs-IsoDbiPwJvChG7O8p3DgwtI,229
|
|
|
17
17
|
schemathesis/hooks.py,sha256=dveqMmThIvt4fDahUXhU2nCq5pFvYjzzd1Ys_MhrJZA,12398
|
|
18
18
|
schemathesis/lazy.py,sha256=eVdGkTZK0fWvUlFUCFGGlViH2NWEtYIjxiNkF4fBWhI,15218
|
|
19
19
|
schemathesis/loaders.py,sha256=OtCD1o0TVmSNAUF7dgHpouoAXtY6w9vEtsRVGv4lE0g,4588
|
|
20
|
-
schemathesis/models.py,sha256=
|
|
20
|
+
schemathesis/models.py,sha256=nbm9Agqw94RYib2Q4OH7iOiEPDGw64NlQnEB7o5Spio,44925
|
|
21
21
|
schemathesis/parameters.py,sha256=PndmqQRlEYsCt1kWjSShPsFf6vj7X_7FRdz_-A95eNg,2258
|
|
22
22
|
schemathesis/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
23
23
|
schemathesis/sanitization.py,sha256=mRR4YvXpzqbmgX8Xu6rume6LBcz9g_oyusvbesZl44I,8958
|
|
@@ -98,8 +98,8 @@ schemathesis/specs/graphql/schemas.py,sha256=i6fAW9pYcOplQE7BejP6P8GQ9z6Y43Vx4_f
|
|
|
98
98
|
schemathesis/specs/graphql/validation.py,sha256=uINIOt-2E7ZuQV2CxKzwez-7L9tDtqzMSpnVoRWvxy0,1635
|
|
99
99
|
schemathesis/specs/openapi/__init__.py,sha256=HDcx3bqpa6qWPpyMrxAbM3uTo0Lqpg-BUNZhDJSJKnw,279
|
|
100
100
|
schemathesis/specs/openapi/_cache.py,sha256=PAiAu4X_a2PQgD2lG5H3iisXdyg4SaHpU46bRZvfNkM,4320
|
|
101
|
-
schemathesis/specs/openapi/_hypothesis.py,sha256=
|
|
102
|
-
schemathesis/specs/openapi/checks.py,sha256=
|
|
101
|
+
schemathesis/specs/openapi/_hypothesis.py,sha256=hPctM9QN4mGZrEPnLbesoPEDrSF4TCyI5RMJUmchhnQ,24070
|
|
102
|
+
schemathesis/specs/openapi/checks.py,sha256=eFjbV9-0202qg0s0JjaBNRr7nf8Bd8VMLPHEfMvZoc4,10967
|
|
103
103
|
schemathesis/specs/openapi/constants.py,sha256=JqM_FHOenqS_MuUE9sxVQ8Hnw0DNM8cnKDwCwPLhID4,783
|
|
104
104
|
schemathesis/specs/openapi/converter.py,sha256=TaYgc5BBHPdkN-n0lqpbeVgLu3eL3L8Wu3y_Vo3TJaQ,2800
|
|
105
105
|
schemathesis/specs/openapi/definitions.py,sha256=Z186F0gNBSCmPg-Kk7Q-n6XxEZHIOzgUyeqixlC62XE,94058
|
|
@@ -111,7 +111,7 @@ schemathesis/specs/openapi/loaders.py,sha256=JJdIz1aT03J9WmUWTLOz6Yhuu69IqmhobQ9
|
|
|
111
111
|
schemathesis/specs/openapi/media_types.py,sha256=dNTxpRQbY3SubdVjh4Cjb38R6Bc9MF9BsRQwPD87x0g,1017
|
|
112
112
|
schemathesis/specs/openapi/parameters.py,sha256=_6vNCnPXcdxjfAQbykCRLHjvmTpu_02xDJghxDrGYr8,13611
|
|
113
113
|
schemathesis/specs/openapi/references.py,sha256=euxM02kQGMHh4Ss1jWjOY_gyw_HazafKITIsvOEiAvI,9831
|
|
114
|
-
schemathesis/specs/openapi/schemas.py,sha256=
|
|
114
|
+
schemathesis/specs/openapi/schemas.py,sha256=sZXzw4ToOpTbrjFQdvCwGymaCWW1b_ofzm7jJQK03kI,52287
|
|
115
115
|
schemathesis/specs/openapi/security.py,sha256=nEhDB_SvEFldmfpa9uOQywfWN6DtXHKmgtwucJvfN5Q,7096
|
|
116
116
|
schemathesis/specs/openapi/serialization.py,sha256=5qGdFHZ3n80UlbSXrO_bkr4Al_7ci_Z3aSUjZczNDQY,11384
|
|
117
117
|
schemathesis/specs/openapi/utils.py,sha256=-TCu0hTrlwp2x5qHNp-TxiHRMeIZC9OBmlhLssjRIiQ,742
|
|
@@ -124,7 +124,7 @@ schemathesis/specs/openapi/expressions/lexer.py,sha256=LeVE6fgYT9-fIsXrv0-YrRHnI
|
|
|
124
124
|
schemathesis/specs/openapi/expressions/nodes.py,sha256=DUbAtuXdUDsxZ_pGeCVXAlL3gTj8nt9KulMGaIS-N2I,3948
|
|
125
125
|
schemathesis/specs/openapi/expressions/parser.py,sha256=gM_Ob-TlTGxpgjZGRHNyPhBj1YAvRgRoSlNCrE7-djk,4452
|
|
126
126
|
schemathesis/specs/openapi/negative/__init__.py,sha256=gw0w_9tVQf_MY5Df3_xTZFC4rAy1TTBS4wBccm36uFs,3697
|
|
127
|
-
schemathesis/specs/openapi/negative/mutations.py,sha256=
|
|
127
|
+
schemathesis/specs/openapi/negative/mutations.py,sha256=lLEN0GLxvPmZBQ3tHCznDSjmZ4yQiQxspjv1UpO4Kx0,19019
|
|
128
128
|
schemathesis/specs/openapi/negative/types.py,sha256=a7buCcVxNBG6ILBM3A7oNTAX0lyDseEtZndBuej8MbI,174
|
|
129
129
|
schemathesis/specs/openapi/negative/utils.py,sha256=ozcOIuASufLqZSgnKUACjX-EOZrrkuNdXX0SDnLoGYA,168
|
|
130
130
|
schemathesis/specs/openapi/stateful/__init__.py,sha256=fAA52Nk0olm46u1e6OtZTXwmFM5W2r0jhRhZ24iz7GY,7673
|
|
@@ -144,8 +144,8 @@ schemathesis/transports/auth.py,sha256=4z7c-K7lfyyVqgR6X1v4yiE8ewR_ViAznWFTAsCL0
|
|
|
144
144
|
schemathesis/transports/content_types.py,sha256=VrcRQvF5T_TUjrCyrZcYF2LOwKfs3IrLcMtkVSp1ImI,2189
|
|
145
145
|
schemathesis/transports/headers.py,sha256=hr_AIDOfUxsJxpHfemIZ_uNG3_vzS_ZeMEKmZjbYiBE,990
|
|
146
146
|
schemathesis/transports/responses.py,sha256=6-gvVcRK0Ho_lSydUysBNFWoJwZEiEgf6Iv-GWkQGd8,1675
|
|
147
|
-
schemathesis-3.31.
|
|
148
|
-
schemathesis-3.31.
|
|
149
|
-
schemathesis-3.31.
|
|
150
|
-
schemathesis-3.31.
|
|
151
|
-
schemathesis-3.31.
|
|
147
|
+
schemathesis-3.31.1.dist-info/METADATA,sha256=W2PcjGom3XH8VuWRWR7kqLv12ZUliovQllOYeyMsnqk,17706
|
|
148
|
+
schemathesis-3.31.1.dist-info/WHEEL,sha256=hKi7AIIx6qfnsRbr087vpeJnrVUuDokDHZacPPMW7-Y,87
|
|
149
|
+
schemathesis-3.31.1.dist-info/entry_points.txt,sha256=VHyLcOG7co0nOeuk8WjgpRETk5P1E2iCLrn26Zkn5uk,158
|
|
150
|
+
schemathesis-3.31.1.dist-info/licenses/LICENSE,sha256=PsPYgrDhZ7g9uwihJXNG-XVb55wj2uYhkl2DD8oAzY0,1103
|
|
151
|
+
schemathesis-3.31.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|