Make distributable: config.toml support, blanked Matrix defaults, env creds fallback

- ~/.config/coe-mirror/config.toml (tomllib, Python 3.11+ required)
- Precedence uniform for every key: env var > config file > hardcoded default
- API_BASE, OWNERS, PCLOUD_TARGET, PENDING_DIR, matrix homeserver/room resolved
  via _resolve() helper
- Matrix homeserver/room now default to empty; _send_matrix short-circuits when
  either is blank so forks don't spam the original author's room
- _fetch_creds falls back to COE_MIRROR_MATRIX_USER/PASSWORD env vars when the
  Infisical creds binary is unavailable (required for external distribution)
- CLI subcommands: config init (writes example TOML, won't overwrite), config path
  Bare invocation and 'run' still work unchanged for the systemd timer
- README rewritten as a public quick-start with the precedence table and Matrix
  as opt-in
- Tests: 3 new for config precedence, 1 for Matrix silent no-op when blank,
  2 for creds env fallback (26 total, all pass)

DevTrack #1285
This commit is contained in:
n0mad1k
2026-07-07 11:04:07 -04:00
parent 9ffe38cbbc
commit 376115a9de
4 changed files with 357 additions and 75 deletions
+105 -9
View File
@@ -42,6 +42,8 @@ def mock_paths(tmp_path, monkeypatch):
monkeypatch.setattr(coe_mirror, "LOG_DIR", log_dir)
monkeypatch.setattr(coe_mirror, "LOG_FILE", log_dir / "test.log")
monkeypatch.setattr(coe_mirror, "OWNERS", ["TestOwner"])
monkeypatch.setattr(coe_mirror, "MATRIX_HOMESERVER", "https://matrix.test.local")
monkeypatch.setattr(coe_mirror, "MATRIX_ROOM", "!testroom:test.local")
return {
"pcloud_target": pcloud_target,
@@ -625,8 +627,8 @@ class TestMatrixSendSuccess:
# Mock send endpoint to return 200
with requests_mock.Mocker() as m:
m.post(
"https://m.example.org/_matrix/client/r0/rooms/"
"%21REDACTEDROOM%3Am.example.org/send/m.room.message",
f"{coe_mirror.MATRIX_HOMESERVER}/_matrix/client/r0/rooms/"
"%21testroom%3Atest.local/send/m.room.message",
json={"event_id": "$event1"},
status_code=200
)
@@ -662,8 +664,8 @@ class TestMatrixSendFailureNonfatal:
with requests_mock.Mocker() as m:
m.post(
"https://m.example.org/_matrix/client/r0/rooms/"
"%21REDACTEDROOM%3Am.example.org/send/m.room.message",
f"{coe_mirror.MATRIX_HOMESERVER}/_matrix/client/r0/rooms/"
"%21testroom%3Atest.local/send/m.room.message",
status_code=500,
text="Internal Server Error"
)
@@ -714,8 +716,8 @@ class TestMatrixTokenRefreshOn401:
with requests_mock.Mocker() as m:
# First send returns 401
m.post(
"https://m.example.org/_matrix/client/r0/rooms/"
"%21REDACTEDROOM%3Am.example.org/send/m.room.message",
f"{coe_mirror.MATRIX_HOMESERVER}/_matrix/client/r0/rooms/"
"%21testroom%3Atest.local/send/m.room.message",
[
{"status_code": 401, "text": "Unauthorized"},
{"status_code": 200, "json": {"event_id": "$event1"}}
@@ -724,7 +726,7 @@ class TestMatrixTokenRefreshOn401:
# Login endpoint
m.post(
"https://m.example.org/_matrix/client/r0/login",
f"{coe_mirror.MATRIX_HOMESERVER}/_matrix/client/r0/login",
json={"access_token": "new-token", "expires_in_ms": 604800000}
)
@@ -765,7 +767,7 @@ class TestMatrixRoomEnvOverride:
with requests_mock.Mocker() as m:
m.post(
"https://m.example.org/_matrix/client/r0/rooms/"
f"{coe_mirror.MATRIX_HOMESERVER}/_matrix/client/r0/rooms/"
"%21override%3Aexample.org/send/m.room.message",
json={"event_id": "$event1"},
status_code=200
@@ -806,7 +808,7 @@ class TestMatrixLoginWritesCacheChmod600:
with requests_mock.Mocker() as m:
m.post(
"https://m.example.org/_matrix/client/r0/login",
f"{coe_mirror.MATRIX_HOMESERVER}/_matrix/client/r0/login",
json={"access_token": "new-token", "expires_in_ms": 604800000}
)
@@ -913,3 +915,97 @@ class TestMultiOwner:
assert captured["failed_owners"] == ["OwnerA"]
# summary contains only the OwnerB entry
assert any(owner == "OwnerB" and repo == "onlyone" for owner, repo, *_ in captured["summary"])
class TestConfigLoading:
"""Config precedence: env var > config.toml > hardcoded default."""
def test_config_file_populates_owners_and_pcloud(self, tmp_path, monkeypatch):
"""When config.toml exists and no env override, its values load."""
cfg = tmp_path / "config.toml"
cfg.write_text(
'api_base = "https://forgejo.example.org"\n'
'pcloud_target = "/tmp/coe-mirror-cfg-test"\n'
'owners = ["Alice", "Bob"]\n'
)
# Clear any env overrides
for k in ("COE_MIRROR_API_BASE", "COE_MIRROR_OWNERS", "COE_MIRROR_PCLOUD_TARGET"):
monkeypatch.delenv(k, raising=False)
monkeypatch.setenv("COE_MIRROR_CONFIG", str(cfg))
# Re-import to re-run module-level config resolution
import importlib, coe_mirror as cm
importlib.reload(cm)
try:
assert cm.API_BASE == "https://forgejo.example.org"
assert cm.OWNERS == ["Alice", "Bob"]
assert cm.PCLOUD_TARGET == Path("/tmp/coe-mirror-cfg-test")
finally:
monkeypatch.delenv("COE_MIRROR_CONFIG", raising=False)
importlib.reload(cm)
def test_missing_config_falls_back_to_defaults(self, tmp_path, monkeypatch):
"""No config file + no env overrides → hardcoded defaults."""
missing = tmp_path / "does-not-exist.toml"
for k in ("COE_MIRROR_API_BASE", "COE_MIRROR_OWNERS", "COE_MIRROR_PCLOUD_TARGET",
"COE_MIRROR_MATRIX_HOMESERVER", "COE_MIRROR_MATRIX_ROOM"):
monkeypatch.delenv(k, raising=False)
monkeypatch.setenv("COE_MIRROR_CONFIG", str(missing))
import importlib, coe_mirror as cm
importlib.reload(cm)
try:
assert cm.API_BASE == "https://git.churchofmalware.org"
assert "Nightmare_Eclipse" in cm.OWNERS
# Matrix defaults must be blank so notify no-ops on fresh checkouts
assert cm.MATRIX_HOMESERVER == ""
assert cm.MATRIX_ROOM == ""
finally:
monkeypatch.delenv("COE_MIRROR_CONFIG", raising=False)
importlib.reload(cm)
def test_env_var_beats_config_file(self, tmp_path, monkeypatch):
"""When both env var and config file are set, env wins."""
cfg = tmp_path / "config.toml"
cfg.write_text('owners = ["FromConfig"]\napi_base = "https://from-config.example"\n')
monkeypatch.setenv("COE_MIRROR_CONFIG", str(cfg))
monkeypatch.setenv("COE_MIRROR_OWNERS", "FromEnvA,FromEnvB")
monkeypatch.setenv("COE_MIRROR_API_BASE", "https://from-env.example")
import importlib, coe_mirror as cm
importlib.reload(cm)
try:
assert cm.OWNERS == ["FromEnvA", "FromEnvB"]
assert cm.API_BASE == "https://from-env.example"
finally:
for k in ("COE_MIRROR_CONFIG", "COE_MIRROR_OWNERS", "COE_MIRROR_API_BASE"):
monkeypatch.delenv(k, raising=False)
importlib.reload(cm)
class TestMatrixSilentWhenUnconfigured:
"""_send_matrix must no-op silently when homeserver/room are blank."""
def test_send_matrix_returns_false_when_blank(self, mock_paths, setup_logging, monkeypatch):
monkeypatch.setattr(coe_mirror, "MATRIX_HOMESERVER", "")
monkeypatch.setattr(coe_mirror, "MATRIX_ROOM", "")
# If this hits the network it will fail loudly; the point is it must NOT hit the network
assert coe_mirror._send_matrix("hello") is False
class TestCredsEnvFallback:
"""`_fetch_creds` falls through to env vars when `creds` binary is unavailable."""
def test_env_fallback_when_creds_missing(self, setup_logging, monkeypatch):
# Force `creds` subprocess to raise FileNotFoundError
def raise_fnf(*args, **kwargs):
raise FileNotFoundError("no creds binary")
monkeypatch.setattr(coe_mirror.subprocess, "run", raise_fnf)
monkeypatch.setenv("COE_MIRROR_MATRIX_USER", "@mirror:example.org")
monkeypatch.setenv("COE_MIRROR_MATRIX_PASSWORD", "s3cret")
assert coe_mirror._fetch_creds() == ("@mirror:example.org", "s3cret")
def test_returns_none_when_both_unavailable(self, setup_logging, monkeypatch):
def raise_fnf(*args, **kwargs):
raise FileNotFoundError("no creds binary")
monkeypatch.setattr(coe_mirror.subprocess, "run", raise_fnf)
monkeypatch.delenv("COE_MIRROR_MATRIX_USER", raising=False)
monkeypatch.delenv("COE_MIRROR_MATRIX_PASSWORD", raising=False)
assert coe_mirror._fetch_creds() is None