schemathesis 4.0.13__py3-none-any.whl → 4.0.15__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.
@@ -36,6 +36,15 @@ def schemathesis(ctx: click.Context, config_file: str | None) -> None:
36
36
  config = SchemathesisConfig.from_path(config_file)
37
37
  else:
38
38
  config = SchemathesisConfig.discover()
39
+ except FileNotFoundError:
40
+ display_header(SCHEMATHESIS_VERSION)
41
+ click.secho(
42
+ f"❌ Failed to load configuration file from {config_file}",
43
+ fg="red",
44
+ bold=True,
45
+ )
46
+ click.echo("\nThe configuration file does not exist")
47
+ ctx.exit(1)
39
48
  except (TOMLDecodeError, ConfigError) as exc:
40
49
  display_header(SCHEMATHESIS_VERSION)
41
50
  click.secho(
@@ -9,7 +9,7 @@ from click.utils import LazyFile
9
9
  from schemathesis.checks import CHECKS, load_all_checks
10
10
  from schemathesis.cli.commands.run import executor, validation
11
11
  from schemathesis.cli.commands.run.filters import with_filters
12
- from schemathesis.cli.constants import DEFAULT_WORKERS, MAX_WORKERS, MIN_WORKERS
12
+ from schemathesis.cli.constants import MAX_WORKERS, MIN_WORKERS
13
13
  from schemathesis.cli.core import ensure_color
14
14
  from schemathesis.cli.ext.groups import group, grouped_option
15
15
  from schemathesis.cli.ext.options import (
@@ -62,7 +62,7 @@ DEFAULT_PHASES = ["examples", "coverage", "fuzzing", "stateful"]
62
62
  ["auto", *list(map(str, range(MIN_WORKERS, MAX_WORKERS + 1)))],
63
63
  choices_repr=f"[auto, {MIN_WORKERS}-{MAX_WORKERS}]",
64
64
  ),
65
- default=str(DEFAULT_WORKERS),
65
+ default=None,
66
66
  show_default=True,
67
67
  callback=validation.convert_workers,
68
68
  metavar="",
@@ -172,7 +172,7 @@ DEFAULT_PHASES = ["examples", "coverage", "fuzzing", "stateful"]
172
172
  help="Skip deprecated operations",
173
173
  is_flag=True,
174
174
  is_eager=True,
175
- default=False,
175
+ default=None,
176
176
  show_default=True,
177
177
  )
178
178
  @group("Network requests options")
@@ -206,7 +206,7 @@ DEFAULT_PHASES = ["examples", "coverage", "fuzzing", "stateful"]
206
206
  "request_tls_verify",
207
207
  help="Path to CA bundle for TLS verification, or 'false' to disable",
208
208
  type=str,
209
- default="true",
209
+ default=None,
210
210
  show_default=True,
211
211
  callback=validation.convert_boolean_string,
212
212
  )
@@ -280,13 +280,13 @@ DEFAULT_PHASES = ["examples", "coverage", "fuzzing", "stateful"]
280
280
  help="Retain exact byte sequence of payloads in cassettes, encoded as base64",
281
281
  type=bool,
282
282
  is_flag=True,
283
- default=False,
283
+ default=None,
284
284
  callback=validation.validate_preserve_bytes,
285
285
  )
286
286
  @grouped_option(
287
287
  "--output-sanitize",
288
288
  type=str,
289
- default="true",
289
+ default=None,
290
290
  show_default=True,
291
291
  help="Enable or disable automatic output sanitization to obscure sensitive data",
292
292
  metavar="BOOLEAN",
@@ -296,7 +296,7 @@ DEFAULT_PHASES = ["examples", "coverage", "fuzzing", "stateful"]
296
296
  "--output-truncate",
297
297
  help="Truncate schemas and responses in error messages",
298
298
  type=str,
299
- default="true",
299
+ default=None,
300
300
  show_default=True,
301
301
  metavar="BOOLEAN",
302
302
  callback=validation.convert_boolean_string,
@@ -331,20 +331,21 @@ DEFAULT_PHASES = ["examples", "coverage", "fuzzing", "stateful"]
331
331
  "generation_no_shrink",
332
332
  help="Disable test case shrinking. Makes test failures harder to debug but improves performance",
333
333
  is_flag=True,
334
+ default=None,
334
335
  )
335
336
  @grouped_option(
336
337
  "--generation-deterministic",
337
338
  help="Enables deterministic mode, which eliminates random variation between tests",
338
339
  is_flag=True,
339
340
  is_eager=True,
340
- default=False,
341
+ default=None,
341
342
  show_default=True,
342
343
  )
343
344
  @grouped_option(
344
345
  "--generation-allow-x00",
345
346
  help="Whether to allow the generation of 'NULL' bytes within strings",
346
347
  type=str,
347
- default="true",
348
+ default=None,
348
349
  show_default=True,
349
350
  metavar="BOOLEAN",
350
351
  callback=validation.convert_boolean_string,
@@ -353,7 +354,7 @@ DEFAULT_PHASES = ["examples", "coverage", "fuzzing", "stateful"]
353
354
  "--generation-codec",
354
355
  help="The codec used for generating strings",
355
356
  type=str,
356
- default="utf-8",
357
+ default=None,
357
358
  callback=validation.validate_generation_codec,
358
359
  )
