schemathesis 3.38.6__py3-none-any.whl → 3.38.7__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.
@@ -224,8 +224,11 @@ def _iter_coverage_cases(
224
224
  from .specs.openapi.constants import LOCATION_TO_CONTAINER
225
225
  from .specs.openapi.examples import find_in_responses, find_matching_in_responses
226
226
 
227
- def _stringify_value(val: Any) -> str:
227
+ def _stringify_value(val: Any, location: str) -> str | list[str]:
228
228
  if isinstance(val, list):
229
+ if location == "query":
230
+ # Having a list here ensures there will be multiple query parameters wit the same name
231
+ return [json.dumps(item) for item in val]
229
232
  # use comma-separated values style for arrays
230
233
  return ",".join(json.dumps(sub) for sub in val)
231
234
  return json.dumps(val)
@@ -246,8 +249,8 @@ def _iter_coverage_cases(
246
249
  location = parameter.location
247
250
  name = parameter.name
248
251
  container = template.setdefault(LOCATION_TO_CONTAINER[location], {})
249
- if location in ("header", "cookie", "path") and not isinstance(value.value, str):
250
- container[name] = _stringify_value(value.value)
252
+ if location in ("header", "cookie", "path", "query") and not isinstance(value.value, str):
253
+ container[name] = _stringify_value(value.value, location)
251
254
  else:
252
255
  container[name] = value.value
253
256
  generators[(location, name)] = gen
@@ -297,8 +300,8 @@ def _iter_coverage_cases(
297
300
  container_name = LOCATION_TO_CONTAINER[location]
298
301
  container = template[container_name]
299
302
  for value in gen:
300
- if location in ("header", "cookie", "path") and not isinstance(value.value, str):
301
- generated = _stringify_value(value.value)
303
+ if location in ("header", "cookie", "path", "query") and not isinstance(value.value, str):
304
+ generated = _stringify_value(value.value, location)
302
305
  else:
303
306
  generated = value.value
304
307
  case = operation.make_case(**{**template, container_name: {**container, name: generated}})
@@ -310,8 +313,32 @@ def _iter_coverage_cases(
310
313
  parameter_location=location,
311
314
  )
312
315
  yield case
313
- # Generate missing required parameters
314
316
  if DataGenerationMethod.negative in data_generation_methods:
317
+ # Generate HTTP methods that are not specified in the spec
318
+ methods = {"get", "put", "post", "delete", "options", "head", "patch", "trace"} - set(
319
+ operation.schema[operation.path]
320
+ )
321
+ for method in methods:
322
+ case = operation.make_case(**template)
323
+ case._explicit_method = method
324
+ case.data_generation_method = DataGenerationMethod.negative
325
+ case.meta = _make_meta(description=f"Unspecified HTTP method: {method}")
326
+ yield case
327
+ # Generate duplicate query parameters
328
+ if operation.query:
329
+ container = template["query"]
330
+ for parameter in operation.query:
331
+ value = container[parameter.name]
332
+ case = operation.make_case(**{**template, "query": {**container, parameter.name: [value, value]}})
333
+ case.data_generation_method = DataGenerationMethod.negative
334
+ case.meta = _make_meta(
335
+ description=f"Duplicate `{parameter.name}` query parameter",
336
+ location=None,
337
+ parameter=parameter.name,
338
+ parameter_location="query",
339
+ )
340
+ yield case
341
+ # Generate missing required parameters
315
342
  for parameter in operation.iter_parameters():
316
343
  if parameter.is_required and parameter.location != "path":
317
344
  name = parameter.name
@@ -324,7 +351,7 @@ def _iter_coverage_cases(
324
351
  case.data_generation_method = DataGenerationMethod.negative
325
352
  case.meta = _make_meta(
326
353
  description=f"Missing `{name}` at {location}",
327
- location=parameter.location,
354
+ location=None,
328
355
  parameter=name,
329
356
  parameter_location=location,
330
357
  )
@@ -348,21 +375,26 @@ def _iter_coverage_cases(
348
375
 
349
376
  # Helper function to create and yield a case
350
377
  def make_case(
351
- container_values: dict, description: str, _location: str, _container_name: str, _parameter: str | None
378
+ container_values: dict,
379
+ description: str,
380
+ _location: str,
381
+ _container_name: str,
382
+ _parameter: str | None,
383
+ _data_generation_method: DataGenerationMethod,
352
384
  ) -> Case:
353
- if _location in ("header", "cookie", "path"):
385
+ if _location in ("header", "cookie", "path", "query"):
354
386
  container = {
355
- name: _stringify_value(val) if not isinstance(val, str) else val
387
+ name: _stringify_value(val, _location) if not isinstance(val, str) else val
356
388
  for name, val in container_values.items()
357
389
  }
358
390
  else:
359
391
  container = container_values
360
392
 
361
393
  case = operation.make_case(**{**template, _container_name: container})
362
- case.data_generation_method = DataGenerationMethod.positive
394
+ case.data_generation_method = _data_generation_method
363
395
  case.meta = _make_meta(
364
396
  description=description,
365
- location=_location,
397
+ location=None,
366
398
  parameter=_parameter,
367
399
  parameter_location=_location,
368
400
  )
@@ -388,12 +420,21 @@ def _iter_coverage_cases(
388
420
  coverage.CoverageContext(data_generation_methods=[DataGenerationMethod.negative]),
389
421
  subschema,
390
422
  ):
391
- yield make_case(more.value, more.description, _location, _container_name, more.parameter)
423
+ yield make_case(
424
+ more.value,
425
+ more.description,
426
+ _location,
427
+ _container_name,
428
+ more.parameter,
429
+ DataGenerationMethod.negative,
430
+ )
392
431
 
393
432
  # 1. Generate only required properties
394
433
  if required and all_params != required:
395
434
  only_required = {k: v for k, v in base_container.items() if k in required}
396
- yield make_case(only_required, "Only required properties", location, container_name, None)
435
+ yield make_case(
436
+ only_required, "Only required properties", location, container_name, None, DataGenerationMethod.positive
437
+ )
397
438
  if DataGenerationMethod.negative in data_generation_methods:
398
439
  subschema = _combination_schema(only_required, required, parameter_set)
399
440
  yield from _yield_negative(subschema, location, container_name)
@@ -403,7 +444,12 @@ def _iter_coverage_cases(
403
444
  combo = {k: v for k, v in base_container.items() if k in required or k == opt_param}
404
445
  if combo != base_container:
405
446
  yield make_case(
406
- combo, f"All required properties and optional '{opt_param}'", location, container_name, None
447
+ combo,
448
+ f"All required properties and optional '{opt_param}'",
449
+ location,
450
+ container_name,
451
+ None,
452
+ DataGenerationMethod.positive,
407
453
  )
408
454
  if DataGenerationMethod.negative in data_generation_methods:
409
455
  subschema = _combination_schema(combo, required, parameter_set)
@@ -416,7 +462,12 @@ def _iter_coverage_cases(
416
462
  combo = {k: v for k, v in base_container.items() if k in required or k in combination}
417
463
  if combo != base_container:
418
464
  yield make_case(
419
- combo, f"All required and {size} optional properties", location, container_name, None
465
+ combo,
466
+ f"All required and {size} optional properties",
467
+ location,
468
+ container_name,
469
+ None,
470
+ DataGenerationMethod.positive,
420
471
  )
421
472
 
422
473
 
@@ -324,9 +324,9 @@ def cover_schema_iter(
324
324
  for key, value in schema.items():
325
325
  with _ignore_unfixable(), ctx.location(key):
326
326
  if key == "enum":
327
- yield from _negative_enum(ctx, value)
327
+ yield from _negative_enum(ctx, value, seen)
328
328
  elif key == "const":
329
- for value_ in _negative_enum(ctx, [value]):
329
+ for value_ in _negative_enum(ctx, [value], seen):
330
330
  k = _to_hashable_key(value_.value)
331
331
  if k not in seen:
332
332
  yield value_
@@ -744,13 +744,20 @@ def select_combinations(optional: list[str]) -> Iterator[tuple[str, ...]]:
744
744
  yield next(combinations(optional, size))
745
745
 
746
746
 
747
- def _negative_enum(ctx: CoverageContext, value: list) -> Generator[GeneratedValue, None, None]:
747
+ def _negative_enum(
748
+ ctx: CoverageContext, value: list, seen: set[Any | tuple[type, str]]
749
+ ) -> Generator[GeneratedValue, None, None]:
748
750
  def is_not_in_value(x: Any) -> bool:
749
- return x not in value
751
+ if x in value:
752
+ return False
753
+ _hashed = _to_hashable_key(x)
754
+ return _hashed not in seen
750
755
 
751
- strategy = JSON_STRATEGY.filter(is_not_in_value)
752
- # The exact negative value is not important here
753
- yield NegativeValue(ctx.generate_from(strategy), description="Invalid enum value", location=ctx.current_location)
756
+ strategy = (st.none() | st.booleans() | NUMERIC_STRATEGY | st.text()).filter(is_not_in_value)
757
+ value = ctx.generate_from(strategy)
758
+ yield NegativeValue(value, description="Invalid enum value", location=ctx.current_location)
759
+ hashed = _to_hashable_key(value)
760
+ seen.add(hashed)
754
761
 
755
762
 
756
763
  def _negative_properties(
@@ -846,6 +853,7 @@ def _negative_required(
846
853
  {k: v for k, v in template.items() if k != key},
847
854
  description=f"Missing required property: {key}",
848
855
  location=ctx.current_location,
856
+ parameter=key,
849
857
  )
850
858
 
851
859
 
schemathesis/models.py CHANGED
@@ -203,6 +203,7 @@ class Case:
203
203
  data_generation_method: DataGenerationMethod | None = None
204
204
  _auth: requests.auth.AuthBase | None = None
205
205
  _has_explicit_auth: bool = False
206
+ _explicit_method: str | None = None
206
207
 
207
208
  def __post_init__(self) -> None:
208
209
  self._original_path_parameters = self.path_parameters.copy() if self.path_parameters else None
@@ -265,7 +266,7 @@ class Case:
265
266
 
266
267
  @property
267
268
  def method(self) -> str:
268
- return self.operation.method.upper()
269
+ return self._explicit_method.upper() if self._explicit_method else self.operation.method.upper()
269
270
 
270
271
  @property
271
272
  def base_url(self) -> str | None:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: schemathesis
3
- Version: 3.38.6
3
+ Version: 3.38.7
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=SHFQKVeKsKX55D7Rfb2DTG7gq7h-jLXMGGD8RG81ac4,21339
4
+ schemathesis/_hypothesis.py,sha256=7hQItl5JqtctCiN7gL4DhWmj8A0Ku6t1_pkxEqkID1g,23463
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
@@ -18,7 +18,7 @@ schemathesis/graphql.py,sha256=XiuKcfoOB92iLFC8zpz2msLkM0_V0TLdxPNBqrrGZ8w,216
18
18
  schemathesis/hooks.py,sha256=p5AXgjVGtka0jn9MOeyBaRUtNbqZTs4iaJqytYTacHc,14856
19
19
  schemathesis/lazy.py,sha256=Ddhkk7Tpc_VcRGYkCtKDmP2gpjxVmEZ3b01ZTNjbm8I,19004
20
20
  schemathesis/loaders.py,sha256=MoEhcdOEBJxNRn5X-ZNhWB9jZDHQQNpkNfEdQjf_NDw,4590
21
- schemathesis/models.py,sha256=s0lt63sBShsgKoM_kqLPb56jIeHSwTfk-TDQmQowXH8,49669
21
+ schemathesis/models.py,sha256=Xn1rEF0_g3HNqJaJB9sC8N3gJ9suFPPlgc8ho01VyF0,49769
22
22
  schemathesis/parameters.py,sha256=izlu4MFYT1RWrC4RBxrV6weeCal-ODbdLQLMb0PYCZY,2327
23
23
  schemathesis/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
24
24
  schemathesis/sanitization.py,sha256=Lycn1VVfula9B6XpzkxTHja7CZ7RHqbUh9kBic0Yi4M,9056
@@ -61,7 +61,7 @@ schemathesis/fixups/utf8_bom.py,sha256=lWT9RNmJG8i-l5AXIpaCT3qCPUwRgzXPW3eoOjmZE
61
61
  schemathesis/generation/__init__.py,sha256=29Zys_tD6kfngaC4zHeC6TOBZQcmo7CWm7KDSYsHStQ,1581
62
62
  schemathesis/generation/_hypothesis.py,sha256=74fzLPHugZgMQXerWYFAMqCAjtAXz5E4gek7Gnkhli4,1756
63
63
  schemathesis/generation/_methods.py,sha256=r8oVlJ71_gXcnEhU-byw2E0R2RswQQFm8U7yGErSqbw,1204
64
- schemathesis/generation/coverage.py,sha256=6VCaQ8bUtBmbG-DCOS11rXHA-s6hiIzVtbXAB0pYMZU,38604
64
+ schemathesis/generation/coverage.py,sha256=Y-eKsEDe6INu-2SjlzGpuzayRXF_wdDwTBLsW4pGlrc,38843
65
65
  schemathesis/internal/__init__.py,sha256=93HcdG3LF0BbQKbCteOsFMa1w6nXl8yTmx87QLNJOik,161
66
66
  schemathesis/internal/checks.py,sha256=SBx2gesB-XzgVSMX_u7Mb416jSxJ68eQKtcdkWlkyOo,2441
67
67
  schemathesis/internal/copy.py,sha256=DcL56z-d69kKR_5u8mlHvjSL1UTyUKNMAwexrwHFY1s,1031
@@ -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.38.6.dist-info/METADATA,sha256=CbrAdVVhZVREx6pwkPRARvVXqETpf8YE34flEN3Tr4I,12923
157
- schemathesis-3.38.6.dist-info/WHEEL,sha256=WJ9WQ4-pUYxfD_tEj5GvKSG9KSULNumqkTQdolV8mME,87
158
- schemathesis-3.38.6.dist-info/entry_points.txt,sha256=VHyLcOG7co0nOeuk8WjgpRETk5P1E2iCLrn26Zkn5uk,158
159
- schemathesis-3.38.6.dist-info/licenses/LICENSE,sha256=PsPYgrDhZ7g9uwihJXNG-XVb55wj2uYhkl2DD8oAzY0,1103
160
- schemathesis-3.38.6.dist-info/RECORD,,
156
+ schemathesis-3.38.7.dist-info/METADATA,sha256=iBsAbz1uaJxY_GUPTnjdD2uMxS3HCk6lX_4IVj_s2yY,12923
157
+ schemathesis-3.38.7.dist-info/WHEEL,sha256=C2FUgwZgiLbznR-k0b_5k3Ai_1aASOXDss3lzCUsUug,87
158
+ schemathesis-3.38.7.dist-info/entry_points.txt,sha256=VHyLcOG7co0nOeuk8WjgpRETk5P1E2iCLrn26Zkn5uk,158
159
+ schemathesis-3.38.7.dist-info/licenses/LICENSE,sha256=PsPYgrDhZ7g9uwihJXNG-XVb55wj2uYhkl2DD8oAzY0,1103
160
+ schemathesis-3.38.7.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.26.2
2
+ Generator: hatchling 1.26.3
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any