schemathesis 3.39.10__py3-none-any.whl → 3.39.12__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 +119 -61
- schemathesis/cli/__init__.py +14 -1
- schemathesis/cli/callbacks.py +8 -0
- schemathesis/cli/options.py +6 -1
- schemathesis/failures.py +6 -0
- schemathesis/generation/__init__.py +1 -0
- schemathesis/internal/checks.py +3 -1
- schemathesis/runner/impl/core.py +18 -8
- schemathesis/specs/openapi/checks.py +6 -1
- schemathesis/specs/openapi/examples.py +3 -5
- schemathesis/specs/openapi/patterns.py +33 -3
- schemathesis/specs/openapi/serialization.py +12 -0
- {schemathesis-3.39.10.dist-info → schemathesis-3.39.12.dist-info}/METADATA +1 -1
- {schemathesis-3.39.10.dist-info → schemathesis-3.39.12.dist-info}/RECORD +17 -17
- {schemathesis-3.39.10.dist-info → schemathesis-3.39.12.dist-info}/WHEEL +0 -0
- {schemathesis-3.39.10.dist-info → schemathesis-3.39.12.dist-info}/entry_points.txt +0 -0
- {schemathesis-3.39.10.dist-info → schemathesis-3.39.12.dist-info}/licenses/LICENSE +0 -0
schemathesis/_hypothesis.py
CHANGED
@@ -4,7 +4,6 @@ from __future__ import annotations
|
|
4
4
|
|
5
5
|
import asyncio
|
6
6
|
from dataclasses import dataclass
|
7
|
-
import json
|
8
7
|
import warnings
|
9
8
|
from functools import wraps
|
10
9
|
from itertools import combinations
|
@@ -16,8 +15,10 @@ from hypothesis.errors import HypothesisWarning, Unsatisfiable
|
|
16
15
|
from hypothesis.internal.entropy import deterministic_PRNG
|
17
16
|
from jsonschema.exceptions import SchemaError
|
18
17
|
|
18
|
+
from schemathesis.serializers import get_first_matching_media_type
|
19
|
+
|
19
20
|
from . import _patches
|
20
|
-
from .auths import get_auth_storage_from_test
|
21
|
+
from .auths import AuthStorage, get_auth_storage_from_test
|
21
22
|
from .constants import DEFAULT_DEADLINE, NOT_SET
|
22
23
|
from .exceptions import OperationSchemaError, SerializationNotPossible
|
23
24
|
from .experimental import COVERAGE_PHASE
|
@@ -28,6 +29,7 @@ from .parameters import ParameterSet
|
|
28
29
|
from .transports.content_types import parse_content_type
|
29
30
|
from .transports.headers import has_invalid_characters, is_latin_1_encodable
|
30
31
|
from .types import NotSet
|
32
|
+
from schemathesis import auths
|
31
33
|
|
32
34
|
if TYPE_CHECKING:
|
33
35
|
from .utils import GivenInput
|
@@ -112,7 +114,15 @@ def create_test(
|
|
112
114
|
wrapped_test, operation, hook_dispatcher=hook_dispatcher, as_strategy_kwargs=as_strategy_kwargs
|
113
115
|
)
|
114
116
|
if COVERAGE_PHASE.is_enabled:
|
115
|
-
|
117
|
+
unexpected_methods = generation_config.unexpected_methods if generation_config else None
|
118
|
+
wrapped_test = add_coverage(
|
119
|
+
wrapped_test,
|
120
|
+
operation,
|
121
|
+
data_generation_methods,
|
122
|
+
auth_storage,
|
123
|
+
as_strategy_kwargs,
|
124
|
+
unexpected_methods,
|
125
|
+
)
|
116
126
|
return wrapped_test
|
117
127
|
|
118
128
|
|
@@ -216,20 +226,46 @@ def adjust_urlencoded_payload(case: Case) -> None:
|
|
216
226
|
|
217
227
|
|
218
228
|
def add_coverage(
|
219
|
-
test: Callable,
|
229
|
+
test: Callable,
|
230
|
+
operation: APIOperation,
|
231
|
+
data_generation_methods: list[DataGenerationMethod],
|
232
|
+
auth_storage: AuthStorage | None,
|
233
|
+
as_strategy_kwargs: dict[str, Any],
|
234
|
+
unexpected_methods: set[str] | None = None,
|
220
235
|
) -> Callable:
|
221
|
-
|
222
|
-
|
223
|
-
|
236
|
+
from schemathesis.specs.openapi.constants import LOCATION_TO_CONTAINER
|
237
|
+
|
238
|
+
auth_context = auths.AuthContext(
|
239
|
+
operation=operation,
|
240
|
+
app=operation.app,
|
241
|
+
)
|
242
|
+
overrides = {
|
243
|
+
container: as_strategy_kwargs[container]
|
244
|
+
for container in LOCATION_TO_CONTAINER.values()
|
245
|
+
if container in as_strategy_kwargs
|
246
|
+
}
|
247
|
+
for case in _iter_coverage_cases(operation, data_generation_methods, unexpected_methods):
|
248
|
+
if case.media_type and get_first_matching_media_type(case.media_type) is None:
|
249
|
+
continue
|
250
|
+
adjust_urlencoded_payload(case)
|
251
|
+
auths.set_on_case(case, auth_context, auth_storage)
|
252
|
+
for container_name, value in overrides.items():
|
253
|
+
container = getattr(case, container_name)
|
254
|
+
if container is None:
|
255
|
+
setattr(case, container_name, value)
|
256
|
+
else:
|
257
|
+
container.update(value)
|
258
|
+
test = hypothesis.example(case=case)(test)
|
224
259
|
return test
|
225
260
|
|
226
261
|
|
227
262
|
class Template:
|
228
|
-
__slots__ = ("_components", "_template")
|
263
|
+
__slots__ = ("_components", "_template", "_serializers")
|
229
264
|
|
230
|
-
def __init__(self) -> None:
|
265
|
+
def __init__(self, serializers: dict[str, Callable]) -> None:
|
231
266
|
self._components: dict[str, DataGenerationMethod] = {}
|
232
267
|
self._template: dict[str, Any] = {}
|
268
|
+
self._serializers = serializers
|
233
269
|
|
234
270
|
def __contains__(self, key: str) -> bool:
|
235
271
|
return key in self._template
|
@@ -251,36 +287,58 @@ class Template:
|
|
251
287
|
self._components[component_name] = DataGenerationMethod.negative
|
252
288
|
|
253
289
|
container = self._template.setdefault(component_name, {})
|
254
|
-
|
255
|
-
container[name] = _stringify_value(value.value, location)
|
256
|
-
else:
|
257
|
-
container[name] = value.value
|
290
|
+
container[name] = value.value
|
258
291
|
|
259
292
|
def set_body(self, body: coverage.GeneratedValue, media_type: str) -> None:
|
260
293
|
self._template["body"] = body.value
|
261
294
|
self._template["media_type"] = media_type
|
262
295
|
self._components["body"] = body.data_generation_method
|
263
296
|
|
297
|
+
def _serialize(self, kwargs: dict[str, Any]) -> dict[str, Any]:
|
298
|
+
from schemathesis.specs.openapi._hypothesis import quote_all
|
299
|
+
|
300
|
+
output = {}
|
301
|
+
for container_name, value in kwargs.items():
|
302
|
+
serializer = self._serializers.get(container_name)
|
303
|
+
if container_name in ("headers", "cookies") and isinstance(value, dict):
|
304
|
+
value = _stringify_value(value, container_name)
|
305
|
+
if serializer is not None:
|
306
|
+
value = serializer(value)
|
307
|
+
if container_name == "query" and isinstance(value, dict):
|
308
|
+
value = _stringify_value(value, container_name)
|
309
|
+
if container_name == "path_parameters" and isinstance(value, dict):
|
310
|
+
value = _stringify_value(quote_all(value), container_name)
|
311
|
+
output[container_name] = value
|
312
|
+
return output
|
313
|
+
|
264
314
|
def unmodified(self) -> TemplateValue:
|
265
|
-
|
315
|
+
kwargs = self._template.copy()
|
316
|
+
kwargs = self._serialize(kwargs)
|
317
|
+
return TemplateValue(kwargs=kwargs, components=self._components.copy())
|
266
318
|
|
267
319
|
def with_body(self, *, media_type: str, value: coverage.GeneratedValue) -> TemplateValue:
|
268
320
|
kwargs = {**self._template, "media_type": media_type, "body": value.value}
|
321
|
+
kwargs = self._serialize(kwargs)
|
269
322
|
components = {**self._components, "body": value.data_generation_method}
|
270
323
|
return TemplateValue(kwargs=kwargs, components=components)
|
271
324
|
|
272
325
|
def with_parameter(self, *, location: str, name: str, value: coverage.GeneratedValue) -> TemplateValue:
|
273
326
|
from .specs.openapi.constants import LOCATION_TO_CONTAINER
|
274
327
|
|
275
|
-
if _should_stringify(location, value):
|
276
|
-
generated = _stringify_value(value.value, location)
|
277
|
-
else:
|
278
|
-
generated = value.value
|
279
|
-
|
280
328
|
container_name = LOCATION_TO_CONTAINER[location]
|
281
329
|
container = self._template[container_name]
|
282
|
-
|
283
|
-
|
330
|
+
return self.with_container(
|
331
|
+
container_name=container_name,
|
332
|
+
value={**container, name: value.value},
|
333
|
+
data_generation_method=value.data_generation_method,
|
334
|
+
)
|
335
|
+
|
336
|
+
def with_container(
|
337
|
+
self, *, container_name: str, value: Any, data_generation_method: DataGenerationMethod
|
338
|
+
) -> TemplateValue:
|
339
|
+
kwargs = {**self._template, container_name: value}
|
340
|
+
kwargs = self._serialize(kwargs)
|
341
|
+
components = {**self._components, container_name: data_generation_method}
|
284
342
|
return TemplateValue(kwargs=kwargs, components=components)
|
285
343
|
|
286
344
|
|
@@ -291,38 +349,41 @@ class TemplateValue:
|
|
291
349
|
__slots__ = ("kwargs", "components")
|
292
350
|
|
293
351
|
|
294
|
-
def
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
352
|
+
def _stringify_value(val: Any, container_name: str) -> Any:
|
353
|
+
if val is None:
|
354
|
+
return "null"
|
355
|
+
if val is True:
|
356
|
+
return "true"
|
357
|
+
if val is False:
|
358
|
+
return "false"
|
359
|
+
if isinstance(val, (int, float)):
|
360
|
+
return str(val)
|
299
361
|
if isinstance(val, list):
|
300
|
-
if
|
362
|
+
if container_name == "query":
|
301
363
|
# Having a list here ensures there will be multiple query parameters wit the same name
|
302
|
-
return [
|
364
|
+
return [_stringify_value(item, container_name) for item in val]
|
303
365
|
# use comma-separated values style for arrays
|
304
|
-
return ",".join(
|
305
|
-
|
366
|
+
return ",".join(_stringify_value(sub, container_name) for sub in val)
|
367
|
+
if isinstance(val, dict):
|
368
|
+
return {key: _stringify_value(sub, container_name) for key, sub in val.items()}
|
369
|
+
return val
|
306
370
|
|
307
371
|
|
308
372
|
def _iter_coverage_cases(
|
309
|
-
operation: APIOperation,
|
373
|
+
operation: APIOperation,
|
374
|
+
data_generation_methods: list[DataGenerationMethod],
|
375
|
+
unexpected_methods: set[str] | None = None,
|
310
376
|
) -> Generator[Case, None, None]:
|
311
377
|
from .specs.openapi.constants import LOCATION_TO_CONTAINER
|
312
378
|
from .specs.openapi.examples import find_in_responses, find_matching_in_responses
|
313
|
-
|
314
|
-
def _stringify_value(val: Any, location: str) -> str | list[str]:
|
315
|
-
if isinstance(val, list):
|
316
|
-
if location == "query":
|
317
|
-
# Having a list here ensures there will be multiple query parameters wit the same name
|
318
|
-
return [json.dumps(item) for item in val]
|
319
|
-
# use comma-separated values style for arrays
|
320
|
-
return ",".join(json.dumps(sub) for sub in val)
|
321
|
-
return json.dumps(val)
|
379
|
+
from schemathesis.specs.openapi.serialization import get_serializers_for_operation
|
322
380
|
|
323
381
|
generators: dict[tuple[str, str], Generator[coverage.GeneratedValue, None, None]] = {}
|
324
|
-
|
382
|
+
serializers = get_serializers_for_operation(operation)
|
383
|
+
template = Template(serializers)
|
325
384
|
responses = find_in_responses(operation)
|
385
|
+
# NOTE: The HEAD method is excluded
|
386
|
+
unexpected_methods = unexpected_methods or {"get", "put", "post", "delete", "options", "patch", "trace"}
|
326
387
|
for parameter in operation.iter_parameters():
|
327
388
|
location = parameter.location
|
328
389
|
name = parameter.name
|
@@ -398,8 +459,7 @@ def _iter_coverage_cases(
|
|
398
459
|
yield case
|
399
460
|
if DataGenerationMethod.negative in data_generation_methods:
|
400
461
|
# Generate HTTP methods that are not specified in the spec
|
401
|
-
|
402
|
-
methods = {"get", "put", "post", "delete", "options", "patch", "trace"} - set(operation.schema[operation.path])
|
462
|
+
methods = unexpected_methods - set(operation.schema[operation.path])
|
403
463
|
for method in sorted(methods):
|
404
464
|
data = template.unmodified()
|
405
465
|
case = operation.make_case(**data.kwargs)
|
@@ -415,10 +475,12 @@ def _iter_coverage_cases(
|
|
415
475
|
# I.e. contains just `default` value without any other keywords
|
416
476
|
value = container.get(parameter.name, NOT_SET)
|
417
477
|
if value is not NOT_SET:
|
418
|
-
data = template.
|
419
|
-
|
420
|
-
|
478
|
+
data = template.with_container(
|
479
|
+
container_name="query",
|
480
|
+
value={**container, parameter.name: [value, value]},
|
481
|
+
data_generation_method=DataGenerationMethod.negative,
|
421
482
|
)
|
483
|
+
case = operation.make_case(**data.kwargs)
|
422
484
|
case.data_generation_method = DataGenerationMethod.negative
|
423
485
|
case.meta = _make_meta(
|
424
486
|
description=f"Duplicate `{parameter.name}` query parameter",
|
@@ -435,17 +497,19 @@ def _iter_coverage_cases(
|
|
435
497
|
location = parameter.location
|
436
498
|
container_name = LOCATION_TO_CONTAINER[location]
|
437
499
|
container = template[container_name]
|
438
|
-
data = template.
|
439
|
-
|
440
|
-
|
500
|
+
data = template.with_container(
|
501
|
+
container_name=container_name,
|
502
|
+
value={k: v for k, v in container.items() if k != name},
|
503
|
+
data_generation_method=DataGenerationMethod.negative,
|
441
504
|
)
|
505
|
+
case = operation.make_case(**data.kwargs)
|
442
506
|
case.data_generation_method = DataGenerationMethod.negative
|
443
507
|
case.meta = _make_meta(
|
444
508
|
description=f"Missing `{name}` at {location}",
|
445
509
|
location=None,
|
446
510
|
parameter=name,
|
447
511
|
parameter_location=location,
|
448
|
-
**
|
512
|
+
**data.components,
|
449
513
|
)
|
450
514
|
yield case
|
451
515
|
# Generate combinations for each location
|
@@ -474,23 +538,17 @@ def _iter_coverage_cases(
|
|
474
538
|
_parameter: str | None,
|
475
539
|
_data_generation_method: DataGenerationMethod,
|
476
540
|
) -> Case:
|
477
|
-
|
478
|
-
|
479
|
-
|
480
|
-
|
481
|
-
}
|
482
|
-
else:
|
483
|
-
container = container_values
|
484
|
-
|
485
|
-
data = template.unmodified()
|
486
|
-
case = operation.make_case(**{**data.kwargs, _container_name: container})
|
541
|
+
data = template.with_container(
|
542
|
+
container_name=_container_name, value=container_values, data_generation_method=_data_generation_method
|
543
|
+
)
|
544
|
+
case = operation.make_case(**data.kwargs)
|
487
545
|
case.data_generation_method = _data_generation_method
|
488
546
|
case.meta = _make_meta(
|
489
547
|
description=description,
|
490
548
|
location=None,
|
491
549
|
parameter=_parameter,
|
492
550
|
parameter_location=_location,
|
493
|
-
**
|
551
|
+
**data.components,
|
494
552
|
)
|
495
553
|
return case
|
496
554
|
|
schemathesis/cli/__init__.py
CHANGED
@@ -9,7 +9,7 @@ from collections import defaultdict
|
|
9
9
|
from dataclasses import dataclass
|
10
10
|
from enum import Enum
|
11
11
|
from queue import Queue
|
12
|
-
from typing import TYPE_CHECKING, Any, Callable, Generator, Iterable, Literal, NoReturn, Sequence,
|
12
|
+
from typing import TYPE_CHECKING, Any, Callable, Generator, Iterable, Literal, NoReturn, Sequence, cast
|
13
13
|
from urllib.parse import urlparse
|
14
14
|
|
15
15
|
import click
|
@@ -27,6 +27,7 @@ from ..constants import (
|
|
27
27
|
DEFAULT_STATEFUL_RECURSION_LIMIT,
|
28
28
|
EXTENSIONS_DOCUMENTATION_URL,
|
29
29
|
HOOKS_MODULE_ENV_VAR,
|
30
|
+
HTTP_METHODS,
|
30
31
|
HYPOTHESIS_IN_MEMORY_DATABASE_IDENTIFIER,
|
31
32
|
ISSUE_TRACKER_URL,
|
32
33
|
WAIT_FOR_SCHEMA_ENV_VAR,
|
@@ -323,6 +324,16 @@ REPORT_TO_SERVICE = ReportToService()
|
|
323
324
|
multiple=True,
|
324
325
|
metavar="",
|
325
326
|
)
|
327
|
+
@grouped_option(
|
328
|
+
"--experimental-coverage-unexpected-methods",
|
329
|
+
"coverage_unexpected_methods",
|
330
|
+
help="HTTP methods to use when generating test cases with methods not specified in the API during the coverage phase.",
|
331
|
+
type=CsvChoice(sorted(HTTP_METHODS), case_sensitive=False),
|
332
|
+
callback=callbacks.convert_http_methods,
|
333
|
+
metavar="",
|
334
|
+
default=None,
|
335
|
+
envvar="SCHEMATHESIS_EXPERIMENTAL_COVERAGE_UNEXPECTED_METHODS",
|
336
|
+
)
|
326
337
|
@grouped_option(
|
327
338
|
"--experimental-no-failfast",
|
328
339
|
"no_failfast",
|
@@ -874,6 +885,7 @@ def run(
|
|
874
885
|
set_path: dict[str, str],
|
875
886
|
experiments: list,
|
876
887
|
no_failfast: bool,
|
888
|
+
coverage_unexpected_methods: set[str] | None,
|
877
889
|
missing_required_header_allowed_statuses: list[str],
|
878
890
|
positive_data_acceptance_allowed_statuses: list[str],
|
879
891
|
negative_data_rejection_allowed_statuses: list[str],
|
@@ -1000,6 +1012,7 @@ def run(
|
|
1000
1012
|
graphql_allow_null=generation_graphql_allow_null,
|
1001
1013
|
codec=generation_codec,
|
1002
1014
|
with_security_parameters=generation_with_security_parameters,
|
1015
|
+
unexpected_methods=coverage_unexpected_methods,
|
1003
1016
|
)
|
1004
1017
|
|
1005
1018
|
report: ReportToService | click.utils.LazyFile | None
|
schemathesis/cli/callbacks.py
CHANGED
@@ -344,6 +344,14 @@ def convert_checks(ctx: click.core.Context, param: click.core.Parameter, value:
|
|
344
344
|
return reduce(operator.iadd, value, [])
|
345
345
|
|
346
346
|
|
347
|
+
def convert_http_methods(
|
348
|
+
ctx: click.core.Context, param: click.core.Parameter, value: list[str] | None
|
349
|
+
) -> set[str] | None:
|
350
|
+
if value is None:
|
351
|
+
return value
|
352
|
+
return {item.lower() for item in value}
|
353
|
+
|
354
|
+
|
347
355
|
def convert_status_codes(
|
348
356
|
ctx: click.core.Context, param: click.core.Parameter, value: list[str] | None
|
349
357
|
) -> list[str] | None:
|
schemathesis/cli/options.py
CHANGED
@@ -26,7 +26,12 @@ class CustomHelpMessageChoice(click.Choice):
|
|
26
26
|
class BaseCsvChoice(click.Choice):
|
27
27
|
def parse_value(self, value: str) -> tuple[list[str], set[str]]:
|
28
28
|
selected = [item for item in value.split(",") if item]
|
29
|
-
|
29
|
+
if not self.case_sensitive:
|
30
|
+
invalid_options = {
|
31
|
+
item for item in selected if item.upper() not in {choice.upper() for choice in self.choices}
|
32
|
+
}
|
33
|
+
else:
|
34
|
+
invalid_options = set(selected) - set(self.choices)
|
30
35
|
return selected, invalid_options
|
31
36
|
|
32
37
|
def fail_on_invalid_options(self, invalid_options: set[str], selected: list[str]) -> NoReturn:
|
schemathesis/failures.py
CHANGED
@@ -143,6 +143,12 @@ class AcceptedNegativeData(FailureContext):
|
|
143
143
|
title: str = "Accepted negative data"
|
144
144
|
type: str = "accepted_negative_data"
|
145
145
|
|
146
|
+
def unique_by_key(self, check_message: str | None) -> tuple[str, ...]:
|
147
|
+
return (
|
148
|
+
check_message or self.message,
|
149
|
+
str(self.status_code),
|
150
|
+
)
|
151
|
+
|
146
152
|
|
147
153
|
@dataclass(repr=False)
|
148
154
|
class RejectedPositiveData(FailureContext):
|
schemathesis/internal/checks.py
CHANGED
@@ -21,7 +21,9 @@ CheckFunction = Callable[["CheckContext", "GenericResponse", "Case"], Optional[b
|
|
21
21
|
@dataclass
|
22
22
|
class NegativeDataRejectionConfig:
|
23
23
|
# 5xx will pass through
|
24
|
-
allowed_statuses: list[str] = field(
|
24
|
+
allowed_statuses: list[str] = field(
|
25
|
+
default_factory=lambda: ["400", "401", "403", "404", "406", "422", "428", "5xx"]
|
26
|
+
)
|
25
27
|
|
26
28
|
|
27
29
|
@dataclass
|
schemathesis/runner/impl/core.py
CHANGED
@@ -150,13 +150,6 @@ class BaseRunner:
|
|
150
150
|
def _should_warn_about_only_4xx(result: TestResult) -> bool:
|
151
151
|
if all(check.response is None for check in result.checks):
|
152
152
|
return False
|
153
|
-
# Don't warn if we saw any 2xx or 5xx responses
|
154
|
-
if any(
|
155
|
-
check.response.status_code < 400 or check.response.status_code >= 500
|
156
|
-
for check in result.checks
|
157
|
-
if check.response is not None
|
158
|
-
):
|
159
|
-
return False
|
160
153
|
# Don't duplicate auth warnings
|
161
154
|
if {check.response.status_code for check in result.checks if check.response is not None} <= {401, 403}:
|
162
155
|
return False
|
@@ -164,11 +157,28 @@ class BaseRunner:
|
|
164
157
|
return True
|
165
158
|
|
166
159
|
def _check_warnings() -> None:
|
160
|
+
# Warn if all positive test cases got 4xx in return and no failure was found
|
161
|
+
def all_positive_are_rejected(result: TestResult) -> bool:
|
162
|
+
seen_positive = False
|
163
|
+
for check in result.checks:
|
164
|
+
if check.example.data_generation_method != DataGenerationMethod.positive:
|
165
|
+
continue
|
166
|
+
seen_positive = True
|
167
|
+
if check.response is None:
|
168
|
+
continue
|
169
|
+
# At least one positive response for positive test case
|
170
|
+
if 200 <= check.response.status_code < 300:
|
171
|
+
return False
|
172
|
+
# If there are positive test cases, and we ended up here, then there are no 2xx responses for them
|
173
|
+
# Otherwise, there are no positive test cases at all and this check should pass
|
174
|
+
return seen_positive
|
175
|
+
|
167
176
|
for result in ctx.data.results:
|
168
177
|
# Only warn about 4xx responses in successful positive test scenarios
|
169
178
|
if (
|
170
179
|
all(check.value == Status.success for check in result.checks)
|
171
|
-
and
|
180
|
+
and DataGenerationMethod.positive in result.data_generation_method
|
181
|
+
and all_positive_are_rejected(result)
|
172
182
|
and _should_warn_about_only_4xx(result)
|
173
183
|
):
|
174
184
|
ctx.add_warning(
|
@@ -294,7 +294,12 @@ def missing_required_header(ctx: CheckContext, response: GenericResponse, case:
|
|
294
294
|
|
295
295
|
|
296
296
|
def unsupported_method(ctx: CheckContext, response: GenericResponse, case: Case) -> bool | None:
|
297
|
-
if
|
297
|
+
if (
|
298
|
+
case.meta
|
299
|
+
and case.meta.description
|
300
|
+
and case.meta.description.startswith("Unspecified HTTP method:")
|
301
|
+
and response.request.method != "OPTIONS"
|
302
|
+
):
|
298
303
|
if response.status_code != 405:
|
299
304
|
raise AssertionError(
|
300
305
|
f"Unexpected response status for unspecified HTTP method: {response.status_code}\nExpected: 405"
|
@@ -9,6 +9,8 @@ from typing import TYPE_CHECKING, Any, Generator, Iterator, Union, cast
|
|
9
9
|
import requests
|
10
10
|
from hypothesis_jsonschema import from_schema
|
11
11
|
|
12
|
+
from schemathesis.specs.openapi.serialization import get_serializers_for_operation
|
13
|
+
|
12
14
|
from ...constants import DEFAULT_RESPONSE_TIMEOUT
|
13
15
|
from ...generation import get_single_example
|
14
16
|
from ...internal.copy import fast_deepcopy
|
@@ -48,11 +50,7 @@ def get_strategies_from_examples(
|
|
48
50
|
operation: APIOperation[OpenAPIParameter, Case], as_strategy_kwargs: dict[str, Any] | None = None
|
49
51
|
) -> list[SearchStrategy[Case]]:
|
50
52
|
"""Build a set of strategies that generate test cases based on explicit examples in the schema."""
|
51
|
-
maps =
|
52
|
-
for location, container in LOCATION_TO_CONTAINER.items():
|
53
|
-
serializer = operation.get_parameter_serializer(location)
|
54
|
-
if serializer is not None:
|
55
|
-
maps[container] = serializer
|
53
|
+
maps = get_serializers_for_operation(operation)
|
56
54
|
|
57
55
|
def serialize_components(case: Case) -> Case:
|
58
56
|
"""Applies special serialization rules for case components.
|
@@ -3,6 +3,8 @@ from __future__ import annotations
|
|
3
3
|
import re
|
4
4
|
from functools import lru_cache
|
5
5
|
|
6
|
+
from ...exceptions import InternalError
|
7
|
+
|
6
8
|
try: # pragma: no cover
|
7
9
|
import re._constants as sre
|
8
10
|
import re._parser as sre_parse
|
@@ -29,7 +31,15 @@ def update_quantifier(pattern: str, min_length: int | None, max_length: int | No
|
|
29
31
|
|
30
32
|
try:
|
31
33
|
parsed = sre_parse.parse(pattern)
|
32
|
-
|
34
|
+
updated = _handle_parsed_pattern(parsed, pattern, min_length, max_length)
|
35
|
+
try:
|
36
|
+
re.compile(updated)
|
37
|
+
except re.error as exc:
|
38
|
+
raise InternalError(
|
39
|
+
f"The combination of min_length={min_length} and max_length={max_length} applied to the original pattern '{pattern}' resulted in an invalid regex: '{updated}'. "
|
40
|
+
"This indicates a bug in the regex quantifier merging logic"
|
41
|
+
) from exc
|
42
|
+
return updated
|
33
43
|
except re.error:
|
34
44
|
# Invalid pattern
|
35
45
|
return pattern
|
@@ -114,6 +124,21 @@ def _handle_anchored_pattern(parsed: list, pattern: str, min_length: int | None,
|
|
114
124
|
|
115
125
|
for op, value in pattern_parts:
|
116
126
|
if op == LITERAL:
|
127
|
+
# Check if the literal comes from a bracketed expression,
|
128
|
+
# e.g. Python regex parses "[+]" as a single LITERAL token.
|
129
|
+
if pattern[current_position] == "[":
|
130
|
+
# Find the matching closing bracket.
|
131
|
+
end_idx = current_position + 1
|
132
|
+
while end_idx < len(pattern):
|
133
|
+
# Check for an unescaped closing bracket.
|
134
|
+
if pattern[end_idx] == "]" and (end_idx == current_position + 1 or pattern[end_idx - 1] != "\\"):
|
135
|
+
end_idx += 1
|
136
|
+
break
|
137
|
+
end_idx += 1
|
138
|
+
# Append the entire character set.
|
139
|
+
result += pattern[current_position:end_idx]
|
140
|
+
current_position = end_idx
|
141
|
+
continue
|
117
142
|
if pattern[current_position] == "\\":
|
118
143
|
# Escaped value
|
119
144
|
current_position += 2
|
@@ -261,13 +286,18 @@ def _handle_repeat_quantifier(
|
|
261
286
|
min_length, max_length = _build_size(min_repeat, max_repeat, min_length, max_length)
|
262
287
|
if min_length > max_length:
|
263
288
|
return pattern
|
264
|
-
|
289
|
+
inner = _strip_quantifier(pattern)
|
290
|
+
if inner.startswith("(") and inner.endswith(")"):
|
291
|
+
inner = inner[1:-1]
|
292
|
+
return f"({inner})" + _build_quantifier(min_length, max_length)
|
265
293
|
|
266
294
|
|
267
295
|
def _handle_literal_or_in_quantifier(pattern: str, min_length: int | None, max_length: int | None) -> str:
|
268
296
|
"""Handle literal or character class quantifiers."""
|
269
297
|
min_length = 1 if min_length is None else max(min_length, 1)
|
270
|
-
|
298
|
+
if pattern.startswith("(") and pattern.endswith(")"):
|
299
|
+
pattern = pattern[1:-1]
|
300
|
+
return f"({pattern})" + _build_quantifier(min_length, max_length)
|
271
301
|
|
272
302
|
|
273
303
|
def _build_quantifier(minimum: int | None, maximum: int | None) -> str:
|
@@ -3,6 +3,9 @@ from __future__ import annotations
|
|
3
3
|
import json
|
4
4
|
from typing import Any, Callable, Dict, Generator, List
|
5
5
|
|
6
|
+
from schemathesis.schemas import APIOperation
|
7
|
+
from schemathesis.specs.openapi.constants import LOCATION_TO_CONTAINER
|
8
|
+
|
6
9
|
from ...utils import compose
|
7
10
|
|
8
11
|
Generated = Dict[str, Any]
|
@@ -11,6 +14,15 @@ DefinitionList = List[Definition]
|
|
11
14
|
MapFunction = Callable[[Generated], Generated]
|
12
15
|
|
13
16
|
|
17
|
+
def get_serializers_for_operation(operation: APIOperation) -> dict[str, Callable]:
|
18
|
+
serializers = {}
|
19
|
+
for location, container in LOCATION_TO_CONTAINER.items():
|
20
|
+
serializer = operation.get_parameter_serializer(location)
|
21
|
+
if serializer is not None:
|
22
|
+
serializers[container] = serializer
|
23
|
+
return serializers
|
24
|
+
|
25
|
+
|
14
26
|
def make_serializer(
|
15
27
|
func: Callable[[DefinitionList], Generator[Callable | None, None, None]],
|
16
28
|
) -> Callable[[DefinitionList], Callable | None]:
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: schemathesis
|
3
|
-
Version: 3.39.
|
3
|
+
Version: 3.39.12
|
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=CEfWX38CsPy-RzwMGdKuJD9mY_AV8fIq_ZhabGp4tW0,30759
|
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
|
@@ -12,7 +12,7 @@ schemathesis/checks.py,sha256=YPUI1N5giGBy1072vd77e6HWelGAKrJUmJLEG4oqfF8,2630
|
|
12
12
|
schemathesis/code_samples.py,sha256=rsdTo6ksyUs3ZMhqx0mmmkPSKUCFa--snIOYsXgZd80,4120
|
13
13
|
schemathesis/constants.py,sha256=RHwog2lAz84qG6KCpP1U15A4a9w1xcwbgZ97aY4juQg,2555
|
14
14
|
schemathesis/exceptions.py,sha256=5zjPlyVoQNJGbwufplL6ZVV7FEBPBNPHGdlQRJ7xnhE,20449
|
15
|
-
schemathesis/failures.py,sha256=
|
15
|
+
schemathesis/failures.py,sha256=OsQM5FDs79usUM4MLA20WIDaoW6SKTR39IyKDjeiwtQ,8197
|
16
16
|
schemathesis/filters.py,sha256=f3c_yXIBwIin-9Y0qU2TkcC1NEM_Mw34jGUHQc0BOyw,17026
|
17
17
|
schemathesis/graphql.py,sha256=XiuKcfoOB92iLFC8zpz2msLkM0_V0TLdxPNBqrrGZ8w,216
|
18
18
|
schemathesis/hooks.py,sha256=p5AXgjVGtka0jn9MOeyBaRUtNbqZTs4iaJqytYTacHc,14856
|
@@ -28,16 +28,16 @@ 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=jiDVw31fF8oNl0TyAWJpDo-166PkCqNV4t1rUcy_4oo,76352
|
32
32
|
schemathesis/cli/__main__.py,sha256=MWaenjaUTZIfNPFzKmnkTiawUri7DVldtg3mirLwzU8,92
|
33
|
-
schemathesis/cli/callbacks.py,sha256
|
33
|
+
schemathesis/cli/callbacks.py,sha256=UMCYPfoHXgxtT-uui0zlfeNoOLzteJu90q9ID3qt1m4,16540
|
34
34
|
schemathesis/cli/cassettes.py,sha256=zji-B-uuwyr0Z0BzQX-DLMV6lWb58JtLExcUE1v3m4Y,20153
|
35
35
|
schemathesis/cli/constants.py,sha256=wk-0GsoJIel8wFFerQ6Kf_6eAYUtIWkwMFwyAqv3yj4,1635
|
36
36
|
schemathesis/cli/context.py,sha256=j_lvYQiPa6Q7P4P_IGCM9V2y2gJSpDbpxIIzR5oFB2I,2567
|
37
37
|
schemathesis/cli/debug.py,sha256=_YA-bX1ujHl4bqQDEum7M-I2XHBTEGbvgkhvcvKhmgU,658
|
38
38
|
schemathesis/cli/handlers.py,sha256=EXSAFe5TQlHANz1AVlSttfsoDT2oeaeFbqq1N7e2udw,467
|
39
39
|
schemathesis/cli/junitxml.py,sha256=_psBdqGwH4OKySSWeva41mbgGLav86UnWhQyOt99gnU,5331
|
40
|
-
schemathesis/cli/options.py,sha256=
|
40
|
+
schemathesis/cli/options.py,sha256=jPqJxkuAb91wtB_aOUFkGDdGtJ3UDwT2Nn3vvL-odsE,3062
|
41
41
|
schemathesis/cli/reporting.py,sha256=KC3sxSc1u4aFQ-0Q8CQ3G4HTEl7QxlubGnJgNKmVJdQ,3627
|
42
42
|
schemathesis/cli/sanitization.py,sha256=Onw_NWZSom6XTVNJ5NHnC0PAhrYAcGzIXJbsBCzLkn4,1005
|
43
43
|
schemathesis/cli/output/__init__.py,sha256=AXaUzQ1nhQ-vXhW4-X-91vE2VQtEcCOrGtQXXNN55iQ,29
|
@@ -58,12 +58,12 @@ schemathesis/extra/pytest_plugin.py,sha256=3FF7pcqK26J__FGq6uOZdaD-1tZ-khQEwpdwb
|
|
58
58
|
schemathesis/fixups/__init__.py,sha256=RP5QYJVJhp8LXjhH89fCRaIVU26dHCy74jD9seoYMuc,967
|
59
59
|
schemathesis/fixups/fast_api.py,sha256=mn-KzBqnR8jl4W5fY-_ZySabMDMUnpzCIESMHnlvE1c,1304
|
60
60
|
schemathesis/fixups/utf8_bom.py,sha256=lWT9RNmJG8i-l5AXIpaCT3qCPUwRgzXPW3eoOjmZETA,745
|
61
|
-
schemathesis/generation/__init__.py,sha256=
|
61
|
+
schemathesis/generation/__init__.py,sha256=PClFLK3bu-8Gsy71rgdD0ULMqySrzX-Um8Tan77x_5A,1628
|
62
62
|
schemathesis/generation/_hypothesis.py,sha256=74fzLPHugZgMQXerWYFAMqCAjtAXz5E4gek7Gnkhli4,1756
|
63
63
|
schemathesis/generation/_methods.py,sha256=r8oVlJ71_gXcnEhU-byw2E0R2RswQQFm8U7yGErSqbw,1204
|
64
64
|
schemathesis/generation/coverage.py,sha256=1CilQSe2DIdMdeWA6RL22so2bZULPRwc0CQBRxcLRFs,39370
|
65
65
|
schemathesis/internal/__init__.py,sha256=93HcdG3LF0BbQKbCteOsFMa1w6nXl8yTmx87QLNJOik,161
|
66
|
-
schemathesis/internal/checks.py,sha256=
|
66
|
+
schemathesis/internal/checks.py,sha256=ZPvsPJ7gWwK0IpzBFgOMaq4L2e0yfeC8qxPrnpauVFA,2741
|
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=XnzwSegbbdQyoTF1OGW_s9pdjIfN_Uzzdb2rfah1w2o,1261
|
@@ -80,7 +80,7 @@ schemathesis/runner/probes.py,sha256=no5AfO3kse25qvHevjeUfB0Q3C860V2AYzschUW3QMQ
|
|
80
80
|
schemathesis/runner/serialization.py,sha256=vZi1wd9HX9Swp9VJ_hZFeDgy3Y726URpHra-TbPvQhk,20762
|
81
81
|
schemathesis/runner/impl/__init__.py,sha256=1E2iME8uthYPBh9MjwVBCTFV-P3fi7AdphCCoBBspjs,199
|
82
82
|
schemathesis/runner/impl/context.py,sha256=KY06FXVOFQ6DBaa_FomSBXL81ULs3D21IW1u3yLqs1E,2434
|
83
|
-
schemathesis/runner/impl/core.py,sha256=
|
83
|
+
schemathesis/runner/impl/core.py,sha256=pB_0zs_MfzAcbrVvRU85mONAn8QXVqFfM_y7yV8a2l0,49962
|
84
84
|
schemathesis/runner/impl/solo.py,sha256=y5QSxgK8nBCEjZVD5BpFvYUXmB6tEjk6TwxAo__NejA,2911
|
85
85
|
schemathesis/runner/impl/threadpool.py,sha256=yNR5LYE8f3N_4t42OwSgy0_qdGgBPM7d11F9c9oEAAs,15075
|
86
86
|
schemathesis/service/__init__.py,sha256=cDVTCFD1G-vvhxZkJUwiToTAEQ-0ByIoqwXvJBCf_V8,472
|
@@ -107,21 +107,21 @@ 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=cuHTZsoHV2fdUz23_F99-mLelT1xtvaiS9EcG7R-hZs,26897
|
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
|
114
|
-
schemathesis/specs/openapi/examples.py,sha256=
|
114
|
+
schemathesis/specs/openapi/examples.py,sha256=yBK0hjq5ROjk7BCLe7BO2dr7raijeZ6_KlZEol-cU-E,20401
|
115
115
|
schemathesis/specs/openapi/formats.py,sha256=3KtEC-8nQRwMErS-WpMadXsr8R0O-NzYwFisZqMuc-8,2761
|
116
116
|
schemathesis/specs/openapi/links.py,sha256=C4Uir2P_EcpqME8ee_a1vdUM8Tm3ZcKNn2YsGjZiMUQ,17935
|
117
117
|
schemathesis/specs/openapi/loaders.py,sha256=jlTYLoG5sVRh8xycIF2M2VDCZ44M80Sct07a_ycg1Po,25698
|
118
118
|
schemathesis/specs/openapi/media_types.py,sha256=dNTxpRQbY3SubdVjh4Cjb38R6Bc9MF9BsRQwPD87x0g,1017
|
119
119
|
schemathesis/specs/openapi/parameters.py,sha256=X_3PKqUScIiN_vbSFEauPYyxASyFv-_9lZ_9QEZRLqo,14655
|
120
|
-
schemathesis/specs/openapi/patterns.py,sha256=
|
120
|
+
schemathesis/specs/openapi/patterns.py,sha256=L99UtslPvwObCVf5ndq3vL2YjQ7H1nMb-ZNMcyz_Qvk,12677
|
121
121
|
schemathesis/specs/openapi/references.py,sha256=euxM02kQGMHh4Ss1jWjOY_gyw_HazafKITIsvOEiAvI,9831
|
122
122
|
schemathesis/specs/openapi/schemas.py,sha256=JA9SiBnwYg75kYnd4_0CWOuQv_XTfYwuDeGmFe4RtVo,53724
|
123
123
|
schemathesis/specs/openapi/security.py,sha256=Z-6pk2Ga1PTUtBe298KunjVHsNh5A-teegeso7zcPIE,7138
|
124
|
-
schemathesis/specs/openapi/serialization.py,sha256=
|
124
|
+
schemathesis/specs/openapi/serialization.py,sha256=rcZfqQbWer_RELedu4Sh5h_RhKYPWTfUjnmLwpP2R_A,11842
|
125
125
|
schemathesis/specs/openapi/utils.py,sha256=ER4vJkdFVDIE7aKyxyYatuuHVRNutytezgE52pqZNE8,900
|
126
126
|
schemathesis/specs/openapi/validation.py,sha256=Q9ThZlwU-mSz7ExDnIivnZGi1ivC5hlX2mIMRAM79kc,999
|
127
127
|
schemathesis/specs/openapi/expressions/__init__.py,sha256=hFpJrIWbPi55GcIVjNFRDDUL8xmDu3mdbdldoHBoFJ0,1729
|
@@ -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.39.
|
157
|
-
schemathesis-3.39.
|
158
|
-
schemathesis-3.39.
|
159
|
-
schemathesis-3.39.
|
160
|
-
schemathesis-3.39.
|
156
|
+
schemathesis-3.39.12.dist-info/METADATA,sha256=B2ychHNPR-8UTeOqBUcqdtLzYPdZEO61YaYadjp9B2A,11977
|
157
|
+
schemathesis-3.39.12.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
158
|
+
schemathesis-3.39.12.dist-info/entry_points.txt,sha256=VHyLcOG7co0nOeuk8WjgpRETk5P1E2iCLrn26Zkn5uk,158
|
159
|
+
schemathesis-3.39.12.dist-info/licenses/LICENSE,sha256=PsPYgrDhZ7g9uwihJXNG-XVb55wj2uYhkl2DD8oAzY0,1103
|
160
|
+
schemathesis-3.39.12.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|