schemathesis 3.39.9__py3-none-any.whl → 3.39.11__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.
@@ -3,7 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import asyncio
6
- import json
6
+ from dataclasses import dataclass
7
7
  import warnings
8
8
  from functools import wraps
9
9
  from itertools import combinations
@@ -15,8 +15,10 @@ from hypothesis.errors import HypothesisWarning, Unsatisfiable
15
15
  from hypothesis.internal.entropy import deterministic_PRNG
16
16
  from jsonschema.exceptions import SchemaError
17
17
 
18
+ from schemathesis.serializers import get_first_matching_media_type
19
+
18
20
  from . import _patches
19
- from .auths import get_auth_storage_from_test
21
+ from .auths import AuthStorage, get_auth_storage_from_test
20
22
  from .constants import DEFAULT_DEADLINE, NOT_SET
21
23
  from .exceptions import OperationSchemaError, SerializationNotPossible
22
24
  from .experimental import COVERAGE_PHASE
@@ -27,6 +29,7 @@ from .parameters import ParameterSet
27
29
  from .transports.content_types import parse_content_type
28
30
  from .transports.headers import has_invalid_characters, is_latin_1_encodable
29
31
  from .types import NotSet
32
+ from schemathesis import auths
30
33
 
31
34
  if TYPE_CHECKING:
32
35
  from .utils import GivenInput
@@ -111,7 +114,9 @@ def create_test(
111
114
  wrapped_test, operation, hook_dispatcher=hook_dispatcher, as_strategy_kwargs=as_strategy_kwargs
112
115
  )
113
116
  if COVERAGE_PHASE.is_enabled:
114
- wrapped_test = add_coverage(wrapped_test, operation, data_generation_methods)
117
+ wrapped_test = add_coverage(
118
+ wrapped_test, operation, data_generation_methods, auth_storage, as_strategy_kwargs
119
+ )
115
120
  return wrapped_test
116
121
 
117
122
 
@@ -215,31 +220,158 @@ def adjust_urlencoded_payload(case: Case) -> None:
215
220
 
216
221
 
217
222
  def add_coverage(
218
- test: Callable, operation: APIOperation, data_generation_methods: list[DataGenerationMethod]
223
+ test: Callable,
224
+ operation: APIOperation,
225
+ data_generation_methods: list[DataGenerationMethod],
226
+ auth_storage: AuthStorage | None,
227
+ as_strategy_kwargs: dict[str, Any],
219
228
  ) -> Callable:
220
- for example in _iter_coverage_cases(operation, data_generation_methods):
221
- adjust_urlencoded_payload(example)
222
- test = hypothesis.example(case=example)(test)
229
+ from schemathesis.specs.openapi.constants import LOCATION_TO_CONTAINER
230
+
231
+ auth_context = auths.AuthContext(
232
+ operation=operation,
233
+ app=operation.app,
234
+ )
235
+ overrides = {
236
+ container: as_strategy_kwargs[container]
237
+ for container in LOCATION_TO_CONTAINER.values()
238
+ if container in as_strategy_kwargs
239
+ }
240
+ for case in _iter_coverage_cases(operation, data_generation_methods):
241
+ if case.media_type and get_first_matching_media_type(case.media_type) is None:
242
+ continue
243
+ adjust_urlencoded_payload(case)
244
+ auths.set_on_case(case, auth_context, auth_storage)
245
+ for container_name, value in overrides.items():
246
+ container = getattr(case, container_name)
247
+ if container is None:
248
+ setattr(case, container_name, value)
249
+ else:
250
+ container.update(value)
251
+ test = hypothesis.example(case=case)(test)
223
252
  return test
224
253
 
225
254
 