359
360
  @grouped_option(
@@ -371,7 +372,7 @@ DEFAULT_PHASES = ["examples", "coverage", "fuzzing", "stateful"]
371
372
  "--generation-with-security-parameters",
372
373
  help="Whether to generate security parameters",
373
374
  type=str,
374
- default="true",
375
+ default=None,
375
376
  show_default=True,
376
377
  callback=validation.convert_boolean_string,
377
378
  metavar="BOOLEAN",
@@ -380,7 +381,7 @@ DEFAULT_PHASES = ["examples", "coverage", "fuzzing", "stateful"]
380
381
  "--generation-graphql-allow-null",
381
382
  help="Whether to use `null` values for optional arguments in GraphQL queries",
382
383
  type=str,
383
- default="true",
384
+ default=None,
384
385
  show_default=True,
385
386
  callback=validation.convert_boolean_string,
386
387
  metavar="BOOLEAN",
@@ -398,7 +399,7 @@ DEFAULT_PHASES = ["examples", "coverage", "fuzzing", "stateful"]
398
399
  "generation_unique_inputs",
399
400
  help="Force the generation of unique test cases",
400
401
  is_flag=True,
401
- default=False,
402
+ default=None,
402
403
  show_default=True,
403
404
  metavar="BOOLEAN",
404
405
  )
