fix(core): audit matches split *_HOST/*_PORT env pairs
The consumption audit only matched host:port inside a single value (a URL, DATABASE_URL), missing the very common split-var shape (X_HOST=localhost + X_PORT=1883, e.g. castle-api's mqtt config). Resolve those pairs too — factored into a shared _resolve helper. A bare *_PORT with no *_HOST is ignored so a deployment's own listen port isn't mistaken for a dependency. Adds test_audit.py.
This commit is contained in:
94
core/tests/test_audit.py
Normal file
94
core/tests/test_audit.py
Normal file
@@ -0,0 +1,94 @@
|
||||
"""Tests for the consumption audit (core/src/castle_core/audit.py)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import castle_core.config as C
|
||||
from castle_core import audit
|
||||
from castle_core.manifest import SystemdDeployment
|
||||
|
||||
|
||||
def _svc(
|
||||
program: str,
|
||||
*,
|
||||
tcp: int | None = None,
|
||||
http: int | None = None,
|
||||
env: dict | None = None,
|
||||
requires: list | None = None,
|
||||
) -> SystemdDeployment:
|
||||
spec: dict = {
|
||||
"manager": "systemd",
|
||||
"program": program,
|
||||
"run": {"launcher": "command", "argv": [program]},
|
||||
}
|
||||
if tcp is not None:
|
||||
spec["expose"] = {"tcp": {"port": tcp}}
|
||||
spec["reach"] = "internal"
|
||||
elif http is not None:
|
||||
spec["expose"] = {"http": {"internal": {"port": http}}}
|
||||
spec["reach"] = "internal"
|
||||
if env is not None:
|
||||
spec["defaults"] = {"env": env}
|
||||
if requires is not None:
|
||||
spec["requires"] = requires
|
||||
return SystemdDeployment.model_validate(spec)
|
||||
|
||||
|
||||
def _cfg(deployments: dict) -> C.CastleConfig:
|
||||
return C.CastleConfig(
|
||||
root=None,
|
||||
gateway=C.GatewayConfig(port=9000),
|
||||
repo=None,
|
||||
programs={},
|
||||
deployments=deployments,
|
||||
)
|
||||
|
||||
|
||||
def _pairs(cfg: C.CastleConfig) -> set[tuple[str, str]]:
|
||||
return {(s.consumer, s.provider) for s in audit.suggest_consumption(cfg)}
|
||||
|
||||
|
||||
def test_split_host_port_pair_is_suggested() -> None:
|
||||
"""A split X_HOST + X_PORT pair resolves like a single host:port value."""
|
||||
cfg = _cfg(
|
||||
{
|
||||
"broker": _svc("broker", tcp=1883),
|
||||
"api": _svc(
|
||||
"api",
|
||||
http=9020,
|
||||
env={"CASTLE_API_MQTT_HOST": "localhost", "CASTLE_API_MQTT_PORT": "1883"},
|
||||
),
|
||||
}
|
||||
)
|
||||
assert ("api", "broker") in _pairs(cfg)
|
||||
|
||||
|
||||
def test_single_value_url_is_suggested() -> None:
|
||||
cfg = _cfg(
|
||||
{
|
||||
"db": _svc("db", tcp=5432),
|
||||
"app": _svc("app", http=9001, env={"DATABASE_URL": "postgresql://u@localhost:5432/x"}),
|
||||
}
|
||||
)
|
||||
assert ("app", "db") in _pairs(cfg)
|
||||
|
||||
|
||||
def test_bare_port_without_host_is_not_matched() -> None:
|
||||
"""A deployment's own listen port (no host) must not become a dependency."""
|
||||
cfg = _cfg({"app": _svc("app", http=9001, env={"APP_PORT": "9001"})})
|
||||
assert _pairs(cfg) == set()
|
||||
|
||||
|
||||
def test_declared_pair_is_not_suggested() -> None:
|
||||
"""Already-declared consumption is not re-suggested."""
|
||||
cfg = _cfg(
|
||||
{
|
||||
"broker": _svc("broker", tcp=1883),
|
||||
"api": _svc(
|
||||
"api",
|
||||
http=9020,
|
||||
env={"X_HOST": "localhost", "X_PORT": "1883"},
|
||||
requires=[{"ref": "broker"}],
|
||||
),
|
||||
}
|
||||
)
|
||||
assert ("api", "broker") not in _pairs(cfg)
|
||||
Reference in New Issue
Block a user