255
+ class Template:
256
+ __slots__ = ("_components", "_template", "_serializers")
257
+
258
+ def __init__(self, serializers: dict[str, Callable]) -> None:
259
+ self._components: dict[str, DataGenerationMethod] = {}
260
+ self._template: dict[str, Any] = {}
261
+ self._serializers = serializers
262
+
263
+ def __contains__(self, key: str) -> bool:
264
+ return key in self._template
265
+
266
+ def __getitem__(self, key: str) -> dict:
267
+ return self._template[key]
268
+
269
+ def get(self, key: str, default: Any = None) -> dict:
270
+ return self._template.get(key, default)
271
+
272
+ def add_parameter(self, location: str, name: str, value: coverage.GeneratedValue) -> None:
273
+ from .specs.openapi.constants import LOCATION_TO_CONTAINER
274
+
275
+ component_name = LOCATION_TO_CONTAINER[location]
276
+ method = self._components.get(component_name)
277
+ if method is None:
278
+ self._components[component_name] = value.data_generation_method
279
+ elif value.data_generation_method == DataGenerationMethod.negative:
280
+ self._components[component_name] = DataGenerationMethod.negative
281
+
282
+ container = self._template.setdefault(component_name, {})
283
+ container[name] = value.value
284
+
285
+ def set_body(self, body: coverage.GeneratedValue, media_type: str) -> None:
286
+ self._template["body"] = body.value
287
+ self._template["media_type"] = media_type
288
+ self._components["body"] = body.data_generation_method
289
+
290
+ def _serialize(self, kwargs: dict[str, Any]) -> dict[str, Any]:
291
+ from schemathesis.specs.openapi._hypothesis import quote_all
292
+
293
+ output = {}
294
+ for container_name, value in kwargs.items():
295
+ serializer = self._serializers.get(container_name)
296
+ if container_name in ("headers", "cookies") and isinstance(value, dict):
297
+ value = _stringify_value(value, container_name)
298
+ if serializer is not None:
299
+ value = serializer(value)
300
+ if container_name == "query" and isinstance(value, dict):
301
+ value = _stringify_value(value, container_name)
302
+ if container_name == "path_parameters" and isinstance(value, dict):
303
+ value = _stringify_value(quote_all(value), container_name)
304
+ output[container_name] = value
305
+ return output
306
+
307
+ def unmodified(self) -> TemplateValue:
308
+ kwargs = self._template.copy()
309
+ kwargs = self._serialize(kwargs)
310
+ return TemplateValue(kwargs=kwargs, components=self._components.copy())
311
+
312
+ def with_body(self, *, media_type: str, value: coverage.GeneratedValue) -> TemplateValue:
313
+ kwargs = {**self._template, "media_type": media_type, "body": value.value}
314
+ kwargs = self._serialize(kwargs)
315
+ components = {**self._components, "body": value.data_generation_method}
316
+ return TemplateValue(kwargs=kwargs, components=components)
317
+
318
+ def with_parameter(self, *, location: str, name: str, value: coverage.GeneratedValue) -> TemplateValue:
319
+ from .specs.openapi.constants import LOCATION_TO_CONTAINER
320
+
321
+ container_name = LOCATION_TO_CONTAINER[location]
322
+ container = self._template[container_name]
323
+ return self.with_container(
324
+ container_name=container_name,
325
+ value={**container, name: value.value},
326
+ data_generation_method=value.data_generation_method,
327
+ )
328
+
329
+ def with_container(
330
+ self, *, container_name: str, value: Any, data_generation_method: DataGenerationMethod
331
+ ) -> TemplateValue:
332
+ kwargs = {**self._template, container_name: value}
333
+ kwargs = self._serialize(kwargs)
334
+ components = {**self._components, container_name: data_generation_method}
335
+ return TemplateValue(kwargs=kwargs, components=components)
336
+
337
+
338
+ @dataclass
339
+ class TemplateValue:
340
+ kwargs: dict[str, Any]
341
+ components: dict[str, DataGenerationMethod]
342
+ __slots__ = ("kwargs", "components")
343
+
344
+
345
+ def _stringify_value(val: Any, container_name: str) -> Any:
346
+ if val is None:
347
+ return "null"
348
+ if val is True:
349
+ return "true"
350
+ if val is False:
351
+ return "false"
352
+ if isinstance(val, (int, float)):
353
+ return str(val)
354
+ if isinstance(val, list):
355
+ if container_name == "query":
356
+ # Having a list here ensures there will be multiple query parameters wit the same name
357
+ return [_stringify_value(item, container_name) for item in val]
358
+ # use comma-separated values style for arrays
359
+ return ",".join(_stringify_value(sub, container_name) for sub in val)
360
+ if isinstance(val, dict):
361
+ return {key: _stringify_value(sub, container_name) for key, sub in val.items()}
362
+ return val
363
+
364
+
226
365
  def _iter_coverage_cases(
227
366
  operation: APIOperation, data_generation_methods: list[DataGenerationMethod]
228
367
  ) -> Generator[Case, None, None]:
229
368
  from .specs.openapi.constants import LOCATION_TO_CONTAINER
230
369
  from .specs.openapi.examples import find_in_responses, find_matching_in_responses
231
-
232
- def _stringify_value(val: Any, location: str) -> str | list[str]:
233
- if isinstance(val, list):
234
- if location == "query":
235
- # Having a list here ensures there will be multiple query parameters wit the same name
236
- return [json.dumps(item) for item in val]
237
- # use comma-separated values style for arrays
238
- return ",".join(json.dumps(sub) for sub in val)
239
- return json.dumps(val)
370
+ from schemathesis.specs.openapi.serialization import get_serializers_for_operation
240
371
 
241
372
  generators: dict[tuple[str, str], Generator[coverage.GeneratedValue, None, None]] = {}
242
- template: dict[str, Any] = {}
373
+ serializers = get_serializers_for_operation(operation)
374
+ template = Template(serializers)
243
375
  responses = find_in_responses(operation)
244
376
  for parameter in operation.iter_parameters():
245
377
  location = parameter.location
