Source code for pytest_routes.generation.headers

"""Header generation strategies for HTTP requests."""

from __future__ import annotations

from typing import TYPE_CHECKING

from hypothesis import strategies as st

if TYPE_CHECKING:
    from hypothesis.strategies import SearchStrategy

# Registry for custom header strategies
_HEADER_STRATEGIES: dict[str, SearchStrategy[str]] = {}

# Common header value strategies
CONTENT_TYPE_STRATEGY = st.sampled_from(
    [
        "application/json",
        "application/x-www-form-urlencoded",
        "multipart/form-data",
        "text/plain",
        "text/html",
        "application/xml",
    ]
)

ACCEPT_STRATEGY = st.sampled_from(
    [
        "application/json",
        "*/*",
        "text/html",
        "application/xml",
        "text/plain",
    ]
)

AUTHORIZATION_STRATEGY = st.builds(
    lambda token: f"Bearer {token}",
    st.text(
        alphabet=st.characters(
            blacklist_categories=("Cc", "Cs"),
            blacklist_characters=(" ", "\t", "\n", "\r"),
        ),
        min_size=20,
        max_size=64,
    ),
)

USER_AGENT_STRATEGY = st.sampled_from(
    [
        "pytest-routes/1.0",
        "Mozilla/5.0 (compatible; pytest-routes/1.0)",
        "python-httpx/0.27.0",
    ]
)

# Default strategies for common headers
_DEFAULT_HEADER_STRATEGIES: dict[str, SearchStrategy[str]] = {
    "content-type": CONTENT_TYPE_STRATEGY,
    "accept": ACCEPT_STRATEGY,
    "authorization": AUTHORIZATION_STRATEGY,
    "user-agent": USER_AGENT_STRATEGY,
}


[docs] def register_header_strategy(header_name: str, strategy: SearchStrategy[str]) -> None: """Register a custom strategy for a specific HTTP header. This allows users to override default header generation or add strategies for custom headers. Args: header_name: The HTTP header name (case-insensitive). strategy: A Hypothesis strategy that generates string values. Example: >>> from hypothesis import strategies as st >>> register_header_strategy("X-Custom-ID", st.uuids().map(str)) """ _HEADER_STRATEGIES[header_name.lower()] = strategy
def _get_strategy_for_header( header_name: str, header_type: type | None = None, # noqa: ARG001 ) -> SearchStrategy[str]: """Get the strategy for a specific header. Args: header_name: The HTTP header name (case-insensitive). header_type: Optional type hint for the header value (reserved for future use). Returns: A Hypothesis strategy that generates string values. """ normalized_name = header_name.lower() # Check custom registry first if normalized_name in _HEADER_STRATEGIES: return _HEADER_STRATEGIES[normalized_name] # Check default strategies if normalized_name in _DEFAULT_HEADER_STRATEGIES: return _DEFAULT_HEADER_STRATEGIES[normalized_name] # Fallback to generic text strategy for HTTP headers # HTTP headers should be printable ASCII, no control chars return st.text( alphabet=st.characters( min_codepoint=32, # Space max_codepoint=126, # Tilde (printable ASCII) blacklist_characters=("\r", "\n"), ), min_size=1, max_size=100, )
[docs] def generate_headers( header_specs: dict[str, type] | None = None, *, include_content_type: bool = False, include_accept: bool = False, include_authorization: bool = False, ) -> SearchStrategy[dict[str, str]]: """Generate HTTP headers based on specifications. All header values are generated as strings to comply with HTTP standards. Args: header_specs: Mapping of header names to their types. If None, only optional headers based on flags will be included. include_content_type: Whether to include Content-Type header. include_accept: Whether to include Accept header. include_authorization: Whether to include Authorization header. Returns: A Hypothesis strategy that generates dictionaries of HTTP headers. Example: >>> from hypothesis import strategies as st >>> # Generate custom headers >>> header_strategy = generate_headers( ... header_specs={"X-Request-ID": str, "X-Trace-ID": str}, ... include_accept=True, ... ) >>> # Generate only standard headers >>> standard_headers = generate_headers( ... include_content_type=True, ... include_accept=True, ... ) """ strategies: dict[str, SearchStrategy[str]] = {} # Add specified headers if header_specs: for header_name, header_type in header_specs.items(): strategies[header_name] = _get_strategy_for_header(header_name, header_type) # Add optional standard headers (check custom registry for overrides) if include_content_type: strategies["Content-Type"] = _get_strategy_for_header("Content-Type") if include_accept: strategies["Accept"] = _get_strategy_for_header("Accept") if include_authorization: strategies["Authorization"] = _get_strategy_for_header("Authorization") # If no headers specified, return empty dict if not strategies: return st.just({}) # Build a strategy that generates a dict with all specified headers return st.fixed_dictionaries(strategies)
[docs] def generate_optional_headers( required_headers: dict[str, type] | None = None, optional_headers: dict[str, type] | None = None, ) -> SearchStrategy[dict[str, str]]: """Generate headers with required and optional fields. This is useful for testing routes where some headers are mandatory and others are optional. Args: required_headers: Headers that must always be present. optional_headers: Headers that may or may not be included. Returns: A Hypothesis strategy that generates dictionaries of HTTP headers. Example: >>> header_strategy = generate_optional_headers( ... required_headers={"Authorization": str}, ... optional_headers={"X-Request-ID": str, "X-Trace-ID": str}, ... ) """ required = required_headers or {} optional = optional_headers or {} # Build strategies for required headers required_strategies: dict[str, SearchStrategy[str]] = { header_name: _get_strategy_for_header(header_name, header_type) for header_name, header_type in required.items() } # Build strategies for optional headers optional_strategies: dict[str, SearchStrategy[str]] = { header_name: _get_strategy_for_header(header_name, header_type) for header_name, header_type in optional.items() } if not required_strategies and not optional_strategies: return st.just({}) # Generate required headers required_dict_strategy = st.just({}) if not required_strategies else st.fixed_dictionaries(required_strategies) # Generate optional headers (some may be omitted) if not optional_strategies: return required_dict_strategy # Use fixed_dictionaries with optional parameter # Pass empty dict for required (already handled above) and optionals in optional param if not required_strategies: # Only optional headers return st.fixed_dictionaries({}, optional=optional_strategies) # Both required and optional headers return st.fixed_dictionaries(required_strategies, optional=optional_strategies)