schemathesis 3.38.6__py3-none-any.whl → 3.38.7__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 +67 -16
- schemathesis/generation/coverage.py +15 -7
- schemathesis/models.py +2 -1
- {schemathesis-3.38.6.dist-info → schemathesis-3.38.7.dist-info}/METADATA +1 -1
- {schemathesis-3.38.6.dist-info → schemathesis-3.38.7.dist-info}/RECORD +8 -8
- {schemathesis-3.38.6.dist-info → schemathesis-3.38.7.dist-info}/WHEEL +1 -1
- {schemathesis-3.38.6.dist-info → schemathesis-3.38.7.dist-info}/entry_points.txt +0 -0
- {schemathesis-3.38.6.dist-info → schemathesis-3.38.7.dist-info}/licenses/LICENSE +0 -0
schemathesis/_hypothesis.py
CHANGED
|
@@ -224,8 +224,11 @@ def _iter_coverage_cases(
|
|
|
224
224
|
from .specs.openapi.constants import LOCATION_TO_CONTAINER
|
|
225
225
|
from .specs.openapi.examples import find_in_responses, find_matching_in_responses
|
|
226
226
|
|
|
227
|
-
def _stringify_value(val: Any) -> str:
|
|
227
|
+
def _stringify_value(val: Any, location: str) -> str | list[str]:
|
|
228
228
|
if isinstance(val, list):
|
|
229
|
+
if location == "query":
|
|
230
|
+
# Having a list here ensures there will be multiple query parameters wit the same name
|
|
231
|
+
return [json.dumps(item) for item in val]
|
|
229
232
|
# use comma-separated values style for arrays
|
|
230
233
|
return ",".join(json.dumps(sub) for sub in val)
|
|
231
234
|
return json.dumps(val)
|
|
@@ -246,8 +249,8 @@ def _iter_coverage_cases(
|
|
|
246
249
|
location = parameter.location
|
|
247
250
|
name = parameter.name
|
|
248
251
|
container = template.setdefault(LOCATION_TO_CONTAINER[location], {})
|
|
249
|
-
if location in ("header", "cookie", "path") and not isinstance(value.value, str):
|
|
250
|
-
container[name] = _stringify_value(value.value)
|
|
252
|
+
if location in ("header", "cookie", "path", "query") and not isinstance(value.value, str):
|
|
253
|
+
container[name] = _stringify_value(value.value, location)
|
|
251
254
|
else:
|
|
252
255
|
container[name] = value.value
|
|
253
256
|
generators[(location, name)] = gen
|
|
@@ -297,8 +300,8 @@ def _iter_coverage_cases(
|
|
|
297
300
|
container_name = LOCATION_TO_CONTAINER[location]
|
|
298
301
|
container = template[container_name]
|
|
299
302
|
for value in gen:
|
|
300
|
-
if location in ("header", "cookie", "path") and not isinstance(value.value, str):
|
|
301
|
-
generated = _stringify_value(value.value)
|
|
303
|
+
if location in ("header", "cookie", "path", "query") and not isinstance(value.value, str):
|
|
304
|
+
generated = _stringify_value(value.value, location)
|
|
302
305
|
else:
|
|
303
306
|
generated = value.value
|
|
304
307
|
case = operation.make_case(**{**template, container_name: {**container, name: generated}})
|
|
@@ -310,8 +313,32 @@ def _iter_coverage_cases(
|
|
|
310
313
|
parameter_location=location,
|
|
311
314
|
)
|
|
312
315
|
yield case
|
|
313
|
-
# Generate missing required parameters
|
|
314
316
|
if DataGenerationMethod.negative in data_generation_methods:
|
|
317
|
+
# Generate HTTP methods that are not specified in the spec
|
|
318
|
+
methods = {"get", "put", "post", "delete", "options", "head", "patch", "trace"} - set(
|
|
319
|
+
operation.schema[operation.path]
|
|
320
|
+
)
|
|
321
|
+
for method in methods:
|
|
322
|
+
case = operation.make_case(**template)
|
|
323
|
+
case._explicit_method = method
|
|
324
|
+
case.data_generation_method = DataGenerationMethod.negative
|
|
325
|
+
case.meta = _make_meta(description=f"Unspecified HTTP method: {method}")
|
|
326
|
+
yield case
|
|
327
|
+
# Generate duplicate query parameters
|
|
328
|
+
if operation.query:
|
|
329
|
+
container = template["query"]
|
|
330
|
+
for parameter in operation.query:
|
|
331
|
+
value = container[parameter.name]
|
|
332
|
+
case = operation.make_case(**{**template, "query": {**container, parameter.name: [value, value]}})
|
|
333
|
+
case.data_generation_method = DataGenerationMethod.negative
|
|
334
|
+
case.meta = _make_meta(
|
|
335
|
+
description=f"Duplicate `{parameter.name}` query parameter",
|
|
336
|
+
location=None,
|
|
337
|
+
parameter=parameter.name,
|
|
338
|
+
parameter_location="query",
|
|
339
|
+
)
|
|
340
|
+
yield case
|
|
341
|
+
# Generate missing required parameters
|
|
315
342
|
for parameter in operation.iter_parameters():
|
|
316
343
|
if parameter.is_required and parameter.location != "path":
|
|
317
344
|
name = parameter.name
|
|
@@ -324,7 +351,7 @@ def _iter_coverage_cases(
|
|
|
324
351
|
case.data_generation_method = DataGenerationMethod.negative
|
|
325
352
|
case.meta = _make_meta(
|
|
326
353
|
description=f"Missing `{name}` at {location}",
|
|
327
|
-
location=
|
|
354
|
+
location=None,
|
|
328
355
|
parameter=name,
|
|
329
356
|
parameter_location=location,
|
|
330
357
|
)
|
|
@@ -348,21 +375,26 @@ def _iter_coverage_cases(
|
|
|
348
375
|
|
|
349
376
|
# Helper function to create and yield a case
|
|
350
377
|
def make_case(
|
|
351
|
-
container_values: dict,
|
|
378
|
+
container_values: dict,
|
|
379
|
+
description: str,
|
|
380
|
+
_location: str,
|
|
381
|
+
_container_name: str,
|
|
382
|
+
_parameter: str | None,
|
|
383
|
+
_data_generation_method: DataGenerationMethod,
|
|
352
384
|
) -> Case:
|
|
353
|
-
if _location in ("header", "cookie", "path"):
|
|
385
|
+
if _location in ("header", "cookie", "path", "query"):
|
|
354
386
|
container = {
|
|
355
|
-
name: _stringify_value(val) if not isinstance(val, str) else val
|
|
387
|
+
name: _stringify_value(val, _location) if not isinstance(val, str) else val
|
|
356
388
|
for name, val in container_values.items()
|
|
357
389
|
}
|
|
358
390
|
else:
|
|
359
391
|
container = container_values
|
|
360
392
|
|
|
361
393
|
case = operation.make_case(**{**template, _container_name: container})
|
|
362
|
-
case.data_generation_method =
|
|
394
|
+
case.data_generation_method = _data_generation_method
|
|
363
395
|
case.meta = _make_meta(
|
|
364
396
|
description=description,
|
|
365
|
-
location=
|
|
397
|
+
location=None,
|
|
366
398
|
parameter=_parameter,
|
|
367
399
|
parameter_location=_location,
|
|
368
400
|
)
|
|
@@ -388,12 +420,21 @@ def _iter_coverage_cases(
|
|
|
388
420
|
coverage.CoverageContext(data_generation_methods=[DataGenerationMethod.negative]),
|
|
389
421
|
subschema,
|
|
390
422
|
):
|
|
391
|
-
yield make_case(
|
|
423
|
+
yield make_case(
|
|
424
|
+
more.value,
|
|
425
|
+
more.description,
|
|
426
|
+
_location,
|
|
427
|
+
_container_name,
|
|
428
|
+
more.parameter,
|
|
429
|
+
DataGenerationMethod.negative,
|
|
430
|
+
)
|
|
392
431
|
|
|
393
432
|
# 1. Generate only required properties
|
|
394
433
|
if required and all_params != required:
|
|
395
434
|
only_required = {k: v for k, v in base_container.items() if k in required}
|
|
396
|
-
yield make_case(
|
|
435
|
+
yield make_case(
|
|
436
|
+
only_required, "Only required properties", location, container_name, None, DataGenerationMethod.positive
|
|
437
|
+
)
|
|
397
438
|
if DataGenerationMethod.negative in data_generation_methods:
|
|
398
439
|
subschema = _combination_schema(only_required, required, parameter_set)
|
|
399
440
|
yield from _yield_negative(subschema, location, container_name)
|
|
@@ -403,7 +444,12 @@ def _iter_coverage_cases(
|
|
|
403
444
|
combo = {k: v for k, v in base_container.items() if k in required or k == opt_param}
|
|
404
445
|
if combo != base_container:
|
|
405
446
|
yield make_case(
|
|
406
|
-
combo,
|
|
447
|
+
combo,
|
|
448
|
+
f"All required properties and optional '{opt_param}'",
|
|
449
|
+
location,
|
|
450
|
+
container_name,
|
|
451
|
+
None,
|
|
452
|
+
DataGenerationMethod.positive,
|
|
407
453
|
)
|
|
408
454
|
if DataGenerationMethod.negative in data_generation_methods:
|
|
409
455
|
subschema = _combination_schema(combo, required, parameter_set)
|
|
@@ -416,7 +462,12 @@ def _iter_coverage_cases(
|
|
|
416
462
|
combo = {k: v for k, v in base_container.items() if k in required or k in combination}
|
|
417
463
|
if combo != base_container:
|
|
418
464
|
yield make_case(
|
|
419
|
-
combo,
|
|
465
|
+
combo,
|
|
466
|
+
f"All required and {size} optional properties",
|
|
467
|
+
location,
|
|
468
|
+
container_name,
|
|
469
|
+
None,
|
|
470
|
+
DataGenerationMethod.positive,
|
|
420
471
|
)
|
|
421
472
|
|
|
422
473
|
|
|
@@ -324,9 +324,9 @@ def cover_schema_iter(
|
|
|
324
324
|
for key, value in schema.items():
|
|
325
325
|
with _ignore_unfixable(), ctx.location(key):
|
|
326
326
|
if key == "enum":
|
|
327
|
-
yield from _negative_enum(ctx, value)
|
|
327
|
+
yield from _negative_enum(ctx, value, seen)
|
|
328
328
|
elif key == "const":
|
|
329
|
-
for value_ in _negative_enum(ctx, [value]):
|
|
329
|
+
for value_ in _negative_enum(ctx, [value], seen):
|
|
330
330
|
k = _to_hashable_key(value_.value)
|
|
331
331
|
if k not in seen:
|
|
332
332
|
yield value_
|
|
@@ -744,13 +744,20 @@ def select_combinations(optional: list[str]) -> Iterator[tuple[str, ...]]:
|
|
|
744
744
|
yield next(combinations(optional, size))
|
|
745
745
|
|
|
746
746
|
|
|
747
|
-
def _negative_enum(
|
|
747
|
+
def _negative_enum(
|
|
748
|
+
ctx: CoverageContext, value: list, seen: set[Any | tuple[type, str]]
|
|
749
|
+
) -> Generator[GeneratedValue, None, None]:
|
|
748
750
|
def is_not_in_value(x: Any) -> bool:
|
|
749
|
-
|
|
751
|
+
if x in value:
|
|
752
|
+
return False
|
|
753
|
+
_hashed = _to_hashable_key(x)
|
|
754
|
+
return _hashed not in seen
|
|
750
755
|
|
|
751
|
-
strategy =
|
|
752
|
-
|
|
753
|
-
yield NegativeValue(
|
|
756
|
+
strategy = (st.none() | st.booleans() | NUMERIC_STRATEGY | st.text()).filter(is_not_in_value)
|
|
757
|
+
value = ctx.generate_from(strategy)
|
|
758
|
+
yield NegativeValue(value, description="Invalid enum value", location=ctx.current_location)
|
|
759
|
+
hashed = _to_hashable_key(value)
|
|
760
|
+
seen.add(hashed)
|
|
754
761
|
|
|
755
762
|
|
|
756
763
|
def _negative_properties(
|
|
@@ -846,6 +853,7 @@ def _negative_required(
|
|
|
846
853
|
{k: v for k, v in template.items() if k != key},
|
|
847
854
|
description=f"Missing required property: {key}",
|
|
848
855
|
location=ctx.current_location,
|
|
856
|
+
parameter=key,
|
|
849
857
|
)
|
|
850
858
|
|
|
851
859
|
|
schemathesis/models.py
CHANGED
|
@@ -203,6 +203,7 @@ class Case:
|
|
|
203
203
|
data_generation_method: DataGenerationMethod | None = None
|
|
204
204
|
_auth: requests.auth.AuthBase | None = None
|
|
205
205
|
_has_explicit_auth: bool = False
|
|
206
|
+
_explicit_method: str | None = None
|
|
206
207
|
|
|
207
208
|
def __post_init__(self) -> None:
|
|
208
209
|
self._original_path_parameters = self.path_parameters.copy() if self.path_parameters else None
|
|
@@ -265,7 +266,7 @@ class Case:
|
|
|
265
266
|
|
|
266
267
|
@property
|
|
267
268
|
def method(self) -> str:
|
|
268
|
-
return self.operation.method.upper()
|
|
269
|
+
return self._explicit_method.upper() if self._explicit_method else self.operation.method.upper()
|
|
269
270
|
|
|
270
271
|
@property
|
|
271
272
|
def base_url(self) -> str | None:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: schemathesis
|
|
3
|
-
Version: 3.38.
|
|
3
|
+
Version: 3.38.7
|
|
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=7hQItl5JqtctCiN7gL4DhWmj8A0Ku6t1_pkxEqkID1g,23463
|
|
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
|
|
@@ -18,7 +18,7 @@ schemathesis/graphql.py,sha256=XiuKcfoOB92iLFC8zpz2msLkM0_V0TLdxPNBqrrGZ8w,216
|
|
|
18
18
|
schemathesis/hooks.py,sha256=p5AXgjVGtka0jn9MOeyBaRUtNbqZTs4iaJqytYTacHc,14856
|
|
19
19
|
schemathesis/lazy.py,sha256=Ddhkk7Tpc_VcRGYkCtKDmP2gpjxVmEZ3b01ZTNjbm8I,19004
|
|
20
20
|
schemathesis/loaders.py,sha256=MoEhcdOEBJxNRn5X-ZNhWB9jZDHQQNpkNfEdQjf_NDw,4590
|
|
21
|
-
schemathesis/models.py,sha256=
|
|
21
|
+
schemathesis/models.py,sha256=Xn1rEF0_g3HNqJaJB9sC8N3gJ9suFPPlgc8ho01VyF0,49769
|
|
22
22
|
schemathesis/parameters.py,sha256=izlu4MFYT1RWrC4RBxrV6weeCal-ODbdLQLMb0PYCZY,2327
|
|
23
23
|
schemathesis/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
24
24
|
schemathesis/sanitization.py,sha256=Lycn1VVfula9B6XpzkxTHja7CZ7RHqbUh9kBic0Yi4M,9056
|
|
@@ -61,7 +61,7 @@ schemathesis/fixups/utf8_bom.py,sha256=lWT9RNmJG8i-l5AXIpaCT3qCPUwRgzXPW3eoOjmZE
|
|
|
61
61
|
schemathesis/generation/__init__.py,sha256=29Zys_tD6kfngaC4zHeC6TOBZQcmo7CWm7KDSYsHStQ,1581
|
|
62
62
|
schemathesis/generation/_hypothesis.py,sha256=74fzLPHugZgMQXerWYFAMqCAjtAXz5E4gek7Gnkhli4,1756
|
|
63
63
|
schemathesis/generation/_methods.py,sha256=r8oVlJ71_gXcnEhU-byw2E0R2RswQQFm8U7yGErSqbw,1204
|
|
64
|
-
schemathesis/generation/coverage.py,sha256=
|
|
64
|
+
schemathesis/generation/coverage.py,sha256=Y-eKsEDe6INu-2SjlzGpuzayRXF_wdDwTBLsW4pGlrc,38843
|
|
65
65
|
schemathesis/internal/__init__.py,sha256=93HcdG3LF0BbQKbCteOsFMa1w6nXl8yTmx87QLNJOik,161
|
|
66
66
|
schemathesis/internal/checks.py,sha256=SBx2gesB-XzgVSMX_u7Mb416jSxJ68eQKtcdkWlkyOo,2441
|
|
67
67
|
schemathesis/internal/copy.py,sha256=DcL56z-d69kKR_5u8mlHvjSL1UTyUKNMAwexrwHFY1s,1031
|
|
@@ -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.7.dist-info/METADATA,sha256=iBsAbz1uaJxY_GUPTnjdD2uMxS3HCk6lX_4IVj_s2yY,12923
|
|
157
|
+
schemathesis-3.38.7.dist-info/WHEEL,sha256=C2FUgwZgiLbznR-k0b_5k3Ai_1aASOXDss3lzCUsUug,87
|
|
158
|
+
schemathesis-3.38.7.dist-info/entry_points.txt,sha256=VHyLcOG7co0nOeuk8WjgpRETk5P1E2iCLrn26Zkn5uk,158
|
|
159
|
+
schemathesis-3.38.7.dist-info/licenses/LICENSE,sha256=PsPYgrDhZ7g9uwihJXNG-XVb55wj2uYhkl2DD8oAzY0,1103
|
|
160
|
+
schemathesis-3.38.7.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|