schemathesis 4.0.0a9__py3-none-any.whl → 4.0.0a11__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/__init__.py +3 -7
- schemathesis/checks.py +17 -7
- schemathesis/cli/commands/__init__.py +51 -3
- schemathesis/cli/commands/data.py +10 -0
- schemathesis/cli/commands/run/__init__.py +147 -260
- schemathesis/cli/commands/run/context.py +2 -3
- schemathesis/cli/commands/run/events.py +4 -0
- schemathesis/cli/commands/run/executor.py +60 -73
- schemathesis/cli/commands/run/filters.py +15 -165
- schemathesis/cli/commands/run/handlers/cassettes.py +105 -104
- schemathesis/cli/commands/run/handlers/junitxml.py +6 -5
- schemathesis/cli/commands/run/handlers/output.py +26 -47
- schemathesis/cli/commands/run/loaders.py +35 -50
- schemathesis/cli/commands/run/validation.py +36 -161
- schemathesis/cli/core.py +5 -3
- schemathesis/cli/ext/fs.py +7 -5
- schemathesis/cli/ext/options.py +0 -21
- schemathesis/config/__init__.py +188 -0
- schemathesis/config/_auth.py +51 -0
- schemathesis/config/_checks.py +268 -0
- schemathesis/config/_diff_base.py +99 -0
- schemathesis/config/_env.py +21 -0
- schemathesis/config/_error.py +156 -0
- schemathesis/config/_generation.py +150 -0
- schemathesis/config/_health_check.py +24 -0
- schemathesis/config/_operations.py +313 -0
- schemathesis/config/_output.py +171 -0
- schemathesis/config/_parameters.py +19 -0
- schemathesis/config/_phases.py +151 -0
- schemathesis/config/_projects.py +495 -0
- schemathesis/config/_rate_limit.py +17 -0
- schemathesis/config/_report.py +116 -0
- schemathesis/config/_validator.py +9 -0
- schemathesis/config/schema.json +837 -0
- schemathesis/core/__init__.py +3 -0
- schemathesis/core/compat.py +16 -9
- schemathesis/core/errors.py +19 -2
- schemathesis/core/failures.py +6 -7
- schemathesis/core/hooks.py +20 -0
- schemathesis/core/output/__init__.py +14 -37
- schemathesis/core/output/sanitization.py +3 -146
- schemathesis/core/validation.py +16 -0
- schemathesis/engine/__init__.py +2 -4
- schemathesis/engine/context.py +41 -43
- schemathesis/engine/core.py +7 -5
- schemathesis/engine/phases/__init__.py +10 -0
- schemathesis/engine/phases/probes.py +8 -8
- schemathesis/engine/phases/stateful/_executor.py +68 -43
- schemathesis/engine/phases/unit/__init__.py +23 -15
- schemathesis/engine/phases/unit/_executor.py +77 -17
- schemathesis/engine/phases/unit/_pool.py +1 -1
- schemathesis/errors.py +2 -0
- schemathesis/filters.py +2 -3
- schemathesis/generation/__init__.py +6 -31
- schemathesis/generation/case.py +5 -3
- schemathesis/generation/coverage.py +174 -134
- schemathesis/generation/hypothesis/__init__.py +7 -1
- schemathesis/generation/hypothesis/builder.py +40 -14
- schemathesis/generation/meta.py +3 -3
- schemathesis/generation/overrides.py +37 -1
- schemathesis/generation/stateful/state_machine.py +8 -1
- schemathesis/graphql/loaders.py +21 -12
- schemathesis/openapi/checks.py +12 -8
- schemathesis/openapi/generation/filters.py +10 -8
- schemathesis/openapi/loaders.py +22 -13
- schemathesis/pytest/lazy.py +2 -5
- schemathesis/pytest/plugin.py +11 -2
- schemathesis/schemas.py +13 -61
- schemathesis/specs/graphql/schemas.py +11 -15
- schemathesis/specs/openapi/_hypothesis.py +12 -8
- schemathesis/specs/openapi/checks.py +16 -18
- schemathesis/specs/openapi/examples.py +4 -3
- schemathesis/specs/openapi/formats.py +2 -2
- schemathesis/specs/openapi/negative/__init__.py +2 -2
- schemathesis/specs/openapi/patterns.py +46 -16
- schemathesis/specs/openapi/references.py +2 -3
- schemathesis/specs/openapi/schemas.py +11 -20
- schemathesis/specs/openapi/stateful/__init__.py +10 -5
- schemathesis/transport/prepare.py +7 -6
- schemathesis/transport/requests.py +3 -1
- schemathesis/transport/wsgi.py +3 -4
- {schemathesis-4.0.0a9.dist-info → schemathesis-4.0.0a11.dist-info}/METADATA +7 -8
- schemathesis-4.0.0a11.dist-info/RECORD +166 -0
- schemathesis/cli/commands/run/checks.py +0 -79
- schemathesis/cli/commands/run/hypothesis.py +0 -78
- schemathesis/cli/commands/run/reports.py +0 -72
- schemathesis/cli/hooks.py +0 -36
- schemathesis/engine/config.py +0 -59
- schemathesis/experimental/__init__.py +0 -72
- schemathesis-4.0.0a9.dist-info/RECORD +0 -153
- {schemathesis-4.0.0a9.dist-info → schemathesis-4.0.0a11.dist-info}/WHEEL +0 -0
- {schemathesis-4.0.0a9.dist-info → schemathesis-4.0.0a11.dist-info}/entry_points.txt +0 -0
- {schemathesis-4.0.0a9.dist-info → schemathesis-4.0.0a11.dist-info}/licenses/LICENSE +0 -0
@@ -5,11 +5,14 @@ DEFAULT_DEADLINE = 15000
|
|
5
5
|
|
6
6
|
|
7
7
|
def setup() -> None:
|
8
|
+
from hypothesis import core as root_core
|
9
|
+
from hypothesis.internal.conjecture import engine
|
8
10
|
from hypothesis.internal.entropy import deterministic_PRNG
|
9
11
|
from hypothesis.internal.reflection import is_first_param_referenced_in_function
|
10
|
-
from hypothesis.strategies._internal import core
|
12
|
+
from hypothesis.strategies._internal import collections, core
|
11
13
|
from hypothesis_jsonschema import _from_schema, _resolve
|
12
14
|
|
15
|
+
from schemathesis.core import INTERNAL_BUFFER_SIZE
|
13
16
|
from schemathesis.core.transforms import deepclone
|
14
17
|
|
15
18
|
# Forcefully initializes Hypothesis' global PRNG to avoid races that initialize it
|
@@ -28,3 +31,6 @@ def setup() -> None:
|
|
28
31
|
core.is_first_param_referenced_in_function = _is_first_param_referenced_in_function # type: ignore
|
29
32
|
_resolve.deepcopy = deepclone # type: ignore
|
30
33
|
_from_schema.deepcopy = deepclone # type: ignore
|
34
|
+
root_core.BUFFER_SIZE = INTERNAL_BUFFER_SIZE # type: ignore
|
35
|
+
engine.BUFFER_SIZE = INTERNAL_BUFFER_SIZE
|
36
|
+
collections.BUFFER_SIZE = INTERNAL_BUFFER_SIZE # type: ignore
|
@@ -1,7 +1,6 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
3
|
import asyncio
|
4
|
-
import os
|
5
4
|
from dataclasses import dataclass, field
|
6
5
|
from enum import Enum
|
7
6
|
from functools import wraps
|
@@ -15,15 +14,17 @@ from hypothesis import strategies as st
|
|
15
14
|
from hypothesis._settings import all_settings
|
16
15
|
from hypothesis.errors import Unsatisfiable
|
17
16
|
from jsonschema.exceptions import SchemaError
|
17
|
+
from requests.models import CaseInsensitiveDict
|
18
18
|
|
19
19
|
from schemathesis import auths
|
20
20
|
from schemathesis.auths import AuthStorage, AuthStorageMark
|
21
|
-
from schemathesis.
|
21
|
+
from schemathesis.config import ProjectConfig
|
22
|
+
from schemathesis.core import NOT_SET, NotSet, SpecificationFeature, media_types
|
22
23
|
from schemathesis.core.errors import InvalidSchema, SerializationNotPossible
|
23
24
|
from schemathesis.core.marks import Mark
|
24
25
|
from schemathesis.core.transport import prepare_urlencoded
|
25
26
|
from schemathesis.core.validation import has_invalid_characters, is_latin_1_encodable
|
26
|
-
from schemathesis.generation import
|
27
|
+
from schemathesis.generation import GenerationMode, coverage
|
27
28
|
from schemathesis.generation.case import Case
|
28
29
|
from schemathesis.generation.hypothesis import DEFAULT_DEADLINE, examples, setup, strategies
|
29
30
|
from schemathesis.generation.hypothesis.given import GivenInput
|
@@ -49,7 +50,7 @@ class HypothesisTestMode(str, Enum):
|
|
49
50
|
|
50
51
|
@dataclass
|
51
52
|
class HypothesisTestConfig:
|
52
|
-
|
53
|
+
project: ProjectConfig
|
53
54
|
modes: list[HypothesisTestMode]
|
54
55
|
settings: hypothesis.settings | None = None
|
55
56
|
seed: int | None = None
|
@@ -71,11 +72,11 @@ def create_test(
|
|
71
72
|
strategy_kwargs = {
|
72
73
|
"hooks": hook_dispatcher,
|
73
74
|
"auth_storage": auth_storage,
|
74
|
-
"generation_config": config.generation,
|
75
75
|
**config.as_strategy_kwargs,
|
76
76
|
}
|
77
|
+
generation = config.project.generation_for(operation=operation)
|
77
78
|
strategy = strategies.combine(
|
78
|
-
[operation.as_strategy(generation_mode=mode, **strategy_kwargs) for mode in
|
79
|
+
[operation.as_strategy(generation_mode=mode, **strategy_kwargs) for mode in generation.modes]
|
79
80
|
)
|
80
81
|
|
81
82
|
hypothesis_test = create_base_test(
|
@@ -127,23 +128,21 @@ def create_test(
|
|
127
128
|
):
|
128
129
|
hypothesis_test = add_examples(hypothesis_test, operation, hook_dispatcher=hook_dispatcher, **strategy_kwargs)
|
129
130
|
|
130
|
-
disable_coverage = string_to_boolean(os.getenv("SCHEMATHESIS_DISABLE_COVERAGE", ""))
|
131
|
-
|
132
131
|
if (
|
133
|
-
|
134
|
-
and HypothesisTestMode.COVERAGE in config.modes
|
132
|
+
HypothesisTestMode.COVERAGE in config.modes
|
135
133
|
and Phase.explicit in settings.phases
|
136
134
|
and specification.supports_feature(SpecificationFeature.COVERAGE)
|
137
135
|
and not config.given_args
|
138
136
|
and not config.given_kwargs
|
139
137
|
):
|
138
|
+
phases_config = config.project.phases_for(operation=operation)
|
140
139
|
hypothesis_test = add_coverage(
|
141
140
|
hypothesis_test,
|
142
141
|
operation,
|
143
|
-
|
142
|
+
generation.modes,
|
144
143
|
auth_storage,
|
145
144
|
config.as_strategy_kwargs,
|
146
|
-
|
145
|
+
phases_config.coverage.unexpected_methods,
|
147
146
|
)
|
148
147
|
|
149
148
|
setattr(hypothesis_test, SETTINGS_ATTRIBUTE_NAME, settings)
|
@@ -388,7 +387,7 @@ def _stringify_value(val: Any, container_name: str) -> Any:
|
|
388
387
|
# Having a list here ensures there will be multiple query parameters wit the same name
|
389
388
|
return [_stringify_value(item, container_name) for item in val]
|
390
389
|
# use comma-separated values style for arrays
|
391
|
-
return ",".join(_stringify_value(sub, container_name) for sub in val)
|
390
|
+
return ",".join(str(_stringify_value(sub, container_name)) for sub in val)
|
392
391
|
if isinstance(val, dict):
|
393
392
|
return {key: _stringify_value(sub, container_name) for key, sub in val.items()}
|
394
393
|
return val
|
@@ -411,6 +410,10 @@ def _iter_coverage_cases(
|
|
411
410
|
responses = find_in_responses(operation)
|
412
411
|
# NOTE: The HEAD method is excluded
|
413
412
|
unexpected_methods = unexpected_methods or {"get", "put", "post", "delete", "options", "patch", "trace"}
|
413
|
+
|
414
|
+
seen_negative = coverage.HashSet()
|
415
|
+
seen_positive = coverage.HashSet()
|
416
|
+
|
414
417
|
for parameter in operation.iter_parameters():
|
415
418
|
location = parameter.location
|
416
419
|
name = parameter.name
|
@@ -473,7 +476,7 @@ def _iter_coverage_cases(
|
|
473
476
|
meta=CaseMetadata(
|
474
477
|
generation=GenerationInfo(
|
475
478
|
time=instant.elapsed,
|
476
|
-
mode=
|
479
|
+
mode=next_value.generation_mode,
|
477
480
|
),
|
478
481
|
components=data.components,
|
479
482
|
phase=PhaseInfo.coverage(
|
@@ -488,6 +491,7 @@ def _iter_coverage_cases(
|
|
488
491
|
break
|
489
492
|
elif GenerationMode.POSITIVE in generation_modes:
|
490
493
|
data = template.unmodified()
|
494
|
+
seen_positive.insert(data.kwargs)
|
491
495
|
yield operation.Case(
|
492
496
|
**data.kwargs,
|
493
497
|
meta=CaseMetadata(
|
@@ -510,6 +514,12 @@ def _iter_coverage_cases(
|
|
510
514
|
except StopIteration:
|
511
515
|
break
|
512
516
|
|
517
|
+
if value.generation_mode == GenerationMode.NEGATIVE:
|
518
|
+
seen_negative.insert(data.kwargs)
|
519
|
+
elif value.generation_mode == GenerationMode.POSITIVE and not seen_positive.insert(data.kwargs):
|
520
|
+
# Was already generated before
|
521
|
+
continue
|
522
|
+
|
513
523
|
yield operation.Case(
|
514
524
|
**data.kwargs,
|
515
525
|
meta=CaseMetadata(
|
@@ -689,6 +699,9 @@ def _iter_coverage_cases(
|
|
689
699
|
if GenerationMode.NEGATIVE in generation_modes:
|
690
700
|
subschema = _combination_schema(only_required, required, parameter_set)
|
691
701
|
for case in _yield_negative(subschema, location, container_name):
|
702
|
+
kwargs = _case_to_kwargs(case)
|
703
|
+
if not seen_negative.insert(kwargs):
|
704
|
+
continue
|
692
705
|
assert case.meta is not None
|
693
706
|
assert isinstance(case.meta.phase.data, CoveragePhaseData)
|
694
707
|
# Already generated in one of the blocks above
|
@@ -738,6 +751,19 @@ def _iter_coverage_cases(
|
|
738
751
|
)
|
739
752
|
|
740
753
|
|
754
|
+
def _case_to_kwargs(case: Case) -> dict:
|
755
|
+
from schemathesis.specs.openapi.constants import LOCATION_TO_CONTAINER
|
756
|
+
|
757
|
+
kwargs = {}
|
758
|
+
for container_name in LOCATION_TO_CONTAINER.values():
|
759
|
+
value = getattr(case, container_name)
|
760
|
+
if isinstance(value, CaseInsensitiveDict):
|
761
|
+
kwargs[container_name] = dict(value)
|
762
|
+
elif value and value is not NOT_SET:
|
763
|
+
kwargs[container_name] = value
|
764
|
+
return kwargs
|
765
|
+
|
766
|
+
|
741
767
|
def find_invalid_headers(headers: Mapping) -> Generator[tuple[str, str], None, None]:
|
742
768
|
for name, value in headers.items():
|
743
769
|
if not is_latin_1_encodable(value) or has_invalid_characters(name, value):
|
schemathesis/generation/meta.py
CHANGED
@@ -9,9 +9,9 @@ from schemathesis.generation import GenerationMode
|
|
9
9
|
class TestPhase(str, Enum):
|
10
10
|
__test__ = False
|
11
11
|
|
12
|
-
|
12
|
+
EXAMPLES = "examples"
|
13
13
|
COVERAGE = "coverage"
|
14
|
-
|
14
|
+
FUZZING = "fuzzing"
|
15
15
|
|
16
16
|
|
17
17
|
class ComponentKind(str, Enum):
|
@@ -81,7 +81,7 @@ class PhaseInfo:
|
|
81
81
|
|
82
82
|
@classmethod
|
83
83
|
def generate(cls) -> PhaseInfo:
|
84
|
-
return cls(name=TestPhase.
|
84
|
+
return cls(name=TestPhase.FUZZING, data=GeneratePhaseData())
|
85
85
|
|
86
86
|
|
87
87
|
@dataclass
|
@@ -4,6 +4,7 @@ from collections.abc import Mapping
|
|
4
4
|
from dataclasses import dataclass
|
5
5
|
from typing import TYPE_CHECKING, Any, Callable
|
6
6
|
|
7
|
+
from schemathesis.config import ProjectConfig
|
7
8
|
from schemathesis.core.errors import IncorrectUsage
|
8
9
|
from schemathesis.core.marks import Mark
|
9
10
|
from schemathesis.core.transforms import diff
|
@@ -11,7 +12,7 @@ from schemathesis.generation.meta import ComponentKind
|
|
11
12
|
|
12
13
|
if TYPE_CHECKING:
|
13
14
|
from schemathesis.generation.case import Case
|
14
|
-
from schemathesis.schemas import APIOperation, ParameterSet
|
15
|
+
from schemathesis.schemas import APIOperation, Parameter, ParameterSet
|
15
16
|
|
16
17
|
|
17
18
|
@dataclass
|
@@ -41,6 +42,41 @@ class Override:
|
|
41
42
|
)
|
42
43
|
|
43
44
|
|
45
|
+
def for_operation(config: ProjectConfig, *, operation: APIOperation) -> Override:
|
46
|
+
operation_config = config.operations.get_for_operation(operation)
|
47
|
+
|
48
|
+
output = Override(query={}, headers={}, cookies={}, path_parameters={})
|
49
|
+
groups = [
|
50
|
+
(output.query, operation.query),
|
51
|
+
(output.headers, operation.headers),
|
52
|
+
(output.cookies, operation.cookies),
|
53
|
+
(output.path_parameters, operation.path_parameters),
|
54
|
+
]
|
55
|
+
for container, params in groups:
|
56
|
+
for param in params:
|
57
|
+
# Attempt to get the override from the operation-specific configuration.
|
58
|
+
value = None
|
59
|
+
if operation_config:
|
60
|
+
value = _get_override_value(param, operation_config.parameters)
|
61
|
+
# Fallback to the global project configuration.
|
62
|
+
if value is None:
|
63
|
+
value = _get_override_value(param, config.parameters)
|
64
|
+
if value is not None:
|
65
|
+
container[param.name] = value
|
66
|
+
|
67
|
+
return output
|
68
|
+
|
69
|
+
|
70
|
+
def _get_override_value(param: Parameter, parameters: dict[str, Any]) -> Any:
|
71
|
+
key = param.name
|
72
|
+
full_key = f"{param.location}.{param.name}"
|
73
|
+
if key in parameters:
|
74
|
+
return parameters[key]
|
75
|
+
elif full_key in parameters:
|
76
|
+
return parameters[full_key]
|
77
|
+
return None
|
78
|
+
|
79
|
+
|
44
80
|
def _for_parameters(overridden: dict[str, str], defined: ParameterSet) -> dict[str, str]:
|
45
81
|
output = {}
|
46
82
|
for param in defined:
|
@@ -10,6 +10,7 @@ from hypothesis.errors import InvalidDefinition
|
|
10
10
|
from hypothesis.stateful import RuleBasedStateMachine
|
11
11
|
|
12
12
|
from schemathesis.checks import CheckFunction
|
13
|
+
from schemathesis.core import DEFAULT_STATEFUL_STEP_COUNT
|
13
14
|
from schemathesis.core.errors import NoLinksFound
|
14
15
|
from schemathesis.core.result import Result
|
15
16
|
from schemathesis.core.transport import Response
|
@@ -22,7 +23,6 @@ if TYPE_CHECKING:
|
|
22
23
|
from schemathesis.schemas import BaseSchema
|
23
24
|
|
24
25
|
|
25
|
-
DEFAULT_STATEFUL_STEP_COUNT = 6
|
26
26
|
DEFAULT_STATE_MACHINE_SETTINGS = hypothesis.settings(
|
27
27
|
phases=[hypothesis.Phase.generate],
|
28
28
|
deadline=None,
|
@@ -184,6 +184,13 @@ class APIStateMachine(RuleBasedStateMachine):
|
|
184
184
|
if target is not None:
|
185
185
|
super()._add_result_to_targets((target,), result)
|
186
186
|
|
187
|
+
def _add_results_to_targets(self, targets: tuple[str, ...], results: list[StepOutput]) -> None:
|
188
|
+
# Hypothesis >6.131.15
|
189
|
+
for result in results:
|
190
|
+
target = self._get_target_for_result(result)
|
191
|
+
if target is not None:
|
192
|
+
super()._add_results_to_targets((target,), [result])
|
193
|
+
|
187
194
|
@classmethod
|
188
195
|
def run(cls, *, settings: hypothesis.settings | None = None) -> None:
|
189
196
|
"""Run state machine as a test."""
|
schemathesis/graphql/loaders.py
CHANGED
@@ -6,6 +6,7 @@ from os import PathLike
|
|
6
6
|
from pathlib import Path
|
7
7
|
from typing import IO, TYPE_CHECKING, Any, Callable, Dict, NoReturn, TypeVar, cast
|
8
8
|
|
9
|
+
from schemathesis.config import SchemathesisConfig
|
9
10
|
from schemathesis.core.errors import LoaderError, LoaderErrorKind
|
10
11
|
from schemathesis.core.loaders import load_from_url, prepare_request_kwargs, raise_for_status, require_relative_url
|
11
12
|
from schemathesis.hooks import HookContext, dispatch
|
@@ -17,16 +18,16 @@ if TYPE_CHECKING:
|
|
17
18
|
from schemathesis.specs.graphql.schemas import GraphQLSchema
|
18
19
|
|
19
20
|
|
20
|
-
def from_asgi(path: str, app: Any, **kwargs: Any) -> GraphQLSchema:
|
21
|
+
def from_asgi(path: str, app: Any, *, config: SchemathesisConfig | None = None, **kwargs: Any) -> GraphQLSchema:
|
21
22
|
require_relative_url(path)
|
22
23
|
kwargs.setdefault("json", {"query": get_introspection_query()})
|
23
24
|
client = asgi.get_client(app)
|
24
25
|
response = load_from_url(client.post, url=path, **kwargs)
|
25
26
|
schema = extract_schema_from_response(response, lambda r: r.json())
|
26
|
-
return from_dict(schema=schema).configure(app=app, location=path)
|
27
|
+
return from_dict(schema=schema, config=config).configure(app=app, location=path)
|
27
28
|
|
28
29
|
|
29
|
-
def from_wsgi(path: str, app: Any, **kwargs: Any) -> GraphQLSchema:
|
30
|
+
def from_wsgi(path: str, app: Any, *, config: SchemathesisConfig | None = None, **kwargs: Any) -> GraphQLSchema:
|
30
31
|
require_relative_url(path)
|
31
32
|
prepare_request_kwargs(kwargs)
|
32
33
|
kwargs.setdefault("json", {"query": get_introspection_query()})
|
@@ -34,26 +35,30 @@ def from_wsgi(path: str, app: Any, **kwargs: Any) -> GraphQLSchema:
|
|
34
35
|
response = client.post(path=path, **kwargs)
|
35
36
|
raise_for_status(response)
|
36
37
|
schema = extract_schema_from_response(response, lambda r: r.json)
|
37
|
-
return from_dict(schema=schema).configure(app=app, location=path)
|
38
|
+
return from_dict(schema=schema, config=config).configure(app=app, location=path)
|
38
39
|
|
39
40
|
|
40
|
-
def from_url(
|
41
|
+
def from_url(
|
42
|
+
url: str, *, config: SchemathesisConfig | None = None, wait_for_schema: float | None = None, **kwargs: Any
|
43
|
+
) -> GraphQLSchema:
|
41
44
|
"""Load from URL."""
|
42
45
|
import requests
|
43
46
|
|
44
47
|
kwargs.setdefault("json", {"query": get_introspection_query()})
|
45
48
|
response = load_from_url(requests.post, url=url, wait_for_schema=wait_for_schema, **kwargs)
|
46
49
|
schema = extract_schema_from_response(response, lambda r: r.json())
|
47
|
-
return from_dict(schema).configure(location=url)
|
50
|
+
return from_dict(schema, config=config).configure(location=url)
|
48
51
|
|
49
52
|
|
50
|
-
def from_path(
|
53
|
+
def from_path(
|
54
|
+
path: PathLike | str, *, config: SchemathesisConfig | None = None, encoding: str = "utf-8"
|
55
|
+
) -> GraphQLSchema:
|
51
56
|
"""Load from a filesystem path."""
|
52
57
|
with open(path, encoding=encoding) as file:
|
53
|
-
return from_file(file=file).configure(location=Path(path).absolute().as_uri())
|
58
|
+
return from_file(file=file, config=config).configure(location=Path(path).absolute().as_uri())
|
54
59
|
|
55
60
|
|
56
|
-
def from_file(file: IO[str] | str) -> GraphQLSchema:
|
61
|
+
def from_file(file: IO[str] | str, *, config: SchemathesisConfig | None = None) -> GraphQLSchema:
|
57
62
|
"""Load from file-like object or string."""
|
58
63
|
import graphql
|
59
64
|
|
@@ -78,10 +83,10 @@ def from_file(file: IO[str] | str) -> GraphQLSchema:
|
|
78
83
|
_on_invalid_schema(exc)
|
79
84
|
except json.JSONDecodeError:
|
80
85
|
_on_invalid_schema(exc, extras=[entry for entry in str(exc).splitlines() if entry])
|
81
|
-
return from_dict(schema)
|
86
|
+
return from_dict(schema, config=config)
|
82
87
|
|
83
88
|
|
84
|
-
def from_dict(schema: dict[str, Any]) -> GraphQLSchema:
|
89
|
+
def from_dict(schema: dict[str, Any], *, config: SchemathesisConfig | None = None) -> GraphQLSchema:
|
85
90
|
"""Base loader that others build upon."""
|
86
91
|
from schemathesis.specs.graphql.schemas import GraphQLSchema
|
87
92
|
|
@@ -89,7 +94,11 @@ def from_dict(schema: dict[str, Any]) -> GraphQLSchema:
|
|
89
94
|
schema = schema["data"]
|
90
95
|
hook_context = HookContext()
|
91
96
|
dispatch("before_load_schema", hook_context, schema)
|
92
|
-
|
97
|
+
|
98
|
+
if config is None:
|
99
|
+
config = SchemathesisConfig.discover()
|
100
|
+
project_config = config.projects.get(schema)
|
101
|
+
instance = GraphQLSchema(schema, config=project_config)
|
93
102
|
dispatch("after_load_schema", hook_context, instance)
|
94
103
|
return instance
|
95
104
|
|
schemathesis/openapi/checks.py
CHANGED
@@ -4,8 +4,9 @@ import textwrap
|
|
4
4
|
from dataclasses import dataclass, field
|
5
5
|
from typing import TYPE_CHECKING, Any
|
6
6
|
|
7
|
+
from schemathesis.config import OutputConfig
|
7
8
|
from schemathesis.core.failures import Failure, Severity
|
8
|
-
from schemathesis.core.output import
|
9
|
+
from schemathesis.core.output import truncate_json
|
9
10
|
|
10
11
|
if TYPE_CHECKING:
|
11
12
|
from jsonschema import ValidationError
|
@@ -141,11 +142,14 @@ class JsonSchemaError(Failure):
|
|
141
142
|
title: str = "Response violates schema",
|
142
143
|
operation: str,
|
143
144
|
exc: ValidationError,
|
144
|
-
|
145
|
+
config: OutputConfig | None = None,
|
145
146
|
) -> JsonSchemaError:
|
146
|
-
|
147
|
-
|
148
|
-
|
147
|
+
schema = textwrap.indent(
|
148
|
+
truncate_json(exc.schema, config=config or OutputConfig(), max_lines=20), prefix=" "
|
149
|
+
)
|
150
|
+
value = textwrap.indent(
|
151
|
+
truncate_json(exc.instance, config=config or OutputConfig(), max_lines=20), prefix=" "
|
152
|
+
)
|
149
153
|
schema_path = list(exc.absolute_schema_path)
|
150
154
|
if len(schema_path) > 1:
|
151
155
|
# Exclude the last segment, which is already in the schema
|
@@ -336,7 +340,7 @@ class IgnoredAuth(Failure):
|
|
336
340
|
class AcceptedNegativeData(Failure):
|
337
341
|
"""Response with negative data was accepted."""
|
338
342
|
|
339
|
-
__slots__ = ("operation", "message", "status_code", "
|
343
|
+
__slots__ = ("operation", "message", "status_code", "expected_statuses", "title", "case_id", "severity")
|
340
344
|
|
341
345
|
def __init__(
|
342
346
|
self,
|
@@ -344,14 +348,14 @@ class AcceptedNegativeData(Failure):
|
|
344
348
|
operation: str,
|
345
349
|
message: str,
|
346
350
|
status_code: int,
|
347
|
-
|
351
|
+
expected_statuses: list[str],
|
348
352
|
title: str = "Accepted negative data",
|
349
353
|
case_id: str | None = None,
|
350
354
|
) -> None:
|
351
355
|
self.operation = operation
|
352
356
|
self.message = message
|
353
357
|
self.status_code = status_code
|
354
|
-
self.
|
358
|
+
self.expected_statuses = expected_statuses
|
355
359
|
self.title = title
|
356
360
|
self.case_id = case_id
|
357
361
|
self.severity = Severity.MEDIUM
|
@@ -1,4 +1,5 @@
|
|
1
1
|
from collections.abc import Mapping
|
2
|
+
from typing import Any
|
2
3
|
|
3
4
|
from schemathesis.core import NOT_SET
|
4
5
|
from schemathesis.core.validation import contains_unicode_surrogate_pair, has_invalid_characters, is_latin_1_encodable
|
@@ -21,14 +22,15 @@ def is_valid_path(parameters: dict[str, object]) -> bool:
|
|
21
22
|
In this case one variable in the path template will be empty, which will lead to 404 in most of the cases.
|
22
23
|
Because of it this case doesn't bring much value and might lead to false positives results of Schemathesis runs.
|
23
24
|
"""
|
24
|
-
return not any(
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
)
|
31
|
-
|
25
|
+
return not any(is_invalid_path_parameter(value) for value in parameters.values())
|
26
|
+
|
27
|
+
|
28
|
+
def is_invalid_path_parameter(value: Any) -> bool:
|
29
|
+
return (
|
30
|
+
value in ("/", "")
|
31
|
+
or contains_unicode_surrogate_pair(value)
|
32
|
+
or isinstance(value, str)
|
33
|
+
and ("/" in value or "}" in value or "{" in value)
|
32
34
|
)
|
33
35
|
|
34
36
|
|
schemathesis/openapi/loaders.py
CHANGED
@@ -7,6 +7,7 @@ from os import PathLike
|
|
7
7
|
from pathlib import Path
|
8
8
|
from typing import IO, TYPE_CHECKING, Any, Mapping
|
9
9
|
|
10
|
+
from schemathesis.config import SchemathesisConfig
|
10
11
|
from schemathesis.core import media_types
|
11
12
|
from schemathesis.core.deserialization import deserialize_yaml
|
12
13
|
from schemathesis.core.errors import LoaderError, LoaderErrorKind
|
@@ -18,16 +19,16 @@ if TYPE_CHECKING:
|
|
18
19
|
from schemathesis.specs.openapi.schemas import BaseOpenAPISchema
|
19
20
|
|
20
21
|
|
21
|
-
def from_asgi(path: str, app: Any, **kwargs: Any) -> BaseOpenAPISchema:
|
22
|
+
def from_asgi(path: str, app: Any, *, config: SchemathesisConfig | None = None, **kwargs: Any) -> BaseOpenAPISchema:
|
22
23
|
require_relative_url(path)
|
23
24
|
client = asgi.get_client(app)
|
24
25
|
response = load_from_url(client.get, url=path, **kwargs)
|
25
26
|
content_type = detect_content_type(headers=response.headers, path=path)
|
26
27
|
schema = load_content(response.text, content_type)
|
27
|
-
return from_dict(schema=schema).configure(app=app, location=path)
|
28
|
+
return from_dict(schema=schema, config=config).configure(app=app, location=path)
|
28
29
|
|
29
30
|
|
30
|
-
def from_wsgi(path: str, app: Any, **kwargs: Any) -> BaseOpenAPISchema:
|
31
|
+
def from_wsgi(path: str, app: Any, *, config: SchemathesisConfig | None = None, **kwargs: Any) -> BaseOpenAPISchema:
|
31
32
|
require_relative_url(path)
|
32
33
|
prepare_request_kwargs(kwargs)
|
33
34
|
client = wsgi.get_client(app)
|
@@ -35,28 +36,32 @@ def from_wsgi(path: str, app: Any, **kwargs: Any) -> BaseOpenAPISchema:
|
|
35
36
|
raise_for_status(response)
|
36
37
|
content_type = detect_content_type(headers=response.headers, path=path)
|
37
38
|
schema = load_content(response.text, content_type)
|
38
|
-
return from_dict(schema=schema).configure(app=app, location=path)
|
39
|
+
return from_dict(schema=schema, config=config).configure(app=app, location=path)
|
39
40
|
|
40
41
|
|
41
|
-
def from_url(
|
42
|
+
def from_url(
|
43
|
+
url: str, *, config: SchemathesisConfig | None = None, wait_for_schema: float | None = None, **kwargs: Any
|
44
|
+
) -> BaseOpenAPISchema:
|
42
45
|
"""Load from URL."""
|
43
46
|
import requests
|
44
47
|
|
45
48
|
response = load_from_url(requests.get, url=url, wait_for_schema=wait_for_schema, **kwargs)
|
46
49
|
content_type = detect_content_type(headers=response.headers, path=url)
|
47
50
|
schema = load_content(response.text, content_type)
|
48
|
-
return from_dict(schema=schema).configure(location=url)
|
51
|
+
return from_dict(schema=schema, config=config).configure(location=url)
|
49
52
|
|
50
53
|
|
51
|
-
def from_path(
|
54
|
+
def from_path(
|
55
|
+
path: PathLike | str, *, config: SchemathesisConfig | None = None, encoding: str = "utf-8"
|
56
|
+
) -> BaseOpenAPISchema:
|
52
57
|
"""Load from a filesystem path."""
|
53
58
|
with open(path, encoding=encoding) as file:
|
54
59
|
content_type = detect_content_type(headers=None, path=str(path))
|
55
60
|
schema = load_content(file.read(), content_type)
|
56
|
-
return from_dict(schema=schema).configure(location=Path(path).absolute().as_uri())
|
61
|
+
return from_dict(schema=schema, config=config).configure(location=Path(path).absolute().as_uri())
|
57
62
|
|
58
63
|
|
59
|
-
def from_file(file: IO[str] | str) -> BaseOpenAPISchema:
|
64
|
+
def from_file(file: IO[str] | str, *, config: SchemathesisConfig | None = None) -> BaseOpenAPISchema:
|
60
65
|
"""Load from file-like object or string."""
|
61
66
|
if isinstance(file, str):
|
62
67
|
data = file
|
@@ -66,10 +71,10 @@ def from_file(file: IO[str] | str) -> BaseOpenAPISchema:
|
|
66
71
|
schema = json.loads(data)
|
67
72
|
except json.JSONDecodeError:
|
68
73
|
schema = _load_yaml(data)
|
69
|
-
return from_dict(schema)
|
74
|
+
return from_dict(schema, config=config)
|
70
75
|
|
71
76
|
|
72
|
-
def from_dict(schema: dict[str, Any]) -> BaseOpenAPISchema:
|
77
|
+
def from_dict(schema: dict[str, Any], *, config: SchemathesisConfig | None = None) -> BaseOpenAPISchema:
|
73
78
|
"""Base loader that others build upon."""
|
74
79
|
from schemathesis.specs.openapi.schemas import OpenApi30, SwaggerV20
|
75
80
|
|
@@ -78,8 +83,12 @@ def from_dict(schema: dict[str, Any]) -> BaseOpenAPISchema:
|
|
78
83
|
hook_context = HookContext()
|
79
84
|
dispatch("before_load_schema", hook_context, schema)
|
80
85
|
|
86
|
+
if config is None:
|
87
|
+
config = SchemathesisConfig.discover()
|
88
|
+
project_config = config.projects.get(schema)
|
89
|
+
|
81
90
|
if "swagger" in schema:
|
82
|
-
instance = SwaggerV20(schema)
|
91
|
+
instance = SwaggerV20(raw_schema=schema, config=project_config)
|
83
92
|
elif "openapi" in schema:
|
84
93
|
version = schema["openapi"]
|
85
94
|
if not OPENAPI_VERSION_RE.match(version):
|
@@ -87,7 +96,7 @@ def from_dict(schema: dict[str, Any]) -> BaseOpenAPISchema:
|
|
87
96
|
LoaderErrorKind.OPEN_API_UNSUPPORTED_VERSION,
|
88
97
|
f"The provided schema uses Open API {version}, which is currently not supported.",
|
89
98
|
)
|
90
|
-
instance = OpenApi30(schema)
|
99
|
+
instance = OpenApi30(raw_schema=schema, config=project_config)
|
91
100
|
else:
|
92
101
|
raise LoaderError(
|
93
102
|
LoaderErrorKind.OPEN_API_UNSPECIFIED_VERSION,
|
schemathesis/pytest/lazy.py
CHANGED
@@ -12,7 +12,6 @@ from pytest_subtests import SubTests
|
|
12
12
|
from schemathesis.core.errors import InvalidSchema
|
13
13
|
from schemathesis.core.result import Ok, Result
|
14
14
|
from schemathesis.filters import FilterSet, FilterValue, MatcherFunc, RegexValue, is_deprecated
|
15
|
-
from schemathesis.generation import GenerationConfig
|
16
15
|
from schemathesis.generation.hypothesis.builder import HypothesisTestConfig, HypothesisTestMode, create_test
|
17
16
|
from schemathesis.generation.hypothesis.given import (
|
18
17
|
GivenArgsMark,
|
@@ -38,7 +37,6 @@ def get_all_tests(
|
|
38
37
|
*,
|
39
38
|
schema: BaseSchema,
|
40
39
|
test_func: Callable,
|
41
|
-
generation_config: GenerationConfig,
|
42
40
|
modes: list[HypothesisTestMode],
|
43
41
|
settings: hypothesis.settings | None = None,
|
44
42
|
seed: int | None = None,
|
@@ -46,7 +44,7 @@ def get_all_tests(
|
|
46
44
|
given_kwargs: dict[str, GivenInput] | None = None,
|
47
45
|
) -> Generator[Result[tuple[APIOperation, Callable], InvalidSchema], None, None]:
|
48
46
|
"""Generate all operations and Hypothesis tests for them."""
|
49
|
-
for result in schema.get_all_operations(
|
47
|
+
for result in schema.get_all_operations():
|
50
48
|
if isinstance(result, Ok):
|
51
49
|
operation = result.ok()
|
52
50
|
if callable(as_strategy_kwargs):
|
@@ -60,7 +58,7 @@ def get_all_tests(
|
|
60
58
|
settings=settings,
|
61
59
|
modes=modes,
|
62
60
|
seed=seed,
|
63
|
-
|
61
|
+
project=schema.config,
|
64
62
|
as_strategy_kwargs=_as_strategy_kwargs,
|
65
63
|
given_kwargs=given_kwargs or {},
|
66
64
|
),
|
@@ -194,7 +192,6 @@ class LazySchema:
|
|
194
192
|
test_func=test_func,
|
195
193
|
settings=settings,
|
196
194
|
modes=list(HypothesisTestMode),
|
197
|
-
generation_config=schema.generation_config,
|
198
195
|
as_strategy_kwargs=as_strategy_kwargs,
|
199
196
|
given_kwargs=given_kwargs,
|
200
197
|
)
|
schemathesis/pytest/plugin.py
CHANGED
@@ -134,13 +134,22 @@ class SchemathesisCase(PyCollector):
|
|
134
134
|
as_strategy_kwargs[location] = entry
|
135
135
|
else:
|
136
136
|
as_strategy_kwargs = {}
|
137
|
+
modes = []
|
138
|
+
phases = self.schema.config.phases_for(operation=operation)
|
139
|
+
if phases.examples.enabled:
|
140
|
+
modes.append(HypothesisTestMode.EXAMPLES)
|
141
|
+
if phases.fuzzing.enabled:
|
142
|
+
modes.append(HypothesisTestMode.FUZZING)
|
143
|
+
if phases.coverage.enabled:
|
144
|
+
modes.append(HypothesisTestMode.COVERAGE)
|
145
|
+
|
137
146
|
funcobj = create_test(
|
138
147
|
operation=operation,
|
139
148
|
test_func=self.test_function,
|
140
149
|
config=HypothesisTestConfig(
|
141
|
-
modes=
|
150
|
+
modes=modes,
|
142
151
|
given_kwargs=self.given_kwargs,
|
143
|
-
|
152
|
+
project=self.schema.config,
|
144
153
|
as_strategy_kwargs=as_strategy_kwargs,
|
145
154
|
),
|
146
155
|
)
|