""" Schemathesis contract tests. Generates requests from the OpenAPI spec and verifies that no input causes a 5xx. Currently scoped to `not_a_server_error` only — full response-schema conformance (including undocumented 401 responses) is blocked by DEBT-020 (missing error response declarations across all protected endpoints). Once DEBT-020 is resolved, replace the checks list with the default (remove the argument) for full compliance. Requires DECNET_DEVELOPER=true (set in tests/conftest.py) to expose /openapi.json. """ import pytest import schemathesis as st from hypothesis import settings, Verbosity from decnet.web.auth import create_access_token import subprocess import socket import sys import atexit import os import time from datetime import datetime, timezone from pathlib import Path def _free_port() -> int: """Bind to port 0, let the OS pick a free port, return it.""" with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: s.bind(("127.0.0.1", 0)) return s.getsockname()[1] # Configuration for the automated live server LIVE_PORT = _free_port() 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}" # Force SSE stream to close after the initial snapshot so the test doesn't hang if case.path and case.path.endswith("/stream"): case.query = case.query or {} case.query["maxOutput"] = 0 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 log_dir = Path(__file__).parent.parent.parent / "logs" log_dir.mkdir(exist_ok=True) ts = datetime.now(timezone.utc).strftime("%Y%m%d_%H%M%S") log_file = open(log_dir / f"fuzz_server_{LIVE_PORT}_{ts}.log", "w") proc = subprocess.Popen( [uvicorn_path, "decnet.web.api:app", "--host", "127.0.0.1", "--port", str(LIVE_PORT), "--log-level", "info"], env=env, stdout=log_file, stderr=log_file, ) # Register cleanup atexit.register(proc.terminate) atexit.register(log_file.close) 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 @st.pytest.parametrize(api=schema) @settings(max_examples=3000, deadline=None, verbosity=Verbosity.debug) def test_schema_compliance(case): case.call_and_validate()