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.
287 lines
8.2 KiB
Python
287 lines
8.2 KiB
Python
"""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"
|