Skip to content

Commit

Permalink
fix: Not respecting allow_x00 and codec configs options during fi…
Browse files Browse the repository at this point in the history
…lling gaps in explicit examples

Signed-off-by: Dmitry Dygalo <dmitry@dygalo.dev>
  • Loading branch information
Stranger6667 committed Feb 29, 2024
1 parent 3de22bb commit f24ffb9
Show file tree
Hide file tree
Showing 4 changed files with 79 additions and 29 deletions.
4 changes: 4 additions & 0 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ Changelog
:version:`Unreleased <v3.25.5...HEAD>` - TBD
--------------------------------------------

**Fixed**

- Not respecting ``allow_x00`` and ``codec`` configs options during filling gaps in explicit examples.

.. _v3.25.5:

:version:`3.25.5 <v3.25.4...v3.25.5>` - 2024-02-29
Expand Down
2 changes: 1 addition & 1 deletion src/schemathesis/specs/openapi/_hypothesis.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ def get_case_strategy(

context = HookContext(operation)

generation_config = generation_config or GenerationConfig()
generation_config = generation_config or operation.schema.generation_config

path_parameters_ = generate_parameter(
"path", path_parameters, operation, draw, context, hooks, generator, generation_config
Expand Down
46 changes: 22 additions & 24 deletions src/schemathesis/specs/openapi/examples.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,24 @@
from contextlib import suppress
from dataclasses import dataclass
from functools import lru_cache
from itertools import islice, cycle, chain
from typing import Any, Generator, Union, cast
from itertools import chain, cycle, islice
from typing import TYPE_CHECKING, Any, Generator, Union, cast

import requests
import hypothesis
from hypothesis_jsonschema import from_schema
from hypothesis.strategies import SearchStrategy
from hypothesis_jsonschema import from_schema

from .parameters import OpenAPIParameter, OpenAPIBody
from ...constants import DEFAULT_RESPONSE_TIMEOUT
from ...models import APIOperation, Case
from ._hypothesis import get_case_strategy
from ..._hypothesis import get_single_example
from ._hypothesis import get_case_strategy, get_default_format_strategies
from .formats import STRING_FORMATS
from .constants import LOCATION_TO_CONTAINER
from .parameters import OpenAPIBody, OpenAPIParameter


if TYPE_CHECKING:
from ...generation import GenerationConfig


@dataclass
Expand Down Expand Up @@ -248,7 +253,7 @@ def extract_from_schema(
# Generated by one of `anyOf` or similar sub-schemas
continue
subschema = operation.schema.prepare_schema(subschema)
generated = _generate_single_example(subschema)
generated = _generate_single_example(subschema, operation.schema.generation_config)
variants[name] = [generated]
# Calculate the maximum number of examples any property has
total_combos = max(len(examples) for examples in variants.values())
Expand All @@ -264,24 +269,17 @@ def extract_from_schema(
yield [value]


def _generate_single_example(schema: dict[str, Any]) -> Any:
examples = []

@hypothesis.given(from_schema(schema)) # type: ignore
@hypothesis.settings( # type: ignore
database=None,
max_examples=1,
deadline=None,
verbosity=hypothesis.Verbosity.quiet,
phases=(hypothesis.Phase.generate,),
suppress_health_check=list(hypothesis.HealthCheck),
def _generate_single_example(
schema: dict[str, Any],
generation_config: GenerationConfig,
) -> Any:
strategy = from_schema(
schema,
custom_formats={**get_default_format_strategies(), **STRING_FORMATS},
allow_x00=generation_config.allow_x00,
codec=generation_config.codec,
)
def example_generating_inner_function(ex: Any) -> None:
examples.append(ex)

example_generating_inner_function()

return examples[0]
return get_single_example(strategy)


def produce_combinations(examples: list[Example]) -> Generator[dict[str, Any], None, None]:
Expand Down
56 changes: 52 additions & 4 deletions test/specs/openapi/test_examples.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,23 @@
from __future__ import annotations

from unittest.mock import ANY
from schemathesis.generation import GenerationConfig

from test.utils import assert_requests_call
from typing import Any
from unittest.mock import ANY

import jsonschema
import pytest
import yaml
from _pytest.main import ExitCode
from hypothesis import find
from hypothesis import find, settings, given, HealthCheck, strategies as st, Phase

import schemathesis
from schemathesis._hypothesis import get_single_example
from schemathesis.models import APIOperation
from schemathesis.specs.openapi import examples
from schemathesis.specs.openapi.examples import (
extract_inner_examples,
ParameterExample,
extract_inner_examples,
)
from schemathesis.specs.openapi.parameters import parameters_to_json_schema
from schemathesis.specs.openapi.schemas import BaseOpenAPISchema
Expand Down Expand Up @@ -765,6 +765,54 @@ def test_partial_examples(empty_open_api_3_schema):
jsonschema.validate(example.path_parameters, parameters_schema)


def test_partial_examples_without_null_bytes_and_formats(empty_open_api_3_schema):
schemathesis.openapi.format("even_4_digits", st.from_regex(r"\A[0-9]{4}\Z").filter(lambda x: int(x) % 2 == 0))
empty_open_api_3_schema["paths"] = {
"/test/": {
"post": {
"parameters": [
{
"name": "q1",
"in": "query",
"required": True,
"schema": {
"type": "object",
"properties": {"foo": {"type": "string"}},
"required": ["foo"],
"additionalProperties": False,
},
},
{
"name": "q2",
"in": "query",
"required": True,
"schema": {
"type": "object",
"properties": {"foo": {"type": "string", "format": "even_4_digits"}},
"required": ["foo"],
"additionalProperties": False,
},
},
{"name": "q3", "in": "query", "required": True, "schema": {"type": "string"}, "example": "test"},
],
"responses": {"default": {"description": "OK"}},
}
}
}
schema = schemathesis.from_dict(empty_open_api_3_schema, generation_config=GenerationConfig(allow_x00=False))
operation = schema["/test/"]["POST"]
strategy = operation.get_strategies_from_examples()[0]

@given(case=strategy)
@settings(deadline=None, suppress_health_check=list(HealthCheck), phases=[Phase.generate])
def test(case):
assert "\x00" not in case.query["q1"]["foo"]
assert len(case.query["q2"]["foo"]) == 4
assert int(case.query["q2"]["foo"]) % 2 == 0

test()


def test_external_value(empty_open_api_3_schema, server):
# When the API schema contains examples via `externalValue` keyword
empty_open_api_3_schema["paths"] = {
Expand Down

0 comments on commit f24ffb9

Please sign in to comment.