mirror of
https://github.com/CyberMind-FR/secubox-deb.git
synced 2026-07-28 21:17:36 +00:00
C1: drop debian/compat (kept debhelper-compat (= 13) Build-Depends) — the package was the only one in the repo carrying both, which debhelper refuses to build. C2: cli._run() now returns rc=None for "did not execute" (OSError/timeout), mirroring observe._run_cmd's contract. _cmd_scan checks rc for both systemctl list-unit-files and lxc-ls and aborts rather than silently deriving LXC modules as runtime="native" from empty output; a non-root invocation (rc=0, empty output) is refused explicitly since it is indistinguishable from "no containers" by rc alone. C3: PROTECTED_IDS moves from scan.py to manifest.py (a schema property, not a profiler detail) and load_manifest() now forces protected=True for those ids regardless of what a shipped manifest declares, closing the gap where a sloppy module package could silently unprotect auth/aggregator/core/etc. Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
141 lines
4.9 KiB
Python
141 lines
4.9 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 pytest
|
|
|
|
from api.manifest import PROTECTED_IDS, Manifest, ManifestError, load_all, load_manifest
|
|
|
|
FULL = """
|
|
id = "peertube"
|
|
category = "media"
|
|
runtime = "lxc"
|
|
exposure = "public"
|
|
units = ["secubox-peertube.service"]
|
|
lxc = "peertube"
|
|
portal = { domain = "peertube.gk2.secubox.in" }
|
|
priority = 40
|
|
protected = false
|
|
needs = ["auth"]
|
|
"""
|
|
|
|
MINIMAL = """
|
|
id = "lyrion"
|
|
category = "media"
|
|
runtime = "native"
|
|
exposure = "lan"
|
|
units = ["secubox-lyrion.service"]
|
|
"""
|
|
|
|
|
|
def test_load_full_manifest(tmp_path):
|
|
p = tmp_path / "peertube.toml"
|
|
p.write_text(FULL)
|
|
m = load_manifest(p)
|
|
assert m == Manifest(
|
|
id="peertube", category="media", runtime="lxc", exposure="public",
|
|
units=("secubox-peertube.service",), lxc="peertube",
|
|
portal_domain="peertube.gk2.secubox.in", priority=40,
|
|
protected=False, needs=("auth",),
|
|
)
|
|
|
|
|
|
def test_defaults_are_applied(tmp_path):
|
|
# Un manifeste minimal doit rester valide : la plupart des 134 modules
|
|
# n'ont ni LXC, ni portail, ni deps.
|
|
p = tmp_path / "lyrion.toml"
|
|
p.write_text(MINIMAL)
|
|
m = load_manifest(p)
|
|
assert m.lxc is None and m.portal_domain is None
|
|
assert m.priority == 50 and m.protected is False and m.needs == ()
|
|
|
|
|
|
@pytest.mark.parametrize("field,original,bad", [
|
|
("runtime", 'runtime = "native"', 'runtime = "docker"'),
|
|
("exposure", 'exposure = "lan"', 'exposure = "world"'),
|
|
("category", 'category = "media"', 'category = "divers"'),
|
|
])
|
|
def test_rejects_unknown_enum(tmp_path, field, original, bad):
|
|
# Une valeur inconnue doit échouer bruyamment : un manifeste mal typé
|
|
# deviendrait une décision d'extinction erronée en Phase 3.
|
|
src = MINIMAL.replace(original, bad)
|
|
# Sanity check: the substitution must actually have happened, otherwise
|
|
# this test would silently exercise the unchanged MINIMAL fixture.
|
|
assert src != MINIMAL
|
|
# The filename must match MINIMAL's id ("lyrion") so the id-vs-filename
|
|
# check passes and the enum validator is the one actually reached.
|
|
p = tmp_path / "lyrion.toml"
|
|
p.write_text(src)
|
|
with pytest.raises(ManifestError):
|
|
load_manifest(p)
|
|
|
|
|
|
def test_rejects_lxc_runtime_without_lxc_name(tmp_path):
|
|
p = tmp_path / "lyrion.toml"
|
|
p.write_text(MINIMAL.replace('runtime = "native"', 'runtime = "lxc"'))
|
|
with pytest.raises(ManifestError):
|
|
load_manifest(p)
|
|
|
|
|
|
def test_rejects_priority_out_of_range(tmp_path):
|
|
p = tmp_path / "lyrion.toml"
|
|
p.write_text(MINIMAL + "\npriority = 101\n")
|
|
with pytest.raises(ManifestError):
|
|
load_manifest(p)
|
|
|
|
|
|
def test_rejects_needs_as_bare_string(tmp_path):
|
|
# `needs = "auth"` (forgotten brackets) must not silently explode into
|
|
# ('a', 'u', 't', 'h') via tuple(str) — it must fail loudly.
|
|
p = tmp_path / "lyrion.toml"
|
|
p.write_text(MINIMAL + '\nneeds = "auth"\n')
|
|
with pytest.raises(ManifestError):
|
|
load_manifest(p)
|
|
|
|
|
|
def test_rejects_protected_as_string(tmp_path):
|
|
# `protected = "false"` must not be coerced by Python truthiness into
|
|
# True — protected gates whether a module may ever be shut down.
|
|
p = tmp_path / "lyrion.toml"
|
|
p.write_text(MINIMAL + '\nprotected = "false"\n')
|
|
with pytest.raises(ManifestError):
|
|
load_manifest(p)
|
|
|
|
|
|
def test_rejects_id_mismatching_filename(tmp_path):
|
|
# L'id pilote pins/profils ; s'il diverge du nom de fichier, un pin
|
|
# viserait un module fantôme.
|
|
p = tmp_path / "autre.toml"
|
|
p.write_text(MINIMAL)
|
|
with pytest.raises(ManifestError):
|
|
load_manifest(p)
|
|
|
|
|
|
def test_load_manifest_forces_protected_true_for_core_ids_even_when_data_says_false(tmp_path):
|
|
# Design : modules.d/*.toml est shipé par le paquet du module lui-même.
|
|
# Un secubox-auth un peu négligent (ou compromis) pourrait shipper
|
|
# protected=false et désactiver silencieusement la protection du noyau.
|
|
# C1 review: la garde doit être structurelle (PROTECTED_IDS), pas
|
|
# data-driven — load_manifest doit forcer True quoi que dise le fichier.
|
|
assert "auth" in PROTECTED_IDS
|
|
p = tmp_path / "auth.toml"
|
|
p.write_text(
|
|
'id = "auth"\n'
|
|
'category = "security"\n'
|
|
'runtime = "native"\n'
|
|
'exposure = "internal"\n'
|
|
'units = ["secubox-auth.service"]\n'
|
|
'protected = false\n'
|
|
)
|
|
m = load_manifest(p)
|
|
assert m.protected is True
|
|
|
|
|
|
def test_load_all_indexes_by_id_and_skips_non_toml(tmp_path):
|
|
(tmp_path / "lyrion.toml").write_text(MINIMAL)
|
|
(tmp_path / "peertube.toml").write_text(FULL)
|
|
(tmp_path / "notes.txt").write_text("ignore me")
|
|
all_m = load_all(tmp_path)
|
|
assert sorted(all_m) == ["lyrion", "peertube"]
|
|
assert all_m["peertube"].runtime == "lxc"
|