schemathesis 3.38.2__py3-none-any.whl → 3.38.4__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 +129 -34
- schemathesis/schemas.py +4 -0
- {schemathesis-3.38.2.dist-info → schemathesis-3.38.4.dist-info}/METADATA +1 -1
- {schemathesis-3.38.2.dist-info → schemathesis-3.38.4.dist-info}/RECORD +7 -7
- {schemathesis-3.38.2.dist-info → schemathesis-3.38.4.dist-info}/WHEEL +0 -0
- {schemathesis-3.38.2.dist-info → schemathesis-3.38.4.dist-info}/entry_points.txt +0 -0
- {schemathesis-3.38.2.dist-info → schemathesis-3.38.4.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
|
|
@@ -23,6 +23,7 @@ from .experimental import COVERAGE_PHASE
|
|
|
23
23
|
from .generation import DataGenerationMethod, GenerationConfig, combine_strategies, coverage, get_single_example
|
|
24
24
|
from .hooks import GLOBAL_HOOK_DISPATCHER, HookContext, HookDispatcher
|
|
25
25
|
from .models import APIOperation, Case, GenerationMetadata, TestPhase
|
|
26
|
+
from .parameters import ParameterSet
|
|
26
27
|
from .transports.content_types import parse_content_type
|
|
27
28
|
from .transports.headers import has_invalid_characters, is_latin_1_encodable
|
|
28
29
|
from .types import NotSet
|
|
@@ -223,18 +224,6 @@ def _iter_coverage_cases(
|
|
|
223
224
|
from .specs.openapi.constants import LOCATION_TO_CONTAINER
|
|
224
225
|
from .specs.openapi.examples import find_in_responses, find_matching_in_responses
|
|
225
226
|
|
|
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
227
|
generators: dict[tuple[str, str], Generator[coverage.GeneratedValue, None, None]] = {}
|
|
239
228
|
template: dict[str, Any] = {}
|
|
240
229
|
responses = find_in_responses(operation)
|
|
@@ -275,25 +264,27 @@ def _iter_coverage_cases(
|
|
|
275
264
|
template["media_type"] = body.media_type
|
|
276
265
|
case = operation.make_case(**{**template, "body": value.value, "media_type": body.media_type})
|
|
277
266
|
case.data_generation_method = value.data_generation_method
|
|
278
|
-
case.meta =
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
267
|
+
case.meta = _make_meta(
|
|
268
|
+
description=value.description,
|
|
269
|
+
location=value.location,
|
|
270
|
+
parameter=body.media_type,
|
|
271
|
+
parameter_location="body",
|
|
272
|
+
)
|
|
283
273
|
yield case
|
|
284
274
|
for next_value in gen:
|
|
285
275
|
case = operation.make_case(**{**template, "body": next_value.value, "media_type": body.media_type})
|
|
286
276
|
case.data_generation_method = next_value.data_generation_method
|
|
287
|
-
case.meta =
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
277
|
+
case.meta = _make_meta(
|
|
278
|
+
description=next_value.description,
|
|
279
|
+
location=next_value.location,
|
|
280
|
+
parameter=body.media_type,
|
|
281
|
+
parameter_location="body",
|
|
282
|
+
)
|
|
292
283
|
yield case
|
|
293
284
|
elif DataGenerationMethod.positive in data_generation_methods:
|
|
294
285
|
case = operation.make_case(**template)
|
|
295
286
|
case.data_generation_method = DataGenerationMethod.positive
|
|
296
|
-
case.meta =
|
|
287
|
+
case.meta = _make_meta(description="Default positive test case")
|
|
297
288
|
yield case
|
|
298
289
|
for (location, name), gen in generators.items():
|
|
299
290
|
container_name = LOCATION_TO_CONTAINER[location]
|
|
@@ -305,11 +296,12 @@ def _iter_coverage_cases(
|
|
|
305
296
|
generated = value.value
|
|
306
297
|
case = operation.make_case(**{**template, container_name: {**container, name: generated}})
|
|
307
298
|
case.data_generation_method = value.data_generation_method
|
|
308
|
-
case.meta =
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
299
|
+
case.meta = _make_meta(
|
|
300
|
+
description=value.description,
|
|
301
|
+
location=value.location,
|
|
302
|
+
parameter=name,
|
|
303
|
+
parameter_location=location,
|
|
304
|
+
)
|
|
313
305
|
yield case
|
|
314
306
|
# Generate missing required parameters
|
|
315
307
|
if DataGenerationMethod.negative in data_generation_methods:
|
|
@@ -323,12 +315,115 @@ def _iter_coverage_cases(
|
|
|
323
315
|
**{**template, container_name: {k: v for k, v in container.items() if k != name}}
|
|
324
316
|
)
|
|
325
317
|
case.data_generation_method = DataGenerationMethod.negative
|
|
326
|
-
case.meta =
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
318
|
+
case.meta = _make_meta(
|
|
319
|
+
description=f"Missing `{name}` at {location}",
|
|
320
|
+
location=parameter.location,
|
|
321
|
+
parameter=name,
|
|
322
|
+
parameter_location=location,
|
|
323
|
+
)
|
|
331
324
|
yield case
|
|
325
|
+
# Generate combinations for each location
|
|
326
|
+
for location, parameter_set in [
|
|
327
|
+
("query", operation.query),
|
|
328
|
+
("header", operation.headers),
|
|
329
|
+
("cookie", operation.cookies),
|
|
330
|
+
]:
|
|
331
|
+
if not parameter_set:
|
|
332
|
+
continue
|
|
333
|
+
|
|
334
|
+
container_name = LOCATION_TO_CONTAINER[location]
|
|
335
|
+
base_container = template.get(container_name, {})
|
|
336
|
+
|
|
337
|
+
# Get required and optional parameters
|
|
338
|
+
required = {p.name for p in parameter_set if p.is_required}
|
|
339
|
+
all_params = {p.name for p in parameter_set}
|
|
340
|
+
optional = sorted(all_params - required)
|
|
341
|
+
|
|
342
|
+
# Helper function to create and yield a case
|
|
343
|
+
def make_case(container_values: dict, description: str, _location: str, _container_name: str) -> Case:
|
|
344
|
+
if _location in ("header", "cookie"):
|
|
345
|
+
container = {
|
|
346
|
+
name: json.dumps(val) if not isinstance(val, str) else val for name, val in container_values.items()
|
|
347
|
+
}
|
|
348
|
+
else:
|
|
349
|
+
container = container_values
|
|
350
|
+
|
|
351
|
+
case = operation.make_case(**{**template, _container_name: container})
|
|
352
|
+
case.data_generation_method = DataGenerationMethod.positive
|
|
353
|
+
case.meta = _make_meta(
|
|
354
|
+
description=description,
|
|
355
|
+
location=_location,
|
|
356
|
+
parameter_location=_location,
|
|
357
|
+
)
|
|
358
|
+
return case
|
|
359
|
+
|
|
360
|
+
def _combination_schema(
|
|
361
|
+
combination: dict[str, Any], _required: set[str], _parameter_set: ParameterSet
|
|
362
|
+
) -> dict[str, Any]:
|
|
363
|
+
return {
|
|
364
|
+
"properties": {
|
|
365
|
+
parameter.name: parameter.as_json_schema(operation)
|
|
366
|
+
for parameter in _parameter_set
|
|
367
|
+
if parameter.name in combination
|
|
368
|
+
},
|
|
369
|
+
"required": list(_required),
|
|
370
|
+
"additionalProperties": False,
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
def _yield_negative(
|
|
374
|
+
subschema: dict[str, Any], _location: str, _container_name: str
|
|
375
|
+
) -> Generator[Case, None, None]:
|
|
376
|
+
for more in coverage.cover_schema_iter(
|
|
377
|
+
coverage.CoverageContext(data_generation_methods=[DataGenerationMethod.negative]),
|
|
378
|
+
subschema,
|
|
379
|
+
):
|
|
380
|
+
yield make_case(more.value, more.description, _location, _container_name)
|
|
381
|
+
|
|
382
|
+
# 1. Generate only required properties
|
|
383
|
+
if required and all_params != required:
|
|
384
|
+
only_required = {k: v for k, v in base_container.items() if k in required}
|
|
385
|
+
yield make_case(only_required, "Only required properties", location, container_name)
|
|
386
|
+
if DataGenerationMethod.negative in data_generation_methods:
|
|
387
|
+
subschema = _combination_schema(only_required, required, parameter_set)
|
|
388
|
+
yield from _yield_negative(subschema, location, container_name)
|
|
389
|
+
|
|
390
|
+
# 2. Generate combinations with required properties and one optional property
|
|
391
|
+
for opt_param in optional:
|
|
392
|
+
combo = {k: v for k, v in base_container.items() if k in required or k == opt_param}
|
|
393
|
+
if combo != base_container:
|
|
394
|
+
yield make_case(combo, f"All required properties and optional '{opt_param}'", location, container_name)
|
|
395
|
+
if DataGenerationMethod.negative in data_generation_methods:
|
|
396
|
+
subschema = _combination_schema(combo, required, parameter_set)
|
|
397
|
+
yield from _yield_negative(subschema, location, container_name)
|
|
398
|
+
|
|
399
|
+
# 3. Generate one combination for each size from 2 to N-1 of optional parameters
|
|
400
|
+
if len(optional) > 1:
|
|
401
|
+
for size in range(2, len(optional)):
|
|
402
|
+
for combination in combinations(optional, size):
|
|
403
|
+
combo = {k: v for k, v in base_container.items() if k in required or k in combination}
|
|
404
|
+
if combo != base_container:
|
|
405
|
+
yield make_case(combo, f"All required and {size} optional properties", location, container_name)
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
def _make_meta(
|
|
409
|
+
*,
|
|
410
|
+
description: str,
|
|
411
|
+
location: str | None = None,
|
|
412
|
+
parameter: str | None = None,
|
|
413
|
+
parameter_location: str | None = None,
|
|
414
|
+
) -> GenerationMetadata:
|
|
415
|
+
return GenerationMetadata(
|
|
416
|
+
query=None,
|
|
417
|
+
path_parameters=None,
|
|
418
|
+
headers=None,
|
|
419
|
+
cookies=None,
|
|
420
|
+
body=None,
|
|
421
|
+
phase=TestPhase.COVERAGE,
|
|
422
|
+
description=description,
|
|
423
|
+
location=location,
|
|
424
|
+
parameter=parameter,
|
|
425
|
+
parameter_location=parameter_location,
|
|
426
|
+
)
|
|
332
427
|
|
|
333
428
|
|
|
334
429
|
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.4
|
|
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=lm0J9uQFENfm7-6_wEaNKshGF1wmZebfZJ9G_sUhlqE,20839
|
|
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.4.dist-info/METADATA,sha256=hXln3HxI_xM3plasHcuEeu6OkknqAZgZBAZr-LJZxE0,12956
|
|
157
|
+
schemathesis-3.38.4.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
|
|
158
|
+
schemathesis-3.38.4.dist-info/entry_points.txt,sha256=VHyLcOG7co0nOeuk8WjgpRETk5P1E2iCLrn26Zkn5uk,158
|
|
159
|
+
schemathesis-3.38.4.dist-info/licenses/LICENSE,sha256=PsPYgrDhZ7g9uwihJXNG-XVb55wj2uYhkl2DD8oAzY0,1103
|
|
160
|
+
schemathesis-3.38.4.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|