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 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 = parameters_to_json_schema(operation, parameters)
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 case.data_generation_method and case.data_generation_method.is_negative and 200 <= response.status_code < 300:
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
- # Headers are always strings, can't negate this
207
- if context.is_header_location:
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=cls, resolver=resolver)
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.0
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=4kAiutx5BEZ4h4AHMvZVW7gJpObur7kkGFuuF2cTEkU,44491
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=9O8gTVWtq17UezgvlxHxjTHGVmggQ2mxwAnVIvZKgsk,23619
102
- schemathesis/specs/openapi/checks.py,sha256=1Fu3Kgai9ySCoGtCrx99Q9oVCEWXgkqHd1gTqG_569s,9364
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=UMPfQKndW7HRAeLNlu0zmZVmJMaD7CvRJ8p-c8a93uc,52204
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=JRTkJNO9njma3xTYYoxSVpQgz-CfeMBLv4NDNPGiogI,18644
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.0.dist-info/METADATA,sha256=xq28YKL2SXo0XpmPf68tqEGoY8khEIjTEnoEPkV4tPo,17706
148
- schemathesis-3.31.0.dist-info/WHEEL,sha256=hKi7AIIx6qfnsRbr087vpeJnrVUuDokDHZacPPMW7-Y,87
149
- schemathesis-3.31.0.dist-info/entry_points.txt,sha256=VHyLcOG7co0nOeuk8WjgpRETk5P1E2iCLrn26Zkn5uk,158
150
- schemathesis-3.31.0.dist-info/licenses/LICENSE,sha256=PsPYgrDhZ7g9uwihJXNG-XVb55wj2uYhkl2DD8oAzY0,1103
151
- schemathesis-3.31.0.dist-info/RECORD,,
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,,