firegex/tests/unit/test_refused_rebuild.py
2026-10-01 10:05:17 +02:00

143 lines
4.6 KiB
Python

"""An edit refused by a rebuild, and a service that could not start at all.
Both used to leave the same thing behind: a service that was not running, while what the
operator was shown said otherwise. An edit whose rebuild failed was answered "refused" —
which reads as "nothing happened" — after the rebuild had already stopped the service. And
a service that could not come back at boot kept `active` in the database, which is what
the list showed, with nothing filtering it.
"""
import asyncio
from types import SimpleNamespace
import pytest
from modules.services import firewall
from modules.services.models import STATUS, TRANSPORT, Service
def _link(name: str):
return SimpleNamespace(name=name, filter=SimpleNamespace(name=name, active=True))
class _Queued:
"""Stands in for the NFQUEUE layer: a new shape cannot be pushed, only rebuilt, and a
chain holding the filter called `broken` does not start."""
def __init__(self, started: list):
self.started = started
async def start(self, chain):
names = [link.name for link in chain]
self.started.append(names)
if "broken" in names:
raise Exception("the nfqueue binary rejected the filters")
return {}
async def reload(self, chain):
raise firewall.transports.ChainShapeChanged()
async def stop(self):
pass
async def retire(self, keep_filtering, then=None):
if then:
then()
class _Quiet:
"""The rules and the log: none of them is the question."""
def add(self, *args, **kwargs):
pass
def delete(self, *args, **kwargs):
return set()
def release_guards(self, *args, **kwargs):
pass
class _Db:
def __init__(self):
self.written = []
def query(self, sql, *args):
if sql.startswith("UPDATE"):
self.written.append((sql, args))
return []
@pytest.fixture
def service(monkeypatch):
started = []
state = {"chain": ["ok"]}
monkeypatch.setattr(firewall, "nft", _Quiet())
monkeypatch.setattr(firewall, "log_for", lambda service_id: _Quiet())
monkeypatch.setattr(firewall.transports, "build", lambda srv, **kw: _Queued(started))
srv = Service(service_id="s", name="n", status=STATUS.STOP, proto="tcp",
transport=TRANSPORT.NFQUEUE)
db = _Db()
manager = firewall.ServiceManager(srv, db)
manager.chain = lambda: [_link(name) for name in state["chain"]]
return manager, state, started, db
def test_a_rebuild_the_edit_cannot_survive_starts_the_service_again_as_it_was(service):
manager, state, started, _ = service
async def run():
await manager.enable()
state["chain"] = ["ok", "broken"]
with pytest.raises(Exception):
await manager.update_chain(lambda: state.update(chain=["ok"]))
asyncio.run(run())
assert manager.active, "an edit that was refused left the service stopped"
assert started == [["ok"], ["ok", "broken"], ["ok"]], started
def test_the_undo_is_run_once_whoever_asks_for_it():
"""The manager runs it before starting the service again; the router's own `except`,
which has always run it, now arrives second."""
from routers.services import _once
ran = []
undo = _once(lambda: ran.append(1))
undo()
undo()
assert ran == [1]
_once(None)() # nothing to undo is not an error
def test_stopping_a_service_that_could_not_start_is_remembered(service):
"""It is not running, so stopping it changed nothing in the kernel — but it is still
a decision, and kept as `active` the next boot would try to start it again."""
manager, state, _, db = service
manager.srv.status = STATUS.ACTIVE # what the database says: meant to run
state["chain"] = ["broken"]
async def run():
with pytest.raises(Exception):
await manager.enable()
assert not manager.active
await manager.disable()
asyncio.run(run())
assert manager.srv.status == STATUS.STOP
assert any(args[0] == STATUS.STOP for _, args in db.written), db.written
def test_the_list_says_whether_a_service_is_running_not_whether_it_is_meant_to(monkeypatch):
import routers.services as router
class Manager:
active = False
monkeypatch.setitem(router.firewall.services, "x", Manager())
assert router._running_status({"service_id": "x", "status": STATUS.ACTIVE}) == STATUS.STOP
Manager.active = True
assert router._running_status({"service_id": "x", "status": STATUS.STOP}) == STATUS.ACTIVE
assert router._running_status({"service_id": "nobody", "status": STATUS.ACTIVE}) \
== STATUS.ACTIVE, "a service the firewall does not know keeps what it stored"