schemathesis 4.0.0a12__py3-none-any.whl → 4.0.1__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 +9 -4
- schemathesis/auths.py +20 -30
- schemathesis/checks.py +5 -0
- schemathesis/cli/commands/run/__init__.py +9 -6
- schemathesis/cli/commands/run/handlers/output.py +13 -0
- schemathesis/cli/constants.py +1 -1
- schemathesis/config/_operations.py +16 -21
- schemathesis/config/_projects.py +5 -1
- schemathesis/core/errors.py +10 -17
- schemathesis/core/transport.py +81 -1
- schemathesis/engine/errors.py +1 -1
- schemathesis/generation/case.py +152 -28
- schemathesis/generation/hypothesis/builder.py +12 -12
- schemathesis/generation/overrides.py +11 -27
- schemathesis/generation/stateful/__init__.py +13 -0
- schemathesis/generation/stateful/state_machine.py +31 -108
- schemathesis/graphql/loaders.py +14 -4
- schemathesis/hooks.py +1 -4
- schemathesis/openapi/checks.py +82 -20
- schemathesis/openapi/generation/filters.py +9 -2
- schemathesis/openapi/loaders.py +14 -4
- schemathesis/pytest/lazy.py +4 -31
- schemathesis/pytest/plugin.py +21 -11
- schemathesis/schemas.py +153 -89
- schemathesis/specs/graphql/schemas.py +6 -6
- schemathesis/specs/openapi/_hypothesis.py +39 -14
- schemathesis/specs/openapi/checks.py +95 -34
- schemathesis/specs/openapi/expressions/nodes.py +1 -1
- schemathesis/specs/openapi/negative/__init__.py +5 -3
- schemathesis/specs/openapi/negative/mutations.py +2 -2
- schemathesis/specs/openapi/parameters.py +0 -3
- schemathesis/specs/openapi/schemas.py +6 -91
- schemathesis/specs/openapi/stateful/links.py +1 -63
- schemathesis/transport/requests.py +12 -1
- schemathesis/transport/serialization.py +0 -4
- schemathesis/transport/wsgi.py +7 -0
- {schemathesis-4.0.0a12.dist-info → schemathesis-4.0.1.dist-info}/METADATA +8 -10
- {schemathesis-4.0.0a12.dist-info → schemathesis-4.0.1.dist-info}/RECORD +41 -41
- {schemathesis-4.0.0a12.dist-info → schemathesis-4.0.1.dist-info}/WHEEL +0 -0
- {schemathesis-4.0.0a12.dist-info → schemathesis-4.0.1.dist-info}/entry_points.txt +0 -0
- {schemathesis-4.0.0a12.dist-info → schemathesis-4.0.1.dist-info}/licenses/LICENSE +0 -0
schemathesis/schemas.py
CHANGED
@@ -40,8 +40,11 @@ from .filters import (
|
|
40
40
|
from .hooks import GLOBAL_HOOK_DISPATCHER, HookContext, HookDispatcher, HookScope, dispatch, to_filterable_hook
|
41
41
|
|
42
42
|
if TYPE_CHECKING:
|
43
|
+
import httpx
|
44
|
+
import requests
|
43
45
|
from hypothesis.strategies import SearchStrategy
|
44
|
-
from
|
46
|
+
from requests.structures import CaseInsensitiveDict
|
47
|
+
from werkzeug.test import TestResponse
|
45
48
|
|
46
49
|
from schemathesis.core import Specification
|
47
50
|
from schemathesis.generation.stateful.state_machine import APIStateMachine
|
@@ -136,7 +139,25 @@ class BaseSchema(Mapping):
|
|
136
139
|
operation_id: FilterValue | None = None,
|
137
140
|
operation_id_regex: RegexValue | None = None,
|
138
141
|
) -> BaseSchema:
|
139
|
-
"""
|
142
|
+
"""Return a new schema containing only operations matching the specified criteria.
|
143
|
+
|
144
|
+
Args:
|
145
|
+
func: Custom filter function that accepts operation context.
|
146
|
+
name: Operation name(s) to include.
|
147
|
+
name_regex: Regex pattern for operation names.
|
148
|
+
method: HTTP method(s) to include.
|
149
|
+
method_regex: Regex pattern for HTTP methods.
|
150
|
+
path: API path(s) to include.
|
151
|
+
path_regex: Regex pattern for API paths.
|
152
|
+
tag: OpenAPI tag(s) to include.
|
153
|
+
tag_regex: Regex pattern for OpenAPI tags.
|
154
|
+
operation_id: Operation ID(s) to include.
|
155
|
+
operation_id_regex: Regex pattern for operation IDs.
|
156
|
+
|
157
|
+
Returns:
|
158
|
+
New schema instance with applied include filters.
|
159
|
+
|
160
|
+
"""
|
140
161
|
filter_set = self.filter_set.clone()
|
141
162
|
filter_set.include(
|
142
163
|
func,
|
@@ -169,7 +190,26 @@ class BaseSchema(Mapping):
|
|
169
190
|
operation_id_regex: RegexValue | None = None,
|
170
191
|
deprecated: bool = False,
|
171
192
|
) -> BaseSchema:
|
172
|
-
"""
|
193
|
+
"""Return a new schema excluding operations matching the specified criteria.
|
194
|
+
|
195
|
+
Args:
|
196
|
+
func: Custom filter function that accepts operation context.
|
197
|
+
name: Operation name(s) to exclude.
|
198
|
+
name_regex: Regex pattern for operation names.
|
199
|
+
method: HTTP method(s) to exclude.
|
200
|
+
method_regex: Regex pattern for HTTP methods.
|
201
|
+
path: API path(s) to exclude.
|
202
|
+
path_regex: Regex pattern for API paths.
|
203
|
+
tag: OpenAPI tag(s) to exclude.
|
204
|
+
tag_regex: Regex pattern for OpenAPI tags.
|
205
|
+
operation_id: Operation ID(s) to exclude.
|
206
|
+
operation_id_regex: Regex pattern for operation IDs.
|
207
|
+
deprecated: Whether to exclude deprecated operations.
|
208
|
+
|
209
|
+
Returns:
|
210
|
+
New schema instance with applied exclude filters.
|
211
|
+
|
212
|
+
"""
|
173
213
|
filter_set = self.filter_set.clone()
|
174
214
|
if deprecated:
|
175
215
|
if func is None:
|
@@ -211,10 +251,15 @@ class BaseSchema(Mapping):
|
|
211
251
|
return self.statistic.operations.total
|
212
252
|
|
213
253
|
def hook(self, hook: str | Callable) -> Callable:
|
254
|
+
"""Register a hook function for this schema only.
|
255
|
+
|
256
|
+
Args:
|
257
|
+
hook: Hook name string or hook function to register.
|
258
|
+
|
259
|
+
"""
|
214
260
|
return self.hooks.hook(hook)
|
215
261
|
|
216
262
|
def get_full_path(self, path: str) -> str:
|
217
|
-
"""Compute full path for the given path."""
|
218
263
|
return get_full_path(self.base_path, path)
|
219
264
|
|
220
265
|
@property
|
@@ -258,19 +303,27 @@ class BaseSchema(Mapping):
|
|
258
303
|
raise NotImplementedError
|
259
304
|
|
260
305
|
def get_strategies_from_examples(self, operation: APIOperation, **kwargs: Any) -> list[SearchStrategy[Case]]:
|
261
|
-
"""Get examples from the API operation."""
|
262
306
|
raise NotImplementedError
|
263
307
|
|
264
308
|
def get_security_requirements(self, operation: APIOperation) -> list[str]:
|
265
|
-
"""Get applied security requirements for the given API operation."""
|
266
309
|
raise NotImplementedError
|
267
310
|
|
268
311
|
def get_parameter_serializer(self, operation: APIOperation, location: str) -> Callable | None:
|
269
|
-
"""Get a function that serializes parameters for the given location."""
|
270
312
|
raise NotImplementedError
|
271
313
|
|
272
314
|
def parametrize(self) -> Callable:
|
273
|
-
"""
|
315
|
+
"""Return a decorator that marks a test function for `pytest` parametrization.
|
316
|
+
|
317
|
+
The decorated test function will be parametrized with test cases generated
|
318
|
+
from the schema's API operations.
|
319
|
+
|
320
|
+
Returns:
|
321
|
+
Decorator function for test parametrization.
|
322
|
+
|
323
|
+
Raises:
|
324
|
+
IncorrectUsage: If applied to the same function multiple times.
|
325
|
+
|
326
|
+
"""
|
274
327
|
|
275
328
|
def wrapper(func: Callable) -> Callable:
|
276
329
|
from schemathesis.pytest.plugin import SchemaHandleMark
|
@@ -293,7 +346,13 @@ class BaseSchema(Mapping):
|
|
293
346
|
return wrapper
|
294
347
|
|
295
348
|
def given(self, *args: GivenInput, **kwargs: GivenInput) -> Callable:
|
296
|
-
"""Proxy Hypothesis
|
349
|
+
"""Proxy to Hypothesis's `given` decorator for adding custom strategies.
|
350
|
+
|
351
|
+
Args:
|
352
|
+
*args: Positional arguments passed to `hypothesis.given`.
|
353
|
+
**kwargs: Keyword arguments passed to `hypothesis.given`.
|
354
|
+
|
355
|
+
"""
|
297
356
|
return given_proxy(*args, **kwargs)
|
298
357
|
|
299
358
|
def clone(
|
@@ -320,7 +379,6 @@ class BaseSchema(Mapping):
|
|
320
379
|
)
|
321
380
|
|
322
381
|
def get_local_hook_dispatcher(self) -> HookDispatcher | None:
|
323
|
-
"""Get a HookDispatcher instance bound to the test if present."""
|
324
382
|
# It might be not present when it is used without pytest via `APIOperation.as_strategy()`
|
325
383
|
if self.test_function is not None:
|
326
384
|
# Might be missing it in case of `LazySchema` usage
|
@@ -328,7 +386,6 @@ class BaseSchema(Mapping):
|
|
328
386
|
return None
|
329
387
|
|
330
388
|
def dispatch_hook(self, name: str, context: HookContext, *args: Any, **kwargs: Any) -> None:
|
331
|
-
"""Dispatch a hook via all available dispatchers."""
|
332
389
|
dispatch(name, context, *args, **kwargs)
|
333
390
|
self.hooks.dispatch(name, context, *args, **kwargs)
|
334
391
|
local_dispatcher = self.get_local_hook_dispatcher()
|
@@ -338,10 +395,6 @@ class BaseSchema(Mapping):
|
|
338
395
|
def prepare_multipart(
|
339
396
|
self, form_data: dict[str, Any], operation: APIOperation
|
340
397
|
) -> tuple[list | None, dict[str, Any] | None]:
|
341
|
-
"""Split content of `form_data` into files & data.
|
342
|
-
|
343
|
-
Forms may contain file fields, that we should send via `files` argument in `requests`.
|
344
|
-
"""
|
345
398
|
raise NotImplementedError
|
346
399
|
|
347
400
|
def get_request_payload_content_types(self, operation: APIOperation) -> list[str]:
|
@@ -354,7 +407,7 @@ class BaseSchema(Mapping):
|
|
354
407
|
method: str | None = None,
|
355
408
|
path: str | None = None,
|
356
409
|
path_parameters: dict[str, Any] | None = None,
|
357
|
-
headers: dict[str, Any] | None = None,
|
410
|
+
headers: dict[str, Any] | CaseInsensitiveDict | None = None,
|
358
411
|
cookies: dict[str, Any] | None = None,
|
359
412
|
query: dict[str, Any] | None = None,
|
360
413
|
body: list | dict[str, Any] | str | int | float | bool | bytes | NotSet = NOT_SET,
|
@@ -374,7 +427,12 @@ class BaseSchema(Mapping):
|
|
374
427
|
raise NotImplementedError
|
375
428
|
|
376
429
|
def as_state_machine(self) -> type[APIStateMachine]:
|
377
|
-
"""Create a state machine class.
|
430
|
+
"""Create a state machine class for stateful testing of linked API operations.
|
431
|
+
|
432
|
+
Returns:
|
433
|
+
APIStateMachine subclass configured for this schema.
|
434
|
+
|
435
|
+
"""
|
378
436
|
raise NotImplementedError
|
379
437
|
|
380
438
|
def get_links(self, operation: APIOperation) -> dict[str, dict[str, Any]]:
|
@@ -394,36 +452,28 @@ class BaseSchema(Mapping):
|
|
394
452
|
|
395
453
|
def as_strategy(
|
396
454
|
self,
|
397
|
-
hooks: HookDispatcher | None = None,
|
398
|
-
auth_storage: AuthStorage | None = None,
|
399
455
|
generation_mode: GenerationMode = GenerationMode.POSITIVE,
|
400
456
|
**kwargs: Any,
|
401
457
|
) -> SearchStrategy:
|
402
|
-
"""
|
458
|
+
"""Create a Hypothesis strategy that generates test cases for all schema operations.
|
459
|
+
|
460
|
+
Use with `@given` in non-Schemathesis tests.
|
461
|
+
|
462
|
+
Args:
|
463
|
+
generation_mode: Whether to generate positive or negative test data.
|
464
|
+
**kwargs: Additional keywords for each strategy.
|
465
|
+
|
466
|
+
Returns:
|
467
|
+
Combined Hypothesis strategy for all valid operations in the schema.
|
468
|
+
|
469
|
+
"""
|
403
470
|
_strategies = [
|
404
|
-
operation.ok().as_strategy(
|
405
|
-
hooks=hooks,
|
406
|
-
auth_storage=auth_storage,
|
407
|
-
generation_mode=generation_mode,
|
408
|
-
**kwargs,
|
409
|
-
)
|
471
|
+
operation.ok().as_strategy(generation_mode=generation_mode, **kwargs)
|
410
472
|
for operation in self.get_all_operations()
|
411
473
|
if isinstance(operation, Ok)
|
412
474
|
]
|
413
475
|
return strategies.combine(_strategies)
|
414
476
|
|
415
|
-
def configure(
|
416
|
-
self,
|
417
|
-
*,
|
418
|
-
location: str | None | NotSet = NOT_SET,
|
419
|
-
app: Any | NotSet = NOT_SET,
|
420
|
-
) -> Self:
|
421
|
-
if not isinstance(location, NotSet):
|
422
|
-
self.location = location
|
423
|
-
if not isinstance(app, NotSet):
|
424
|
-
self.app = app
|
425
|
-
return self
|
426
|
-
|
427
477
|
def find_operation_by_label(self, label: str) -> APIOperation | None:
|
428
478
|
raise NotImplementedError
|
429
479
|
|
@@ -444,20 +494,23 @@ class APIOperationMap(Mapping):
|
|
444
494
|
|
445
495
|
def as_strategy(
|
446
496
|
self,
|
447
|
-
hooks: HookDispatcher | None = None,
|
448
|
-
auth_storage: AuthStorage | None = None,
|
449
497
|
generation_mode: GenerationMode = GenerationMode.POSITIVE,
|
450
498
|
**kwargs: Any,
|
451
499
|
) -> SearchStrategy:
|
452
|
-
"""
|
500
|
+
"""Create a Hypothesis strategy that generates test cases for all schema operations in this subset.
|
501
|
+
|
502
|
+
Use with `@given` in non-Schemathesis tests.
|
503
|
+
|
504
|
+
Args:
|
505
|
+
generation_mode: Whether to generate positive or negative test data.
|
506
|
+
**kwargs: Additional keywords for each strategy.
|
507
|
+
|
508
|
+
Returns:
|
509
|
+
Combined Hypothesis strategy for all valid operations in the schema.
|
510
|
+
|
511
|
+
"""
|
453
512
|
_strategies = [
|
454
|
-
operation.as_strategy(
|
455
|
-
hooks=hooks,
|
456
|
-
auth_storage=auth_storage,
|
457
|
-
generation_mode=generation_mode,
|
458
|
-
**kwargs,
|
459
|
-
)
|
460
|
-
for operation in self._data.values()
|
513
|
+
operation.as_strategy(generation_mode=generation_mode, **kwargs) for operation in self._data.values()
|
461
514
|
]
|
462
515
|
return strategies.combine(_strategies)
|
463
516
|
|
@@ -563,16 +616,7 @@ class OperationDefinition(Generic[D]):
|
|
563
616
|
|
564
617
|
@dataclass(eq=False)
|
565
618
|
class APIOperation(Generic[P]):
|
566
|
-
"""
|
567
|
-
|
568
|
-
You can get one via a ``schema`` instance.
|
569
|
-
|
570
|
-
.. code-block:: python
|
571
|
-
|
572
|
-
# Get the POST /items operation
|
573
|
-
operation = schema["/items"]["POST"]
|
574
|
-
|
575
|
-
"""
|
619
|
+
"""An API operation (e.g., `GET /users`)."""
|
576
620
|
|
577
621
|
# `path` does not contain `basePath`
|
578
622
|
# Example <scheme>://<host>/<basePath>/users - "/users" is path
|
@@ -610,7 +654,6 @@ class APIOperation(Generic[P]):
|
|
610
654
|
return self.schema.get_tags(self)
|
611
655
|
|
612
656
|
def iter_parameters(self) -> Iterator[P]:
|
613
|
-
"""Iterate over all operation's parameters."""
|
614
657
|
return chain(self.path_parameters, self.headers, self.cookies, self.query)
|
615
658
|
|
616
659
|
def _lookup_container(self, location: str) -> ParameterSet[P] | PayloadAlternatives[P] | None:
|
@@ -623,11 +666,6 @@ class APIOperation(Generic[P]):
|
|
623
666
|
}.get(location)
|
624
667
|
|
625
668
|
def add_parameter(self, parameter: P) -> None:
|
626
|
-
"""Add a new processed parameter to an API operation.
|
627
|
-
|
628
|
-
:param parameter: A parameter that will be used with this operation.
|
629
|
-
:rtype: None
|
630
|
-
"""
|
631
669
|
# If the parameter has a typo, then by default, there will be an error from `jsonschema` earlier.
|
632
670
|
# But if the user wants to skip schema validation, we choose to ignore a malformed parameter.
|
633
671
|
# In this case, we still might generate some tests for an API operation, but without this parameter,
|
@@ -644,13 +682,19 @@ class APIOperation(Generic[P]):
|
|
644
682
|
|
645
683
|
def as_strategy(
|
646
684
|
self,
|
647
|
-
hooks: HookDispatcher | None = None,
|
648
|
-
auth_storage: AuthStorage | None = None,
|
649
685
|
generation_mode: GenerationMode = GenerationMode.POSITIVE,
|
650
686
|
**kwargs: Any,
|
651
687
|
) -> SearchStrategy[Case]:
|
652
|
-
"""
|
653
|
-
|
688
|
+
"""Create a Hypothesis strategy that generates test cases for this API operation.
|
689
|
+
|
690
|
+
Use with `@given` in non-Schemathesis tests.
|
691
|
+
|
692
|
+
Args:
|
693
|
+
generation_mode: Whether to generate positive or negative test data.
|
694
|
+
**kwargs: Extra arguments to the underlying strategy function.
|
695
|
+
|
696
|
+
"""
|
697
|
+
strategy = self.schema.get_case_strategy(self, generation_mode=generation_mode, **kwargs)
|
654
698
|
|
655
699
|
def _apply_hooks(dispatcher: HookDispatcher, _strategy: SearchStrategy[Case]) -> SearchStrategy[Case]:
|
656
700
|
context = HookContext(operation=self)
|
@@ -669,6 +713,7 @@ class APIOperation(Generic[P]):
|
|
669
713
|
|
670
714
|
strategy = _apply_hooks(GLOBAL_HOOK_DISPATCHER, strategy)
|
671
715
|
strategy = _apply_hooks(self.schema.hooks, strategy)
|
716
|
+
hooks = kwargs.get("hooks")
|
672
717
|
if hooks is not None:
|
673
718
|
strategy = _apply_hooks(hooks, strategy)
|
674
719
|
return strategy
|
@@ -677,15 +722,9 @@ class APIOperation(Generic[P]):
|
|
677
722
|
return self.schema.get_security_requirements(self)
|
678
723
|
|
679
724
|
def get_strategies_from_examples(self, **kwargs: Any) -> list[SearchStrategy[Case]]:
|
680
|
-
"""Get examples from the API operation."""
|
681
725
|
return self.schema.get_strategies_from_examples(self, **kwargs)
|
682
726
|
|
683
727
|
def get_parameter_serializer(self, location: str) -> Callable | None:
|
684
|
-
"""Get a function that serializes parameters for the given location.
|
685
|
-
|
686
|
-
It handles serializing data into various `collectionFormat` options and similar.
|
687
|
-
Note that payload is handled by this function - it is handled by serializers.
|
688
|
-
"""
|
689
728
|
return self.schema.get_parameter_serializer(self, location)
|
690
729
|
|
691
730
|
def prepare_multipart(self, form_data: dict[str, Any]) -> tuple[list | None, dict[str, Any] | None]:
|
@@ -712,27 +751,37 @@ class APIOperation(Generic[P]):
|
|
712
751
|
*,
|
713
752
|
method: str | None = None,
|
714
753
|
path_parameters: dict[str, Any] | None = None,
|
715
|
-
headers: dict[str, Any] | None = None,
|
754
|
+
headers: dict[str, Any] | CaseInsensitiveDict | None = None,
|
716
755
|
cookies: dict[str, Any] | None = None,
|
717
756
|
query: dict[str, Any] | None = None,
|
718
757
|
body: list | dict[str, Any] | str | int | float | bool | bytes | NotSet = NOT_SET,
|
719
758
|
media_type: str | None = None,
|
720
|
-
|
759
|
+
_meta: CaseMetadata | None = None,
|
721
760
|
) -> Case:
|
722
|
-
"""Create a
|
761
|
+
"""Create a test case with specific data instead of generated values.
|
762
|
+
|
763
|
+
Args:
|
764
|
+
method: Override HTTP method.
|
765
|
+
path_parameters: Override path variables.
|
766
|
+
headers: Override HTTP headers.
|
767
|
+
cookies: Override cookies.
|
768
|
+
query: Override query parameters.
|
769
|
+
body: Override request body.
|
770
|
+
media_type: Override media type.
|
723
771
|
|
724
|
-
The main use case is constructing Case instances completely manually, without data generation.
|
725
772
|
"""
|
773
|
+
from requests.structures import CaseInsensitiveDict
|
774
|
+
|
726
775
|
return self.schema.make_case(
|
727
776
|
operation=self,
|
728
777
|
method=method,
|
729
|
-
path_parameters=path_parameters,
|
730
|
-
headers=headers,
|
731
|
-
cookies=cookies,
|
732
|
-
query=query,
|
778
|
+
path_parameters=path_parameters or {},
|
779
|
+
headers=CaseInsensitiveDict() if headers is None else CaseInsensitiveDict(headers),
|
780
|
+
cookies=cookies or {},
|
781
|
+
query=query or {},
|
733
782
|
body=body,
|
734
783
|
media_type=media_type,
|
735
|
-
meta=
|
784
|
+
meta=_meta,
|
736
785
|
)
|
737
786
|
|
738
787
|
@property
|
@@ -740,15 +789,30 @@ class APIOperation(Generic[P]):
|
|
740
789
|
path = self.path.replace("~", "~0").replace("/", "~1")
|
741
790
|
return f"#/paths/{path}/{self.method}"
|
742
791
|
|
743
|
-
def validate_response(self, response: Response) -> bool | None:
|
744
|
-
"""Validate
|
792
|
+
def validate_response(self, response: Response | httpx.Response | requests.Response | TestResponse) -> bool | None:
|
793
|
+
"""Validate a response against the API schema.
|
794
|
+
|
795
|
+
Args:
|
796
|
+
response: The HTTP response to validate. Can be a `requests.Response`,
|
797
|
+
`httpx.Response`, `werkzeug.test.TestResponse`, or `schemathesis.Response`.
|
798
|
+
|
799
|
+
Raises:
|
800
|
+
FailureGroup: If the response does not conform to the schema.
|
745
801
|
|
746
|
-
:raises FailureGroup: If the response does not conform to the API schema.
|
747
802
|
"""
|
748
|
-
return self.schema.validate_response(self, response)
|
803
|
+
return self.schema.validate_response(self, Response.from_any(response))
|
804
|
+
|
805
|
+
def is_valid_response(self, response: Response | httpx.Response | requests.Response | TestResponse) -> bool:
|
806
|
+
"""Check if the provided response is valid against the API schema.
|
749
807
|
|
750
|
-
|
751
|
-
|
808
|
+
Args:
|
809
|
+
response: The HTTP response to validate. Can be a `requests.Response`,
|
810
|
+
`httpx.Response`, `werkzeug.test.TestResponse`, or `schemathesis.Response`.
|
811
|
+
|
812
|
+
Returns:
|
813
|
+
`True` if response is valid, `False` otherwise.
|
814
|
+
|
815
|
+
"""
|
752
816
|
try:
|
753
817
|
self.validate_response(response)
|
754
818
|
return True
|
@@ -253,7 +253,7 @@ class GraphQLSchema(BaseSchema):
|
|
253
253
|
method: str | None = None,
|
254
254
|
path: str | None = None,
|
255
255
|
path_parameters: dict[str, Any] | None = None,
|
256
|
-
headers: dict[str, Any] | None = None,
|
256
|
+
headers: dict[str, Any] | CaseInsensitiveDict | None = None,
|
257
257
|
cookies: dict[str, Any] | None = None,
|
258
258
|
query: dict[str, Any] | None = None,
|
259
259
|
body: list | dict[str, Any] | str | int | float | bool | bytes | NotSet = NOT_SET,
|
@@ -264,10 +264,10 @@ class GraphQLSchema(BaseSchema):
|
|
264
264
|
operation=operation,
|
265
265
|
method=method or operation.method.upper(),
|
266
266
|
path=path or operation.path,
|
267
|
-
path_parameters=path_parameters,
|
268
|
-
headers=CaseInsensitiveDict(
|
269
|
-
cookies=cookies,
|
270
|
-
query=query,
|
267
|
+
path_parameters=path_parameters or {},
|
268
|
+
headers=CaseInsensitiveDict() if headers is None else CaseInsensitiveDict(headers),
|
269
|
+
cookies=cookies or {},
|
270
|
+
query=query or {},
|
271
271
|
body=body,
|
272
272
|
media_type=media_type or "application/json",
|
273
273
|
meta=meta,
|
@@ -376,7 +376,7 @@ def graphql_cases(
|
|
376
376
|
cookies=cookies_,
|
377
377
|
query=query_,
|
378
378
|
body=body,
|
379
|
-
|
379
|
+
_meta=CaseMetadata(
|
380
380
|
generation=GenerationInfo(
|
381
381
|
time=time.monotonic() - start,
|
382
382
|
mode=generation_mode,
|
@@ -7,9 +7,11 @@ from typing import Any, Callable, Dict, Iterable, Optional, Union, cast
|
|
7
7
|
from urllib.parse import quote_plus
|
8
8
|
from weakref import WeakKeyDictionary
|
9
9
|
|
10
|
+
import jsonschema.protocols
|
10
11
|
from hypothesis import event, note, reject
|
11
12
|
from hypothesis import strategies as st
|
12
13
|
from hypothesis_jsonschema import from_schema
|
14
|
+
from requests.structures import CaseInsensitiveDict
|
13
15
|
|
14
16
|
from schemathesis.config import GenerationConfig
|
15
17
|
from schemathesis.core import NOT_SET, NotSet, media_types
|
@@ -42,7 +44,9 @@ from .parameters import OpenAPIBody, OpenAPIParameter, parameters_to_json_schema
|
|
42
44
|
from .utils import is_header_location
|
43
45
|
|
44
46
|
SLASH = "/"
|
45
|
-
StrategyFactory = Callable[
|
47
|
+
StrategyFactory = Callable[
|
48
|
+
[Dict[str, Any], str, str, Optional[str], GenerationConfig, type[jsonschema.protocols.Validator]], st.SearchStrategy
|
49
|
+
]
|
46
50
|
|
47
51
|
|
48
52
|
@st.composite # type: ignore
|
@@ -154,12 +158,12 @@ def openapi_cases(
|
|
154
158
|
|
155
159
|
instance = operation.Case(
|
156
160
|
media_type=media_type,
|
157
|
-
path_parameters=path_parameters_.value,
|
158
|
-
headers=headers_.value,
|
159
|
-
cookies=cookies_.value,
|
160
|
-
query=query_.value,
|
161
|
+
path_parameters=path_parameters_.value or {},
|
162
|
+
headers=headers_.value or CaseInsensitiveDict(),
|
163
|
+
cookies=cookies_.value or {},
|
164
|
+
query=query_.value or {},
|
161
165
|
body=body_.value,
|
162
|
-
|
166
|
+
_meta=CaseMetadata(
|
163
167
|
generation=GenerationInfo(
|
164
168
|
time=time.monotonic() - start,
|
165
169
|
mode=generation_mode,
|
@@ -195,6 +199,8 @@ def _get_body_strategy(
|
|
195
199
|
operation: APIOperation,
|
196
200
|
generation_config: GenerationConfig,
|
197
201
|
) -> st.SearchStrategy:
|
202
|
+
from schemathesis.specs.openapi.schemas import BaseOpenAPISchema
|
203
|
+
|
198
204
|
if parameter.media_type in MEDIA_TYPES:
|
199
205
|
return MEDIA_TYPES[parameter.media_type]
|
200
206
|
# The cache key relies on object ids, which means that the parameter should not be mutated
|
@@ -203,7 +209,10 @@ def _get_body_strategy(
|
|
203
209
|
return _BODY_STRATEGIES_CACHE[parameter][strategy_factory]
|
204
210
|
schema = parameter.as_json_schema(operation)
|
205
211
|
schema = operation.schema.prepare_schema(schema)
|
206
|
-
|
212
|
+
assert isinstance(operation.schema, BaseOpenAPISchema)
|
213
|
+
strategy = strategy_factory(
|
214
|
+
schema, operation.label, "body", parameter.media_type, generation_config, operation.schema.validator_cls
|
215
|
+
)
|
207
216
|
if not parameter.is_required:
|
208
217
|
strategy |= st.just(NOT_SET)
|
209
218
|
_BODY_STRATEGIES_CACHE.setdefault(parameter, {})[strategy_factory] = strategy
|
@@ -337,6 +346,8 @@ def get_parameters_strategy(
|
|
337
346
|
exclude: Iterable[str] = (),
|
338
347
|
) -> st.SearchStrategy:
|
339
348
|
"""Create a new strategy for the case's component from the API operation parameters."""
|
349
|
+
from schemathesis.specs.openapi.schemas import BaseOpenAPISchema
|
350
|
+
|
340
351
|
parameters = getattr(operation, LOCATION_TO_CONTAINER[location])
|
341
352
|
if parameters:
|
342
353
|
# The cache key relies on object ids, which means that the parameter should not be mutated
|
@@ -344,17 +355,28 @@ def get_parameters_strategy(
|
|
344
355
|
if operation in _PARAMETER_STRATEGIES_CACHE and nested_cache_key in _PARAMETER_STRATEGIES_CACHE[operation]:
|
345
356
|
return _PARAMETER_STRATEGIES_CACHE[operation][nested_cache_key]
|
346
357
|
schema = get_schema_for_location(operation, location, parameters)
|
347
|
-
|
348
|
-
#
|
349
|
-
|
350
|
-
schema["properties"]
|
351
|
-
|
352
|
-
|
358
|
+
if location == "header" and exclude:
|
359
|
+
# Remove excluded headers case-insensitively
|
360
|
+
exclude_lower = {name.lower() for name in exclude}
|
361
|
+
schema["properties"] = {
|
362
|
+
key: value for key, value in schema["properties"].items() if key.lower() not in exclude_lower
|
363
|
+
}
|
364
|
+
if "required" in schema:
|
365
|
+
schema["required"] = [key for key in schema["required"] if key.lower() not in exclude_lower]
|
366
|
+
elif exclude:
|
367
|
+
# Non-header locations: remove by exact name
|
368
|
+
for name in exclude:
|
369
|
+
schema["properties"].pop(name, None)
|
370
|
+
with suppress(ValueError):
|
371
|
+
schema["required"].remove(name)
|
353
372
|
if not schema["properties"] and strategy_factory is make_negative_strategy:
|
354
373
|
# Nothing to negate - all properties were excluded
|
355
374
|
strategy = st.none()
|
356
375
|
else:
|
357
|
-
|
376
|
+
assert isinstance(operation.schema, BaseOpenAPISchema)
|
377
|
+
strategy = strategy_factory(
|
378
|
+
schema, operation.label, location, None, generation_config, operation.schema.validator_cls
|
379
|
+
)
|
358
380
|
serialize = operation.get_parameter_serializer(location)
|
359
381
|
if serialize is not None:
|
360
382
|
strategy = strategy.map(serialize)
|
@@ -418,6 +440,7 @@ def make_positive_strategy(
|
|
418
440
|
location: str,
|
419
441
|
media_type: str | None,
|
420
442
|
generation_config: GenerationConfig,
|
443
|
+
validator_cls: type[jsonschema.protocols.Validator],
|
421
444
|
custom_formats: dict[str, st.SearchStrategy] | None = None,
|
422
445
|
) -> st.SearchStrategy:
|
423
446
|
"""Strategy for generating values that fit the schema."""
|
@@ -448,6 +471,7 @@ def make_negative_strategy(
|
|
448
471
|
location: str,
|
449
472
|
media_type: str | None,
|
450
473
|
generation_config: GenerationConfig,
|
474
|
+
validator_cls: type[jsonschema.protocols.Validator],
|
451
475
|
custom_formats: dict[str, st.SearchStrategy] | None = None,
|
452
476
|
) -> st.SearchStrategy:
|
453
477
|
custom_formats = _build_custom_formats(custom_formats, generation_config)
|
@@ -458,6 +482,7 @@ def make_negative_strategy(
|
|
458
482
|
media_type=media_type,
|
459
483
|
custom_formats=custom_formats,
|
460
484
|
generation_config=generation_config,
|
485
|
+
validator_cls=validator_cls,
|
461
486
|
)
|
462
487
|
|
463
488
|
|