@@ -440,15 +441,15 @@ def run(
440
441
  exclude_operation_id_regex: str | None,
441
442
  include_by: Callable | None = None,
442
443
  exclude_by: Callable | None = None,
443
- exclude_deprecated: bool = False,
444
- workers: int = DEFAULT_WORKERS,
444
+ exclude_deprecated: bool | None = None,
445
+ workers: int | None = None,
445
446
  base_url: str | None,
446
447
  wait_for_schema: float | None = None,
447
448
  suppress_health_check: list[HealthCheck] | None,
448
449
  warnings: bool | list[SchemathesisWarning] | None,
449
450
  rate_limit: str | None = None,
450
451
  request_timeout: int | None = None,
451
- request_tls_verify: bool = True,
452
+ request_tls_verify: bool | None = None,
452
453
  request_cert: str | None = None,
453
454
  request_cert_key: str | None = None,
454
455
  request_proxy: str | None = None,
@@ -457,21 +458,21 @@ def run(
457
458
  report_junit_path: LazyFile | None = None,
458
459
  report_vcr_path: LazyFile | None = None,
459
460
  report_har_path: LazyFile | None = None,
460
- report_preserve_bytes: bool = False,
461
- output_sanitize: bool = True,
462
- output_truncate: bool = True,
461
+ report_preserve_bytes: bool | None = None,
462
+ output_sanitize: bool | None = None,
463
+ output_truncate: bool | None = None,
463
464
  generation_modes: list[GenerationMode],
464
465
  generation_seed: int | None = None,
465
466
  generation_max_examples: int | None = None,
466
467
  generation_maximize: list[MetricFunction] | None,
467
- generation_deterministic: bool = False,
468
+ generation_deterministic: bool | None = None,
468
469
  generation_database: str | None = None,
469
- generation_unique_inputs: bool = False,
470
- generation_allow_x00: bool = True,
471
- generation_graphql_allow_null: bool = True,
472
- generation_with_security_parameters: bool = True,
473
- generation_codec: str = "utf-8",
474
- generation_no_shrink: bool = False,
470
+ generation_unique_inputs: bool | None = None,
471
+ generation_allow_x00: bool | None = None,
472
+ generation_graphql_allow_null: bool | None = None,
473
+ generation_with_security_parameters: bool | None = None,
474
+ generation_codec: str | None = None,
475
+ generation_no_shrink: bool | None = None,
475
476
  force_color: bool = False,
476
477
  no_color: bool = False,
477
478
  **__kwargs: Any,
@@ -73,7 +73,11 @@ def validate_base_url(ctx: click.core.Context, param: click.core.Parameter, raw_
73
73
  return raw_value
74
74
 
75
75
 
76
- def validate_generation_codec(ctx: click.core.Context, param: click.core.Parameter, raw_value: str) -> str:
76
+ def validate_generation_codec(
77
+ ctx: click.core.Context, param: click.core.Parameter, raw_value: str | None
78
+ ) -> str | None:
79
+ if raw_value is None:
80
+ return raw_value
77
81
  try:
78
82
  codecs.getencoder(raw_value)
79
83
  except LookupError as exc:
@@ -214,7 +218,11 @@ def convert_generation_mode(ctx: click.core.Context, param: click.core.Parameter
214
218
  return [GenerationMode(value)]
215
219
 
216
220
 
217
- def convert_boolean_string(ctx: click.core.Context, param: click.core.Parameter, value: str) -> str | bool:
221
+ def convert_boolean_string(
222
+ ctx: click.core.Context, param: click.core.Parameter, value: str | None
223
+ ) -> str | bool | None:
224
+ if value is None:
225
+ return value
218
226
  return string_to_boolean(value)
219
227
 
220
228
 
@@ -226,7 +234,9 @@ def reraise_format_error(raw_value: str) -> Generator[None, None, None]:
226
234
  raise click.BadParameter(f"Expected KEY:VALUE format, received {raw_value}.") from exc
227
235
 
228
236
 
229
- def convert_workers(ctx: click.core.Context, param: click.core.Parameter, value: str) -> int:
237
+ def convert_workers(ctx: click.core.Context, param: click.core.Parameter, value: str | None) -> int | None:
238
+ if value is None:
239
+ return value
230
240
  if value == "auto":
231
241
  return get_workers_count()
232
242
  return int(value)
@@ -105,34 +105,34 @@ class GenerationConfig(DiffBase):
105
105
  *,
106
106
  modes: list[GenerationMode] | None = None,
107
107
  max_examples: int | None = None,
108
- no_shrink: bool = False,
108
+ no_shrink: bool | None = None,
109
109
  deterministic: bool | None = None,
110
- allow_x00: bool = True,
110
+ allow_x00: bool | None = None,
111
111
  codec: str | None = None,
112
112
  maximize: list[MetricFunction] | None = None,
113
113
  with_security_parameters: bool | None = None,
114
- graphql_allow_null: bool = True,
114
+ graphql_allow_null: bool | None = None,
115
115
  database: str | None = None,
116
- unique_inputs: bool = False,
116
+ unique_inputs: bool | None = None,
117
117
  exclude_header_characters: str | None = None,
118
118
  ) -> None:
119
119
  if modes is not None:
120
120
  self.modes = modes
121
121
  if max_examples is not None:
122
122
  self.max_examples = max_examples
123
- self.no_shrink = no_shrink
123
+ self.no_shrink = no_shrink or False
124
124
  self.deterministic = deterministic or False
125
- self.allow_x00 = allow_x00
125
+ self.allow_x00 = allow_x00 if allow_x00 is not None else True
126
126
  if codec is not None:
127
127
  self.codec = codec
128
128
  if maximize is not None:
129
129
  self.maximize = maximize
130
130
  if with_security_parameters is not None:
131
131
  self.with_security_parameters = with_security_parameters
132
- self.graphql_allow_null = graphql_allow_null
132
+ self.graphql_allow_null = graphql_allow_null if graphql_allow_null is not None else True
133
133
  if database is not None:
134
134
  self.database = database
135
- self.unique_inputs = unique_inputs
135
+ self.unique_inputs = unique_inputs or False
136
136
  if exclude_header_characters is not None:
137
137
  self.exclude_header_characters = exclude_header_characters
138
138
 
@@ -98,7 +98,7 @@ class ProjectConfig(DiffBase):
98
98
  workers: int | Literal["auto"] = DEFAULT_WORKERS,
99
99
  proxy: str | None = None,
100
100
  continue_on_failure: bool | None = None,
101
- tls_verify: bool | str | None = None,
101
+ tls_verify: bool | str = True,
102
102
  rate_limit: str | None = None,
103
103
  request_timeout: float | int | None = None,
104
104
  request_cert: str | None = None,
@@ -155,7 +155,7 @@ class ProjectConfig(DiffBase):
155
155
  workers=data.get("workers", DEFAULT_WORKERS),
156
156
  proxy=resolve(data.get("proxy")),
157
157
  continue_on_failure=data.get("continue-on-failure", None),
158
- tls_verify=resolve(data.get("tls-verify")),
158
+ tls_verify=resolve(data.get("tls-verify", True)),
159
159
  rate_limit=resolve(data.get("rate-limit")),
160
160
  request_timeout=data.get("request-timeout"),
161
161
  request_cert=resolve(data.get("request-cert")),
@@ -94,7 +94,7 @@ class ReportsConfig(DiffBase):
94
94
  vcr_path: str | None = None,
95
95
  har_path: str | None = None,
96
96
  directory: Path = DEFAULT_REPORT_DIRECTORY,
97
- preserve_bytes: bool = False,
97
+ preserve_bytes: bool | None = None,
98
98
  ) -> None:
99
99
  formats = formats or []
100
100
  if junit_path is not None or ReportFormat.JUNIT in formats:
@@ -8,8 +8,9 @@ from functools import lru_cache, partial
8
8
  from itertools import combinations
9
9
  from json.encoder import _make_iterencode, c_make_encoder, encode_basestring_ascii # type: ignore
10
10
  from typing import Any, Callable, Generator, Iterator, TypeVar, cast
11
+ from urllib.parse import quote_plus
11
12
 
12
- import jsonschema
13
+ import jsonschema.protocols
13
14
  from hypothesis import strategies as st
14
15
  from hypothesis.errors import InvalidArgument, Unsatisfiable
15
16
  from hypothesis_jsonschema import from_schema
@@ -19,7 +20,7 @@ from hypothesis_jsonschema._from_schema import STRING_FORMATS as BUILT_IN_STRING
19
20
  from schemathesis.core import INTERNAL_BUFFER_SIZE, NOT_SET
20
21
  from schemathesis.core.compat import RefResolutionError
21
22
  from schemathesis.core.transforms import deepclone
22
- from schemathesis.core.validation import has_invalid_characters, is_latin_1_encodable
23
+ from schemathesis.core.validation import contains_unicode_surrogate_pair, has_invalid_characters, is_latin_1_encodable
23
24
  from schemathesis.generation import GenerationMode
24
25
  from schemathesis.generation.hypothesis import examples
25
26
  from schemathesis.openapi.generation.filters import is_invalid_path_parameter
@@ -34,7 +35,7 @@ def _replace_zero_with_nonzero(x: float) -> float:
34
35
 
35
36
 
36
37
  def json_recursive_strategy(strategy: st.SearchStrategy) -> st.SearchStrategy:
37
- return st.lists(strategy, max_size=3) | st.dictionaries(st.text(), strategy, max_size=3)
38
+ return st.lists(strategy, max_size=2) | st.dictionaries(st.text(), strategy, max_size=2)
38
39
 
39
40
 
40
41
  NEGATIVE_MODE_MAX_LENGTH_WITH_PATTERN = 100
@@ -42,10 +43,12 @@ NEGATIVE_MODE_MAX_ITEMS = 15
42
43
  FLOAT_STRATEGY: st.SearchStrategy = st.floats(allow_nan=False, allow_infinity=False).map(_replace_zero_with_nonzero)
43
44
  NUMERIC_STRATEGY: st.SearchStrategy = st.integers() | FLOAT_STRATEGY
44
45
  JSON_STRATEGY: st.SearchStrategy = st.recursive(
45
- st.none() | st.booleans() | NUMERIC_STRATEGY | st.text(), json_recursive_strategy
46
+ st.none() | st.booleans() | NUMERIC_STRATEGY | st.text(max_size=16),
47
+ json_recursive_strategy,
48
+ max_leaves=2,
46
49
  )
47
- ARRAY_STRATEGY: st.SearchStrategy = st.lists(JSON_STRATEGY, min_size=2)
48
- OBJECT_STRATEGY: st.SearchStrategy = st.dictionaries(st.text(), JSON_STRATEGY)
50
+ ARRAY_STRATEGY: st.SearchStrategy = st.lists(JSON_STRATEGY, min_size=2, max_size=3)
51
+ OBJECT_STRATEGY: st.SearchStrategy = st.dictionaries(st.text(max_size=16), JSON_STRATEGY, max_size=2)
49
52
 
50
53
 
51
54
  STRATEGIES_FOR_TYPE = {
@@ -112,8 +115,9 @@ class CoverageContext:
112
115
  is_required: bool
113
116
  path: list[str | int]
114
117
  custom_formats: dict[str, st.SearchStrategy]
118
+ validator_cls: type[jsonschema.protocols.Validator]
115
119
 
116
- __slots__ = ("location", "generation_modes", "is_required", "path", "custom_formats")
120
+ __slots__ = ("location", "generation_modes", "is_required", "path", "custom_formats", "validator_cls")
117
121
 
118
122
  def __init__(
119
123
  self,
@@ -123,12 +127,14 @@ class CoverageContext:
123
127
  is_required: bool,
124
128
  path: list[str | int] | None = None,
125
129
  custom_formats: dict[str, st.SearchStrategy],
130
+ validator_cls: type[jsonschema.protocols.Validator],
126
131
  ) -> None:
127
132
  self.location = location
128
133
  self.generation_modes = generation_modes if generation_modes is not None else list(GenerationMode)
129
134
  self.is_required = is_required
130
135
  self.path = path or []
131
136
  self.custom_formats = custom_formats
137
+ self.validator_cls = validator_cls
132
138
 
133
139
  @contextmanager
134
140
  def at(self, key: str | int) -> Generator[None, None, None]:
@@ -149,6 +155,7 @@ class CoverageContext:
149
155
  is_required=self.is_required,
150
156
  path=self.path,
151
157
  custom_formats=self.custom_formats,
158
+ validator_cls=self.validator_cls,
152
159
  )
153
160
 
154
161
  def with_negative(self) -> CoverageContext:
@@ -158,6 +165,7 @@ class CoverageContext:
158
165
  is_required=self.is_required,
159
166
  path=self.path,
160
167
  custom_formats=self.custom_formats,
168
+ validator_cls=self.validator_cls,
161
169
  )
162
170
 
163
171
  def is_valid_for_location(self, value: Any) -> bool:
@@ -173,20 +181,21 @@ class CoverageContext:
173
181
  if isinstance(value, list) and not self.is_required:
174
182
  # Optional parameters should be present
175
183
  return any(item not in [{}, []] for item in value)
176
- if isinstance(value, dict) and not self.is_required:
177
- return bool(value)
178
184
  return True
179
185
 
186
+ def will_be_serialized_to_string(self) -> bool:
187
+ return self.location in ("query", "path", "header", "cookie")
188
+
180
189
  def can_be_negated(self, schema: dict[str, Any]) -> bool:
181
190
  # Path, query, header, and cookie parameters will be stringified anyway
182
191
  # If there are no constraints, then anything will match the original schema after serialization
183
- if self.location in ("query", "path", "header", "cookie"):
192
+ if self.will_be_serialized_to_string():
184
193
  cleaned = {
185
194
  k: v
186
195
  for k, v in schema.items()
187
196
  if not k.startswith("x-") and k not in ["description", "example", "examples"]
188
197
  }
189
- return cleaned != {}
198
+ return cleaned not in [{}, {"type": "string"}]
190
199
  return True
191
200
 
192
201
  def generate_from(self, strategy: st.SearchStrategy) -> Any:
@@ -224,6 +233,9 @@ class CoverageContext:
224
233
  return cached_draw(st.integers(min_value=schema.get("minimum"), max_value=schema.get("maximum")))
225
234
  if "enum" in schema:
226
235
  return cached_draw(st.sampled_from(schema["enum"]))
236
+ if keys == ["multipleOf", "type"] and schema["type"] in ("integer", "number"):
237
+ step = schema["multipleOf"]
238
+ return cached_draw(st.integers().map(step.__mul__))
227
239
  if "pattern" in schema:
228
240
  pattern = schema["pattern"]
229
241
  try:
@@ -411,7 +423,7 @@ def cover_schema_iter(
411
423
  for value_ in _negative_enum(ctx, [value], seen):
412
424
  yield value_
413
425
  elif key == "type":
414
- yield from _negative_type(ctx, value, seen)
426
+ yield from _negative_type(ctx, value, seen, schema)
415
427
  elif key == "properties":
416
428
  template = template or ctx.generate_from_schema(_get_template_schema(schema, "object"))
417
429
  yield from _negative_properties(ctx, template, value)
@@ -1038,7 +1050,7 @@ def _negative_multiple_of(
1038
1050
 
1039
1051
 
1040
1052
  def _negative_unique_items(ctx: CoverageContext, schema: dict) -> Generator[GeneratedValue, None, None]:
1041
- unique = ctx.generate_from_schema({**schema, "type": "array", "minItems": 1, "maxItems": 1})
1053
+ unique = jsonify(ctx.generate_from_schema({**schema, "type": "array", "minItems": 1, "maxItems": 1}))
1042
1054
  yield NegativeValue(unique + unique, description="Non-unique items", location=ctx.current_path)
1043
1055
 
1044
1056
 
@@ -1086,22 +1098,120 @@ def _is_non_integer_float(x: float) -> bool:
1086
1098
  return x != int(x)
1087
1099
 
1088
1100
 
1101
+ def is_valid_header_value(value: Any) -> bool:
1102
+ value = str(value)
1103
+ if not is_latin_1_encodable(value):
1104
+ return False
1105
+ if has_invalid_characters("A", value):
1106
+ return False
1107
+ return True
1108
+
1109
+
1110
+ def jsonify(value: Any) -> Any:
1111
+ if isinstance(value, bool):
1112
+ return "true" if value else "false"
1113
+ elif value is None:
1114
+ return "null"
1115
+
1116
+ stack: list = [value]
1117
+ while stack:
1118
+ item = stack.pop()
1119
+ if isinstance(item, dict):
1120
+ for key, sub_item in item.items():
1121
+ if isinstance(sub_item, bool):
1122
+ item[key] = "true" if sub_item else "false"
1123
+ elif sub_item is None:
1124
+ item[key] = "null"
1125
+ elif isinstance(sub_item, dict):
1126
+ stack.append(sub_item)
1127
+ elif isinstance(sub_item, list):
1128
+ stack.extend(item)
1129
+ elif isinstance(item, list):
1130
+ for idx, sub_item in enumerate(item):
1131
+ if isinstance(sub_item, bool):
1132
+ item[idx] = "true" if sub_item else "false"
1133
+ elif sub_item is None:
1134
+ item[idx] = "null"
1135
+ else:
1136
+ stack.extend(item)
1137
+ return value
1138
+
1139
+
1140
+ def quote_path_parameter(value: Any) -> str:
1141
+ if isinstance(value, str):
1142
+ if value == ".":
1143
+ return "%2E"
1144
+ elif value == "..":
1145
+ return "%2E%2E"
1146
+ else:
1147
+ return quote_plus(value)
1148
+ if isinstance(value, list):
1149
+ return ",".join(map(str, value))
1150
+ return str(value)
1151
+
1152
+
1089
1153
  def _negative_type(
1090
- ctx: CoverageContext,
1091
- ty: str | list[str],
1092
- seen: HashSet,
1154
+ ctx: CoverageContext, ty: str | list[str], seen: HashSet, schema: dict[str, Any]
1093
1155
  ) -> Generator[GeneratedValue, None, None]:
1094
1156
  if isinstance(ty, str):
1095
1157
  types = [ty]
1096
1158
  else:
1097
1159
  types = ty
1098
1160
  strategies = {ty: strategy for ty, strategy in STRATEGIES_FOR_TYPE.items() if ty not in types}
1161
+
1162
+ filter_func = {
1163
+ "path": lambda x: not is_invalid_path_parameter(x),
1164
+ "header": is_valid_header_value,
1165
+ "cookie": is_valid_header_value,
1166
+ "query": lambda x: not contains_unicode_surrogate_pair(x),
1167
+ }.get(ctx.location)
1168
+
1099
1169
  if "number" in types:
1100
1170
  del strategies["integer"]
1101
1171
  if "integer" in types:
1102
1172
  strategies["number"] = FLOAT_STRATEGY.filter(_is_non_integer_float)
1103
1173
  if ctx.location == "query":
1104
1174
  strategies.pop("object", None)
1175
+ if filter_func is not None:
1176
+ for ty, strategy in strategies.items():
1177
+ strategies[ty] = strategy.filter(filter_func)
1178
+
1179
+ pattern = schema.get("pattern")
1180
+ if pattern is not None:
1181
+ try:
1182
+ re.compile(pattern)
1183
+ except re.error:
1184
+ schema = schema.copy()
1185
+ del schema["pattern"]
1186
+ return
1187
+
1188
+ validator = ctx.validator_cls(
1189
+ schema,
1190
+ format_checker=jsonschema.Draft202012Validator.FORMAT_CHECKER,
1191
+ )
1192
+ is_valid = validator.is_valid
1193
+ try:
1194
+ is_valid(None)
1195
+ apply_validation = True
1196
+ except Exception:
1197
+ # Schema is not correct and we can't validate the generated instances.
1198
+ # In such a scenario it is better to generate at least something with some chances to have a false
1199
+ # positive failure
1200
+ apply_validation = False
1201
+
1202
+ def _does_not_match_the_original_schema(value: Any) -> bool:
1203
+ return not is_valid(str(value))
1204
+
1205
+ if ctx.location == "path":
1206
+ for ty, strategy in strategies.items():
1207
+ strategies[ty] = strategy.map(jsonify).map(quote_path_parameter)
1208
+ elif ctx.location == "query":
1209
+ for ty, strategy in strategies.items():
1210
+ strategies[ty] = strategy.map(jsonify)
1211
+
1212
+ if apply_validation and ctx.will_be_serialized_to_string():
1213
+ for ty, strategy in strategies.items():
1214
+ strategies[ty] = strategy.filter(_does_not_match_the_original_schema)
1105
1215
  for strategy in strategies.values():
1106
1216
  value = ctx.generate_from(strategy)
1107
1217
  if seen.insert(value) and ctx.is_valid_for_location(value):
@@ -22,6 +22,7 @@ from schemathesis.config import GenerationConfig, ProjectConfig
22
22
  from schemathesis.core import NOT_SET, NotSet, SpecificationFeature, media_types
23
23
  from schemathesis.core.errors import InvalidSchema, SerializationNotPossible
24
24
  from schemathesis.core.marks import Mark
25
+ from schemathesis.core.transforms import deepclone
25
26
  from schemathesis.core.transport import prepare_urlencoded
26
27
  from schemathesis.core.validation import has_invalid_characters, is_latin_1_encodable
27
28
  from schemathesis.generation import GenerationMode, coverage
@@ -397,7 +398,7 @@ class Template:
397
398
  return output
398
399
 
399
400
  def unmodified(self) -> TemplateValue:
400
- kwargs = self._template.copy()
401
+ kwargs = deepclone(self._template)
401
402
  kwargs = self._serialize(kwargs)
402
403
  return TemplateValue(kwargs=kwargs, components=self._components.copy())
403
404
 
@@ -462,6 +463,7 @@ def _iter_coverage_cases(
462
463
  from schemathesis.specs.openapi._hypothesis import _build_custom_formats
463
464
  from schemathesis.specs.openapi.constants import LOCATION_TO_CONTAINER
464
465
  from schemathesis.specs.openapi.examples import find_in_responses, find_matching_in_responses
466
+ from schemathesis.specs.openapi.schemas import BaseOpenAPISchema
465
467
  from schemathesis.specs.openapi.serialization import get_serializers_for_operation
466
468
 
467
469
  generators: dict[tuple[str, str], Generator[coverage.GeneratedValue, None, None]] = {}
@@ -476,6 +478,8 @@ def _iter_coverage_cases(
476
478
 
477
479
  seen_negative = coverage.HashSet()
478
480
  seen_positive = coverage.HashSet()
481
+ assert isinstance(operation.schema, BaseOpenAPISchema)
482
+ validator_cls = operation.schema.validator_cls
479
483
 
480
484
  for parameter in operation.iter_parameters():
481
485
  location = parameter.location
@@ -489,6 +493,7 @@ def _iter_coverage_cases(
489
493
  generation_modes=generation_modes,
490
494
  is_required=parameter.is_required,
491
495
  custom_formats=custom_formats,
496
+ validator_cls=validator_cls,
492
497
  ),
493
498
  schema,
494
499
  )
@@ -513,6 +518,7 @@ def _iter_coverage_cases(
513
518
  generation_modes=generation_modes,
514
519
  is_required=body.is_required,
515
520
  custom_formats=custom_formats,
521
+ validator_cls=validator_cls,
516
522
  ),
517
523
  schema,
518
524
  )
@@ -658,14 +664,14 @@ def _iter_coverage_cases(
658
664
  name = parameter.name
659
665
  location = parameter.location
660
666
  container_name = LOCATION_TO_CONTAINER[location]
661
- # NOTE: if the schema is overly permissive we may not have any negative test cases
662
- if container_name in template:
663
- container = template[container_name]
664
- data = template.with_container(
665
- container_name=container_name,
666
- value={k: v for k, v in container.items() if k != name},
667
- generation_mode=GenerationMode.NEGATIVE,
668
- )
667
+ container = template.get(container_name, {})
668
+ data = template.with_container(
669
+ container_name=container_name,
670
+ value={k: v for k, v in container.items() if k != name},
671
+ generation_mode=GenerationMode.NEGATIVE,
672
+ )
673
+
674
+ if seen_negative.insert(data.kwargs):
669
675
  yield operation.Case(
670
676
  **data.kwargs,
671
677
  _meta=CaseMetadata(
@@ -747,6 +753,7 @@ def _iter_coverage_cases(
747
753
  generation_modes=[GenerationMode.NEGATIVE],
748
754
  is_required=is_required,
749
755
  custom_formats=custom_formats,
756
+ validator_cls=validator_cls,
750
757
  ),
751
758
  subschema,
752
759
  )
@@ -195,7 +195,7 @@ class BaseOpenAPISchema(BaseSchema):
195
195
  self.resolver.push_scope(scope)
196
196
  try:
197
197
  for method, definition in path_item.items():
198
- if method not in HTTP_METHODS:
198
+ if method not in HTTP_METHODS or not definition:
199
199
  continue
200
200
  statistic.operations.total += 1
201
201
  is_selected = not should_skip(path, method, definition)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: schemathesis
3
- Version: 4.0.13
3
+ Version: 4.0.15
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://github.com/schemathesis/schemathesis/blob/master/CHANGELOG.md
@@ -10,15 +10,15 @@ schemathesis/cli/__init__.py,sha256=U9gjzWWpiFhaqevPjZbwyTNjABdpvXETI4HgwdGKnvs,
10
10
  schemathesis/cli/__main__.py,sha256=MWaenjaUTZIfNPFzKmnkTiawUri7DVldtg3mirLwzU8,92
11
11
  schemathesis/cli/constants.py,sha256=CVcQNHEiX-joAQmyuEVKWPOSxDHsOw_EXXZsEclzLuY,341
12
12
  schemathesis/cli/core.py,sha256=ue7YUdVo3YvuzGL4s6i62NL6YqNDeVPBSnQ1znrvG2w,480
13
- schemathesis/cli/commands/__init__.py,sha256=rubTCCRGuMIbNYOl8yQEioiuHtTq__tWjkUtFWYGhqQ,3433
13
+ schemathesis/cli/commands/__init__.py,sha256=DNzKEnXu7GjGSVe0244ZErmygUBA3nGSyVY6JP3ixD0,3740
14
14
  schemathesis/cli/commands/data.py,sha256=_ALywjIeCZjuaoDQFy-Kj8RZkEGqXd-Y95O47h8Jszs,171
15
- schemathesis/cli/commands/run/__init__.py,sha256=qr6wSZSQMbLDNqsEyChLJr0616GuY1Wcg5gHjoTbPss,18648
15
+ schemathesis/cli/commands/run/__init__.py,sha256=F8KgDQwWRqbJxnAL9nHREphcgGW-Ghn-kbe4yAquadw,18686
16
16
  schemathesis/cli/commands/run/context.py,sha256=taegOHWc_B-HDwiU1R9Oi4q57mdfLXc-B954QUj8t7A,7984
17
17
  schemathesis/cli/commands/run/events.py,sha256=ew0TQOc9T2YBZynYWv95k9yfAk8-hGuZDLMxjT8EhvY,1595
18
18
  schemathesis/cli/commands/run/executor.py,sha256=kFbZw583SZ-jqjv8goTp2yEJOpZ_bzecyTeZvdc6qTE,5327
19
19
  schemathesis/cli/commands/run/filters.py,sha256=pzkNRcf5vLPSsMfnvt711GNzRSBK5iZIFjPA0fiH1N4,1701
20
20
  schemathesis/cli/commands/run/loaders.py,sha256=6j0ez7wduAUYbUT28ELKxMf-dYEWr_67m_KIuTSyNGk,4358
21
- schemathesis/cli/commands/run/validation.py,sha256=FzCzYdW1-hn3OgyzPO1p6wHEX5PG7HdewbPRvclV_vc,9002
21
+ schemathesis/cli/commands/run/validation.py,sha256=DQaMiBLN2tYT9hONvv8xnyPvNXZH768UlOdUxTd5kZs,9193
22
22
  schemathesis/cli/commands/run/handlers/__init__.py,sha256=TPZ3KdGi8m0fjlN0GjA31MAXXn1qI7uU4FtiDwroXZI,1915
23
23
  schemathesis/cli/commands/run/handlers/base.py,sha256=yDsTtCiztLksfk7cRzg8JlaAVOfS-zwK3tsJMOXAFyc,530
24
24
  schemathesis/cli/commands/run/handlers/cassettes.py,sha256=rRD4byjp4HXCkJS-zx3jSIFOJsPq77ejPpYeyCtsEZs,19461
@@ -34,15 +34,15 @@ schemathesis/config/_checks.py,sha256=F0r16eSSiICvoiTUkNNOE2PH73EGd8bikoeZdME_3Y
34
34
  schemathesis/config/_diff_base.py,sha256=-XqS6cTzZC5fplz8_2RSZHDMSAPJhBBIEP6H8wcgHmo,4221
35
35
  schemathesis/config/_env.py,sha256=8XfIyrnGNQuCDnfG0lwmKRFbasRUjgeQGBAMupsmtOU,613
36
36
  schemathesis/config/_error.py,sha256=TxuuqQ1olwJc7P7ssfxXb1dB_Xn5uVsoazjvYvRxrxA,5437
37
- schemathesis/config/_generation.py,sha256=_THqCfC20i8RRRsO2hAwoJ52FV-CS1xOA6me3Wp3FHw,5087
37
+ schemathesis/config/_generation.py,sha256=giWs4z17z9nRe_9Z3mAZ3LEoyh4hkcJnlAA6LSy6iEo,5210
38
38
  schemathesis/config/_health_check.py,sha256=zC9inla5ibMBlEy5WyM4_TME7ju_KH3Bwfo21RI3Gks,561
39
39
  schemathesis/config/_operations.py,sha256=2M36b4MMoFtaaFpe9yG-aWRqh0Qm1dpdk5M0V23X2yA,12129
40
40
  schemathesis/config/_output.py,sha256=3G9SOi-4oNcQPHeNRG3HggFCwvcKOW1kF28a9m0H-pU,4434
41
41
  schemathesis/config/_parameters.py,sha256=i76Hwaf834fBAMmtKfKTl1SFCicJ-Y-5tZt5QNGW2fA,618
42
42
  schemathesis/config/_phases.py,sha256=NFUhn-xzEQdNtgNVW1t51lMquXbjRNGR_QuiCRLCi28,6454
43
- schemathesis/config/_projects.py,sha256=1rbI178wv743492FnexjdlsGvggNYpsbVUzQcUcJhAk,19487
43
+ schemathesis/config/_projects.py,sha256=_fbivneAqa2Y7sCX0T1CBSjo3CHPD1qLZcJYsYnWpQk,19486
44
44
  schemathesis/config/_rate_limit.py,sha256=ekEW-jP_Ichk_O6hYpj-h2TTTKfp7Fm0nyFUbvlWcbA,456
45
- schemathesis/config/_report.py,sha256=aYLnPO74B7Wfn_qTwlEp5zY9L74U1XFuYS10yjwKKWY,3885
45
+ schemathesis/config/_report.py,sha256=ZECDpaCY4WWHD5UbjvgZoSjLz-rlTvfd5Ivzdgzqf2I,3891
46
46
  schemathesis/config/_validator.py,sha256=IcE8geFZ0ZwR18rkIRs25i7pTl7Z84XbjYGUB-mqReU,258
47
47
  schemathesis/config/_warnings.py,sha256=sI0VZcTj3dOnphhBwYwU_KTagxr89HGWTtQ99HcY84k,772
48
48
  schemathesis/config/schema.json,sha256=wC1qe9M_fXotfmlBOmW_FCTRw9K5YC814-PipMGKllE,18907
@@ -85,13 +85,13 @@ schemathesis/engine/phases/unit/_executor.py,sha256=9MmZoKSBVSPk0LWwN3PZ3iaO9nzp
85
85
  schemathesis/engine/phases/unit/_pool.py,sha256=iU0hdHDmohPnEv7_S1emcabuzbTf-Cznqwn0pGQ5wNQ,2480
86
86
  schemathesis/generation/__init__.py,sha256=tvNO2FLiY8z3fZ_kL_QJhSgzXfnT4UqwSXMHCwfLI0g,645
87
87
  schemathesis/generation/case.py,sha256=zwAwFQ-Fp7SOxCXYOQyAdwAtNwVJe63PdLpvqackFQY,12296
88
- schemathesis/generation/coverage.py,sha256=0d52xHf1HUbilPmCeJlplnw7knSdc0lEv4Hr0HXYUTE,49949
88
+ schemathesis/generation/coverage.py,sha256=jpl1GpAGZXyoO2aZLUZlIE0h_ygdHMnvUK14oBg51k4,53896
89
89
  schemathesis/generation/meta.py,sha256=adkoMuCfzSjHJ9ZDocQn0GnVldSCkLL3eVR5A_jafwM,2552
90
90
  schemathesis/generation/metrics.py,sha256=cZU5HdeAMcLFEDnTbNE56NuNq4P0N4ew-g1NEz5-kt4,2836
91
91
  schemathesis/generation/modes.py,sha256=Q1fhjWr3zxabU5qdtLvKfpMFZJAwlW9pnxgenjeXTyU,481
92
92
  schemathesis/generation/overrides.py,sha256=OBWqDQPreiliaf2M-oyXppVKHoJkCRzxtwSJx1b6AFw,3759
93
93
  schemathesis/generation/hypothesis/__init__.py,sha256=SVwM-rx07jPZzms0idWYACgUtWAxh49HRuTnaQ__zf0,1549
94
- schemathesis/generation/hypothesis/builder.py,sha256=EPJkeyeirU-pMuD4NGrY1e4HRp0cmBNg_1NLRFRxOfk,34550
94
+ schemathesis/generation/hypothesis/builder.py,sha256=ujPp9ByUfNYdxWyKoJlxE0h8pAVBpmNJwWIi9MnR968,34824
95
95
  schemathesis/generation/hypothesis/examples.py,sha256=6eGaKUEC3elmKsaqfKj1sLvM8EHc-PWT4NRBq4NI0Rs,1409
96
96
  schemathesis/generation/hypothesis/given.py,sha256=sTZR1of6XaHAPWtHx2_WLlZ50M8D5Rjux0GmWkWjDq4,2337
97
97
  schemathesis/generation/hypothesis/reporting.py,sha256=uDVow6Ya8YFkqQuOqRsjbzsbyP4KKfr3jA7ZaY4FuKY,279
@@ -134,7 +134,7 @@ schemathesis/specs/openapi/media_types.py,sha256=F5M6TKl0s6Z5X8mZpPsWDEdPBvxclKR
134
134
  schemathesis/specs/openapi/parameters.py,sha256=ifu_QQCMUzsUHQAkvsOvLuokns6CzesssmQ3Nd3zxII,14594
135
135
  schemathesis/specs/openapi/patterns.py,sha256=cBj8W4wn7VLJd4nABaIH5f502-zBDiqljxLgPWUn-50,15609
136
136
  schemathesis/specs/openapi/references.py,sha256=40YcDExPLR2B8EOwt-Csw-5MYFi2xj_DXf91J0Pc9d4,8855
137
- schemathesis/specs/openapi/schemas.py,sha256=knaGUtxcy4CKeOpvTrVgPifnqKh9eSYdXRPhExFKElk,52539
137
+ schemathesis/specs/openapi/schemas.py,sha256=FXD6yLindLNtjiW5D_GllWxfMAigSKjpjl8_GbqJXxE,52557
138
138
  schemathesis/specs/openapi/security.py,sha256=6UWYMhL-dPtkTineqqBFNKca1i4EuoTduw-EOLeE0aQ,7149
139
139
  schemathesis/specs/openapi/serialization.py,sha256=VdDLmeHqxlWM4cxQQcCkvrU6XurivolwEEaT13ohelA,11972
140
140
  schemathesis/specs/openapi/utils.py,sha256=ER4vJkdFVDIE7aKyxyYatuuHVRNutytezgE52pqZNE8,900
@@ -157,8 +157,8 @@ schemathesis/transport/prepare.py,sha256=erYXRaxpQokIDzaIuvt_csHcw72iHfCyNq8VNEz
157
157
  schemathesis/transport/requests.py,sha256=rziZTrZCVMAqgy6ldB8iTwhkpAsnjKSgK8hj5Sq3ThE,10656
158
158
  schemathesis/transport/serialization.py,sha256=igUXKZ_VJ9gV7P0TUc5PDQBJXl_s0kK9T3ljGWWvo6E,10339
159
159
  schemathesis/transport/wsgi.py,sha256=KoAfvu6RJtzyj24VGB8e-Iaa9smpgXJ3VsM8EgAz2tc,6152
160
- schemathesis-4.0.13.dist-info/METADATA,sha256=2E4DqYcjnW29smDSYVi-vCZVU_oFIYqvRGVZgHOAniw,8472
161
- schemathesis-4.0.13.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
162
- schemathesis-4.0.13.dist-info/entry_points.txt,sha256=hiK3un-xfgPdwj9uj16YVDtTNpO128bmk0U82SMv8ZQ,152
163
- schemathesis-4.0.13.dist-info/licenses/LICENSE,sha256=2Ve4J8v5jMQAWrT7r1nf3bI8Vflk3rZVQefiF2zpxwg,1121
164
- schemathesis-4.0.13.dist-info/RECORD,,
160
+ schemathesis-4.0.15.dist-info/METADATA,sha256=nQbpPuX0uQ1CRaGdgF7Sor6E9pqtPKFYNHraxONfRdM,8472
161
+ schemathesis-4.0.15.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
162
+ schemathesis-4.0.15.dist-info/entry_points.txt,sha256=hiK3un-xfgPdwj9uj16YVDtTNpO128bmk0U82SMv8ZQ,152
163
+ schemathesis-4.0.15.dist-info/licenses/LICENSE,sha256=2Ve4J8v5jMQAWrT7r1nf3bI8Vflk3rZVQefiF2zpxwg,1121
164
+ schemathesis-4.0.15.dist-info/RECORD,,