schemathesis 3.38.1__py3-none-any.whl → 3.38.3__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/_hypothesis.py +101 -35
- schemathesis/schemas.py +4 -0
- {schemathesis-3.38.1.dist-info → schemathesis-3.38.3.dist-info}/METADATA +1 -1
- {schemathesis-3.38.1.dist-info → schemathesis-3.38.3.dist-info}/RECORD +7 -7
- {schemathesis-3.38.1.dist-info → schemathesis-3.38.3.dist-info}/WHEEL +0 -0
- {schemathesis-3.38.1.dist-info → schemathesis-3.38.3.dist-info}/entry_points.txt +0 -0
- {schemathesis-3.38.1.dist-info → schemathesis-3.38.3.dist-info}/licenses/LICENSE +0 -0
schemathesis/_hypothesis.py
CHANGED
|
@@ -5,8 +5,8 @@ from __future__ import annotations
|
|
|
5
5
|
import asyncio
|
|
6
6
|
import json
|
|
7
7
|
import warnings
|
|
8
|
-
from copy import copy
|
|
9
8
|
from functools import wraps
|
|
9
|
+
from itertools import combinations
|
|
10
10
|
from typing import TYPE_CHECKING, Any, Callable, Generator, Mapping
|
|
11
11
|
|
|
12
12
|
import hypothesis
|
|
@@ -223,18 +223,6 @@ def _iter_coverage_cases(
|
|
|
223
223
|
from .specs.openapi.constants import LOCATION_TO_CONTAINER
|
|
224
224
|
from .specs.openapi.examples import find_in_responses, find_matching_in_responses
|
|
225
225
|
|
|
226
|
-
meta = GenerationMetadata(
|
|
227
|
-
query=None,
|
|
228
|
-
path_parameters=None,
|
|
229
|
-
headers=None,
|
|
230
|
-
cookies=None,
|
|
231
|
-
body=None,
|
|
232
|
-
phase=TestPhase.COVERAGE,
|
|
233
|
-
description=None,
|
|
234
|
-
location=None,
|
|
235
|
-
parameter=None,
|
|
236
|
-
parameter_location=None,
|
|
237
|
-
)
|
|
238
226
|
generators: dict[tuple[str, str], Generator[coverage.GeneratedValue, None, None]] = {}
|
|
239
227
|
template: dict[str, Any] = {}
|
|
240
228
|
responses = find_in_responses(operation)
|
|
@@ -275,25 +263,27 @@ def _iter_coverage_cases(
|
|
|
275
263
|
template["media_type"] = body.media_type
|
|
276
264
|
case = operation.make_case(**{**template, "body": value.value, "media_type": body.media_type})
|
|
277
265
|
case.data_generation_method = value.data_generation_method
|
|
278
|
-
case.meta =
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
266
|
+
case.meta = _make_meta(
|
|
267
|
+
description=value.description,
|
|
268
|
+
location=value.location,
|
|
269
|
+
parameter=body.media_type,
|
|
270
|
+
parameter_location="body",
|
|
271
|
+
)
|
|
283
272
|
yield case
|
|
284
273
|
for next_value in gen:
|
|
285
274
|
case = operation.make_case(**{**template, "body": next_value.value, "media_type": body.media_type})
|
|
286
275
|
case.data_generation_method = next_value.data_generation_method
|
|
287
|
-
case.meta =
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
276
|
+
case.meta = _make_meta(
|
|
277
|
+
description=next_value.description,
|
|
278
|
+
location=next_value.location,
|
|
279
|
+
parameter=body.media_type,
|
|
280
|
+
parameter_location="body",
|
|
281
|
+
)
|
|
292
282
|
yield case
|
|
293
283
|
elif DataGenerationMethod.positive in data_generation_methods:
|
|
294
284
|
case = operation.make_case(**template)
|
|
295
285
|
case.data_generation_method = DataGenerationMethod.positive
|
|
296
|
-
case.meta =
|
|
286
|
+
case.meta = _make_meta(description="Default positive test case")
|
|
297
287
|
yield case
|
|
298
288
|
for (location, name), gen in generators.items():
|
|
299
289
|
container_name = LOCATION_TO_CONTAINER[location]
|
|
@@ -305,16 +295,17 @@ def _iter_coverage_cases(
|
|
|
305
295
|
generated = value.value
|
|
306
296
|
case = operation.make_case(**{**template, container_name: {**container, name: generated}})
|
|
307
297
|
case.data_generation_method = value.data_generation_method
|
|
308
|
-
case.meta =
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
298
|
+
case.meta = _make_meta(
|
|
299
|
+
description=value.description,
|
|
300
|
+
location=value.location,
|
|
301
|
+
parameter=name,
|
|
302
|
+
parameter_location=location,
|
|
303
|
+
)
|
|
313
304
|
yield case
|
|
314
305
|
# Generate missing required parameters
|
|
315
306
|
if DataGenerationMethod.negative in data_generation_methods:
|
|
316
307
|
for parameter in operation.iter_parameters():
|
|
317
|
-
if parameter.is_required:
|
|
308
|
+
if parameter.is_required and parameter.location != "path":
|
|
318
309
|
name = parameter.name
|
|
319
310
|
location = parameter.location
|
|
320
311
|
container_name = LOCATION_TO_CONTAINER[location]
|
|
@@ -323,12 +314,87 @@ def _iter_coverage_cases(
|
|
|
323
314
|
**{**template, container_name: {k: v for k, v in container.items() if k != name}}
|
|
324
315
|
)
|
|
325
316
|
case.data_generation_method = DataGenerationMethod.negative
|
|
326
|
-
case.meta =
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
317
|
+
case.meta = _make_meta(
|
|
318
|
+
description=f"Missing `{name}` at {location}",
|
|
319
|
+
location=parameter.location,
|
|
320
|
+
parameter=name,
|
|
321
|
+
parameter_location=location,
|
|
322
|
+
)
|
|
331
323
|
yield case
|
|
324
|
+
# Generate combinations for each location
|
|
325
|
+
for location, parameter_set in [
|
|
326
|
+
("query", operation.query),
|
|
327
|
+
("header", operation.headers),
|
|
328
|
+
("cookie", operation.cookies),
|
|
329
|
+
]:
|
|
330
|
+
if not parameter_set:
|
|
331
|
+
continue
|
|
332
|
+
|
|
333
|
+
container_name = LOCATION_TO_CONTAINER[location]
|
|
334
|
+
base_container = template.get(container_name, {})
|
|
335
|
+
|
|
336
|
+
# Get required and optional parameters
|
|
337
|
+
required = {p.name for p in parameter_set if p.is_required}
|
|
338
|
+
all_params = {p.name for p in parameter_set}
|
|
339
|
+
optional = sorted(all_params - required)
|
|
340
|
+
|
|
341
|
+
# Helper function to create and yield a case
|
|
342
|
+
def make_case(container_values: dict, description: str, _location: str, _container_name: str) -> Case:
|
|
343
|
+
if _location in ("header", "cookie"):
|
|
344
|
+
container = {
|
|
345
|
+
name: json.dumps(val) if not isinstance(val, str) else val for name, val in container_values.items()
|
|
346
|
+
}
|
|
347
|
+
else:
|
|
348
|
+
container = container_values
|
|
349
|
+
|
|
350
|
+
case = operation.make_case(**{**template, _container_name: container})
|
|
351
|
+
case.data_generation_method = DataGenerationMethod.positive
|
|
352
|
+
case.meta = _make_meta(
|
|
353
|
+
description=description,
|
|
354
|
+
location=_location,
|
|
355
|
+
parameter_location=_location,
|
|
356
|
+
)
|
|
357
|
+
return case
|
|
358
|
+
|
|
359
|
+
# 1. Generate only required properties
|
|
360
|
+
if required and all_params != required:
|
|
361
|
+
only_required = {k: v for k, v in base_container.items() if k in required}
|
|
362
|
+
yield make_case(only_required, "Only required properties", location, container_name)
|
|
363
|
+
|
|
364
|
+
# 2. Generate combinations with required properties and one optional property
|
|
365
|
+
for opt_param in optional:
|
|
366
|
+
combo = {k: v for k, v in base_container.items() if k in required or k == opt_param}
|
|
367
|
+
if combo != base_container:
|
|
368
|
+
yield make_case(combo, f"All required properties and optional '{opt_param}'", location, container_name)
|
|
369
|
+
|
|
370
|
+
# 3. Generate one combination for each size from 2 to N-1 of optional parameters
|
|
371
|
+
if len(optional) > 1:
|
|
372
|
+
for size in range(2, len(optional)):
|
|
373
|
+
for combination in combinations(optional, size):
|
|
374
|
+
combo = {k: v for k, v in base_container.items() if k in required or k in combination}
|
|
375
|
+
if combo != base_container:
|
|
376
|
+
yield make_case(combo, f"All required and {size} optional properties", location, container_name)
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def _make_meta(
|
|
380
|
+
*,
|
|
381
|
+
description: str,
|
|
382
|
+
location: str | None = None,
|
|
383
|
+
parameter: str | None = None,
|
|
384
|
+
parameter_location: str | None = None,
|
|
385
|
+
) -> GenerationMetadata:
|
|
386
|
+
return GenerationMetadata(
|
|
387
|
+
query=None,
|
|
388
|
+
path_parameters=None,
|
|
389
|
+
headers=None,
|
|
390
|
+
cookies=None,
|
|
391
|
+
body=None,
|
|
392
|
+
phase=TestPhase.COVERAGE,
|
|
393
|
+
description=description,
|
|
394
|
+
location=location,
|
|
395
|
+
parameter=parameter,
|
|
396
|
+
parameter_location=parameter_location,
|
|
397
|
+
)
|
|
332
398
|
|
|
333
399
|
|
|
334
400
|
def find_invalid_headers(headers: Mapping) -> Generator[tuple[str, str], None, None]:
|
schemathesis/schemas.py
CHANGED
|
@@ -49,6 +49,7 @@ from .utils import PARAMETRIZE_MARKER, GivenInput, given_proxy
|
|
|
49
49
|
if TYPE_CHECKING:
|
|
50
50
|
import hypothesis
|
|
51
51
|
from hypothesis.strategies import SearchStrategy
|
|
52
|
+
from hypothesis.vendor.pretty import RepresentationPrinter
|
|
52
53
|
from pyrate_limiter import Limiter
|
|
53
54
|
|
|
54
55
|
from .stateful import Stateful, StatefulTest
|
|
@@ -102,6 +103,9 @@ class BaseSchema(Mapping):
|
|
|
102
103
|
def __post_init__(self) -> None:
|
|
103
104
|
self.hook = to_filterable_hook(self.hooks) # type: ignore[method-assign]
|
|
104
105
|
|
|
106
|
+
def _repr_pretty_(self, printer: RepresentationPrinter, cycle: bool) -> None:
|
|
107
|
+
return None
|
|
108
|
+
|
|
105
109
|
def include(
|
|
106
110
|
self,
|
|
107
111
|
func: MatcherFunc | None = None,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: schemathesis
|
|
3
|
-
Version: 3.38.
|
|
3
|
+
Version: 3.38.3
|
|
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
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
schemathesis/__init__.py,sha256=UW2Bq8hDDkcBeAAA7PzpBFXkOOxkmHox-mfQwzHDjL0,1914
|
|
2
2
|
schemathesis/_compat.py,sha256=y4RZd59i2NCnZ91VQhnKeMn_8t3SgvLOk2Xm8nymUHY,1837
|
|
3
3
|
schemathesis/_dependency_versions.py,sha256=pjEkkGAfOQJYNb-9UOo84V8nj_lKHr_TGDVdFwY2UU0,816
|
|
4
|
-
schemathesis/_hypothesis.py,sha256=
|
|
4
|
+
schemathesis/_hypothesis.py,sha256=F-cGpyEX0rWiCPOS58WU067wvL-PsxderMRxX3qS91k,19376
|
|
5
5
|
schemathesis/_lazy_import.py,sha256=aMhWYgbU2JOltyWBb32vnWBb6kykOghucEzI_F70yVE,470
|
|
6
6
|
schemathesis/_override.py,sha256=TAjYB3eJQmlw9K_xiR9ptt9Wj7if4U7UFlUhGjpBAoM,1625
|
|
7
7
|
schemathesis/_patches.py,sha256=Hsbpn4UVeXUQD2Kllrbq01CSWsTYENWa0VJTyhX5C2k,895
|
|
@@ -22,7 +22,7 @@ schemathesis/models.py,sha256=2kMMJ3JVe4_91uhRxgsZ_G1FOyksxTiYAo52M5asWLA,49868
|
|
|
22
22
|
schemathesis/parameters.py,sha256=_LN3NL5XwoRfvjcU8o2ArrNFK9sbBZo25UFdxuywkRw,2425
|
|
23
23
|
schemathesis/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
24
24
|
schemathesis/sanitization.py,sha256=Lycn1VVfula9B6XpzkxTHja7CZ7RHqbUh9kBic0Yi4M,9056
|
|
25
|
-
schemathesis/schemas.py,sha256=
|
|
25
|
+
schemathesis/schemas.py,sha256=KyG8IUNv7_3tPBzT9ARGDmUVEPvHHj6f1wW7GYbfvI4,20623
|
|
26
26
|
schemathesis/serializers.py,sha256=HyYVSVR71FhWfIErnH6OoGLOa4tkh9mTeVUTIpzEW24,11739
|
|
27
27
|
schemathesis/targets.py,sha256=XIGRghvEzbmEJjse9aZgNEj67L3jAbiazm2rxURWgDE,2351
|
|
28
28
|
schemathesis/throttling.py,sha256=aisUc4MJDGIOGUAs9L2DlWWpdd4KyAFuNVKhYoaUC9M,1719
|
|
@@ -153,8 +153,8 @@ schemathesis/transports/auth.py,sha256=urSTO9zgFO1qU69xvnKHPFQV0SlJL3d7_Ojl0tLnZ
|
|
|
153
153
|
schemathesis/transports/content_types.py,sha256=MiKOm-Hy5i75hrROPdpiBZPOTDzOwlCdnthJD12AJzI,2187
|
|
154
154
|
schemathesis/transports/headers.py,sha256=hr_AIDOfUxsJxpHfemIZ_uNG3_vzS_ZeMEKmZjbYiBE,990
|
|
155
155
|
schemathesis/transports/responses.py,sha256=OFD4ZLqwEFpo7F9vaP_SVgjhxAqatxIj38FS4XVq8Qs,1680
|
|
156
|
-
schemathesis-3.38.
|
|
157
|
-
schemathesis-3.38.
|
|
158
|
-
schemathesis-3.38.
|
|
159
|
-
schemathesis-3.38.
|
|
160
|
-
schemathesis-3.38.
|
|
156
|
+
schemathesis-3.38.3.dist-info/METADATA,sha256=N4rZPrKlOyw9dEae6FkXFD-WLAMsYDCQNLFWEZePxmA,12956
|
|
157
|
+
schemathesis-3.38.3.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
|
|
158
|
+
schemathesis-3.38.3.dist-info/entry_points.txt,sha256=VHyLcOG7co0nOeuk8WjgpRETk5P1E2iCLrn26Zkn5uk,158
|
|
159
|
+
schemathesis-3.38.3.dist-info/licenses/LICENSE,sha256=PsPYgrDhZ7g9uwihJXNG-XVb55wj2uYhkl2DD8oAzY0,1103
|
|
160
|
+
schemathesis-3.38.3.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|