@@ -253,11 +385,7 @@ def _iter_coverage_cases(
253
385
  value = next(gen, NOT_SET)
254
386
  if isinstance(value, NotSet):
255
387
  continue
256
- container = template.setdefault(LOCATION_TO_CONTAINER[location], {})
257
- if location in ("header", "cookie", "path", "query") and not isinstance(value.value, str):
258
- container[name] = _stringify_value(value.value, location)
259
- else:
260
- container[name] = value.value
388
+ template.add_parameter(location, name, value)
261
389
  generators[(location, name)] = gen
262
390
  if operation.body:
263
391
  for body in operation.body:
@@ -274,48 +402,48 @@ def _iter_coverage_cases(
274
402
  if isinstance(value, NotSet):
275
403
  continue
276
404
  if "body" not in template:
277
- template["body"] = value.value
278
- template["media_type"] = body.media_type
279
- case = operation.make_case(**{**template, "body": value.value, "media_type": body.media_type})
405
+ template.set_body(value, body.media_type)
406
+ data = template.with_body(value=value, media_type=body.media_type)
407
+ case = operation.make_case(**data.kwargs)
280
408
  case.data_generation_method = value.data_generation_method
281
409
  case.meta = _make_meta(
282
410
  description=value.description,
283
411
  location=value.location,
284
412
  parameter=body.media_type,
285
413
  parameter_location="body",
414
+ **data.components,
286
415
  )
287
416
  yield case
288
417
  for next_value in gen:
289
- case = operation.make_case(**{**template, "body": next_value.value, "media_type": body.media_type})
418
+ data = template.with_body(value=next_value, media_type=body.media_type)
419
+ case = operation.make_case(**data.kwargs)
290
420
  case.data_generation_method = next_value.data_generation_method
291
421
  case.meta = _make_meta(
292
422
  description=next_value.description,
293
423
  location=next_value.location,
294
424
  parameter=body.media_type,
295
425
  parameter_location="body",
426
+ **data.components,
296
427
  )
297
428
  yield case
298
429
  elif DataGenerationMethod.positive in data_generation_methods:
299
- case = operation.make_case(**template)
430
+ data = template.unmodified()
431
+ case = operation.make_case(**data.kwargs)
300
432
  case.data_generation_method = DataGenerationMethod.positive
301
- case.meta = _make_meta(description="Default positive test case")
433
+ case.meta = _make_meta(description="Default positive test case", **data.components)
302
434
  yield case
303
435
 
304
436
  for (location, name), gen in generators.items():
305
- container_name = LOCATION_TO_CONTAINER[location]
306
- container = template[container_name]
307
437
  for value in gen:
308
- if location in ("header", "cookie", "path", "query") and not isinstance(value.value, str):
309
- generated = _stringify_value(value.value, location)
310
- else:
311
- generated = value.value
312
- case = operation.make_case(**{**template, container_name: {**container, name: generated}})
438
+ data = template.with_parameter(location=location, name=name, value=value)
439
+ case = operation.make_case(**data.kwargs)
313
440
  case.data_generation_method = value.data_generation_method
314
441
  case.meta = _make_meta(
315
442
  description=value.description,
316
443
  location=value.location,
317
444
  parameter=name,
318
445
  parameter_location=location,
446
+ **data.components,
319
447
  )
320
448
  yield case
321
449
  if DataGenerationMethod.negative in data_generation_methods:
@@ -323,10 +451,11 @@ def _iter_coverage_cases(
323
451
  # NOTE: The HEAD method is excluded
324
452
  methods = {"get", "put", "post", "delete", "options", "patch", "trace"} - set(operation.schema[operation.path])
325
453
  for method in sorted(methods):
326
- case = operation.make_case(**template)
454
+ data = template.unmodified()
455
+ case = operation.make_case(**data.kwargs)
327
456
  case._explicit_method = method
328
457
  case.data_generation_method = DataGenerationMethod.negative
329
- case.meta = _make_meta(description=f"Unspecified HTTP method: {method.upper()}")
458
+ case.meta = _make_meta(description=f"Unspecified HTTP method: {method.upper()}", **data.components)
330
459
  yield case
331
460
  # Generate duplicate query parameters
332
461
  if operation.query:
@@ -336,13 +465,19 @@ def _iter_coverage_cases(
336
465
  # I.e. contains just `default` value without any other keywords
337
466
  value = container.get(parameter.name, NOT_SET)
338
467
  if value is not NOT_SET:
339
- case = operation.make_case(**{**template, "query": {**container, parameter.name: [value, value]}})
468
+ data = template.with_container(
469
+ container_name="query",
470
+ value={**container, parameter.name: [value, value]},
471
+ data_generation_method=DataGenerationMethod.negative,
472
+ )
473
+ case = operation.make_case(**data.kwargs)
340
474
  case.data_generation_method = DataGenerationMethod.negative
341
475
  case.meta = _make_meta(
342
476
  description=f"Duplicate `{parameter.name}` query parameter",
343
477
  location=None,
344
478
  parameter=parameter.name,
345
479
  parameter_location="query",
480
+ **data.components,
346
481
  )
347
482
  yield case
348
483
  # Generate missing required parameters
@@ -352,15 +487,19 @@ def _iter_coverage_cases(
352
487
  location = parameter.location
353
488
  container_name = LOCATION_TO_CONTAINER[location]
354
489
  container = template[container_name]
355
- case = operation.make_case(
356
- **{**template, container_name: {k: v for k, v in container.items() if k != name}}
490
+ data = template.with_container(
491
+ container_name=container_name,
492
+ value={k: v for k, v in container.items() if k != name},
493
+ data_generation_method=DataGenerationMethod.negative,
357
494
  )
495
+ case = operation.make_case(**data.kwargs)
358
496
  case.data_generation_method = DataGenerationMethod.negative
359
497
  case.meta = _make_meta(
360
498
  description=f"Missing `{name}` at {location}",
361
499
  location=None,
362
500
  parameter=name,
363
501
  parameter_location=location,
502
+ **data.components,
364
503
  )
365
504
  yield case
366
505
  # Generate combinations for each location
@@ -389,21 +528,17 @@ def _iter_coverage_cases(
389
528
  _parameter: str | None,
390
529
  _data_generation_method: DataGenerationMethod,
391
530
  ) -> Case:
392
- if _location in ("header", "cookie", "path", "query"):
393
- container = {
394
- name: _stringify_value(val, _location) if not isinstance(val, str) else val
395
- for name, val in container_values.items()
396
- }
397
- else:
398
- container = container_values
399
-
400
- case = operation.make_case(**{**template, _container_name: container})
531
+ data = template.with_container(
532
+ container_name=_container_name, value=container_values, data_generation_method=_data_generation_method
533
+ )
534
+ case = operation.make_case(**data.kwargs)
401
535
  case.data_generation_method = _data_generation_method
402
536
  case.meta = _make_meta(
403
537
  description=description,
404
538
  location=None,
405
539
  parameter=_parameter,
406
540
  parameter_location=_location,
541
+ **data.components,
407
542
  )
408
543
  return case
409
544
 
@@ -496,13 +631,18 @@ def _make_meta(
496
631
  location: str | None = None,
497
632
  parameter: str | None = None,
498
633
  parameter_location: str | None = None,
634
+ query: DataGenerationMethod | None = None,
635
+ path_parameters: DataGenerationMethod | None = None,
636
+ headers: DataGenerationMethod | None = None,
637
+ cookies: DataGenerationMethod | None = None,
638
+ body: DataGenerationMethod | None = None,
499
639
  ) -> GenerationMetadata:
500
640
  return GenerationMetadata(
501
- query=None,
502
- path_parameters=None,
503
- headers=None,
504
- cookies=None,
505
- body=None,
641
+ query=query,
642
+ path_parameters=path_parameters,
643
+ headers=headers,
644
+ cookies=cookies,
645
+ body=body,
506
646
  phase=TestPhase.COVERAGE,
507
647
  description=description,
508
648
  location=location,
@@ -14,11 +14,8 @@ import click
14
14
  from ... import experimental, service
15
15
  from ...constants import (
16
16
  DISCORD_LINK,
17
- FALSE_VALUES,
18
17
  FLAKY_FAILURE_MESSAGE,
19
- GITHUB_APP_LINK,
20
18
  ISSUE_TRACKER_URL,
21
- REPORT_SUGGESTION_ENV_VAR,
22
19
  SCHEMATHESIS_TEST_CASE_HEADER,
23
20
  SCHEMATHESIS_VERSION,
24
21
  )
@@ -499,19 +496,6 @@ def display_statistic(context: ExecutionContext, event: events.Finished) -> None
499
496
  elif isinstance(context.report, ServiceReportContext):
500
497
  click.echo()
501
498
  handle_service_integration(context.report)
502
- else:
503
- env_var = os.getenv(REPORT_SUGGESTION_ENV_VAR)
504
- if env_var is not None and env_var.lower() in FALSE_VALUES:
505
- return
506
- click.echo(
507
- f"\n{bold('Tip')}: Use the {bold('`--report`')} CLI option to visualize test results via Schemathesis.io.\n"
508
- "We run additional conformance checks on reports from public repos."
509
- )
510
- if service.ci.detect() == service.ci.CIProvider.GITHUB:
511
- click.echo(
512
- "Optionally, for reporting results as PR comments, install the Schemathesis GitHub App:\n\n"
513
- f" {GITHUB_APP_LINK}"
514
- )
515
499
 
516
500
 
517
501
  def handle_service_integration(context: ServiceReportContext) -> None:
schemathesis/constants.py CHANGED
@@ -13,7 +13,6 @@ USER_AGENT = f"schemathesis/{SCHEMATHESIS_VERSION}"
13
13
  SCHEMATHESIS_TEST_CASE_HEADER = "X-Schemathesis-TestCaseId"
14
14
  HYPOTHESIS_IN_MEMORY_DATABASE_IDENTIFIER = ":memory:"
15
15
  DISCORD_LINK = "https://discord.gg/R9ASRAmHnA"
16
- GITHUB_APP_LINK = "https://github.com/apps/schemathesis"
17
16
  # Maximum test running time
18
17
  DEFAULT_DEADLINE = 15000
19
18
  DEFAULT_RESPONSE_TIMEOUT = 10000
@@ -50,7 +49,6 @@ HOOKS_MODULE_ENV_VAR = "SCHEMATHESIS_HOOKS"
50
49
  API_NAME_ENV_VAR = "SCHEMATHESIS_API_NAME"
51
50
  BASE_URL_ENV_VAR = "SCHEMATHESIS_BASE_URL"
52
51
  WAIT_FOR_SCHEMA_ENV_VAR = "SCHEMATHESIS_WAIT_FOR_SCHEMA"
53
- REPORT_SUGGESTION_ENV_VAR = "SCHEMATHESIS_REPORT_SUGGESTION"
54
52
 
55
53
  TRUE_VALUES = ("y", "yes", "t", "true", "on", "1")
56
54
  FALSE_VALUES = ("n", "no", "f", "false", "off", "0")
schemathesis/failures.py CHANGED
@@ -143,6 +143,12 @@ class AcceptedNegativeData(FailureContext):
143
143
  title: str = "Accepted negative data"
144
144
  type: str = "accepted_negative_data"
145
145
 
146
+ def unique_by_key(self, check_message: str | None) -> tuple[str, ...]:
147
+ return (
148
+ check_message or self.message,
149
+ str(self.status_code),
150
+ )
151
+
146
152
 
147
153
  @dataclass(repr=False)
148
154
  class RejectedPositiveData(FailureContext):
@@ -21,7 +21,9 @@ CheckFunction = Callable[["CheckContext", "GenericResponse", "Case"], Optional[b
21
21
  @dataclass
22
22
  class NegativeDataRejectionConfig:
23
23
  # 5xx will pass through
24
- allowed_statuses: list[str] = field(default_factory=lambda: ["400", "401", "403", "404", "422", "428", "5xx"])
24
+ allowed_statuses: list[str] = field(
25
+ default_factory=lambda: ["400", "401", "403", "404", "406", "422", "428", "5xx"]
26
+ )
25
27
 
26
28
 
27
29
  @dataclass
@@ -69,22 +69,6 @@ class RunnerContext:
69
69
  def is_stopped(self) -> bool:
70
70
  return self.stop_event.is_set()
71
71
 
72
- @property
73
- def has_all_not_found(self) -> bool:
74
- """Check if all responses are 404."""
75
- has_not_found = False
76
- for entry in self.data.results:
77
- for check in entry.checks:
78
- if check.response is not None:
79
- if check.response.status_code == 404:
80
- has_not_found = True
81
- else:
82
- # There are non-404 responses, no reason to check any other response
83
- return False
84
- # Only happens if all responses are 404, or there are no responses at all.
85
- # In the first case, it returns True, for the latter - False
86
- return has_not_found
87
-
88
72
  def add_result(self, result: TestResult) -> None:
89
73
  self.data.append(result)
90
74
 
@@ -147,6 +147,44 @@ class BaseRunner:
147
147
  __probes = None
148
148
  __analysis: Result[AnalysisResult, Exception] | None = None
149
149
 
150
+ def _should_warn_about_only_4xx(result: TestResult) -> bool:
151
+ if all(check.response is None for check in result.checks):
152
+ return False
153
+ # Don't duplicate auth warnings
154
+ if {check.response.status_code for check in result.checks if check.response is not None} <= {401, 403}:
155
+ return False
156
+ # At this point we know we only have 4xx responses
157
+ return True
158
+
159
+ def _check_warnings() -> None:
160
+ # Warn if all positive test cases got 4xx in return and no failure was found
161
+ def all_positive_are_rejected(result: TestResult) -> bool:
162
+ seen_positive = False
163
+ for check in result.checks:
164
+ if check.example.data_generation_method != DataGenerationMethod.positive:
165
+ continue
166
+ seen_positive = True
167
+ if check.response is None:
168
+ continue
169
+ # At least one positive response for positive test case
170
+ if 200 <= check.response.status_code < 300:
171
+ return False
172
+ # If there are positive test cases, and we ended up here, then there are no 2xx responses for them
173
+ # Otherwise, there are no positive test cases at all and this check should pass
174
+ return seen_positive
175
+
176
+ for result in ctx.data.results:
177
+ # Only warn about 4xx responses in successful positive test scenarios
178
+ if (
179
+ all(check.value == Status.success for check in result.checks)
180
+ and DataGenerationMethod.positive in result.data_generation_method
181
+ and all_positive_are_rejected(result)
182
+ and _should_warn_about_only_4xx(result)
183
+ ):
184
+ ctx.add_warning(
185
+ f"`{result.verbose_name}` returned only 4xx responses during unit tests. Check base URL or adjust data generation settings"
186
+ )
187
+
150
188
  def _initialize() -> events.Initialized:
151
189
  nonlocal initialized
152
190
  initialized = events.Initialized.from_schema(
@@ -159,8 +197,7 @@ class BaseRunner:
159
197
  return initialized
160
198
 
161
199
  def _finish() -> events.Finished:
162
- if ctx.has_all_not_found:
163
- ctx.add_warning(ALL_NOT_FOUND_WARNING_MESSAGE)
200
+ _check_warnings()
164
201
  return events.Finished.from_results(results=ctx.data, running_time=time.monotonic() - start_time)
165
202
 
166
203
  def _before_probes() -> events.BeforeProbing:
@@ -742,9 +779,6 @@ def has_too_many_responses_with_status(result: TestResult, status_code: int) ->
742
779
  return unauthorized_count / total >= TOO_MANY_RESPONSES_THRESHOLD
743
780
 
744
781
 
745
- ALL_NOT_FOUND_WARNING_MESSAGE = "All API responses have a 404 status code. Did you specify the proper API location?"
746
-
747
-
748
782
  def setup_hypothesis_database_key(test: Callable, operation: APIOperation) -> None:
749
783
  """Make Hypothesis use separate database entries for every API operation.
750
784
 
@@ -294,7 +294,12 @@ def missing_required_header(ctx: CheckContext, response: GenericResponse, case:
294
294
 
295
295
 
296
296
  def unsupported_method(ctx: CheckContext, response: GenericResponse, case: Case) -> bool | None:
297
- if case.meta and case.meta.description and case.meta.description.startswith("Unspecified HTTP method:"):
297
+ if (
298
+ case.meta
299
+ and case.meta.description
300
+ and case.meta.description.startswith("Unspecified HTTP method:")
301
+ and response.request.method != "OPTIONS"
302
+ ):
298
303
  if response.status_code != 405:
299
304
  raise AssertionError(
300
305
  f"Unexpected response status for unspecified HTTP method: {response.status_code}\nExpected: 405"
@@ -9,6 +9,8 @@ from typing import TYPE_CHECKING, Any, Generator, Iterator, Union, cast
9
9
  import requests
10
10
  from hypothesis_jsonschema import from_schema
11
11
 
12
+ from schemathesis.specs.openapi.serialization import get_serializers_for_operation
13
+
12
14
  from ...constants import DEFAULT_RESPONSE_TIMEOUT
13
15
  from ...generation import get_single_example
14
16
  from ...internal.copy import fast_deepcopy
@@ -48,11 +50,7 @@ def get_strategies_from_examples(
48
50
  operation: APIOperation[OpenAPIParameter, Case], as_strategy_kwargs: dict[str, Any] | None = None
49
51
  ) -> list[SearchStrategy[Case]]:
50
52
  """Build a set of strategies that generate test cases based on explicit examples in the schema."""
51
- maps = {}
52
- for location, container in LOCATION_TO_CONTAINER.items():
53
- serializer = operation.get_parameter_serializer(location)
54
- if serializer is not None:
55
- maps[container] = serializer
53
+ maps = get_serializers_for_operation(operation)
56
54
 
57
55
  def serialize_components(case: Case) -> Case:
58
56
  """Applies special serialization rules for case components.
@@ -3,6 +3,8 @@ from __future__ import annotations
3
3
  import re
4
4
  from functools import lru_cache
5
5
 
6
+ from ...exceptions import InternalError
7
+
6
8
  try: # pragma: no cover
7
9
  import re._constants as sre
8
10
  import re._parser as sre_parse
@@ -29,7 +31,15 @@ def update_quantifier(pattern: str, min_length: int | None, max_length: int | No
29
31
 
30
32
  try:
31
33
  parsed = sre_parse.parse(pattern)
32
- return _handle_parsed_pattern(parsed, pattern, min_length, max_length)
34
+ updated = _handle_parsed_pattern(parsed, pattern, min_length, max_length)
35
+ try:
36
+ re.compile(updated)
37
+ except re.error as exc:
38
+ raise InternalError(
39
+ f"The combination of min_length={min_length} and max_length={max_length} applied to the original pattern '{pattern}' resulted in an invalid regex: '{updated}'. "
40
+ "This indicates a bug in the regex quantifier merging logic"
41
+ ) from exc
42
+ return updated
33
43
  except re.error:
34
44
  # Invalid pattern
35
45
  return pattern
@@ -261,13 +271,18 @@ def _handle_repeat_quantifier(
261
271
  min_length, max_length = _build_size(min_repeat, max_repeat, min_length, max_length)
262
272
  if min_length > max_length:
263
273
  return pattern
264
- return f"({_strip_quantifier(pattern).strip(')(')})" + _build_quantifier(min_length, max_length)
274
+ inner = _strip_quantifier(pattern)
275
+ if inner.startswith("(") and inner.endswith(")"):
276
+ inner = inner[1:-1]
277
+ return f"({inner})" + _build_quantifier(min_length, max_length)
265
278
 
266
279
 
267
280
  def _handle_literal_or_in_quantifier(pattern: str, min_length: int | None, max_length: int | None) -> str:
268
281
  """Handle literal or character class quantifiers."""
269
282
  min_length = 1 if min_length is None else max(min_length, 1)
270
- return f"({pattern.strip(')(')})" + _build_quantifier(min_length, max_length)
283
+ if pattern.startswith("(") and pattern.endswith(")"):
284
+ pattern = pattern[1:-1]
285
+ return f"({pattern})" + _build_quantifier(min_length, max_length)
271
286
 
272
287
 
273
288
  def _build_quantifier(minimum: int | None, maximum: int | None) -> str:
@@ -3,6 +3,9 @@ from __future__ import annotations
3
3
  import json
4
4
  from typing import Any, Callable, Dict, Generator, List
5
5
 
6
+ from schemathesis.schemas import APIOperation
7
+ from schemathesis.specs.openapi.constants import LOCATION_TO_CONTAINER
8
+
6
9
  from ...utils import compose
7
10
 
8
11
  Generated = Dict[str, Any]
@@ -11,6 +14,15 @@ DefinitionList = List[Definition]
11
14
  MapFunction = Callable[[Generated], Generated]
12
15
 
13
16
 
17
+ def get_serializers_for_operation(operation: APIOperation) -> dict[str, Callable]:
18
+ serializers = {}
19
+ for location, container in LOCATION_TO_CONTAINER.items():
20
+ serializer = operation.get_parameter_serializer(location)
21
+ if serializer is not None:
22
+ serializers[container] = serializer
23
+ return serializers
24
+
25
+
14
26
  def make_serializer(
15
27
  func: Callable[[DefinitionList], Generator[Callable | None, None, None]],
16
28
  ) -> Callable[[DefinitionList], Callable | None]:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: schemathesis
3
- Version: 3.39.9
3
+ Version: 3.39.11
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
@@ -1,7 +1,7 @@
1
1
  schemathesis/__init__.py,sha256=UW2Bq8hDDkcBeAAA7PzpBFXkOOxkmHox-mfQwzHDjL0,1914
2
2
  schemathesis/_compat.py,sha256=y4RZd59i2NCnZ91VQhnKeMn_8t3SgvLOk2Xm8nymUHY,1837
3
3
  schemathesis/_dependency_versions.py,sha256=pjEkkGAfOQJYNb-9UOo84V8nj_lKHr_TGDVdFwY2UU0,816
4
- schemathesis/_hypothesis.py,sha256=d9MgjqKSQ_chQ8a8dCBnSQTAboOt2h3914IAmIZTXUI,24660
4
+ schemathesis/_hypothesis.py,sha256=2v6nQk5wiV0z6M_JPbOYhXJTXJ3Cyf6oWfef5VYCNdQ,30350
5
5
  schemathesis/_lazy_import.py,sha256=aMhWYgbU2JOltyWBb32vnWBb6kykOghucEzI_F70yVE,470
6
6
  schemathesis/_override.py,sha256=TAjYB3eJQmlw9K_xiR9ptt9Wj7if4U7UFlUhGjpBAoM,1625
7
7
  schemathesis/_patches.py,sha256=Hsbpn4UVeXUQD2Kllrbq01CSWsTYENWa0VJTyhX5C2k,895
@@ -10,9 +10,9 @@ schemathesis/_xml.py,sha256=qc2LydEwIqcSfgqQOJqiYicivA4YFJGKgCBOem_JqNc,8560
10
10
  schemathesis/auths.py,sha256=De97IS_iOlC36-jRhkZ2DUndjUpXYgsd8R-nA-iHn88,16837
11
11
  schemathesis/checks.py,sha256=YPUI1N5giGBy1072vd77e6HWelGAKrJUmJLEG4oqfF8,2630
12
12
  schemathesis/code_samples.py,sha256=rsdTo6ksyUs3ZMhqx0mmmkPSKUCFa--snIOYsXgZd80,4120
13
- schemathesis/constants.py,sha256=l1YQ7PXhEj9dyf9CTESVUpPOaFCH7iz-Fe8o4v6Th_s,2673
13
+ schemathesis/constants.py,sha256=RHwog2lAz84qG6KCpP1U15A4a9w1xcwbgZ97aY4juQg,2555
14
14
  schemathesis/exceptions.py,sha256=5zjPlyVoQNJGbwufplL6ZVV7FEBPBNPHGdlQRJ7xnhE,20449
15
- schemathesis/failures.py,sha256=mrDu7F-OrQY8pRMNVtIxTjovhfyIkcXYjnSkRw-OMuQ,8016
15
+ schemathesis/failures.py,sha256=OsQM5FDs79usUM4MLA20WIDaoW6SKTR39IyKDjeiwtQ,8197
16
16
  schemathesis/filters.py,sha256=f3c_yXIBwIin-9Y0qU2TkcC1NEM_Mw34jGUHQc0BOyw,17026
17
17
  schemathesis/graphql.py,sha256=XiuKcfoOB92iLFC8zpz2msLkM0_V0TLdxPNBqrrGZ8w,216
18
18
  schemathesis/hooks.py,sha256=p5AXgjVGtka0jn9MOeyBaRUtNbqZTs4iaJqytYTacHc,14856
@@ -41,7 +41,7 @@ schemathesis/cli/options.py,sha256=yL7nrzKkbGCc4nQya9wpTW48XGz_OT9hOFrzPxRrDe4,2
41
41
  schemathesis/cli/reporting.py,sha256=KC3sxSc1u4aFQ-0Q8CQ3G4HTEl7QxlubGnJgNKmVJdQ,3627
42
42
  schemathesis/cli/sanitization.py,sha256=Onw_NWZSom6XTVNJ5NHnC0PAhrYAcGzIXJbsBCzLkn4,1005
43
43
  schemathesis/cli/output/__init__.py,sha256=AXaUzQ1nhQ-vXhW4-X-91vE2VQtEcCOrGtQXXNN55iQ,29
44
- schemathesis/cli/output/default.py,sha256=MwpvDp29PHaPdkuqO_HIXkbar0n_vlnbXFKEqlbZTKE,39777
44
+ schemathesis/cli/output/default.py,sha256=kRJPcZ5RL9Nsy9k4bSZaRtAezzPiHE6hybdNZLfrEhs,39071
45
45
  schemathesis/cli/output/short.py,sha256=CL6-Apxr5tuZ3BL1vecV1MiRY1wDt21g0wiUwZu6mLM,2607
46
46
  schemathesis/contrib/__init__.py,sha256=FH8NL8NXgSKBFOF8Jy_EB6T4CJEaiM-tmDhz16B2o4k,187
47
47
  schemathesis/contrib/unique_data.py,sha256=cTjJfoNpfLMobUzmGnm3k6kVrZcL34_FMPLlpDDsg4c,1249
@@ -63,7 +63,7 @@ schemathesis/generation/_hypothesis.py,sha256=74fzLPHugZgMQXerWYFAMqCAjtAXz5E4ge
63
63
  schemathesis/generation/_methods.py,sha256=r8oVlJ71_gXcnEhU-byw2E0R2RswQQFm8U7yGErSqbw,1204
64
64
  schemathesis/generation/coverage.py,sha256=1CilQSe2DIdMdeWA6RL22so2bZULPRwc0CQBRxcLRFs,39370
65
65
  schemathesis/internal/__init__.py,sha256=93HcdG3LF0BbQKbCteOsFMa1w6nXl8yTmx87QLNJOik,161
66
- schemathesis/internal/checks.py,sha256=YBhldvs-oQTrtvTlz3cjaO9Ri2oQeyobFcquO4Y0UJ8,2720
66
+ schemathesis/internal/checks.py,sha256=ZPvsPJ7gWwK0IpzBFgOMaq4L2e0yfeC8qxPrnpauVFA,2741
67
67
  schemathesis/internal/copy.py,sha256=DcL56z-d69kKR_5u8mlHvjSL1UTyUKNMAwexrwHFY1s,1031
68
68
  schemathesis/internal/datetime.py,sha256=zPLBL0XXLNfP-KYel3H2m8pnsxjsA_4d-zTOhJg2EPQ,136
69
69
  schemathesis/internal/deprecation.py,sha256=XnzwSegbbdQyoTF1OGW_s9pdjIfN_Uzzdb2rfah1w2o,1261
@@ -79,8 +79,8 @@ schemathesis/runner/events.py,sha256=cRKKSDvHvKLBIyFBz-J0JtAKshbGGKco9eaMyLCgzsY
79
79
  schemathesis/runner/probes.py,sha256=no5AfO3kse25qvHevjeUfB0Q3C860V2AYzschUW3QMQ,5688
80
80
  schemathesis/runner/serialization.py,sha256=vZi1wd9HX9Swp9VJ_hZFeDgy3Y726URpHra-TbPvQhk,20762
81
81
  schemathesis/runner/impl/__init__.py,sha256=1E2iME8uthYPBh9MjwVBCTFV-P3fi7AdphCCoBBspjs,199
82
- schemathesis/runner/impl/context.py,sha256=oEdkXnlibVDobDRCMliImQwtX5RPEKgVEwVBCN67mfE,3132
83
- schemathesis/runner/impl/core.py,sha256=aUeJxW3cvfi5IYwU2GqhDfcKrCK3GtMnsts-qyaIXGQ,48086
82
+ schemathesis/runner/impl/context.py,sha256=KY06FXVOFQ6DBaa_FomSBXL81ULs3D21IW1u3yLqs1E,2434
83
+ schemathesis/runner/impl/core.py,sha256=pB_0zs_MfzAcbrVvRU85mONAn8QXVqFfM_y7yV8a2l0,49962
84
84
  schemathesis/runner/impl/solo.py,sha256=y5QSxgK8nBCEjZVD5BpFvYUXmB6tEjk6TwxAo__NejA,2911
85
85
  schemathesis/runner/impl/threadpool.py,sha256=yNR5LYE8f3N_4t42OwSgy0_qdGgBPM7d11F9c9oEAAs,15075
86
86
  schemathesis/service/__init__.py,sha256=cDVTCFD1G-vvhxZkJUwiToTAEQ-0ByIoqwXvJBCf_V8,472
@@ -107,21 +107,21 @@ schemathesis/specs/graphql/validation.py,sha256=uINIOt-2E7ZuQV2CxKzwez-7L9tDtqzM
107
107
  schemathesis/specs/openapi/__init__.py,sha256=HDcx3bqpa6qWPpyMrxAbM3uTo0Lqpg-BUNZhDJSJKnw,279
108
108
  schemathesis/specs/openapi/_cache.py,sha256=PAiAu4X_a2PQgD2lG5H3iisXdyg4SaHpU46bRZvfNkM,4320
109
109
  schemathesis/specs/openapi/_hypothesis.py,sha256=nU8UDn1PzGCre4IVmwIuO9-CZv1KJe1fYY0d2BojhSo,22981
110
- schemathesis/specs/openapi/checks.py,sha256=NzUoZ0gZMjyC12KfV7J-5ww8IMaaaNiKum4y7bmA_EA,26816
110
+ schemathesis/specs/openapi/checks.py,sha256=cuHTZsoHV2fdUz23_F99-mLelT1xtvaiS9EcG7R-hZs,26897
111
111
  schemathesis/specs/openapi/constants.py,sha256=JqM_FHOenqS_MuUE9sxVQ8Hnw0DNM8cnKDwCwPLhID4,783
112
112
  schemathesis/specs/openapi/converter.py,sha256=Yxw9lS_JKEyi-oJuACT07fm04bqQDlAu-iHwzkeDvE4,3546
113
113
  schemathesis/specs/openapi/definitions.py,sha256=WTkWwCgTc3OMxfKsqh6YDoGfZMTThSYrHGp8h0vLAK0,93935
114
- schemathesis/specs/openapi/examples.py,sha256=hdeq7et8AexYGY2iU6SfMZWJ7G0PbOfapUtc4upNs_4,20483
114
+ schemathesis/specs/openapi/examples.py,sha256=yBK0hjq5ROjk7BCLe7BO2dr7raijeZ6_KlZEol-cU-E,20401
115
115
  schemathesis/specs/openapi/formats.py,sha256=3KtEC-8nQRwMErS-WpMadXsr8R0O-NzYwFisZqMuc-8,2761
116
116
  schemathesis/specs/openapi/links.py,sha256=C4Uir2P_EcpqME8ee_a1vdUM8Tm3ZcKNn2YsGjZiMUQ,17935
117
117
  schemathesis/specs/openapi/loaders.py,sha256=jlTYLoG5sVRh8xycIF2M2VDCZ44M80Sct07a_ycg1Po,25698
118
118
  schemathesis/specs/openapi/media_types.py,sha256=dNTxpRQbY3SubdVjh4Cjb38R6Bc9MF9BsRQwPD87x0g,1017
119
119
  schemathesis/specs/openapi/parameters.py,sha256=X_3PKqUScIiN_vbSFEauPYyxASyFv-_9lZ_9QEZRLqo,14655
120
- schemathesis/specs/openapi/patterns.py,sha256=OxZp31cBEHv8fwoeYJ9JcdWNHFMIGzRISNN3dCBc9Dg,11260
120
+ schemathesis/specs/openapi/patterns.py,sha256=1hhhLJTJtF2snYEAEd_RzAwEBrNB5ayCXff--Fv6JEs,11881
121
121
  schemathesis/specs/openapi/references.py,sha256=euxM02kQGMHh4Ss1jWjOY_gyw_HazafKITIsvOEiAvI,9831
122
122
  schemathesis/specs/openapi/schemas.py,sha256=JA9SiBnwYg75kYnd4_0CWOuQv_XTfYwuDeGmFe4RtVo,53724
123
123
  schemathesis/specs/openapi/security.py,sha256=Z-6pk2Ga1PTUtBe298KunjVHsNh5A-teegeso7zcPIE,7138
124
- schemathesis/specs/openapi/serialization.py,sha256=5qGdFHZ3n80UlbSXrO_bkr4Al_7ci_Z3aSUjZczNDQY,11384
124
+ schemathesis/specs/openapi/serialization.py,sha256=rcZfqQbWer_RELedu4Sh5h_RhKYPWTfUjnmLwpP2R_A,11842
125
125
  schemathesis/specs/openapi/utils.py,sha256=ER4vJkdFVDIE7aKyxyYatuuHVRNutytezgE52pqZNE8,900
126
126
  schemathesis/specs/openapi/validation.py,sha256=Q9ThZlwU-mSz7ExDnIivnZGi1ivC5hlX2mIMRAM79kc,999
127
127
  schemathesis/specs/openapi/expressions/__init__.py,sha256=hFpJrIWbPi55GcIVjNFRDDUL8xmDu3mdbdldoHBoFJ0,1729
@@ -153,8 +153,8 @@ schemathesis/transports/auth.py,sha256=urSTO9zgFO1qU69xvnKHPFQV0SlJL3d7_Ojl0tLnZ
153
153
  schemathesis/transports/content_types.py,sha256=MiKOm-Hy5i75hrROPdpiBZPOTDzOwlCdnthJD12AJzI,2187
154
154
  schemathesis/transports/headers.py,sha256=hr_AIDOfUxsJxpHfemIZ_uNG3_vzS_ZeMEKmZjbYiBE,990
155
155
  schemathesis/transports/responses.py,sha256=OFD4ZLqwEFpo7F9vaP_SVgjhxAqatxIj38FS4XVq8Qs,1680
156
- schemathesis-3.39.9.dist-info/METADATA,sha256=RnHYXJblK6aXZcNFawqkoBBO4GoIc-QTmbNVSGIlR0w,11976
157
- schemathesis-3.39.9.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
158
- schemathesis-3.39.9.dist-info/entry_points.txt,sha256=VHyLcOG7co0nOeuk8WjgpRETk5P1E2iCLrn26Zkn5uk,158
159
- schemathesis-3.39.9.dist-info/licenses/LICENSE,sha256=PsPYgrDhZ7g9uwihJXNG-XVb55wj2uYhkl2DD8oAzY0,1103
160
- schemathesis-3.39.9.dist-info/RECORD,,
156
+ schemathesis-3.39.11.dist-info/METADATA,sha256=iM14usTIHG9JMQNJsUkBWUUQEcuYJwL74itZ9mZmvXc,11977
157
+ schemathesis-3.39.11.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
158
+ schemathesis-3.39.11.dist-info/entry_points.txt,sha256=VHyLcOG7co0nOeuk8WjgpRETk5P1E2iCLrn26Zkn5uk,158
159
+ schemathesis-3.39.11.dist-info/licenses/LICENSE,sha256=PsPYgrDhZ7g9uwihJXNG-XVb55wj2uYhkl2DD8oAzY0,1103
160
+ schemathesis-3.39.11.dist-info/RECORD,,