schemathesis 3.38.7__py3-none-any.whl → 3.38.9__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 +24 -12
- schemathesis/cli/__init__.py +15 -0
- schemathesis/generation/coverage.py +54 -42
- schemathesis/internal/checks.py +6 -0
- schemathesis/models.py +1 -0
- schemathesis/specs/openapi/checks.py +14 -0
- {schemathesis-3.38.7.dist-info → schemathesis-3.38.9.dist-info}/METADATA +1 -1
- {schemathesis-3.38.7.dist-info → schemathesis-3.38.9.dist-info}/RECORD +11 -11
- {schemathesis-3.38.7.dist-info → schemathesis-3.38.9.dist-info}/WHEEL +0 -0
- {schemathesis-3.38.7.dist-info → schemathesis-3.38.9.dist-info}/entry_points.txt +0 -0
- {schemathesis-3.38.7.dist-info → schemathesis-3.38.9.dist-info}/licenses/LICENSE +0 -0
schemathesis/_hypothesis.py
CHANGED
|
@@ -237,17 +237,17 @@ def _iter_coverage_cases(
|
|
|
237
237
|
template: dict[str, Any] = {}
|
|
238
238
|
responses = find_in_responses(operation)
|
|
239
239
|
for parameter in operation.iter_parameters():
|
|
240
|
+
location = parameter.location
|
|
241
|
+
name = parameter.name
|
|
240
242
|
schema = parameter.as_json_schema(operation, update_quantifiers=False)
|
|
241
243
|
for value in find_matching_in_responses(responses, parameter.name):
|
|
242
244
|
schema.setdefault("examples", []).append(value)
|
|
243
245
|
gen = coverage.cover_schema_iter(
|
|
244
|
-
coverage.CoverageContext(data_generation_methods=data_generation_methods), schema
|
|
246
|
+
coverage.CoverageContext(location=location, data_generation_methods=data_generation_methods), schema
|
|
245
247
|
)
|
|
246
248
|
value = next(gen, NOT_SET)
|
|
247
249
|
if isinstance(value, NotSet):
|
|
248
250
|
continue
|
|
249
|
-
location = parameter.location
|
|
250
|
-
name = parameter.name
|
|
251
251
|
container = template.setdefault(LOCATION_TO_CONTAINER[location], {})
|
|
252
252
|
if location in ("header", "cookie", "path", "query") and not isinstance(value.value, str):
|
|
253
253
|
container[name] = _stringify_value(value.value, location)
|
|
@@ -263,7 +263,7 @@ def _iter_coverage_cases(
|
|
|
263
263
|
if examples:
|
|
264
264
|
schema.setdefault("examples", []).extend(examples)
|
|
265
265
|
gen = coverage.cover_schema_iter(
|
|
266
|
-
coverage.CoverageContext(data_generation_methods=data_generation_methods), schema
|
|
266
|
+
coverage.CoverageContext(location="body", data_generation_methods=data_generation_methods), schema
|
|
267
267
|
)
|
|
268
268
|
value = next(gen, NOT_SET)
|
|
269
269
|
if isinstance(value, NotSet):
|
|
@@ -417,7 +417,7 @@ def _iter_coverage_cases(
|
|
|
417
417
|
subschema: dict[str, Any], _location: str, _container_name: str
|
|
418
418
|
) -> Generator[Case, None, None]:
|
|
419
419
|
for more in coverage.cover_schema_iter(
|
|
420
|
-
coverage.CoverageContext(data_generation_methods=[DataGenerationMethod.negative]),
|
|
420
|
+
coverage.CoverageContext(location=_location, data_generation_methods=[DataGenerationMethod.negative]),
|
|
421
421
|
subschema,
|
|
422
422
|
):
|
|
423
423
|
yield make_case(
|
|
@@ -432,17 +432,26 @@ def _iter_coverage_cases(
|
|
|
432
432
|
# 1. Generate only required properties
|
|
433
433
|
if required and all_params != required:
|
|
434
434
|
only_required = {k: v for k, v in base_container.items() if k in required}
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
435
|
+
if DataGenerationMethod.positive in data_generation_methods:
|
|
436
|
+
yield make_case(
|
|
437
|
+
only_required,
|
|
438
|
+
"Only required properties",
|
|
439
|
+
location,
|
|
440
|
+
container_name,
|
|
441
|
+
None,
|
|
442
|
+
DataGenerationMethod.positive,
|
|
443
|
+
)
|
|
438
444
|
if DataGenerationMethod.negative in data_generation_methods:
|
|
439
445
|
subschema = _combination_schema(only_required, required, parameter_set)
|
|
440
|
-
|
|
446
|
+
for case in _yield_negative(subschema, location, container_name):
|
|
447
|
+
# Already generated in one of the blocks above
|
|
448
|
+
if location != "path" and not case.meta.description.startswith("Missing required property"):
|
|
449
|
+
yield case
|
|
441
450
|
|
|
442
451
|
# 2. Generate combinations with required properties and one optional property
|
|
443
452
|
for opt_param in optional:
|
|
444
453
|
combo = {k: v for k, v in base_container.items() if k in required or k == opt_param}
|
|
445
|
-
if combo != base_container:
|
|
454
|
+
if combo != base_container and DataGenerationMethod.positive in data_generation_methods:
|
|
446
455
|
yield make_case(
|
|
447
456
|
combo,
|
|
448
457
|
f"All required properties and optional '{opt_param}'",
|
|
@@ -453,10 +462,13 @@ def _iter_coverage_cases(
|
|
|
453
462
|
)
|
|
454
463
|
if DataGenerationMethod.negative in data_generation_methods:
|
|
455
464
|
subschema = _combination_schema(combo, required, parameter_set)
|
|
456
|
-
|
|
465
|
+
for case in _yield_negative(subschema, location, container_name):
|
|
466
|
+
# Already generated in one of the blocks above
|
|
467
|
+
if location != "path" and not case.meta.description.startswith("Missing required property"):
|
|
468
|
+
yield case
|
|
457
469
|
|
|
458
470
|
# 3. Generate one combination for each size from 2 to N-1 of optional parameters
|
|
459
|
-
if len(optional) > 1:
|
|
471
|
+
if len(optional) > 1 and DataGenerationMethod.positive in data_generation_methods:
|
|
460
472
|
for size in range(2, len(optional)):
|
|
461
473
|
for combination in combinations(optional, size):
|
|
462
474
|
combo = {k: v for k, v in base_container.items() if k in required or k in combination}
|
schemathesis/cli/__init__.py
CHANGED
|
@@ -323,6 +323,15 @@ REPORT_TO_SERVICE = ReportToService()
|
|
|
323
323
|
multiple=True,
|
|
324
324
|
metavar="",
|
|
325
325
|
)
|
|
326
|
+
@grouped_option(
|
|
327
|
+
"--experimental-missing-required-header-allowed-statuses",
|
|
328
|
+
"missing_required_header_allowed_statuses",
|
|
329
|
+
help="Comma-separated list of status codes expected for test cases with a missing required header",
|
|
330
|
+
type=CsvListChoice(),
|
|
331
|
+
callback=callbacks.convert_status_codes,
|
|
332
|
+
metavar="",
|
|
333
|
+
envvar="SCHEMATHESIS_EXPERIMENTAL_MISSING_REQUIRED_HEADER_ALLOWED_STATUSES",
|
|
334
|
+
)
|
|
326
335
|
@grouped_option(
|
|
327
336
|
"--experimental-positive-data-acceptance-allowed-statuses",
|
|
328
337
|
"positive_data_acceptance_allowed_statuses",
|
|
@@ -855,6 +864,7 @@ def run(
|
|
|
855
864
|
set_cookie: dict[str, str],
|
|
856
865
|
set_path: dict[str, str],
|
|
857
866
|
experiments: list,
|
|
867
|
+
missing_required_header_allowed_statuses: list[str],
|
|
858
868
|
positive_data_acceptance_allowed_statuses: list[str],
|
|
859
869
|
negative_data_rejection_allowed_statuses: list[str],
|
|
860
870
|
checks: Iterable[str] = DEFAULT_CHECKS_NAMES,
|
|
@@ -1172,6 +1182,11 @@ def run(
|
|
|
1172
1182
|
selected_checks += (positive_data_acceptance,)
|
|
1173
1183
|
if positive_data_acceptance_allowed_statuses:
|
|
1174
1184
|
checks_config.positive_data_acceptance.allowed_statuses = positive_data_acceptance_allowed_statuses
|
|
1185
|
+
if missing_required_header_allowed_statuses:
|
|
1186
|
+
from ..specs.openapi.checks import missing_required_header
|
|
1187
|
+
|
|
1188
|
+
selected_checks += (missing_required_header,)
|
|
1189
|
+
checks_config.missing_required_header.allowed_statuses = missing_required_header_allowed_statuses
|
|
1175
1190
|
if negative_data_rejection_allowed_statuses:
|
|
1176
1191
|
checks_config.negative_data_rejection.allowed_statuses = negative_data_rejection_allowed_statuses
|
|
1177
1192
|
|
|
@@ -21,6 +21,7 @@ from ..internal.copy import fast_deepcopy
|
|
|
21
21
|
from ..specs.openapi.converter import update_pattern_in_schema
|
|
22
22
|
from ..specs.openapi.formats import STRING_FORMATS, get_default_format_strategies
|
|
23
23
|
from ..specs.openapi.patterns import update_quantifier
|
|
24
|
+
from ..transports.headers import has_invalid_characters, is_latin_1_encodable
|
|
24
25
|
from ._hypothesis import get_single_example
|
|
25
26
|
from ._methods import DataGenerationMethod
|
|
26
27
|
|
|
@@ -103,42 +104,55 @@ def cached_draw(strategy: st.SearchStrategy) -> Any:
|
|
|
103
104
|
@dataclass
|
|
104
105
|
class CoverageContext:
|
|
105
106
|
data_generation_methods: list[DataGenerationMethod]
|
|
106
|
-
|
|
107
|
+
location: str
|
|
108
|
+
path: list[str | int]
|
|
107
109
|
|
|
108
|
-
__slots__ = ("data_generation_methods", "
|
|
110
|
+
__slots__ = ("location", "data_generation_methods", "path")
|
|
109
111
|
|
|
110
112
|
def __init__(
|
|
111
113
|
self,
|
|
114
|
+
*,
|
|
115
|
+
location: str,
|
|
112
116
|
data_generation_methods: list[DataGenerationMethod] | None = None,
|
|
113
|
-
|
|
117
|
+
path: list[str | int] | None = None,
|
|
114
118
|
) -> None:
|
|
119
|
+
self.location = location
|
|
115
120
|
self.data_generation_methods = (
|
|
116
121
|
data_generation_methods if data_generation_methods is not None else DataGenerationMethod.all()
|
|
117
122
|
)
|
|
118
|
-
self.
|
|
123
|
+
self.path = path or []
|
|
119
124
|
|
|
120
125
|
@contextmanager
|
|
121
|
-
def
|
|
122
|
-
self.
|
|
126
|
+
def at(self, key: str | int) -> Generator[None, None, None]:
|
|
127
|
+
self.path.append(key)
|
|
123
128
|
try:
|
|
124
129
|
yield
|
|
125
130
|
finally:
|
|
126
|
-
self.
|
|
131
|
+
self.path.pop()
|
|
127
132
|
|
|
128
133
|
@property
|
|
129
|
-
def
|
|
130
|
-
return "/" + "/".join(str(key) for key in self.
|
|
134
|
+
def current_path(self) -> str:
|
|
135
|
+
return "/" + "/".join(str(key) for key in self.path)
|
|
131
136
|
|
|
132
137
|
def with_positive(self) -> CoverageContext:
|
|
133
138
|
return CoverageContext(
|
|
134
|
-
|
|
139
|
+
location=self.location,
|
|
140
|
+
data_generation_methods=[DataGenerationMethod.positive],
|
|
141
|
+
path=self.path,
|
|
135
142
|
)
|
|
136
143
|
|
|
137
144
|
def with_negative(self) -> CoverageContext:
|
|
138
145
|
return CoverageContext(
|
|
139
|
-
|
|
146
|
+
location=self.location,
|
|
147
|
+
data_generation_methods=[DataGenerationMethod.negative],
|
|
148
|
+
path=self.path,
|
|
140
149
|
)
|
|
141
150
|
|
|
151
|
+
def is_valid_for_location(self, value: Any) -> bool:
|
|
152
|
+
if self.location in ("header", "cookie") and isinstance(value, str):
|
|
153
|
+
return is_latin_1_encodable(value) and not has_invalid_characters("", value)
|
|
154
|
+
return True
|
|
155
|
+
|
|
142
156
|
def generate_from(self, strategy: st.SearchStrategy) -> Any:
|
|
143
157
|
return cached_draw(strategy)
|
|
144
158
|
|
|
@@ -322,7 +336,7 @@ def cover_schema_iter(
|
|
|
322
336
|
if DataGenerationMethod.negative in ctx.data_generation_methods:
|
|
323
337
|
template = None
|
|
324
338
|
for key, value in schema.items():
|
|
325
|
-
with _ignore_unfixable(), ctx.
|
|
339
|
+
with _ignore_unfixable(), ctx.at(key):
|
|
326
340
|
if key == "enum":
|
|
327
341
|
yield from _negative_enum(ctx, value, seen)
|
|
328
342
|
elif key == "const":
|
|
@@ -350,21 +364,17 @@ def cover_schema_iter(
|
|
|
350
364
|
elif key == "maximum":
|
|
351
365
|
next = value + 1
|
|
352
366
|
if next not in seen:
|
|
353
|
-
yield NegativeValue(
|
|
354
|
-
next, description="Value greater than maximum", location=ctx.current_location
|
|
355
|
-
)
|
|
367
|
+
yield NegativeValue(next, description="Value greater than maximum", location=ctx.current_path)
|
|
356
368
|
seen.add(next)
|
|
357
369
|
elif key == "minimum":
|
|
358
370
|
next = value - 1
|
|
359
371
|
if next not in seen:
|
|
360
|
-
yield NegativeValue(
|
|
361
|
-
next, description="Value smaller than minimum", location=ctx.current_location
|
|
362
|
-
)
|
|
372
|
+
yield NegativeValue(next, description="Value smaller than minimum", location=ctx.current_path)
|
|
363
373
|
seen.add(next)
|
|
364
374
|
elif key == "exclusiveMaximum" or key == "exclusiveMinimum" and value not in seen:
|
|
365
375
|
verb = "greater" if key == "exclusiveMaximum" else "smaller"
|
|
366
376
|
limit = "maximum" if key == "exclusiveMaximum" else "minimum"
|
|
367
|
-
yield NegativeValue(value, description=f"Value {verb} than {limit}", location=ctx.
|
|
377
|
+
yield NegativeValue(value, description=f"Value {verb} than {limit}", location=ctx.current_path)
|
|
368
378
|
seen.add(value)
|
|
369
379
|
elif key == "multipleOf":
|
|
370
380
|
for value_ in _negative_multiple_of(ctx, schema, value):
|
|
@@ -391,7 +401,7 @@ def cover_schema_iter(
|
|
|
391
401
|
k = _to_hashable_key(value)
|
|
392
402
|
if k not in seen:
|
|
393
403
|
yield NegativeValue(
|
|
394
|
-
value, description="String smaller than minLength", location=ctx.
|
|
404
|
+
value, description="String smaller than minLength", location=ctx.current_path
|
|
395
405
|
)
|
|
396
406
|
seen.add(k)
|
|
397
407
|
elif key == "maxLength" and value < BUFFER_SIZE:
|
|
@@ -413,7 +423,7 @@ def cover_schema_iter(
|
|
|
413
423
|
k = _to_hashable_key(value)
|
|
414
424
|
if k not in seen:
|
|
415
425
|
yield NegativeValue(
|
|
416
|
-
value, description="String larger than maxLength", location=ctx.
|
|
426
|
+
value, description="String larger than maxLength", location=ctx.current_path
|
|
417
427
|
)
|
|
418
428
|
seen.add(k)
|
|
419
429
|
except (InvalidArgument, Unsatisfiable):
|
|
@@ -428,12 +438,12 @@ def cover_schema_iter(
|
|
|
428
438
|
yield NegativeValue(
|
|
429
439
|
{**template, UNKNOWN_PROPERTY_KEY: UNKNOWN_PROPERTY_VALUE},
|
|
430
440
|
description="Object with unexpected properties",
|
|
431
|
-
location=ctx.
|
|
441
|
+
location=ctx.current_path,
|
|
432
442
|
)
|
|
433
443
|
elif key == "allOf":
|
|
434
444
|
nctx = ctx.with_negative()
|
|
435
445
|
if len(value) == 1:
|
|
436
|
-
with nctx.
|
|
446
|
+
with nctx.at(0):
|
|
437
447
|
yield from cover_schema_iter(nctx, value[0], seen)
|
|
438
448
|
else:
|
|
439
449
|
with _ignore_unfixable():
|
|
@@ -443,7 +453,7 @@ def cover_schema_iter(
|
|
|
443
453
|
nctx = ctx.with_negative()
|
|
444
454
|
# NOTE: Other sub-schemas are not filtered out
|
|
445
455
|
for idx, sub_schema in enumerate(value):
|
|
446
|
-
with nctx.
|
|
456
|
+
with nctx.at(idx):
|
|
447
457
|
yield from cover_schema_iter(nctx, sub_schema, seen)
|
|
448
458
|
|
|
449
459
|
|
|
@@ -487,15 +497,17 @@ def _positive_string(ctx: CoverageContext, schema: dict) -> Generator[GeneratedV
|
|
|
487
497
|
examples = schema.get("examples")
|
|
488
498
|
default = schema.get("default")
|
|
489
499
|
if example or examples or default:
|
|
490
|
-
if example:
|
|
500
|
+
if example and ctx.is_valid_for_location(example):
|
|
491
501
|
yield PositiveValue(example, description="Example value")
|
|
492
502
|
if examples:
|
|
493
503
|
for example in examples:
|
|
494
|
-
|
|
504
|
+
if ctx.is_valid_for_location(example):
|
|
505
|
+
yield PositiveValue(example, description="Example value")
|
|
495
506
|
if (
|
|
496
507
|
default
|
|
497
508
|
and not (example is not None and default == example)
|
|
498
509
|
and not (examples is not None and any(default == ex for ex in examples))
|
|
510
|
+
and ctx.is_valid_for_location(default)
|
|
499
511
|
):
|
|
500
512
|
yield PositiveValue(default, description="Default value")
|
|
501
513
|
elif not min_length and not max_length:
|
|
@@ -748,14 +760,14 @@ def _negative_enum(
|
|
|
748
760
|
ctx: CoverageContext, value: list, seen: set[Any | tuple[type, str]]
|
|
749
761
|
) -> Generator[GeneratedValue, None, None]:
|
|
750
762
|
def is_not_in_value(x: Any) -> bool:
|
|
751
|
-
if x in value:
|
|
763
|
+
if x in value or not ctx.is_valid_for_location(x):
|
|
752
764
|
return False
|
|
753
765
|
_hashed = _to_hashable_key(x)
|
|
754
766
|
return _hashed not in seen
|
|
755
767
|
|
|
756
768
|
strategy = (st.none() | st.booleans() | NUMERIC_STRATEGY | st.text()).filter(is_not_in_value)
|
|
757
769
|
value = ctx.generate_from(strategy)
|
|
758
|
-
yield NegativeValue(value, description="Invalid enum value", location=ctx.
|
|
770
|
+
yield NegativeValue(value, description="Invalid enum value", location=ctx.current_path)
|
|
759
771
|
hashed = _to_hashable_key(value)
|
|
760
772
|
seen.add(hashed)
|
|
761
773
|
|
|
@@ -765,12 +777,12 @@ def _negative_properties(
|
|
|
765
777
|
) -> Generator[GeneratedValue, None, None]:
|
|
766
778
|
nctx = ctx.with_negative()
|
|
767
779
|
for key, sub_schema in properties.items():
|
|
768
|
-
with nctx.
|
|
780
|
+
with nctx.at(key):
|
|
769
781
|
for value in cover_schema_iter(nctx, sub_schema):
|
|
770
782
|
yield NegativeValue(
|
|
771
783
|
{**template, key: value.value},
|
|
772
784
|
description=f"Object with invalid '{key}' value: {value.description}",
|
|
773
|
-
location=nctx.
|
|
785
|
+
location=nctx.current_path,
|
|
774
786
|
parameter=key,
|
|
775
787
|
)
|
|
776
788
|
|
|
@@ -784,12 +796,12 @@ def _negative_pattern_properties(
|
|
|
784
796
|
key = ctx.generate_from(st.from_regex(pattern))
|
|
785
797
|
except re.error:
|
|
786
798
|
continue
|
|
787
|
-
with nctx.
|
|
799
|
+
with nctx.at(pattern):
|
|
788
800
|
for value in cover_schema_iter(nctx, sub_schema):
|
|
789
801
|
yield NegativeValue(
|
|
790
802
|
{**template, key: value.value},
|
|
791
803
|
description=f"Object with invalid pattern key '{key}' ('{pattern}') value: {value.description}",
|
|
792
|
-
location=nctx.
|
|
804
|
+
location=nctx.current_path,
|
|
793
805
|
)
|
|
794
806
|
|
|
795
807
|
|
|
@@ -800,7 +812,7 @@ def _negative_items(ctx: CoverageContext, schema: dict[str, Any] | bool) -> Gene
|
|
|
800
812
|
yield NegativeValue(
|
|
801
813
|
[value.value],
|
|
802
814
|
description=f"Array with invalid items: {value.description}",
|
|
803
|
-
location=nctx.
|
|
815
|
+
location=nctx.current_path,
|
|
804
816
|
)
|
|
805
817
|
|
|
806
818
|
|
|
@@ -817,12 +829,12 @@ def _negative_pattern(
|
|
|
817
829
|
return
|
|
818
830
|
yield NegativeValue(
|
|
819
831
|
ctx.generate_from(
|
|
820
|
-
st.text(min_size=min_length or 0, max_size=max_length)
|
|
821
|
-
|
|
822
|
-
)
|
|
832
|
+
st.text(min_size=min_length or 0, max_size=max_length)
|
|
833
|
+
.filter(partial(_not_matching_pattern, pattern=compiled))
|
|
834
|
+
.filter(ctx.is_valid_for_location)
|
|
823
835
|
),
|
|
824
836
|
description=f"Value not matching the '{pattern}' pattern",
|
|
825
|
-
location=ctx.
|
|
837
|
+
location=ctx.current_path,
|
|
826
838
|
)
|
|
827
839
|
|
|
828
840
|
|
|
@@ -836,13 +848,13 @@ def _negative_multiple_of(
|
|
|
836
848
|
yield NegativeValue(
|
|
837
849
|
ctx.generate_from_schema(_with_negated_key(schema, "multipleOf", multiple_of)),
|
|
838
850
|
description=f"Non-multiple of {multiple_of}",
|
|
839
|
-
location=ctx.
|
|
851
|
+
location=ctx.current_path,
|
|
840
852
|
)
|
|
841
853
|
|
|
842
854
|
|
|
843
855
|
def _negative_unique_items(ctx: CoverageContext, schema: dict) -> Generator[GeneratedValue, None, None]:
|
|
844
856
|
unique = ctx.generate_from_schema({**schema, "type": "array", "minItems": 1, "maxItems": 1})
|
|
845
|
-
yield NegativeValue(unique + unique, description="Non-unique items", location=ctx.
|
|
857
|
+
yield NegativeValue(unique + unique, description="Non-unique items", location=ctx.current_path)
|
|
846
858
|
|
|
847
859
|
|
|
848
860
|
def _negative_required(
|
|
@@ -852,7 +864,7 @@ def _negative_required(
|
|
|
852
864
|
yield NegativeValue(
|
|
853
865
|
{k: v for k, v in template.items() if k != key},
|
|
854
866
|
description=f"Missing required property: {key}",
|
|
855
|
-
location=ctx.
|
|
867
|
+
location=ctx.current_path,
|
|
856
868
|
parameter=key,
|
|
857
869
|
)
|
|
858
870
|
|
|
@@ -878,7 +890,7 @@ def _negative_format(ctx: CoverageContext, schema: dict, format: str) -> Generat
|
|
|
878
890
|
yield NegativeValue(
|
|
879
891
|
ctx.generate_from(strategy),
|
|
880
892
|
description=f"Value not matching the '{format}' format",
|
|
881
|
-
location=ctx.
|
|
893
|
+
location=ctx.current_path,
|
|
882
894
|
)
|
|
883
895
|
|
|
884
896
|
|
|
@@ -901,7 +913,7 @@ def _negative_type(ctx: CoverageContext, seen: set, ty: str | list[str]) -> Gene
|
|
|
901
913
|
hashed = _to_hashable_key(value)
|
|
902
914
|
if hashed in seen:
|
|
903
915
|
continue
|
|
904
|
-
yield NegativeValue(value, description="Incorrect type", location=ctx.
|
|
916
|
+
yield NegativeValue(value, description="Incorrect type", location=ctx.current_path)
|
|
905
917
|
seen.add(hashed)
|
|
906
918
|
|
|
907
919
|
|
schemathesis/internal/checks.py
CHANGED
|
@@ -29,8 +29,14 @@ class PositiveDataAcceptanceConfig:
|
|
|
29
29
|
allowed_statuses: list[str] = field(default_factory=lambda: ["2xx", "401", "403", "404"])
|
|
30
30
|
|
|
31
31
|
|
|
32
|
+
@dataclass
|
|
33
|
+
class MissingRequiredHeaderConfig:
|
|
34
|
+
allowed_statuses: list[str] = field(default_factory=lambda: ["406"])
|
|
35
|
+
|
|
36
|
+
|
|
32
37
|
@dataclass
|
|
33
38
|
class CheckConfig:
|
|
39
|
+
missing_required_header: MissingRequiredHeaderConfig = field(default_factory=MissingRequiredHeaderConfig)
|
|
34
40
|
negative_data_rejection: NegativeDataRejectionConfig = field(default_factory=NegativeDataRejectionConfig)
|
|
35
41
|
positive_data_acceptance: PositiveDataAcceptanceConfig = field(default_factory=PositiveDataAcceptanceConfig)
|
|
36
42
|
|
schemathesis/models.py
CHANGED
|
@@ -269,6 +269,20 @@ def positive_data_acceptance(ctx: CheckContext, response: GenericResponse, case:
|
|
|
269
269
|
return None
|
|
270
270
|
|
|
271
271
|
|
|
272
|
+
def missing_required_header(ctx: CheckContext, response: GenericResponse, case: Case) -> bool | None:
|
|
273
|
+
if (
|
|
274
|
+
case.meta
|
|
275
|
+
and case.meta.parameter_location == "header"
|
|
276
|
+
and case.meta.description
|
|
277
|
+
and case.meta.description.startswith("Missing ")
|
|
278
|
+
):
|
|
279
|
+
config = ctx.config.missing_required_header
|
|
280
|
+
allowed_statuses = expand_status_codes(config.allowed_statuses or [])
|
|
281
|
+
if response.status_code not in allowed_statuses:
|
|
282
|
+
raise AssertionError(f"Unexpected response status for a missing header: {response.status_code}")
|
|
283
|
+
return None
|
|
284
|
+
|
|
285
|
+
|
|
272
286
|
def has_only_additional_properties_in_non_body_parameters(case: Case) -> bool:
|
|
273
287
|
# Check if the case contains only additional properties in query, headers, or cookies.
|
|
274
288
|
# This function is used to determine if negation is solely in the form of extra properties,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: schemathesis
|
|
3
|
-
Version: 3.38.
|
|
3
|
+
Version: 3.38.9
|
|
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=PsmomVk_mXZaqhOZJh5qftYSJ7qJs3wwBTSITiBXvT0,24273
|
|
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=zE-j-5uyQdIxnWx-rqJyXCbyrWZlrTyiTml109yZeN4,49797
|
|
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
|
|
@@ -28,7 +28,7 @@ schemathesis/targets.py,sha256=XIGRghvEzbmEJjse9aZgNEj67L3jAbiazm2rxURWgDE,2351
|
|
|
28
28
|
schemathesis/throttling.py,sha256=aisUc4MJDGIOGUAs9L2DlWWpdd4KyAFuNVKhYoaUC9M,1719
|
|
29
29
|
schemathesis/types.py,sha256=Tem2Q_zyMCd0Clp5iGKv6Fu13wdcbxVE8tCVH9WWt7A,1065
|
|
30
30
|
schemathesis/utils.py,sha256=LwqxqoAKmRiAdj-qUbNmgQgsamc49V5lc5TnOIDuuMA,4904
|
|
31
|
-
schemathesis/cli/__init__.py,sha256=
|
|
31
|
+
schemathesis/cli/__init__.py,sha256=GlM52jH2p8pSoHnSc0mfxNRTGVLslT_9rTLLy3jgbgo,75272
|
|
32
32
|
schemathesis/cli/__main__.py,sha256=MWaenjaUTZIfNPFzKmnkTiawUri7DVldtg3mirLwzU8,92
|
|
33
33
|
schemathesis/cli/callbacks.py,sha256=-VA_I_mVma9WxFNtUR8d2KNICKJD5ScayfSdKKPEP5Y,16321
|
|
34
34
|
schemathesis/cli/cassettes.py,sha256=zji-B-uuwyr0Z0BzQX-DLMV6lWb58JtLExcUE1v3m4Y,20153
|
|
@@ -61,9 +61,9 @@ 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=XgT1yX6iy__qEXN3lFs0PYZkFwXFHAgJf7ow3nmjcDc,39243
|
|
65
65
|
schemathesis/internal/__init__.py,sha256=93HcdG3LF0BbQKbCteOsFMa1w6nXl8yTmx87QLNJOik,161
|
|
66
|
-
schemathesis/internal/checks.py,sha256=
|
|
66
|
+
schemathesis/internal/checks.py,sha256=EI5EjN_PI9QWNwJlsnUKjC6B1r0arRdpqniVZT-N0mE,2672
|
|
67
67
|
schemathesis/internal/copy.py,sha256=DcL56z-d69kKR_5u8mlHvjSL1UTyUKNMAwexrwHFY1s,1031
|
|
68
68
|
schemathesis/internal/datetime.py,sha256=zPLBL0XXLNfP-KYel3H2m8pnsxjsA_4d-zTOhJg2EPQ,136
|
|
69
69
|
schemathesis/internal/deprecation.py,sha256=Ty5VBFBlufkITpP0WWTPIPbnB7biDi0kQgXVYWZp820,1273
|
|
@@ -107,7 +107,7 @@ schemathesis/specs/graphql/validation.py,sha256=uINIOt-2E7ZuQV2CxKzwez-7L9tDtqzM
|
|
|
107
107
|
schemathesis/specs/openapi/__init__.py,sha256=HDcx3bqpa6qWPpyMrxAbM3uTo0Lqpg-BUNZhDJSJKnw,279
|
|
108
108
|
schemathesis/specs/openapi/_cache.py,sha256=PAiAu4X_a2PQgD2lG5H3iisXdyg4SaHpU46bRZvfNkM,4320
|
|
109
109
|
schemathesis/specs/openapi/_hypothesis.py,sha256=nU8UDn1PzGCre4IVmwIuO9-CZv1KJe1fYY0d2BojhSo,22981
|
|
110
|
-
schemathesis/specs/openapi/checks.py,sha256=
|
|
110
|
+
schemathesis/specs/openapi/checks.py,sha256=MbFRk78U1m5iVGVOJNKifM2KgaGHQaT1DOF6HdR1UZQ,24496
|
|
111
111
|
schemathesis/specs/openapi/constants.py,sha256=JqM_FHOenqS_MuUE9sxVQ8Hnw0DNM8cnKDwCwPLhID4,783
|
|
112
112
|
schemathesis/specs/openapi/converter.py,sha256=Yxw9lS_JKEyi-oJuACT07fm04bqQDlAu-iHwzkeDvE4,3546
|
|
113
113
|
schemathesis/specs/openapi/definitions.py,sha256=WTkWwCgTc3OMxfKsqh6YDoGfZMTThSYrHGp8h0vLAK0,93935
|
|
@@ -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.9.dist-info/METADATA,sha256=c8_79jrn5P4jA6wzGq1UDK-LdZ74GyVDXzFajk4U6rc,12923
|
|
157
|
+
schemathesis-3.38.9.dist-info/WHEEL,sha256=C2FUgwZgiLbznR-k0b_5k3Ai_1aASOXDss3lzCUsUug,87
|
|
158
|
+
schemathesis-3.38.9.dist-info/entry_points.txt,sha256=VHyLcOG7co0nOeuk8WjgpRETk5P1E2iCLrn26Zkn5uk,158
|
|
159
|
+
schemathesis-3.38.9.dist-info/licenses/LICENSE,sha256=PsPYgrDhZ7g9uwihJXNG-XVb55wj2uYhkl2DD8oAzY0,1103
|
|
160
|
+
schemathesis-3.38.9.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|