schemathesis 4.0.0a4__py3-none-any.whl → 4.0.0a6__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/cli/commands/run/__init__.py +15 -45
- schemathesis/cli/commands/run/checks.py +2 -3
- schemathesis/cli/commands/run/context.py +30 -17
- schemathesis/cli/commands/run/executor.py +1 -0
- schemathesis/cli/commands/run/handlers/output.py +168 -88
- schemathesis/cli/commands/run/hypothesis.py +7 -45
- schemathesis/core/__init__.py +7 -1
- schemathesis/engine/config.py +2 -2
- schemathesis/engine/core.py +11 -1
- schemathesis/engine/events.py +7 -0
- schemathesis/engine/phases/__init__.py +16 -4
- schemathesis/engine/phases/unit/__init__.py +77 -52
- schemathesis/engine/phases/unit/_executor.py +14 -12
- schemathesis/engine/phases/unit/_pool.py +8 -0
- schemathesis/experimental/__init__.py +0 -6
- schemathesis/generation/hypothesis/builder.py +222 -97
- schemathesis/openapi/checks.py +3 -1
- schemathesis/pytest/lazy.py +41 -2
- schemathesis/pytest/plugin.py +2 -1
- schemathesis/specs/openapi/checks.py +1 -1
- schemathesis/specs/openapi/examples.py +39 -25
- schemathesis/specs/openapi/patterns.py +39 -7
- schemathesis/specs/openapi/serialization.py +14 -0
- schemathesis/transport/requests.py +10 -1
- {schemathesis-4.0.0a4.dist-info → schemathesis-4.0.0a6.dist-info}/METADATA +47 -91
- {schemathesis-4.0.0a4.dist-info → schemathesis-4.0.0a6.dist-info}/RECORD +29 -29
- {schemathesis-4.0.0a4.dist-info → schemathesis-4.0.0a6.dist-info}/licenses/LICENSE +1 -1
- {schemathesis-4.0.0a4.dist-info → schemathesis-4.0.0a6.dist-info}/WHEEL +0 -0
- {schemathesis-4.0.0a4.dist-info → schemathesis-4.0.0a6.dist-info}/entry_points.txt +0 -0
@@ -1,7 +1,7 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
|
-
import json
|
4
3
|
from dataclasses import dataclass, field
|
4
|
+
from enum import Enum
|
5
5
|
from functools import wraps
|
6
6
|
from itertools import combinations
|
7
7
|
from time import perf_counter
|
@@ -13,62 +13,41 @@ from hypothesis import strategies as st
|
|
13
13
|
from hypothesis.errors import Unsatisfiable
|
14
14
|
from jsonschema.exceptions import SchemaError
|
15
15
|
|
16
|
-
from schemathesis
|
17
|
-
from schemathesis.
|
16
|
+
from schemathesis import auths
|
17
|
+
from schemathesis.auths import AuthStorage, AuthStorageMark
|
18
|
+
from schemathesis.core import NOT_SET, NotSet, SpecificationFeature, media_types
|
18
19
|
from schemathesis.core.errors import InvalidSchema, SerializationNotPossible
|
19
20
|
from schemathesis.core.marks import Mark
|
20
|
-
from schemathesis.core.result import Ok, Result
|
21
21
|
from schemathesis.core.transport import prepare_urlencoded
|
22
22
|
from schemathesis.core.validation import has_invalid_characters, is_latin_1_encodable
|
23
|
-
from schemathesis.experimental import COVERAGE_PHASE
|
24
23
|
from schemathesis.generation import GenerationConfig, GenerationMode, coverage
|
25
24
|
from schemathesis.generation.case import Case
|
26
25
|
from schemathesis.generation.hypothesis import DEFAULT_DEADLINE, examples, setup, strategies
|
27
26
|
from schemathesis.generation.hypothesis.given import GivenInput
|
28
|
-
from schemathesis.generation.meta import
|
27
|
+
from schemathesis.generation.meta import (
|
28
|
+
CaseMetadata,
|
29
|
+
ComponentInfo,
|
30
|
+
ComponentKind,
|
31
|
+
CoveragePhaseData,
|
32
|
+
GenerationInfo,
|
33
|
+
PhaseInfo,
|
34
|
+
)
|
29
35
|
from schemathesis.hooks import GLOBAL_HOOK_DISPATCHER, HookContext, HookDispatcher, HookDispatcherMark
|
30
|
-
from schemathesis.schemas import APIOperation,
|
36
|
+
from schemathesis.schemas import APIOperation, ParameterSet
|
31
37
|
|
32
38
|
setup()
|
33
39
|
|
34
40
|
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
generation_config: GenerationConfig,
|
40
|
-
settings: hypothesis.settings | None = None,
|
41
|
-
seed: int | None = None,
|
42
|
-
as_strategy_kwargs: Callable[[APIOperation], dict[str, Any]] | None = None,
|
43
|
-
given_kwargs: dict[str, GivenInput] | None = None,
|
44
|
-
) -> Generator[Result[tuple[APIOperation, Callable], InvalidSchema], None, None]:
|
45
|
-
"""Generate all operations and Hypothesis tests for them."""
|
46
|
-
for result in schema.get_all_operations(generation_config=generation_config):
|
47
|
-
if isinstance(result, Ok):
|
48
|
-
operation = result.ok()
|
49
|
-
if callable(as_strategy_kwargs):
|
50
|
-
_as_strategy_kwargs = as_strategy_kwargs(operation)
|
51
|
-
else:
|
52
|
-
_as_strategy_kwargs = {}
|
53
|
-
test = create_test(
|
54
|
-
operation=operation,
|
55
|
-
test_func=test_func,
|
56
|
-
config=HypothesisTestConfig(
|
57
|
-
settings=settings,
|
58
|
-
seed=seed,
|
59
|
-
generation=generation_config,
|
60
|
-
as_strategy_kwargs=_as_strategy_kwargs,
|
61
|
-
given_kwargs=given_kwargs or {},
|
62
|
-
),
|
63
|
-
)
|
64
|
-
yield Ok((operation, test))
|
65
|
-
else:
|
66
|
-
yield result
|
41
|
+
class HypothesisTestMode(Enum):
|
42
|
+
EXAMPLES = "examples"
|
43
|
+
COVERAGE = "coverage"
|
44
|
+
FUZZING = "fuzzing"
|
67
45
|
|
68
46
|
|
69
47
|
@dataclass
|
70
48
|
class HypothesisTestConfig:
|
71
49
|
generation: GenerationConfig
|
50
|
+
modes: list[HypothesisTestMode]
|
72
51
|
settings: hypothesis.settings | None = None
|
73
52
|
seed: int | None = None
|
74
53
|
as_strategy_kwargs: dict[str, Any] = field(default_factory=dict)
|
@@ -124,16 +103,33 @@ def create_test(
|
|
124
103
|
phases = tuple(phase for phase in settings.phases if phase != Phase.explain)
|
125
104
|
settings = hypothesis.settings(settings, phases=phases)
|
126
105
|
|
106
|
+
# Remove `reuse` & `generate` phases to avoid yielding any test cases if we don't do fuzzing
|
107
|
+
if HypothesisTestMode.FUZZING not in config.modes and (
|
108
|
+
Phase.generate in settings.phases or Phase.reuse in settings.phases
|
109
|
+
):
|
110
|
+
phases = tuple(phase for phase in settings.phases if phase not in (Phase.reuse, Phase.generate))
|
111
|
+
settings = hypothesis.settings(settings, phases=phases)
|
112
|
+
|
113
|
+
specification = operation.schema.specification
|
114
|
+
|
127
115
|
# Add examples if explicit phase is enabled
|
128
|
-
if
|
116
|
+
if (
|
117
|
+
HypothesisTestMode.EXAMPLES in config.modes
|
118
|
+
and Phase.explicit in settings.phases
|
119
|
+
and specification.supports_feature(SpecificationFeature.EXAMPLES)
|
120
|
+
):
|
129
121
|
hypothesis_test = add_examples(hypothesis_test, operation, hook_dispatcher=hook_dispatcher, **strategy_kwargs)
|
130
122
|
|
131
|
-
if
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
123
|
+
if (
|
124
|
+
HypothesisTestMode.COVERAGE in config.modes
|
125
|
+
and Phase.explicit in settings.phases
|
126
|
+
and specification.supports_feature(SpecificationFeature.COVERAGE)
|
127
|
+
and not config.given_args
|
128
|
+
and not config.given_kwargs
|
129
|
+
):
|
130
|
+
hypothesis_test = add_coverage(
|
131
|
+
hypothesis_test, operation, config.generation.modes, auth_storage, config.as_strategy_kwargs
|
132
|
+
)
|
137
133
|
|
138
134
|
setattr(hypothesis_test, SETTINGS_ATTRIBUTE_NAME, settings)
|
139
135
|
|
@@ -184,6 +180,7 @@ def add_examples(
|
|
184
180
|
NonSerializableMark.set(test, exc)
|
185
181
|
if isinstance(exc, SchemaError):
|
186
182
|
InvalidRegexMark.set(test, exc)
|
183
|
+
|
187
184
|
context = HookContext(operation) # context should be passed here instead
|
188
185
|
GLOBAL_HOOK_DISPATCHER.dispatch("before_add_examples", context, result)
|
189
186
|
operation.schema.hooks.dispatch("before_add_examples", context, result)
|
@@ -198,6 +195,7 @@ def add_examples(
|
|
198
195
|
continue
|
199
196
|
adjust_urlencoded_payload(example)
|
200
197
|
test = hypothesis.example(case=example)(test)
|
198
|
+
|
201
199
|
return test
|
202
200
|
|
203
201
|
|
@@ -211,10 +209,37 @@ def adjust_urlencoded_payload(case: Case) -> None:
|
|
211
209
|
pass
|
212
210
|
|
213
211
|
|
214
|
-
def add_coverage(
|
215
|
-
|
216
|
-
|
217
|
-
|
212
|
+
def add_coverage(
|
213
|
+
test: Callable,
|
214
|
+
operation: APIOperation,
|
215
|
+
generation_modes: list[GenerationMode],
|
216
|
+
auth_storage: AuthStorage | None,
|
217
|
+
as_strategy_kwargs: dict[str, Any],
|
218
|
+
) -> Callable:
|
219
|
+
from schemathesis.specs.openapi.constants import LOCATION_TO_CONTAINER
|
220
|
+
|
221
|
+
auth_context = auths.AuthContext(
|
222
|
+
operation=operation,
|
223
|
+
app=operation.app,
|
224
|
+
)
|
225
|
+
overrides = {
|
226
|
+
container: as_strategy_kwargs[container]
|
227
|
+
for container in LOCATION_TO_CONTAINER.values()
|
228
|
+
if container in as_strategy_kwargs
|
229
|
+
}
|
230
|
+
for case in _iter_coverage_cases(operation, generation_modes):
|
231
|
+
if case.media_type and operation.schema.transport.get_first_matching_media_type(case.media_type) is None:
|
232
|
+
continue
|
233
|
+
adjust_urlencoded_payload(case)
|
234
|
+
auths.set_on_case(case, auth_context, auth_storage)
|
235
|
+
for container_name, value in overrides.items():
|
236
|
+
container = getattr(case, container_name)
|
237
|
+
if container is None:
|
238
|
+
setattr(case, container_name, value)
|
239
|
+
else:
|
240
|
+
container.update(value)
|
241
|
+
|
242
|
+
test = hypothesis.example(case=case)(test)
|
218
243
|
return test
|
219
244
|
|
220
245
|
|
@@ -229,23 +254,123 @@ class Instant:
|
|
229
254
|
return perf_counter() - self.start
|
230
255
|
|
231
256
|
|
257
|
+
class Template:
|
258
|
+
__slots__ = ("_components", "_template", "_serializers")
|
259
|
+
|
260
|
+
def __init__(self, serializers: dict[str, Callable]) -> None:
|
261
|
+
self._components: dict[ComponentKind, ComponentInfo] = {}
|
262
|
+
self._template: dict[str, Any] = {}
|
263
|
+
self._serializers = serializers
|
264
|
+
|
265
|
+
def __contains__(self, key: str) -> bool:
|
266
|
+
return key in self._template
|
267
|
+
|
268
|
+
def __getitem__(self, key: str) -> dict:
|
269
|
+
return self._template[key]
|
270
|
+
|
271
|
+
def get(self, key: str, default: Any = None) -> dict:
|
272
|
+
return self._template.get(key, default)
|
273
|
+
|
274
|
+
def add_parameter(self, location: str, name: str, value: coverage.GeneratedValue) -> None:
|
275
|
+
from schemathesis.specs.openapi.constants import LOCATION_TO_CONTAINER
|
276
|
+
|
277
|
+
component_name = LOCATION_TO_CONTAINER[location]
|
278
|
+
kind = ComponentKind(component_name)
|
279
|
+
info = self._components.get(kind)
|
280
|
+
if info is None:
|
281
|
+
self._components[kind] = ComponentInfo(mode=value.generation_mode)
|
282
|
+
elif value.generation_mode == GenerationMode.NEGATIVE:
|
283
|
+
info.mode = GenerationMode.NEGATIVE
|
284
|
+
|
285
|
+
container = self._template.setdefault(component_name, {})
|
286
|
+
container[name] = value.value
|
287
|
+
|
288
|
+
def set_body(self, body: coverage.GeneratedValue, media_type: str) -> None:
|
289
|
+
self._template["body"] = body.value
|
290
|
+
self._template["media_type"] = media_type
|
291
|
+
self._components[ComponentKind.BODY] = ComponentInfo(mode=body.generation_mode)
|
292
|
+
|
293
|
+
def _serialize(self, kwargs: dict[str, Any]) -> dict[str, Any]:
|
294
|
+
from schemathesis.specs.openapi._hypothesis import quote_all
|
295
|
+
|
296
|
+
output = {}
|
297
|
+
for container_name, value in kwargs.items():
|
298
|
+
serializer = self._serializers.get(container_name)
|
299
|
+
if container_name in ("headers", "cookies") and isinstance(value, dict):
|
300
|
+
value = _stringify_value(value, container_name)
|
301
|
+
if serializer is not None:
|
302
|
+
value = serializer(value)
|
303
|
+
if container_name == "query" and isinstance(value, dict):
|
304
|
+
value = _stringify_value(value, container_name)
|
305
|
+
if container_name == "path_parameters" and isinstance(value, dict):
|
306
|
+
value = _stringify_value(quote_all(value), container_name)
|
307
|
+
output[container_name] = value
|
308
|
+
return output
|
309
|
+
|
310
|
+
def unmodified(self) -> TemplateValue:
|
311
|
+
kwargs = self._template.copy()
|
312
|
+
kwargs = self._serialize(kwargs)
|
313
|
+
return TemplateValue(kwargs=kwargs, components=self._components.copy())
|
314
|
+
|
315
|
+
def with_body(self, *, media_type: str, value: coverage.GeneratedValue) -> TemplateValue:
|
316
|
+
kwargs = {**self._template, "media_type": media_type, "body": value.value}
|
317
|
+
kwargs = self._serialize(kwargs)
|
318
|
+
components = {**self._components, ComponentKind.BODY: ComponentInfo(mode=value.generation_mode)}
|
319
|
+
return TemplateValue(kwargs=kwargs, components=components)
|
320
|
+
|
321
|
+
def with_parameter(self, *, location: str, name: str, value: coverage.GeneratedValue) -> TemplateValue:
|
322
|
+
from schemathesis.specs.openapi.constants import LOCATION_TO_CONTAINER
|
323
|
+
|
324
|
+
container_name = LOCATION_TO_CONTAINER[location]
|
325
|
+
container = self._template[container_name]
|
326
|
+
return self.with_container(
|
327
|
+
container_name=container_name, value={**container, name: value.value}, generation_mode=value.generation_mode
|
328
|
+
)
|
329
|
+
|
330
|
+
def with_container(self, *, container_name: str, value: Any, generation_mode: GenerationMode) -> TemplateValue:
|
331
|
+
kwargs = {**self._template, container_name: value}
|
332
|
+
components = {**self._components, ComponentKind(container_name): ComponentInfo(mode=generation_mode)}
|
333
|
+
kwargs = self._serialize(kwargs)
|
334
|
+
return TemplateValue(kwargs=kwargs, components=components)
|
335
|
+
|
336
|
+
|
337
|
+
@dataclass
|
338
|
+
class TemplateValue:
|
339
|
+
kwargs: dict[str, Any]
|
340
|
+
components: dict[ComponentKind, ComponentInfo]
|
341
|
+
__slots__ = ("kwargs", "components")
|
342
|
+
|
343
|
+
|
344
|
+
def _stringify_value(val: Any, container_name: str) -> Any:
|
345
|
+
if val is None:
|
346
|
+
return "null"
|
347
|
+
if val is True:
|
348
|
+
return "true"
|
349
|
+
if val is False:
|
350
|
+
return "false"
|
351
|
+
if isinstance(val, (int, float)):
|
352
|
+
return str(val)
|
353
|
+
if isinstance(val, list):
|
354
|
+
if container_name == "query":
|
355
|
+
# Having a list here ensures there will be multiple query parameters wit the same name
|
356
|
+
return [_stringify_value(item, container_name) for item in val]
|
357
|
+
# use comma-separated values style for arrays
|
358
|
+
return ",".join(_stringify_value(sub, container_name) for sub in val)
|
359
|
+
if isinstance(val, dict):
|
360
|
+
return {key: _stringify_value(sub, container_name) for key, sub in val.items()}
|
361
|
+
return val
|
362
|
+
|
363
|
+
|
232
364
|
def _iter_coverage_cases(
|
233
365
|
operation: APIOperation, generation_modes: list[GenerationMode]
|
234
366
|
) -> Generator[Case, None, None]:
|
235
367
|
from schemathesis.specs.openapi.constants import LOCATION_TO_CONTAINER
|
236
368
|
from schemathesis.specs.openapi.examples import find_in_responses, find_matching_in_responses
|
237
|
-
|
238
|
-
def _stringify_value(val: Any, location: str) -> str | list[str]:
|
239
|
-
if isinstance(val, list):
|
240
|
-
if location == "query":
|
241
|
-
# Having a list here ensures there will be multiple query parameters wit the same name
|
242
|
-
return [json.dumps(item) for item in val]
|
243
|
-
# use comma-separated values style for arrays
|
244
|
-
return ",".join(json.dumps(sub) for sub in val)
|
245
|
-
return json.dumps(val)
|
369
|
+
from schemathesis.specs.openapi.serialization import get_serializers_for_operation
|
246
370
|
|
247
371
|
generators: dict[tuple[str, str], Generator[coverage.GeneratedValue, None, None]] = {}
|
248
|
-
|
372
|
+
serializers = get_serializers_for_operation(operation)
|
373
|
+
template = Template(serializers)
|
249
374
|
|
250
375
|
instant = Instant()
|
251
376
|
responses = find_in_responses(operation)
|
@@ -261,11 +386,7 @@ def _iter_coverage_cases(
|
|
261
386
|
value = next(gen, NOT_SET)
|
262
387
|
if isinstance(value, NotSet):
|
263
388
|
continue
|
264
|
-
|
265
|
-
if location in ("header", "cookie", "path", "query") and not isinstance(value.value, str):
|
266
|
-
container[name] = _stringify_value(value.value, location)
|
267
|
-
else:
|
268
|
-
container[name] = value.value
|
389
|
+
template.add_parameter(location, name, value)
|
269
390
|
generators[(location, name)] = gen
|
270
391
|
template_time = instant.elapsed
|
271
392
|
if operation.body:
|
@@ -286,16 +407,16 @@ def _iter_coverage_cases(
|
|
286
407
|
elapsed = instant.elapsed
|
287
408
|
if "body" not in template:
|
288
409
|
template_time += elapsed
|
289
|
-
template
|
290
|
-
|
410
|
+
template.set_body(value, body.media_type)
|
411
|
+
data = template.with_body(value=value, media_type=body.media_type)
|
291
412
|
yield operation.Case(
|
292
|
-
**
|
413
|
+
**data.kwargs,
|
293
414
|
meta=CaseMetadata(
|
294
415
|
generation=GenerationInfo(
|
295
416
|
time=elapsed,
|
296
417
|
mode=value.generation_mode,
|
297
418
|
),
|
298
|
-
components=
|
419
|
+
components=data.components,
|
299
420
|
phase=PhaseInfo.coverage(
|
300
421
|
description=value.description,
|
301
422
|
location=value.location,
|
@@ -309,14 +430,15 @@ def _iter_coverage_cases(
|
|
309
430
|
instant = Instant()
|
310
431
|
try:
|
311
432
|
next_value = next(iterator)
|
433
|
+
data = template.with_body(value=next_value, media_type=body.media_type)
|
312
434
|
yield operation.Case(
|
313
|
-
**
|
435
|
+
**data.kwargs,
|
314
436
|
meta=CaseMetadata(
|
315
437
|
generation=GenerationInfo(
|
316
438
|
time=instant.elapsed,
|
317
439
|
mode=value.generation_mode,
|
318
440
|
),
|
319
|
-
components=
|
441
|
+
components=data.components,
|
320
442
|
phase=PhaseInfo.coverage(
|
321
443
|
description=next_value.description,
|
322
444
|
location=next_value.location,
|
@@ -328,37 +450,34 @@ def _iter_coverage_cases(
|
|
328
450
|
except StopIteration:
|
329
451
|
break
|
330
452
|
elif GenerationMode.POSITIVE in generation_modes:
|
453
|
+
data = template.unmodified()
|
331
454
|
yield operation.Case(
|
332
|
-
**
|
455
|
+
**data.kwargs,
|
333
456
|
meta=CaseMetadata(
|
334
457
|
generation=GenerationInfo(
|
335
458
|
time=template_time,
|
336
459
|
mode=GenerationMode.POSITIVE,
|
337
460
|
),
|
338
|
-
components=
|
461
|
+
components=data.components,
|
339
462
|
phase=PhaseInfo.coverage(description="Default positive test case"),
|
340
463
|
),
|
341
464
|
)
|
342
465
|
|
343
466
|
for (location, name), gen in generators.items():
|
344
|
-
container_name = LOCATION_TO_CONTAINER[location]
|
345
|
-
container = template[container_name]
|
346
467
|
iterator = iter(gen)
|
347
468
|
while True:
|
348
469
|
instant = Instant()
|
349
470
|
try:
|
350
471
|
value = next(iterator)
|
351
|
-
|
352
|
-
generated = _stringify_value(value.value, location)
|
353
|
-
else:
|
354
|
-
generated = value.value
|
472
|
+
data = template.with_parameter(location=location, name=name, value=value)
|
355
473
|
except StopIteration:
|
356
474
|
break
|
475
|
+
|
357
476
|
yield operation.Case(
|
358
|
-
**
|
477
|
+
**data.kwargs,
|
359
478
|
meta=CaseMetadata(
|
360
479
|
generation=GenerationInfo(time=instant.elapsed, mode=value.generation_mode),
|
361
|
-
components=
|
480
|
+
components=data.components,
|
362
481
|
phase=PhaseInfo.coverage(
|
363
482
|
description=value.description,
|
364
483
|
location=value.location,
|
@@ -372,12 +491,13 @@ def _iter_coverage_cases(
|
|
372
491
|
methods = {"get", "put", "post", "delete", "options", "patch", "trace"} - set(operation.schema[operation.path])
|
373
492
|
for method in sorted(methods):
|
374
493
|
instant = Instant()
|
494
|
+
data = template.unmodified()
|
375
495
|
yield operation.Case(
|
376
|
-
**
|
496
|
+
**data.kwargs,
|
377
497
|
method=method.upper(),
|
378
498
|
meta=CaseMetadata(
|
379
499
|
generation=GenerationInfo(time=instant.elapsed, mode=GenerationMode.NEGATIVE),
|
380
|
-
components=
|
500
|
+
components=data.components,
|
381
501
|
phase=PhaseInfo.coverage(description=f"Unspecified HTTP method: {method.upper()}"),
|
382
502
|
),
|
383
503
|
)
|
@@ -390,11 +510,16 @@ def _iter_coverage_cases(
|
|
390
510
|
# I.e. contains just `default` value without any other keywords
|
391
511
|
value = container.get(parameter.name, NOT_SET)
|
392
512
|
if value is not NOT_SET:
|
513
|
+
data = template.with_container(
|
514
|
+
container_name="query",
|
515
|
+
value={**container, parameter.name: [value, value]},
|
516
|
+
generation_mode=GenerationMode.NEGATIVE,
|
517
|
+
)
|
393
518
|
yield operation.Case(
|
394
|
-
**
|
519
|
+
**data.kwargs,
|
395
520
|
meta=CaseMetadata(
|
396
521
|
generation=GenerationInfo(time=instant.elapsed, mode=GenerationMode.NEGATIVE),
|
397
|
-
components=
|
522
|
+
components=data.components,
|
398
523
|
phase=PhaseInfo.coverage(
|
399
524
|
description=f"Duplicate `{parameter.name}` query parameter",
|
400
525
|
parameter=parameter.name,
|
@@ -410,11 +535,16 @@ def _iter_coverage_cases(
|
|
410
535
|
location = parameter.location
|
411
536
|
container_name = LOCATION_TO_CONTAINER[location]
|
412
537
|
container = template[container_name]
|
538
|
+
data = template.with_container(
|
539
|
+
container_name=container_name,
|
540
|
+
value={k: v for k, v in container.items() if k != name},
|
541
|
+
generation_mode=GenerationMode.NEGATIVE,
|
542
|
+
)
|
413
543
|
yield operation.Case(
|
414
|
-
**
|
544
|
+
**data.kwargs,
|
415
545
|
meta=CaseMetadata(
|
416
546
|
generation=GenerationInfo(time=instant.elapsed, mode=GenerationMode.NEGATIVE),
|
417
|
-
components=
|
547
|
+
components=data.components,
|
418
548
|
phase=PhaseInfo.coverage(
|
419
549
|
description=f"Missing `{name}` at {location}",
|
420
550
|
parameter=name,
|
@@ -449,22 +579,17 @@ def _iter_coverage_cases(
|
|
449
579
|
_generation_mode: GenerationMode,
|
450
580
|
_instant: Instant,
|
451
581
|
) -> Case:
|
452
|
-
|
453
|
-
|
454
|
-
|
455
|
-
for name, val in container_values.items()
|
456
|
-
}
|
457
|
-
else:
|
458
|
-
container = container_values
|
459
|
-
|
582
|
+
data = template.with_container(
|
583
|
+
container_name=_container_name, value=container_values, generation_mode=_generation_mode
|
584
|
+
)
|
460
585
|
return operation.Case(
|
461
|
-
**
|
586
|
+
**data.kwargs,
|
462
587
|
meta=CaseMetadata(
|
463
588
|
generation=GenerationInfo(
|
464
589
|
time=_instant.elapsed,
|
465
590
|
mode=_generation_mode,
|
466
591
|
),
|
467
|
-
components=
|
592
|
+
components=data.components,
|
468
593
|
phase=PhaseInfo.coverage(
|
469
594
|
description=description,
|
470
595
|
parameter=_parameter,
|
schemathesis/openapi/checks.py
CHANGED
@@ -14,7 +14,9 @@ if TYPE_CHECKING:
|
|
14
14
|
@dataclass
|
15
15
|
class NegativeDataRejectionConfig:
|
16
16
|
# 5xx will pass through
|
17
|
-
allowed_statuses: list[str] = field(
|
17
|
+
allowed_statuses: list[str] = field(
|
18
|
+
default_factory=lambda: ["400", "401", "403", "404", "406", "422", "428", "5xx"]
|
19
|
+
)
|
18
20
|
|
19
21
|
|
20
22
|
@dataclass
|
schemathesis/pytest/lazy.py
CHANGED
@@ -10,9 +10,10 @@ from hypothesis.core import HypothesisHandle
|
|
10
10
|
from pytest_subtests import SubTests
|
11
11
|
|
12
12
|
from schemathesis.core.errors import InvalidSchema
|
13
|
-
from schemathesis.core.result import Ok
|
13
|
+
from schemathesis.core.result import Ok, Result
|
14
14
|
from schemathesis.filters import FilterSet, FilterValue, MatcherFunc, RegexValue, is_deprecated
|
15
|
-
from schemathesis.generation
|
15
|
+
from schemathesis.generation import GenerationConfig
|
16
|
+
from schemathesis.generation.hypothesis.builder import HypothesisTestConfig, HypothesisTestMode, create_test
|
16
17
|
from schemathesis.generation.hypothesis.given import (
|
17
18
|
GivenArgsMark,
|
18
19
|
GivenInput,
|
@@ -27,11 +28,48 @@ from schemathesis.pytest.control_flow import fail_on_no_matches
|
|
27
28
|
from schemathesis.schemas import BaseSchema
|
28
29
|
|
29
30
|
if TYPE_CHECKING:
|
31
|
+
import hypothesis
|
30
32
|
from _pytest.fixtures import FixtureRequest
|
31
33
|
|
32
34
|
from schemathesis.schemas import APIOperation
|
33
35
|
|
34
36
|
|
37
|
+
def get_all_tests(
|
38
|
+
*,
|
39
|
+
schema: BaseSchema,
|
40
|
+
test_func: Callable,
|
41
|
+
generation_config: GenerationConfig,
|
42
|
+
modes: list[HypothesisTestMode],
|
43
|
+
settings: hypothesis.settings | None = None,
|
44
|
+
seed: int | None = None,
|
45
|
+
as_strategy_kwargs: Callable[[APIOperation], dict[str, Any]] | None = None,
|
46
|
+
given_kwargs: dict[str, GivenInput] | None = None,
|
47
|
+
) -> Generator[Result[tuple[APIOperation, Callable], InvalidSchema], None, None]:
|
48
|
+
"""Generate all operations and Hypothesis tests for them."""
|
49
|
+
for result in schema.get_all_operations(generation_config=generation_config):
|
50
|
+
if isinstance(result, Ok):
|
51
|
+
operation = result.ok()
|
52
|
+
if callable(as_strategy_kwargs):
|
53
|
+
_as_strategy_kwargs = as_strategy_kwargs(operation)
|
54
|
+
else:
|
55
|
+
_as_strategy_kwargs = {}
|
56
|
+
test = create_test(
|
57
|
+
operation=operation,
|
58
|
+
test_func=test_func,
|
59
|
+
config=HypothesisTestConfig(
|
60
|
+
settings=settings,
|
61
|
+
modes=modes,
|
62
|
+
seed=seed,
|
63
|
+
generation=generation_config,
|
64
|
+
as_strategy_kwargs=_as_strategy_kwargs,
|
65
|
+
given_kwargs=given_kwargs or {},
|
66
|
+
),
|
67
|
+
)
|
68
|
+
yield Ok((operation, test))
|
69
|
+
else:
|
70
|
+
yield result
|
71
|
+
|
72
|
+
|
35
73
|
@dataclass
|
36
74
|
class LazySchema:
|
37
75
|
fixture_name: str
|
@@ -155,6 +193,7 @@ class LazySchema:
|
|
155
193
|
schema=schema,
|
156
194
|
test_func=test_func,
|
157
195
|
settings=settings,
|
196
|
+
modes=list(HypothesisTestMode),
|
158
197
|
generation_config=schema.generation_config,
|
159
198
|
as_strategy_kwargs=as_strategy_kwargs,
|
160
199
|
given_kwargs=given_kwargs,
|
schemathesis/pytest/plugin.py
CHANGED
@@ -108,7 +108,7 @@ class SchemathesisCase(PyCollector):
|
|
108
108
|
This implementation is based on the original one in pytest, but with slight adjustments
|
109
109
|
to produce tests out of hypothesis ones.
|
110
110
|
"""
|
111
|
-
from schemathesis.generation.hypothesis.builder import HypothesisTestConfig, create_test
|
111
|
+
from schemathesis.generation.hypothesis.builder import HypothesisTestConfig, HypothesisTestMode, create_test
|
112
112
|
|
113
113
|
is_trio_test = False
|
114
114
|
for mark in getattr(self.test_function, "pytestmark", []):
|
@@ -133,6 +133,7 @@ class SchemathesisCase(PyCollector):
|
|
133
133
|
operation=operation,
|
134
134
|
test_func=self.test_function,
|
135
135
|
config=HypothesisTestConfig(
|
136
|
+
modes=list(HypothesisTestMode),
|
136
137
|
given_kwargs=self.given_kwargs,
|
137
138
|
generation=self.schema.generation_config,
|
138
139
|
as_strategy_kwargs=as_strategy_kwargs,
|
@@ -299,7 +299,7 @@ def missing_required_header(ctx: CheckContext, response: Response, case: Case) -
|
|
299
299
|
|
300
300
|
def unsupported_method(ctx: CheckContext, response: Response, case: Case) -> bool | None:
|
301
301
|
meta = case.meta
|
302
|
-
if meta is None or not isinstance(meta.phase.data, CoveragePhaseData):
|
302
|
+
if meta is None or not isinstance(meta.phase.data, CoveragePhaseData) or response.request.method == "OPTIONS":
|
303
303
|
return None
|
304
304
|
data = meta.phase.data
|
305
305
|
if data.description and data.description.startswith("Unspecified HTTP method:"):
|