secubox-deb/packages/secubox-profiles/tests/test_observe.py
CyberMind-FR aa430739e6 fix(profiles): stop fabricating False in observe() systemd/routes branches
_run_cmd returned rc=1 both when systemctl genuinely answered "disabled"
and when the command failed to execute (OSError/timeout) — the latter
then read as a fabricated `enabled=False`/`active=False`, which a later
phase would treat as "off" and wrongly start. _run_cmd now returns
rc=None as the not-executed sentinel, and observe() maps it to None
instead of False for both enabled and active.

Likewise load_routes() folded an unreadable/corrupt routes file into the
same "no routes" set() as a genuinely absent file, producing a fabricated
portal_routed=False. It now returns None when the file exists but can't
be read/parsed, and observe() propagates that as portal_routed=None.

Also restores the `m: Manifest` type annotation on observe() that had
been dropped.

Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-17 12:04:55 +02:00

167 lines
7.2 KiB
Python

# SPDX-License-Identifier: LicenseRef-CMSD-1.0
# Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr>
# Source-Disclosed License — All rights reserved except as expressly granted.
# See LICENCE-CMSD-1.0.md for terms.
import json
from pathlib import Path
from api.manifest import Manifest
from api.observe import Actual, is_on, load_routes, observe
def fake_run(table):
"""table: {(argv tuple): (rc, stdout)}"""
def _run(argv):
return table.get(tuple(argv), (1, ""))
return _run
NATIVE = Manifest(id="lyrion", category="media", runtime="native", exposure="lan",
units=("secubox-lyrion.service",))
LXC = Manifest(id="peertube", category="media", runtime="lxc", exposure="public",
units=("secubox-peertube.service",), lxc="peertube",
portal_domain="peertube.gk2.secubox.in")
def test_observe_native_enabled_and_active():
run = fake_run({
("systemctl", "is-enabled", "secubox-lyrion.service"): (0, "enabled\n"),
("systemctl", "is-active", "secubox-lyrion.service"): (0, "active\n"),
("systemctl", "show", "secubox-lyrion.service", "-p", "MainPID", "--value"): (0, "0\n"),
})
a = observe(NATIVE, run=run, routes=set())
assert a.enabled is True and a.active is True
assert a.lxc_running is None and a.lxc_autostart is None
assert a.portal_routed is None # pas de portail déclaré
def test_observe_native_disabled():
run = fake_run({
("systemctl", "is-enabled", "secubox-lyrion.service"): (1, "disabled\n"),
("systemctl", "is-active", "secubox-lyrion.service"): (3, "inactive\n"),
("systemctl", "show", "secubox-lyrion.service", "-p", "MainPID", "--value"): (0, "0\n"),
})
a = observe(NATIVE, run=run, routes=set())
assert a.enabled is False and a.active is False
def test_observe_lxc_and_portal():
run = fake_run({
("systemctl", "is-enabled", "secubox-peertube.service"): (0, "enabled\n"),
("systemctl", "is-active", "secubox-peertube.service"): (0, "active\n"),
("systemctl", "show", "secubox-peertube.service", "-p", "MainPID", "--value"): (0, "0\n"),
("lxc-info", "-n", "peertube", "-s"): (0, "State: RUNNING\n"),
("lxc-info", "-n", "peertube", "-c", "lxc.start.auto"): (0, "lxc.start.auto = 1\n"),
})
a = observe(LXC, run=run, routes={"peertube.gk2.secubox.in"})
assert a.lxc_running is True and a.lxc_autostart is True
assert a.portal_routed is True
def test_lxc_state_unknown_is_none_not_false():
# lxc-info échoue depuis le contexte non privilégié de l'API (motif connu de
# cette box : lxc_state='absent' alors que le service répond). Inconnu doit
# rester None — surtout pas False, qui déclencherait un faux 'à allumer'.
run = fake_run({
("systemctl", "is-enabled", "secubox-peertube.service"): (0, "enabled\n"),
("systemctl", "is-active", "secubox-peertube.service"): (0, "active\n"),
("systemctl", "show", "secubox-peertube.service", "-p", "MainPID", "--value"): (0, "0\n"),
})
a = observe(LXC, run=run, routes=set())
assert a.lxc_running is None and a.lxc_autostart is None
assert a.portal_routed is False # portail déclaré mais absent des routes
def test_rss_read_from_mainpid(tmp_path, monkeypatch):
run = fake_run({
("systemctl", "is-enabled", "secubox-lyrion.service"): (0, "enabled\n"),
("systemctl", "is-active", "secubox-lyrion.service"): (0, "active\n"),
("systemctl", "show", "secubox-lyrion.service", "-p", "MainPID", "--value"): (0, "4242\n"),
})
status = tmp_path / "4242"
status.mkdir()
(status / "status").write_text("Name:\tpython3\nVmRSS:\t 123456 kB\n")
monkeypatch.setattr("api.observe.PROC", tmp_path)
a = observe(NATIVE, run=run, routes=set())
assert a.rss_kb == 123456
def test_is_on_requires_enabled_and_active():
assert is_on(Actual(enabled=True, active=True)) is True
assert is_on(Actual(enabled=True, active=False)) is False
assert is_on(Actual(enabled=False, active=True)) is False
def test_load_routes(tmp_path):
p = tmp_path / "haproxy-routes.json"
p.write_text(json.dumps({"peertube.gk2.secubox.in": ["127.0.0.1", 9000],
"billets.gk2.secubox.in": ["127.0.0.1", 8910]}))
assert load_routes(p) == {"peertube.gk2.secubox.in", "billets.gk2.secubox.in"}
def test_load_routes_missing_file_is_empty(tmp_path):
assert load_routes(tmp_path / "absent.json") == set()
def test_observe_systemctl_execution_failure_is_none_not_false():
# rc=None = la commande n'a PAS pu s'exécuter (OSError/timeout dans
# _run_cmd) — indistinguable d'un rc!=0 authentique si on ne traite pas
# ce cas à part. Doit rester None, jamais un False fabriqué.
run = fake_run({
("systemctl", "is-enabled", "secubox-lyrion.service"): (None, ""),
("systemctl", "is-active", "secubox-lyrion.service"): (None, ""),
("systemctl", "show", "secubox-lyrion.service", "-p", "MainPID", "--value"): (None, ""),
})
a = observe(NATIVE, run=run, routes=set())
assert a.enabled is None
assert a.active is None
assert a.rss_kb is None
def test_observe_systemctl_genuine_rc1_is_false_not_none():
# Contrepartie du test précédent : un rc=1 AUTHENTIQUE (la commande s'est
# exécutée et a répondu "disabled"/"inactive") doit rester un vrai False,
# pas être confondu avec l'échec d'exécution.
run = fake_run({
("systemctl", "is-enabled", "secubox-lyrion.service"): (1, "disabled\n"),
("systemctl", "is-active", "secubox-lyrion.service"): (3, "inactive\n"),
("systemctl", "show", "secubox-lyrion.service", "-p", "MainPID", "--value"): (0, "0\n"),
})
a = observe(NATIVE, run=run, routes=set())
assert a.enabled is False
assert a.active is False
def test_load_routes_corrupt_file_is_none_not_empty(tmp_path):
# Un JSON corrompu (ex. écriture concurrente par HAProxy) est indéterminable,
# pas "aucune route" : ne pas fabriquer un set() vide qui masquerait l'erreur.
p = tmp_path / "haproxy-routes.json"
p.write_text("{not valid json")
assert load_routes(p) is None
def test_load_routes_unreadable_file_is_none_not_empty(tmp_path, monkeypatch):
p = tmp_path / "haproxy-routes.json"
p.write_text(json.dumps(["peertube.gk2.secubox.in"]))
def _raise(*_a, **_kw):
raise OSError("permission denied")
monkeypatch.setattr(Path, "read_text", _raise)
assert load_routes(p) is None
def test_observe_portal_routed_none_when_routes_undeterminable(monkeypatch):
# Si load_routes() ne peut pas déterminer les routes (fichier présent mais
# illisible/corrompu → None), portal_routed doit rester None — surtout pas
# False, qui laisserait croire "pas routé".
import api.observe as observe_mod
monkeypatch.setattr(observe_mod, "load_routes", lambda: None)
run = fake_run({
("systemctl", "is-enabled", "secubox-peertube.service"): (0, "enabled\n"),
("systemctl", "is-active", "secubox-peertube.service"): (0, "active\n"),
("systemctl", "show", "secubox-peertube.service", "-p", "MainPID", "--value"): (0, "0\n"),
})
a = observe(LXC, run=run)
assert a.portal_routed is None