schemathesis 3.36.3__py3-none-any.whl → 3.37.0__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 +1 -1
- schemathesis/cli/__init__.py +41 -5
- schemathesis/cli/callbacks.py +42 -0
- schemathesis/cli/cassettes.py +6 -9
- schemathesis/cli/options.py +7 -0
- schemathesis/exceptions.py +4 -0
- schemathesis/experimental/__init__.py +7 -0
- schemathesis/failures.py +13 -0
- schemathesis/generation/_hypothesis.py +7 -1
- schemathesis/generation/_methods.py +4 -0
- schemathesis/generation/coverage.py +67 -26
- schemathesis/hooks.py +2 -2
- schemathesis/internal/checks.py +21 -2
- schemathesis/lazy.py +4 -3
- schemathesis/runner/__init__.py +9 -0
- schemathesis/runner/impl/context.py +11 -2
- schemathesis/runner/impl/core.py +18 -5
- schemathesis/specs/openapi/checks.py +40 -3
- schemathesis/specs/openapi/definitions.py +7 -11
- schemathesis/specs/openapi/utils.py +5 -1
- schemathesis/stateful/runner.py +5 -1
- schemathesis/stateful/validation.py +11 -14
- schemathesis/transports/__init__.py +1 -1
- {schemathesis-3.36.3.dist-info → schemathesis-3.37.0.dist-info}/METADATA +2 -2
- {schemathesis-3.36.3.dist-info → schemathesis-3.37.0.dist-info}/RECORD +28 -28
- {schemathesis-3.36.3.dist-info → schemathesis-3.37.0.dist-info}/WHEEL +0 -0
- {schemathesis-3.36.3.dist-info → schemathesis-3.37.0.dist-info}/entry_points.txt +0 -0
- {schemathesis-3.36.3.dist-info → schemathesis-3.37.0.dist-info}/licenses/LICENSE +0 -0
schemathesis/_hypothesis.py
CHANGED
schemathesis/cli/__init__.py
CHANGED
|
@@ -36,6 +36,7 @@ from ..filters import FilterSet, expression_to_filter_function, is_deprecated
|
|
|
36
36
|
from ..fixups import ALL_FIXUPS
|
|
37
37
|
from ..generation import DEFAULT_DATA_GENERATION_METHODS, DataGenerationMethod
|
|
38
38
|
from ..hooks import GLOBAL_HOOK_DISPATCHER, HookContext, HookDispatcher, HookScope
|
|
39
|
+
from ..internal.checks import CheckConfig, PositiveDataAcceptanceConfig
|
|
39
40
|
from ..internal.datetime import current_datetime
|
|
40
41
|
from ..internal.output import OutputConfig
|
|
41
42
|
from ..internal.validation import file_exists
|
|
@@ -52,7 +53,7 @@ from .context import ExecutionContext, FileReportContext, ServiceReportContext
|
|
|
52
53
|
from .debug import DebugOutputHandler
|
|
53
54
|
from .handlers import EventHandler
|
|
54
55
|
from .junitxml import JunitXMLHandler
|
|
55
|
-
from .options import CsvChoice, CsvEnumChoice, CustomHelpMessageChoice, OptionalInt
|
|
56
|
+
from .options import CsvChoice, CsvEnumChoice, CsvListChoice, CustomHelpMessageChoice, OptionalInt
|
|
56
57
|
from .sanitization import SanitizationHandler
|
|
57
58
|
|
|
58
59
|
if TYPE_CHECKING:
|
|
@@ -296,6 +297,14 @@ REPORT_TO_SERVICE = ReportToService()
|
|
|
296
297
|
default=False,
|
|
297
298
|
help="Simulate test execution without making any actual requests, useful for validating data generation",
|
|
298
299
|
)
|
|
300
|
+
@grouped_option(
|
|
301
|
+
"--fixups",
|
|
302
|
+
help="Apply compatibility adjustments",
|
|
303
|
+
multiple=True,
|
|
304
|
+
type=click.Choice([*ALL_FIXUPS, "all"]),
|
|
305
|
+
metavar="",
|
|
306
|
+
)
|
|
307
|
+
@group("Experimental options")
|
|
299
308
|
@grouped_option(
|
|
300
309
|
"--experimental",
|
|
301
310
|
"experiments",
|
|
@@ -307,6 +316,7 @@ REPORT_TO_SERVICE = ReportToService()
|
|
|
307
316
|
experimental.STATEFUL_TEST_RUNNER.name,
|
|
308
317
|
experimental.STATEFUL_ONLY.name,
|
|
309
318
|
experimental.COVERAGE_PHASE.name,
|
|
319
|
+
experimental.POSITIVE_DATA_ACCEPTANCE.name,
|
|
310
320
|
]
|
|
311
321
|
),
|
|
312
322
|
callback=callbacks.convert_experimental,
|
|
@@ -314,11 +324,22 @@ REPORT_TO_SERVICE = ReportToService()
|
|
|
314
324
|
metavar="",
|
|
315
325
|
)
|
|
316
326
|
@grouped_option(
|
|
317
|
-
"--
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
type=
|
|
327
|
+
"--experimental-positive-data-acceptance-allowed-statuses",
|
|
328
|
+
"positive_data_acceptance_allowed_statuses",
|
|
329
|
+
help="Comma-separated list of status codes considered as successful responses",
|
|
330
|
+
type=CsvListChoice(),
|
|
331
|
+
callback=callbacks.convert_status_codes,
|
|
332
|
+
metavar="",
|
|
333
|
+
envvar="SCHEMATHESIS_EXPERIMENTAL_POSITIVE_DATA_ACCEPTANCE_ALLOWED_STATUSES",
|
|
334
|
+
)
|
|
335
|
+
@grouped_option(
|
|
336
|
+
"--experimental-negative-data-rejection-allowed-statuses",
|
|
337
|
+
"negative_data_rejection_allowed_statuses",
|
|
338
|
+
help="Comma-separated list of status codes expected for rejected negative data",
|
|
339
|
+
type=CsvListChoice(),
|
|
340
|
+
callback=callbacks.convert_status_codes,
|
|
321
341
|
metavar="",
|
|
342
|
+
envvar="SCHEMATHESIS_EXPERIMENTAL_NEGATIVE_DATA_REJECTION_ALLOWED_STATUSES",
|
|
322
343
|
)
|
|
323
344
|
@group("API validation options")
|
|
324
345
|
@grouped_option(
|
|
@@ -834,6 +855,8 @@ def run(
|
|
|
834
855
|
set_cookie: dict[str, str],
|
|
835
856
|
set_path: dict[str, str],
|
|
836
857
|
experiments: list,
|
|
858
|
+
positive_data_acceptance_allowed_statuses: list[str],
|
|
859
|
+
negative_data_rejection_allowed_statuses: list[str],
|
|
837
860
|
checks: Iterable[str] = DEFAULT_CHECKS_NAMES,
|
|
838
861
|
exclude_checks: Iterable[str] = (),
|
|
839
862
|
data_generation_methods: tuple[DataGenerationMethod, ...] = DEFAULT_DATA_GENERATION_METHODS,
|
|
@@ -1142,6 +1165,16 @@ def run(
|
|
|
1142
1165
|
else:
|
|
1143
1166
|
selected_checks = tuple(check for check in checks_module.ALL_CHECKS if check.__name__ in checks)
|
|
1144
1167
|
|
|
1168
|
+
checks_config = CheckConfig()
|
|
1169
|
+
if experimental.POSITIVE_DATA_ACCEPTANCE.is_enabled:
|
|
1170
|
+
from ..specs.openapi.checks import positive_data_acceptance
|
|
1171
|
+
|
|
1172
|
+
selected_checks += (positive_data_acceptance,)
|
|
1173
|
+
if positive_data_acceptance_allowed_statuses:
|
|
1174
|
+
checks_config.positive_data_acceptance.allowed_statuses = positive_data_acceptance_allowed_statuses
|
|
1175
|
+
if negative_data_rejection_allowed_statuses:
|
|
1176
|
+
checks_config.negative_data_rejection.allowed_statuses = negative_data_rejection_allowed_statuses
|
|
1177
|
+
|
|
1145
1178
|
selected_checks = tuple(check for check in selected_checks if check.__name__ not in exclude_checks)
|
|
1146
1179
|
|
|
1147
1180
|
if fixups:
|
|
@@ -1197,6 +1230,7 @@ def run(
|
|
|
1197
1230
|
stateful_recursion_limit=stateful_recursion_limit,
|
|
1198
1231
|
hypothesis_settings=hypothesis_settings,
|
|
1199
1232
|
generation_config=generation_config,
|
|
1233
|
+
checks_config=checks_config,
|
|
1200
1234
|
output_config=output_config,
|
|
1201
1235
|
service_client=client,
|
|
1202
1236
|
filter_set=filter_set,
|
|
@@ -1300,6 +1334,7 @@ def into_event_stream(
|
|
|
1300
1334
|
filter_set: FilterSet,
|
|
1301
1335
|
# Runtime behavior
|
|
1302
1336
|
checks: Iterable[CheckFunction],
|
|
1337
|
+
checks_config: CheckConfig,
|
|
1303
1338
|
max_response_time: int | None,
|
|
1304
1339
|
targets: Iterable[Target],
|
|
1305
1340
|
workers_num: int,
|
|
@@ -1358,6 +1393,7 @@ def into_event_stream(
|
|
|
1358
1393
|
dry_run=dry_run,
|
|
1359
1394
|
store_interactions=store_interactions,
|
|
1360
1395
|
checks=checks,
|
|
1396
|
+
checks_config=checks_config,
|
|
1361
1397
|
max_response_time=max_response_time,
|
|
1362
1398
|
targets=targets,
|
|
1363
1399
|
workers_num=workers_num,
|
schemathesis/cli/callbacks.py
CHANGED
|
@@ -344,6 +344,48 @@ 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_status_codes(
|
|
348
|
+
ctx: click.core.Context, param: click.core.Parameter, value: list[str] | None
|
|
349
|
+
) -> list[str] | None:
|
|
350
|
+
if not value:
|
|
351
|
+
return value
|
|
352
|
+
|
|
353
|
+
invalid = []
|
|
354
|
+
|
|
355
|
+
for code in value:
|
|
356
|
+
if len(code) != 3:
|
|
357
|
+
invalid.append(code)
|
|
358
|
+
continue
|
|
359
|
+
|
|
360
|
+
if code[0] not in {"1", "2", "3", "4", "5"}:
|
|
361
|
+
invalid.append(code)
|
|
362
|
+
continue
|
|
363
|
+
|
|
364
|
+
upper_code = code.upper()
|
|
365
|
+
|
|
366
|
+
if "X" in upper_code:
|
|
367
|
+
if (
|
|
368
|
+
upper_code[1:] == "XX"
|
|
369
|
+
or (upper_code[1] == "X" and upper_code[2].isdigit())
|
|
370
|
+
or (upper_code[1].isdigit() and upper_code[2] == "X")
|
|
371
|
+
):
|
|
372
|
+
continue
|
|
373
|
+
else:
|
|
374
|
+
invalid.append(code)
|
|
375
|
+
continue
|
|
376
|
+
|
|
377
|
+
if not code.isnumeric():
|
|
378
|
+
invalid.append(code)
|
|
379
|
+
|
|
380
|
+
if invalid:
|
|
381
|
+
raise click.UsageError(
|
|
382
|
+
f"Invalid status code(s): {', '.join(invalid)}. "
|
|
383
|
+
"Use valid 3-digit codes between 100 and 599, "
|
|
384
|
+
"or wildcards (e.g., 2XX, 2X0, 20X), where X is a wildcard digit."
|
|
385
|
+
)
|
|
386
|
+
return value
|
|
387
|
+
|
|
388
|
+
|
|
347
389
|
def convert_code_sample_style(ctx: click.core.Context, param: click.core.Parameter, value: str) -> CodeSampleStyle:
|
|
348
390
|
return CodeSampleStyle.from_str(value)
|
|
349
391
|
|
schemathesis/cli/cassettes.py
CHANGED
|
@@ -79,24 +79,18 @@ class CassetteWriter(EventHandler):
|
|
|
79
79
|
def handle_event(self, context: ExecutionContext, event: events.ExecutionEvent) -> None:
|
|
80
80
|
if isinstance(event, events.Initialized):
|
|
81
81
|
# In the beginning we write metadata and start `http_interactions` list
|
|
82
|
-
self.queue.put(Initialize())
|
|
82
|
+
self.queue.put(Initialize(seed=event.seed))
|
|
83
83
|
elif isinstance(event, events.AfterExecution):
|
|
84
|
-
# Seed is always present at this point, the original Optional[int] type is there because `TestResult`
|
|
85
|
-
# instance is created before `seed` is generated on the hypothesis side
|
|
86
|
-
seed = cast(int, event.result.seed)
|
|
87
84
|
self.queue.put(
|
|
88
85
|
Process(
|
|
89
|
-
seed=seed,
|
|
90
86
|
correlation_id=event.correlation_id,
|
|
91
87
|
thread_id=event.thread_id,
|
|
92
88
|
interactions=event.result.interactions,
|
|
93
89
|
)
|
|
94
90
|
)
|
|
95
91
|
elif isinstance(event, events.AfterStatefulExecution):
|
|
96
|
-
seed = cast(int, event.result.seed)
|
|
97
92
|
self.queue.put(
|
|
98
93
|
Process(
|
|
99
|
-
seed=seed,
|
|
100
94
|
# Correlation ID is not used in stateful testing
|
|
101
95
|
correlation_id="",
|
|
102
96
|
thread_id=event.thread_id,
|
|
@@ -118,12 +112,13 @@ class CassetteWriter(EventHandler):
|
|
|
118
112
|
class Initialize:
|
|
119
113
|
"""Start up, the first message to make preparations before proceeding the input data."""
|
|
120
114
|
|
|
115
|
+
seed: int | None
|
|
116
|
+
|
|
121
117
|
|
|
122
118
|
@dataclass
|
|
123
119
|
class Process:
|
|
124
120
|
"""A new chunk of data should be processed."""
|
|
125
121
|
|
|
126
|
-
seed: int
|
|
127
122
|
correlation_id: str
|
|
128
123
|
thread_id: int
|
|
129
124
|
interactions: list[SerializedInteraction]
|
|
@@ -219,9 +214,11 @@ def vcr_writer(file_handle: click.utils.LazyFile, preserve_exact_body_bytes: boo
|
|
|
219
214
|
)
|
|
220
215
|
write_double_quoted(output, string)
|
|
221
216
|
|
|
217
|
+
seed = "null"
|
|
222
218
|
while True:
|
|
223
219
|
item = queue.get()
|
|
224
220
|
if isinstance(item, Initialize):
|
|
221
|
+
seed = f"'{item.seed}'"
|
|
225
222
|
stream.write(
|
|
226
223
|
f"""command: '{get_command_representation()}'
|
|
227
224
|
recorded_with: 'Schemathesis {SCHEMATHESIS_VERSION}'
|
|
@@ -235,7 +232,7 @@ http_interactions:"""
|
|
|
235
232
|
stream.write(
|
|
236
233
|
f"""\n- id: '{current_id}'
|
|
237
234
|
status: '{status}'
|
|
238
|
-
seed:
|
|
235
|
+
seed: {seed}
|
|
239
236
|
thread_id: {item.thread_id}
|
|
240
237
|
correlation_id: '{item.correlation_id}'
|
|
241
238
|
data_generation_method: '{interaction.data_generation_method.value}'
|
schemathesis/cli/options.py
CHANGED
|
@@ -58,6 +58,13 @@ class CsvChoice(BaseCsvChoice):
|
|
|
58
58
|
self.fail_on_invalid_options(invalid_options, selected)
|
|
59
59
|
|
|
60
60
|
|
|
61
|
+
class CsvListChoice(click.ParamType):
|
|
62
|
+
def convert( # type: ignore[return]
|
|
63
|
+
self, value: str, param: click.core.Parameter | None, ctx: click.core.Context | None
|
|
64
|
+
) -> list[str]:
|
|
65
|
+
return [item for item in value.split(",") if item]
|
|
66
|
+
|
|
67
|
+
|
|
61
68
|
class OptionalInt(click.types.IntRange):
|
|
62
69
|
def convert( # type: ignore
|
|
63
70
|
self, value: str, param: click.core.Parameter | None, ctx: click.core.Context | None
|
schemathesis/exceptions.py
CHANGED
|
@@ -148,6 +148,10 @@ def get_negative_rejection_error(prefix: str, status: int) -> type[CheckFailed]:
|
|
|
148
148
|
return _get_hashed_exception(f"AcceptedNegativeDataError{prefix}", str(status))
|
|
149
149
|
|
|
150
150
|
|
|
151
|
+
def get_positive_acceptance_error(prefix: str, status: int) -> type[CheckFailed]:
|
|
152
|
+
return _get_hashed_exception(f"RejectedPositiveDataError{prefix}", str(status))
|
|
153
|
+
|
|
154
|
+
|
|
151
155
|
def get_use_after_free_error(free: str) -> type[CheckFailed]:
|
|
152
156
|
return _get_hashed_exception("UseAfterFreeError", free)
|
|
153
157
|
|
|
@@ -100,3 +100,10 @@ COVERAGE_PHASE = GLOBAL_EXPERIMENTS.create_experiment(
|
|
|
100
100
|
description="Generate covering test cases",
|
|
101
101
|
discussion_url="https://github.com/schemathesis/schemathesis/discussions/2418",
|
|
102
102
|
)
|
|
103
|
+
POSITIVE_DATA_ACCEPTANCE = GLOBAL_EXPERIMENTS.create_experiment(
|
|
104
|
+
name="positive_data_acceptance",
|
|
105
|
+
verbose_name="Positive Data Acceptance",
|
|
106
|
+
env_var="POSITIVE_DATA_ACCEPTANCE",
|
|
107
|
+
description="Verifying schema-conformant data is accepted",
|
|
108
|
+
discussion_url="https://github.com/schemathesis/schemathesis/discussions/2499",
|
|
109
|
+
)
|
schemathesis/failures.py
CHANGED
|
@@ -137,10 +137,23 @@ class AcceptedNegativeData(FailureContext):
|
|
|
137
137
|
"""Response with negative data was accepted."""
|
|
138
138
|
|
|
139
139
|
message: str
|
|
140
|
+
status_code: int
|
|
141
|
+
allowed_statuses: list[str]
|
|
140
142
|
title: str = "Accepted negative data"
|
|
141
143
|
type: str = "accepted_negative_data"
|
|
142
144
|
|
|
143
145
|
|
|
146
|
+
@dataclass(repr=False)
|
|
147
|
+
class RejectedPositiveData(FailureContext):
|
|
148
|
+
"""Response with positive data was rejected."""
|
|
149
|
+
|
|
150
|
+
message: str
|
|
151
|
+
status_code: int
|
|
152
|
+
allowed_statuses: list[str]
|
|
153
|
+
title: str = "Rejected positive data"
|
|
154
|
+
type: str = "rejected_positive_data"
|
|
155
|
+
|
|
156
|
+
|
|
144
157
|
@dataclass(repr=False)
|
|
145
158
|
class UseAfterFree(FailureContext):
|
|
146
159
|
"""Resource was used after a successful DELETE operation on it."""
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import os
|
|
3
4
|
from functools import lru_cache, reduce
|
|
4
5
|
from operator import or_
|
|
5
6
|
from typing import TYPE_CHECKING, TypeVar
|
|
@@ -8,6 +9,8 @@ if TYPE_CHECKING:
|
|
|
8
9
|
from hypothesis import settings
|
|
9
10
|
from hypothesis import strategies as st
|
|
10
11
|
|
|
12
|
+
SCHEMATHESIS_BENCHMARK_SEED = os.environ.get("SCHEMATHESIS_BENCHMARK_SEED")
|
|
13
|
+
|
|
11
14
|
|
|
12
15
|
@lru_cache
|
|
13
16
|
def default_settings() -> settings:
|
|
@@ -33,13 +36,16 @@ def get_single_example(strategy: st.SearchStrategy[T]) -> T: # type: ignore[typ
|
|
|
33
36
|
|
|
34
37
|
|
|
35
38
|
def add_single_example(strategy: st.SearchStrategy[T], examples: list[T]) -> None:
|
|
36
|
-
from hypothesis import given
|
|
39
|
+
from hypothesis import given, seed
|
|
37
40
|
|
|
38
41
|
@given(strategy) # type: ignore
|
|
39
42
|
@default_settings() # type: ignore
|
|
40
43
|
def example_generating_inner_function(ex: T) -> None:
|
|
41
44
|
examples.append(ex)
|
|
42
45
|
|
|
46
|
+
if SCHEMATHESIS_BENCHMARK_SEED is not None:
|
|
47
|
+
example_generating_inner_function = seed(SCHEMATHESIS_BENCHMARK_SEED)(example_generating_inner_function)
|
|
48
|
+
|
|
43
49
|
example_generating_inner_function()
|
|
44
50
|
|
|
45
51
|
|
|
@@ -26,6 +26,10 @@ class DataGenerationMethod(str, Enum):
|
|
|
26
26
|
DataGenerationMethod.negative: "N",
|
|
27
27
|
}[self]
|
|
28
28
|
|
|
29
|
+
@property
|
|
30
|
+
def is_positive(self) -> bool:
|
|
31
|
+
return self == DataGenerationMethod.positive
|
|
32
|
+
|
|
29
33
|
@property
|
|
30
34
|
def is_negative(self) -> bool:
|
|
31
35
|
return self == DataGenerationMethod.negative
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import functools
|
|
3
4
|
import json
|
|
5
|
+
import re
|
|
4
6
|
from contextlib import contextmanager, suppress
|
|
5
|
-
from dataclasses import dataclass
|
|
6
|
-
from functools import lru_cache
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from functools import lru_cache, partial
|
|
7
9
|
from itertools import combinations
|
|
8
10
|
from typing import Any, Generator, Iterator, TypeVar, cast
|
|
9
11
|
|
|
@@ -13,17 +15,25 @@ from hypothesis.errors import InvalidArgument, Unsatisfiable
|
|
|
13
15
|
from hypothesis_jsonschema import from_schema
|
|
14
16
|
from hypothesis_jsonschema._canonicalise import canonicalish
|
|
15
17
|
|
|
16
|
-
from
|
|
17
|
-
|
|
18
|
+
from ..constants import NOT_SET
|
|
19
|
+
from ..specs.openapi.patterns import update_quantifier
|
|
18
20
|
from ._hypothesis import get_single_example
|
|
19
21
|
from ._methods import DataGenerationMethod
|
|
20
22
|
|
|
23
|
+
|
|
24
|
+
def _replace_zero_with_nonzero(x: float) -> float:
|
|
25
|
+
return x or 0.0
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def json_recursive_strategy(strategy: st.SearchStrategy) -> st.SearchStrategy:
|
|
29
|
+
return st.lists(strategy, max_size=3) | st.dictionaries(st.text(), strategy, max_size=3)
|
|
30
|
+
|
|
31
|
+
|
|
21
32
|
BUFFER_SIZE = 8 * 1024
|
|
22
|
-
FLOAT_STRATEGY: st.SearchStrategy = st.floats(allow_nan=False, allow_infinity=False).map(
|
|
33
|
+
FLOAT_STRATEGY: st.SearchStrategy = st.floats(allow_nan=False, allow_infinity=False).map(_replace_zero_with_nonzero)
|
|
23
34
|
NUMERIC_STRATEGY: st.SearchStrategy = st.integers() | FLOAT_STRATEGY
|
|
24
35
|
JSON_STRATEGY: st.SearchStrategy = st.recursive(
|
|
25
|
-
st.none() | st.booleans() | NUMERIC_STRATEGY | st.text(),
|
|
26
|
-
lambda strategy: st.lists(strategy, max_size=3) | st.dictionaries(st.text(), strategy, max_size=3),
|
|
36
|
+
st.none() | st.booleans() | NUMERIC_STRATEGY | st.text(), json_recursive_strategy
|
|
27
37
|
)
|
|
28
38
|
ARRAY_STRATEGY: st.SearchStrategy = st.lists(JSON_STRATEGY)
|
|
29
39
|
OBJECT_STRATEGY: st.SearchStrategy = st.dictionaries(st.text(), JSON_STRATEGY)
|
|
@@ -60,7 +70,14 @@ def cached_draw(strategy: st.SearchStrategy) -> Any:
|
|
|
60
70
|
|
|
61
71
|
@dataclass
|
|
62
72
|
class CoverageContext:
|
|
63
|
-
data_generation_methods: list[DataGenerationMethod]
|
|
73
|
+
data_generation_methods: list[DataGenerationMethod]
|
|
74
|
+
|
|
75
|
+
__slots__ = ("data_generation_methods",)
|
|
76
|
+
|
|
77
|
+
def __init__(self, data_generation_methods: list[DataGenerationMethod] | None = None) -> None:
|
|
78
|
+
self.data_generation_methods = (
|
|
79
|
+
data_generation_methods if data_generation_methods is not None else DataGenerationMethod.all()
|
|
80
|
+
)
|
|
64
81
|
|
|
65
82
|
@classmethod
|
|
66
83
|
def with_positive(cls) -> CoverageContext:
|
|
@@ -70,12 +87,8 @@ class CoverageContext:
|
|
|
70
87
|
def with_negative(cls) -> CoverageContext:
|
|
71
88
|
return CoverageContext(data_generation_methods=[DataGenerationMethod.negative])
|
|
72
89
|
|
|
73
|
-
def generate_from(self, strategy: st.SearchStrategy
|
|
74
|
-
|
|
75
|
-
value = cached_draw(strategy)
|
|
76
|
-
else:
|
|
77
|
-
value = get_single_example(strategy)
|
|
78
|
-
return value
|
|
90
|
+
def generate_from(self, strategy: st.SearchStrategy) -> Any:
|
|
91
|
+
return cached_draw(strategy)
|
|
79
92
|
|
|
80
93
|
def generate_from_schema(self, schema: dict) -> Any:
|
|
81
94
|
return self.generate_from(from_schema(schema))
|
|
@@ -221,14 +234,23 @@ def cover_schema_iter(
|
|
|
221
234
|
seen.add(k)
|
|
222
235
|
elif key == "minLength" and 0 < value < BUFFER_SIZE:
|
|
223
236
|
with suppress(InvalidArgument):
|
|
224
|
-
|
|
237
|
+
min_length = max_length = value - 1
|
|
238
|
+
new_schema = {**schema, "minLength": min_length, "maxLength": max_length}
|
|
239
|
+
if "pattern" in new_schema:
|
|
240
|
+
new_schema["pattern"] = update_quantifier(schema["pattern"], min_length, max_length)
|
|
241
|
+
value = ctx.generate_from_schema(new_schema)
|
|
225
242
|
k = _to_hashable_key(value)
|
|
226
243
|
if k not in seen:
|
|
227
244
|
yield NegativeValue(value, description="String smaller than minLength")
|
|
228
245
|
seen.add(k)
|
|
229
246
|
elif key == "maxLength" and value < BUFFER_SIZE:
|
|
230
|
-
with suppress(InvalidArgument):
|
|
231
|
-
|
|
247
|
+
with suppress(InvalidArgument, Unsatisfiable):
|
|
248
|
+
min_length = value + 1
|
|
249
|
+
max_length = value + 1
|
|
250
|
+
new_schema = {**schema, "minLength": min_length, "maxLength": max_length}
|
|
251
|
+
if "pattern" in new_schema:
|
|
252
|
+
new_schema["pattern"] = update_quantifier(schema["pattern"], min_length, max_length)
|
|
253
|
+
value = ctx.generate_from_schema(new_schema)
|
|
232
254
|
k = _to_hashable_key(value)
|
|
233
255
|
if k not in seen:
|
|
234
256
|
yield NegativeValue(value, description="String larger than maxLength")
|
|
@@ -554,9 +576,12 @@ def select_combinations(optional: list[str]) -> Iterator[tuple[str, ...]]:
|
|
|
554
576
|
|
|
555
577
|
|
|
556
578
|
def _negative_enum(ctx: CoverageContext, value: list) -> Generator[GeneratedValue, None, None]:
|
|
557
|
-
|
|
579
|
+
def is_not_in_value(x: Any) -> bool:
|
|
580
|
+
return x not in value
|
|
581
|
+
|
|
582
|
+
strategy = JSON_STRATEGY.filter(is_not_in_value)
|
|
558
583
|
# The exact negative value is not important here
|
|
559
|
-
yield NegativeValue(ctx.generate_from(strategy
|
|
584
|
+
yield NegativeValue(ctx.generate_from(strategy), description="Invalid enum value")
|
|
560
585
|
|
|
561
586
|
|
|
562
587
|
def _negative_properties(
|
|
@@ -571,9 +596,13 @@ def _negative_properties(
|
|
|
571
596
|
)
|
|
572
597
|
|
|
573
598
|
|
|
599
|
+
def _not_matching_pattern(value: str, pattern: str) -> bool:
|
|
600
|
+
return re.search(pattern, value) is None
|
|
601
|
+
|
|
602
|
+
|
|
574
603
|
def _negative_pattern(ctx: CoverageContext, pattern: str) -> Generator[GeneratedValue, None, None]:
|
|
575
604
|
yield NegativeValue(
|
|
576
|
-
ctx.generate_from(st.text().filter(
|
|
605
|
+
ctx.generate_from(st.text().filter(partial(_not_matching_pattern, pattern=pattern))),
|
|
577
606
|
description=f"Value not matching the '{pattern}' pattern",
|
|
578
607
|
)
|
|
579
608
|
|
|
@@ -606,19 +635,31 @@ def _negative_required(
|
|
|
606
635
|
)
|
|
607
636
|
|
|
608
637
|
|
|
638
|
+
def _is_invalid_hostname(v: Any) -> bool:
|
|
639
|
+
return v == "" or not jsonschema.Draft202012Validator.FORMAT_CHECKER.conforms(v, "hostname")
|
|
640
|
+
|
|
641
|
+
|
|
642
|
+
def _is_invalid_format(v: Any, format: str) -> bool:
|
|
643
|
+
return not jsonschema.Draft202012Validator.FORMAT_CHECKER.conforms(v, format)
|
|
644
|
+
|
|
645
|
+
|
|
609
646
|
def _negative_format(ctx: CoverageContext, schema: dict, format: str) -> Generator[GeneratedValue, None, None]:
|
|
610
647
|
# Hypothesis-jsonschema does not canonicalise it properly right now, which leads to unsatisfiable schema
|
|
611
648
|
without_format = {k: v for k, v in schema.items() if k != "format"}
|
|
612
649
|
without_format.setdefault("type", "string")
|
|
613
650
|
strategy = from_schema(without_format)
|
|
614
651
|
if format in jsonschema.Draft202012Validator.FORMAT_CHECKER.checkers:
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
652
|
+
if format == "hostname":
|
|
653
|
+
strategy = strategy.filter(_is_invalid_hostname)
|
|
654
|
+
else:
|
|
655
|
+
strategy = strategy.filter(functools.partial(_is_invalid_format, format=format))
|
|
619
656
|
yield NegativeValue(ctx.generate_from(strategy), description=f"Value not matching the '{format}' format")
|
|
620
657
|
|
|
621
658
|
|
|
659
|
+
def _is_non_integer_float(x: float) -> bool:
|
|
660
|
+
return x != int(x)
|
|
661
|
+
|
|
662
|
+
|
|
622
663
|
def _negative_type(ctx: CoverageContext, seen: set, ty: str | list[str]) -> Generator[GeneratedValue, None, None]:
|
|
623
664
|
strategies = {
|
|
624
665
|
"integer": st.integers(),
|
|
@@ -638,9 +679,9 @@ def _negative_type(ctx: CoverageContext, seen: set, ty: str | list[str]) -> Gene
|
|
|
638
679
|
if "number" in types:
|
|
639
680
|
del strategies["integer"]
|
|
640
681
|
if "integer" in types:
|
|
641
|
-
strategies["number"] = FLOAT_STRATEGY.filter(
|
|
682
|
+
strategies["number"] = FLOAT_STRATEGY.filter(_is_non_integer_float)
|
|
642
683
|
for strat in strategies.values():
|
|
643
|
-
value = ctx.generate_from(strat
|
|
684
|
+
value = ctx.generate_from(strat)
|
|
644
685
|
hashed = _to_hashable_key(value)
|
|
645
686
|
if hashed in seen:
|
|
646
687
|
continue
|
schemathesis/hooks.py
CHANGED
|
@@ -285,9 +285,9 @@ class HookDispatcher:
|
|
|
285
285
|
self._hooks = defaultdict(list)
|
|
286
286
|
|
|
287
287
|
|
|
288
|
-
def _should_skip_hook(hook: Callable,
|
|
288
|
+
def _should_skip_hook(hook: Callable, ctx: HookContext) -> bool:
|
|
289
289
|
filter_set = getattr(hook, "filter_set", None)
|
|
290
|
-
return filter_set is not None and not filter_set.match(
|
|
290
|
+
return filter_set is not None and ctx.operation is not None and not filter_set.match(ctx)
|
|
291
291
|
|
|
292
292
|
|
|
293
293
|
def apply_to_all_dispatchers(
|
schemathesis/internal/checks.py
CHANGED
|
@@ -2,10 +2,11 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import inspect
|
|
4
4
|
import warnings
|
|
5
|
-
from dataclasses import dataclass
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
6
|
from typing import TYPE_CHECKING, Callable, Optional
|
|
7
7
|
|
|
8
8
|
if TYPE_CHECKING:
|
|
9
|
+
from requests.auth import HTTPDigestAuth
|
|
9
10
|
from requests.structures import CaseInsensitiveDict
|
|
10
11
|
|
|
11
12
|
from ..models import Case
|
|
@@ -16,6 +17,23 @@ if TYPE_CHECKING:
|
|
|
16
17
|
CheckFunction = Callable[["CheckContext", "GenericResponse", "Case"], Optional[bool]]
|
|
17
18
|
|
|
18
19
|
|
|
20
|
+
@dataclass
|
|
21
|
+
class NegativeDataRejectionConfig:
|
|
22
|
+
# 5xx will pass through
|
|
23
|
+
allowed_statuses: list[str] = field(default_factory=lambda: ["4xx", "5xx"])
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class PositiveDataAcceptanceConfig:
|
|
28
|
+
allowed_statuses: list[str] = field(default_factory=lambda: ["2xx", "401", "403", "404"])
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class CheckConfig:
|
|
33
|
+
negative_data_rejection: NegativeDataRejectionConfig = field(default_factory=NegativeDataRejectionConfig)
|
|
34
|
+
positive_data_acceptance: PositiveDataAcceptanceConfig = field(default_factory=PositiveDataAcceptanceConfig)
|
|
35
|
+
|
|
36
|
+
|
|
19
37
|
@dataclass
|
|
20
38
|
class CheckContext:
|
|
21
39
|
"""Context for Schemathesis checks.
|
|
@@ -23,8 +41,9 @@ class CheckContext:
|
|
|
23
41
|
Provides access to broader test execution data beyond individual test cases.
|
|
24
42
|
"""
|
|
25
43
|
|
|
26
|
-
auth: RawAuth | None = None
|
|
44
|
+
auth: HTTPDigestAuth | RawAuth | None = None
|
|
27
45
|
headers: CaseInsensitiveDict | None = None
|
|
46
|
+
config: CheckConfig = field(default_factory=CheckConfig)
|
|
28
47
|
|
|
29
48
|
|
|
30
49
|
def wrap_check(check: Callable) -> CheckFunction:
|
schemathesis/lazy.py
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
from contextlib import nullcontext
|
|
3
4
|
from dataclasses import dataclass, field
|
|
4
5
|
from inspect import signature
|
|
5
|
-
from typing import TYPE_CHECKING, Any, Callable, Generator
|
|
6
|
+
from typing import TYPE_CHECKING, Any, Callable, Generator, Type
|
|
6
7
|
|
|
7
8
|
import pytest
|
|
8
9
|
from hypothesis.core import HypothesisHandle
|
|
9
10
|
from hypothesis.errors import Flaky
|
|
10
11
|
from hypothesis.internal.escalation import format_exception, get_trimmed_traceback
|
|
11
12
|
from hypothesis.internal.reflection import impersonate
|
|
12
|
-
from pytest_subtests import SubTests
|
|
13
|
+
from pytest_subtests import SubTests
|
|
13
14
|
|
|
14
15
|
from ._compat import MultipleFailures, get_interesting_origin
|
|
15
16
|
from ._override import CaseOverride, check_no_override_mark, get_override_from_mark, set_override_mark
|
|
@@ -310,7 +311,7 @@ def _copy_marks(source: Callable, target: Callable) -> None:
|
|
|
310
311
|
target.pytestmark.extend(marks) # type: ignore
|
|
311
312
|
|
|
312
313
|
|
|
313
|
-
def _get_capturemanager(request: FixtureRequest) -> Generator:
|
|
314
|
+
def _get_capturemanager(request: FixtureRequest) -> Generator | Type[nullcontext]:
|
|
314
315
|
capturemanager = request.node.config.pluginmanager.get_plugin("capturemanager")
|
|
315
316
|
if capturemanager is not None:
|
|
316
317
|
return capturemanager.global_and_fixture_disabled
|
schemathesis/runner/__init__.py
CHANGED
|
@@ -11,6 +11,7 @@ from ..constants import (
|
|
|
11
11
|
)
|
|
12
12
|
from ..exceptions import SchemaError
|
|
13
13
|
from ..generation import DEFAULT_DATA_GENERATION_METHODS, DataGenerationMethod, GenerationConfig
|
|
14
|
+
from ..internal.checks import CheckConfig
|
|
14
15
|
from ..internal.datetime import current_datetime
|
|
15
16
|
from ..internal.deprecation import deprecated_function
|
|
16
17
|
from ..internal.validation import file_exists
|
|
@@ -355,6 +356,7 @@ def from_schema(
|
|
|
355
356
|
count_operations: bool = True,
|
|
356
357
|
count_links: bool = True,
|
|
357
358
|
probe_config: ProbeConfig | None = None,
|
|
359
|
+
checks_config: CheckConfig | None = None,
|
|
358
360
|
service_client: ServiceClient | None = None,
|
|
359
361
|
) -> BaseRunner:
|
|
360
362
|
import hypothesis
|
|
@@ -371,6 +373,7 @@ def from_schema(
|
|
|
371
373
|
)
|
|
372
374
|
|
|
373
375
|
checks = checks or DEFAULT_CHECKS
|
|
376
|
+
checks_config = checks_config or CheckConfig()
|
|
374
377
|
probe_config = probe_config or ProbeConfig()
|
|
375
378
|
|
|
376
379
|
hypothesis_settings = hypothesis_settings or hypothesis.settings(deadline=DEFAULT_DEADLINE)
|
|
@@ -413,6 +416,7 @@ def from_schema(
|
|
|
413
416
|
count_operations=count_operations,
|
|
414
417
|
count_links=count_links,
|
|
415
418
|
probe_config=probe_config,
|
|
419
|
+
checks_config=checks_config,
|
|
416
420
|
service_client=service_client,
|
|
417
421
|
)
|
|
418
422
|
if is_asgi_app(schema.app):
|
|
@@ -439,6 +443,7 @@ def from_schema(
|
|
|
439
443
|
count_operations=count_operations,
|
|
440
444
|
count_links=count_links,
|
|
441
445
|
probe_config=probe_config,
|
|
446
|
+
checks_config=checks_config,
|
|
442
447
|
service_client=service_client,
|
|
443
448
|
)
|
|
444
449
|
return ThreadPoolWSGIRunner(
|
|
@@ -465,6 +470,7 @@ def from_schema(
|
|
|
465
470
|
count_operations=count_operations,
|
|
466
471
|
count_links=count_links,
|
|
467
472
|
probe_config=probe_config,
|
|
473
|
+
checks_config=checks_config,
|
|
468
474
|
service_client=service_client,
|
|
469
475
|
)
|
|
470
476
|
if not schema.app:
|
|
@@ -492,6 +498,7 @@ def from_schema(
|
|
|
492
498
|
count_operations=count_operations,
|
|
493
499
|
count_links=count_links,
|
|
494
500
|
probe_config=probe_config,
|
|
501
|
+
checks_config=checks_config,
|
|
495
502
|
service_client=service_client,
|
|
496
503
|
)
|
|
497
504
|
if is_asgi_app(schema.app):
|
|
@@ -518,6 +525,7 @@ def from_schema(
|
|
|
518
525
|
count_operations=count_operations,
|
|
519
526
|
count_links=count_links,
|
|
520
527
|
probe_config=probe_config,
|
|
528
|
+
checks_config=checks_config,
|
|
521
529
|
service_client=service_client,
|
|
522
530
|
)
|
|
523
531
|
return SingleThreadWSGIRunner(
|
|
@@ -543,6 +551,7 @@ def from_schema(
|
|
|
543
551
|
count_operations=count_operations,
|
|
544
552
|
count_links=count_links,
|
|
545
553
|
probe_config=probe_config,
|
|
554
|
+
checks_config=checks_config,
|
|
546
555
|
service_client=service_client,
|
|
547
556
|
)
|
|
548
557
|
|
|
@@ -4,6 +4,7 @@ from dataclasses import dataclass
|
|
|
4
4
|
from typing import TYPE_CHECKING
|
|
5
5
|
|
|
6
6
|
from ...constants import NOT_SET
|
|
7
|
+
from ...internal.checks import CheckConfig
|
|
7
8
|
from ...models import TestResult, TestResultSet
|
|
8
9
|
|
|
9
10
|
if TYPE_CHECKING:
|
|
@@ -24,11 +25,18 @@ class RunnerContext:
|
|
|
24
25
|
stop_event: threading.Event
|
|
25
26
|
unique_data: bool
|
|
26
27
|
outcome_cache: dict[int, BaseException | None]
|
|
28
|
+
checks_config: CheckConfig
|
|
27
29
|
|
|
28
|
-
__slots__ = ("data", "auth", "seed", "stop_event", "unique_data", "outcome_cache")
|
|
30
|
+
__slots__ = ("data", "auth", "seed", "stop_event", "unique_data", "outcome_cache", "checks_config")
|
|
29
31
|
|
|
30
32
|
def __init__(
|
|
31
|
-
self,
|
|
33
|
+
self,
|
|
34
|
+
*,
|
|
35
|
+
seed: int | None,
|
|
36
|
+
auth: RawAuth | None,
|
|
37
|
+
stop_event: threading.Event,
|
|
38
|
+
unique_data: bool,
|
|
39
|
+
checks_config: CheckConfig,
|
|
32
40
|
) -> None:
|
|
33
41
|
self.data = TestResultSet(seed=seed)
|
|
34
42
|
self.auth = auth
|
|
@@ -36,6 +44,7 @@ class RunnerContext:
|
|
|
36
44
|
self.stop_event = stop_event
|
|
37
45
|
self.outcome_cache = {}
|
|
38
46
|
self.unique_data = unique_data
|
|
47
|
+
self.checks_config = checks_config
|
|
39
48
|
|
|
40
49
|
@property
|
|
41
50
|
def is_stopped(self) -> bool:
|
schemathesis/runner/impl/core.py
CHANGED
|
@@ -57,7 +57,7 @@ from ...exceptions import (
|
|
|
57
57
|
)
|
|
58
58
|
from ...generation import DataGenerationMethod, GenerationConfig
|
|
59
59
|
from ...hooks import HookContext, get_all_by_name
|
|
60
|
-
from ...internal.checks import CheckContext
|
|
60
|
+
from ...internal.checks import CheckConfig, CheckContext
|
|
61
61
|
from ...internal.datetime import current_datetime
|
|
62
62
|
from ...internal.result import Err, Ok, Result
|
|
63
63
|
from ...models import APIOperation, Case, Check, Status, TestResult
|
|
@@ -102,6 +102,7 @@ class BaseRunner:
|
|
|
102
102
|
hypothesis_settings: hypothesis.settings
|
|
103
103
|
generation_config: GenerationConfig | None
|
|
104
104
|
probe_config: probes.ProbeConfig
|
|
105
|
+
checks_config: CheckConfig
|
|
105
106
|
request_config: RequestConfig = field(default_factory=RequestConfig)
|
|
106
107
|
override: CaseOverride | None = None
|
|
107
108
|
auth: RawAuth | None = None
|
|
@@ -131,7 +132,13 @@ class BaseRunner:
|
|
|
131
132
|
# If auth is explicitly provided, then the global provider is ignored
|
|
132
133
|
if self.auth is not None:
|
|
133
134
|
unregister_auth()
|
|
134
|
-
ctx = RunnerContext(
|
|
135
|
+
ctx = RunnerContext(
|
|
136
|
+
auth=self.auth,
|
|
137
|
+
seed=self.seed,
|
|
138
|
+
stop_event=stop_event,
|
|
139
|
+
unique_data=self.unique_data,
|
|
140
|
+
checks_config=self.checks_config,
|
|
141
|
+
)
|
|
135
142
|
start_time = time.monotonic()
|
|
136
143
|
initialized = None
|
|
137
144
|
__probes = None
|
|
@@ -1017,7 +1024,9 @@ def _network_test(
|
|
|
1017
1024
|
run_targets(targets, context)
|
|
1018
1025
|
status = Status.success
|
|
1019
1026
|
|
|
1020
|
-
check_ctx = CheckContext(
|
|
1027
|
+
check_ctx = CheckContext(
|
|
1028
|
+
auth=ctx.auth, headers=CaseInsensitiveDict(headers) if headers else None, config=ctx.checks_config
|
|
1029
|
+
)
|
|
1021
1030
|
try:
|
|
1022
1031
|
run_checks(
|
|
1023
1032
|
case=case,
|
|
@@ -1109,7 +1118,9 @@ def _wsgi_test(
|
|
|
1109
1118
|
result.logs.extend(recorded.records)
|
|
1110
1119
|
status = Status.success
|
|
1111
1120
|
check_results: list[Check] = []
|
|
1112
|
-
check_ctx = CheckContext(
|
|
1121
|
+
check_ctx = CheckContext(
|
|
1122
|
+
auth=ctx.auth, headers=CaseInsensitiveDict(headers) if headers else None, config=ctx.checks_config
|
|
1123
|
+
)
|
|
1113
1124
|
try:
|
|
1114
1125
|
run_checks(
|
|
1115
1126
|
case=case,
|
|
@@ -1189,7 +1200,9 @@ def _asgi_test(
|
|
|
1189
1200
|
run_targets(targets, context)
|
|
1190
1201
|
status = Status.success
|
|
1191
1202
|
check_results: list[Check] = []
|
|
1192
|
-
check_ctx = CheckContext(
|
|
1203
|
+
check_ctx = CheckContext(
|
|
1204
|
+
auth=ctx.auth, headers=CaseInsensitiveDict(headers) if headers else None, config=ctx.checks_config
|
|
1205
|
+
)
|
|
1193
1206
|
try:
|
|
1194
1207
|
run_checks(
|
|
1195
1208
|
case=case,
|
|
@@ -14,6 +14,7 @@ from ...exceptions import (
|
|
|
14
14
|
get_malformed_media_type_error,
|
|
15
15
|
get_missing_content_type_error,
|
|
16
16
|
get_negative_rejection_error,
|
|
17
|
+
get_positive_acceptance_error,
|
|
17
18
|
get_response_type_error,
|
|
18
19
|
get_schema_validation_error,
|
|
19
20
|
get_status_code_error,
|
|
@@ -21,7 +22,7 @@ from ...exceptions import (
|
|
|
21
22
|
)
|
|
22
23
|
from ...internal.transformation import convert_boolean_string
|
|
23
24
|
from ...transports.content_types import parse_content_type
|
|
24
|
-
from .utils import expand_status_code
|
|
25
|
+
from .utils import expand_status_code, expand_status_codes
|
|
25
26
|
|
|
26
27
|
if TYPE_CHECKING:
|
|
27
28
|
from requests import PreparedRequest
|
|
@@ -218,16 +219,52 @@ def negative_data_rejection(ctx: CheckContext, response: GenericResponse, case:
|
|
|
218
219
|
|
|
219
220
|
if not isinstance(case.operation.schema, BaseOpenAPISchema):
|
|
220
221
|
return True
|
|
222
|
+
|
|
223
|
+
config = ctx.config.negative_data_rejection
|
|
224
|
+
allowed_statuses = expand_status_codes(config.allowed_statuses or [])
|
|
225
|
+
|
|
221
226
|
if (
|
|
222
227
|
case.data_generation_method
|
|
223
228
|
and case.data_generation_method.is_negative
|
|
224
|
-
and
|
|
229
|
+
and response.status_code not in allowed_statuses
|
|
225
230
|
and not has_only_additional_properties_in_non_body_parameters(case)
|
|
226
231
|
):
|
|
232
|
+
message = f"Allowed statuses: {', '.join(config.allowed_statuses)}"
|
|
227
233
|
exc_class = get_negative_rejection_error(case.operation.verbose_name, response.status_code)
|
|
228
234
|
raise exc_class(
|
|
229
235
|
failures.AcceptedNegativeData.title,
|
|
230
|
-
context=failures.AcceptedNegativeData(
|
|
236
|
+
context=failures.AcceptedNegativeData(
|
|
237
|
+
message=message,
|
|
238
|
+
status_code=response.status_code,
|
|
239
|
+
allowed_statuses=config.allowed_statuses,
|
|
240
|
+
),
|
|
241
|
+
)
|
|
242
|
+
return None
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def positive_data_acceptance(ctx: CheckContext, response: GenericResponse, case: Case) -> bool | None:
|
|
246
|
+
from .schemas import BaseOpenAPISchema
|
|
247
|
+
|
|
248
|
+
if not isinstance(case.operation.schema, BaseOpenAPISchema):
|
|
249
|
+
return True
|
|
250
|
+
|
|
251
|
+
config = ctx.config.positive_data_acceptance
|
|
252
|
+
allowed_statuses = expand_status_codes(config.allowed_statuses or [])
|
|
253
|
+
|
|
254
|
+
if (
|
|
255
|
+
case.data_generation_method
|
|
256
|
+
and case.data_generation_method.is_positive
|
|
257
|
+
and response.status_code not in allowed_statuses
|
|
258
|
+
):
|
|
259
|
+
message = f"Allowed statuses: {', '.join(config.allowed_statuses)}"
|
|
260
|
+
exc_class = get_positive_acceptance_error(case.operation.verbose_name, response.status_code)
|
|
261
|
+
raise exc_class(
|
|
262
|
+
failures.RejectedPositiveData.title,
|
|
263
|
+
context=failures.RejectedPositiveData(
|
|
264
|
+
message=message,
|
|
265
|
+
status_code=response.status_code,
|
|
266
|
+
allowed_statuses=config.allowed_statuses,
|
|
267
|
+
),
|
|
231
268
|
)
|
|
232
269
|
return None
|
|
233
270
|
|
|
@@ -1330,6 +1330,8 @@ OPENAPI_30 = {
|
|
|
1330
1330
|
},
|
|
1331
1331
|
},
|
|
1332
1332
|
}
|
|
1333
|
+
# Generated from the updated schema.yaml from 0035208, which includes unpublished bugfixes
|
|
1334
|
+
# https://github.com/OAI/OpenAPI-Specification/blob/0035208611701b4f7f2c959eb99a8725cca41e6e/schemas/v3.1/schema.yaml
|
|
1333
1335
|
OPENAPI_31 = {
|
|
1334
1336
|
"$id": "https://spec.openapis.org/oas/3.1/schema/2022-10-07",
|
|
1335
1337
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
@@ -1345,7 +1347,7 @@ OPENAPI_31 = {
|
|
|
1345
1347
|
},
|
|
1346
1348
|
"servers": {"type": "array", "items": {"$ref": "#/$defs/server"}, "default": [{"url": "/"}]},
|
|
1347
1349
|
"paths": {"$ref": "#/$defs/paths"},
|
|
1348
|
-
"webhooks": {"type": "object", "additionalProperties": {"$ref": "#/$defs/path-item
|
|
1350
|
+
"webhooks": {"type": "object", "additionalProperties": {"$ref": "#/$defs/path-item"}},
|
|
1349
1351
|
"components": {"$ref": "#/$defs/components"},
|
|
1350
1352
|
"security": {"type": "array", "items": {"$ref": "#/$defs/security-requirement"}},
|
|
1351
1353
|
"tags": {"type": "array", "items": {"$ref": "#/$defs/tag"}},
|
|
@@ -1400,7 +1402,7 @@ OPENAPI_31 = {
|
|
|
1400
1402
|
"$comment": "https://spec.openapis.org/oas/v3.1.0#server-object",
|
|
1401
1403
|
"type": "object",
|
|
1402
1404
|
"properties": {
|
|
1403
|
-
"url": {"type": "string"
|
|
1405
|
+
"url": {"type": "string"},
|
|
1404
1406
|
"description": {"type": "string"},
|
|
1405
1407
|
"variables": {"type": "object", "additionalProperties": {"$ref": "#/$defs/server-variable"}},
|
|
1406
1408
|
},
|
|
@@ -1439,7 +1441,7 @@ OPENAPI_31 = {
|
|
|
1439
1441
|
},
|
|
1440
1442
|
"links": {"type": "object", "additionalProperties": {"$ref": "#/$defs/link-or-reference"}},
|
|
1441
1443
|
"callbacks": {"type": "object", "additionalProperties": {"$ref": "#/$defs/callbacks-or-reference"}},
|
|
1442
|
-
"pathItems": {"type": "object", "additionalProperties": {"$ref": "#/$defs/path-item
|
|
1444
|
+
"pathItems": {"type": "object", "additionalProperties": {"$ref": "#/$defs/path-item"}},
|
|
1443
1445
|
},
|
|
1444
1446
|
"patternProperties": {
|
|
1445
1447
|
"^(schemas|responses|parameters|examples|requestBodies|headers|securitySchemes|links|callbacks|pathItems)$": {
|
|
@@ -1461,6 +1463,7 @@ OPENAPI_31 = {
|
|
|
1461
1463
|
"$comment": "https://spec.openapis.org/oas/v3.1.0#path-item-object",
|
|
1462
1464
|
"type": "object",
|
|
1463
1465
|
"properties": {
|
|
1466
|
+
"$ref": {"type": "string", "format": "uri-reference"},
|
|
1464
1467
|
"summary": {"type": "string"},
|
|
1465
1468
|
"description": {"type": "string"},
|
|
1466
1469
|
"servers": {"type": "array", "items": {"$ref": "#/$defs/server"}},
|
|
@@ -1477,11 +1480,6 @@ OPENAPI_31 = {
|
|
|
1477
1480
|
"$ref": "#/$defs/specification-extensions",
|
|
1478
1481
|
"unevaluatedProperties": False,
|
|
1479
1482
|
},
|
|
1480
|
-
"path-item-or-reference": {
|
|
1481
|
-
"if": {"type": "object", "required": ["$ref"]},
|
|
1482
|
-
"then": {"$ref": "#/$defs/reference"},
|
|
1483
|
-
"else": {"$ref": "#/$defs/path-item"},
|
|
1484
|
-
},
|
|
1485
1483
|
"operation": {
|
|
1486
1484
|
"$comment": "https://spec.openapis.org/oas/v3.1.0#operation-object",
|
|
1487
1485
|
"type": "object",
|
|
@@ -1542,7 +1540,6 @@ OPENAPI_31 = {
|
|
|
1542
1540
|
"if": {"properties": {"in": {"const": "path"}}, "required": ["in"]},
|
|
1543
1541
|
"then": {
|
|
1544
1542
|
"properties": {
|
|
1545
|
-
"name": {"pattern": "[^/#?]+$"},
|
|
1546
1543
|
"style": {"default": "simple", "enum": ["matrix", "label", "simple"]},
|
|
1547
1544
|
"required": {"const": True},
|
|
1548
1545
|
},
|
|
@@ -1662,7 +1659,7 @@ OPENAPI_31 = {
|
|
|
1662
1659
|
"$comment": "https://spec.openapis.org/oas/v3.1.0#callback-object",
|
|
1663
1660
|
"type": "object",
|
|
1664
1661
|
"$ref": "#/$defs/specification-extensions",
|
|
1665
|
-
"additionalProperties": {"$ref": "#/$defs/path-item
|
|
1662
|
+
"additionalProperties": {"$ref": "#/$defs/path-item"},
|
|
1666
1663
|
},
|
|
1667
1664
|
"callbacks-or-reference": {
|
|
1668
1665
|
"if": {"type": "object", "required": ["$ref"]},
|
|
@@ -1755,7 +1752,6 @@ OPENAPI_31 = {
|
|
|
1755
1752
|
"summary": {"type": "string"},
|
|
1756
1753
|
"description": {"type": "string"},
|
|
1757
1754
|
},
|
|
1758
|
-
"unevaluatedProperties": False,
|
|
1759
1755
|
},
|
|
1760
1756
|
"schema": {
|
|
1761
1757
|
"$comment": "https://spec.openapis.org/oas/v3.1.0#schema-object",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import string
|
|
4
|
-
from itertools import product
|
|
4
|
+
from itertools import chain, product
|
|
5
5
|
from typing import Any, Generator
|
|
6
6
|
|
|
7
7
|
|
|
@@ -11,6 +11,10 @@ def expand_status_code(status_code: str | int) -> Generator[int, None, None]:
|
|
|
11
11
|
yield int("".join(expanded))
|
|
12
12
|
|
|
13
13
|
|
|
14
|
+
def expand_status_codes(status_codes: list[str]) -> set[int]:
|
|
15
|
+
return set(chain.from_iterable(expand_status_code(code) for code in status_codes))
|
|
16
|
+
|
|
17
|
+
|
|
14
18
|
def is_header_location(location: str) -> bool:
|
|
15
19
|
"""Whether this location affects HTTP headers."""
|
|
16
20
|
return location in ("header", "cookie")
|
schemathesis/stateful/runner.py
CHANGED
|
@@ -12,6 +12,7 @@ from hypothesis.control import current_build_context
|
|
|
12
12
|
from hypothesis.errors import Flaky, Unsatisfiable
|
|
13
13
|
|
|
14
14
|
from ..exceptions import CheckFailed
|
|
15
|
+
from ..internal.checks import CheckContext
|
|
15
16
|
from ..targets import TargetMetricCollector
|
|
16
17
|
from . import events
|
|
17
18
|
from .config import StatefulTestRunnerConfig
|
|
@@ -113,6 +114,7 @@ def _execute_state_machine_loop(
|
|
|
113
114
|
) -> None:
|
|
114
115
|
"""Execute the state machine testing loop."""
|
|
115
116
|
from hypothesis import reporting
|
|
117
|
+
from requests.structures import CaseInsensitiveDict
|
|
116
118
|
|
|
117
119
|
from ..transports import RequestsTransport
|
|
118
120
|
|
|
@@ -129,6 +131,7 @@ def _execute_state_machine_loop(
|
|
|
129
131
|
if config.auth is not None:
|
|
130
132
|
session.auth = config.auth
|
|
131
133
|
call_kwargs["session"] = session
|
|
134
|
+
check_ctx = CheckContext(auth=config.auth, headers=CaseInsensitiveDict(config.headers) if config.headers else None)
|
|
132
135
|
|
|
133
136
|
class _InstrumentedStateMachine(state_machine): # type: ignore[valid-type,misc]
|
|
134
137
|
"""State machine with additional hooks for emitting events."""
|
|
@@ -223,7 +226,8 @@ def _execute_state_machine_loop(
|
|
|
223
226
|
validate_response(
|
|
224
227
|
response=response,
|
|
225
228
|
case=case,
|
|
226
|
-
|
|
229
|
+
runner_ctx=ctx,
|
|
230
|
+
check_ctx=check_ctx,
|
|
227
231
|
checks=config.checks,
|
|
228
232
|
additional_checks=additional_checks,
|
|
229
233
|
max_response_time=config.max_response_time,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from typing import TYPE_CHECKING
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
4
|
|
|
5
5
|
from ..exceptions import CheckFailed, get_grouped_exception
|
|
6
6
|
from ..internal.checks import CheckContext
|
|
@@ -17,27 +17,24 @@ def validate_response(
|
|
|
17
17
|
*,
|
|
18
18
|
response: GenericResponse,
|
|
19
19
|
case: Case,
|
|
20
|
-
|
|
20
|
+
runner_ctx: RunnerContext,
|
|
21
|
+
check_ctx: CheckContext,
|
|
21
22
|
checks: tuple[CheckFunction, ...],
|
|
22
23
|
additional_checks: tuple[CheckFunction, ...] = (),
|
|
23
24
|
max_response_time: int | None = None,
|
|
24
|
-
headers: dict[str, Any] | None = None,
|
|
25
25
|
) -> None:
|
|
26
26
|
"""Validate the response against the provided checks."""
|
|
27
|
-
from requests.structures import CaseInsensitiveDict
|
|
28
|
-
|
|
29
27
|
from .._compat import MultipleFailures
|
|
30
28
|
from ..checks import _make_max_response_time_failure_message
|
|
31
29
|
from ..failures import ResponseTimeExceeded
|
|
32
30
|
from ..models import Check, Status
|
|
33
31
|
|
|
34
32
|
exceptions: list[CheckFailed | AssertionError] = []
|
|
35
|
-
check_results =
|
|
36
|
-
check_ctx = CheckContext(headers=CaseInsensitiveDict(headers) if headers else None)
|
|
33
|
+
check_results = runner_ctx.checks_for_step
|
|
37
34
|
|
|
38
35
|
def _on_failure(exc: CheckFailed | AssertionError, message: str, context: FailureContext | None) -> None:
|
|
39
36
|
exceptions.append(exc)
|
|
40
|
-
if
|
|
37
|
+
if runner_ctx.is_seen_in_suite(exc):
|
|
41
38
|
return
|
|
42
39
|
failed_check = Check(
|
|
43
40
|
name=name,
|
|
@@ -49,9 +46,9 @@ def validate_response(
|
|
|
49
46
|
context=context,
|
|
50
47
|
request=None,
|
|
51
48
|
)
|
|
52
|
-
|
|
49
|
+
runner_ctx.add_failed_check(failed_check)
|
|
53
50
|
check_results.append(failed_check)
|
|
54
|
-
|
|
51
|
+
runner_ctx.mark_as_seen_in_suite(exc)
|
|
55
52
|
|
|
56
53
|
def _on_passed(_name: str, _case: Case) -> None:
|
|
57
54
|
passed_check = Check(
|
|
@@ -72,16 +69,16 @@ def validate_response(
|
|
|
72
69
|
if not skip_check:
|
|
73
70
|
_on_passed(name, copied_case)
|
|
74
71
|
except CheckFailed as exc:
|
|
75
|
-
if
|
|
72
|
+
if runner_ctx.is_seen_in_run(exc):
|
|
76
73
|
continue
|
|
77
74
|
_on_failure(exc, str(exc), exc.context)
|
|
78
75
|
except AssertionError as exc:
|
|
79
|
-
if
|
|
76
|
+
if runner_ctx.is_seen_in_run(exc):
|
|
80
77
|
continue
|
|
81
78
|
_on_failure(exc, str(exc) or f"Custom check failed: `{name}`", None)
|
|
82
79
|
except MultipleFailures as exc:
|
|
83
80
|
for subexc in exc.exceptions:
|
|
84
|
-
if
|
|
81
|
+
if runner_ctx.is_seen_in_run(subexc):
|
|
85
82
|
continue
|
|
86
83
|
_on_failure(subexc, str(subexc), subexc.context)
|
|
87
84
|
|
|
@@ -93,7 +90,7 @@ def validate_response(
|
|
|
93
90
|
try:
|
|
94
91
|
raise AssertionError(message)
|
|
95
92
|
except AssertionError as _exc:
|
|
96
|
-
if not
|
|
93
|
+
if not runner_ctx.is_seen_in_run(_exc):
|
|
97
94
|
_on_failure(_exc, message, context)
|
|
98
95
|
else:
|
|
99
96
|
_on_passed("max_response_time", case)
|
|
@@ -7,7 +7,7 @@ from contextlib import contextmanager
|
|
|
7
7
|
from dataclasses import dataclass
|
|
8
8
|
from datetime import timedelta
|
|
9
9
|
from inspect import iscoroutinefunction
|
|
10
|
-
from typing import TYPE_CHECKING, Any, Generator,
|
|
10
|
+
from typing import TYPE_CHECKING, Any, Generator, Protocol, TypeVar, cast
|
|
11
11
|
from urllib.parse import urlparse
|
|
12
12
|
|
|
13
13
|
from .. import failures
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: schemathesis
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.37.0
|
|
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
|
|
@@ -40,7 +40,7 @@ Requires-Dist: hypothesis[zoneinfo]<7,>=6.103.4; python_version == '3.8'
|
|
|
40
40
|
Requires-Dist: jsonschema[format]<5.0,>=4.18.0
|
|
41
41
|
Requires-Dist: junit-xml<2.0,>=1.9
|
|
42
42
|
Requires-Dist: pyrate-limiter<4.0,>=2.10
|
|
43
|
-
Requires-Dist: pytest-subtests<0.
|
|
43
|
+
Requires-Dist: pytest-subtests<0.14.0,>=0.2.1
|
|
44
44
|
Requires-Dist: pytest<9,>=4.6.4
|
|
45
45
|
Requires-Dist: pyyaml<7.0,>=5.1
|
|
46
46
|
Requires-Dist: requests<3,>=2.22
|
|
@@ -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=Vk38ROENT5yOo38qS2s6JSjRCLDzTmnzQ1sy0ZoZ1C8,14930
|
|
5
5
|
schemathesis/_lazy_import.py,sha256=aMhWYgbU2JOltyWBb32vnWBb6kykOghucEzI_F70yVE,470
|
|
6
6
|
schemathesis/_override.py,sha256=TAjYB3eJQmlw9K_xiR9ptt9Wj7if4U7UFlUhGjpBAoM,1625
|
|
7
7
|
schemathesis/_rate_limiter.py,sha256=q_XWst5hzuAyXQRiZc4s_bx7-JlPYZM_yKDmeavt3oo,242
|
|
@@ -10,12 +10,12 @@ schemathesis/auths.py,sha256=De97IS_iOlC36-jRhkZ2DUndjUpXYgsd8R-nA-iHn88,16837
|
|
|
10
10
|
schemathesis/checks.py,sha256=YPUI1N5giGBy1072vd77e6HWelGAKrJUmJLEG4oqfF8,2630
|
|
11
11
|
schemathesis/code_samples.py,sha256=rsdTo6ksyUs3ZMhqx0mmmkPSKUCFa--snIOYsXgZd80,4120
|
|
12
12
|
schemathesis/constants.py,sha256=l1YQ7PXhEj9dyf9CTESVUpPOaFCH7iz-Fe8o4v6Th_s,2673
|
|
13
|
-
schemathesis/exceptions.py,sha256=
|
|
14
|
-
schemathesis/failures.py,sha256=
|
|
13
|
+
schemathesis/exceptions.py,sha256=5zjPlyVoQNJGbwufplL6ZVV7FEBPBNPHGdlQRJ7xnhE,20449
|
|
14
|
+
schemathesis/failures.py,sha256=fybNkCF2rqH90e3KW_XwpgZnSM6f7_FERcxHT9Pd3NM,7911
|
|
15
15
|
schemathesis/filters.py,sha256=f3c_yXIBwIin-9Y0qU2TkcC1NEM_Mw34jGUHQc0BOyw,17026
|
|
16
16
|
schemathesis/graphql.py,sha256=XiuKcfoOB92iLFC8zpz2msLkM0_V0TLdxPNBqrrGZ8w,216
|
|
17
|
-
schemathesis/hooks.py,sha256=
|
|
18
|
-
schemathesis/lazy.py,sha256=
|
|
17
|
+
schemathesis/hooks.py,sha256=ORbrF6xKx7CQJio97Yo9BSpgKFTQ0mTmSooToX_vXAY,14787
|
|
18
|
+
schemathesis/lazy.py,sha256=Ddhkk7Tpc_VcRGYkCtKDmP2gpjxVmEZ3b01ZTNjbm8I,19004
|
|
19
19
|
schemathesis/loaders.py,sha256=MoEhcdOEBJxNRn5X-ZNhWB9jZDHQQNpkNfEdQjf_NDw,4590
|
|
20
20
|
schemathesis/models.py,sha256=mgufFwK6pVWn6hjfJIJwbk0qZM-XUeUaVVaOOEPaR50,46646
|
|
21
21
|
schemathesis/parameters.py,sha256=PndmqQRlEYsCt1kWjSShPsFf6vj7X_7FRdz_-A95eNg,2258
|
|
@@ -27,16 +27,16 @@ schemathesis/targets.py,sha256=XIGRghvEzbmEJjse9aZgNEj67L3jAbiazm2rxURWgDE,2351
|
|
|
27
27
|
schemathesis/throttling.py,sha256=aisUc4MJDGIOGUAs9L2DlWWpdd4KyAFuNVKhYoaUC9M,1719
|
|
28
28
|
schemathesis/types.py,sha256=Tem2Q_zyMCd0Clp5iGKv6Fu13wdcbxVE8tCVH9WWt7A,1065
|
|
29
29
|
schemathesis/utils.py,sha256=8RkTZ9Ft5IUaGkxABhh34oU7WO2ouMsfgtvFPTx9alI,4875
|
|
30
|
-
schemathesis/cli/__init__.py,sha256=
|
|
30
|
+
schemathesis/cli/__init__.py,sha256=qtoLaYmgkxZ_o4JYe-KTYcVHxR8Tus-vDexRYAbTbXA,74566
|
|
31
31
|
schemathesis/cli/__main__.py,sha256=MWaenjaUTZIfNPFzKmnkTiawUri7DVldtg3mirLwzU8,92
|
|
32
|
-
schemathesis/cli/callbacks.py,sha256
|
|
33
|
-
schemathesis/cli/cassettes.py,sha256=
|
|
32
|
+
schemathesis/cli/callbacks.py,sha256=-VA_I_mVma9WxFNtUR8d2KNICKJD5ScayfSdKKPEP5Y,16321
|
|
33
|
+
schemathesis/cli/cassettes.py,sha256=jD1JTkkEALUUEyzyuJ-KuxgntfGodILUuOu3C9HKjIw,19412
|
|
34
34
|
schemathesis/cli/constants.py,sha256=wk-0GsoJIel8wFFerQ6Kf_6eAYUtIWkwMFwyAqv3yj4,1635
|
|
35
35
|
schemathesis/cli/context.py,sha256=j_lvYQiPa6Q7P4P_IGCM9V2y2gJSpDbpxIIzR5oFB2I,2567
|
|
36
36
|
schemathesis/cli/debug.py,sha256=_YA-bX1ujHl4bqQDEum7M-I2XHBTEGbvgkhvcvKhmgU,658
|
|
37
37
|
schemathesis/cli/handlers.py,sha256=EXSAFe5TQlHANz1AVlSttfsoDT2oeaeFbqq1N7e2udw,467
|
|
38
38
|
schemathesis/cli/junitxml.py,sha256=iWbD5G2mzxrLiC24OuGHCLwkgHQg80OXY00stnGaXL0,5054
|
|
39
|
-
schemathesis/cli/options.py,sha256=
|
|
39
|
+
schemathesis/cli/options.py,sha256=yL7nrzKkbGCc4nQya9wpTW48XGz_OT9hOFrzPxRrDe4,2853
|
|
40
40
|
schemathesis/cli/reporting.py,sha256=KC3sxSc1u4aFQ-0Q8CQ3G4HTEl7QxlubGnJgNKmVJdQ,3627
|
|
41
41
|
schemathesis/cli/sanitization.py,sha256=Onw_NWZSom6XTVNJ5NHnC0PAhrYAcGzIXJbsBCzLkn4,1005
|
|
42
42
|
schemathesis/cli/output/__init__.py,sha256=AXaUzQ1nhQ-vXhW4-X-91vE2VQtEcCOrGtQXXNN55iQ,29
|
|
@@ -48,7 +48,7 @@ schemathesis/contrib/openapi/__init__.py,sha256=N-BoCzrLGq9aynubhmQBS-LJUBv1wPJc
|
|
|
48
48
|
schemathesis/contrib/openapi/fill_missing_examples.py,sha256=SL3LXG4khjGKneU3aBu1MGIhYtwRMjK77NH8L--9JBE,583
|
|
49
49
|
schemathesis/contrib/openapi/formats/__init__.py,sha256=OpHWPW8MkTLVv83QXPYY1HVLflhmSH49hSVefm1oVV0,111
|
|
50
50
|
schemathesis/contrib/openapi/formats/uuid.py,sha256=PG7aV0QAQnQ1zKmKiDK3cJue3Xy-TLGzyMeCB_RQbgk,391
|
|
51
|
-
schemathesis/experimental/__init__.py,sha256=
|
|
51
|
+
schemathesis/experimental/__init__.py,sha256=0YVz037-Koc1OZ9Y5oYujc4if0gE9SdtfFlWnfzrWNo,3498
|
|
52
52
|
schemathesis/extra/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
53
53
|
schemathesis/extra/_aiohttp.py,sha256=-bIY0ucv7pfK3gA9PHiO4n7ajtZJJM9pS3EY3cLau9c,957
|
|
54
54
|
schemathesis/extra/_flask.py,sha256=lpKQxg_kdEFo59Q8sV5T78IJYhgx6p4SCwIohJYSycw,345
|
|
@@ -58,11 +58,11 @@ schemathesis/fixups/__init__.py,sha256=RP5QYJVJhp8LXjhH89fCRaIVU26dHCy74jD9seoYM
|
|
|
58
58
|
schemathesis/fixups/fast_api.py,sha256=mn-KzBqnR8jl4W5fY-_ZySabMDMUnpzCIESMHnlvE1c,1304
|
|
59
59
|
schemathesis/fixups/utf8_bom.py,sha256=lWT9RNmJG8i-l5AXIpaCT3qCPUwRgzXPW3eoOjmZETA,745
|
|
60
60
|
schemathesis/generation/__init__.py,sha256=29Zys_tD6kfngaC4zHeC6TOBZQcmo7CWm7KDSYsHStQ,1581
|
|
61
|
-
schemathesis/generation/_hypothesis.py,sha256=
|
|
62
|
-
schemathesis/generation/_methods.py,sha256=
|
|
63
|
-
schemathesis/generation/coverage.py,sha256=
|
|
61
|
+
schemathesis/generation/_hypothesis.py,sha256=Aaol5w3TEOVZn8znrnnJCYfrll8eALW0dCRtz3k0Eis,1661
|
|
62
|
+
schemathesis/generation/_methods.py,sha256=r8oVlJ71_gXcnEhU-byw2E0R2RswQQFm8U7yGErSqbw,1204
|
|
63
|
+
schemathesis/generation/coverage.py,sha256=S8qxFHpIkHr87vZQQSmhttRvmlfRZIL445gj_p2GjoQ,29070
|
|
64
64
|
schemathesis/internal/__init__.py,sha256=93HcdG3LF0BbQKbCteOsFMa1w6nXl8yTmx87QLNJOik,161
|
|
65
|
-
schemathesis/internal/checks.py,sha256=
|
|
65
|
+
schemathesis/internal/checks.py,sha256=0NudOP1vpz76n3OjnN4OkNC2d5bNKAkZLFzTG2k4NBY,2352
|
|
66
66
|
schemathesis/internal/copy.py,sha256=DcL56z-d69kKR_5u8mlHvjSL1UTyUKNMAwexrwHFY1s,1031
|
|
67
67
|
schemathesis/internal/datetime.py,sha256=zPLBL0XXLNfP-KYel3H2m8pnsxjsA_4d-zTOhJg2EPQ,136
|
|
68
68
|
schemathesis/internal/deprecation.py,sha256=Ty5VBFBlufkITpP0WWTPIPbnB7biDi0kQgXVYWZp820,1273
|
|
@@ -72,13 +72,13 @@ schemathesis/internal/output.py,sha256=zMaG5knIuBieVH8CrcmPJgbmQukDs2xdekX0BrK7B
|
|
|
72
72
|
schemathesis/internal/result.py,sha256=d449YvyONjqjDs-A5DAPgtAI96iT753K8sU6_1HLo2Q,461
|
|
73
73
|
schemathesis/internal/transformation.py,sha256=M5LA4pFZC4nD_0iGfih1wLF3_q8xJas94Uuaymt-7Cw,690
|
|
74
74
|
schemathesis/internal/validation.py,sha256=G7i8jIMUpAeOnDsDF_eWYvRZe_yMprRswx0QAtMPyEw,966
|
|
75
|
-
schemathesis/runner/__init__.py,sha256=
|
|
75
|
+
schemathesis/runner/__init__.py,sha256=b96aoJQo9Kash0GNKI-uCiLMEKOI8cxKjKCKQlWxkUw,21925
|
|
76
76
|
schemathesis/runner/events.py,sha256=cRKKSDvHvKLBIyFBz-J0JtAKshbGGKco9eaMyLCgzsY,11734
|
|
77
77
|
schemathesis/runner/probes.py,sha256=no5AfO3kse25qvHevjeUfB0Q3C860V2AYzschUW3QMQ,5688
|
|
78
78
|
schemathesis/runner/serialization.py,sha256=jHpfm1PgPAmorNkF8_rkzIYoeA43jpbSKeh5Hm5nqF0,20495
|
|
79
79
|
schemathesis/runner/impl/__init__.py,sha256=1E2iME8uthYPBh9MjwVBCTFV-P3fi7AdphCCoBBspjs,199
|
|
80
|
-
schemathesis/runner/impl/context.py,sha256=
|
|
81
|
-
schemathesis/runner/impl/core.py,sha256=
|
|
80
|
+
schemathesis/runner/impl/context.py,sha256=xVxHnXerSoOl-I6IzrdA-bRhaYQDKS4TuShFULsa154,2716
|
|
81
|
+
schemathesis/runner/impl/core.py,sha256=qnALY8PCkU2mmjZlEY6Jboa9pZNLmyCakh0gkuKg43c,47466
|
|
82
82
|
schemathesis/runner/impl/solo.py,sha256=y5QSxgK8nBCEjZVD5BpFvYUXmB6tEjk6TwxAo__NejA,2911
|
|
83
83
|
schemathesis/runner/impl/threadpool.py,sha256=yNR5LYE8f3N_4t42OwSgy0_qdGgBPM7d11F9c9oEAAs,15075
|
|
84
84
|
schemathesis/service/__init__.py,sha256=cDVTCFD1G-vvhxZkJUwiToTAEQ-0ByIoqwXvJBCf_V8,472
|
|
@@ -105,10 +105,10 @@ schemathesis/specs/graphql/validation.py,sha256=uINIOt-2E7ZuQV2CxKzwez-7L9tDtqzM
|
|
|
105
105
|
schemathesis/specs/openapi/__init__.py,sha256=HDcx3bqpa6qWPpyMrxAbM3uTo0Lqpg-BUNZhDJSJKnw,279
|
|
106
106
|
schemathesis/specs/openapi/_cache.py,sha256=PAiAu4X_a2PQgD2lG5H3iisXdyg4SaHpU46bRZvfNkM,4320
|
|
107
107
|
schemathesis/specs/openapi/_hypothesis.py,sha256=Ym1d3GXlabOSbDk_AEkmkZGl9EMIDpumciLZtfYNdUI,24323
|
|
108
|
-
schemathesis/specs/openapi/checks.py,sha256
|
|
108
|
+
schemathesis/specs/openapi/checks.py,sha256=eRNSs8wGNOqe4Zxt1DsCUQBU9OgaVceu1uioeRTI1XU,23650
|
|
109
109
|
schemathesis/specs/openapi/constants.py,sha256=JqM_FHOenqS_MuUE9sxVQ8Hnw0DNM8cnKDwCwPLhID4,783
|
|
110
110
|
schemathesis/specs/openapi/converter.py,sha256=NkrzBNjtmVwQTeE73NOtwB_puvQTjxxqqrc7gD_yscc,3241
|
|
111
|
-
schemathesis/specs/openapi/definitions.py,sha256=
|
|
111
|
+
schemathesis/specs/openapi/definitions.py,sha256=WTkWwCgTc3OMxfKsqh6YDoGfZMTThSYrHGp8h0vLAK0,93935
|
|
112
112
|
schemathesis/specs/openapi/examples.py,sha256=FwhPWca7bpdHpUp_LRoK09DVgusojO3aXXhXYrK373I,20354
|
|
113
113
|
schemathesis/specs/openapi/formats.py,sha256=JmmkQWNAj5XreXb7Edgj4LADAf4m86YulR_Ec8evpJ4,1220
|
|
114
114
|
schemathesis/specs/openapi/links.py,sha256=a8JmWM9aZhrR5CfyIh6t2SkfonMLfYKOScXY2XlZYN0,17749
|
|
@@ -120,7 +120,7 @@ schemathesis/specs/openapi/references.py,sha256=euxM02kQGMHh4Ss1jWjOY_gyw_HazafK
|
|
|
120
120
|
schemathesis/specs/openapi/schemas.py,sha256=t3Gz2q-d9b8Oy-hDhz0rNfjYT3Nx-uOeLOmjO9hpRM0,53741
|
|
121
121
|
schemathesis/specs/openapi/security.py,sha256=Z-6pk2Ga1PTUtBe298KunjVHsNh5A-teegeso7zcPIE,7138
|
|
122
122
|
schemathesis/specs/openapi/serialization.py,sha256=5qGdFHZ3n80UlbSXrO_bkr4Al_7ci_Z3aSUjZczNDQY,11384
|
|
123
|
-
schemathesis/specs/openapi/utils.py,sha256
|
|
123
|
+
schemathesis/specs/openapi/utils.py,sha256=ER4vJkdFVDIE7aKyxyYatuuHVRNutytezgE52pqZNE8,900
|
|
124
124
|
schemathesis/specs/openapi/validation.py,sha256=Q9ThZlwU-mSz7ExDnIivnZGi1ivC5hlX2mIMRAM79kc,999
|
|
125
125
|
schemathesis/specs/openapi/expressions/__init__.py,sha256=hFpJrIWbPi55GcIVjNFRDDUL8xmDu3mdbdldoHBoFJ0,1729
|
|
126
126
|
schemathesis/specs/openapi/expressions/context.py,sha256=GsraXq4azVg4pn0vilc6P7zZjzUWvoO7VUqmshqvmeQ,350
|
|
@@ -140,19 +140,19 @@ schemathesis/stateful/__init__.py,sha256=HBg-h131EI8IipHQgufSaXe-CrFTKmffPVsoEFj
|
|
|
140
140
|
schemathesis/stateful/config.py,sha256=huYzqDoD6x20p_VNAR79NgxPwUFO8UXoc3_z4BEuHqU,3586
|
|
141
141
|
schemathesis/stateful/context.py,sha256=vJ9nxTTjI5wo7A6PBGCvVVO_7y-ELs3XERi9PxLzykA,5085
|
|
142
142
|
schemathesis/stateful/events.py,sha256=CyYvyQebOaeTn6UevaB7HXOrUhxCWbqXMfQ7pZK7fV8,6727
|
|
143
|
-
schemathesis/stateful/runner.py,sha256=
|
|
143
|
+
schemathesis/stateful/runner.py,sha256=e3vvRrx0NwN20RdC0cC4mZjcsYezwkhoDwE1cCVOAlw,12615
|
|
144
144
|
schemathesis/stateful/sink.py,sha256=bHYlgh-fMwg1Srxk_XGs0-WV34YccotwH9PGrxCK57A,2474
|
|
145
145
|
schemathesis/stateful/state_machine.py,sha256=PFztY82W5enuXjO6k4Mz8fbHmDJ7Z8OLYZRWtuBeyjg,12956
|
|
146
146
|
schemathesis/stateful/statistic.py,sha256=2-uU5xpT9CbMulKgJWLZN6MUpC0Fskf5yXTt4ef4NFA,542
|
|
147
|
-
schemathesis/stateful/validation.py,sha256=
|
|
148
|
-
schemathesis/transports/__init__.py,sha256=
|
|
147
|
+
schemathesis/stateful/validation.py,sha256=23qSZjC1_xRmtCX4OqsyG6pGxdlo6IZYid695ZpDQyU,3747
|
|
148
|
+
schemathesis/transports/__init__.py,sha256=ybH90TrGwODO5s94UMEX2P2HX-6Jb66X5UUOgKTbZz8,12882
|
|
149
149
|
schemathesis/transports/asgi.py,sha256=bwW9vMd1h89Jh7I4jHJVwSNUQzHvc7-JOD5u4hSHZd8,212
|
|
150
150
|
schemathesis/transports/auth.py,sha256=urSTO9zgFO1qU69xvnKHPFQV0SlJL3d7_Ojl0tLnZwo,1143
|
|
151
151
|
schemathesis/transports/content_types.py,sha256=MiKOm-Hy5i75hrROPdpiBZPOTDzOwlCdnthJD12AJzI,2187
|
|
152
152
|
schemathesis/transports/headers.py,sha256=hr_AIDOfUxsJxpHfemIZ_uNG3_vzS_ZeMEKmZjbYiBE,990
|
|
153
153
|
schemathesis/transports/responses.py,sha256=OFD4ZLqwEFpo7F9vaP_SVgjhxAqatxIj38FS4XVq8Qs,1680
|
|
154
|
-
schemathesis-3.
|
|
155
|
-
schemathesis-3.
|
|
156
|
-
schemathesis-3.
|
|
157
|
-
schemathesis-3.
|
|
158
|
-
schemathesis-3.
|
|
154
|
+
schemathesis-3.37.0.dist-info/METADATA,sha256=Rt0KgoLdxayzfnVhiEbj8lzthkYuWItyJvdjrMw_WxQ,12905
|
|
155
|
+
schemathesis-3.37.0.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
|
|
156
|
+
schemathesis-3.37.0.dist-info/entry_points.txt,sha256=VHyLcOG7co0nOeuk8WjgpRETk5P1E2iCLrn26Zkn5uk,158
|
|
157
|
+
schemathesis-3.37.0.dist-info/licenses/LICENSE,sha256=PsPYgrDhZ7g9uwihJXNG-XVb55wj2uYhkl2DD8oAzY0,1103
|
|
158
|
+
schemathesis-3.37.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|