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:
+105
-9
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user