test: update and add tests for all updated subsystems
This commit is contained in:
@@ -0,0 +1,145 @@
|
||||
"""Tests that daemon.iface stays in sync with registered server routes.
|
||||
|
||||
Verifies a two-way contract:
|
||||
1. Every iface constant has a matching handler registered.
|
||||
2. Every registered handler has a matching iface constant.
|
||||
|
||||
Run with: pytest tests/test_iface_sync.py -v
|
||||
"""
|
||||
|
||||
from collections import defaultdict
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _load_handlers():
|
||||
"""Load all handler modules so registry is populated."""
|
||||
# Import server to get registry, then load handlers
|
||||
from daemon import server
|
||||
|
||||
# Force route registration
|
||||
server._register_routes()
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def registry():
|
||||
from daemon import server
|
||||
|
||||
return server.registry
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def iface_module():
|
||||
import daemon.iface as iface
|
||||
|
||||
return iface
|
||||
|
||||
|
||||
def _get_iface_pairs(iface_module):
|
||||
"""Extract all (method, path) pairs from iface module."""
|
||||
iface = iface_module
|
||||
return {
|
||||
name: val
|
||||
for name, val in iface.__dict__.items()
|
||||
if isinstance(val, tuple) and len(val) == 2 and isinstance(val[0], str)
|
||||
}
|
||||
|
||||
|
||||
def _get_registered_routes(registry):
|
||||
"""Extract all (METHOD, path) keys from the registry."""
|
||||
return {(method.upper(), path) for (method, path) in registry._routes}
|
||||
|
||||
|
||||
class TestIfaceSync:
|
||||
"""Verify iface constants match registered routes."""
|
||||
|
||||
def test_iface_constants_non_empty(self, iface_module):
|
||||
pairs = _get_iface_pairs(iface_module)
|
||||
assert len(pairs) >= 50, f"Expected many iface constants, got {len(pairs)}"
|
||||
|
||||
def test_iface_constants_have_registered_handlers(self, registry, iface_module):
|
||||
"""Every iface constant should map to a registered route."""
|
||||
registered = _get_registered_routes(registry)
|
||||
iface_pairs = _get_iface_pairs(iface_module)
|
||||
|
||||
# These 5 routes go through add_route() in create_app(), not @registry.register
|
||||
add_route_paths = {
|
||||
"/health",
|
||||
"/status/all",
|
||||
"/status/refresh",
|
||||
"/ws",
|
||||
"/batch",
|
||||
}
|
||||
|
||||
missing = []
|
||||
for name, (method, path) in iface_pairs.items():
|
||||
key = (method.upper(), path)
|
||||
if path not in add_route_paths and key not in registered:
|
||||
missing.append(
|
||||
(
|
||||
name,
|
||||
{
|
||||
"method": method,
|
||||
"path": path,
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
if missing:
|
||||
detail = "\n".join(f" {name}: {pair}" for name, pair in missing)
|
||||
pytest.fail(
|
||||
f"{len(missing)} iface constant(s) have no matching handler:\n{detail}"
|
||||
)
|
||||
|
||||
def test_registered_routes_have_iface_constants(self, registry, iface_module):
|
||||
"""Every registered route should have a matching iface constant."""
|
||||
registered = _get_registered_routes(registry)
|
||||
iface_pairs = _get_iface_pairs(iface_module)
|
||||
|
||||
iface_keys = set(iface_pairs.values())
|
||||
missing = registered - iface_keys
|
||||
|
||||
if missing:
|
||||
detail = "\n".join(f" {method} {path}" for method, path in sorted(missing))
|
||||
pytest.fail(
|
||||
f"{len(missing)} registered route(s) have no matching iface constant:\n{detail}"
|
||||
)
|
||||
|
||||
def test_no_duplicate_iface_constants(self, iface_module):
|
||||
"""All iface constants should have unique (method, path) pairs."""
|
||||
iface_pairs = _get_iface_pairs(iface_module)
|
||||
seen = defaultdict(list)
|
||||
for name, val in iface_pairs.items():
|
||||
seen[val].append(name)
|
||||
|
||||
dupes = {pair: names for pair, names in seen.items() if len(names) > 1}
|
||||
assert not dupes, "Duplicate iface constants:\n" + "".join(
|
||||
f" {pair}: {names}\n" for pair, names in dupes.items()
|
||||
)
|
||||
|
||||
|
||||
class TestIfaceFormat:
|
||||
"""Verify iface constants follow the expected format."""
|
||||
|
||||
def test_all_constants_are_tuples_of_str(self, iface_module):
|
||||
iface_pairs = _get_iface_pairs(iface_module)
|
||||
for name, val in iface_pairs.items():
|
||||
assert isinstance(val, tuple), f"{name} should be a tuple"
|
||||
assert len(val) == 2, f"{name} should have length 2"
|
||||
assert isinstance(val[0], str), f"{name} method should be a string"
|
||||
assert isinstance(val[1], str), f"{name} path should be a string"
|
||||
|
||||
def test_all_constants_have_uppercase_methods(self, iface_module):
|
||||
iface_pairs = _get_iface_pairs(iface_module)
|
||||
valid_methods = {"GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"}
|
||||
for name, val in iface_pairs.items():
|
||||
assert val[0].upper() in valid_methods, (
|
||||
f"{name} has invalid method: {val[0]} — "
|
||||
f"should be one of {valid_methods}"
|
||||
)
|
||||
|
||||
def test_all_constants_have_leading_slash_path(self, iface_module):
|
||||
iface_pairs = _get_iface_pairs(iface_module)
|
||||
for name, val in iface_pairs.items():
|
||||
assert val[1].startswith("/"), f"{name} path should start with /: {val[1]}"
|
||||
Reference in New Issue
Block a user