schemathesis 3.30.4__py3-none-any.whl → 3.31.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/cli/__init__.py +27 -2
- schemathesis/cli/callbacks.py +5 -0
- schemathesis/cli/cassettes.py +150 -12
- schemathesis/exceptions.py +3 -1
- schemathesis/generation/__init__.py +2 -0
- schemathesis/models.py +25 -1
- schemathesis/runner/events.py +2 -0
- schemathesis/runner/impl/core.py +43 -0
- schemathesis/service/serialization.py +24 -1
- schemathesis/specs/openapi/_hypothesis.py +26 -13
- schemathesis/specs/openapi/checks.py +34 -1
- schemathesis/specs/openapi/negative/mutations.py +9 -2
- schemathesis/specs/openapi/references.py +19 -0
- schemathesis/specs/openapi/schemas.py +24 -13
- schemathesis/specs/openapi/security.py +16 -3
- schemathesis/stateful/context.py +7 -2
- schemathesis/stateful/events.py +16 -4
- schemathesis/stateful/runner.py +6 -11
- schemathesis/stateful/sink.py +1 -1
- schemathesis/stateful/validation.py +24 -12
- {schemathesis-3.30.4.dist-info → schemathesis-3.31.1.dist-info}/METADATA +2 -1
- {schemathesis-3.30.4.dist-info → schemathesis-3.31.1.dist-info}/RECORD +25 -25
- {schemathesis-3.30.4.dist-info → schemathesis-3.31.1.dist-info}/WHEEL +0 -0
- {schemathesis-3.30.4.dist-info → schemathesis-3.31.1.dist-info}/entry_points.txt +0 -0
- {schemathesis-3.30.4.dist-info → schemathesis-3.31.1.dist-info}/licenses/LICENSE +0 -0
schemathesis/cli/__init__.py
CHANGED
|
@@ -522,6 +522,13 @@ The report data, consisting of a tar gz file with multiple JSON files, is subjec
|
|
|
522
522
|
type=click.File("w", encoding="utf-8"),
|
|
523
523
|
is_eager=True,
|
|
524
524
|
)
|
|
525
|
+
@click.option(
|
|
526
|
+
"--cassette-format",
|
|
527
|
+
help="Format of the saved cassettes.",
|
|
528
|
+
type=click.Choice([item.name.lower() for item in cassettes.CassetteFormat]),
|
|
529
|
+
default=cassettes.CassetteFormat.VCR.name.lower(),
|
|
530
|
+
callback=callbacks.convert_cassette_format,
|
|
531
|
+
)
|
|
525
532
|
@click.option(
|
|
526
533
|
"--cassette-preserve-exact-body-bytes",
|
|
527
534
|
help="Retains exact byte sequence of payloads in cassettes, encoded as base64.",
|
|
@@ -718,6 +725,14 @@ The report data, consisting of a tar gz file with multiple JSON files, is subjec
|
|
|
718
725
|
default="utf-8",
|
|
719
726
|
callback=callbacks.validate_generation_codec,
|
|
720
727
|
)
|
|
728
|
+
@click.option(
|
|
729
|
+
"--generation-with-security-parameters",
|
|
730
|
+
help="Whether to generate security parameters.",
|
|
731
|
+
type=str,
|
|
732
|
+
default="true",
|
|
733
|
+
show_default=True,
|
|
734
|
+
callback=callbacks.convert_boolean_string,
|
|
735
|
+
)
|
|
721
736
|
@click.option(
|
|
722
737
|
"--schemathesis-io-token",
|
|
723
738
|
help="Schemathesis.io authentication token.",
|
|
@@ -783,6 +798,7 @@ def run(
|
|
|
783
798
|
show_trace: bool = False,
|
|
784
799
|
code_sample_style: CodeSampleStyle = CodeSampleStyle.default(),
|
|
785
800
|
cassette_path: click.utils.LazyFile | None = None,
|
|
801
|
+
cassette_format: cassettes.CassetteFormat = cassettes.CassetteFormat.VCR,
|
|
786
802
|
cassette_preserve_exact_body_bytes: bool = False,
|
|
787
803
|
store_network_log: click.utils.LazyFile | None = None,
|
|
788
804
|
wait_for_schema: float | None = None,
|
|
@@ -810,6 +826,7 @@ def run(
|
|
|
810
826
|
no_color: bool = False,
|
|
811
827
|
report_value: str | None = None,
|
|
812
828
|
generation_allow_x00: bool = True,
|
|
829
|
+
generation_with_security_parameters: bool = True,
|
|
813
830
|
generation_codec: str = "utf-8",
|
|
814
831
|
schemathesis_io_token: str | None = None,
|
|
815
832
|
schemathesis_io_url: str = service.DEFAULT_URL,
|
|
@@ -849,7 +866,11 @@ def run(
|
|
|
849
866
|
|
|
850
867
|
override = CaseOverride(query=set_query, headers=set_header, cookies=set_cookie, path_parameters=set_path)
|
|
851
868
|
|
|
852
|
-
generation_config = generation.GenerationConfig(
|
|
869
|
+
generation_config = generation.GenerationConfig(
|
|
870
|
+
allow_x00=generation_allow_x00,
|
|
871
|
+
codec=generation_codec,
|
|
872
|
+
with_security_parameters=generation_with_security_parameters,
|
|
873
|
+
)
|
|
853
874
|
|
|
854
875
|
report: ReportToService | click.utils.LazyFile | None
|
|
855
876
|
if report_value is None:
|
|
@@ -1006,6 +1027,7 @@ def run(
|
|
|
1006
1027
|
wait_for_schema=wait_for_schema,
|
|
1007
1028
|
validate_schema=validate_schema,
|
|
1008
1029
|
cassette_path=cassette_path,
|
|
1030
|
+
cassette_format=cassette_format,
|
|
1009
1031
|
cassette_preserve_exact_body_bytes=cassette_preserve_exact_body_bytes,
|
|
1010
1032
|
junit_xml=junit_xml,
|
|
1011
1033
|
verbosity=verbosity,
|
|
@@ -1387,6 +1409,7 @@ def execute(
|
|
|
1387
1409
|
wait_for_schema: float | None,
|
|
1388
1410
|
validate_schema: bool,
|
|
1389
1411
|
cassette_path: click.utils.LazyFile | None,
|
|
1412
|
+
cassette_format: cassettes.CassetteFormat,
|
|
1390
1413
|
cassette_preserve_exact_body_bytes: bool,
|
|
1391
1414
|
junit_xml: click.utils.LazyFile | None,
|
|
1392
1415
|
verbosity: int,
|
|
@@ -1449,7 +1472,9 @@ def execute(
|
|
|
1449
1472
|
# This handler should be first to have logs writing completed when the output handler will display statistic
|
|
1450
1473
|
_open_file(cassette_path)
|
|
1451
1474
|
handlers.append(
|
|
1452
|
-
cassettes.CassetteWriter(
|
|
1475
|
+
cassettes.CassetteWriter(
|
|
1476
|
+
cassette_path, format=cassette_format, preserve_exact_body_bytes=cassette_preserve_exact_body_bytes
|
|
1477
|
+
)
|
|
1453
1478
|
)
|
|
1454
1479
|
handlers.append(get_output_handler(workers_num))
|
|
1455
1480
|
if sanitize_output:
|
schemathesis/cli/callbacks.py
CHANGED
|
@@ -24,6 +24,7 @@ from ..service.hosts import get_temporary_hosts_file
|
|
|
24
24
|
from ..stateful import Stateful
|
|
25
25
|
from ..transports.headers import has_invalid_characters, is_latin_1_encodable
|
|
26
26
|
from ..types import PathLike
|
|
27
|
+
from .cassettes import CassetteFormat
|
|
27
28
|
from .constants import DEFAULT_WORKERS
|
|
28
29
|
|
|
29
30
|
if TYPE_CHECKING:
|
|
@@ -344,6 +345,10 @@ def convert_code_sample_style(ctx: click.core.Context, param: click.core.Paramet
|
|
|
344
345
|
return CodeSampleStyle.from_str(value)
|
|
345
346
|
|
|
346
347
|
|
|
348
|
+
def convert_cassette_format(ctx: click.core.Context, param: click.core.Parameter, value: str) -> CassetteFormat:
|
|
349
|
+
return CassetteFormat.from_str(value)
|
|
350
|
+
|
|
351
|
+
|
|
347
352
|
def convert_data_generation_method(
|
|
348
353
|
ctx: click.core.Context, param: click.core.Parameter, value: str
|
|
349
354
|
) -> list[DataGenerationMethod]:
|
schemathesis/cli/cassettes.py
CHANGED
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import base64
|
|
4
|
+
import enum
|
|
4
5
|
import json
|
|
5
6
|
import re
|
|
6
7
|
import sys
|
|
7
8
|
import threading
|
|
8
9
|
from dataclasses import dataclass, field
|
|
10
|
+
from http.cookies import SimpleCookie
|
|
9
11
|
from queue import Queue
|
|
10
|
-
from typing import IO, TYPE_CHECKING, Any, Generator, Iterator, cast
|
|
12
|
+
from typing import IO, TYPE_CHECKING, Any, Callable, Generator, Iterator, cast
|
|
13
|
+
from urllib.parse import parse_qsl, urlparse
|
|
14
|
+
|
|
15
|
+
import harfile
|
|
11
16
|
|
|
12
17
|
from ..constants import SCHEMATHESIS_VERSION
|
|
13
18
|
from ..runner import events
|
|
@@ -27,6 +32,23 @@ if TYPE_CHECKING:
|
|
|
27
32
|
WRITER_WORKER_JOIN_TIMEOUT = 1
|
|
28
33
|
|
|
29
34
|
|
|
35
|
+
class CassetteFormat(str, enum.Enum):
|
|
36
|
+
"""Type of the cassette."""
|
|
37
|
+
|
|
38
|
+
VCR = "vcr"
|
|
39
|
+
HAR = "har"
|
|
40
|
+
|
|
41
|
+
@classmethod
|
|
42
|
+
def from_str(cls, value: str) -> CassetteFormat:
|
|
43
|
+
try:
|
|
44
|
+
return cls[value.upper()]
|
|
45
|
+
except KeyError:
|
|
46
|
+
available_formats = ", ".join(cls)
|
|
47
|
+
raise ValueError(
|
|
48
|
+
f"Invalid value for cassette format: {value}. Available formats: {available_formats}"
|
|
49
|
+
) from None
|
|
50
|
+
|
|
51
|
+
|
|
30
52
|
@dataclass
|
|
31
53
|
class CassetteWriter(EventHandler):
|
|
32
54
|
"""Write interactions in a YAML cassette.
|
|
@@ -36,26 +58,30 @@ class CassetteWriter(EventHandler):
|
|
|
36
58
|
"""
|
|
37
59
|
|
|
38
60
|
file_handle: click.utils.LazyFile
|
|
61
|
+
format: CassetteFormat
|
|
39
62
|
preserve_exact_body_bytes: bool
|
|
40
63
|
queue: Queue = field(default_factory=Queue)
|
|
41
64
|
worker: threading.Thread = field(init=False)
|
|
42
65
|
|
|
43
66
|
def __post_init__(self) -> None:
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
67
|
+
kwargs = {
|
|
68
|
+
"file_handle": self.file_handle,
|
|
69
|
+
"queue": self.queue,
|
|
70
|
+
"preserve_exact_body_bytes": self.preserve_exact_body_bytes,
|
|
71
|
+
}
|
|
72
|
+
writer: Callable
|
|
73
|
+
if self.format == CassetteFormat.HAR:
|
|
74
|
+
writer = har_writer
|
|
75
|
+
else:
|
|
76
|
+
writer = vcr_writer
|
|
77
|
+
self.worker = threading.Thread(name="SchemathesisCassetteWriter", target=writer, kwargs=kwargs)
|
|
52
78
|
self.worker.start()
|
|
53
79
|
|
|
54
80
|
def handle_event(self, context: ExecutionContext, event: events.ExecutionEvent) -> None:
|
|
55
81
|
if isinstance(event, events.Initialized):
|
|
56
82
|
# In the beginning we write metadata and start `http_interactions` list
|
|
57
83
|
self.queue.put(Initialize())
|
|
58
|
-
|
|
84
|
+
elif isinstance(event, events.AfterExecution):
|
|
59
85
|
# Seed is always present at this point, the original Optional[int] type is there because `TestResult`
|
|
60
86
|
# instance is created before `seed` is generated on the hypothesis side
|
|
61
87
|
seed = cast(int, event.result.seed)
|
|
@@ -71,7 +97,19 @@ class CassetteWriter(EventHandler):
|
|
|
71
97
|
interactions=event.result.interactions,
|
|
72
98
|
)
|
|
73
99
|
)
|
|
74
|
-
|
|
100
|
+
elif isinstance(event, events.AfterStatefulExecution):
|
|
101
|
+
seed = cast(int, event.result.seed)
|
|
102
|
+
self.queue.put(
|
|
103
|
+
Process(
|
|
104
|
+
seed=seed,
|
|
105
|
+
# Correlation ID is not used in stateful testing
|
|
106
|
+
correlation_id="",
|
|
107
|
+
thread_id=event.thread_id,
|
|
108
|
+
data_generation_method=event.data_generation_method[0],
|
|
109
|
+
interactions=event.result.interactions,
|
|
110
|
+
)
|
|
111
|
+
)
|
|
112
|
+
elif isinstance(event, events.Finished):
|
|
75
113
|
self.shutdown()
|
|
76
114
|
|
|
77
115
|
def shutdown(self) -> None:
|
|
@@ -112,7 +150,7 @@ def get_command_representation() -> str:
|
|
|
112
150
|
return f"st {args}"
|
|
113
151
|
|
|
114
152
|
|
|
115
|
-
def
|
|
153
|
+
def vcr_writer(file_handle: click.utils.LazyFile, preserve_exact_body_bytes: bool, queue: Queue) -> None:
|
|
116
154
|
"""Write YAML to a file in an incremental manner.
|
|
117
155
|
|
|
118
156
|
This implementation doesn't use `pyyaml` package and composes YAML manually as string due to the following reasons:
|
|
@@ -278,6 +316,106 @@ def write_double_quoted(stream: IO, text: str) -> None:
|
|
|
278
316
|
stream.write('"')
|
|
279
317
|
|
|
280
318
|
|
|
319
|
+
def har_writer(file_handle: click.utils.LazyFile, preserve_exact_body_bytes: bool, queue: Queue) -> None:
|
|
320
|
+
if preserve_exact_body_bytes:
|
|
321
|
+
|
|
322
|
+
def get_body(body: str) -> str:
|
|
323
|
+
return body
|
|
324
|
+
else:
|
|
325
|
+
|
|
326
|
+
def get_body(body: str) -> str:
|
|
327
|
+
return base64.b64decode(body).decode("utf-8", errors="replace")
|
|
328
|
+
|
|
329
|
+
with harfile.open(file_handle) as har:
|
|
330
|
+
while True:
|
|
331
|
+
item = queue.get()
|
|
332
|
+
if isinstance(item, Process):
|
|
333
|
+
for interaction in item.interactions:
|
|
334
|
+
time = round(interaction.response.elapsed * 1000, 2)
|
|
335
|
+
content_type = interaction.response.headers.get("Content-Type", [""])[0]
|
|
336
|
+
content = harfile.Content(
|
|
337
|
+
size=interaction.response.body_size or 0,
|
|
338
|
+
mimeType=content_type,
|
|
339
|
+
text=get_body(interaction.response.body) if interaction.response.body is not None else None,
|
|
340
|
+
encoding="base64"
|
|
341
|
+
if interaction.response.body is not None and preserve_exact_body_bytes
|
|
342
|
+
else None,
|
|
343
|
+
)
|
|
344
|
+
http_version = f"HTTP/{interaction.response.http_version}"
|
|
345
|
+
query_params = urlparse(interaction.request.uri).query
|
|
346
|
+
if interaction.request.body is not None:
|
|
347
|
+
post_data = harfile.PostData(
|
|
348
|
+
mimeType=content_type,
|
|
349
|
+
text=get_body(interaction.request.body),
|
|
350
|
+
)
|
|
351
|
+
else:
|
|
352
|
+
post_data = None
|
|
353
|
+
har.add_entry(
|
|
354
|
+
startedDateTime=interaction.recorded_at,
|
|
355
|
+
time=time,
|
|
356
|
+
request=harfile.Request(
|
|
357
|
+
method=interaction.request.method.upper(),
|
|
358
|
+
url=interaction.request.uri,
|
|
359
|
+
httpVersion=http_version,
|
|
360
|
+
headers=[
|
|
361
|
+
harfile.Record(name=name, value=values[0])
|
|
362
|
+
for name, values in interaction.request.headers.items()
|
|
363
|
+
],
|
|
364
|
+
queryString=[
|
|
365
|
+
harfile.Record(name=name, value=value)
|
|
366
|
+
for name, value in parse_qsl(query_params, keep_blank_values=True)
|
|
367
|
+
],
|
|
368
|
+
cookies=_extract_cookies(interaction.request.headers.get("Cookie", [])),
|
|
369
|
+
headersSize=_headers_size(interaction.request.headers),
|
|
370
|
+
bodySize=interaction.request.body_size or 0,
|
|
371
|
+
postData=post_data,
|
|
372
|
+
),
|
|
373
|
+
response=harfile.Response(
|
|
374
|
+
status=interaction.response.status_code,
|
|
375
|
+
httpVersion=http_version,
|
|
376
|
+
statusText=interaction.response.message,
|
|
377
|
+
headers=[
|
|
378
|
+
harfile.Record(name=name, value=values[0])
|
|
379
|
+
for name, values in interaction.response.headers.items()
|
|
380
|
+
],
|
|
381
|
+
cookies=_extract_cookies(interaction.response.headers.get("Set-Cookie", [])),
|
|
382
|
+
content=content,
|
|
383
|
+
headersSize=_headers_size(interaction.response.headers),
|
|
384
|
+
bodySize=interaction.response.body_size or 0,
|
|
385
|
+
redirectURL=interaction.response.headers.get("Location", [""])[0],
|
|
386
|
+
),
|
|
387
|
+
timings=harfile.Timings(send=0, wait=0, receive=time, blocked=0, dns=0, connect=0, ssl=0),
|
|
388
|
+
)
|
|
389
|
+
elif isinstance(item, Finalize):
|
|
390
|
+
break
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def _headers_size(headers: dict[str, list[str]]) -> int:
|
|
394
|
+
size = 0
|
|
395
|
+
for name, values in headers.items():
|
|
396
|
+
# 4 is for ": " and "\r\n"
|
|
397
|
+
size += len(name) + 4 + len(values[0])
|
|
398
|
+
return size
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
def _extract_cookies(headers: list[str]) -> list[harfile.Cookie]:
|
|
402
|
+
return [cookie for items in headers for item in items for cookie in _cookie_to_har(item)]
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def _cookie_to_har(cookie: str) -> Iterator[harfile.Cookie]:
|
|
406
|
+
parsed = SimpleCookie(cookie)
|
|
407
|
+
for name, data in parsed.items():
|
|
408
|
+
yield harfile.Cookie(
|
|
409
|
+
name=name,
|
|
410
|
+
value=data.value,
|
|
411
|
+
path=data["path"] or None,
|
|
412
|
+
domain=data["domain"] or None,
|
|
413
|
+
expires=data["expires"] or None,
|
|
414
|
+
httpOnly=data["httponly"] or None,
|
|
415
|
+
secure=data["secure"] or None,
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
|
|
281
419
|
@dataclass
|
|
282
420
|
class Replayed:
|
|
283
421
|
interaction: dict[str, Any]
|
schemathesis/exceptions.py
CHANGED
|
@@ -222,9 +222,11 @@ class OperationSchemaError(Exception):
|
|
|
222
222
|
def from_reference_resolution_error(
|
|
223
223
|
cls, error: RefResolutionError, path: str | None, method: str | None, full_path: str | None
|
|
224
224
|
) -> OperationSchemaError:
|
|
225
|
+
notes = getattr(error, "__notes__", [])
|
|
226
|
+
# Some exceptions don't have the actual reference in them, hence we add it manually via notes
|
|
227
|
+
pointer = f"'{notes[0]}'"
|
|
225
228
|
message = "Unresolvable JSON pointer in the schema"
|
|
226
229
|
# Get the pointer value from "Unresolvable JSON pointer: 'components/UnknownParameter'"
|
|
227
|
-
pointer = str(error).split(": ", 1)[-1]
|
|
228
230
|
message += f"\n\nError details:\n JSON pointer: {pointer}"
|
|
229
231
|
message += "\n This typically means that the schema is referencing a component that doesn't exist."
|
|
230
232
|
message += f"\n\n{SCHEMA_ERROR_SUGGESTION}"
|
|
@@ -77,5 +77,7 @@ class GenerationConfig:
|
|
|
77
77
|
allow_x00: bool = True
|
|
78
78
|
# Generate strings using the given codec
|
|
79
79
|
codec: str | None = "utf-8"
|
|
80
|
+
# Whether to generate security parameters
|
|
81
|
+
with_security_parameters: bool = True
|
|
80
82
|
# Header generation configuration
|
|
81
83
|
headers: HeaderConfig = field(default_factory=HeaderConfig)
|
schemathesis/models.py
CHANGED
|
@@ -119,6 +119,19 @@ def prepare_request_data(kwargs: dict[str, Any]) -> PreparedRequestData:
|
|
|
119
119
|
)
|
|
120
120
|
|
|
121
121
|
|
|
122
|
+
@dataclass
|
|
123
|
+
class GenerationMetadata:
|
|
124
|
+
"""Stores various information about how data is generated."""
|
|
125
|
+
|
|
126
|
+
query: DataGenerationMethod | None
|
|
127
|
+
path_parameters: DataGenerationMethod | None
|
|
128
|
+
headers: DataGenerationMethod | None
|
|
129
|
+
cookies: DataGenerationMethod | None
|
|
130
|
+
body: DataGenerationMethod | None
|
|
131
|
+
|
|
132
|
+
__slots__ = ("query", "path_parameters", "headers", "cookies", "body")
|
|
133
|
+
|
|
134
|
+
|
|
122
135
|
@dataclass(repr=False)
|
|
123
136
|
class Case:
|
|
124
137
|
"""A single test case parameters."""
|
|
@@ -139,6 +152,8 @@ class Case:
|
|
|
139
152
|
media_type: str | None = None
|
|
140
153
|
source: CaseSource | None = None
|
|
141
154
|
|
|
155
|
+
meta: GenerationMetadata | None = None
|
|
156
|
+
|
|
142
157
|
# The way the case was generated (None for manually crafted ones)
|
|
143
158
|
data_generation_method: DataGenerationMethod | None = None
|
|
144
159
|
_auth: requests.auth.AuthBase | None = None
|
|
@@ -492,6 +507,7 @@ class Case:
|
|
|
492
507
|
query=fast_deepcopy(self.query),
|
|
493
508
|
body=fast_deepcopy(self.body),
|
|
494
509
|
generation_time=self.generation_time,
|
|
510
|
+
id=self.id,
|
|
495
511
|
)
|
|
496
512
|
|
|
497
513
|
|
|
@@ -831,6 +847,7 @@ class Request:
|
|
|
831
847
|
method: str
|
|
832
848
|
uri: str
|
|
833
849
|
body: str | None
|
|
850
|
+
body_size: int | None
|
|
834
851
|
headers: Headers
|
|
835
852
|
|
|
836
853
|
@classmethod
|
|
@@ -861,6 +878,7 @@ class Request:
|
|
|
861
878
|
method=method,
|
|
862
879
|
headers={key: [value] for (key, value) in prepared.headers.items()},
|
|
863
880
|
body=serialize_payload(body) if body is not None else body,
|
|
881
|
+
body_size=len(body) if body is not None else None,
|
|
864
882
|
)
|
|
865
883
|
|
|
866
884
|
def deserialize_body(self) -> bytes | None:
|
|
@@ -880,6 +898,7 @@ class Response:
|
|
|
880
898
|
message: str
|
|
881
899
|
headers: dict[str, list[str]]
|
|
882
900
|
body: str | None
|
|
901
|
+
body_size: int | None
|
|
883
902
|
encoding: str | None
|
|
884
903
|
http_version: str
|
|
885
904
|
elapsed: float
|
|
@@ -906,6 +925,7 @@ class Response:
|
|
|
906
925
|
status_code=response.status_code,
|
|
907
926
|
message=response.reason,
|
|
908
927
|
body=body,
|
|
928
|
+
body_size=len(response.content) if body is not None else None,
|
|
909
929
|
encoding=response.encoding,
|
|
910
930
|
headers=headers,
|
|
911
931
|
http_version=http_version,
|
|
@@ -933,6 +953,7 @@ class Response:
|
|
|
933
953
|
status_code=response.status_code,
|
|
934
954
|
message=message,
|
|
935
955
|
body=body,
|
|
956
|
+
body_size=len(data) if body is not None else None,
|
|
936
957
|
encoding=encoding,
|
|
937
958
|
headers=headers,
|
|
938
959
|
http_version="1.1",
|
|
@@ -949,6 +970,9 @@ class Response:
|
|
|
949
970
|
return deserialize_payload(self.body)
|
|
950
971
|
|
|
951
972
|
|
|
973
|
+
TIMEZONE = datetime.datetime.now(datetime.timezone.utc).astimezone().tzinfo
|
|
974
|
+
|
|
975
|
+
|
|
952
976
|
@dataclass
|
|
953
977
|
class Interaction:
|
|
954
978
|
"""A single interaction with the target app."""
|
|
@@ -958,7 +982,7 @@ class Interaction:
|
|
|
958
982
|
checks: list[Check]
|
|
959
983
|
status: Status
|
|
960
984
|
data_generation_method: DataGenerationMethod
|
|
961
|
-
recorded_at: str = field(default_factory=lambda: datetime.datetime.now().isoformat())
|
|
985
|
+
recorded_at: str = field(default_factory=lambda: datetime.datetime.now(TIMEZONE).isoformat())
|
|
962
986
|
|
|
963
987
|
@classmethod
|
|
964
988
|
def from_requests(cls, case: Case, response: requests.Response, status: Status, checks: list[Check]) -> Interaction:
|
schemathesis/runner/events.py
CHANGED
schemathesis/runner/impl/core.py
CHANGED
|
@@ -236,12 +236,54 @@ class BaseRunner:
|
|
|
236
236
|
state_machine = self.schema.as_state_machine()
|
|
237
237
|
runner = state_machine.runner(config=config)
|
|
238
238
|
status = Status.success
|
|
239
|
+
|
|
240
|
+
def from_step_status(step_status: stateful_events.StepStatus) -> Status:
|
|
241
|
+
return {
|
|
242
|
+
stateful_events.StepStatus.SUCCESS: Status.success,
|
|
243
|
+
stateful_events.StepStatus.FAILURE: Status.failure,
|
|
244
|
+
stateful_events.StepStatus.ERROR: Status.error,
|
|
245
|
+
stateful_events.StepStatus.INTERRUPTED: Status.error,
|
|
246
|
+
}[step_status]
|
|
247
|
+
|
|
248
|
+
if self.store_interactions:
|
|
249
|
+
if isinstance(state_machine.schema.transport, RequestsTransport):
|
|
250
|
+
|
|
251
|
+
def on_step_finished(event: stateful_events.StepFinished) -> None:
|
|
252
|
+
if event.response is not None:
|
|
253
|
+
response = cast(requests.Response, event.response)
|
|
254
|
+
result.store_requests_response(
|
|
255
|
+
status=from_step_status(event.status),
|
|
256
|
+
case=event.case,
|
|
257
|
+
response=response,
|
|
258
|
+
checks=event.checks,
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
else:
|
|
262
|
+
|
|
263
|
+
def on_step_finished(event: stateful_events.StepFinished) -> None:
|
|
264
|
+
if event.response is not None:
|
|
265
|
+
response = cast(WSGIResponse, event.response)
|
|
266
|
+
result.store_wsgi_response(
|
|
267
|
+
status=from_step_status(event.status),
|
|
268
|
+
case=event.case,
|
|
269
|
+
response=response,
|
|
270
|
+
headers=self.headers or {},
|
|
271
|
+
elapsed=response.elapsed.total_seconds(),
|
|
272
|
+
checks=event.checks,
|
|
273
|
+
)
|
|
274
|
+
else:
|
|
275
|
+
|
|
276
|
+
def on_step_finished(event: stateful_events.StepFinished) -> None:
|
|
277
|
+
return None
|
|
278
|
+
|
|
239
279
|
for stateful_event in runner.execute():
|
|
240
280
|
if isinstance(stateful_event, stateful_events.SuiteFinished):
|
|
241
281
|
if stateful_event.failures and status != Status.error:
|
|
242
282
|
status = Status.failure
|
|
243
283
|
for failure in stateful_event.failures:
|
|
244
284
|
result.checks.append(failure)
|
|
285
|
+
elif isinstance(stateful_event, stateful_events.StepFinished):
|
|
286
|
+
on_step_finished(stateful_event)
|
|
245
287
|
elif isinstance(stateful_event, stateful_events.Errored):
|
|
246
288
|
status = Status.error
|
|
247
289
|
yield events.StatefulEvent(data=stateful_event)
|
|
@@ -249,6 +291,7 @@ class BaseRunner:
|
|
|
249
291
|
yield events.AfterStatefulExecution(
|
|
250
292
|
status=status,
|
|
251
293
|
result=SerializedTestResult.from_test_result(result),
|
|
294
|
+
data_generation_method=self.schema.data_generation_methods,
|
|
252
295
|
)
|
|
253
296
|
|
|
254
297
|
def _run_tests(
|
|
@@ -171,13 +171,36 @@ def _serialize_stateful_event(event: stateful_events.StatefulEvent) -> dict[str,
|
|
|
171
171
|
"timestamp": event.timestamp,
|
|
172
172
|
"exception": format_exception(event.exception, True),
|
|
173
173
|
}
|
|
174
|
+
elif isinstance(event, stateful_events.StepFinished):
|
|
175
|
+
data = {
|
|
176
|
+
"timestamp": event.timestamp,
|
|
177
|
+
"status": event.status,
|
|
178
|
+
"transition_id": {
|
|
179
|
+
"name": event.transition_id.name,
|
|
180
|
+
"status_code": event.transition_id.status_code,
|
|
181
|
+
"source": event.transition_id.source,
|
|
182
|
+
}
|
|
183
|
+
if event.transition_id is not None
|
|
184
|
+
else None,
|
|
185
|
+
"target": event.target,
|
|
186
|
+
"response": {
|
|
187
|
+
"status_code": event.response.status_code,
|
|
188
|
+
"elapsed": event.response.elapsed.total_seconds(),
|
|
189
|
+
}
|
|
190
|
+
if event.response is not None
|
|
191
|
+
else None,
|
|
192
|
+
}
|
|
174
193
|
else:
|
|
175
194
|
data = asdict(event)
|
|
176
195
|
return {"data": {event.__class__.__name__: data}}
|
|
177
196
|
|
|
178
197
|
|
|
179
198
|
def serialize_after_stateful_execution(event: events.AfterStatefulExecution) -> dict[str, Any] | None:
|
|
180
|
-
return {
|
|
199
|
+
return {
|
|
200
|
+
"status": event.status,
|
|
201
|
+
"data_generation_method": event.data_generation_method,
|
|
202
|
+
"result": asdict(event.result),
|
|
203
|
+
}
|
|
181
204
|
|
|
182
205
|
|
|
183
206
|
SERIALIZER_MAP = {
|
|
@@ -25,7 +25,7 @@ from ...generation import DataGenerationMethod, GenerationConfig
|
|
|
25
25
|
from ...hooks import HookContext, HookDispatcher, apply_to_all_dispatchers
|
|
26
26
|
from ...internal.copy import fast_deepcopy
|
|
27
27
|
from ...internal.validation import is_illegal_surrogate
|
|
28
|
-
from ...models import APIOperation, Case, cant_serialize
|
|
28
|
+
from ...models import APIOperation, Case, GenerationMetadata, cant_serialize
|
|
29
29
|
from ...serializers import Binary
|
|
30
30
|
from ...transports.content_types import parse_content_type
|
|
31
31
|
from ...transports.headers import has_invalid_characters, is_latin_1_encodable
|
|
@@ -36,7 +36,7 @@ from .formats import STRING_FORMATS
|
|
|
36
36
|
from .media_types import MEDIA_TYPES
|
|
37
37
|
from .negative import negative_schema
|
|
38
38
|
from .negative.utils import can_negate
|
|
39
|
-
from .parameters import OpenAPIBody, parameters_to_json_schema
|
|
39
|
+
from .parameters import OpenAPIBody, OpenAPIParameter, parameters_to_json_schema
|
|
40
40
|
from .utils import is_header_location
|
|
41
41
|
|
|
42
42
|
HEADER_FORMAT = "_header_value"
|
|
@@ -207,6 +207,13 @@ def get_case_strategy(
|
|
|
207
207
|
query=query_.value,
|
|
208
208
|
body=body_.value,
|
|
209
209
|
data_generation_method=generator,
|
|
210
|
+
meta=GenerationMetadata(
|
|
211
|
+
query=query_.generator,
|
|
212
|
+
path_parameters=path_parameters_.generator,
|
|
213
|
+
headers=headers_.generator,
|
|
214
|
+
cookies=cookies_.generator,
|
|
215
|
+
body=body_.generator,
|
|
216
|
+
),
|
|
210
217
|
)
|
|
211
218
|
auth_context = auths.AuthContext(
|
|
212
219
|
operation=operation,
|
|
@@ -347,6 +354,22 @@ def can_negate_headers(operation: APIOperation, location: str) -> bool:
|
|
|
347
354
|
return any(header != {"type": "string"} for header in headers.values())
|
|
348
355
|
|
|
349
356
|
|
|
357
|
+
def get_schema_for_location(
|
|
358
|
+
operation: APIOperation, location: str, parameters: Iterable[OpenAPIParameter]
|
|
359
|
+
) -> dict[str, Any]:
|
|
360
|
+
schema = parameters_to_json_schema(operation, parameters)
|
|
361
|
+
if location == "path":
|
|
362
|
+
if not operation.schema.validate_schema:
|
|
363
|
+
# If schema validation is disabled, we try to generate data even if the parameter definition
|
|
364
|
+
# contains errors.
|
|
365
|
+
# In this case, we know that the `required` keyword should always be `True`.
|
|
366
|
+
schema["required"] = list(schema["properties"])
|
|
367
|
+
for prop in schema.get("properties", {}).values():
|
|
368
|
+
if prop.get("type") == "string":
|
|
369
|
+
prop.setdefault("minLength", 1)
|
|
370
|
+
return operation.schema.prepare_schema(schema)
|
|
371
|
+
|
|
372
|
+
|
|
350
373
|
def get_parameters_strategy(
|
|
351
374
|
operation: APIOperation,
|
|
352
375
|
strategy_factory: StrategyFactory,
|
|
@@ -361,17 +384,7 @@ def get_parameters_strategy(
|
|
|
361
384
|
nested_cache_key = (strategy_factory, location, tuple(sorted(exclude)))
|
|
362
385
|
if operation in _PARAMETER_STRATEGIES_CACHE and nested_cache_key in _PARAMETER_STRATEGIES_CACHE[operation]:
|
|
363
386
|
return _PARAMETER_STRATEGIES_CACHE[operation][nested_cache_key]
|
|
364
|
-
schema =
|
|
365
|
-
if location == "path":
|
|
366
|
-
if not operation.schema.validate_schema:
|
|
367
|
-
# If schema validation is disabled, we try to generate data even if the parameter definition
|
|
368
|
-
# contains errors.
|
|
369
|
-
# In this case, we know that the `required` keyword should always be `True`.
|
|
370
|
-
schema["required"] = list(schema["properties"])
|
|
371
|
-
for prop in schema.get("properties", {}).values():
|
|
372
|
-
if prop.get("type") == "string":
|
|
373
|
-
prop.setdefault("minLength", 1)
|
|
374
|
-
schema = operation.schema.prepare_schema(schema)
|
|
387
|
+
schema = get_schema_for_location(operation, location, parameters)
|
|
375
388
|
for name in exclude:
|
|
376
389
|
# Values from `exclude` are not necessarily valid for the schema - they come from user-defined examples
|
|
377
390
|
# that may be invalid
|
|
@@ -145,7 +145,12 @@ def negative_data_rejection(response: GenericResponse, case: Case) -> bool | Non
|
|
|
145
145
|
|
|
146
146
|
if not isinstance(case.operation.schema, BaseOpenAPISchema):
|
|
147
147
|
return True
|
|
148
|
-
if
|
|
148
|
+
if (
|
|
149
|
+
case.data_generation_method
|
|
150
|
+
and case.data_generation_method.is_negative
|
|
151
|
+
and 200 <= response.status_code < 300
|
|
152
|
+
and not has_only_additional_properties_in_non_body_parameters(case)
|
|
153
|
+
):
|
|
149
154
|
exc_class = get_negative_rejection_error(case.operation.verbose_name, response.status_code)
|
|
150
155
|
raise exc_class(
|
|
151
156
|
failures.AcceptedNegativeData.title,
|
|
@@ -154,6 +159,34 @@ def negative_data_rejection(response: GenericResponse, case: Case) -> bool | Non
|
|
|
154
159
|
return None
|
|
155
160
|
|
|
156
161
|
|
|
162
|
+
def has_only_additional_properties_in_non_body_parameters(case: Case) -> bool:
|
|
163
|
+
# Check if the case contains only additional properties in query, headers, or cookies.
|
|
164
|
+
# This function is used to determine if negation is solely in the form of extra properties,
|
|
165
|
+
# which are often ignored for backward-compatibility by the tested apps
|
|
166
|
+
from ._hypothesis import get_schema_for_location
|
|
167
|
+
|
|
168
|
+
meta = case.meta
|
|
169
|
+
if meta is None:
|
|
170
|
+
# Ignore manually created cases
|
|
171
|
+
return False
|
|
172
|
+
if (meta.body and meta.body.is_negative) or (meta.path_parameters and meta.path_parameters.is_negative):
|
|
173
|
+
# Body or path negations always imply other negations
|
|
174
|
+
return False
|
|
175
|
+
validator_cls = case.operation.schema.validator_cls # type: ignore[attr-defined]
|
|
176
|
+
for container in ("query", "headers", "cookies"):
|
|
177
|
+
meta_for_location = getattr(meta, container)
|
|
178
|
+
value = getattr(case, container)
|
|
179
|
+
if value is not None and meta_for_location is not None and meta_for_location.is_negative:
|
|
180
|
+
parameters = getattr(case.operation, container)
|
|
181
|
+
value_without_additional_properties = {k: v for k, v in value.items() if k in parameters}
|
|
182
|
+
schema = get_schema_for_location(case.operation, container, parameters)
|
|
183
|
+
if not validator_cls(schema).is_valid(value_without_additional_properties):
|
|
184
|
+
# Other types of negation found
|
|
185
|
+
return False
|
|
186
|
+
# Only additional properties are added
|
|
187
|
+
return True
|
|
188
|
+
|
|
189
|
+
|
|
157
190
|
def use_after_free(response: GenericResponse, original: Case) -> bool | None:
|
|
158
191
|
from ...transports.responses import get_reason
|
|
159
192
|
from .schemas import BaseOpenAPISchema
|
|
@@ -81,6 +81,10 @@ class MutationContext:
|
|
|
81
81
|
def is_path_location(self) -> bool:
|
|
82
82
|
return self.location == "path"
|
|
83
83
|
|
|
84
|
+
@property
|
|
85
|
+
def is_query_location(self) -> bool:
|
|
86
|
+
return self.location == "query"
|
|
87
|
+
|
|
84
88
|
def mutate(self, draw: Draw) -> Schema:
|
|
85
89
|
# On the top level, Schemathesis creates "object" schemas for all parameter "in" values except "body", which is
|
|
86
90
|
# taken as-is. Therefore, we can only apply mutations that won't change the Open API semantics of the schema.
|
|
@@ -203,8 +207,11 @@ def change_type(context: MutationContext, draw: Draw, schema: Schema) -> Mutatio
|
|
|
203
207
|
if context.media_type == "application/x-www-form-urlencoded":
|
|
204
208
|
# Form data should be an object, do not change it
|
|
205
209
|
return MutationResult.FAILURE
|
|
206
|
-
#
|
|
207
|
-
|
|
210
|
+
# For headers, query and path parameters, if the current type is string, then it already
|
|
211
|
+
# includes all possible values as those parameters will be stringified before sending,
|
|
212
|
+
# therefore it can't be negated.
|
|
213
|
+
types = get_type(schema)
|
|
214
|
+
if "string" in types and (context.is_header_location or context.is_path_location or context.is_query_location):
|
|
208
215
|
return MutationResult.FAILURE
|
|
209
216
|
candidates = _get_type_candidates(context, schema)
|
|
210
217
|
if not candidates:
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import sys
|
|
3
4
|
from dataclasses import dataclass
|
|
4
5
|
from functools import lru_cache
|
|
5
6
|
from typing import Any, Callable, Dict, Union, overload
|
|
@@ -7,6 +8,7 @@ from urllib.request import urlopen
|
|
|
7
8
|
|
|
8
9
|
import jsonschema
|
|
9
10
|
import requests
|
|
11
|
+
from jsonschema.exceptions import RefResolutionError
|
|
10
12
|
|
|
11
13
|
from ...constants import DEFAULT_RESPONSE_TIMEOUT
|
|
12
14
|
from ...internal.copy import fast_deepcopy
|
|
@@ -55,6 +57,23 @@ class InliningResolver(jsonschema.RefResolver):
|
|
|
55
57
|
)
|
|
56
58
|
super().__init__(*args, **kwargs)
|
|
57
59
|
|
|
60
|
+
if sys.version_info >= (3, 11):
|
|
61
|
+
|
|
62
|
+
def resolve(self, ref: str) -> tuple[str, Any]:
|
|
63
|
+
try:
|
|
64
|
+
return super().resolve(ref)
|
|
65
|
+
except RefResolutionError as exc:
|
|
66
|
+
exc.add_note(ref)
|
|
67
|
+
raise
|
|
68
|
+
else:
|
|
69
|
+
|
|
70
|
+
def resolve(self, ref: str) -> tuple[str, Any]:
|
|
71
|
+
try:
|
|
72
|
+
return super().resolve(ref)
|
|
73
|
+
except RefResolutionError as exc:
|
|
74
|
+
exc.__notes__ = [ref]
|
|
75
|
+
raise
|
|
76
|
+
|
|
58
77
|
@overload
|
|
59
78
|
def resolve_all(self, item: dict[str, Any], recursion_level: int = 0) -> dict[str, Any]:
|
|
60
79
|
pass
|
|
@@ -19,6 +19,7 @@ from typing import (
|
|
|
19
19
|
Mapping,
|
|
20
20
|
NoReturn,
|
|
21
21
|
Sequence,
|
|
22
|
+
Type,
|
|
22
23
|
TypeVar,
|
|
23
24
|
cast,
|
|
24
25
|
)
|
|
@@ -77,7 +78,13 @@ from .parameters import (
|
|
|
77
78
|
OpenAPI30Parameter,
|
|
78
79
|
OpenAPIParameter,
|
|
79
80
|
)
|
|
80
|
-
from .references import
|
|
81
|
+
from .references import (
|
|
82
|
+
RECURSION_DEPTH_LIMIT,
|
|
83
|
+
UNRESOLVABLE,
|
|
84
|
+
ConvertingResolver,
|
|
85
|
+
InliningResolver,
|
|
86
|
+
resolve_pointer,
|
|
87
|
+
)
|
|
81
88
|
from .security import BaseSecurityProcessor, OpenAPISecurityProcessor, SwaggerSecurityProcessor
|
|
82
89
|
from .stateful import create_state_machine
|
|
83
90
|
|
|
@@ -385,7 +392,8 @@ class BaseOpenAPISchema(BaseSchema):
|
|
|
385
392
|
)
|
|
386
393
|
for parameter in parameters:
|
|
387
394
|
operation.add_parameter(parameter)
|
|
388
|
-
self.
|
|
395
|
+
if self.generation_config.with_security_parameters:
|
|
396
|
+
self.security.process_definitions(self.raw_schema, operation, self.resolver)
|
|
389
397
|
self.dispatch_hook("before_init_operation", HookContext(operation=operation), operation)
|
|
390
398
|
return operation
|
|
391
399
|
|
|
@@ -507,12 +515,13 @@ class BaseOpenAPISchema(BaseSchema):
|
|
|
507
515
|
|
|
508
516
|
def get_parameter_serializer(self, operation: APIOperation, location: str) -> Callable | None:
|
|
509
517
|
definitions = [item.definition for item in operation.iter_parameters() if item.location == location]
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
518
|
+
if self.generation_config.with_security_parameters:
|
|
519
|
+
security_parameters = self.security.get_security_definitions_as_parameters(
|
|
520
|
+
self.raw_schema, operation, self.resolver, location
|
|
521
|
+
)
|
|
522
|
+
security_parameters = [item for item in security_parameters if item["in"] == location]
|
|
523
|
+
if security_parameters:
|
|
524
|
+
definitions.extend(security_parameters)
|
|
516
525
|
if definitions:
|
|
517
526
|
return self._get_parameter_serializer(definitions)
|
|
518
527
|
return None
|
|
@@ -607,6 +616,12 @@ class BaseOpenAPISchema(BaseSchema):
|
|
|
607
616
|
def get_tags(self, operation: APIOperation) -> list[str] | None:
|
|
608
617
|
return operation.definition.raw.get("tags")
|
|
609
618
|
|
|
619
|
+
@property
|
|
620
|
+
def validator_cls(self) -> Type[jsonschema.Validator]:
|
|
621
|
+
if self.spec_version.startswith("3.1") and experimental.OPEN_API_3_1.is_enabled:
|
|
622
|
+
return jsonschema.Draft202012Validator
|
|
623
|
+
return jsonschema.Draft4Validator
|
|
624
|
+
|
|
610
625
|
def validate_response(self, operation: APIOperation, response: GenericResponse) -> bool | None:
|
|
611
626
|
responses = {str(key): value for key, value in operation.definition.raw.get("responses", {}).items()}
|
|
612
627
|
status_code = str(response.status_code)
|
|
@@ -650,13 +665,9 @@ class BaseOpenAPISchema(BaseSchema):
|
|
|
650
665
|
resolver = ConvertingResolver(
|
|
651
666
|
self.location or "", self.raw_schema, nullable_name=self.nullable_name, is_response_schema=True
|
|
652
667
|
)
|
|
653
|
-
if self.spec_version.startswith("3.1") and experimental.OPEN_API_3_1.is_enabled:
|
|
654
|
-
cls = jsonschema.Draft202012Validator
|
|
655
|
-
else:
|
|
656
|
-
cls = jsonschema.Draft4Validator
|
|
657
668
|
with in_scopes(resolver, scopes):
|
|
658
669
|
try:
|
|
659
|
-
jsonschema.validate(data, schema, cls=
|
|
670
|
+
jsonschema.validate(data, schema, cls=self.validator_cls, resolver=resolver)
|
|
660
671
|
except jsonschema.ValidationError as exc:
|
|
661
672
|
exc_class = get_schema_validation_error(operation.verbose_name, exc)
|
|
662
673
|
ctx = failures.ValidationErrorContext.from_exception(exc, output_config=operation.schema.output_config)
|
|
@@ -126,10 +126,23 @@ class OpenAPISecurityProcessor(BaseSecurityProcessor):
|
|
|
126
126
|
"""In Open API 3 security definitions are located in ``components`` and may have references inside."""
|
|
127
127
|
components = schema.get("components", {})
|
|
128
128
|
security_schemes = components.get("securitySchemes", {})
|
|
129
|
+
# At this point, the resolution scope could differ from the root scope, that's why we need to restore it
|
|
130
|
+
# as now we resolve root-level references
|
|
131
|
+
if len(resolver._scopes_stack) > 1:
|
|
132
|
+
scope = resolver.resolution_scope
|
|
133
|
+
resolver.pop_scope()
|
|
134
|
+
else:
|
|
135
|
+
scope = None
|
|
129
136
|
resolve = resolver.resolve
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
137
|
+
try:
|
|
138
|
+
if "$ref" in security_schemes:
|
|
139
|
+
return resolve(security_schemes["$ref"])[1]
|
|
140
|
+
return {
|
|
141
|
+
key: resolve(value["$ref"])[1] if "$ref" in value else value for key, value in security_schemes.items()
|
|
142
|
+
}
|
|
143
|
+
finally:
|
|
144
|
+
if scope is not None:
|
|
145
|
+
resolver._scopes_stack.append(scope)
|
|
133
146
|
|
|
134
147
|
def _make_http_auth_parameter(self, definition: dict[str, Any]) -> dict[str, Any]:
|
|
135
148
|
schema = make_auth_header_schema(definition)
|
schemathesis/stateful/context.py
CHANGED
|
@@ -39,6 +39,8 @@ class RunnerContext:
|
|
|
39
39
|
seen_in_suite: set[FailureKey] = field(default_factory=set)
|
|
40
40
|
# Unique failures collected in the current suite
|
|
41
41
|
failures_for_suite: list[Check] = field(default_factory=list)
|
|
42
|
+
# All checks executed in the current run
|
|
43
|
+
checks_for_step: list[Check] = field(default_factory=list)
|
|
42
44
|
# Status of the current step
|
|
43
45
|
current_step_status: events.StepStatus | None = None
|
|
44
46
|
current_response: GenericResponse | None = None
|
|
@@ -55,10 +57,13 @@ class RunnerContext:
|
|
|
55
57
|
return events.ScenarioStatus.INTERRUPTED
|
|
56
58
|
return events.ScenarioStatus.REJECTED
|
|
57
59
|
|
|
58
|
-
def
|
|
60
|
+
def reset_scenario(self) -> None:
|
|
59
61
|
self.current_step_status = None
|
|
60
62
|
self.current_response = None
|
|
61
63
|
|
|
64
|
+
def reset_step(self) -> None:
|
|
65
|
+
self.checks_for_step = []
|
|
66
|
+
|
|
62
67
|
def step_succeeded(self) -> None:
|
|
63
68
|
self.current_step_status = events.StepStatus.SUCCESS
|
|
64
69
|
|
|
@@ -100,4 +105,4 @@ class RunnerContext:
|
|
|
100
105
|
def reset(self) -> None:
|
|
101
106
|
self.failures_for_suite = []
|
|
102
107
|
self.seen_in_suite.clear()
|
|
103
|
-
self.
|
|
108
|
+
self.reset_scenario()
|
schemathesis/stateful/events.py
CHANGED
|
@@ -6,7 +6,8 @@ from enum import Enum
|
|
|
6
6
|
from typing import TYPE_CHECKING, Type
|
|
7
7
|
|
|
8
8
|
if TYPE_CHECKING:
|
|
9
|
-
from ..models import Check
|
|
9
|
+
from ..models import Case, Check
|
|
10
|
+
from ..transports.responses import GenericResponse
|
|
10
11
|
from .state_machine import APIStateMachine
|
|
11
12
|
|
|
12
13
|
|
|
@@ -178,17 +179,28 @@ class StepFinished(StatefulEvent):
|
|
|
178
179
|
status: StepStatus
|
|
179
180
|
transition_id: TransitionId | None
|
|
180
181
|
target: str
|
|
181
|
-
|
|
182
|
+
case: Case
|
|
183
|
+
response: GenericResponse | None
|
|
184
|
+
checks: list[Check]
|
|
182
185
|
|
|
183
|
-
__slots__ = ("timestamp", "status", "transition_id", "target", "response")
|
|
186
|
+
__slots__ = ("timestamp", "status", "transition_id", "target", "case", "response", "checks")
|
|
184
187
|
|
|
185
188
|
def __init__(
|
|
186
|
-
self,
|
|
189
|
+
self,
|
|
190
|
+
*,
|
|
191
|
+
status: StepStatus,
|
|
192
|
+
transition_id: TransitionId | None,
|
|
193
|
+
target: str,
|
|
194
|
+
case: Case,
|
|
195
|
+
response: GenericResponse | None,
|
|
196
|
+
checks: list[Check],
|
|
187
197
|
) -> None:
|
|
188
198
|
self.status = status
|
|
189
199
|
self.transition_id = transition_id
|
|
190
200
|
self.target = target
|
|
201
|
+
self.case = case
|
|
191
202
|
self.response = response
|
|
203
|
+
self.checks = checks
|
|
192
204
|
self.timestamp = time.monotonic()
|
|
193
205
|
|
|
194
206
|
|
schemathesis/stateful/runner.py
CHANGED
|
@@ -157,30 +157,25 @@ def _execute_state_machine_loop(
|
|
|
157
157
|
)
|
|
158
158
|
else:
|
|
159
159
|
transition_id = None
|
|
160
|
-
response: events.ResponseData | None
|
|
161
|
-
if ctx.current_response is not None:
|
|
162
|
-
response = events.ResponseData(
|
|
163
|
-
status_code=ctx.current_response.status_code,
|
|
164
|
-
elapsed=ctx.current_response.elapsed.total_seconds(),
|
|
165
|
-
)
|
|
166
|
-
else:
|
|
167
|
-
response = None
|
|
168
160
|
status = cast(events.StepStatus, ctx.current_step_status)
|
|
169
161
|
event_queue.put(
|
|
170
162
|
events.StepFinished(
|
|
171
163
|
status=status,
|
|
172
164
|
transition_id=transition_id,
|
|
173
165
|
target=case.operation.verbose_name,
|
|
174
|
-
|
|
166
|
+
case=case,
|
|
167
|
+
response=ctx.current_response,
|
|
168
|
+
checks=ctx.checks_for_step,
|
|
175
169
|
)
|
|
176
170
|
)
|
|
171
|
+
ctx.reset_step()
|
|
177
172
|
return result
|
|
178
173
|
|
|
179
174
|
def validate_response(
|
|
180
175
|
self, response: GenericResponse, case: Case, additional_checks: tuple[CheckFunction, ...] = ()
|
|
181
176
|
) -> None:
|
|
182
177
|
ctx.current_response = response
|
|
183
|
-
validate_response(response, case, ctx, config.checks, additional_checks)
|
|
178
|
+
validate_response(response, case, ctx, config.checks, ctx.checks_for_step, additional_checks)
|
|
184
179
|
|
|
185
180
|
def teardown(self) -> None:
|
|
186
181
|
build_ctx = current_build_context()
|
|
@@ -190,7 +185,7 @@ def _execute_state_machine_loop(
|
|
|
190
185
|
is_final=build_ctx.is_final,
|
|
191
186
|
)
|
|
192
187
|
)
|
|
193
|
-
ctx.
|
|
188
|
+
ctx.reset_scenario()
|
|
194
189
|
super().teardown()
|
|
195
190
|
|
|
196
191
|
while True:
|
schemathesis/stateful/sink.py
CHANGED
|
@@ -51,7 +51,7 @@ class StateMachineSink:
|
|
|
51
51
|
responses = self.response_times.setdefault(event.target, {})
|
|
52
52
|
if event.response is not None:
|
|
53
53
|
average = responses.setdefault(event.response.status_code, AverageResponseTime())
|
|
54
|
-
average.total += event.response.elapsed
|
|
54
|
+
average.total += event.response.elapsed.total_seconds()
|
|
55
55
|
average.count += 1
|
|
56
56
|
elif isinstance(event, events.ScenarioFinished):
|
|
57
57
|
self.scenarios[event.status] += 1
|
|
@@ -7,7 +7,7 @@ from .context import RunnerContext
|
|
|
7
7
|
|
|
8
8
|
if TYPE_CHECKING:
|
|
9
9
|
from ..failures import FailureContext
|
|
10
|
-
from ..models import Case, CheckFunction
|
|
10
|
+
from ..models import Case, CheckFunction, Check
|
|
11
11
|
from ..transports.responses import GenericResponse
|
|
12
12
|
|
|
13
13
|
|
|
@@ -16,6 +16,7 @@ def validate_response(
|
|
|
16
16
|
case: Case,
|
|
17
17
|
failures: RunnerContext,
|
|
18
18
|
checks: tuple[CheckFunction, ...],
|
|
19
|
+
check_results: list[Check],
|
|
19
20
|
additional_checks: tuple[CheckFunction, ...] = (),
|
|
20
21
|
) -> None:
|
|
21
22
|
"""Validate the response against the provided checks."""
|
|
@@ -28,18 +29,18 @@ def validate_response(
|
|
|
28
29
|
exceptions.append(exc)
|
|
29
30
|
if failures.is_seen_in_suite(exc):
|
|
30
31
|
return
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
request=None,
|
|
41
|
-
)
|
|
32
|
+
failed_check = Check(
|
|
33
|
+
name=name,
|
|
34
|
+
value=Status.failure,
|
|
35
|
+
response=response,
|
|
36
|
+
elapsed=response.elapsed.total_seconds(),
|
|
37
|
+
example=copied_case,
|
|
38
|
+
message=message,
|
|
39
|
+
context=context,
|
|
40
|
+
request=None,
|
|
42
41
|
)
|
|
42
|
+
failures.add_failed_check(failed_check)
|
|
43
|
+
check_results.append(failed_check)
|
|
43
44
|
failures.mark_as_seen_in_suite(exc)
|
|
44
45
|
|
|
45
46
|
for check in checks + additional_checks:
|
|
@@ -47,6 +48,17 @@ def validate_response(
|
|
|
47
48
|
copied_case = case.partial_deepcopy()
|
|
48
49
|
try:
|
|
49
50
|
check(response, copied_case)
|
|
51
|
+
skip_check = check(response, copied_case)
|
|
52
|
+
if not skip_check:
|
|
53
|
+
passed_check = Check(
|
|
54
|
+
name=name,
|
|
55
|
+
value=Status.success,
|
|
56
|
+
response=response,
|
|
57
|
+
elapsed=response.elapsed.total_seconds(),
|
|
58
|
+
example=copied_case,
|
|
59
|
+
request=None,
|
|
60
|
+
)
|
|
61
|
+
check_results.append(passed_check)
|
|
50
62
|
except CheckFailed as exc:
|
|
51
63
|
if failures.is_seen_in_run(exc):
|
|
52
64
|
continue
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: schemathesis
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.31.1
|
|
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
|
|
@@ -31,6 +31,7 @@ Requires-Python: >=3.8
|
|
|
31
31
|
Requires-Dist: backoff<3.0,>=2.1.2
|
|
32
32
|
Requires-Dist: click<9.0,>=7.0
|
|
33
33
|
Requires-Dist: colorama<1.0,>=0.4
|
|
34
|
+
Requires-Dist: harfile<1.0,>=0.3.0
|
|
34
35
|
Requires-Dist: httpx<1.0,>=0.22.0
|
|
35
36
|
Requires-Dist: hypothesis-graphql<1,>=0.11.0
|
|
36
37
|
Requires-Dist: hypothesis-jsonschema<0.24,>=0.23.1
|
|
@@ -10,14 +10,14 @@ schemathesis/auths.py,sha256=NeJqsjtDgJtHMyrHc6V1NTpkAh1K8ZKLalpB3v80cT4,14734
|
|
|
10
10
|
schemathesis/checks.py,sha256=XplKduiRbRrxZx-wDMGaW91aIitr5RL9vjMP6bvCFRg,2272
|
|
11
11
|
schemathesis/code_samples.py,sha256=xk1-1jnXg5hS40VzIZp8PEtZwGaazNlVKMT7_X-zG-M,4123
|
|
12
12
|
schemathesis/constants.py,sha256=l1YQ7PXhEj9dyf9CTESVUpPOaFCH7iz-Fe8o4v6Th_s,2673
|
|
13
|
-
schemathesis/exceptions.py,sha256=
|
|
13
|
+
schemathesis/exceptions.py,sha256=tFDlNui1Sxc8a6-OQCIjj62SoToYoEN9zveguPpzQIc,19712
|
|
14
14
|
schemathesis/failures.py,sha256=wXz5Kr5i-ojcYc-BdzFlNbNGOfoVXHZM6kd4iULdHK4,7003
|
|
15
15
|
schemathesis/filters.py,sha256=0fYzn9sJ35k3Znx1P8FrbSdoUcdslcibtGh-IOTRwB8,10251
|
|
16
16
|
schemathesis/graphql.py,sha256=YkoKWY5K8lxp7H3ikAs-IsoDbiPwJvChG7O8p3DgwtI,229
|
|
17
17
|
schemathesis/hooks.py,sha256=dveqMmThIvt4fDahUXhU2nCq5pFvYjzzd1Ys_MhrJZA,12398
|
|
18
18
|
schemathesis/lazy.py,sha256=eVdGkTZK0fWvUlFUCFGGlViH2NWEtYIjxiNkF4fBWhI,15218
|
|
19
19
|
schemathesis/loaders.py,sha256=OtCD1o0TVmSNAUF7dgHpouoAXtY6w9vEtsRVGv4lE0g,4588
|
|
20
|
-
schemathesis/models.py,sha256=
|
|
20
|
+
schemathesis/models.py,sha256=nbm9Agqw94RYib2Q4OH7iOiEPDGw64NlQnEB7o5Spio,44925
|
|
21
21
|
schemathesis/parameters.py,sha256=PndmqQRlEYsCt1kWjSShPsFf6vj7X_7FRdz_-A95eNg,2258
|
|
22
22
|
schemathesis/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
23
23
|
schemathesis/sanitization.py,sha256=mRR4YvXpzqbmgX8Xu6rume6LBcz9g_oyusvbesZl44I,8958
|
|
@@ -27,10 +27,10 @@ schemathesis/targets.py,sha256=N1Zzgqa1PNycWIeGpra7q-6ASn2x4r9Jompn35bmlsE,1163
|
|
|
27
27
|
schemathesis/throttling.py,sha256=aisUc4MJDGIOGUAs9L2DlWWpdd4KyAFuNVKhYoaUC9M,1719
|
|
28
28
|
schemathesis/types.py,sha256=xOzNAeMs6qqeaJnWs5Fpw5JPbvVjyfRfxTJa3G2Ln5I,920
|
|
29
29
|
schemathesis/utils.py,sha256=NX04p9mO-lCAH3DIISXDXPxWZk6lkGNM4-ubRi8vlvY,5234
|
|
30
|
-
schemathesis/cli/__init__.py,sha256=
|
|
30
|
+
schemathesis/cli/__init__.py,sha256=Gw2uVput5JySsSg8gjAzibi-tK_PixwyNYyQLTukAJE,66535
|
|
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=R2noVRu8zDBWmA3dJ2YnmhAjYdkxPDA1zIpA3_2DkFQ,15144
|
|
33
|
+
schemathesis/cli/cassettes.py,sha256=3vumBEP0XXBVpNvwMEGYvcIp1TSHtCNUIL60TPDVgD4,19024
|
|
34
34
|
schemathesis/cli/constants.py,sha256=ogSuZs68KvzHNKF0yaiBkxLcGbo8IVR3xaIfsy1H1IQ,1546
|
|
35
35
|
schemathesis/cli/context.py,sha256=EMeyAbU9mRujR46anc43yr6ab4rGYtIDaHC3cV9Qa-Q,2092
|
|
36
36
|
schemathesis/cli/debug.py,sha256=_YA-bX1ujHl4bqQDEum7M-I2XHBTEGbvgkhvcvKhmgU,658
|
|
@@ -57,7 +57,7 @@ schemathesis/extra/pytest_plugin.py,sha256=ymicV2NjmSzee0ccUUUjNEvb9ihCxxf_8M60g
|
|
|
57
57
|
schemathesis/fixups/__init__.py,sha256=RP5QYJVJhp8LXjhH89fCRaIVU26dHCy74jD9seoYMuc,967
|
|
58
58
|
schemathesis/fixups/fast_api.py,sha256=mn-KzBqnR8jl4W5fY-_ZySabMDMUnpzCIESMHnlvE1c,1304
|
|
59
59
|
schemathesis/fixups/utf8_bom.py,sha256=lWT9RNmJG8i-l5AXIpaCT3qCPUwRgzXPW3eoOjmZETA,745
|
|
60
|
-
schemathesis/generation/__init__.py,sha256=
|
|
60
|
+
schemathesis/generation/__init__.py,sha256=mC1NVAHyce1_B_wu-GYO9U21Gut8KFrjPXETREC9ABQ,2366
|
|
61
61
|
schemathesis/internal/__init__.py,sha256=93HcdG3LF0BbQKbCteOsFMa1w6nXl8yTmx87QLNJOik,161
|
|
62
62
|
schemathesis/internal/copy.py,sha256=lcK01CODz6jogXyH0tsKkPv1PBEX8jeBPxI9MzQ6LN4,942
|
|
63
63
|
schemathesis/internal/datetime.py,sha256=zPLBL0XXLNfP-KYel3H2m8pnsxjsA_4d-zTOhJg2EPQ,136
|
|
@@ -68,11 +68,11 @@ schemathesis/internal/result.py,sha256=d449YvyONjqjDs-A5DAPgtAI96iT753K8sU6_1HLo
|
|
|
68
68
|
schemathesis/internal/transformation.py,sha256=3S6AzAqdsEsB5iobFgSuvL0UMUqH0JHC7hGxKwcpqPw,450
|
|
69
69
|
schemathesis/internal/validation.py,sha256=G7i8jIMUpAeOnDsDF_eWYvRZe_yMprRswx0QAtMPyEw,966
|
|
70
70
|
schemathesis/runner/__init__.py,sha256=n-4CanmlfMiGnir6xbZH-fM5a0pe2GloBFj2NTtet-M,21419
|
|
71
|
-
schemathesis/runner/events.py,sha256=
|
|
71
|
+
schemathesis/runner/events.py,sha256=VVFy-qAgxoHBNrexBakq_QX_hx52pnWK-67HgSPpYRw,10694
|
|
72
72
|
schemathesis/runner/probes.py,sha256=J-TT0hOKu9j4htWKBcYKmsomcRxmvOl4WpmnKLVXu8M,5546
|
|
73
73
|
schemathesis/runner/serialization.py,sha256=J8fuG8MSJq3rE3IJs73U1YXWFrNa05k7PGd5Bvq1uec,17356
|
|
74
74
|
schemathesis/runner/impl/__init__.py,sha256=1E2iME8uthYPBh9MjwVBCTFV-P3fi7AdphCCoBBspjs,199
|
|
75
|
-
schemathesis/runner/impl/core.py,sha256=
|
|
75
|
+
schemathesis/runner/impl/core.py,sha256=Ec_HlCRJdQSluN_JYEt0eh99SFXf0RGX6knPpayPUXw,44148
|
|
76
76
|
schemathesis/runner/impl/solo.py,sha256=MatxThgqKsY2tX_hVwjy78oKFeKejb6dFJoX3kGzW4U,3359
|
|
77
77
|
schemathesis/runner/impl/threadpool.py,sha256=fj2QYoWxIJIxpTCcJQyM_VCRO1YDnW9XQJJnNVFVQxY,15253
|
|
78
78
|
schemathesis/service/__init__.py,sha256=cDVTCFD1G-vvhxZkJUwiToTAEQ-0ByIoqwXvJBCf_V8,472
|
|
@@ -86,7 +86,7 @@ schemathesis/service/hosts.py,sha256=ad2Lxq9Zcc9PP-1eFLQnxen4ImglcGOH8n7CGG72NNg
|
|
|
86
86
|
schemathesis/service/metadata.py,sha256=x2LeCED1mdPf-YQJmjY8xtcIKHfD1ap5V0BGl-UgqNo,2087
|
|
87
87
|
schemathesis/service/models.py,sha256=ihItUJ9CvH4TvmdfJY3W88NR82OODF8a3RD7WRXn6RM,6578
|
|
88
88
|
schemathesis/service/report.py,sha256=4A8nf6_KOjDW3x1VXF8gSf_WY2xXp1Cbz-Owl_GeR7o,8294
|
|
89
|
-
schemathesis/service/serialization.py,sha256=
|
|
89
|
+
schemathesis/service/serialization.py,sha256=GFNc1-w1ShZopFdR7OFLT9ZKe8Y2S1hYnlXMrdMj3VE,10600
|
|
90
90
|
schemathesis/service/usage.py,sha256=UbXqxeDq5mAjKkfV4hApZsReZmQHXiqoXUYn_Z6YuZk,2438
|
|
91
91
|
schemathesis/specs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
92
92
|
schemathesis/specs/graphql/__init__.py,sha256=fgyHtvWNUVWismBTOqxQtgLoTighTfvMv6v6QCD_Oyc,85
|
|
@@ -98,8 +98,8 @@ schemathesis/specs/graphql/schemas.py,sha256=i6fAW9pYcOplQE7BejP6P8GQ9z6Y43Vx4_f
|
|
|
98
98
|
schemathesis/specs/graphql/validation.py,sha256=uINIOt-2E7ZuQV2CxKzwez-7L9tDtqzMSpnVoRWvxy0,1635
|
|
99
99
|
schemathesis/specs/openapi/__init__.py,sha256=HDcx3bqpa6qWPpyMrxAbM3uTo0Lqpg-BUNZhDJSJKnw,279
|
|
100
100
|
schemathesis/specs/openapi/_cache.py,sha256=PAiAu4X_a2PQgD2lG5H3iisXdyg4SaHpU46bRZvfNkM,4320
|
|
101
|
-
schemathesis/specs/openapi/_hypothesis.py,sha256=
|
|
102
|
-
schemathesis/specs/openapi/checks.py,sha256=
|
|
101
|
+
schemathesis/specs/openapi/_hypothesis.py,sha256=hPctM9QN4mGZrEPnLbesoPEDrSF4TCyI5RMJUmchhnQ,24070
|
|
102
|
+
schemathesis/specs/openapi/checks.py,sha256=eFjbV9-0202qg0s0JjaBNRr7nf8Bd8VMLPHEfMvZoc4,10967
|
|
103
103
|
schemathesis/specs/openapi/constants.py,sha256=JqM_FHOenqS_MuUE9sxVQ8Hnw0DNM8cnKDwCwPLhID4,783
|
|
104
104
|
schemathesis/specs/openapi/converter.py,sha256=TaYgc5BBHPdkN-n0lqpbeVgLu3eL3L8Wu3y_Vo3TJaQ,2800
|
|
105
105
|
schemathesis/specs/openapi/definitions.py,sha256=Z186F0gNBSCmPg-Kk7Q-n6XxEZHIOzgUyeqixlC62XE,94058
|
|
@@ -110,9 +110,9 @@ schemathesis/specs/openapi/links.py,sha256=2ucOLs50OhCqu0PEdbT_BGUM3fKnHBl97YGIS
|
|
|
110
110
|
schemathesis/specs/openapi/loaders.py,sha256=JJdIz1aT03J9WmUWTLOz6Yhuu69IqmhobQ9_vL6XJ6U,24916
|
|
111
111
|
schemathesis/specs/openapi/media_types.py,sha256=dNTxpRQbY3SubdVjh4Cjb38R6Bc9MF9BsRQwPD87x0g,1017
|
|
112
112
|
schemathesis/specs/openapi/parameters.py,sha256=_6vNCnPXcdxjfAQbykCRLHjvmTpu_02xDJghxDrGYr8,13611
|
|
113
|
-
schemathesis/specs/openapi/references.py,sha256=
|
|
114
|
-
schemathesis/specs/openapi/schemas.py,sha256=
|
|
115
|
-
schemathesis/specs/openapi/security.py,sha256=
|
|
113
|
+
schemathesis/specs/openapi/references.py,sha256=euxM02kQGMHh4Ss1jWjOY_gyw_HazafKITIsvOEiAvI,9831
|
|
114
|
+
schemathesis/specs/openapi/schemas.py,sha256=sZXzw4ToOpTbrjFQdvCwGymaCWW1b_ofzm7jJQK03kI,52287
|
|
115
|
+
schemathesis/specs/openapi/security.py,sha256=nEhDB_SvEFldmfpa9uOQywfWN6DtXHKmgtwucJvfN5Q,7096
|
|
116
116
|
schemathesis/specs/openapi/serialization.py,sha256=5qGdFHZ3n80UlbSXrO_bkr4Al_7ci_Z3aSUjZczNDQY,11384
|
|
117
117
|
schemathesis/specs/openapi/utils.py,sha256=-TCu0hTrlwp2x5qHNp-TxiHRMeIZC9OBmlhLssjRIiQ,742
|
|
118
118
|
schemathesis/specs/openapi/validation.py,sha256=Q9ThZlwU-mSz7ExDnIivnZGi1ivC5hlX2mIMRAM79kc,999
|
|
@@ -124,7 +124,7 @@ schemathesis/specs/openapi/expressions/lexer.py,sha256=LeVE6fgYT9-fIsXrv0-YrRHnI
|
|
|
124
124
|
schemathesis/specs/openapi/expressions/nodes.py,sha256=DUbAtuXdUDsxZ_pGeCVXAlL3gTj8nt9KulMGaIS-N2I,3948
|
|
125
125
|
schemathesis/specs/openapi/expressions/parser.py,sha256=gM_Ob-TlTGxpgjZGRHNyPhBj1YAvRgRoSlNCrE7-djk,4452
|
|
126
126
|
schemathesis/specs/openapi/negative/__init__.py,sha256=gw0w_9tVQf_MY5Df3_xTZFC4rAy1TTBS4wBccm36uFs,3697
|
|
127
|
-
schemathesis/specs/openapi/negative/mutations.py,sha256=
|
|
127
|
+
schemathesis/specs/openapi/negative/mutations.py,sha256=lLEN0GLxvPmZBQ3tHCznDSjmZ4yQiQxspjv1UpO4Kx0,19019
|
|
128
128
|
schemathesis/specs/openapi/negative/types.py,sha256=a7buCcVxNBG6ILBM3A7oNTAX0lyDseEtZndBuej8MbI,174
|
|
129
129
|
schemathesis/specs/openapi/negative/utils.py,sha256=ozcOIuASufLqZSgnKUACjX-EOZrrkuNdXX0SDnLoGYA,168
|
|
130
130
|
schemathesis/specs/openapi/stateful/__init__.py,sha256=fAA52Nk0olm46u1e6OtZTXwmFM5W2r0jhRhZ24iz7GY,7673
|
|
@@ -132,20 +132,20 @@ schemathesis/specs/openapi/stateful/statistic.py,sha256=EJK4NqeAYRYl1FtU9YEuTLyh
|
|
|
132
132
|
schemathesis/specs/openapi/stateful/types.py,sha256=UuGcCTFvaHsqeLN9ZeUNcbjsEwmthoT3UcHfDHchOYo,419
|
|
133
133
|
schemathesis/stateful/__init__.py,sha256=qyQJ-9Ect-AWZiAsK63F3BTGu-jZnPCOp1q46YAonkQ,4911
|
|
134
134
|
schemathesis/stateful/config.py,sha256=kuKGLNNk0YrR6G0IzcZh6v0oysHHTPfHi9nHUsfQsF0,2298
|
|
135
|
-
schemathesis/stateful/context.py,sha256=
|
|
136
|
-
schemathesis/stateful/events.py,sha256=
|
|
137
|
-
schemathesis/stateful/runner.py,sha256=
|
|
138
|
-
schemathesis/stateful/sink.py,sha256=
|
|
135
|
+
schemathesis/stateful/context.py,sha256=nYurdKz3yLA4xvOAUBc5hiJikDaMTiIn69RQhskreVU,3932
|
|
136
|
+
schemathesis/stateful/events.py,sha256=VF0jRi4eq5ybQnyIjSvScRMjQK-NwKd-pNZ7ZVJwygk,5215
|
|
137
|
+
schemathesis/stateful/runner.py,sha256=4VDjo_PgSoNxfMVsAOfXlJVXZPhqh8fc76Lt7cXPXZ4,9404
|
|
138
|
+
schemathesis/stateful/sink.py,sha256=yWY9xJeUyOWOsu1tNxCgsDFQVJxK2xgQavJ9vQoxK1I,2441
|
|
139
139
|
schemathesis/stateful/state_machine.py,sha256=H-AzMPTKuCKnoCv0b7XPFDsHkzRftNfbvh5xb2H5Hfk,12156
|
|
140
140
|
schemathesis/stateful/statistic.py,sha256=xPLiCw61ofNXQicqcK_sZyLHiqiGcgQARpwd8AiRubM,487
|
|
141
|
-
schemathesis/stateful/validation.py,sha256=
|
|
141
|
+
schemathesis/stateful/validation.py,sha256=RiXnZBTu6zMnjCpZBLyMwPDJrPTnSHWSijrg34VtRoM,2825
|
|
142
142
|
schemathesis/transports/__init__.py,sha256=fwICPJBLHA7_L4IDAiW1SmxNHXlLD_Eqp-74w9TRRIs,12160
|
|
143
143
|
schemathesis/transports/auth.py,sha256=4z7c-K7lfyyVqgR6X1v4yiE8ewR_ViAznWFTAsCL0RI,405
|
|
144
144
|
schemathesis/transports/content_types.py,sha256=VrcRQvF5T_TUjrCyrZcYF2LOwKfs3IrLcMtkVSp1ImI,2189
|
|
145
145
|
schemathesis/transports/headers.py,sha256=hr_AIDOfUxsJxpHfemIZ_uNG3_vzS_ZeMEKmZjbYiBE,990
|
|
146
146
|
schemathesis/transports/responses.py,sha256=6-gvVcRK0Ho_lSydUysBNFWoJwZEiEgf6Iv-GWkQGd8,1675
|
|
147
|
-
schemathesis-3.
|
|
148
|
-
schemathesis-3.
|
|
149
|
-
schemathesis-3.
|
|
150
|
-
schemathesis-3.
|
|
151
|
-
schemathesis-3.
|
|
147
|
+
schemathesis-3.31.1.dist-info/METADATA,sha256=W2PcjGom3XH8VuWRWR7kqLv12ZUliovQllOYeyMsnqk,17706
|
|
148
|
+
schemathesis-3.31.1.dist-info/WHEEL,sha256=hKi7AIIx6qfnsRbr087vpeJnrVUuDokDHZacPPMW7-Y,87
|
|
149
|
+
schemathesis-3.31.1.dist-info/entry_points.txt,sha256=VHyLcOG7co0nOeuk8WjgpRETk5P1E2iCLrn26Zkn5uk,158
|
|
150
|
+
schemathesis-3.31.1.dist-info/licenses/LICENSE,sha256=PsPYgrDhZ7g9uwihJXNG-XVb55wj2uYhkl2DD8oAzY0,1103
|
|
151
|
+
schemathesis-3.31.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|