firegex/tests/unit/test_datapath_acks.py

80 lines
2.7 KiB
Python

"""What a datapath process's answer to a configuration push leaves running.
Both NFQUEUE binaries answer `ACK FAIL` with the filters they had still in force, so a
refusal is an error for the operator and nothing more. A push that is not answered at all
is different: whatever the process ends up doing is unknown, and its answer, if it comes,
would be read as the answer to the next command.
"""
import asyncio
import pytest
from modules.services import transports
from modules.services.models import KIND, Filter, Service, TRANSPORT
from modules.services.transports import ChainLink, NfqueueTransport, _QueueStage
class _Stdin:
def write(self, data: bytes) -> None:
self.data = data
async def drain(self) -> None:
pass
class _Process:
def __init__(self):
self.returncode = None
self.stdin = _Stdin()
self.killed = False
def kill(self) -> None:
self.killed = True
self.returncode = -9
def _stage() -> tuple[_QueueStage, _Process, list[str]]:
said: list[str] = []
srv = Service(service_id="s", name="n", status="active", proto="tcp",
transport=TRANSPORT.NFQUEUE)
owner = NfqueueTransport(srv, on_engine=said.append)
flt = Filter(filter_id="f1", service_id="s", position=0, kind=KIND.REGEX, name="p",
active=True)
stage = _QueueStage(srv, ChainLink(flt), owner)
stage.process = _Process()
return stage, stage.process, said
def test_a_refused_push_leaves_the_binary_running():
"""It used to be stopped, and stopped *on purpose*, so its watchdog said nothing: the
service went on reading active with no process behind its queue."""
stage, process, _ = _stage()
async def run():
pushing = asyncio.create_task(stage._push(b"payload\n"))
await asyncio.sleep(0)
stage._ack.set_result((False, "Failed to compile hyperscan db"))
with pytest.raises(Exception, match="rejected the filters"):
await pushing
asyncio.run(run())
assert not process.killed
assert stage.process is process and not stage._stopped
def test_an_unanswered_push_hands_the_binary_to_its_watchdog(monkeypatch):
"""Killed, and **not** marked stopped: its watchdog is what rebuilds the service from
the saved configuration, and what the process had applied cannot be known."""
monkeypatch.setattr(transports, "LOAD_TIMEOUT", 0.05)
stage, process, said = _stage()
async def run():
with pytest.raises(Exception, match="did not acknowledge"):
await stage._push(b"payload\n")
asyncio.run(run())
assert process.killed
assert not stage._stopped
assert any("restarting it" in line for line in said), said