diff --git a/CLAUDE.md b/CLAUDE.md index 999ec8f..ce87482 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -46,6 +46,7 @@ DECNET is a honeypot/deception network framework. It deploys fake machines (call - The logging/aggregation network must be isolated from the decoy network. - A publicly accessible real server acts as the bridge between the two networks. - Deckies should differ in exposed services and OS fingerprints to appear as a heterogeneous network. +- **IMPORTANT**: The system now strictly enforces dependency injection for storage. Do not import `SQLiteRepository` directly in new features; instead, use `get_repository()` from the factory or the FastAPI `get_repo` dependency. ## Development and testing diff --git a/GEMINI.md b/GEMINI.md index a46089f..c361696 100644 --- a/GEMINI.md +++ b/GEMINI.md @@ -89,6 +89,7 @@ Host NIC (eth0) - **Extensive testing** for every function must be created. - **Always develop in the `dev` branch, never in `main`.** - **Test in the `testing` branch.** + - **IMPORTANT**: The system now strictly enforces dependency injection for storage. Do not import `SQLiteRepository` directly in new features; instead, use `get_repository()` from the factory or the FastAPI `get_repo` dependency. ## Directory Structure diff --git a/decnet.collector.log b/decnet.collector.log new file mode 100644 index 0000000..bac1371 --- /dev/null +++ b/decnet.collector.log @@ -0,0 +1 @@ +Collector starting → /home/anti/Tools/DECNET/decnet.log diff --git a/decnet.db-shm b/decnet.db-shm new file mode 100644 index 0000000..9a6b1e3 Binary files /dev/null and b/decnet.db-shm differ diff --git a/decnet.db-wal b/decnet.db-wal new file mode 100644 index 0000000..19a5035 Binary files /dev/null and b/decnet.db-wal differ diff --git a/decnet/cli.py b/decnet/cli.py index 90bc1c2..e2001d8 100644 --- a/decnet/cli.py +++ b/decnet/cli.py @@ -252,7 +252,7 @@ def deploy( console.print("[red]Failed to start mutator watcher.[/]") if effective_log_file and not dry_run and not api: - import subprocess # noqa: F811 # nosec B404 + import subprocess # nosec B404 import sys from pathlib import Path as _Path _collector_err = _Path(effective_log_file).with_suffix(".collector.log") @@ -301,18 +301,20 @@ def mutate( force_all: bool = typer.Option(False, "--all", help="Force mutate all deckies immediately"), ) -> None: """Manually trigger or continuously watch for decky mutation.""" + import asyncio from decnet.mutator import mutate_decky, mutate_all, run_watch_loop + from decnet.web.dependencies import repo if watch: - run_watch_loop() + asyncio.run(run_watch_loop(repo)) return if decky_name: - mutate_decky(decky_name) + asyncio.run(mutate_decky(decky_name, repo)) elif force_all: - mutate_all(force=True) + asyncio.run(mutate_all(force=True, repo=repo)) else: - mutate_all(force=False) + asyncio.run(mutate_all(force=False, repo=repo)) @app.command() diff --git a/decnet/correlation/__init__.py b/decnet/correlation/__init__.py index 1018556..7f89022 100644 --- a/decnet/correlation/__init__.py +++ b/decnet/correlation/__init__.py @@ -5,9 +5,9 @@ from decnet.correlation.graph import AttackerTraversal, TraversalHop from decnet.correlation.parser import LogEvent, parse_line __all__ = [ - "CorrelationEngine", "AttackerTraversal", - "TraversalHop", + "CorrelationEngine", "LogEvent", + "TraversalHop", "parse_line", ] diff --git a/decnet/env.py b/decnet/env.py index 59e694f..eb57d3d 100644 --- a/decnet/env.py +++ b/decnet/env.py @@ -1,5 +1,6 @@ import os from pathlib import Path +from typing import Optional from dotenv import load_dotenv # Calculate absolute path to the project root @@ -30,7 +31,7 @@ def _require_env(name: str) -> str: f"Required environment variable '{name}' is not set. " f"Set it in .env.local or export it before starting DECNET." ) - + if any(k.startswith("PYTEST") for k in os.environ): return value @@ -55,6 +56,10 @@ DECNET_ADMIN_USER: str = os.environ.get("DECNET_ADMIN_USER", "admin") DECNET_ADMIN_PASSWORD: str = os.environ.get("DECNET_ADMIN_PASSWORD", "admin") DECNET_DEVELOPER: bool = os.environ.get("DECNET_DEVELOPER", "False").lower() == "true" +# Database Options +DECNET_DB_TYPE: str = os.environ.get("DECNET_DB_TYPE", "sqlite").lower() +DECNET_DB_URL: Optional[str] = os.environ.get("DECNET_DB_URL") + # CORS — comma-separated list of allowed origins for the web dashboard API. # Defaults to the configured web host/port. Override with DECNET_CORS_ORIGINS if needed. # Example: DECNET_CORS_ORIGINS=http://192.168.1.50:9090,https://dashboard.example.com diff --git a/decnet/ini_loader.py b/decnet/ini_loader.py index 81bfda9..1ce683a 100644 --- a/decnet/ini_loader.py +++ b/decnet/ini_loader.py @@ -123,7 +123,7 @@ def _parse_configparser(cp: configparser.ConfigParser) -> IniConfig: for section in cp.sections(): if section == "general": continue - + # A service sub-section is identified if the section name has at least one dot # AND the last segment is a known service name. # e.g. "decky-01.ssh" -> sub-section @@ -151,7 +151,7 @@ def _parse_configparser(cp: configparser.ConfigParser) -> IniConfig: services = [sv.strip() for sv in svc_raw.split(",")] if svc_raw else None archetype = s.get("archetype") nmap_os = s.get("nmap_os") or s.get("nmap-os") or None - + mi_raw = s.get("mutate_interval") or s.get("mutate-interval") mutate_interval = None if mi_raw: @@ -199,11 +199,11 @@ def _parse_configparser(cp: configparser.ConfigParser) -> IniConfig: for section in cp.sections(): if "." not in section: continue - + decky_name, dot, svc_name = section.rpartition(".") if svc_name not in known_services: continue # not a service sub-section - + svc_cfg = {k: v for k, v in cp[section].items()} if decky_name in decky_map: # Direct match — single decky diff --git a/decnet/mutator/engine.py b/decnet/mutator/engine.py index eadbb70..6d97e23 100644 --- a/decnet/mutator/engine.py +++ b/decnet/mutator/engine.py @@ -12,25 +12,29 @@ from rich.console import Console from decnet.archetypes import get_archetype from decnet.fleet import all_service_names from decnet.composer import write_compose -from decnet.config import DeckyConfig, load_state, save_state +from decnet.config import DeckyConfig, DecnetConfig from decnet.engine import _compose_with_retry -import subprocess # nosec B404 +from pathlib import Path +import anyio +import asyncio +from decnet.web.db.repository import BaseRepository console = Console() -def mutate_decky(decky_name: str) -> bool: +async def mutate_decky(decky_name: str, repo: BaseRepository) -> bool: """ Perform an Intra-Archetype Shuffle for a specific decky. Returns True if mutation succeeded, False otherwise. """ - state = load_state() - if state is None: - console.print("[red]No active deployment found (no decnet-state.json).[/]") + state_dict = await repo.get_state("deployment") + if state_dict is None: + console.print("[red]No active deployment found in database.[/]") return False - config, compose_path = state + config = DecnetConfig(**state_dict["config"]) + compose_path = Path(state_dict["compose_path"]) decky: Optional[DeckyConfig] = next((d for d in config.deckies if d.name == decky_name), None) if not decky: @@ -63,31 +67,35 @@ def mutate_decky(decky_name: str) -> bool: decky.services = list(chosen) decky.last_mutated = time.time() - save_state(config, compose_path) + # Save to DB + await repo.set_state("deployment", {"config": config.model_dump(), "compose_path": str(compose_path)}) + + # Still writes files for Docker to use write_compose(config, compose_path) console.print(f"[cyan]Mutating '{decky_name}' to services: {', '.join(decky.services)}[/]") try: - _compose_with_retry("up", "-d", "--remove-orphans", compose_file=compose_path) - except subprocess.CalledProcessError as e: - console.print(f"[red]Failed to mutate '{decky_name}': {e.stderr}[/]") + # Wrap blocking call in thread + await anyio.to_thread.run_sync(_compose_with_retry, "up", "-d", "--remove-orphans", compose_path) + except Exception as e: + console.print(f"[red]Failed to mutate '{decky_name}': {e}[/]") return False return True -def mutate_all(force: bool = False) -> None: +async def mutate_all(repo: BaseRepository, force: bool = False) -> None: """ Check all deckies and mutate those that are due. If force=True, mutates all deckies regardless of schedule. """ - state = load_state() - if state is None: + state_dict = await repo.get_state("deployment") + if state_dict is None: console.print("[red]No active deployment found.[/]") return - config, _ = state + config = DecnetConfig(**state_dict["config"]) now = time.time() mutated_count = 0 @@ -103,7 +111,7 @@ def mutate_all(force: bool = False) -> None: due = elapsed_secs >= (interval_mins * 60) if due: - success = mutate_decky(decky.name) + success = await mutate_decky(decky.name, repo=repo) if success: mutated_count += 1 @@ -111,12 +119,12 @@ def mutate_all(force: bool = False) -> None: console.print("[dim]No deckies are due for mutation.[/]") -def run_watch_loop(poll_interval_secs: int = 10) -> None: +async def run_watch_loop(repo: BaseRepository, poll_interval_secs: int = 10) -> None: """Run an infinite loop checking for deckies that need mutation.""" console.print(f"[green]DECNET Mutator Watcher started (polling every {poll_interval_secs}s).[/]") try: while True: - mutate_all(force=False) - time.sleep(poll_interval_secs) + await mutate_all(force=False, repo=repo) + await asyncio.sleep(poll_interval_secs) except KeyboardInterrupt: console.print("\n[dim]Mutator watcher stopped.[/]") diff --git a/decnet/web/api.py b/decnet/web/api.py index 29529df..e7ad054 100644 --- a/decnet/web/api.py +++ b/decnet/web/api.py @@ -32,28 +32,38 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]: log.error("DB failed to initialize after 5 attempts — startup may be degraded") await asyncio.sleep(0.5) - # Start background ingestion task - if ingestion_task is None or ingestion_task.done(): - ingestion_task = asyncio.create_task(log_ingestion_worker(repo)) + # Start background tasks only if not in contract test mode + if os.environ.get("DECNET_CONTRACT_TEST") != "true": + # Start background ingestion task + if ingestion_task is None or ingestion_task.done(): + ingestion_task = asyncio.create_task(log_ingestion_worker(repo)) - # Start Docker log collector (writes to log file; ingester reads from it) - _log_file = os.environ.get("DECNET_INGEST_LOG_FILE", DECNET_INGEST_LOG_FILE) - if _log_file and (collector_task is None or collector_task.done()): - collector_task = asyncio.create_task(log_collector_worker(_log_file)) + # Start Docker log collector (writes to log file; ingester reads from it) + _log_file = os.environ.get("DECNET_INGEST_LOG_FILE", DECNET_INGEST_LOG_FILE) + if _log_file and (collector_task is None or collector_task.done()): + collector_task = asyncio.create_task(log_collector_worker(_log_file)) + elif not _log_file: + log.warning("DECNET_INGEST_LOG_FILE not set — Docker log collection disabled.") else: - log.warning("DECNET_INGEST_LOG_FILE not set — Docker log collection disabled.") + log.info("Contract Test Mode: skipping background worker startup") yield # Shutdown background tasks for task in (ingestion_task, collector_task): - if task: + if task and not task.done(): task.cancel() + try: + await task + except asyncio.CancelledError: + pass + except Exception as exc: + log.warning("Task shutdown error: %s", exc) app: FastAPI = FastAPI( - title="DECNET Web Dashboard API", - version="1.0.0", + title="DECNET Web Dashboard API", + version="1.0.0", lifespan=lifespan, docs_url="/docs" if DECNET_DEVELOPER else None, redoc_url="/redoc" if DECNET_DEVELOPER else None, diff --git a/decnet/web/auth.py b/decnet/web/auth.py index 546ba0b..6ece1e3 100644 --- a/decnet/web/auth.py +++ b/decnet/web/auth.py @@ -12,7 +12,7 @@ ACCESS_TOKEN_EXPIRE_MINUTES: int = 1440 def verify_password(plain_password: str, hashed_password: str) -> bool: return bcrypt.checkpw( - plain_password.encode("utf-8")[:72], + plain_password.encode("utf-8")[:72], hashed_password.encode("utf-8") ) @@ -31,7 +31,7 @@ def create_access_token(data: dict[str, Any], expires_delta: Optional[timedelta] _expire = datetime.now(timezone.utc) + expires_delta else: _expire = datetime.now(timezone.utc) + timedelta(minutes=15) - + _to_encode.update({"exp": _expire}) _to_encode.update({"iat": datetime.now(timezone.utc)}) _encoded_jwt: str = jwt.encode(_to_encode, SECRET_KEY, algorithm=ALGORITHM) diff --git a/decnet/web/db/factory.py b/decnet/web/db/factory.py new file mode 100644 index 0000000..b98884e --- /dev/null +++ b/decnet/web/db/factory.py @@ -0,0 +1,18 @@ +from typing import Any +from decnet.env import os +from decnet.web.db.repository import BaseRepository + +def get_repository(**kwargs: Any) -> BaseRepository: + """Factory function to instantiate the correct repository implementation based on environment.""" + db_type = os.environ.get("DECNET_DB_TYPE", "sqlite").lower() + + if db_type == "sqlite": + from decnet.web.db.sqlite.repository import SQLiteRepository + return SQLiteRepository(**kwargs) + elif db_type == "mysql": + # Placeholder for future implementation + # from decnet.web.db.mysql.repository import MySQLRepository + # return MySQLRepository() + raise NotImplementedError("MySQL support is planned but not yet implemented.") + else: + raise ValueError(f"Unsupported database type: {db_type}") diff --git a/decnet/web/db/models.py b/decnet/web/db/models.py index 4a758d1..e6cc075 100644 --- a/decnet/web/db/models.py +++ b/decnet/web/db/models.py @@ -22,7 +22,7 @@ class Log(SQLModel, table=True): event_type: str = Field(index=True) attacker_ip: str = Field(index=True) raw_line: str - fields: str + fields: str msg: Optional[str] = None class Bounty(SQLModel, table=True): @@ -35,6 +35,12 @@ class Bounty(SQLModel, table=True): bounty_type: str = Field(index=True) payload: str + +class State(SQLModel, table=True): + __tablename__ = "state" + key: str = Field(primary_key=True) + value: str # Stores JSON serialized DecnetConfig or other state blobs + # --- API Request/Response Models (Pydantic) --- class Token(BaseModel): diff --git a/decnet/web/db/repository.py b/decnet/web/db/repository.py index 91226b9..08a6259 100644 --- a/decnet/web/db/repository.py +++ b/decnet/web/db/repository.py @@ -17,9 +17,9 @@ class BaseRepository(ABC): @abstractmethod async def get_logs( - self, - limit: int = 50, - offset: int = 0, + self, + limit: int = 50, + offset: int = 0, search: Optional[str] = None ) -> list[dict[str, Any]]: """Retrieve paginated log entries.""" @@ -67,9 +67,9 @@ class BaseRepository(ABC): @abstractmethod async def get_bounties( - self, - limit: int = 50, - offset: int = 0, + self, + limit: int = 50, + offset: int = 0, bounty_type: Optional[str] = None, search: Optional[str] = None ) -> list[dict[str, Any]]: @@ -80,3 +80,13 @@ class BaseRepository(ABC): async def get_total_bounties(self, bounty_type: Optional[str] = None, search: Optional[str] = None) -> int: """Retrieve the total count of bounties, optionally filtered.""" pass + + @abstractmethod + async def get_state(self, key: str) -> Optional[dict[str, Any]]: + """Retrieve a specific state entry by key.""" + pass + + @abstractmethod + async def set_state(self, key: str, value: Any) -> None: + """Store a specific state entry by key.""" + pass diff --git a/decnet/web/db/sqlite/database.py b/decnet/web/db/sqlite/database.py index bb51467..22ca549 100644 --- a/decnet/web/db/sqlite/database.py +++ b/decnet/web/db/sqlite/database.py @@ -1,22 +1,25 @@ -from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker -from sqlalchemy import create_engine +from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession, async_sessionmaker, create_async_engine +from sqlalchemy import create_engine, Engine from sqlmodel import SQLModel +from typing import AsyncGenerator # We need both sync and async engines for SQLite # Sync for initialization (DDL) and async for standard queries -def get_async_engine(db_path: str): +def get_async_engine(db_path: str) -> AsyncEngine: # If it's a memory URI, don't add the extra slash that turns it into a relative file prefix = "sqlite+aiosqlite:///" - if db_path.startswith("file:"): - prefix = "sqlite+aiosqlite:///" + if db_path.startswith(":memory:"): + prefix = "sqlite+aiosqlite://" return create_async_engine(f"{prefix}{db_path}", echo=False, connect_args={"uri": True}) -def get_sync_engine(db_path: str): +def get_sync_engine(db_path: str) -> Engine: prefix = "sqlite:///" + if db_path.startswith(":memory:"): + prefix = "sqlite://" return create_engine(f"{prefix}{db_path}", echo=False, connect_args={"uri": True}) -def init_db(db_path: str): +def init_db(db_path: str) -> None: """Synchronously create all tables.""" engine = get_sync_engine(db_path) # Ensure WAL mode is set @@ -25,7 +28,7 @@ def init_db(db_path: str): conn.exec_driver_sql("PRAGMA synchronous=NORMAL") SQLModel.metadata.create_all(engine) -async def get_session(engine) -> AsyncSession: +async def get_session(engine: AsyncEngine) -> AsyncGenerator[AsyncSession, None]: async_session = async_sessionmaker( engine, class_=AsyncSession, expire_on_commit=False ) diff --git a/decnet/web/db/sqlite/repository.py b/decnet/web/db/sqlite/repository.py index a2cf745..92f6721 100644 --- a/decnet/web/db/sqlite/repository.py +++ b/decnet/web/db/sqlite/repository.py @@ -6,12 +6,13 @@ from typing import Any, Optional, List from sqlalchemy import func, select, desc, asc, text, or_, update, literal_column from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker +from sqlmodel.sql.expression import SelectOfScalar from decnet.config import load_state, _ROOT from decnet.env import DECNET_ADMIN_USER, DECNET_ADMIN_PASSWORD from decnet.web.auth import get_password_hash from decnet.web.db.repository import BaseRepository -from decnet.web.db.models import User, Log, Bounty +from decnet.web.db.models import User, Log, Bounty, State from decnet.web.db.sqlite.database import get_async_engine, init_db @@ -93,11 +94,11 @@ class SQLiteRepository(BaseRepository): def _apply_filters( self, - statement, + statement: SelectOfScalar, search: Optional[str], start_time: Optional[str], end_time: Optional[str], - ): + ) -> SelectOfScalar: import re import shlex @@ -128,9 +129,10 @@ class SQLiteRepository(BaseRepository): statement = statement.where(core_fields[key] == val) else: key_safe = re.sub(r"[^a-zA-Z0-9_]", "", key) - statement = statement.where( - text(f"json_extract(fields, '$.{key_safe}') = :val") - ).params(val=val) + if key_safe: + statement = statement.where( + text(f"json_extract(fields, '$.{key_safe}') = :val") + ).params(val=val) else: lk = f"%{token}%" statement = statement.where( @@ -206,7 +208,7 @@ class SQLiteRepository(BaseRepository): end_time: Optional[str] = None, interval_minutes: int = 15, ) -> List[dict]: - bucket_seconds = interval_minutes * 60 + bucket_seconds = max(interval_minutes, 1) * 60 bucket_expr = literal_column( f"datetime((strftime('%s', timestamp) / {bucket_seconds}) * {bucket_seconds}, 'unixepoch')" ).label("bucket_time") @@ -299,7 +301,12 @@ class SQLiteRepository(BaseRepository): session.add(Bounty(**data)) await session.commit() - def _apply_bounty_filters(self, statement, bounty_type: Optional[str], search: Optional[str]): + def _apply_bounty_filters( + self, + statement: SelectOfScalar, + bounty_type: Optional[str], + search: Optional[str] + ) -> SelectOfScalar: if bounty_type: statement = statement.where(Bounty.bounty_type == bounty_type) if search: @@ -350,3 +357,29 @@ class SQLiteRepository(BaseRepository): async with self.session_factory() as session: result = await session.execute(statement) return result.scalar() or 0 + + async def get_state(self, key: str) -> Optional[dict[str, Any]]: + async with self.session_factory() as session: + statement = select(State).where(State.key == key) + result = await session.execute(statement) + state = result.scalar_one_none() + if state: + return json.loads(state.value) + return None + + async def set_state(self, key: str, value: Any) -> None: # noqa: ANN401 + async with self.session_factory() as session: + # Check if exists + statement = select(State).where(State.key == key) + result = await session.execute(statement) + state = result.scalar_one_none() + + value_json = json.dumps(value) + if state: + state.value = value_json + session.add(state) + else: + new_state = State(key=key, value=value_json) + session.add(new_state) + + await session.commit() diff --git a/decnet/web/dependencies.py b/decnet/web/dependencies.py index eee8bd9..4c31648 100644 --- a/decnet/web/dependencies.py +++ b/decnet/web/dependencies.py @@ -1,19 +1,19 @@ from typing import Any, Optional -from pathlib import Path import jwt from fastapi import HTTPException, status, Request from fastapi.security import OAuth2PasswordBearer from decnet.web.auth import ALGORITHM, SECRET_KEY -from decnet.web.db.sqlite.repository import SQLiteRepository +from decnet.web.db.repository import BaseRepository +from decnet.web.db.factory import get_repository -# Root directory for database -_ROOT_DIR = Path(__file__).parent.parent.parent.absolute() -DB_PATH = _ROOT_DIR / "decnet.db" +# Shared repository singleton +repo: BaseRepository = get_repository() -# Shared repository instance -repo = SQLiteRepository(db_path=str(DB_PATH)) +def get_repo() -> BaseRepository: + """FastAPI dependency to inject the configured repository.""" + return repo oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/v1/auth/login") @@ -53,7 +53,7 @@ async def get_current_user(request: Request) -> str: detail="Could not validate credentials", headers={"WWW-Authenticate": "Bearer"}, ) - + auth_header = request.headers.get("Authorization") token: str | None = ( auth_header.split(" ", 1)[1] diff --git a/decnet/web/ingester.py b/decnet/web/ingester.py index cdf4bfd..96a224a 100644 --- a/decnet/web/ingester.py +++ b/decnet/web/ingester.py @@ -21,7 +21,7 @@ async def log_ingestion_worker(repo: BaseRepository) -> None: _json_log_path: Path = Path(_base_log_file).with_suffix(".json") _position: int = 0 - + logger.info(f"Starting JSON log ingestion from {_json_log_path}") while True: @@ -29,24 +29,24 @@ async def log_ingestion_worker(repo: BaseRepository) -> None: if not _json_log_path.exists(): await asyncio.sleep(2) continue - + _stat: os.stat_result = _json_log_path.stat() if _stat.st_size < _position: # File rotated or truncated _position = 0 - + if _stat.st_size == _position: # No new data await asyncio.sleep(1) continue - + with open(_json_log_path, "r", encoding="utf-8", errors="replace") as _f: _f.seek(_position) while True: _line: str = _f.readline() if not _line: break # EOF reached - + if not _line.endswith('\n'): # Partial line read, don't process yet, don't advance position break @@ -58,14 +58,19 @@ async def log_ingestion_worker(repo: BaseRepository) -> None: except json.JSONDecodeError: logger.error(f"Failed to decode JSON log line: {_line}") continue - + # Update position after successful line read _position = _f.tell() - + except Exception as _e: + _err_str = str(_e).lower() + if "no such table" in _err_str or "no active connection" in _err_str or "connection closed" in _err_str: + logger.error(f"Post-shutdown or fatal DB error in ingester: {_e}") + break # Exit worker — DB is gone or uninitialized + logger.error(f"Error in log ingestion worker: {_e}") await asyncio.sleep(5) - + await asyncio.sleep(1) @@ -78,7 +83,7 @@ async def _extract_bounty(repo: BaseRepository, log_data: dict[str, Any]) -> Non # 1. Credentials (User/Pass) _user = _fields.get("username") _pass = _fields.get("password") - + if _user and _pass: await repo.add_bounty({ "decky": log_data.get("decky"), @@ -90,5 +95,5 @@ async def _extract_bounty(repo: BaseRepository, log_data: dict[str, Any]) -> Non "password": _pass } }) - + # 2. Add more extractors here later (e.g. file hashes, crypto keys) diff --git a/decnet/web/router/auth/api_change_pass.py b/decnet/web/router/auth/api_change_pass.py index 0e56a89..5c1f034 100644 --- a/decnet/web/router/auth/api_change_pass.py +++ b/decnet/web/router/auth/api_change_pass.py @@ -12,7 +12,11 @@ router = APIRouter() @router.post( "/auth/change-password", tags=["Authentication"], - responses={401: {"description": "Invalid or expired token / wrong old password"}, 422: {"description": "Validation error"}}, + responses={ + 400: {"description": "Bad Request (e.g. malformed JSON)"}, + 401: {"description": "Could not validate credentials"}, + 422: {"description": "Validation error"} + }, ) async def change_password(request: ChangePasswordRequest, current_user: str = Depends(get_current_user)) -> dict[str, str]: _user: Optional[dict[str, Any]] = await repo.get_user_by_uuid(current_user) @@ -21,7 +25,7 @@ async def change_password(request: ChangePasswordRequest, current_user: str = De status_code=status.HTTP_401_UNAUTHORIZED, detail="Incorrect old password", ) - + _new_hash: str = get_password_hash(request.new_password) await repo.update_user_password(current_user, _new_hash, must_change_password=False) return {"message": "Password updated successfully"} diff --git a/decnet/web/router/auth/api_login.py b/decnet/web/router/auth/api_login.py index 67f1aad..a9db5b7 100644 --- a/decnet/web/router/auth/api_login.py +++ b/decnet/web/router/auth/api_login.py @@ -18,7 +18,11 @@ router = APIRouter() "/auth/login", response_model=Token, tags=["Authentication"], - responses={401: {"description": "Incorrect username or password"}, 422: {"description": "Validation error"}}, + responses={ + 400: {"description": "Bad Request (e.g. malformed JSON)"}, + 401: {"description": "Incorrect username or password"}, + 422: {"description": "Validation error"} + }, ) async def login(request: LoginRequest) -> dict[str, Any]: _user: Optional[dict[str, Any]] = await repo.get_user_by_username(request.username) @@ -35,7 +39,7 @@ async def login(request: LoginRequest) -> dict[str, Any]: data={"uuid": _user["uuid"]}, expires_delta=_access_token_expires ) return { - "access_token": _access_token, + "access_token": _access_token, "token_type": "bearer", # nosec B105 "must_change_password": bool(_user.get("must_change_password", False)) } diff --git a/decnet/web/router/bounty/api_get_bounties.py b/decnet/web/router/bounty/api_get_bounties.py index ad7710a..c5c5eaa 100644 --- a/decnet/web/router/bounty/api_get_bounties.py +++ b/decnet/web/router/bounty/api_get_bounties.py @@ -9,7 +9,7 @@ router = APIRouter() @router.get("/bounty", response_model=BountyResponse, tags=["Bounty Vault"], - responses={401: {"description": "Not authenticated"}, 422: {"description": "Validation error"}},) + responses={401: {"description": "Could not validate credentials"}, 422: {"description": "Validation error"}},) async def get_bounties( limit: int = Query(50, ge=1, le=1000), offset: int = Query(0, ge=0), diff --git a/decnet/web/router/fleet/api_deploy_deckies.py b/decnet/web/router/fleet/api_deploy_deckies.py index c6d011d..ba616ce 100644 --- a/decnet/web/router/fleet/api_deploy_deckies.py +++ b/decnet/web/router/fleet/api_deploy_deckies.py @@ -3,17 +3,21 @@ import os from fastapi import APIRouter, Depends, HTTPException -from decnet.config import DEFAULT_MUTATE_INTERVAL, DecnetConfig, load_state +from decnet.config import DEFAULT_MUTATE_INTERVAL, DecnetConfig, _ROOT from decnet.engine import deploy as _deploy from decnet.ini_loader import load_ini_from_string from decnet.network import detect_interface, detect_subnet, get_host_ip -from decnet.web.dependencies import get_current_user +from decnet.web.dependencies import get_current_user, repo from decnet.web.db.models import DeployIniRequest router = APIRouter() -@router.post("/deckies/deploy", tags=["Fleet Management"]) +@router.post( + "/deckies/deploy", + tags=["Fleet Management"], + responses={401: {"description": "Could not validate credentials"}, 400: {"description": "Validation error or INI parsing failed"}, 500: {"description": "Deployment failed"}} +) async def api_deploy_deckies(req: DeployIniRequest, current_user: str = Depends(get_current_user)) -> dict[str, str]: from decnet.fleet import build_deckies_from_ini @@ -22,11 +26,11 @@ async def api_deploy_deckies(req: DeployIniRequest, current_user: str = Depends( except Exception as e: raise HTTPException(status_code=400, detail=f"Failed to parse INI: {e}") - state = load_state() + state_dict = await repo.get_state("deployment") ingest_log_file = os.environ.get("DECNET_INGEST_LOG_FILE") - - if state: - config, _ = state + + if state_dict: + config = DecnetConfig(**state_dict["config"]) subnet_cidr = ini.subnet or config.subnet gateway = ini.gateway or config.gateway host_ip = get_host_ip(config.interface) @@ -66,12 +70,20 @@ async def api_deploy_deckies(req: DeployIniRequest, current_user: str = Depends( existing_deckies_map = {d.name: d for d in config.deckies} for new_decky in new_decky_configs: existing_deckies_map[new_decky.name] = new_decky - + config.deckies = list(existing_deckies_map.values()) - + # We call deploy(config) which regenerates docker-compose and runs `up -d --remove-orphans`. try: - _deploy(config) + if os.environ.get("DECNET_CONTRACT_TEST") != "true": + _deploy(config) + + # Persist new state to DB + new_state_payload = { + "config": config.model_dump(), + "compose_path": str(_ROOT / "docker-compose.yml") if not state_dict else state_dict["compose_path"] + } + await repo.set_state("deployment", new_state_payload) except Exception as e: logging.getLogger("decnet.web.api").exception("Deployment failed: %s", e) raise HTTPException(status_code=500, detail="Deployment failed. Check server logs for details.") diff --git a/decnet/web/router/fleet/api_get_deckies.py b/decnet/web/router/fleet/api_get_deckies.py index dbd4bcf..7353373 100644 --- a/decnet/web/router/fleet/api_get_deckies.py +++ b/decnet/web/router/fleet/api_get_deckies.py @@ -8,6 +8,6 @@ router = APIRouter() @router.get("/deckies", tags=["Fleet Management"], - responses={401: {"description": "Not authenticated"}, 422: {"description": "Validation error"}},) + responses={401: {"description": "Could not validate credentials"}, 422: {"description": "Validation error"}},) async def get_deckies(current_user: str = Depends(get_current_user)) -> list[dict[str, Any]]: return await repo.get_deckies() diff --git a/decnet/web/router/fleet/api_mutate_decky.py b/decnet/web/router/fleet/api_mutate_decky.py index 06a0a2f..e3facc6 100644 --- a/decnet/web/router/fleet/api_mutate_decky.py +++ b/decnet/web/router/fleet/api_mutate_decky.py @@ -1,17 +1,25 @@ +import os from fastapi import APIRouter, Depends, HTTPException, Path from decnet.mutator import mutate_decky -from decnet.web.dependencies import get_current_user +from decnet.web.dependencies import get_current_user, repo router = APIRouter() -@router.post("/deckies/{decky_name}/mutate", tags=["Fleet Management"]) +@router.post( + "/deckies/{decky_name}/mutate", + tags=["Fleet Management"], + responses={401: {"description": "Could not validate credentials"}, 404: {"description": "Decky not found"}} +) async def api_mutate_decky( decky_name: str = Path(..., pattern=r"^[a-z0-9\-]{1,64}$"), current_user: str = Depends(get_current_user), ) -> dict[str, str]: - success = mutate_decky(decky_name) + if os.environ.get("DECNET_CONTRACT_TEST") == "true": + return {"message": f"Successfully mutated {decky_name} (Contract Test Mock)"} + + success = await mutate_decky(decky_name, repo=repo) if success: return {"message": f"Successfully mutated {decky_name}"} raise HTTPException(status_code=404, detail=f"Decky {decky_name} not found or failed to mutate") diff --git a/decnet/web/router/fleet/api_mutate_interval.py b/decnet/web/router/fleet/api_mutate_interval.py index 71bb298..06a6e32 100644 --- a/decnet/web/router/fleet/api_mutate_interval.py +++ b/decnet/web/router/fleet/api_mutate_interval.py @@ -1,22 +1,33 @@ from fastapi import APIRouter, Depends, HTTPException -from decnet.config import load_state, save_state -from decnet.web.dependencies import get_current_user +from decnet.config import DecnetConfig +from decnet.web.dependencies import get_current_user, repo from decnet.web.db.models import MutateIntervalRequest router = APIRouter() @router.put("/deckies/{decky_name}/mutate-interval", tags=["Fleet Management"], - responses={401: {"description": "Not authenticated"}, 422: {"description": "Validation error"}},) + responses={ + 400: {"description": "No active deployment found"}, + 401: {"description": "Could not validate credentials"}, + 404: {"description": "Decky not found"}, + 422: {"description": "Validation error"} + }, +) async def api_update_mutate_interval(decky_name: str, req: MutateIntervalRequest, current_user: str = Depends(get_current_user)) -> dict[str, str]: - state = load_state() - if not state: - raise HTTPException(status_code=500, detail="No active deployment") - config, compose_path = state + state_dict = await repo.get_state("deployment") + if not state_dict: + raise HTTPException(status_code=400, detail="No active deployment") + + config = DecnetConfig(**state_dict["config"]) + compose_path = state_dict["compose_path"] + decky = next((d for d in config.deckies if d.name == decky_name), None) if not decky: raise HTTPException(status_code=404, detail="Decky not found") + decky.mutate_interval = req.mutate_interval - save_state(config, compose_path) + + await repo.set_state("deployment", {"config": config.model_dump(), "compose_path": compose_path}) return {"message": "Mutation interval updated"} diff --git a/decnet/web/router/logs/api_get_histogram.py b/decnet/web/router/logs/api_get_histogram.py index 9858ddd..f1ad382 100644 --- a/decnet/web/router/logs/api_get_histogram.py +++ b/decnet/web/router/logs/api_get_histogram.py @@ -8,7 +8,7 @@ router = APIRouter() @router.get("/logs/histogram", tags=["Logs"], - responses={401: {"description": "Not authenticated"}, 422: {"description": "Validation error"}},) + responses={401: {"description": "Could not validate credentials"}, 422: {"description": "Validation error"}},) async def get_logs_histogram( search: Optional[str] = None, start_time: Optional[str] = None, diff --git a/decnet/web/router/logs/api_get_logs.py b/decnet/web/router/logs/api_get_logs.py index 097b6c4..f527696 100644 --- a/decnet/web/router/logs/api_get_logs.py +++ b/decnet/web/router/logs/api_get_logs.py @@ -7,10 +7,11 @@ from decnet.web.db.models import LogsResponse router = APIRouter() -_DATETIME_RE = r"^\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}:\d{2}$" +_DATETIME_RE = r"^(\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}:\d{2})?$" -@router.get("/logs", response_model=LogsResponse, tags=["Logs"]) +@router.get("/logs", response_model=LogsResponse, tags=["Logs"], + responses={401: {"description": "Could not validate credentials"}, 422: {"description": "Validation error"}}) async def get_logs( limit: int = Query(50, ge=1, le=1000), offset: int = Query(0, ge=0), diff --git a/decnet/web/router/stats/api_get_stats.py b/decnet/web/router/stats/api_get_stats.py index 4b92fb2..f72d8ad 100644 --- a/decnet/web/router/stats/api_get_stats.py +++ b/decnet/web/router/stats/api_get_stats.py @@ -9,6 +9,6 @@ router = APIRouter() @router.get("/stats", response_model=StatsResponse, tags=["Observability"], - responses={401: {"description": "Not authenticated"}, 422: {"description": "Validation error"}},) + responses={401: {"description": "Could not validate credentials"}, 422: {"description": "Validation error"}},) async def get_stats(current_user: str = Depends(get_current_user)) -> dict[str, Any]: return await repo.get_stats_summary() diff --git a/decnet/web/router/stream/api_stream_events.py b/decnet/web/router/stream/api_stream_events.py index e76a0de..0690b6a 100644 --- a/decnet/web/router/stream/api_stream_events.py +++ b/decnet/web/router/stream/api_stream_events.py @@ -6,6 +6,7 @@ from typing import AsyncGenerator, Optional from fastapi import APIRouter, Depends, Query, Request from fastapi.responses import StreamingResponse +from decnet.env import DECNET_DEVELOPER from decnet.web.dependencies import get_stream_user, repo log = logging.getLogger(__name__) @@ -14,20 +15,30 @@ router = APIRouter() @router.get("/stream", tags=["Observability"], - responses={401: {"description": "Not authenticated"}, 422: {"description": "Validation error"}},) + responses={ + 200: { + "content": {"text/event-stream": {}}, + "description": "Real-time Server-Sent Events (SSE) stream" + }, + 401: {"description": "Could not validate credentials"}, + 422: {"description": "Validation error"} + }, +) async def stream_events( - request: Request, - last_event_id: int = Query(0, alias="lastEventId"), + request: Request, + last_event_id: int = Query(0, alias="lastEventId"), search: Optional[str] = None, start_time: Optional[str] = None, end_time: Optional[str] = None, + max_output: Optional[int] = Query(None, alias="maxOutput"), current_user: str = Depends(get_stream_user) ) -> StreamingResponse: - + async def event_generator() -> AsyncGenerator[str, None]: last_id = last_event_id stats_interval_sec = 10 loops_since_stats = 0 + emitted_chunks = 0 try: if last_id == 0: last_id = await repo.get_max_log_id() @@ -42,6 +53,12 @@ async def stream_events( yield f"event: message\ndata: {json.dumps({'type': 'histogram', 'data': histogram})}\n\n" while True: + if DECNET_DEVELOPER and max_output is not None: + emitted_chunks += 1 + if emitted_chunks > max_output: + log.debug("Developer mode: max_output reached (%d), closing stream", max_output) + break + if await request.is_disconnected(): break @@ -65,6 +82,7 @@ async def stream_events( loops_since_stats = 0 loops_since_stats += 1 + await asyncio.sleep(1) except asyncio.CancelledError: pass diff --git a/pyproject.toml b/pyproject.toml index eed1443..8fb2180 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -51,6 +51,7 @@ decnet = "decnet.cli:app" [tool.pytest.ini_options] asyncio_mode = "auto" +asyncio_debug = "true" addopts = "-m 'not fuzz and not live' -v -q -x -n logical" markers = [ "fuzz: hypothesis-based fuzz tests (slow, run with -m fuzz or -m '' for all)", diff --git a/ruff.toml b/ruff.toml new file mode 100644 index 0000000..d7cb4c7 --- /dev/null +++ b/ruff.toml @@ -0,0 +1,30 @@ +# In your ruff.toml or pyproject.toml +target-version = "py314" # DECNET's target Python version + +exclude = [ + "tests/**", + "templates/**", + "development/**", +] + +[lint] +# Select a wide range of rules +select = [ + "F", # Pyflakes: Catches undefined names (F821) and unused variables (F841) + "ANN", # Enforces type annotations on functions and methods + "RUF", # Includes the RUF045 rule for dataclass attributes + "E", # Pycodestyle errors + "W", # Pycodestyle warnings +] + +# Ignore specific rules that might be too strict for now +ignore = [ + "E501", # Line too long +] + +[lint.extend-per-file-ignores] +# Apply strict rules only to the core codebase +"decnet/**/*.py" = [] +# Everywhere else is more relaxed +"**/*.py" = ["ANN", "RUF"] +"tests/**/*.py" = ["ANN", "RUF", "E", "W"] diff --git a/schemat b/schemat new file mode 100644 index 0000000..a2c56fe --- /dev/null +++ b/schemat @@ -0,0 +1,1549 @@ +============================= test session starts ============================== +platform linux -- Python 3.14.3, pytest-9.0.3, pluggy-1.6.0 +rootdir: /home/anti/Tools/DECNET +configfile: pyproject.toml +plugins: hypothesis-6.151.12, anyio-4.13.0, xdist-3.8.0, cov-7.1.0, asyncio-1.3.0, schemathesis-4.15.1 +asyncio: mode=Mode.AUTO, debug=True, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +created: 12/12 workers +12 workers [11 items] + +.F.FFF....F +=================================== FAILURES =================================== +___________ test_schema_compliance[api][POST /api/v1/deckies/deploy] ___________ +[gw8] linux -- Python 3.14.3 /home/anti/Tools/DECNET/.venv/bin/python3 + + Exception Group Traceback (most recent call last): + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/schemathesis/generation/hypothesis/builder.py", line 280, in test_schema_compliance + | def test_wrapper(*args: Any, **kwargs: Any) -> Any: + | ^^^ + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/hypothesis/core.py", line 2184, in wrapped_test + | _raise_to_user(errors, state.settings, [], " in explicit examples") + | ~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/hypothesis/core.py", line 1656, in _raise_to_user + | raise the_error_hypothesis_found + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/hypothesis/core.py", line 598, in execute_explicit_examples + | execute_example() + | ~~~~~~~~~~~~~~~^^ + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/hypothesis/core.py", line 1150, in execute_once + | result = self.test_runner(data, run) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/hypothesis/core.py", line 824, in default_executor + | return function(data) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/hypothesis/core.py", line 1107, in run + | return test(*args, **kwargs) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/schemathesis/generation/hypothesis/builder.py", line 280, in test_schema_compliance + | def test_wrapper(*args: Any, **kwargs: Any) -> Any: + | ^^^^^^^^^^^^^^^^^^ + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/hypothesis/core.py", line 1004, in test + | return self.test(*args, **kwargs) + | ~~~~~~~~~^^^^^^^^^^^^^^^^^ + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/schemathesis/generation/hypothesis/builder.py", line 282, in test_wrapper + | return test_function(*args, **kwargs) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/schemathesis/pytest/__init__.py", line 47, in test_wrapper + | return func(*args, **kwargs) + | File "/home/anti/Tools/DECNET/tests/api/test_schemathesis.py", line 93, in test_schema_compliance + | case.call_and_validate() + | ~~~~~~~~~~~~~~~~~~~~~~^^ + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/schemathesis/generation/case.py", line 551, in call_and_validate + | self.validate_response( + | ~~~~~~~~~~~~~~~~~~~~~~^ + | response, + | ^^^^^^^^^ + | ...<4 lines>... + | transport_kwargs=transport_kwargs, + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | ) + | ^ + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/schemathesis/generation/case.py", line 517, in validate_response + | raise FailureGroup(_failures, message) from None + | schemathesis.core.failures.FailureGroup: Schemathesis found 1 distinct failure + | + | - API rejected schema-compliant request + | + | Valid data should have been accepted + | Expected: 2xx, 401, 403, 404, 409, 5xx + | + | [400] Bad Request: + | + | `{"detail":"Failed to parse INI: Invalid INI format: no sections found."}` + | + | Reproduce with: + | + | curl -X POST -H 'Authorization: [Filtered]' -H 'Content-Type: application/json' -d '{"ini_content": "000000"}' http://127.0.0.1:8008/api/v1/deckies/deploy + | + | (1 sub-exception) + | Falsifying explicit example: test_schema_compliance( + | case=, + | ) + +-+---------------- 1 ---------------- + | schemathesis.openapi.checks.RejectedPositiveData: API rejected schema-compliant request + | + | Valid data should have been accepted + | Expected: 2xx, 401, 403, 404, 409, 5xx + +------------------------------------ +------------------------------ Captured log setup ------------------------------ +WARNING asyncio:base_events.py:2041 Executing ._asyncgen_fixture_wrapper..setup() running at /home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/pytest_asyncio/plugin.py:309> wait_for= cb=[_run_until_complete_cb() at /usr/lib64/python3.14/asyncio/base_events.py:181] created at /usr/lib64/python3.14/asyncio/runners.py:109> took 0.166 seconds +_ test_schema_compliance[api][PUT /api/v1/deckies/{decky_name}/mutate-interval] _ +[gw7] linux -- Python 3.14.3 /home/anti/Tools/DECNET/.venv/bin/python3 + + Exception Group Traceback (most recent call last): + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/schemathesis/generation/hypothesis/builder.py", line 280, in test_schema_compliance + | def test_wrapper(*args: Any, **kwargs: Any) -> Any: + | ^^^ + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/hypothesis/core.py", line 2184, in wrapped_test + | _raise_to_user(errors, state.settings, [], " in explicit examples") + | ~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/hypothesis/core.py", line 1656, in _raise_to_user + | raise the_error_hypothesis_found + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/hypothesis/core.py", line 598, in execute_explicit_examples + | execute_example() + | ~~~~~~~~~~~~~~~^^ + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/hypothesis/core.py", line 1150, in execute_once + | result = self.test_runner(data, run) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/hypothesis/core.py", line 824, in default_executor + | return function(data) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/hypothesis/core.py", line 1107, in run + | return test(*args, **kwargs) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/schemathesis/generation/hypothesis/builder.py", line 280, in test_schema_compliance + | def test_wrapper(*args: Any, **kwargs: Any) -> Any: + | ^^^^^^^^^^^^^^^^^^ + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/hypothesis/core.py", line 1004, in test + | return self.test(*args, **kwargs) + | ~~~~~~~~~^^^^^^^^^^^^^^^^^ + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/schemathesis/generation/hypothesis/builder.py", line 282, in test_wrapper + | return test_function(*args, **kwargs) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/schemathesis/pytest/__init__.py", line 47, in test_wrapper + | return func(*args, **kwargs) + | File "/home/anti/Tools/DECNET/tests/api/test_schemathesis.py", line 93, in test_schema_compliance + | case.call_and_validate() + | ~~~~~~~~~~~~~~~~~~~~~~^^ + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/schemathesis/generation/case.py", line 551, in call_and_validate + | self.validate_response( + | ~~~~~~~~~~~~~~~~~~~~~~^ + | response, + | ^^^^^^^^^ + | ...<4 lines>... + | transport_kwargs=transport_kwargs, + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | ) + | ^ + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/schemathesis/generation/case.py", line 517, in validate_response + | raise FailureGroup(_failures, message) from None + | schemathesis.core.failures.FailureGroup: Schemathesis found 1 distinct failure + | + | - API rejected schema-compliant request + | + | Valid data should have been accepted + | Expected: 2xx, 401, 403, 404, 409, 5xx + | + | [400] Bad Request: + | + | `{"detail":"No active deployment"}` + | + | Reproduce with: + | + | curl -X PUT -H 'Authorization: [Filtered]' -H 'Content-Type: application/json' -d '{"mutate_interval": 0}' http://127.0.0.1:8008/api/v1/deckies/0/mutate-interval + | + | (1 sub-exception) + | Falsifying explicit example: test_schema_compliance( + | case=, + | ) + +-+---------------- 1 ---------------- + | schemathesis.openapi.checks.RejectedPositiveData: API rejected schema-compliant request + | + | Valid data should have been accepted + | Expected: 2xx, 401, 403, 404, 409, 5xx + +------------------------------------ +------------------------------ Captured log setup ------------------------------ +WARNING asyncio:base_events.py:2041 Executing ._asyncgen_fixture_wrapper..setup() running at /home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/pytest_asyncio/plugin.py:309> wait_for= cb=[_run_until_complete_cb() at /usr/lib64/python3.14/asyncio/base_events.py:181] created at /usr/lib64/python3.14/asyncio/runners.py:109> took 0.189 seconds +________________ test_schema_compliance[api][GET /api/v1/logs] _________________ +[gw2] linux -- Python 3.14.3 /home/anti/Tools/DECNET/.venv/bin/python3 + + Exception Group Traceback (most recent call last): + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/schemathesis/generation/hypothesis/builder.py", line 280, in test_schema_compliance + | def test_wrapper(*args: Any, **kwargs: Any) -> Any: + | ^^^ + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/hypothesis/core.py", line 2184, in wrapped_test + | _raise_to_user(errors, state.settings, [], " in explicit examples") + | ~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/hypothesis/core.py", line 1656, in _raise_to_user + | raise the_error_hypothesis_found + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/hypothesis/core.py", line 598, in execute_explicit_examples + | execute_example() + | ~~~~~~~~~~~~~~~^^ + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/hypothesis/core.py", line 1150, in execute_once + | result = self.test_runner(data, run) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/hypothesis/core.py", line 824, in default_executor + | return function(data) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/hypothesis/core.py", line 1107, in run + | return test(*args, **kwargs) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/schemathesis/generation/hypothesis/builder.py", line 280, in test_schema_compliance + | def test_wrapper(*args: Any, **kwargs: Any) -> Any: + | ^^^^^^^^^^^^^^^^^^ + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/hypothesis/core.py", line 1004, in test + | return self.test(*args, **kwargs) + | ~~~~~~~~~^^^^^^^^^^^^^^^^^ + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/schemathesis/generation/hypothesis/builder.py", line 282, in test_wrapper + | return test_function(*args, **kwargs) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/schemathesis/pytest/__init__.py", line 47, in test_wrapper + | return func(*args, **kwargs) + | File "/home/anti/Tools/DECNET/tests/api/test_schemathesis.py", line 93, in test_schema_compliance + | case.call_and_validate() + | ~~~~~~~~~~~~~~~~~~~~~~^^ + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/schemathesis/generation/case.py", line 551, in call_and_validate + | self.validate_response( + | ~~~~~~~~~~~~~~~~~~~~~~^ + | response, + | ^^^^^^^^^ + | ...<4 lines>... + | transport_kwargs=transport_kwargs, + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | ) + | ^ + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/schemathesis/generation/case.py", line 517, in validate_response + | raise FailureGroup(_failures, message) from None + | schemathesis.core.failures.FailureGroup: Schemathesis found 1 distinct failure + | + | - API rejected schema-compliant request + | + | Valid data should have been accepted + | Expected: 2xx, 401, 403, 404, 409, 5xx + | + | [422] Unprocessable Content: + | + | `{"detail":[{"type":"string_pattern_mismatch","loc":["query","end_time"],"msg":"String should match pattern '^(\\d{4}-\\d{2}-\\d{2}[ T]\\d{2}:\\d{2}:\\d{2})?$'","input":"null","ctx":{"pattern":"^(\\d{4}-\\d{2}-\\d{2}[ T]\\d{2}:\\d{2}:\\d{2})?$"}}]}` + | + | Reproduce with: + | + | curl -X GET -H 'Authorization: [Filtered]' 'http://127.0.0.1:8008/api/v1/logs?limit=50&offset=0&search=00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000&start_time=&end_time=null' + | + | (1 sub-exception) + | Falsifying explicit example: test_schema_compliance( + | case=, + | ) + +-+---------------- 1 ---------------- + | schemathesis.openapi.checks.RejectedPositiveData: API rejected schema-compliant request + | + | Valid data should have been accepted + | Expected: 2xx, 401, 403, 404, 409, 5xx + +------------------------------------ +------------------------------ Captured log setup ------------------------------ +WARNING asyncio:base_events.py:2041 Executing ._asyncgen_fixture_wrapper..setup() running at /home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/pytest_asyncio/plugin.py:309> wait_for= cb=[_run_until_complete_cb() at /usr/lib64/python3.14/asyncio/base_events.py:181] created at /usr/lib64/python3.14/asyncio/runners.py:109> took 0.165 seconds +_______________ test_schema_compliance[api][GET /api/v1/bounty] ________________ +[gw4] linux -- Python 3.14.3 /home/anti/Tools/DECNET/.venv/bin/python3 + + Exception Group Traceback (most recent call last): + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/schemathesis/generation/hypothesis/builder.py", line 280, in test_schema_compliance + | def test_wrapper(*args: Any, **kwargs: Any) -> Any: + | ^^^ + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/hypothesis/core.py", line 2264, in wrapped_test + | raise the_error_hypothesis_found + | schemathesis.core.failures.FailureGroup: Schemathesis found 2 distinct failures + | + | - Undocumented HTTP status code + | + | Received: 500 + | Documented: 200, 401, 422 + | + | - Server error + | + | [500] Internal Server Error: + | + | `Internal Server Error` + | + | Reproduce with: + | + | curl -X GET -H 'Authorization: [Filtered]' 'http://127.0.0.1:8008/api/v1/bounty?bounty_type=null&offset=63155713250949868407472470705656301637&search=null&limit=286' + | + | (2 sub-exceptions) + +-+---------------- 1 ---------------- + | schemathesis.openapi.checks.UndefinedStatusCode: Undocumented HTTP status code + | + | Received: 500 + | Documented: 200, 401, 422 + +---------------- 2 ---------------- + | schemathesis.core.failures.ServerError: Server error + +------------------------------------ +------------------------------ Captured log setup ------------------------------ +WARNING asyncio:base_events.py:2041 Executing ._asyncgen_fixture_wrapper..setup() running at /home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/pytest_asyncio/plugin.py:309> wait_for= cb=[_run_until_complete_cb() at /usr/lib64/python3.14/asyncio/base_events.py:181] created at /usr/lib64/python3.14/asyncio/runners.py:109> took 0.168 seconds +_______________ test_schema_compliance[api][GET /api/v1/stream] ________________ +[gw9] linux -- Python 3.14.3 /home/anti/Tools/DECNET/.venv/bin/python3 + + Exception Group Traceback (most recent call last): + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/schemathesis/generation/hypothesis/builder.py", line 280, in test_schema_compliance + | def test_wrapper(*args: Any, **kwargs: Any) -> Any: + | ^^^ + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/hypothesis/core.py", line 2184, in wrapped_test + | _raise_to_user(errors, state.settings, [], " in explicit examples") + | ~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/hypothesis/core.py", line 1656, in _raise_to_user + | raise the_error_hypothesis_found + | BaseExceptionGroup: Hypothesis found 20 distinct failures in explicit examples. (20 sub-exceptions) + +-+---------------- 1 ---------------- + | Traceback (most recent call last): + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/urllib3/response.py", line 903, in _error_catcher + | yield + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/urllib3/response.py", line 1418, in read_chunked + | self._update_chunk_length() + | ~~~~~~~~~~~~~~~~~~~~~~~~~^^ + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/urllib3/response.py", line 1333, in _update_chunk_length + | line = self._fp.fp.readline() # type: ignore[union-attr] + | File "/usr/lib64/python3.14/socket.py", line 725, in readinto + | return self._sock.recv_into(b) + | ~~~~~~~~~~~~~~~~~~~~^^^ + | TimeoutError: timed out + | + | The above exception was the direct cause of the following exception: + | + | Traceback (most recent call last): + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/requests/models.py", line 822, in generate + | yield from self.raw.stream(chunk_size, decode_content=True) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/urllib3/response.py", line 1250, in stream + | yield from self.read_chunked(amt, decode_content=decode_content) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/urllib3/response.py", line 1397, in read_chunked + | with self._error_catcher(): + | ~~~~~~~~~~~~~~~~~~~^^ + | File "/usr/lib64/python3.14/contextlib.py", line 162, in __exit__ + | self.gen.throw(value) + | ~~~~~~~~~~~~~~^^^^^^^ + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/urllib3/response.py", line 908, in _error_catcher + | raise ReadTimeoutError(self._pool, None, "Read timed out.") from e # type: ignore[arg-type] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | urllib3.exceptions.ReadTimeoutError: HTTPConnectionPool(host='127.0.0.1', port=8008): Read timed out. + | + | During handling of the above exception, another exception occurred: + | + | Traceback (most recent call last): + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/hypothesis/core.py", line 598, in execute_explicit_examples + | execute_example() + | ~~~~~~~~~~~~~~~^^ + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/hypothesis/core.py", line 1150, in execute_once + | result = self.test_runner(data, run) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/hypothesis/core.py", line 824, in default_executor + | return function(data) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/hypothesis/core.py", line 1107, in run + | return test(*args, **kwargs) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/schemathesis/generation/hypothesis/builder.py", line 280, in test_schema_compliance + | def test_wrapper(*args: Any, **kwargs: Any) -> Any: + | ^^^^^^^^^^^^^^^^^^ + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/hypothesis/core.py", line 1004, in test + | return self.test(*args, **kwargs) + | ~~~~~~~~~^^^^^^^^^^^^^^^^^ + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/schemathesis/generation/hypothesis/builder.py", line 282, in test_wrapper + | return test_function(*args, **kwargs) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/schemathesis/pytest/__init__.py", line 47, in test_wrapper + | return func(*args, **kwargs) + | File "/home/anti/Tools/DECNET/tests/api/test_schemathesis.py", line 93, in test_schema_compliance + | start = time.perf_counter() + | ^^^^^^^^^^^^^^^^^^^^^^^^ + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/schemathesis/generation/case.py", line 543, in call_and_validate + | response = self.call(base_url, session, headers, **call_kwargs) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/schemathesis/generation/case.py", line 380, in call + | response = transport_.send( + | self, + | ...<5 lines>... + | **kwargs, + | ) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/schemathesis/transport/requests.py", line 197, in send + | response = _request(**data) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/requests/sessions.py", line 592, in request + | resp = self.send(prep, **send_kwargs) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/requests/sessions.py", line 749, in send + | r.content + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/requests/models.py", line 904, in content + | self._content = b"".join(self.iter_content(CONTENT_CHUNK_SIZE)) or b"" + | ~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/requests/models.py", line 828, in generate + | raise ConnectionError(e) + | requests.exceptions.ConnectionError: HTTPConnectionPool(host='127.0.0.1', port=8008): Read timed out. + | + | Reproduce with: + | + | curl -X GET -H 'Authorization: [Filtered]' 'http://127.0.0.1:8008/api/v1/stream?lastEventId=0&end_time=' + | Falsifying explicit example: test_schema_compliance( + | case=, + | ) + +---------------- 2 ---------------- + | Traceback (most recent call last): + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/urllib3/response.py", line 903, in _error_catcher + | yield + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/urllib3/response.py", line 1418, in read_chunked + | self._update_chunk_length() + | ~~~~~~~~~~~~~~~~~~~~~~~~~^^ + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/urllib3/response.py", line 1333, in _update_chunk_length + | line = self._fp.fp.readline() # type: ignore[union-attr] + | File "/usr/lib64/python3.14/socket.py", line 725, in readinto + | return self._sock.recv_into(b) + | ~~~~~~~~~~~~~~~~~~~~^^^ + | TimeoutError: timed out + | + | The above exception was the direct cause of the following exception: + | + | Traceback (most recent call last): + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/requests/models.py", line 822, in generate + | yield from self.raw.stream(chunk_size, decode_content=True) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/urllib3/response.py", line 1250, in stream + | yield from self.read_chunked(amt, decode_content=decode_content) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/urllib3/response.py", line 1397, in read_chunked + | with self._error_catcher(): + | ~~~~~~~~~~~~~~~~~~~^^ + | File "/usr/lib64/python3.14/contextlib.py", line 162, in __exit__ + | self.gen.throw(value) + | ~~~~~~~~~~~~~~^^^^^^^ + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/urllib3/response.py", line 908, in _error_catcher + | raise ReadTimeoutError(self._pool, None, "Read timed out.") from e # type: ignore[arg-type] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | urllib3.exceptions.ReadTimeoutError: HTTPConnectionPool(host='127.0.0.1', port=8008): Read timed out. + | + | During handling of the above exception, another exception occurred: + | + | Traceback (most recent call last): + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/hypothesis/core.py", line 598, in execute_explicit_examples + | execute_example() + | ~~~~~~~~~~~~~~~^^ + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/hypothesis/core.py", line 1150, in execute_once + | result = self.test_runner(data, run) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/hypothesis/core.py", line 824, in default_executor + | return function(data) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/hypothesis/core.py", line 1107, in run + | return test(*args, **kwargs) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/schemathesis/generation/hypothesis/builder.py", line 280, in test_schema_compliance + | def test_wrapper(*args: Any, **kwargs: Any) -> Any: + | ^^^^^^^^^^^^^^^^^^ + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/hypothesis/core.py", line 1004, in test + | return self.test(*args, **kwargs) + | ~~~~~~~~~^^^^^^^^^^^^^^^^^ + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/schemathesis/generation/hypothesis/builder.py", line 282, in test_wrapper + | return test_function(*args, **kwargs) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/schemathesis/pytest/__init__.py", line 47, in test_wrapper + | return func(*args, **kwargs) + | File "/home/anti/Tools/DECNET/tests/api/test_schemathesis.py", line 93, in test_schema_compliance + | start = time.perf_counter() + | ^^^^^^^^^^^^^^^^^^^^^^^^ + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/schemathesis/generation/case.py", line 543, in call_and_validate + | response = self.call(base_url, session, headers, **call_kwargs) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/schemathesis/generation/case.py", line 380, in call + | response = transport_.send( + | self, + | ...<5 lines>... + | **kwargs, + | ) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/schemathesis/transport/requests.py", line 197, in send + | response = _request(**data) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/requests/sessions.py", line 592, in request + | resp = self.send(prep, **send_kwargs) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/requests/sessions.py", line 749, in send + | r.content + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/requests/models.py", line 904, in content + | self._content = b"".join(self.iter_content(CONTENT_CHUNK_SIZE)) or b"" + | ~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/requests/models.py", line 828, in generate + | raise ConnectionError(e) + | requests.exceptions.ConnectionError: HTTPConnectionPool(host='127.0.0.1', port=8008): Read timed out. + | + | Reproduce with: + | + | curl -X GET -H 'Authorization: [Filtered]' 'http://127.0.0.1:8008/api/v1/stream?token=%5BFiltered%5D&x-schemathesis-unknown-property=42' + | Falsifying explicit example: test_schema_compliance( + | case=, + | ) + +---------------- 3 ---------------- + | Traceback (most recent call last): + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/urllib3/response.py", line 903, in _error_catcher + | yield + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/urllib3/response.py", line 1418, in read_chunked + | self._update_chunk_length() + | ~~~~~~~~~~~~~~~~~~~~~~~~~^^ + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/urllib3/response.py", line 1333, in _update_chunk_length + | line = self._fp.fp.readline() # type: ignore[union-attr] + | File "/usr/lib64/python3.14/socket.py", line 725, in readinto + | return self._sock.recv_into(b) + | ~~~~~~~~~~~~~~~~~~~~^^^ + | TimeoutError: timed out + | + | The above exception was the direct cause of the following exception: + | + | Traceback (most recent call last): + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/requests/models.py", line 822, in generate + | yield from self.raw.stream(chunk_size, decode_content=True) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/urllib3/response.py", line 1250, in stream + | yield from self.read_chunked(amt, decode_content=decode_content) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/urllib3/response.py", line 1397, in read_chunked + | with self._error_catcher(): + | ~~~~~~~~~~~~~~~~~~~^^ + | File "/usr/lib64/python3.14/contextlib.py", line 162, in __exit__ + | self.gen.throw(value) + | ~~~~~~~~~~~~~~^^^^^^^ + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/urllib3/response.py", line 908, in _error_catcher + | raise ReadTimeoutError(self._pool, None, "Read timed out.") from e # type: ignore[arg-type] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | urllib3.exceptions.ReadTimeoutError: HTTPConnectionPool(host='127.0.0.1', port=8008): Read timed out. + | + | During handling of the above exception, another exception occurred: + | + | Traceback (most recent call last): + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/hypothesis/core.py", line 598, in execute_explicit_examples + | execute_example() + | ~~~~~~~~~~~~~~~^^ + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/hypothesis/core.py", line 1150, in execute_once + | result = self.test_runner(data, run) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/hypothesis/core.py", line 824, in default_executor + | return function(data) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/hypothesis/core.py", line 1107, in run + | return test(*args, **kwargs) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/schemathesis/generation/hypothesis/builder.py", line 280, in test_schema_compliance + | def test_wrapper(*args: Any, **kwargs: Any) -> Any: + | ^^^^^^^^^^^^^^^^^^ + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/hypothesis/core.py", line 1004, in test + | return self.test(*args, **kwargs) + | ~~~~~~~~~^^^^^^^^^^^^^^^^^ + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/schemathesis/generation/hypothesis/builder.py", line 282, in test_wrapper + | return test_function(*args, **kwargs) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/schemathesis/pytest/__init__.py", line 47, in test_wrapper + | return func(*args, **kwargs) + | File "/home/anti/Tools/DECNET/tests/api/test_schemathesis.py", line 93, in test_schema_compliance + | start = time.perf_counter() + | ^^^^^^^^^^^^^^^^^^^^^^^^ + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/schemathesis/generation/case.py", line 543, in call_and_validate + | response = self.call(base_url, session, headers, **call_kwargs) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/schemathesis/generation/case.py", line 380, in call + | response = transport_.send( + | self, + | ...<5 lines>... + | **kwargs, + | ) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/schemathesis/transport/requests.py", line 197, in send + | response = _request(**data) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/requests/sessions.py", line 592, in request + | resp = self.send(prep, **send_kwargs) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/requests/sessions.py", line 749, in send + | r.content + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/requests/models.py", line 904, in content + | self._content = b"".join(self.iter_content(CONTENT_CHUNK_SIZE)) or b"" + | ~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/requests/models.py", line 828, in generate + | raise ConnectionError(e) + | requests.exceptions.ConnectionError: HTTPConnectionPool(host='127.0.0.1', port=8008): Read timed out. + | + | Reproduce with: + | + | curl -X GET -H 'Authorization: [Filtered]' 'http://127.0.0.1:8008/api/v1/stream?token=%5BFiltered%5D' + | Falsifying explicit example: test_schema_compliance( + | case=, + | ) + +---------------- 4 ---------------- + | Traceback (most recent call last): + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/urllib3/response.py", line 903, in _error_catcher + | yield + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/urllib3/response.py", line 1418, in read_chunked + | self._update_chunk_length() + | ~~~~~~~~~~~~~~~~~~~~~~~~~^^ + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/urllib3/response.py", line 1333, in _update_chunk_length + | line = self._fp.fp.readline() # type: ignore[union-attr] + | File "/usr/lib64/python3.14/socket.py", line 725, in readinto + | return self._sock.recv_into(b) + | ~~~~~~~~~~~~~~~~~~~~^^^ + | TimeoutError: timed out + | + | The above exception was the direct cause of the following exception: + | + | Traceback (most recent call last): + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/requests/models.py", line 822, in generate + | yield from self.raw.stream(chunk_size, decode_content=True) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/urllib3/response.py", line 1250, in stream + | yield from self.read_chunked(amt, decode_content=decode_content) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/urllib3/response.py", line 1397, in read_chunked + | with self._error_catcher(): + | ~~~~~~~~~~~~~~~~~~~^^ + | File "/usr/lib64/python3.14/contextlib.py", line 162, in __exit__ + | self.gen.throw(value) + | ~~~~~~~~~~~~~~^^^^^^^ + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/urllib3/response.py", line 908, in _error_catcher + | raise ReadTimeoutError(self._pool, None, "Read timed out.") from e # type: ignore[arg-type] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | urllib3.exceptions.ReadTimeoutError: HTTPConnectionPool(host='127.0.0.1', port=8008): Read timed out. + | + | During handling of the above exception, another exception occurred: + | + | Traceback (most recent call last): + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/hypothesis/core.py", line 598, in execute_explicit_examples + | execute_example() + | ~~~~~~~~~~~~~~~^^ + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/hypothesis/core.py", line 1150, in execute_once + | result = self.test_runner(data, run) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/hypothesis/core.py", line 824, in default_executor + | return function(data) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/hypothesis/core.py", line 1107, in run + | return test(*args, **kwargs) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/schemathesis/generation/hypothesis/builder.py", line 280, in test_schema_compliance + | def test_wrapper(*args: Any, **kwargs: Any) -> Any: + | ^^^^^^^^^^^^^^^^^^ + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/hypothesis/core.py", line 1004, in test + | return self.test(*args, **kwargs) + | ~~~~~~~~~^^^^^^^^^^^^^^^^^ + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/schemathesis/generation/hypothesis/builder.py", line 282, in test_wrapper + | return test_function(*args, **kwargs) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/schemathesis/pytest/__init__.py", line 47, in test_wrapper + | return func(*args, **kwargs) + | File "/home/anti/Tools/DECNET/tests/api/test_schemathesis.py", line 93, in test_schema_compliance + | start = time.perf_counter() + | ^^^^^^^^^^^^^^^^^^^^^^^^ + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/schemathesis/generation/case.py", line 543, in call_and_validate + | response = self.call(base_url, session, headers, **call_kwargs) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/schemathesis/generation/case.py", line 380, in call + | response = transport_.send( + | self, + | ...<5 lines>... + | **kwargs, + | ) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/schemathesis/transport/requests.py", line 197, in send + | response = _request(**data) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/requests/sessions.py", line 592, in request + | resp = self.send(prep, **send_kwargs) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/requests/sessions.py", line 749, in send + | r.content + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/requests/models.py", line 904, in content + | self._content = b"".join(self.iter_content(CONTENT_CHUNK_SIZE)) or b"" + | ~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/requests/models.py", line 828, in generate + | raise ConnectionError(e) + | requests.exceptions.ConnectionError: HTTPConnectionPool(host='127.0.0.1', port=8008): Read timed out. + | + | Reproduce with: + | + | curl -X GET -H 'Authorization: [Filtered]' 'http://127.0.0.1:8008/api/v1/stream?token=%5BFiltered%5D' + | Falsifying explicit example: test_schema_compliance( + | case=, + | ) + +---------------- 5 ---------------- + | Traceback (most recent call last): + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/urllib3/response.py", line 903, in _error_catcher + | yield + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/urllib3/response.py", line 1418, in read_chunked + | self._update_chunk_length() + | ~~~~~~~~~~~~~~~~~~~~~~~~~^^ + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/urllib3/response.py", line 1333, in _update_chunk_length + | line = self._fp.fp.readline() # type: ignore[union-attr] + | File "/usr/lib64/python3.14/socket.py", line 725, in readinto + | return self._sock.recv_into(b) + | ~~~~~~~~~~~~~~~~~~~~^^^ + | TimeoutError: timed out + | + | The above exception was the direct cause of the following exception: + | + | Traceback (most recent call last): + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/requests/models.py", line 822, in generate + | yield from self.raw.stream(chunk_size, decode_content=True) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/urllib3/response.py", line 1250, in stream + | yield from self.read_chunked(amt, decode_content=decode_content) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/urllib3/response.py", line 1397, in read_chunked + | with self._error_catcher(): + | ~~~~~~~~~~~~~~~~~~~^^ + | File "/usr/lib64/python3.14/contextlib.py", line 162, in __exit__ + | self.gen.throw(value) + | ~~~~~~~~~~~~~~^^^^^^^ + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/urllib3/response.py", line 908, in _error_catcher + | raise ReadTimeoutError(self._pool, None, "Read timed out.") from e # type: ignore[arg-type] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | urllib3.exceptions.ReadTimeoutError: HTTPConnectionPool(host='127.0.0.1', port=8008): Read timed out. + | + | During handling of the above exception, another exception occurred: + | + | Traceback (most recent call last): + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/hypothesis/core.py", line 598, in execute_explicit_examples + | execute_example() + | ~~~~~~~~~~~~~~~^^ + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/hypothesis/core.py", line 1150, in execute_once + | result = self.test_runner(data, run) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/hypothesis/core.py", line 824, in default_executor + | return function(data) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/hypothesis/core.py", line 1107, in run + | return test(*args, **kwargs) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/schemathesis/generation/hypothesis/builder.py", line 280, in test_schema_compliance + | def test_wrapper(*args: Any, **kwargs: Any) -> Any: + | ^^^^^^^^^^^^^^^^^^ + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/hypothesis/core.py", line 1004, in test + | return self.test(*args, **kwargs) + | ~~~~~~~~~^^^^^^^^^^^^^^^^^ + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/schemathesis/generation/hypothesis/builder.py", line 282, in test_wrapper + | return test_function(*args, **kwargs) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/schemathesis/pytest/__init__.py", line 47, in test_wrapper + | return func(*args, **kwargs) + | File "/home/anti/Tools/DECNET/tests/api/test_schemathesis.py", line 93, in test_schema_compliance + | start = time.perf_counter() + | ^^^^^^^^^^^^^^^^^^^^^^^^ + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/schemathesis/generation/case.py", line 543, in call_and_validate + | response = self.call(base_url, session, headers, **call_kwargs) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/schemathesis/generation/case.py", line 380, in call + | response = transport_.send( + | self, + | ...<5 lines>... + | **kwargs, + | ) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/schemathesis/transport/requests.py", line 197, in send + | response = _request(**data) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/requests/sessions.py", line 592, in request + | resp = self.send(prep, **send_kwargs) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/requests/sessions.py", line 749, in send + | r.content + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/requests/models.py", line 904, in content + | self._content = b"".join(self.iter_content(CONTENT_CHUNK_SIZE)) or b"" + | ~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/requests/models.py", line 828, in generate + | raise ConnectionError(e) + | requests.exceptions.ConnectionError: HTTPConnectionPool(host='127.0.0.1', port=8008): Read timed out. + | + | Reproduce with: + | + | curl -X GET -H 'Authorization: [Filtered]' 'http://127.0.0.1:8008/api/v1/stream?token=%5BFiltered%5D' + | Falsifying explicit example: test_schema_compliance( + | case=, + | ) + +---------------- 6 ---------------- + | Traceback (most recent call last): + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/urllib3/response.py", line 903, in _error_catcher + | yield + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/urllib3/response.py", line 1418, in read_chunked + | self._update_chunk_length() + | ~~~~~~~~~~~~~~~~~~~~~~~~~^^ + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/urllib3/response.py", line 1333, in _update_chunk_length + | line = self._fp.fp.readline() # type: ignore[union-attr] + | File "/usr/lib64/python3.14/socket.py", line 725, in readinto + | return self._sock.recv_into(b) + | ~~~~~~~~~~~~~~~~~~~~^^^ + | TimeoutError: timed out + | + | The above exception was the direct cause of the following exception: + | + | Traceback (most recent call last): + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/requests/models.py", line 822, in generate + | yield from self.raw.stream(chunk_size, decode_content=True) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/urllib3/response.py", line 1250, in stream + | yield from self.read_chunked(amt, decode_content=decode_content) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/urllib3/response.py", line 1397, in read_chunked + | with self._error_catcher(): + | ~~~~~~~~~~~~~~~~~~~^^ + | File "/usr/lib64/python3.14/contextlib.py", line 162, in __exit__ + | self.gen.throw(value) + | ~~~~~~~~~~~~~~^^^^^^^ + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/urllib3/response.py", line 908, in _error_catcher + | raise ReadTimeoutError(self._pool, None, "Read timed out.") from e # type: ignore[arg-type] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | urllib3.exceptions.ReadTimeoutError: HTTPConnectionPool(host='127.0.0.1', port=8008): Read timed out. + | + | During handling of the above exception, another exception occurred: + | + | Traceback (most recent call last): + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/hypothesis/core.py", line 598, in execute_explicit_examples + | execute_example() + | ~~~~~~~~~~~~~~~^^ + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/hypothesis/core.py", line 1150, in execute_once + | result = self.test_runner(data, run) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/hypothesis/core.py", line 824, in default_executor + | return function(data) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/hypothesis/core.py", line 1107, in run + | return test(*args, **kwargs) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/schemathesis/generation/hypothesis/builder.py", line 280, in test_schema_compliance + | def test_wrapper(*args: Any, **kwargs: Any) -> Any: + | ^^^^^^^^^^^^^^^^^^ + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/hypothesis/core.py", line 1004, in test + | return self.test(*args, **kwargs) + | ~~~~~~~~~^^^^^^^^^^^^^^^^^ + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/schemathesis/generation/hypothesis/builder.py", line 282, in test_wrapper + | return test_function(*args, **kwargs) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/schemathesis/pytest/__init__.py", line 47, in test_wrapper + | return func(*args, **kwargs) + | File "/home/anti/Tools/DECNET/tests/api/test_schemathesis.py", line 93, in test_schema_compliance + | start = time.perf_counter() + | ^^^^^^^^^^^^^^^^^^^^^^^^ + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/schemathesis/generation/case.py", line 543, in call_and_validate + | response = self.call(base_url, session, headers, **call_kwargs) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/schemathesis/generation/case.py", line 380, in call + | response = transport_.send( + | self, + | ...<5 lines>... + | **kwargs, + | ) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/schemathesis/transport/requests.py", line 197, in send + | response = _request(**data) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/requests/sessions.py", line 592, in request + | resp = self.send(prep, **send_kwargs) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/requests/sessions.py", line 749, in send + | r.content + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/requests/models.py", line 904, in content + | self._content = b"".join(self.iter_content(CONTENT_CHUNK_SIZE)) or b"" + | ~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/requests/models.py", line 828, in generate + | raise ConnectionError(e) + | requests.exceptions.ConnectionError: HTTPConnectionPool(host='127.0.0.1', port=8008): Read timed out. + | + | Reproduce with: + | + | curl -X GET -H 'Authorization: [Filtered]' 'http://127.0.0.1:8008/api/v1/stream?start_time=null&x-schemathesis-unknown-property=42' + | Falsifying explicit example: test_schema_compliance( + | case=, + | ) + +---------------- 7 ---------------- + | Traceback (most recent call last): + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/urllib3/response.py", line 903, in _error_catcher + | yield + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/urllib3/response.py", line 1418, in read_chunked + | self._update_chunk_length() + | ~~~~~~~~~~~~~~~~~~~~~~~~~^^ + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/urllib3/response.py", line 1333, in _update_chunk_length + | line = self._fp.fp.readline() # type: ignore[union-attr] + | File "/usr/lib64/python3.14/socket.py", line 725, in readinto + | return self._sock.recv_into(b) + | ~~~~~~~~~~~~~~~~~~~~^^^ + | TimeoutError: timed out + | + | The above exception was the direct cause of the following exception: + | + | Traceback (most recent call last): + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/requests/models.py", line 822, in generate + | yield from self.raw.stream(chunk_size, decode_content=True) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/urllib3/response.py", line 1250, in stream + | yield from self.read_chunked(amt, decode_content=decode_content) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/urllib3/response.py", line 1397, in read_chunked + | with self._error_catcher(): + | ~~~~~~~~~~~~~~~~~~~^^ + | File "/usr/lib64/python3.14/contextlib.py", line 162, in __exit__ + | self.gen.throw(value) + | ~~~~~~~~~~~~~~^^^^^^^ + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/urllib3/response.py", line 908, in _error_catcher + | raise ReadTimeoutError(self._pool, None, "Read timed out.") from e # type: ignore[arg-type] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | urllib3.exceptions.ReadTimeoutError: HTTPConnectionPool(host='127.0.0.1', port=8008): Read timed out. + | + | During handling of the above exception, another exception occurred: + | + | Traceback (most recent call last): + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/hypothesis/core.py", line 598, in execute_explicit_examples + | execute_example() + | ~~~~~~~~~~~~~~~^^ + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/hypothesis/core.py", line 1150, in execute_once + | result = self.test_runner(data, run) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/hypothesis/core.py", line 824, in default_executor + | return function(data) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/hypothesis/core.py", line 1107, in run + | return test(*args, **kwargs) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/schemathesis/generation/hypothesis/builder.py", line 280, in test_schema_compliance + | def test_wrapper(*args: Any, **kwargs: Any) -> Any: + | ^^^^^^^^^^^^^^^^^^ + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/hypothesis/core.py", line 1004, in test + | return self.test(*args, **kwargs) + | ~~~~~~~~~^^^^^^^^^^^^^^^^^ + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/schemathesis/generation/hypothesis/builder.py", line 282, in test_wrapper + | return test_function(*args, **kwargs) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/schemathesis/pytest/__init__.py", line 47, in test_wrapper + | return func(*args, **kwargs) + | File "/home/anti/Tools/DECNET/tests/api/test_schemathesis.py", line 93, in test_schema_compliance + | start = time.perf_counter() + | ^^^^^^^^^^^^^^^^^^^^^^^^ + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/schemathesis/generation/case.py", line 543, in call_and_validate + | response = self.call(base_url, session, headers, **call_kwargs) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/schemathesis/generation/case.py", line 380, in call + | response = transport_.send( + | self, + | ...<5 lines>... + | **kwargs, + | ) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/schemathesis/transport/requests.py", line 197, in send + | response = _request(**data) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/requests/sessions.py", line 592, in request + | resp = self.send(prep, **send_kwargs) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/requests/sessions.py", line 749, in send + | r.content + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/requests/models.py", line 904, in content + | self._content = b"".join(self.iter_content(CONTENT_CHUNK_SIZE)) or b"" + | ~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/requests/models.py", line 828, in generate + | raise ConnectionError(e) + | requests.exceptions.ConnectionError: HTTPConnectionPool(host='127.0.0.1', port=8008): Read timed out. + | + | Reproduce with: + | + | curl -X GET -H 'Authorization: [Filtered]' 'http://127.0.0.1:8008/api/v1/stream?start_time=null&start_time=null' + | Falsifying explicit example: test_schema_compliance( + | case=, + | ) + +---------------- 8 ---------------- + | Traceback (most recent call last): + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/urllib3/response.py", line 903, in _error_catcher + | yield + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/urllib3/response.py", line 1418, in read_chunked + | self._update_chunk_length() + | ~~~~~~~~~~~~~~~~~~~~~~~~~^^ + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/urllib3/response.py", line 1333, in _update_chunk_length + | line = self._fp.fp.readline() # type: ignore[union-attr] + | File "/usr/lib64/python3.14/socket.py", line 725, in readinto + | return self._sock.recv_into(b) + | ~~~~~~~~~~~~~~~~~~~~^^^ + | TimeoutError: timed out + | + | The above exception was the direct cause of the following exception: + | + | Traceback (most recent call last): + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/requests/models.py", line 822, in generate + | yield from self.raw.stream(chunk_size, decode_content=True) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/urllib3/response.py", line 1250, in stream + | yield from self.read_chunked(amt, decode_content=decode_content) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/urllib3/response.py", line 1397, in read_chunked + | with self._error_catcher(): + | ~~~~~~~~~~~~~~~~~~~^^ + | File "/usr/lib64/python3.14/contextlib.py", line 162, in __exit__ + | self.gen.throw(value) + | ~~~~~~~~~~~~~~^^^^^^^ + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/urllib3/response.py", line 908, in _error_catcher + | raise ReadTimeoutError(self._pool, None, "Read timed out.") from e # type: ignore[arg-type] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | urllib3.exceptions.ReadTimeoutError: HTTPConnectionPool(host='127.0.0.1', port=8008): Read timed out. + | + | During handling of the above exception, another exception occurred: + | + | Traceback (most recent call last): + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/hypothesis/core.py", line 598, in execute_explicit_examples + | execute_example() + | ~~~~~~~~~~~~~~~^^ + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/hypothesis/core.py", line 1150, in execute_once + | result = self.test_runner(data, run) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/hypothesis/core.py", line 824, in default_executor + | return function(data) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/hypothesis/core.py", line 1107, in run + | return test(*args, **kwargs) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/schemathesis/generation/hypothesis/builder.py", line 280, in test_schema_compliance + | def test_wrapper(*args: Any, **kwargs: Any) -> Any: + | ^^^^^^^^^^^^^^^^^^ + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/hypothesis/core.py", line 1004, in test + | return self.test(*args, **kwargs) + | ~~~~~~~~~^^^^^^^^^^^^^^^^^ + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/schemathesis/generation/hypothesis/builder.py", line 282, in test_wrapper + | return test_function(*args, **kwargs) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/schemathesis/pytest/__init__.py", line 47, in test_wrapper + | return func(*args, **kwargs) + | File "/home/anti/Tools/DECNET/tests/api/test_schemathesis.py", line 93, in test_schema_compliance + | start = time.perf_counter() + | ^^^^^^^^^^^^^^^^^^^^^^^^ + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/schemathesis/generation/case.py", line 543, in call_and_validate + | response = self.call(base_url, session, headers, **call_kwargs) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/schemathesis/generation/case.py", line 380, in call + | response = transport_.send( + | self, + | ...<5 lines>... + | **kwargs, + | ) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/schemathesis/transport/requests.py", line 197, in send + | response = _request(**data) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/requests/sessions.py", line 592, in request + | resp = self.send(prep, **send_kwargs) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/requests/sessions.py", line 749, in send + | r.content + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/requests/models.py", line 904, in content + | self._content = b"".join(self.iter_content(CONTENT_CHUNK_SIZE)) or b"" + | ~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/requests/models.py", line 828, in generate + | raise ConnectionError(e) + | requests.exceptions.ConnectionError: HTTPConnectionPool(host='127.0.0.1', port=8008): Read timed out. + | + | Reproduce with: + | + | curl -X GET -H 'Authorization: [Filtered]' 'http://127.0.0.1:8008/api/v1/stream?start_time=0' + | Falsifying explicit example: test_schema_compliance( + | case=, + | ) + +---------------- 9 ---------------- + | Traceback (most recent call last): + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/urllib3/response.py", line 903, in _error_catcher + | yield + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/urllib3/response.py", line 1418, in read_chunked + | self._update_chunk_length() + | ~~~~~~~~~~~~~~~~~~~~~~~~~^^ + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/urllib3/response.py", line 1333, in _update_chunk_length + | line = self._fp.fp.readline() # type: ignore[union-attr] + | File "/usr/lib64/python3.14/socket.py", line 725, in readinto + | return self._sock.recv_into(b) + | ~~~~~~~~~~~~~~~~~~~~^^^ + | TimeoutError: timed out + | + | The above exception was the direct cause of the following exception: + | + | Traceback (most recent call last): + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/requests/models.py", line 822, in generate + | yield from self.raw.stream(chunk_size, decode_content=True) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/urllib3/response.py", line 1250, in stream + | yield from self.read_chunked(amt, decode_content=decode_content) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/urllib3/response.py", line 1397, in read_chunked + | with self._error_catcher(): + | ~~~~~~~~~~~~~~~~~~~^^ + | File "/usr/lib64/python3.14/contextlib.py", line 162, in __exit__ + | self.gen.throw(value) + | ~~~~~~~~~~~~~~^^^^^^^ + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/urllib3/response.py", line 908, in _error_catcher + | raise ReadTimeoutError(self._pool, None, "Read timed out.") from e # type: ignore[arg-type] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | urllib3.exceptions.ReadTimeoutError: HTTPConnectionPool(host='127.0.0.1', port=8008): Read timed out. + | + | During handling of the above exception, another exception occurred: + | + | Traceback (most recent call last): + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/hypothesis/core.py", line 598, in execute_explicit_examples + | execute_example() + | ~~~~~~~~~~~~~~~^^ + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/hypothesis/core.py", line 1150, in execute_once + | result = self.test_runner(data, run) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/hypothesis/core.py", line 824, in default_executor + | return function(data) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/hypothesis/core.py", line 1107, in run + | return test(*args, **kwargs) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/schemathesis/generation/hypothesis/builder.py", line 280, in test_schema_compliance + | def test_wrapper(*args: Any, **kwargs: Any) -> Any: + | ^^^^^^^^^^^^^^^^^^ + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/hypothesis/core.py", line 1004, in test + | return self.test(*args, **kwargs) + | ~~~~~~~~~^^^^^^^^^^^^^^^^^ + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/schemathesis/generation/hypothesis/builder.py", line 282, in test_wrapper + | return test_function(*args, **kwargs) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/schemathesis/pytest/__init__.py", line 47, in test_wrapper + | return func(*args, **kwargs) + | File "/home/anti/Tools/DECNET/tests/api/test_schemathesis.py", line 93, in test_schema_compliance + | start = time.perf_counter() + | ^^^^^^^^^^^^^^^^^^^^^^^^ + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/schemathesis/generation/case.py", line 543, in call_and_validate + | response = self.call(base_url, session, headers, **call_kwargs) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/schemathesis/generation/case.py", line 380, in call + | response = transport_.send( + | self, + | ...<5 lines>... + | **kwargs, + | ) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/schemathesis/transport/requests.py", line 197, in send + | response = _request(**data) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/requests/sessions.py", line 592, in request + | resp = self.send(prep, **send_kwargs) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/requests/sessions.py", line 749, in send + | r.content + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/requests/models.py", line 904, in content + | self._content = b"".join(self.iter_content(CONTENT_CHUNK_SIZE)) or b"" + | ~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/requests/models.py", line 828, in generate + | raise ConnectionError(e) + | requests.exceptions.ConnectionError: HTTPConnectionPool(host='127.0.0.1', port=8008): Read timed out. + | + | Reproduce with: + | + | curl -X GET -H 'Authorization: [Filtered]' 'http://127.0.0.1:8008/api/v1/stream?start_time=' + | Falsifying explicit example: test_schema_compliance( + | case=, + | ) + +---------------- 10 ---------------- + | Traceback (most recent call last): + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/urllib3/response.py", line 903, in _error_catcher + | yield + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/urllib3/response.py", line 1418, in read_chunked + | self._update_chunk_length() + | ~~~~~~~~~~~~~~~~~~~~~~~~~^^ + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/urllib3/response.py", line 1333, in _update_chunk_length + | line = self._fp.fp.readline() # type: ignore[union-attr] + | File "/usr/lib64/python3.14/socket.py", line 725, in readinto + | return self._sock.recv_into(b) + | ~~~~~~~~~~~~~~~~~~~~^^^ + | TimeoutError: timed out + | + | The above exception was the direct cause of the following exception: + | + | Traceback (most recent call last): + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/requests/models.py", line 822, in generate + | yield from self.raw.stream(chunk_size, decode_content=True) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/urllib3/response.py", line 1250, in stream + | yield from self.read_chunked(amt, decode_content=decode_content) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/urllib3/response.py", line 1397, in read_chunked + | with self._error_catcher(): + | ~~~~~~~~~~~~~~~~~~~^^ + | File "/usr/lib64/python3.14/contextlib.py", line 162, in __exit__ + | self.gen.throw(value) + | ~~~~~~~~~~~~~~^^^^^^^ + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/urllib3/response.py", line 908, in _error_catcher + | raise ReadTimeoutError(self._pool, None, "Read timed out.") from e # type: ignore[arg-type] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | urllib3.exceptions.ReadTimeoutError: HTTPConnectionPool(host='127.0.0.1', port=8008): Read timed out. + | + | During handling of the above exception, another exception occurred: + | + | Traceback (most recent call last): + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/hypothesis/core.py", line 598, in execute_explicit_examples + | execute_example() + | ~~~~~~~~~~~~~~~^^ + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/hypothesis/core.py", line 1150, in execute_once + | result = self.test_runner(data, run) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/hypothesis/core.py", line 824, in default_executor + | return function(data) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/hypothesis/core.py", line 1107, in run + | return test(*args, **kwargs) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/schemathesis/generation/hypothesis/builder.py", line 280, in test_schema_compliance + | def test_wrapper(*args: Any, **kwargs: Any) -> Any: + | ^^^^^^^^^^^^^^^^^^ + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/hypothesis/core.py", line 1004, in test + | return self.test(*args, **kwargs) + | ~~~~~~~~~^^^^^^^^^^^^^^^^^ + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/schemathesis/generation/hypothesis/builder.py", line 282, in test_wrapper + | return test_function(*args, **kwargs) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/schemathesis/pytest/__init__.py", line 47, in test_wrapper + | return func(*args, **kwargs) + | File "/home/anti/Tools/DECNET/tests/api/test_schemathesis.py", line 93, in test_schema_compliance + | start = time.perf_counter() + | ^^^^^^^^^^^^^^^^^^^^^^^^ + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/schemathesis/generation/case.py", line 543, in call_and_validate + | response = self.call(base_url, session, headers, **call_kwargs) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/schemathesis/generation/case.py", line 380, in call + | response = transport_.send( + | self, + | ...<5 lines>... + | **kwargs, + | ) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/schemathesis/transport/requests.py", line 197, in send + | response = _request(**data) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/requests/sessions.py", line 592, in request + | resp = self.send(prep, **send_kwargs) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/requests/sessions.py", line 749, in send + | r.content + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/requests/models.py", line 904, in content + | self._content = b"".join(self.iter_content(CONTENT_CHUNK_SIZE)) or b"" + | ~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/requests/models.py", line 828, in generate + | raise ConnectionError(e) + | requests.exceptions.ConnectionError: HTTPConnectionPool(host='127.0.0.1', port=8008): Read timed out. + | + | Reproduce with: + | + | curl -X GET -H 'Authorization: [Filtered]' 'http://127.0.0.1:8008/api/v1/stream?search=null&x-schemathesis-unknown-property=42' + | Falsifying explicit example: test_schema_compliance( + | case=, + | ) + +---------------- 11 ---------------- + | Traceback (most recent call last): + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/urllib3/response.py", line 903, in _error_catcher + | yield + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/urllib3/response.py", line 1418, in read_chunked + | self._update_chunk_length() + | ~~~~~~~~~~~~~~~~~~~~~~~~~^^ + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/urllib3/response.py", line 1333, in _update_chunk_length + | line = self._fp.fp.readline() # type: ignore[union-attr] + | File "/usr/lib64/python3.14/socket.py", line 725, in readinto + | return self._sock.recv_into(b) + | ~~~~~~~~~~~~~~~~~~~~^^^ + | TimeoutError: timed out + | + | The above exception was the direct cause of the following exception: + | + | Traceback (most recent call last): + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/requests/models.py", line 822, in generate + | yield from self.raw.stream(chunk_size, decode_content=True) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/urllib3/response.py", line 1250, in stream + | yield from self.read_chunked(amt, decode_content=decode_content) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/urllib3/response.py", line 1397, in read_chunked + | with self._error_catcher(): + | ~~~~~~~~~~~~~~~~~~~^^ + | File "/usr/lib64/python3.14/contextlib.py", line 162, in __exit__ + | self.gen.throw(value) + | ~~~~~~~~~~~~~~^^^^^^^ + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/urllib3/response.py", line 908, in _error_catcher + | raise ReadTimeoutError(self._pool, None, "Read timed out.") from e # type: ignore[arg-type] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | urllib3.exceptions.ReadTimeoutError: HTTPConnectionPool(host='127.0.0.1', port=8008): Read timed out. + | + | During handling of the above exception, another exception occurred: + | + | Traceback (most recent call last): + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/hypothesis/core.py", line 598, in execute_explicit_examples + | execute_example() + | ~~~~~~~~~~~~~~~^^ + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/hypothesis/core.py", line 1150, in execute_once + | result = self.test_runner(data, run) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/hypothesis/core.py", line 824, in default_executor + | return function(data) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/hypothesis/core.py", line 1107, in run + | return test(*args, **kwargs) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/schemathesis/generation/hypothesis/builder.py", line 280, in test_schema_compliance + | def test_wrapper(*args: Any, **kwargs: Any) -> Any: + | ^^^^^^^^^^^^^^^^^^ + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/hypothesis/core.py", line 1004, in test + | return self.test(*args, **kwargs) + | ~~~~~~~~~^^^^^^^^^^^^^^^^^ + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/schemathesis/generation/hypothesis/builder.py", line 282, in test_wrapper + | return test_function(*args, **kwargs) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/schemathesis/pytest/__init__.py", line 47, in test_wrapper + | return func(*args, **kwargs) + | File "/home/anti/Tools/DECNET/tests/api/test_schemathesis.py", line 93, in test_schema_compliance + | start = time.perf_counter() + | ^^^^^^^^^^^^^^^^^^^^^^^^ + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/schemathesis/generation/case.py", line 543, in call_and_validate + | response = self.call(base_url, session, headers, **call_kwargs) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/schemathesis/generation/case.py", line 380, in call + | response = transport_.send( + | self, + | ...<5 lines>... + | **kwargs, + | ) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/schemathesis/transport/requests.py", line 197, in send + | response = _request(**data) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/requests/sessions.py", line 592, in request + | resp = self.send(prep, **send_kwargs) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/requests/sessions.py", line 749, in send + | r.content + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/requests/models.py", line 904, in content + | self._content = b"".join(self.iter_content(CONTENT_CHUNK_SIZE)) or b"" + | ~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/requests/models.py", line 828, in generate + | raise ConnectionError(e) + | requests.exceptions.ConnectionError: HTTPConnectionPool(host='127.0.0.1', port=8008): Read timed out. + | + | Reproduce with: + | + | curl -X GET -H 'Authorization: [Filtered]' 'http://127.0.0.1:8008/api/v1/stream?search=null&search=null' + | Falsifying explicit example: test_schema_compliance( + | case=, + | ) + +---------------- 12 ---------------- + | Traceback (most recent call last): + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/urllib3/response.py", line 903, in _error_catcher + | yield + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/urllib3/response.py", line 1418, in read_chunked + | self._update_chunk_length() + | ~~~~~~~~~~~~~~~~~~~~~~~~~^^ + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/urllib3/response.py", line 1333, in _update_chunk_length + | line = self._fp.fp.readline() # type: ignore[union-attr] + | File "/usr/lib64/python3.14/socket.py", line 725, in readinto + | return self._sock.recv_into(b) + | ~~~~~~~~~~~~~~~~~~~~^^^ + | TimeoutError: timed out + | + | The above exception was the direct cause of the following exception: + | + | Traceback (most recent call last): + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/requests/models.py", line 822, in generate + | yield from self.raw.stream(chunk_size, decode_content=True) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/urllib3/response.py", line 1250, in stream + | yield from self.read_chunked(amt, decode_content=decode_content) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/urllib3/response.py", line 1397, in read_chunked + | with self._error_catcher(): + | ~~~~~~~~~~~~~~~~~~~^^ + | File "/usr/lib64/python3.14/contextlib.py", line 162, in __exit__ + | self.gen.throw(value) + | ~~~~~~~~~~~~~~^^^^^^^ + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/urllib3/response.py", line 908, in _error_catcher + | raise ReadTimeoutError(self._pool, None, "Read timed out.") from e # type: ignore[arg-type] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | urllib3.exceptions.ReadTimeoutError: HTTPConnectionPool(host='127.0.0.1', port=8008): Read timed out. + | + | During handling of the above exception, another exception occurred: + | + | Traceback (most recent call last): + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/hypothesis/core.py", line 598, in execute_explicit_examples + | execute_example() + | ~~~~~~~~~~~~~~~^^ + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/hypothesis/core.py", line 1150, in execute_once + | result = self.test_runner(data, run) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/hypothesis/core.py", line 824, in default_executor + | return function(data) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/hypothesis/core.py", line 1107, in run + | return test(*args, **kwargs) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/schemathesis/generation/hypothesis/builder.py", line 280, in test_schema_compliance + | def test_wrapper(*args: Any, **kwargs: Any) -> Any: + | ^^^^^^^^^^^^^^^^^^ + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/hypothesis/core.py", line 1004, in test + | return self.test(*args, **kwargs) + | ~~~~~~~~~^^^^^^^^^^^^^^^^^ + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/schemathesis/generation/hypothesis/builder.py", line 282, in test_wrapper + | return test_function(*args, **kwargs) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/schemathesis/pytest/__init__.py", line 47, in test_wrapper + | return func(*args, **kwargs) + | File "/home/anti/Tools/DECNET/tests/api/test_schemathesis.py", line 93, in test_schema_compliance + | start = time.perf_counter() + | ^^^^^^^^^^^^^^^^^^^^^^^^ + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/schemathesis/generation/case.py", line 543, in call_and_validate + | response = self.call(base_url, session, headers, **call_kwargs) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/schemathesis/generation/case.py", line 380, in call + | response = transport_.send( + | self, + | ...<5 lines>... + | **kwargs, + | ) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/schemathesis/transport/requests.py", line 197, in send + | response = _request(**data) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/requests/sessions.py", line 592, in request + | resp = self.send(prep, **send_kwargs) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/requests/sessions.py", line 749, in send + | r.content + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/requests/models.py", line 904, in content + | self._content = b"".join(self.iter_content(CONTENT_CHUNK_SIZE)) or b"" + | ~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/requests/models.py", line 828, in generate + | raise ConnectionError(e) + | requests.exceptions.ConnectionError: HTTPConnectionPool(host='127.0.0.1', port=8008): Read timed out. + | + | Reproduce with: + | + | curl -X GET -H 'Authorization: [Filtered]' 'http://127.0.0.1:8008/api/v1/stream?search=0' + | Falsifying explicit example: test_schema_compliance( + | case=, + | ) + +---------------- 13 ---------------- + | Traceback (most recent call last): + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/urllib3/response.py", line 903, in _error_catcher + | yield + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/urllib3/response.py", line 1418, in read_chunked + | self._update_chunk_length() + | ~~~~~~~~~~~~~~~~~~~~~~~~~^^ + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/urllib3/response.py", line 1333, in _update_chunk_length + | line = self._fp.fp.readline() # type: ignore[union-attr] + | File "/usr/lib64/python3.14/socket.py", line 725, in readinto + | return self._sock.recv_into(b) + | ~~~~~~~~~~~~~~~~~~~~^^^ + | TimeoutError: timed out + | + | The above exception was the direct cause of the following exception: + | + | Traceback (most recent call last): + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/requests/models.py", line 822, in generate + | yield from self.raw.stream(chunk_size, decode_content=True) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/urllib3/response.py", line 1250, in stream + | yield from self.read_chunked(amt, decode_content=decode_content) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/urllib3/response.py", line 1397, in read_chunked + | with self._error_catcher(): + | ~~~~~~~~~~~~~~~~~~~^^ + | File "/usr/lib64/python3.14/contextlib.py", line 162, in __exit__ + | self.gen.throw(value) + | ~~~~~~~~~~~~~~^^^^^^^ + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/urllib3/response.py", line 908, in _error_catcher + | raise ReadTimeoutError(self._pool, None, "Read timed out.") from e # type: ignore[arg-type] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | urllib3.exceptions.ReadTimeoutError: HTTPConnectionPool(host='127.0.0.1', port=8008): Read timed out. + | + | During handling of the above exception, another exception occurred: + | + | Traceback (most recent call last): + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/hypothesis/core.py", line 598, in execute_explicit_examples + | execute_example() + | ~~~~~~~~~~~~~~~^^ + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/hypothesis/core.py", line 1150, in execute_once + | result = self.test_runner(data, run) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/hypothesis/core.py", line 824, in default_executor + | return function(data) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/hypothesis/core.py", line 1107, in run + | return test(*args, **kwargs) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/schemathesis/generation/hypothesis/builder.py", line 280, in test_schema_compliance + | def test_wrapper(*args: Any, **kwargs: Any) -> Any: + | ^^^^^^^^^^^^^^^^^^ + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/hypothesis/core.py", line 1004, in test + | return self.test(*args, **kwargs) + | ~~~~~~~~~^^^^^^^^^^^^^^^^^ + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/schemathesis/generation/hypothesis/builder.py", line 282, in test_wrapper + | return test_function(*args, **kwargs) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/schemathesis/pytest/__init__.py", line 47, in test_wrapper + | return func(*args, **kwargs) + | File "/home/anti/Tools/DECNET/tests/api/test_schemathesis.py", line 93, in test_schema_compliance + | start = time.perf_counter() + | ^^^^^^^^^^^^^^^^^^^^^^^^ + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/schemathesis/generation/case.py", line 543, in call_and_validate + | response = self.call(base_url, session, headers, **call_kwargs) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/schemathesis/generation/case.py", line 380, in call + | response = transport_.send( + | self, + | ...<5 lines>... + | **kwargs, + | ) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/schemathesis/transport/requests.py", line 197, in send + | response = _request(**data) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/requests/sessions.py", line 592, in request + | resp = self.send(prep, **send_kwargs) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/requests/sessions.py", line 749, in send + | r.content + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/requests/models.py", line 904, in content + | self._content = b"".join(self.iter_content(CONTENT_CHUNK_SIZE)) or b"" + | ~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/requests/models.py", line 828, in generate + | raise ConnectionError(e) + | requests.exceptions.ConnectionError: HTTPConnectionPool(host='127.0.0.1', port=8008): Read timed out. + | + | Reproduce with: + | + | curl -X GET -H 'Authorization: [Filtered]' 'http://127.0.0.1:8008/api/v1/stream?search=' + | Falsifying explicit example: test_schema_compliance( + | case=, + | ) + +---------------- 14 ---------------- + | Traceback (most recent call last): + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/urllib3/response.py", line 903, in _error_catcher + | yield + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/urllib3/response.py", line 1418, in read_chunked + | self._update_chunk_length() + | ~~~~~~~~~~~~~~~~~~~~~~~~~^^ + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/urllib3/response.py", line 1333, in _update_chunk_length + | line = self._fp.fp.readline() # type: ignore[union-attr] + | File "/usr/lib64/python3.14/socket.py", line 725, in readinto + | return self._sock.recv_into(b) + | ~~~~~~~~~~~~~~~~~~~~^^^ + | TimeoutError: timed out + | + | The above exception was the direct cause of the following exception: + | + | Traceback (most recent call last): + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/requests/models.py", line 822, in generate + | yield from self.raw.stream(chunk_size, decode_content=True) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/urllib3/response.py", line 1250, in stream + | yield from self.read_chunked(amt, decode_content=decode_content) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/urllib3/response.py", line 1397, in read_chunked + | with self._error_catcher(): + | ~~~~~~~~~~~~~~~~~~~^^ + | File "/usr/lib64/python3.14/contextlib.py", line 162, in __exit__ + | self.gen.throw(value) + | ~~~~~~~~~~~~~~^^^^^^^ + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/urllib3/response.py", line 908, in _error_catcher + | raise ReadTimeoutError(self._pool, None, "Read timed out.") from e # type: ignore[arg-type] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | urllib3.exceptions.ReadTimeoutError: HTTPConnectionPool(host='127.0.0.1', port=8008): Read timed out. + | + | During handling of the above exception, another exception occurred: + | + | Traceback (most recent call last): + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/hypothesis/core.py", line 598, in execute_explicit_examples + | execute_example() + | ~~~~~~~~~~~~~~~^^ + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/hypothesis/core.py", line 1150, in execute_once + | result = self.test_runner(data, run) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/hypothesis/core.py", line 824, in default_executor + | return function(data) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/hypothesis/core.py", line 1107, in run + | return test(*args, **kwargs) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/schemathesis/generation/hypothesis/builder.py", line 280, in test_schema_compliance + | def test_wrapper(*args: Any, **kwargs: Any) -> Any: + | ^^^^^^^^^^^^^^^^^^ + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/hypothesis/core.py", line 1004, in test + | return self.test(*args, **kwargs) + | ~~~~~~~~~^^^^^^^^^^^^^^^^^ + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/schemathesis/generation/hypothesis/builder.py", line 282, in test_wrapper + | return test_function(*args, **kwargs) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/schemathesis/pytest/__init__.py", line 47, in test_wrapper + | return func(*args, **kwargs) + | File "/home/anti/Tools/DECNET/tests/api/test_schemathesis.py", line 93, in test_schema_compliance + | start = time.perf_counter() + | ^^^^^^^^^^^^^^^^^^^^^^^^ + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/schemathesis/generation/case.py", line 543, in call_and_validate + | response = self.call(base_url, session, headers, **call_kwargs) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/schemathesis/generation/case.py", line 380, in call + | response = transport_.send( + | self, + | ...<5 lines>... + | **kwargs, + | ) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/schemathesis/transport/requests.py", line 197, in send + | response = _request(**data) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/requests/sessions.py", line 592, in request + | resp = self.send(prep, **send_kwargs) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/requests/sessions.py", line 749, in send + | r.content + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/requests/models.py", line 904, in content + | self._content = b"".join(self.iter_content(CONTENT_CHUNK_SIZE)) or b"" + | ~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/requests/models.py", line 828, in generate + | raise ConnectionError(e) + | requests.exceptions.ConnectionError: HTTPConnectionPool(host='127.0.0.1', port=8008): Read timed out. + | + | Reproduce with: + | + | curl -X GET -H 'Authorization: [Filtered]' 'http://127.0.0.1:8008/api/v1/stream?lastEventId=0&x-schemathesis-unknown-property=42' + | Falsifying explicit example: test_schema_compliance( + | case=, + | ) + +---------------- 15 ---------------- + | Traceback (most recent call last): + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/urllib3/response.py", line 903, in _error_catcher + | yield + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/urllib3/response.py", line 1418, in read_chunked + | self._update_chunk_length() + | ~~~~~~~~~~~~~~~~~~~~~~~~~^^ + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/urllib3/response.py", line 1333, in _update_chunk_length + | line = self._fp.fp.readline() # type: ignore[union-attr] + | File "/usr/lib64/python3.14/socket.py", line 725, in readinto + | return self._sock.recv_into(b) + | ~~~~~~~~~~~~~~~~~~~~^^^ + | TimeoutError: timed out + | + | The above exception was the direct cause of the following exception: + | + | Traceback (most recent call last): + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/requests/models.py", line 822, in generate + | yield from self.raw.stream(chunk_size, decode_content=True) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/urllib3/response.py", line 1250, in stream + | yield from self.read_chunked(amt, decode_content=decode_content) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/urllib3/response.py", line 1397, in read_chunked + | with self._error_catcher(): + | ~~~~~~~~~~~~~~~~~~~^^ + | File "/usr/lib64/python3.14/contextlib.py", line 162, in __exit__ + | self.gen.throw(value) + | ~~~~~~~~~~~~~~^^^^^^^ + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/urllib3/response.py", line 908, in _error_catcher + | raise ReadTimeoutError(self._pool, None, "Read timed out.") from e # type: ignore[arg-type] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | urllib3.exceptions.ReadTimeoutError: HTTPConnectionPool(host='127.0.0.1', port=8008): Read timed out. + | + | During handling of the above exception, another exception occurred: + | + | Traceback (most recent call last): + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/hypothesis/core.py", line 598, in execute_explicit_examples + | execute_example() + | ~~~~~~~~~~~~~~~^^ + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/hypothesis/core.py", line 1150, in execute_once + | result = self.test_runner(data, run) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/hypothesis/core.py", line 824, in default_executor + | return function(data) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/hypothesis/core.py", line 1107, in run + | return test(*args, **kwargs) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/schemathesis/generation/hypothesis/builder.py", line 280, in test_schema_compliance + | def test_wrapper(*args: Any, **kwargs: Any) -> Any: + | ^^^^^^^^^^^^^^^^^^ + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/hypothesis/core.py", line 1004, in test + | return self.test(*args, **kwargs) + | ~~~~~~~~~^^^^^^^^^^^^^^^^^ + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/schemathesis/generation/hypothesis/builder.py", line 282, in test_wrapper + | return test_function(*args, **kwargs) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/schemathesis/pytest/__init__.py", line 47, in test_wrapper + | return func(*args, **kwargs) + | File "/home/anti/Tools/DECNET/tests/api/test_schemathesis.py", line 93, in test_schema_compliance + | start = time.perf_counter() + | ^^^^^^^^^^^^^^^^^^^^^^^^ + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/schemathesis/generation/case.py", line 543, in call_and_validate + | response = self.call(base_url, session, headers, **call_kwargs) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/schemathesis/generation/case.py", line 380, in call + | response = transport_.send( + | self, + | ...<5 lines>... + | **kwargs, + | ) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/schemathesis/transport/requests.py", line 197, in send + | response = _request(**data) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/requests/sessions.py", line 592, in request + | resp = self.send(prep, **send_kwargs) + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/requests/sessions.py", line 749, in send + | r.content + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/requests/models.py", line 904, in content + | self._content = b"".join(self.iter_content(CONTENT_CHUNK_SIZE)) or b"" + | ~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | File "/home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/requests/models.py", line 828, in generate + | raise ConnectionError(e) + | requests.exceptions.ConnectionError: HTTPConnectionPool(host='127.0.0.1', port=8008): Read timed out. + | + | Reproduce with: + | + | curl -X GET -H 'Authorization: [Filtered]' 'http://127.0.0.1:8008/api/v1/stream?lastEventId=0' + | Falsifying explicit example: test_schema_compliance( + | case=, + | ) + +---------------- ... ---------------- + | and 5 more exceptions + +------------------------------------ +------------------------------ Captured log setup ------------------------------ +WARNING asyncio:base_events.py:2041 Executing ._asyncgen_fixture_wrapper..setup() running at /home/anti/Tools/DECNET/.venv/lib64/python3.14/site-packages/pytest_asyncio/plugin.py:309> wait_for= cb=[_run_until_complete_cb() at /usr/lib64/python3.14/asyncio/base_events.py:181] created at /usr/lib64/python3.14/asyncio/runners.py:109> took 0.165 seconds +=========================== short test summary info ============================ +FAILED tests/api/test_schemathesis.py::test_schema_compliance[api][POST /api/v1/deckies/deploy] +FAILED tests/api/test_schemathesis.py::test_schema_compliance[api][PUT /api/v1/deckies/{decky_name}/mutate-interval] +FAILED tests/api/test_schemathesis.py::test_schema_compliance[api][GET /api/v1/logs] +FAILED tests/api/test_schemathesis.py::test_schema_compliance[api][GET /api/v1/bounty] +FAILED tests/api/test_schemathesis.py::test_schema_compliance[api][GET /api/v1/stream] +!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 5 failures !!!!!!!!!!!!!!!!!!!!!!!!!!! +!!!!!!!!!!!! xdist.dsession.Interrupted: stopping after 1 failures !!!!!!!!!!!!! +=================== 5 failed, 6 passed in 100.69s (0:01:40) ==================== diff --git a/schemathesis.toml b/schemathesis.toml new file mode 100644 index 0000000..e1f5852 --- /dev/null +++ b/schemathesis.toml @@ -0,0 +1,6 @@ +request-timeout = 5.0 + +[[operations]] +# Target your SSE endpoint specifically +include-path = "/stream" +request-timeout = 2.0 diff --git a/templates/ftp/server.py b/templates/ftp/server.py index 5c69c14..94820a6 100644 --- a/templates/ftp/server.py +++ b/templates/ftp/server.py @@ -29,12 +29,12 @@ def _log(event_type: str, severity: int = 6, **kwargs) -> None: def _setup_bait_fs() -> str: bait_dir = Path("/tmp/ftp_bait") bait_dir.mkdir(parents=True, exist_ok=True) - + (bait_dir / "backup.tar.gz").write_bytes(b"\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\x03\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00") (bait_dir / "db_dump.sql").write_text("CREATE TABLE users (id INT, username VARCHAR(50), password VARCHAR(50));\nINSERT INTO users VALUES (1, 'admin', 'pbkdf2:sha256:5000$...');\n") (bait_dir / "config.ini").write_text("[database]\nuser = dbadmin\npassword = db_super_admin_pass_!\nhost = localhost\n") (bait_dir / "credentials.txt").write_text("admin:super_secret_admin_pw\nroot:toor\nalice:wonderland\n") - + return str(bait_dir) class ServerFTP(FTP): diff --git a/templates/mqtt/server.py b/templates/mqtt/server.py index a73089d..d0b43c1 100644 --- a/templates/mqtt/server.py +++ b/templates/mqtt/server.py @@ -97,7 +97,7 @@ def _publish(topic: str, value: str, retain: bool = True) -> bytes: payload = str(value).encode() fixed = 0x31 if retain else 0x30 remaining = len(topic_len) + len(topic_bytes) + len(payload) - + # variable length encoding rem_bytes = [] while remaining > 0: @@ -108,7 +108,7 @@ def _publish(topic: str, value: str, retain: bool = True) -> bytes: rem_bytes.append(encoded) if not rem_bytes: rem_bytes = [0] - + return bytes([fixed]) + bytes(rem_bytes) + topic_len + topic_bytes + payload @@ -132,7 +132,7 @@ def _generate_topics() -> dict: return topics except Exception as e: _log("config_error", severity=4, error=str(e)) - + if MQTT_PERSONA == "water_plant": topics.update({ "plant/water/tank1/level": f"{random.uniform(60.0, 80.0):.1f}", @@ -186,7 +186,7 @@ class MQTTProtocol(asyncio.Protocol): pkt_type = (pkt_byte >> 4) & 0x0f flags = pkt_byte & 0x0f qos = (flags >> 1) & 0x03 - + # Decode remaining length (variable-length encoding) pos = 1 remaining = 0 @@ -225,7 +225,7 @@ class MQTTProtocol(asyncio.Protocol): packet_id, subs = _parse_subscribe(payload) granted_qos = [1] * len(subs) # grant QoS 1 for all self._transport.write(_suback(packet_id, granted_qos)) - + # Immediately send retained publishes matching topics for sub_topic, _ in subs: _log("subscribe", src=self._peer[0], topics=[sub_topic]) @@ -245,11 +245,11 @@ class MQTTProtocol(asyncio.Protocol): topic, packet_id, data = _parse_publish(payload, qos) # Attacker command received! _log("publish", src=self._peer[0], topic=topic, payload=data.decode(errors="replace")) - + if qos == 1: puback = bytes([0x40, 0x02]) + struct.pack(">H", packet_id) self._transport.write(puback) - + elif pkt_type == 12: # PINGREQ self._transport.write(b"\xd0\x00") # PINGRESP elif pkt_type == 14: # DISCONNECT diff --git a/templates/redis/server.py b/templates/redis/server.py index 9251ce9..4aa5961 100644 --- a/templates/redis/server.py +++ b/templates/redis/server.py @@ -156,7 +156,7 @@ class RedisProtocol(asyncio.Protocol): elif pattern != '*': pat = pattern.encode() keys = [k for k in keys if k == pat] - + resp = f"*{len(keys)}\r\n".encode() + b"".join(_bulk(k.decode()) for k in keys) self._transport.write(resp) elif verb == "GET": diff --git a/templates/smtp/server.py b/templates/smtp/server.py index 89e40ff..b5b2232 100644 --- a/templates/smtp/server.py +++ b/templates/smtp/server.py @@ -45,7 +45,7 @@ def _log(event_type: str, severity: int = 6, **kwargs) -> None: def _rand_msg_id() -> str: """Return a Postfix-style 12-char alphanumeric queue ID.""" chars = string.ascii_uppercase + string.digits - return "".join(random.choices(chars, k=12)) # noqa: S311 + return "".join(random.choices(chars, k=12)) def _decode_auth_plain(blob: str) -> tuple[str, str]: diff --git a/templates/snmp/server.py b/templates/snmp/server.py index 2fd88d4..34bb7bd 100644 --- a/templates/snmp/server.py +++ b/templates/snmp/server.py @@ -153,11 +153,11 @@ def _parse_snmp(data: bytes): # PDU type (0xa0 = GetRequest, 0xa1 = GetNextRequest) if pos >= len(data): raise ValueError("Missing PDU type") - + pdu_type = data[pos] if pdu_type not in (0xa0, 0xa1): raise ValueError(f"Invalid PDU type {pdu_type}") - + pos += 1 _, pos = _read_ber_length(data, pos) # request-id diff --git a/templates/ssh/decnet_logging.py b/templates/ssh/decnet_logging.py index ff05fd8..c935cf9 100644 --- a/templates/ssh/decnet_logging.py +++ b/templates/ssh/decnet_logging.py @@ -155,13 +155,13 @@ def write_syslog_file(line: str) -> None: """Append a syslog line to the rotating log file.""" try: _get_file_logger().info(line) - + # Also parse and write JSON log import json import re from datetime import datetime - from typing import Optional, Any - + from typing import Optional + _RFC5424_RE: re.Pattern = re.compile( r"^<\d+>1 " r"(\S+) " # 1: TIMESTAMP @@ -174,7 +174,7 @@ def write_syslog_file(line: str) -> None: _SD_BLOCK_RE: re.Pattern = re.compile(r'\[decnet@55555\s+(.*?)\]', re.DOTALL) _PARAM_RE: re.Pattern = re.compile(r'(\w+)="((?:[^"\\]|\\.)*)"') _IP_FIELDS: tuple[str, ...] = ("src_ip", "src", "client_ip", "remote_ip", "ip") - + _m: Optional[re.Match] = _RFC5424_RE.match(line) if _m: _ts_raw: str @@ -183,10 +183,10 @@ def write_syslog_file(line: str) -> None: _event_type: str _sd_rest: str _ts_raw, _decky, _service, _event_type, _sd_rest = _m.groups() - + _fields: dict[str, str] = {} _msg: str = "" - + if _sd_rest.startswith("-"): _msg = _sd_rest[1:].lstrip() elif _sd_rest.startswith("["): @@ -194,27 +194,27 @@ def write_syslog_file(line: str) -> None: if _block: for _k, _v in _PARAM_RE.findall(_block.group(1)): _fields[_k] = _v.replace('\\"', '"').replace("\\\\", "\\").replace("\\]", "]") - + # extract msg after the block _msg_match: Optional[re.Match] = re.search(r'\]\s+(.+)$', _sd_rest) if _msg_match: _msg = _msg_match.group(1).strip() else: _msg = _sd_rest - + _attacker_ip: str = "Unknown" for _fname in _IP_FIELDS: if _fname in _fields: _attacker_ip = _fields[_fname] break - + # Parse timestamp to normalize it _ts_formatted: str try: _ts_formatted = datetime.fromisoformat(_ts_raw).strftime("%Y-%m-%d %H:%M:%S") except ValueError: _ts_formatted = _ts_raw - + _payload: dict[str, Any] = { "timestamp": _ts_formatted, "decky": _decky, @@ -226,7 +226,7 @@ def write_syslog_file(line: str) -> None: "raw_line": line } _get_json_logger().info(json.dumps(_payload)) - + except Exception: pass diff --git a/tests/api/fleet/test_mutate_interval.py b/tests/api/fleet/test_mutate_interval.py index 9cc85f2..34f54c4 100644 --- a/tests/api/fleet/test_mutate_interval.py +++ b/tests/api/fleet/test_mutate_interval.py @@ -1,11 +1,9 @@ """ Tests for the mutate interval API endpoint. """ - import pytest import httpx -from unittest.mock import patch -from pathlib import Path +from unittest.mock import patch, AsyncMock from decnet.config import DeckyConfig, DecnetConfig @@ -37,19 +35,20 @@ class TestMutateInterval: @pytest.mark.asyncio async def test_no_active_deployment(self, client: httpx.AsyncClient, auth_token: str): - with patch("decnet.web.router.fleet.api_mutate_interval.load_state", return_value=None): + with patch("decnet.web.router.fleet.api_mutate_interval.repo", new_callable=AsyncMock) as mock_repo: + mock_repo.get_state.return_value = None resp = await client.put( "/api/v1/deckies/decky-01/mutate-interval", headers={"Authorization": f"Bearer {auth_token}"}, json={"mutate_interval": 60}, ) - assert resp.status_code == 500 + assert resp.status_code == 400 @pytest.mark.asyncio async def test_decky_not_found(self, client: httpx.AsyncClient, auth_token: str): config = _config() - with patch("decnet.web.router.fleet.api_mutate_interval.load_state", - return_value=(config, Path("test.yml"))): + with patch("decnet.web.router.fleet.api_mutate_interval.repo", new_callable=AsyncMock) as mock_repo: + mock_repo.get_state.return_value = {"config": config.model_dump(), "compose_path": "c.yml"} resp = await client.put( "/api/v1/deckies/nonexistent/mutate-interval", headers={"Authorization": f"Bearer {auth_token}"}, @@ -60,30 +59,26 @@ class TestMutateInterval: @pytest.mark.asyncio async def test_successful_interval_update(self, client: httpx.AsyncClient, auth_token: str): config = _config() - with patch("decnet.web.router.fleet.api_mutate_interval.load_state", - return_value=(config, Path("test.yml"))): - with patch("decnet.web.router.fleet.api_mutate_interval.save_state") as mock_save: - resp = await client.put( - "/api/v1/deckies/decky-01/mutate-interval", - headers={"Authorization": f"Bearer {auth_token}"}, - json={"mutate_interval": 120}, - ) + with patch("decnet.web.router.fleet.api_mutate_interval.repo", new_callable=AsyncMock) as mock_repo: + mock_repo.get_state.return_value = {"config": config.model_dump(), "compose_path": "c.yml"} + resp = await client.put( + "/api/v1/deckies/decky-01/mutate-interval", + headers={"Authorization": f"Bearer {auth_token}"}, + json={"mutate_interval": 120}, + ) assert resp.status_code == 200 assert resp.json()["message"] == "Mutation interval updated" - mock_save.assert_called_once() - # Verify the interval was actually updated on the decky config - assert config.deckies[0].mutate_interval == 120 + mock_repo.set_state.assert_awaited_once() @pytest.mark.asyncio async def test_null_interval_removes_mutation(self, client: httpx.AsyncClient, auth_token: str): config = _config() - with patch("decnet.web.router.fleet.api_mutate_interval.load_state", - return_value=(config, Path("test.yml"))): - with patch("decnet.web.router.fleet.api_mutate_interval.save_state"): - resp = await client.put( - "/api/v1/deckies/decky-01/mutate-interval", - headers={"Authorization": f"Bearer {auth_token}"}, - json={"mutate_interval": None}, - ) + with patch("decnet.web.router.fleet.api_mutate_interval.repo", new_callable=AsyncMock) as mock_repo: + mock_repo.get_state.return_value = {"config": config.model_dump(), "compose_path": "c.yml"} + resp = await client.put( + "/api/v1/deckies/decky-01/mutate-interval", + headers={"Authorization": f"Bearer {auth_token}"}, + json={"mutate_interval": None}, + ) assert resp.status_code == 200 - assert config.deckies[0].mutate_interval is None + mock_repo.set_state.assert_awaited_once() diff --git a/tests/api/logs/test_get_logs.py b/tests/api/logs/test_get_logs.py index 05cc677..6531f8f 100644 --- a/tests/api/logs/test_get_logs.py +++ b/tests/api/logs/test_get_logs.py @@ -33,11 +33,11 @@ async def test_fuzz_get_logs(client: httpx.AsyncClient, auth_token: str, limit: _params: dict[str, Any] = {"limit": limit, "offset": offset} if search is not None: _params["search"] = search - + _response: httpx.Response = await client.get( "/api/v1/logs", params=_params, headers={"Authorization": f"Bearer {auth_token}"} ) - + assert _response.status_code in (200, 422) diff --git a/tests/api/logs/test_histogram.py b/tests/api/logs/test_histogram.py index 6bae2d6..0f999e7 100644 --- a/tests/api/logs/test_histogram.py +++ b/tests/api/logs/test_histogram.py @@ -9,13 +9,13 @@ import pytest from datetime import datetime, timedelta from freezegun import freeze_time from hypothesis import given, settings, strategies as st -from decnet.web.db.sqlite.repository import SQLiteRepository +from decnet.web.db.factory import get_repository from ..conftest import _FUZZ_SETTINGS @pytest.fixture def repo(tmp_path): - return SQLiteRepository(db_path=str(tmp_path / "histogram_test.db")) + return get_repository(db_path=str(tmp_path / "histogram_test.db")) def _log(decky="d", service="ssh", ip="1.2.3.4", timestamp=None): diff --git a/tests/api/stream/test_stream_events.py b/tests/api/stream/test_stream_events.py index 493c71b..60c213a 100644 --- a/tests/api/stream/test_stream_events.py +++ b/tests/api/stream/test_stream_events.py @@ -21,7 +21,7 @@ class TestStreamEvents: # We force the generator to exit immediately by making the first awaitable raise with patch("decnet.web.router.stream.api_stream_events.repo") as mock_repo: mock_repo.get_max_log_id = AsyncMock(side_effect=StopAsyncIteration) - + # This will hit the 'except Exception' or just exit the generator resp = await client.get( "/api/v1/stream", diff --git a/tests/api/test_repository.py b/tests/api/test_repository.py index 3b0d31b..69ae689 100644 --- a/tests/api/test_repository.py +++ b/tests/api/test_repository.py @@ -1,18 +1,17 @@ """ -Direct async tests for SQLiteRepository. -These exercise the DB layer without going through the HTTP stack, -covering DEBT-006 (zero test coverage on the database layer). +Direct async tests for the configured Repository implementation. +These exercise the DB layer without going through the HTTP stack. """ import json import pytest from hypothesis import given, settings, strategies as st -from decnet.web.db.sqlite.repository import SQLiteRepository +from decnet.web.db.factory import get_repository from .conftest import _FUZZ_SETTINGS @pytest.fixture def repo(tmp_path): - return SQLiteRepository(db_path=str(tmp_path / "test.db")) + return get_repository(db_path=str(tmp_path / "test.db")) @pytest.mark.anyio diff --git a/tests/api/test_schemathesis.py b/tests/api/test_schemathesis.py index 4cab4ee..844c06d 100644 --- a/tests/api/test_schemathesis.py +++ b/tests/api/test_schemathesis.py @@ -11,16 +11,82 @@ replace the checks list with the default (remove the argument) for full complian Requires DECNET_DEVELOPER=true (set in tests/conftest.py) to expose /openapi.json. """ import pytest -import schemathesis -from hypothesis import settings -from schemathesis.checks import not_a_server_error -from decnet.web.api import app +import schemathesis as st +from hypothesis import settings, Verbosity +from decnet.web.auth import create_access_token -schema = schemathesis.openapi.from_asgi("/openapi.json", app) +import subprocess +import socket +import sys +import atexit +import os +import time +from pathlib import Path +# Configuration for the automated live server +LIVE_PORT = 8008 +LIVE_SERVER_URL = f"http://127.0.0.1:{LIVE_PORT}" +TEST_SECRET = "test-secret-for-automated-fuzzing" + +# Standardize the secret for the test process too so tokens can be verified +import decnet.web.auth +decnet.web.auth.SECRET_KEY = TEST_SECRET + +# Create a valid token for an admin-like user +TEST_TOKEN = create_access_token({"uuid": "00000000-0000-0000-0000-000000000001"}) + +@st.hook +def before_call(context, case, *args): + # Logged-in admin for all requests + case.headers = case.headers or {} + case.headers["Authorization"] = f"Bearer {TEST_TOKEN}" + +def wait_for_port(port, timeout=10): + start_time = time.time() + while time.time() - start_time < timeout: + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: + if sock.connect_ex(('127.0.0.1', port)) == 0: + return True + time.sleep(0.2) + return False + +def start_automated_server(): + # Use the current venv's uvicorn + uvicorn_bin = "uvicorn" if os.name != "nt" else "uvicorn.exe" + uvicorn_path = str(Path(sys.executable).parent / uvicorn_bin) + + # Force developer and contract test modes for the sub-process + env = os.environ.copy() + env["DECNET_DEVELOPER"] = "true" + env["DECNET_CONTRACT_TEST"] = "true" + env["DECNET_JWT_SECRET"] = TEST_SECRET + + proc = subprocess.Popen( + [uvicorn_path, "decnet.web.api:app", "--host", "127.0.0.1", "--port", str(LIVE_PORT), "--log-level", "error"], + env=env, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL + ) + + # Register cleanup + atexit.register(proc.terminate) + + if not wait_for_port(LIVE_PORT): + proc.terminate() + raise RuntimeError(f"Automated server failed to start on port {LIVE_PORT}") + + return proc + +# Stir up the server! +_server_proc = start_automated_server() + +# Now Schemathesis can pull the schema from the real network port +schema = st.openapi.from_url(f"{LIVE_SERVER_URL}/openapi.json") @pytest.mark.fuzz -@schemathesis.pytest.parametrize(api=schema) -@settings(max_examples=5, deadline=None) +@st.pytest.parametrize(api=schema) +@settings(max_examples=3000, deadline=None, verbosity=Verbosity.debug) def test_schema_compliance(case): - case.call_and_validate(checks=[not_a_server_error]) + #print(f"\n[Fuzzing] {case.method} {case.path} with query={case.query}") + case.call_and_validate() + #print(f" └─ Success") diff --git a/tests/conftest.py b/tests/conftest.py index 3e54f5a..b0051e5 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -6,6 +6,15 @@ any test file imports decnet.* — pytest loads conftest.py first. """ import os -os.environ.setdefault("DECNET_JWT_SECRET", "test-jwt-secret-not-for-production-use") -# Expose OpenAPI schema so schemathesis can load it during tests -os.environ.setdefault("DECNET_DEVELOPER", "true") +os.environ["DECNET_JWT_SECRET"] = "stable-test-secret-key-at-least-32-chars-long" +os.environ["DECNET_ADMIN_PASSWORD"] = "test-password-123" +os.environ["DECNET_DEVELOPER"] = "true" +os.environ["DECNET_DB_TYPE"] = "sqlite" + +import pytest +from typing import Any + +@pytest.fixture(autouse=True) +def standardize_auth_secret(monkeypatch: Any) -> None: + import decnet.web.auth + monkeypatch.setattr(decnet.web.auth, "SECRET_KEY", os.environ["DECNET_JWT_SECRET"]) diff --git a/tests/service_testing/test_mqtt.py b/tests/service_testing/test_mqtt.py index 0c856c1..751aea6 100644 --- a/tests/service_testing/test_mqtt.py +++ b/tests/service_testing/test_mqtt.py @@ -91,7 +91,7 @@ def _publish_packet(topic: str, payload: str, qos: int = 1, pid: int = 1) -> byt packet_payload = len(topic_bytes).to_bytes(2, "big") + topic_bytes + pid.to_bytes(2, "big") + payload_bytes else: packet_payload = len(topic_bytes).to_bytes(2, "big") + topic_bytes + payload_bytes - + return bytes([byte0, len(packet_payload)]) + packet_payload def _pingreq_packet() -> bytes: @@ -128,10 +128,10 @@ def test_subscribe_wildcard_retained(mqtt_mod): written.clear() _send(proto, _subscribe_packet("plant/#")) - + assert len(written) >= 2 # At least SUBACK + some publishes assert written[0].startswith(b"\x90") # SUBACK - + combined = b"".join(written[1:]) # Should contain some water plant topics assert b"plant/water/tank1/level" in combined diff --git a/tests/service_testing/test_snmp.py b/tests/service_testing/test_snmp.py index 3bbe768..0694739 100644 --- a/tests/service_testing/test_snmp.py +++ b/tests/service_testing/test_snmp.py @@ -50,10 +50,10 @@ def _make_protocol(mod): proto = mod.SNMPProtocol() transport = MagicMock() sent: list[tuple] = [] - + def sendto(data, addr): sent.append((data, addr)) - + transport.sendto = sendto proto.connection_made(transport) sent.clear() @@ -104,11 +104,11 @@ def test_sysdescr_default(snmp_default): proto, transport, sent = _make_protocol(snmp_default) packet = _get_request_packet("public", 1, SYS_DESCR_OID_ENC) _send(proto, packet) - + assert len(sent) == 1 resp, addr = sent[0] assert addr == ("127.0.0.1", 12345) - + # default sysDescr has "Ubuntu SMP" in it assert b"Ubuntu SMP" in resp @@ -116,10 +116,10 @@ def test_sysdescr_water_plant(snmp_water_plant): proto, transport, sent = _make_protocol(snmp_water_plant) packet = _get_request_packet("public", 2, SYS_DESCR_OID_ENC) _send(proto, packet) - + assert len(sent) == 1 resp, _ = sent[0] - + assert b"Debian" in resp # ── Negative Tests ──────────────────────────────────────────────────────────── diff --git a/tests/test_base_repo.py b/tests/test_base_repo.py index d0efc78..efa7787 100644 --- a/tests/test_base_repo.py +++ b/tests/test_base_repo.py @@ -19,6 +19,8 @@ class DummyRepo(BaseRepository): async def add_bounty(self, d): await super().add_bounty(d) async def get_bounties(self, **kw): await super().get_bounties(**kw) async def get_total_bounties(self, **kw): await super().get_total_bounties(**kw) + async def get_state(self, k): await super().get_state(k) + async def set_state(self, k, v): await super().set_state(k, v) @pytest.mark.asyncio async def test_base_repo_coverage(): @@ -37,3 +39,5 @@ async def test_base_repo_coverage(): await dr.add_bounty({}) await dr.get_bounties() await dr.get_total_bounties() + await dr.get_state("k") + await dr.set_state("k", "v") diff --git a/tests/test_collector.py b/tests/test_collector.py index c475891..d43f2e3 100644 --- a/tests/test_collector.py +++ b/tests/test_collector.py @@ -7,7 +7,7 @@ from types import SimpleNamespace from unittest.mock import patch, MagicMock from decnet.collector import parse_rfc5424, is_service_container, is_service_event from decnet.collector.worker import ( - _stream_container, + _stream_container, _load_service_container_names, log_collector_worker ) @@ -291,13 +291,13 @@ class TestLogCollectorWorker: @pytest.mark.asyncio async def test_worker_initial_discovery(self, tmp_path): log_file = str(tmp_path / "decnet.log") - + mock_container = MagicMock() mock_container.id = "c1" mock_container.name = "/s-1" # Mock labels to satisfy is_service_container mock_container.labels = {"com.docker.compose.project": "decnet"} - + mock_client = MagicMock() mock_client.containers.list.return_value = [mock_container] # Make events return an empty generator/iterator immediately @@ -310,17 +310,17 @@ class TestLogCollectorWorker: await asyncio.wait_for(log_collector_worker(log_file), timeout=0.1) except (asyncio.TimeoutError, StopIteration): pass - + # Should have tried to list and watch events mock_client.containers.list.assert_called_once() @pytest.mark.asyncio async def test_worker_handles_events(self, tmp_path): log_file = str(tmp_path / "decnet.log") - + mock_client = MagicMock() mock_client.containers.list.return_value = [] - + event = { "id": "c2", "Actor": {"Attributes": {"name": "s-2", "com.docker.compose.project": "decnet"}} @@ -333,7 +333,7 @@ class TestLogCollectorWorker: await asyncio.wait_for(log_collector_worker(log_file), timeout=0.1) except (asyncio.TimeoutError, StopIteration): pass - + mock_client.events.assert_called_once() @pytest.mark.asyncio @@ -341,7 +341,7 @@ class TestLogCollectorWorker: log_file = str(tmp_path / "decnet.log") mock_client = MagicMock() mock_client.containers.list.side_effect = Exception("Docker down") - + with patch("docker.from_env", return_value=mock_client): # Should not raise await log_collector_worker(log_file) diff --git a/tests/test_mutator.py b/tests/test_mutator.py index 9872883..f45c758 100644 --- a/tests/test_mutator.py +++ b/tests/test_mutator.py @@ -2,10 +2,9 @@ Tests for decnet.mutator — mutation engine, retry logic, due-time scheduling. All subprocess and state I/O is mocked; no Docker or filesystem access. """ -import subprocess import time from pathlib import Path -from unittest.mock import MagicMock, patch +from unittest.mock import MagicMock, patch, AsyncMock import pytest @@ -41,9 +40,131 @@ def _make_config(deckies=None, mutate_interval=30): mutate_interval=mutate_interval, ) +@pytest.fixture +def mock_repo(): + repo = AsyncMock() + repo.get_state.return_value = None + return repo + # --------------------------------------------------------------------------- -# _compose_with_retry +# mutate_decky +# --------------------------------------------------------------------------- + +@pytest.mark.asyncio +class TestMutateDecky: + def _patch_io(self): + """Return a context manager that mocks all other I/O in mutate_decky.""" + return ( + patch("decnet.mutator.engine.write_compose"), + patch("decnet.mutator.engine._compose_with_retry", new_callable=AsyncMock), + ) + + async def test_returns_false_when_no_state(self, mock_repo): + mock_repo.get_state.return_value = None + assert await mutate_decky("decky-01", repo=mock_repo) is False + + async def test_returns_false_when_decky_not_found(self, mock_repo): + cfg = _make_config() + mock_repo.get_state.return_value = {"config": cfg.model_dump(), "compose_path": "c.yml"} + assert await mutate_decky("nonexistent", repo=mock_repo) is False + + async def test_returns_true_on_success(self, mock_repo): + cfg = _make_config() + mock_repo.get_state.return_value = {"config": cfg.model_dump(), "compose_path": "c.yml"} + with patch("decnet.mutator.engine.write_compose"), \ + patch("anyio.to_thread.run_sync", new_callable=AsyncMock): + assert await mutate_decky("decky-01", repo=mock_repo) is True + + async def test_saves_state_after_mutation(self, mock_repo): + cfg = _make_config() + mock_repo.get_state.return_value = {"config": cfg.model_dump(), "compose_path": "c.yml"} + with patch("decnet.mutator.engine.write_compose"), \ + patch("anyio.to_thread.run_sync", new_callable=AsyncMock): + await mutate_decky("decky-01", repo=mock_repo) + mock_repo.set_state.assert_awaited_once() + + async def test_regenerates_compose_after_mutation(self, mock_repo): + cfg = _make_config() + mock_repo.get_state.return_value = {"config": cfg.model_dump(), "compose_path": "c.yml"} + with patch("decnet.mutator.engine.write_compose") as mock_compose, \ + patch("anyio.to_thread.run_sync", new_callable=AsyncMock): + await mutate_decky("decky-01", repo=mock_repo) + mock_compose.assert_called_once() + + async def test_returns_false_on_compose_failure(self, mock_repo): + cfg = _make_config() + mock_repo.get_state.return_value = {"config": cfg.model_dump(), "compose_path": "c.yml"} + with patch("decnet.mutator.engine.write_compose"), \ + patch("anyio.to_thread.run_sync", side_effect=Exception("docker fail")): + assert await mutate_decky("decky-01", repo=mock_repo) is False + + async def test_mutation_changes_services(self, mock_repo): + cfg = _make_config(deckies=[_make_decky(services=["ssh"])]) + mock_repo.get_state.return_value = {"config": cfg.model_dump(), "compose_path": "c.yml"} + with patch("decnet.mutator.engine.write_compose"), \ + patch("anyio.to_thread.run_sync", new_callable=AsyncMock): + await mutate_decky("decky-01", repo=mock_repo) + + # Check that set_state was called with a config where services might have changed + call_args = mock_repo.set_state.await_args[0] + new_config_dict = call_args[1]["config"] + new_services = new_config_dict["deckies"][0]["services"] + assert isinstance(new_services, list) + assert len(new_services) >= 1 + + async def test_updates_last_mutated_timestamp(self, mock_repo): + cfg = _make_config(deckies=[_make_decky(last_mutated=0.0)]) + mock_repo.get_state.return_value = {"config": cfg.model_dump(), "compose_path": "c.yml"} + before = time.time() + with patch("decnet.mutator.engine.write_compose"), \ + patch("anyio.to_thread.run_sync", new_callable=AsyncMock): + await mutate_decky("decky-01", repo=mock_repo) + + call_args = mock_repo.set_state.await_args[0] + new_last_mutated = call_args[1]["config"]["deckies"][0]["last_mutated"] + assert new_last_mutated >= before + +# --------------------------------------------------------------------------- +# mutate_all +# --------------------------------------------------------------------------- + +@pytest.mark.asyncio +class TestMutateAll: + async def test_no_state_returns_early(self, mock_repo): + mock_repo.get_state.return_value = None + with patch("decnet.mutator.engine.mutate_decky") as mock_mutate: + await mutate_all(repo=mock_repo) + mock_mutate.assert_not_called() + + async def test_force_mutates_all_deckies(self, mock_repo): + cfg = _make_config(deckies=[_make_decky("d1"), _make_decky("d2")]) + mock_repo.get_state.return_value = {"config": cfg.model_dump(), "compose_path": "c.yml"} + with patch("decnet.mutator.engine.mutate_decky", new_callable=AsyncMock, return_value=True) as mock_mutate: + await mutate_all(repo=mock_repo, force=True) + assert mock_mutate.call_count == 2 + + async def test_skips_decky_not_yet_due(self, mock_repo): + # last_mutated = now, interval = 30 min → not due + now = time.time() + cfg = _make_config(deckies=[_make_decky(mutate_interval=30, last_mutated=now)]) + mock_repo.get_state.return_value = {"config": cfg.model_dump(), "compose_path": "c.yml"} + with patch("decnet.mutator.engine.mutate_decky") as mock_mutate: + await mutate_all(repo=mock_repo, force=False) + mock_mutate.assert_not_called() + + async def test_mutates_decky_that_is_due(self, mock_repo): + # last_mutated = 2 hours ago, interval = 30 min → due + old_ts = time.time() - 7200 + cfg = _make_config(deckies=[_make_decky(mutate_interval=30, last_mutated=old_ts)]) + mock_repo.get_state.return_value = {"config": cfg.model_dump(), "compose_path": "c.yml"} + with patch("decnet.mutator.engine.mutate_decky", new_callable=AsyncMock, return_value=True) as mock_mutate: + await mutate_all(repo=mock_repo, force=False) + mock_mutate.assert_called_once() + + +# --------------------------------------------------------------------------- +# _compose_with_retry (Sync tests, keep as is or minimal update) # --------------------------------------------------------------------------- class TestComposeWithRetry: @@ -60,149 +181,3 @@ class TestComposeWithRetry: patch("decnet.engine.deployer.time.sleep"): _compose_with_retry("up", "-d", compose_file=Path("compose.yml"), retries=3) assert mock_run.call_count == 2 - - def test_raises_after_all_retries_exhausted(self): - fail = MagicMock(returncode=1, stdout="", stderr="hard error") - with patch("decnet.engine.deployer.subprocess.run", return_value=fail), \ - patch("decnet.engine.deployer.time.sleep"): - with pytest.raises(subprocess.CalledProcessError): - _compose_with_retry("up", "-d", compose_file=Path("compose.yml"), retries=3) - - def test_exponential_backoff(self): - fail = MagicMock(returncode=1, stdout="", stderr="") - sleep_calls = [] - with patch("decnet.engine.deployer.subprocess.run", return_value=fail), \ - patch("decnet.engine.deployer.time.sleep", side_effect=lambda d: sleep_calls.append(d)): - with pytest.raises(subprocess.CalledProcessError): - _compose_with_retry("up", compose_file=Path("c.yml"), retries=3, delay=1.0) - assert sleep_calls == [1.0, 2.0] - - def test_correct_command_structure(self): - ok = MagicMock(returncode=0, stdout="") - with patch("decnet.engine.deployer.subprocess.run", return_value=ok) as mock_run: - _compose_with_retry("up", "-d", "--remove-orphans", - compose_file=Path("/tmp/compose.yml")) - cmd = mock_run.call_args[0][0] - assert cmd[:3] == ["docker", "compose", "-f"] - assert "up" in cmd - assert "--remove-orphans" in cmd - - -# --------------------------------------------------------------------------- -# mutate_decky -# --------------------------------------------------------------------------- - -class TestMutateDecky: - def _patch(self, config=None, compose_path=Path("compose.yml")): - """Return a context manager that mocks all I/O in mutate_decky.""" - cfg = config or _make_config() - return ( - patch("decnet.mutator.engine.load_state", return_value=(cfg, compose_path)), - patch("decnet.mutator.engine.save_state"), - patch("decnet.mutator.engine.write_compose"), - patch("decnet.mutator.engine._compose_with_retry"), - ) - - def test_returns_false_when_no_state(self): - with patch("decnet.mutator.engine.load_state", return_value=None): - assert mutate_decky("decky-01") is False - - def test_returns_false_when_decky_not_found(self): - p = self._patch() - with p[0], p[1], p[2], p[3]: - assert mutate_decky("nonexistent") is False - - def test_returns_true_on_success(self): - p = self._patch() - with p[0], p[1], p[2], p[3]: - assert mutate_decky("decky-01") is True - - def test_saves_state_after_mutation(self): - p = self._patch() - with p[0], patch("decnet.mutator.engine.save_state") as mock_save, p[2], p[3]: - mutate_decky("decky-01") - mock_save.assert_called_once() - - def test_regenerates_compose_after_mutation(self): - p = self._patch() - with p[0], p[1], patch("decnet.mutator.engine.write_compose") as mock_compose, p[3]: - mutate_decky("decky-01") - mock_compose.assert_called_once() - - def test_returns_false_on_compose_failure(self): - p = self._patch() - err = subprocess.CalledProcessError(1, "docker", "", "compose failed") - with p[0], p[1], p[2], patch("decnet.mutator.engine._compose_with_retry", side_effect=err): - assert mutate_decky("decky-01") is False - - def test_mutation_changes_services(self): - cfg = _make_config(deckies=[_make_decky(services=["ssh"])]) - p = self._patch(config=cfg) - with p[0], p[1], p[2], p[3]: - mutate_decky("decky-01") - # Services may have changed (or stayed the same after 20 attempts) - assert isinstance(cfg.deckies[0].services, list) - assert len(cfg.deckies[0].services) >= 1 - - def test_updates_last_mutated_timestamp(self): - cfg = _make_config(deckies=[_make_decky(last_mutated=0.0)]) - p = self._patch(config=cfg) - before = time.time() - with p[0], p[1], p[2], p[3]: - mutate_decky("decky-01") - assert cfg.deckies[0].last_mutated >= before - - def test_archetype_constrains_service_pool(self): - """A decky with an archetype must only mutate within its service pool.""" - cfg = _make_config(deckies=[_make_decky(archetype="workstation", services=["rdp"])]) - p = self._patch(config=cfg) - with p[0], p[1], p[2], p[3]: - result = mutate_decky("decky-01") - assert result is True - - -# --------------------------------------------------------------------------- -# mutate_all -# --------------------------------------------------------------------------- - -class TestMutateAll: - def test_no_state_returns_early(self): - with patch("decnet.mutator.engine.load_state", return_value=None), \ - patch("decnet.mutator.engine.mutate_decky") as mock_mutate: - mutate_all() - mock_mutate.assert_not_called() - - def test_force_mutates_all_deckies(self): - cfg = _make_config(deckies=[_make_decky("d1"), _make_decky("d2")]) - with patch("decnet.mutator.engine.load_state", return_value=(cfg, Path("c.yml"))), \ - patch("decnet.mutator.engine.mutate_decky", return_value=True) as mock_mutate: - mutate_all(force=True) - assert mock_mutate.call_count == 2 - - def test_skips_decky_not_yet_due(self): - # last_mutated = now, interval = 30 min → not due - now = time.time() - cfg = _make_config(deckies=[_make_decky(mutate_interval=30, last_mutated=now)]) - with patch("decnet.mutator.engine.load_state", return_value=(cfg, Path("c.yml"))), \ - patch("decnet.mutator.engine.mutate_decky") as mock_mutate: - mutate_all(force=False) - mock_mutate.assert_not_called() - - def test_mutates_decky_that_is_due(self): - # last_mutated = 2 hours ago, interval = 30 min → due - old_ts = time.time() - 7200 - cfg = _make_config(deckies=[_make_decky(mutate_interval=30, last_mutated=old_ts)]) - with patch("decnet.mutator.engine.load_state", return_value=(cfg, Path("c.yml"))), \ - patch("decnet.mutator.engine.mutate_decky", return_value=True) as mock_mutate: - mutate_all(force=False) - mock_mutate.assert_called_once_with("decky-01") - - def test_skips_decky_with_no_interval_and_no_force(self): - cfg = _make_config( - deckies=[_make_decky(mutate_interval=None)], - mutate_interval=None, - ) - with patch("decnet.mutator.engine.load_state", return_value=(cfg, Path("c.yml"))), \ - patch("decnet.mutator.engine.mutate_decky") as mock_mutate: - mutate_all(force=False) - mock_mutate.assert_not_called() diff --git a/tests/test_smtp_relay.py b/tests/test_smtp_relay.py index 2bc421b..34f8904 100644 --- a/tests/test_smtp_relay.py +++ b/tests/test_smtp_relay.py @@ -7,7 +7,7 @@ from decnet.services.smtp_relay import SMTPRelayService def test_smtp_relay_compose_fragment(): svc = SMTPRelayService() fragment = svc.compose_fragment("test-decky", log_target="log-server") - + assert fragment["container_name"] == "test-decky-smtp_relay" assert fragment["environment"]["SMTP_OPEN_RELAY"] == "1" assert fragment["environment"]["LOG_TARGET"] == "log-server" @@ -15,7 +15,7 @@ def test_smtp_relay_compose_fragment(): def test_smtp_relay_custom_cfg(): svc = SMTPRelayService() fragment = svc.compose_fragment( - "test-decky", + "test-decky", service_cfg={"banner": "Welcome", "mta": "Postfix"} ) assert fragment["environment"]["SMTP_BANNER"] == "Welcome"