Implement real Matrix notification for coe-mirror

Replace stub notify() with full Synapse integration targeting General room.
- Add _send_matrix(), _get_matrix_token(), _matrix_login() helpers
- Implement token cache with 30-min refresh threshold and 401 retry
- Fetch credentials via Infisical (MATRIX_USER, MATRIX_PASSWORD from matrix/)
- Add MATRIX_* env overrides for testing (homeserver, room, token cache path)
- Non-fatal failures: Matrix outage logs warning, does not break mirror run
- Add 5 comprehensive test cases covering success, failure, 401 refresh, env override, chmod 600
- All 18 tests passing
- Update README and notes.md with implementation details
This commit is contained in:
n0mad1k
2026-06-14 14:36:14 -04:00
parent 98cab71a21
commit c322488869
4 changed files with 417 additions and 9 deletions
+235
View File
@@ -589,3 +589,238 @@ class TestPlaceIntoPloudSameFileIsNoop:
# Assert file still exists and has same content
assert test_file.exists()
assert test_file.stat().st_size > 0
# Tests for Matrix notification implementation.
# Note: Tests requiring monkeypatched subprocess.run must use raising=False because
# subprocess.run is not a class attribute on the coe_mirror module by default
# — patch via monkeypatch.setattr("coe_mirror.subprocess.run", fake_run).
class TestMatrixSendSuccess:
"""Test successful Matrix message send with cached token."""
def test_matrix_send_success(self, mock_paths, setup_logging, monkeypatch):
"""Pre-write valid token, mock send endpoint 200, assert returns True."""
# Pre-write a valid (un-expired) token to cache
token_cache_file = mock_paths["log_dir"] / "matrix-token.json"
import json
from datetime import datetime, timedelta, timezone
future_time = datetime.now(timezone.utc) + timedelta(days=1)
cache_data = {
"access_token": "cached-token-12345",
"expires_at": future_time.isoformat()
}
token_cache_file.parent.mkdir(parents=True, exist_ok=True)
with open(token_cache_file, "w") as f:
json.dump(cache_data, f)
os.chmod(token_cache_file, 0o600)
# Patch MATRIX_TOKEN_CACHE to point to our temp file
monkeypatch.setattr(coe_mirror, "MATRIX_TOKEN_CACHE", token_cache_file)
# Mock send endpoint to return 200
with requests_mock.Mocker() as m:
m.post(
"https://m.mealeyfamily.com/_matrix/client/r0/rooms/"
"%21MBpPtYlQYMcngtpvJt%3Am.mealeyfamily.com/send/m.room.message",
json={"event_id": "$event1"},
status_code=200
)
# Call _send_matrix
result = coe_mirror._send_matrix("hello")
# Assert success
assert result is True
# Verify the request was made
assert m.called
class TestMatrixSendFailureNonfatal:
"""Test that Matrix send failure is non-fatal."""
def test_matrix_send_failure_nonfatal(self, mock_paths, setup_logging, monkeypatch):
"""Pre-write token, mock send 500, assert returns False, no exception."""
token_cache_file = mock_paths["log_dir"] / "matrix-token.json"
import json
from datetime import datetime, timedelta, timezone
future_time = datetime.now(timezone.utc) + timedelta(days=1)
cache_data = {
"access_token": "cached-token-12345",
"expires_at": future_time.isoformat()
}
token_cache_file.parent.mkdir(parents=True, exist_ok=True)
with open(token_cache_file, "w") as f:
json.dump(cache_data, f)
monkeypatch.setattr(coe_mirror, "MATRIX_TOKEN_CACHE", token_cache_file)
with requests_mock.Mocker() as m:
m.post(
"https://m.mealeyfamily.com/_matrix/client/r0/rooms/"
"%21MBpPtYlQYMcngtpvJt%3Am.mealeyfamily.com/send/m.room.message",
status_code=500,
text="Internal Server Error"
)
result = coe_mirror._send_matrix("hello")
assert result is False
class TestMatrixTokenRefreshOn401:
"""Test that 401 on send triggers token refresh and retry."""
def test_matrix_token_refresh_on_401(self, mock_paths, setup_logging, monkeypatch):
"""Pre-write token, mock send 401 then 200, mock login, assert refreshes and succeeds."""
token_cache_file = mock_paths["log_dir"] / "matrix-token.json"
import json
from datetime import datetime, timedelta, timezone
future_time = datetime.now(timezone.utc) + timedelta(days=1)
cache_data = {
"access_token": "old-token",
"expires_at": future_time.isoformat()
}
token_cache_file.parent.mkdir(parents=True, exist_ok=True)
with open(token_cache_file, "w") as f:
json.dump(cache_data, f)
monkeypatch.setattr(coe_mirror, "MATRIX_TOKEN_CACHE", token_cache_file)
# Mock creds to return user/pass
fake_result_user = MagicMock()
fake_result_user.returncode = 0
fake_result_user.stdout = "test_user"
fake_result_pass = MagicMock()
fake_result_pass.returncode = 0
fake_result_pass.stdout = "test_pass"
def mock_subprocess_run(cmd, **kwargs):
if cmd[2] == "MATRIX_USER":
return fake_result_user
elif cmd[2] == "MATRIX_PASSWORD":
return fake_result_pass
return MagicMock(returncode=1, stdout="")
monkeypatch.setattr(coe_mirror.subprocess, "run", mock_subprocess_run)
with requests_mock.Mocker() as m:
# First send returns 401
m.post(
"https://m.mealeyfamily.com/_matrix/client/r0/rooms/"
"%21MBpPtYlQYMcngtpvJt%3Am.mealeyfamily.com/send/m.room.message",
[
{"status_code": 401, "text": "Unauthorized"},
{"status_code": 200, "json": {"event_id": "$event1"}}
]
)
# Login endpoint
m.post(
"https://m.mealeyfamily.com/_matrix/client/r0/login",
json={"access_token": "new-token", "expires_in_ms": 604800000}
)
result = coe_mirror._send_matrix("hello")
# Assert succeeded after refresh
assert result is True
# Verify new token is in cache
with open(token_cache_file) as f:
cached = json.load(f)
assert cached["access_token"] == "new-token"
class TestMatrixRoomEnvOverride:
"""Test that COE_MIRROR_MATRIX_ROOM env var overrides the default room."""
def test_matrix_room_env_override(self, mock_paths, setup_logging, monkeypatch):
"""Set env override, assert send POSTs to override room URL."""
token_cache_file = mock_paths["log_dir"] / "matrix-token.json"
import json
from datetime import datetime, timedelta, timezone
future_time = datetime.now(timezone.utc) + timedelta(days=1)
cache_data = {
"access_token": "test-token",
"expires_at": future_time.isoformat()
}
token_cache_file.parent.mkdir(parents=True, exist_ok=True)
with open(token_cache_file, "w") as f:
json.dump(cache_data, f)
monkeypatch.setattr(coe_mirror, "MATRIX_TOKEN_CACHE", token_cache_file)
# Override the room
override_room = "!override:example.org"
monkeypatch.setattr(coe_mirror, "MATRIX_ROOM", override_room)
with requests_mock.Mocker() as m:
m.post(
"https://m.mealeyfamily.com/_matrix/client/r0/rooms/"
"%21override%3Aexample.org/send/m.room.message",
json={"event_id": "$event1"},
status_code=200
)
result = coe_mirror._send_matrix("hello")
assert result is True
assert m.called
class TestMatrixLoginWritesCacheChmod600:
"""Test that _matrix_login writes cache file with 0600 permissions."""
def test_matrix_login_writes_cache_chmod_600(self, mock_paths, setup_logging, monkeypatch):
"""Start with no cache, mock creds and login, assert cache exists with 0o600."""
token_cache_file = mock_paths["log_dir"] / "matrix-token.json"
monkeypatch.setattr(coe_mirror, "MATRIX_TOKEN_CACHE", token_cache_file)
# Mock creds to return user/pass
fake_result_user = MagicMock()
fake_result_user.returncode = 0
fake_result_user.stdout = "testuser"
fake_result_pass = MagicMock()
fake_result_pass.returncode = 0
fake_result_pass.stdout = "testpass"
def mock_subprocess_run(cmd, **kwargs):
if cmd[2] == "MATRIX_USER":
return fake_result_user
elif cmd[2] == "MATRIX_PASSWORD":
return fake_result_pass
return MagicMock(returncode=1, stdout="")
monkeypatch.setattr(coe_mirror.subprocess, "run", mock_subprocess_run)
with requests_mock.Mocker() as m:
m.post(
"https://m.mealeyfamily.com/_matrix/client/r0/login",
json={"access_token": "new-token", "expires_in_ms": 604800000}
)
session = requests.Session()
token = coe_mirror._get_matrix_token(session, force_refresh=True)
assert token == "new-token"
assert token_cache_file.exists()
# Check permissions
import stat
mode = stat.S_IMODE(token_cache_file.stat().st_mode)
assert mode == 0o600
# Check content
import json
with open(token_cache_file) as f:
cached = json.load(f)
assert cached["access_token"] == "new-token"