refactor(tests): move flat tests/*.py into per-subsystem subfolders
Groups every flat test_*.py under the module it exercises, matching the
existing tests/{profiler,sniffer,prober,collector,correlation,cli,web,
topology,swarm,bus,updater,api,docker,geoip,...} layout. New folders:
services/, fleet/, config/, logging/, db/ (+ db/mysql/), telemetry/,
mutator/, core/.
Path-dependent __file__ references bumped an extra .parent in three
files that moved one level deeper:
- tests/sniffer/test_sniffer_ja3.py (template path)
- tests/services/test_ssh_capture_emit.py (template path)
- tests/cli/test_mode_gating.py (REPO root)
- tests/web/test_env_lazy_jwt.py (repo var)
Also drops two SQLite runtime artifacts (test_decnet.db-{shm,wal}) that
were leaking into the repo from a previous test run.
Fixes two test_service_isolation cases that patched asyncio.sleep (no
longer on the profiler main-loop hot path — same pre-existing bug I
fixed earlier in test_attacker_worker.py) by patching asyncio.wait_for
and passing interval=0.
This commit is contained in:
0
tests/config/__init__.py
Normal file
0
tests/config/__init__.py
Normal file
143
tests/config/test_config.py
Normal file
143
tests/config/test_config.py
Normal file
@@ -0,0 +1,143 @@
|
||||
"""
|
||||
Tests for decnet.config — Pydantic models, save/load/clear state.
|
||||
Covers the uncovered lines: validators, save_state, load_state, clear_state.
|
||||
"""
|
||||
import pytest
|
||||
|
||||
import decnet.config as config_module
|
||||
from decnet.config import (
|
||||
DeckyConfig,
|
||||
DecnetConfig,
|
||||
save_state,
|
||||
load_state,
|
||||
clear_state,
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# DeckyConfig validator
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestDeckyConfig:
|
||||
def _base(self, **kwargs):
|
||||
defaults = dict(
|
||||
name="decky-01", ip="192.168.1.10", services=["ssh"],
|
||||
distro="debian", base_image="debian", hostname="host-01",
|
||||
)
|
||||
defaults.update(kwargs)
|
||||
return defaults
|
||||
|
||||
def test_valid_decky(self):
|
||||
d = DeckyConfig(**self._base())
|
||||
assert d.name == "decky-01"
|
||||
|
||||
def test_empty_services_raises(self):
|
||||
with pytest.raises(Exception, match="at least 1 item"):
|
||||
DeckyConfig(**self._base(services=[]))
|
||||
|
||||
def test_multiple_services_ok(self):
|
||||
d = DeckyConfig(**self._base(services=["ssh", "smb", "rdp"]))
|
||||
assert len(d.services) == 3
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# DecnetConfig validator
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestDecnetConfig:
|
||||
def _base_decky(self):
|
||||
return DeckyConfig(
|
||||
name="d", ip="10.0.0.2", services=["ssh"],
|
||||
distro="debian", base_image="debian", hostname="h",
|
||||
)
|
||||
|
||||
def test_valid_config(self):
|
||||
cfg = DecnetConfig(
|
||||
mode="unihost", interface="eth0",
|
||||
subnet="10.0.0.0/24", gateway="10.0.0.1",
|
||||
deckies=[self._base_decky()],
|
||||
)
|
||||
assert cfg.mode == "unihost"
|
||||
|
||||
def test_log_file_field(self):
|
||||
cfg = DecnetConfig(
|
||||
mode="unihost", interface="eth0",
|
||||
subnet="10.0.0.0/24", gateway="10.0.0.1",
|
||||
deckies=[self._base_decky()],
|
||||
log_file="/var/log/decnet/decnet.log",
|
||||
)
|
||||
assert cfg.log_file == "/var/log/decnet/decnet.log"
|
||||
|
||||
def test_log_file_defaults_to_none(self):
|
||||
cfg = DecnetConfig(
|
||||
mode="unihost", interface="eth0",
|
||||
subnet="10.0.0.0/24", gateway="10.0.0.1",
|
||||
deckies=[self._base_decky()],
|
||||
)
|
||||
assert cfg.log_file is None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# save_state / load_state / clear_state
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def patch_state_file(tmp_path, monkeypatch):
|
||||
monkeypatch.setattr(config_module, "STATE_FILE", tmp_path / "decnet-state.json")
|
||||
|
||||
|
||||
def _sample_config():
|
||||
return DecnetConfig(
|
||||
mode="unihost", interface="eth0",
|
||||
subnet="192.168.1.0/24", gateway="192.168.1.1",
|
||||
deckies=[
|
||||
DeckyConfig(
|
||||
name="decky-01", ip="192.168.1.10", services=["ssh"],
|
||||
distro="debian", base_image="debian", hostname="host-01",
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
def test_save_and_load_state(tmp_path):
|
||||
cfg = _sample_config()
|
||||
compose = tmp_path / "docker-compose.yml"
|
||||
save_state(cfg, compose)
|
||||
|
||||
result = load_state()
|
||||
assert result is not None
|
||||
loaded_cfg, loaded_compose = result
|
||||
assert loaded_cfg.mode == "unihost"
|
||||
assert loaded_cfg.deckies[0].name == "decky-01"
|
||||
assert loaded_compose == compose
|
||||
|
||||
|
||||
def test_load_state_returns_none_when_missing(tmp_path, monkeypatch):
|
||||
monkeypatch.setattr(config_module, "STATE_FILE", tmp_path / "nonexistent.json")
|
||||
assert load_state() is None
|
||||
|
||||
|
||||
def test_clear_state(tmp_path):
|
||||
cfg = _sample_config()
|
||||
save_state(cfg, tmp_path / "compose.yml")
|
||||
assert config_module.STATE_FILE.exists()
|
||||
|
||||
clear_state()
|
||||
assert not config_module.STATE_FILE.exists()
|
||||
|
||||
|
||||
def test_clear_state_noop_when_missing(tmp_path, monkeypatch):
|
||||
monkeypatch.setattr(config_module, "STATE_FILE", tmp_path / "nonexistent.json")
|
||||
clear_state() # should not raise
|
||||
|
||||
|
||||
def test_state_roundtrip_preserves_all_fields(tmp_path):
|
||||
cfg = _sample_config()
|
||||
cfg.deckies[0].archetype = "workstation"
|
||||
cfg.deckies[0].mutate_interval = 45
|
||||
compose = tmp_path / "compose.yml"
|
||||
save_state(cfg, compose)
|
||||
|
||||
loaded_cfg, _ = load_state()
|
||||
assert loaded_cfg.deckies[0].archetype == "workstation"
|
||||
assert loaded_cfg.deckies[0].mutate_interval == 45
|
||||
286
tests/config/test_config_ini.py
Normal file
286
tests/config/test_config_ini.py
Normal file
@@ -0,0 +1,286 @@
|
||||
"""decnet.config_ini — INI file loader, precedence, section routing."""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from decnet.config_ini import load_ini_config
|
||||
|
||||
|
||||
def _write_ini(tmp_path: Path, body: str) -> Path:
|
||||
p = tmp_path / "decnet.ini"
|
||||
p.write_text(body)
|
||||
return p
|
||||
|
||||
|
||||
def _scrub(monkeypatch: pytest.MonkeyPatch, *names: str) -> None:
|
||||
for n in names:
|
||||
monkeypatch.delenv(n, raising=False)
|
||||
|
||||
|
||||
def test_missing_file_is_noop(monkeypatch, tmp_path):
|
||||
_scrub(monkeypatch, "DECNET_MODE", "DECNET_AGENT_PORT")
|
||||
result = load_ini_config(tmp_path / "does-not-exist.ini")
|
||||
assert result is None
|
||||
assert "DECNET_AGENT_PORT" not in os.environ
|
||||
|
||||
|
||||
def test_agent_section_only_loaded_when_mode_agent(monkeypatch, tmp_path):
|
||||
_scrub(
|
||||
monkeypatch,
|
||||
"DECNET_MODE", "DECNET_DISALLOW_MASTER",
|
||||
"DECNET_AGENT_PORT", "DECNET_MASTER_HOST",
|
||||
"DECNET_API_PORT", "DECNET_SWARMCTL_PORT",
|
||||
)
|
||||
ini = _write_ini(tmp_path, """
|
||||
[decnet]
|
||||
mode = agent
|
||||
|
||||
[agent]
|
||||
agent-port = 8765
|
||||
master-host = 192.168.1.50
|
||||
|
||||
[master]
|
||||
api-port = 9999
|
||||
swarmctl-port = 8770
|
||||
""")
|
||||
load_ini_config(ini)
|
||||
assert os.environ["DECNET_MODE"] == "agent"
|
||||
assert os.environ["DECNET_AGENT_PORT"] == "8765"
|
||||
assert os.environ["DECNET_MASTER_HOST"] == "192.168.1.50"
|
||||
# [master] section values must NOT leak into an agent host's env
|
||||
assert "DECNET_API_PORT" not in os.environ
|
||||
assert "DECNET_SWARMCTL_PORT" not in os.environ
|
||||
|
||||
|
||||
def test_master_section_loaded_when_mode_master(monkeypatch, tmp_path):
|
||||
_scrub(
|
||||
monkeypatch,
|
||||
"DECNET_MODE", "DECNET_API_PORT",
|
||||
"DECNET_SWARMCTL_PORT", "DECNET_AGENT_PORT",
|
||||
)
|
||||
ini = _write_ini(tmp_path, """
|
||||
[decnet]
|
||||
mode = master
|
||||
|
||||
[agent]
|
||||
agent-port = 8765
|
||||
|
||||
[master]
|
||||
api-port = 8000
|
||||
swarmctl-port = 8770
|
||||
""")
|
||||
load_ini_config(ini)
|
||||
assert os.environ["DECNET_MODE"] == "master"
|
||||
assert os.environ["DECNET_API_PORT"] == "8000"
|
||||
assert os.environ["DECNET_SWARMCTL_PORT"] == "8770"
|
||||
assert "DECNET_AGENT_PORT" not in os.environ
|
||||
|
||||
|
||||
def test_env_wins_over_ini(monkeypatch, tmp_path):
|
||||
_scrub(monkeypatch, "DECNET_MODE")
|
||||
monkeypatch.setenv("DECNET_AGENT_PORT", "7777")
|
||||
ini = _write_ini(tmp_path, """
|
||||
[decnet]
|
||||
mode = agent
|
||||
|
||||
[agent]
|
||||
agent-port = 8765
|
||||
""")
|
||||
load_ini_config(ini)
|
||||
# Real env var must beat INI value
|
||||
assert os.environ["DECNET_AGENT_PORT"] == "7777"
|
||||
|
||||
|
||||
def test_common_keys_always_exported(monkeypatch, tmp_path):
|
||||
_scrub(monkeypatch, "DECNET_MODE", "DECNET_DISALLOW_MASTER", "DECNET_LOG_DIRECTORY")
|
||||
ini = _write_ini(tmp_path, """
|
||||
[decnet]
|
||||
mode = agent
|
||||
disallow-master = true
|
||||
log-directory = /var/log/decnet
|
||||
""")
|
||||
load_ini_config(ini)
|
||||
assert os.environ["DECNET_MODE"] == "agent"
|
||||
assert os.environ["DECNET_DISALLOW_MASTER"] == "true"
|
||||
assert os.environ["DECNET_LOG_DIRECTORY"] == "/var/log/decnet"
|
||||
|
||||
|
||||
def test_invalid_mode_raises(monkeypatch, tmp_path):
|
||||
_scrub(monkeypatch, "DECNET_MODE")
|
||||
ini = _write_ini(tmp_path, """
|
||||
[decnet]
|
||||
mode = supervisor
|
||||
""")
|
||||
with pytest.raises(ValueError, match="mode must be"):
|
||||
load_ini_config(ini)
|
||||
|
||||
|
||||
def test_decnet_config_env_var_overrides_default_path(monkeypatch, tmp_path):
|
||||
_scrub(monkeypatch, "DECNET_MODE", "DECNET_API_PORT")
|
||||
ini = _write_ini(tmp_path, """
|
||||
[decnet]
|
||||
mode = master
|
||||
|
||||
[master]
|
||||
api-port = 9001
|
||||
""")
|
||||
monkeypatch.setenv("DECNET_CONFIG", str(ini))
|
||||
# Call with no explicit path — loader reads $DECNET_CONFIG
|
||||
loaded = load_ini_config()
|
||||
assert loaded == ini
|
||||
assert os.environ["DECNET_API_PORT"] == "9001"
|
||||
|
||||
|
||||
# ─── Domain sections ────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_domain_sections_load_regardless_of_mode(monkeypatch, tmp_path):
|
||||
"""[api], [web], [database], etc. load on both master and agent —
|
||||
setdefault makes unused keys harmless on the other role."""
|
||||
_scrub(
|
||||
monkeypatch,
|
||||
"DECNET_MODE", "DECNET_API_HOST", "DECNET_API_PORT",
|
||||
"DECNET_WEB_PORT", "DECNET_ADMIN_USER",
|
||||
"DECNET_DB_TYPE", "DECNET_DB_URL",
|
||||
"DECNET_BUS_ENABLED", "DECNET_BUS_GROUP",
|
||||
"DECNET_SWARM_SYSLOG_PORT",
|
||||
"DECNET_SYSTEM_LOGS", "DECNET_INGEST_LOG_FILE",
|
||||
"DECNET_BATCH_SIZE", "DECNET_BATCH_MAX_WAIT_MS",
|
||||
"DECNET_DEVELOPER_TRACING", "DECNET_OTEL_ENDPOINT",
|
||||
)
|
||||
ini = _write_ini(tmp_path, """
|
||||
[decnet]
|
||||
mode = agent
|
||||
|
||||
[api]
|
||||
host = 0.0.0.0
|
||||
port = 8001
|
||||
|
||||
[web]
|
||||
port = 9090
|
||||
admin-user = superman
|
||||
cors-origins = https://dash.example.com
|
||||
|
||||
[database]
|
||||
type = mysql
|
||||
url = mysql+asyncmy://decnet@db/decnet
|
||||
|
||||
[bus]
|
||||
enabled = false
|
||||
group = custom
|
||||
|
||||
[swarm]
|
||||
syslog-port = 7514
|
||||
|
||||
[logging]
|
||||
system-log = /tmp/decnet.log
|
||||
ingest-log = /tmp/decnet.ingest.log
|
||||
|
||||
[ingester]
|
||||
batch-size = 500
|
||||
batch-max-wait-ms = 1000
|
||||
|
||||
[tracing]
|
||||
enabled = true
|
||||
otel-endpoint = http://otel.internal:4317
|
||||
""")
|
||||
load_ini_config(ini)
|
||||
assert os.environ["DECNET_API_HOST"] == "0.0.0.0"
|
||||
assert os.environ["DECNET_API_PORT"] == "8001"
|
||||
assert os.environ["DECNET_WEB_PORT"] == "9090"
|
||||
assert os.environ["DECNET_ADMIN_USER"] == "superman"
|
||||
assert os.environ["DECNET_CORS_ORIGINS"] == "https://dash.example.com"
|
||||
assert os.environ["DECNET_DB_TYPE"] == "mysql"
|
||||
assert os.environ["DECNET_DB_URL"] == "mysql+asyncmy://decnet@db/decnet"
|
||||
assert os.environ["DECNET_BUS_ENABLED"] == "false"
|
||||
assert os.environ["DECNET_BUS_GROUP"] == "custom"
|
||||
assert os.environ["DECNET_SWARM_SYSLOG_PORT"] == "7514"
|
||||
assert os.environ["DECNET_SYSTEM_LOGS"] == "/tmp/decnet.log"
|
||||
assert os.environ["DECNET_INGEST_LOG_FILE"] == "/tmp/decnet.ingest.log"
|
||||
assert os.environ["DECNET_BATCH_SIZE"] == "500"
|
||||
assert os.environ["DECNET_BATCH_MAX_WAIT_MS"] == "1000"
|
||||
assert os.environ["DECNET_DEVELOPER_TRACING"] == "true"
|
||||
assert os.environ["DECNET_OTEL_ENDPOINT"] == "http://otel.internal:4317"
|
||||
|
||||
|
||||
def test_domain_section_env_wins_over_ini(monkeypatch, tmp_path):
|
||||
"""Real env var beats the INI for a domain-section key, same as
|
||||
the role-specific section contract."""
|
||||
_scrub(monkeypatch, "DECNET_MODE")
|
||||
monkeypatch.setenv("DECNET_API_PORT", "5555")
|
||||
ini = _write_ini(tmp_path, """
|
||||
[decnet]
|
||||
mode = master
|
||||
|
||||
[api]
|
||||
port = 8000
|
||||
""")
|
||||
load_ini_config(ini)
|
||||
assert os.environ["DECNET_API_PORT"] == "5555"
|
||||
|
||||
|
||||
def test_domain_unknown_key_logs_warning(monkeypatch, tmp_path, caplog):
|
||||
"""Typos in a domain section should be visible to the operator —
|
||||
a silent drop is how you spend an afternoon debugging 'why isn't
|
||||
my setting taking effect'."""
|
||||
_scrub(monkeypatch, "DECNET_MODE")
|
||||
ini = _write_ini(tmp_path, """
|
||||
[decnet]
|
||||
mode = master
|
||||
|
||||
[api]
|
||||
host = 127.0.0.1
|
||||
# typo: hostt instead of host
|
||||
hostt = 0.0.0.0
|
||||
""")
|
||||
import logging as _logging
|
||||
with caplog.at_level(_logging.WARNING, logger="decnet.config_ini"):
|
||||
load_ini_config(ini)
|
||||
assert any(
|
||||
"unknown key [api] hostt" in rec.getMessage()
|
||||
for rec in caplog.records
|
||||
), f"expected warning about unknown key, got: {[r.getMessage() for r in caplog.records]}"
|
||||
|
||||
|
||||
def test_domain_absent_section_is_noop(monkeypatch, tmp_path):
|
||||
"""INI with only [decnet] present doesn't touch any domain env var."""
|
||||
_scrub(
|
||||
monkeypatch,
|
||||
"DECNET_MODE", "DECNET_API_PORT", "DECNET_WEB_PORT",
|
||||
"DECNET_DB_TYPE", "DECNET_BUS_ENABLED",
|
||||
)
|
||||
ini = _write_ini(tmp_path, """
|
||||
[decnet]
|
||||
mode = master
|
||||
""")
|
||||
load_ini_config(ini)
|
||||
assert os.environ["DECNET_MODE"] == "master"
|
||||
assert "DECNET_API_PORT" not in os.environ
|
||||
assert "DECNET_WEB_PORT" not in os.environ
|
||||
assert "DECNET_DB_TYPE" not in os.environ
|
||||
assert "DECNET_BUS_ENABLED" not in os.environ
|
||||
|
||||
|
||||
def test_domain_section_does_not_override_role_section(monkeypatch, tmp_path):
|
||||
"""If both [master] (role) and [swarm] (domain) define swarmctl-port,
|
||||
whichever the loader applies first wins via setdefault — and the role
|
||||
section runs first, so the [swarm] value is dropped silently.
|
||||
|
||||
This locks in the precedence order as part of the contract."""
|
||||
_scrub(monkeypatch, "DECNET_MODE", "DECNET_SWARMCTL_PORT")
|
||||
ini = _write_ini(tmp_path, """
|
||||
[decnet]
|
||||
mode = master
|
||||
|
||||
[master]
|
||||
swarmctl-port = 9001
|
||||
|
||||
[swarm]
|
||||
swarmctl-port = 9999
|
||||
""")
|
||||
load_ini_config(ini)
|
||||
# [master] loaded first, [swarm] lost via setdefault
|
||||
assert os.environ["DECNET_SWARMCTL_PORT"] == "9001"
|
||||
217
tests/config/test_ini_loader.py
Normal file
217
tests/config/test_ini_loader.py
Normal file
@@ -0,0 +1,217 @@
|
||||
"""
|
||||
Tests for the INI loader — subsection parsing, custom service definitions,
|
||||
and per-service config propagation.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import textwrap
|
||||
from pathlib import Path
|
||||
from decnet.ini_loader import load_ini
|
||||
|
||||
|
||||
def _write_ini(tmp_path: Path, content: str) -> Path:
|
||||
f = tmp_path / "decnet.ini"
|
||||
f.write_text(textwrap.dedent(content))
|
||||
return f
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Basic decky parsing (regression)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_basic_decky_parsed(tmp_path):
|
||||
ini_file = _write_ini(tmp_path, """
|
||||
[general]
|
||||
net = 192.168.1.0/24
|
||||
gw = 192.168.1.1
|
||||
|
||||
[decky-01]
|
||||
ip = 192.168.1.101
|
||||
services = ssh, http
|
||||
""")
|
||||
cfg = load_ini(ini_file)
|
||||
assert len(cfg.deckies) == 1
|
||||
assert cfg.deckies[0].name == "decky-01"
|
||||
assert cfg.deckies[0].services == ["ssh", "http"]
|
||||
assert cfg.deckies[0].service_config == {}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Per-service subsection parsing
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_subsection_parsed_into_service_config(tmp_path):
|
||||
ini_file = _write_ini(tmp_path, """
|
||||
[decky-01]
|
||||
ip = 192.168.1.101
|
||||
services = ssh
|
||||
|
||||
[decky-01.ssh]
|
||||
kernel_version = 5.15.0-76-generic
|
||||
hardware_platform = x86_64
|
||||
""")
|
||||
cfg = load_ini(ini_file)
|
||||
svc_cfg = cfg.deckies[0].service_config
|
||||
assert "ssh" in svc_cfg
|
||||
assert svc_cfg["ssh"]["kernel_version"] == "5.15.0-76-generic"
|
||||
assert svc_cfg["ssh"]["hardware_platform"] == "x86_64"
|
||||
|
||||
|
||||
def test_multiple_subsections_for_same_decky(tmp_path):
|
||||
ini_file = _write_ini(tmp_path, """
|
||||
[decky-01]
|
||||
services = ssh, http
|
||||
|
||||
[decky-01.ssh]
|
||||
users = root:toor
|
||||
|
||||
[decky-01.http]
|
||||
server_header = nginx/1.18.0
|
||||
fake_app = wordpress
|
||||
""")
|
||||
cfg = load_ini(ini_file)
|
||||
svc_cfg = cfg.deckies[0].service_config
|
||||
assert svc_cfg["ssh"]["users"] == "root:toor"
|
||||
assert svc_cfg["http"]["server_header"] == "nginx/1.18.0"
|
||||
assert svc_cfg["http"]["fake_app"] == "wordpress"
|
||||
|
||||
|
||||
def test_subsection_for_unknown_decky_is_ignored(tmp_path):
|
||||
ini_file = _write_ini(tmp_path, """
|
||||
[decky-01]
|
||||
services = ssh
|
||||
|
||||
[ghost.ssh]
|
||||
kernel_version = 5.15.0
|
||||
""")
|
||||
cfg = load_ini(ini_file)
|
||||
# ghost.ssh must not create a new decky or error out
|
||||
assert len(cfg.deckies) == 1
|
||||
assert cfg.deckies[0].name == "decky-01"
|
||||
assert cfg.deckies[0].service_config == {}
|
||||
|
||||
|
||||
def test_plain_decky_without_subsections_has_empty_service_config(tmp_path):
|
||||
ini_file = _write_ini(tmp_path, """
|
||||
[decky-01]
|
||||
services = http
|
||||
""")
|
||||
cfg = load_ini(ini_file)
|
||||
assert cfg.deckies[0].service_config == {}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Bring-your-own service (BYOS) parsing
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_custom_service_parsed(tmp_path):
|
||||
ini_file = _write_ini(tmp_path, """
|
||||
[general]
|
||||
net = 10.0.0.0/24
|
||||
gw = 10.0.0.1
|
||||
|
||||
[custom-myservice]
|
||||
binary = my-image:latest
|
||||
exec = /usr/bin/myapp -p 8080
|
||||
ports = 8080
|
||||
""")
|
||||
cfg = load_ini(ini_file)
|
||||
assert len(cfg.custom_services) == 1
|
||||
cs = cfg.custom_services[0]
|
||||
assert cs.name == "myservice"
|
||||
assert cs.image == "my-image:latest"
|
||||
assert cs.exec_cmd == "/usr/bin/myapp -p 8080"
|
||||
assert cs.ports == [8080]
|
||||
|
||||
|
||||
def test_custom_service_without_ports(tmp_path):
|
||||
ini_file = _write_ini(tmp_path, """
|
||||
[custom-scanner]
|
||||
binary = scanner:1.0
|
||||
exec = /usr/bin/scanner
|
||||
""")
|
||||
cfg = load_ini(ini_file)
|
||||
assert cfg.custom_services[0].ports == []
|
||||
|
||||
|
||||
def test_custom_service_not_added_to_deckies(tmp_path):
|
||||
ini_file = _write_ini(tmp_path, """
|
||||
[decky-01]
|
||||
services = ssh
|
||||
|
||||
[custom-myservice]
|
||||
binary = foo:bar
|
||||
exec = /bin/foo
|
||||
""")
|
||||
cfg = load_ini(ini_file)
|
||||
assert len(cfg.deckies) == 1
|
||||
assert cfg.deckies[0].name == "decky-01"
|
||||
assert len(cfg.custom_services) == 1
|
||||
|
||||
|
||||
def test_no_custom_services_gives_empty_list(tmp_path):
|
||||
ini_file = _write_ini(tmp_path, """
|
||||
[decky-01]
|
||||
services = http
|
||||
""")
|
||||
cfg = load_ini(ini_file)
|
||||
assert cfg.custom_services == []
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# nmap_os parsing
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_nmap_os_parsed_from_ini(tmp_path):
|
||||
ini_file = _write_ini(tmp_path, """
|
||||
[decky-win]
|
||||
ip = 192.168.1.101
|
||||
services = rdp, smb
|
||||
nmap_os = windows
|
||||
""")
|
||||
cfg = load_ini(ini_file)
|
||||
assert cfg.deckies[0].nmap_os == "windows"
|
||||
|
||||
|
||||
def test_nmap_os_defaults_to_none_when_absent(tmp_path):
|
||||
ini_file = _write_ini(tmp_path, """
|
||||
[decky-01]
|
||||
services = ssh
|
||||
""")
|
||||
cfg = load_ini(ini_file)
|
||||
assert cfg.deckies[0].nmap_os is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize("os_family", ["linux", "windows", "bsd", "embedded", "cisco"])
|
||||
def test_nmap_os_all_families_accepted(tmp_path, os_family):
|
||||
ini_file = _write_ini(tmp_path, f"""
|
||||
[decky-01]
|
||||
services = ssh
|
||||
nmap_os = {os_family}
|
||||
""")
|
||||
cfg = load_ini(ini_file)
|
||||
assert cfg.deckies[0].nmap_os == os_family
|
||||
|
||||
|
||||
def test_nmap_os_propagates_to_amount_expanded_deckies(tmp_path):
|
||||
ini_file = _write_ini(tmp_path, """
|
||||
[corp-printers]
|
||||
services = snmp
|
||||
nmap_os = embedded
|
||||
amount = 3
|
||||
""")
|
||||
cfg = load_ini(ini_file)
|
||||
assert len(cfg.deckies) == 3
|
||||
for d in cfg.deckies:
|
||||
assert d.nmap_os == "embedded"
|
||||
|
||||
|
||||
def test_nmap_os_hyphen_alias_accepted(tmp_path):
|
||||
"""nmap-os= (hyphen) should work as an alias for nmap_os=."""
|
||||
ini_file = _write_ini(tmp_path, """
|
||||
[decky-01]
|
||||
services = ssh
|
||||
nmap-os = bsd
|
||||
""")
|
||||
cfg = load_ini(ini_file)
|
||||
assert cfg.deckies[0].nmap_os == "bsd"
|
||||
27
tests/config/test_ini_spaces.py
Normal file
27
tests/config/test_ini_spaces.py
Normal file
@@ -0,0 +1,27 @@
|
||||
from decnet.ini_loader import load_ini_from_string
|
||||
|
||||
def test_load_ini_with_spaces_around_equals():
|
||||
content = """
|
||||
[general]
|
||||
interface = eth0
|
||||
|
||||
[omega-decky]
|
||||
services = http, ssh
|
||||
"""
|
||||
cfg = load_ini_from_string(content)
|
||||
assert cfg.interface == "eth0"
|
||||
assert len(cfg.deckies) == 1
|
||||
assert cfg.deckies[0].name == "omega-decky"
|
||||
assert cfg.deckies[0].services == ["http", "ssh"]
|
||||
|
||||
def test_load_ini_with_tabs_and_spaces():
|
||||
content = """
|
||||
[general]
|
||||
interface = eth0
|
||||
|
||||
[omega-decky]
|
||||
services = http, ssh
|
||||
"""
|
||||
cfg = load_ini_from_string(content)
|
||||
assert cfg.interface == "eth0"
|
||||
assert cfg.deckies[0].services == ["http", "ssh"]
|
||||
41
tests/config/test_ini_validation.py
Normal file
41
tests/config/test_ini_validation.py
Normal file
@@ -0,0 +1,41 @@
|
||||
import pytest
|
||||
from decnet.ini_loader import load_ini_from_string, validate_ini_string
|
||||
|
||||
def test_validate_ini_string_too_large():
|
||||
content = "[" + "a" * (512 * 1024 + 1) + "]"
|
||||
with pytest.raises(ValueError, match="too large"):
|
||||
validate_ini_string(content)
|
||||
|
||||
def test_validate_ini_string_empty():
|
||||
with pytest.raises(ValueError, match="is empty"):
|
||||
validate_ini_string("")
|
||||
with pytest.raises(ValueError, match="is empty"):
|
||||
validate_ini_string(" ")
|
||||
|
||||
def test_validate_ini_string_no_sections():
|
||||
with pytest.raises(ValueError, match="no sections found"):
|
||||
validate_ini_string("key=value")
|
||||
|
||||
def test_load_ini_from_string_amount_limit():
|
||||
content = """
|
||||
[general]
|
||||
net=192.168.1.0/24
|
||||
|
||||
[decky-01]
|
||||
amount=101
|
||||
archetype=linux-server
|
||||
"""
|
||||
with pytest.raises(ValueError, match="exceeds maximum allowed"):
|
||||
load_ini_from_string(content)
|
||||
|
||||
def test_load_ini_from_string_valid():
|
||||
content = """
|
||||
[general]
|
||||
net=192.168.1.0/24
|
||||
|
||||
[decky-01]
|
||||
amount=5
|
||||
archetype=linux-server
|
||||
"""
|
||||
cfg = load_ini_from_string(content)
|
||||
assert len(cfg.deckies) == 5
|
||||
Reference in New Issue
Block a user