Files
DECNET/tests/api/test_schemathesis.py
anti f2b3393669 chore: relicense to AGPL-3.0-or-later and add SPDX headers
Replaces LICENSE (GPLv3 -> AGPLv3) and prepends
`SPDX-License-Identifier: AGPL-3.0-or-later` to every source file
across decnet/, decnet_web/, tests/, scripts/, and tools/.

Rationale: closes the GPLv3 ASP loophole so any party operating a
modified DECNET as a network service must offer their modified
source. Personal copyright (Samuel Paschuan) + inbound=outbound
contributions make a future unilateral relicense infeasible.

- LICENSE: full AGPL-3.0 text (gnu.org/licenses/agpl-3.0.txt)
- COPYRIGHT: project copyright notice
- tools/add_spdx_headers.py: idempotent header injector
  (shebang- and PEP 263-aware)

Touches 1565 source files (.py, .ts, .tsx, .js, .jsx, .css, .sh).
No behavior change; comments only.
2026-05-22 21:04:16 -04:00

185 lines
5.6 KiB
Python

# SPDX-License-Identifier: AGPL-3.0-or-later
"""
Schemathesis contract tests — full compliance, all checks enabled.
Requires DECNET_DEVELOPER=true (set in tests/conftest.py) to expose /openapi.json.
"""
import pytest
import schemathesis as st
from schemathesis.checks import not_a_server_error
from schemathesis.specs.openapi.checks import (
status_code_conformance,
content_type_conformance,
response_headers_conformance,
response_schema_conformance,
positive_data_acceptance,
negative_data_rejection,
missing_required_header,
use_after_free,
ensure_resource_availability,
ignored_auth,
)
from hypothesis import settings, Verbosity, HealthCheck
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:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.bind(("127.0.0.1", 0))
return s.getsockname()[1]
LIVE_PORT = _free_port()
LIVE_SERVER_URL = f"http://127.0.0.1:{LIVE_PORT}"
TEST_SECRET = "test-secret-for-automated-fuzzing"
_QUICK = os.getenv("SCHEMA_QUICK") == "1"
pytestmark = pytest.mark.xdist_group("schemathesis")
import decnet.web.auth
decnet.web.auth.SECRET_KEY = TEST_SECRET
TEST_TOKEN = create_access_token({"uuid": "00000000-0000-0000-0000-000000000001"})
ALL_CHECKS = (
not_a_server_error,
status_code_conformance,
content_type_conformance,
response_headers_conformance,
response_schema_conformance,
positive_data_acceptance,
negative_data_rejection,
missing_required_header,
# `unsupported_method` is intentionally omitted: it expects 405 for
# any HTTP method not declared on a path, but FastAPI route tables
# frequently collide static (`/topologies/services`) and
# parameterized (`/topologies/{topology_id}`) siblings. A request
# with an undeclared method on the static path falls through to
# the parameterized route, where auth/RBAC fires first and returns
# 401/403. That ordering is deliberate — leaking 405-vs-401 would
# let unauthenticated callers enumerate which strings are valid
# topology UUIDs. The check is incompatible with that design.
use_after_free,
ensure_resource_availability,
)
AUTH_CHECKS = (
not_a_server_error,
ignored_auth,
)
@st.hook
def before_call(context, case, *args):
case.headers = case.headers or {}
case.headers["Authorization"] = f"Bearer {TEST_TOKEN}"
if case.path and case.path.endswith("/stream"):
case.query = case.query or {}
case.query["maxOutput"] = 0
def wait_for_port(port: int, timeout: float = 10.0) -> bool:
deadline = time.time() + timeout
while time.time() < deadline:
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() -> subprocess.Popen:
uvicorn_bin = "uvicorn" if os.name != "nt" else "uvicorn.exe"
uvicorn_path = str(Path(sys.executable).parent / uvicorn_bin)
env = os.environ.copy()
env["DECNET_DEVELOPER"] = "true"
env["DECNET_CONTRACT_TEST"] = "true"
env["DECNET_JWT_SECRET"] = TEST_SECRET
# Schemathesis fires thousands of examples per endpoint; the login
# bucket (10/5min per IP) trips on the second example and turns
# every subsequent valid request into a RejectedPositiveData
# failure. Disable the limiter for the fuzz subprocess — same
# rationale as the load-testing knob in decnet/web/limiter.py.
env["DECNET_LIMITER_ENABLED"] = "false"
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,
)
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
_server_proc = start_automated_server()
schema = st.openapi.from_url(f"{LIVE_SERVER_URL}/openapi.json")
@pytest.mark.fuzz
@st.pytest.parametrize(api=schema)
@settings(
max_examples=10 if _QUICK else 100,
deadline=None,
verbosity=Verbosity.debug,
suppress_health_check=[
HealthCheck.filter_too_much,
HealthCheck.too_slow,
HealthCheck.data_too_large,
],
)
def test_schema_compliance(case):
"""Full contract test: valid + invalid inputs, all response checks."""
case.call_and_validate(checks=ALL_CHECKS)
@pytest.mark.fuzz
@st.pytest.parametrize(api=schema)
@settings(
max_examples=50 if _QUICK else 500,
deadline=None,
verbosity=Verbosity.normal,
suppress_health_check=[
HealthCheck.filter_too_much,
HealthCheck.too_slow,
],
)
def test_auth_enforcement(case):
"""Verify every protected endpoint rejects requests with no token."""
case.headers = {
k: v for k, v in (case.headers or {}).items()
if k.lower() != "authorization"
}
if case.path and case.path.endswith("/stream"):
case.query = case.query or {}
case.query["maxOutput"] = 0
case.call_and_validate(checks=AUTH_CHECKS)