"""Tests for coe_mirror.py Lock test (test_lock_contention_exits_zero) may have ordering dependencies and should run in isolation if flaky. Other tests are independent. """ import io import logging import os import shutil import sys import threading import time import zipfile from fcntl import flock, LOCK_EX, LOCK_NB from pathlib import Path from unittest.mock import patch, MagicMock import pytest import requests import requests_mock # Import the module under test import coe_mirror @pytest.fixture def mock_paths(tmp_path, monkeypatch): """Redirect all paths to tmp_path for test isolation.""" pcloud_target = tmp_path / "pcloud" pending_dir = tmp_path / "pending" log_dir = tmp_path / "logs" pcloud_target.mkdir() (pcloud_target / "TestOwner").mkdir() pending_dir.mkdir() log_dir.mkdir() monkeypatch.setattr(coe_mirror, "PCLOUD_TARGET", pcloud_target) monkeypatch.setattr(coe_mirror, "PENDING_DIR", pending_dir) monkeypatch.setattr(coe_mirror, "LOG_DIR", log_dir) monkeypatch.setattr(coe_mirror, "LOG_FILE", log_dir / "test.log") monkeypatch.setattr(coe_mirror, "OWNERS", ["TestOwner"]) return { "pcloud_target": pcloud_target, "pending_dir": pending_dir, "log_dir": log_dir, } @pytest.fixture def setup_logging(mock_paths, monkeypatch): """Set up logging for tests.""" coe_mirror.setup_logging() yield # Clean up logger logger = logging.getLogger("coe-mirror") for handler in logger.handlers[:]: handler.close() logger.removeHandler(handler) def create_valid_zip(path: Path, content: str = "test") -> None: """Helper to create a valid zip file.""" with zipfile.ZipFile(path, "w") as zf: zf.writestr("README.md", content) @pytest.fixture def mock_lock(monkeypatch): """Mock locking to avoid actual file locks during tests.""" # For most tests, just return a valid fd original_open = os.open def mock_open(path, flags, mode=0o777): # Return a dummy fd that won't conflict return original_open(path, flags, mode) monkeypatch.setattr(os, "open", mock_open) class TestNewRepoDownloadsAndPlacesInPcloud: """Test new repo downloads and places in pCloud.""" def test_new_repo_downloads_and_places_in_pcloud(self, mock_paths, setup_logging, monkeypatch): """Mock list returning one repo, assert file lands in pCloud with expected name.""" monkeypatch.setattr(coe_mirror, "acquire_lock", lambda: 999) monkeypatch.setattr(coe_mirror, "release_lock", lambda fd: None) monkeypatch.setattr(time, "sleep", lambda x: None) # Skip actual sleep # Mock pCloud probe to return healthy monkeypatch.setattr(coe_mirror, "probe_pcloud", lambda: True) with requests_mock.Mocker() as m: # Mock repo list m.get( f"{coe_mirror.API_BASE}/api/v1/users/TestOwner/repos", json=[{"name": "test-repo", "default_branch": "main"}], ) # Mock branch info m.get( f"{coe_mirror.API_BASE}/api/v1/repos/TestOwner/test-repo/branches/main", json={"commit": {"id": "aaaabbbbccccddddeeeeffffgggghhhhiiiijjjj"}}, ) # Create valid zip zip_buffer = io.BytesIO() with zipfile.ZipFile(zip_buffer, "w") as zf: zf.writestr("README.md", "test") zip_buffer.seek(0) m.get( f"{coe_mirror.API_BASE}/TestOwner/test-repo/archive/aaaabbbbccccddddeeeeffffgggghhhhiiiijjjj.zip", content=zip_buffer.getvalue(), ) result = coe_mirror.main() assert result == 0 # Verify file landed in pCloud with expected name expected_file = mock_paths["pcloud_target"] / "TestOwner" / "test-repo_aaaabbbb.zip" assert expected_file.exists() assert expected_file.stat().st_size > 0 class TestNewCommitCreatesSnapshot: """Test that new commit on existing repo creates new snapshot.""" def test_new_commit_on_existing_repo_creates_new_snapshot(self, mock_paths, setup_logging, monkeypatch): """Pre-create old snapshot, fetch new SHA, assert new file lands and old untouched.""" monkeypatch.setattr(coe_mirror, "acquire_lock", lambda: 999) monkeypatch.setattr(coe_mirror, "release_lock", lambda fd: None) monkeypatch.setattr(time, "sleep", lambda x: None) # Mock pCloud probe to return healthy monkeypatch.setattr(coe_mirror, "probe_pcloud", lambda: True) # Pre-create old file old_file = mock_paths["pcloud_target"] / "TestOwner" / "test-repo_aaaabbbb.zip" create_valid_zip(old_file, "old") with requests_mock.Mocker() as m: m.get( f"{coe_mirror.API_BASE}/api/v1/users/TestOwner/repos", json=[{"name": "test-repo", "default_branch": "main"}], ) # Return new SHA m.get( f"{coe_mirror.API_BASE}/api/v1/repos/TestOwner/test-repo/branches/main", json={"commit": {"id": "bbbbccccddddeeeeffffgggghhhhiiiijjjjkkkk"}}, ) # Create valid zip zip_buffer = io.BytesIO() with zipfile.ZipFile(zip_buffer, "w") as zf: zf.writestr("README.md", "new") zip_buffer.seek(0) m.get( f"{coe_mirror.API_BASE}/TestOwner/test-repo/archive/bbbbccccddddeeeeffffgggghhhhiiiijjjjkkkk.zip", content=zip_buffer.getvalue(), ) result = coe_mirror.main() assert result == 0 # Verify old file still exists assert old_file.exists() # Verify new file landed new_file = mock_paths["pcloud_target"] / "TestOwner" / "test-repo_bbbbcccc.zip" assert new_file.exists() class TestPcloudFailureKeepsPending: """Test pCloud write failure keeps file in pending with WARN log.""" def test_pcloud_write_fails_keeps_in_pending(self, mock_paths, setup_logging, monkeypatch): """Make pCloud target non-writable, assert download stays in pending with WARN log.""" monkeypatch.setattr(coe_mirror, "acquire_lock", lambda: 999) monkeypatch.setattr(coe_mirror, "release_lock", lambda fd: None) monkeypatch.setattr(time, "sleep", lambda x: None) # Make pCloud target a file (not a dir) to cause write failure pcloud_as_file = mock_paths["pcloud_target"] shutil.rmtree(pcloud_as_file) pcloud_as_file.write_text("block") # Mock place_into_pcloud to fail def mock_place(src, dst): return False monkeypatch.setattr(coe_mirror, "place_into_pcloud", mock_place) with requests_mock.Mocker() as m: m.get( f"{coe_mirror.API_BASE}/api/v1/users/TestOwner/repos", json=[{"name": "test-repo", "default_branch": "main"}], ) m.get( f"{coe_mirror.API_BASE}/api/v1/repos/TestOwner/test-repo/branches/main", json={"commit": {"id": "aaaabbbbccccddddeeeeffffgggghhhhiiiijjjj"}}, ) zip_buffer = io.BytesIO() with zipfile.ZipFile(zip_buffer, "w") as zf: zf.writestr("README.md", "test") zip_buffer.seek(0) m.get( f"{coe_mirror.API_BASE}/TestOwner/test-repo/archive/aaaabbbbccccddddeeeeffffgggghhhhiiiijjjj.zip", content=zip_buffer.getvalue(), ) result = coe_mirror.main() assert result == 0 # Verify file is in pending pending_file = mock_paths["pending_dir"] / "TestOwner__test-repo_aaaabbbb.zip" assert pending_file.exists() class TestPendingFlushesWhenPcloudRecoveres: """Test pending files flush when pCloud recovers.""" def test_pending_flushes_when_pcloud_recovers(self, mock_paths, setup_logging, monkeypatch): """Drop valid zip in pending, make pCloud healthy, assert file lands in pCloud.""" monkeypatch.setattr(coe_mirror, "acquire_lock", lambda: 999) monkeypatch.setattr(coe_mirror, "release_lock", lambda fd: None) monkeypatch.setattr(time, "sleep", lambda x: None) # Mock pCloud probe to return healthy monkeypatch.setattr(coe_mirror, "probe_pcloud", lambda: True) # Create a pending file pending_file = mock_paths["pending_dir"] / "TestOwner__test-repo_aaaabbbb.zip" create_valid_zip(pending_file, "pending") with requests_mock.Mocker() as m: # Empty repo list (no new downloads) m.get( f"{coe_mirror.API_BASE}/api/v1/users/TestOwner/repos", json=[], ) result = coe_mirror.main() assert result == 0 # Verify file moved to pCloud pcloud_file = mock_paths["pcloud_target"] / "TestOwner" / "test-repo_aaaabbbb.zip" assert pcloud_file.exists() assert not pending_file.exists() class TestMalformedZipRejected: """Test that malformed zip is rejected and not placed.""" def test_malformed_zip_rejected_and_not_placed(self, mock_paths, setup_logging, monkeypatch): """Mock zip download returning garbage, assert nothing lands in pCloud.""" monkeypatch.setattr(coe_mirror, "acquire_lock", lambda: 999) monkeypatch.setattr(coe_mirror, "release_lock", lambda fd: None) monkeypatch.setattr(time, "sleep", lambda x: None) with requests_mock.Mocker() as m: m.get( f"{coe_mirror.API_BASE}/api/v1/users/TestOwner/repos", json=[{"name": "test-repo", "default_branch": "main"}], ) m.get( f"{coe_mirror.API_BASE}/api/v1/repos/TestOwner/test-repo/branches/main", json={"commit": {"id": "aaaabbbbccccddddeeeeffffgggghhhhiiiijjjj"}}, ) # Return binary garbage m.get( f"{coe_mirror.API_BASE}/TestOwner/test-repo/archive/aaaabbbbccccddddeeeeffffgggghhhhiiiijjjj.zip", content=b"GARBAGE_DATA_NOT_A_ZIP", ) result = coe_mirror.main() assert result == 0 # Verify nothing in pCloud or pending pcloud_file = mock_paths["pcloud_target"] / "TestOwner" / "test-repo_aaaabbbb.zip" assert not pcloud_file.exists() pending_file = mock_paths["pending_dir"] / "TestOwner__test-repo_aaaabbbb.zip" assert not pending_file.exists() class TestNotifyStub: """Test notify stub is no-op.""" def test_notify_stub_no_op(self, setup_logging, monkeypatch): """Assert notify function does not call network code.""" # Spy on notify original_notify = coe_mirror.notify call_log = [] def spy_notify(summary, failed_owners=None): call_log.append(summary) return original_notify(summary, failed_owners=failed_owners) monkeypatch.setattr(coe_mirror, "notify", spy_notify) # Test with no files coe_mirror.notify([]) assert len(call_log) == 1 assert call_log[0] == [] # Test with files call_log.clear() coe_mirror.notify([("TestOwner", "repo", "aaaabbbb", "/path")]) assert len(call_log) == 1 class TestApiRetriesOn429: """Test API retries on 429 with backoff.""" def test_api_retries_on_429(self, mock_paths, setup_logging, monkeypatch): """Mock 429 twice then 200, assert success after retries.""" monkeypatch.setattr(coe_mirror, "acquire_lock", lambda: 999) monkeypatch.setattr(coe_mirror, "release_lock", lambda fd: None) monkeypatch.setattr(time, "sleep", lambda x: None) # Skip actual sleep attempt_log = [] def track_attempts(status): def callback(req, ctx): attempt_log.append(status) ctx.status_code = status if status == 200: return [{"name": "test-repo", "default_branch": "main"}] return None return callback with requests_mock.Mocker() as m: m.get( f"{coe_mirror.API_BASE}/api/v1/users/TestOwner/repos", [ {"status_code": 429, "json": None}, {"status_code": 429, "json": None}, {"json": [{"name": "test-repo", "default_branch": "main"}]}, ], ) m.get( f"{coe_mirror.API_BASE}/api/v1/repos/TestOwner/test-repo/branches/main", json={"commit": {"id": "aaaabbbbccccddddeeeeffffgggghhhhiiiijjjj"}}, ) zip_buffer = io.BytesIO() with zipfile.ZipFile(zip_buffer, "w") as zf: zf.writestr("README.md", "test") zip_buffer.seek(0) m.get( f"{coe_mirror.API_BASE}/TestOwner/test-repo/archive/aaaabbbbccccddddeeeeffffgggghhhhiiiijjjj.zip", content=zip_buffer.getvalue(), ) result = coe_mirror.main() assert result == 0 class TestSizeMismatchKeepsPending: """Test size mismatch during placement keeps file in pending.""" def test_size_mismatch_keeps_pending_unlinks_bad_dst(self, mock_paths, setup_logging, monkeypatch): """Mock copy to write short file, assert bad dst removed and src in pending.""" monkeypatch.setattr(coe_mirror, "acquire_lock", lambda: 999) monkeypatch.setattr(coe_mirror, "release_lock", lambda fd: None) monkeypatch.setattr(time, "sleep", lambda x: None) # Mock shutil.copy2 to write a short file def mock_copy2(src, dst): # Write dst but make it smaller than src with open(dst, "wb") as f: f.write(b"SHORT") monkeypatch.setattr(shutil, "copy2", mock_copy2) with requests_mock.Mocker() as m: m.get( f"{coe_mirror.API_BASE}/api/v1/users/TestOwner/repos", json=[{"name": "test-repo", "default_branch": "main"}], ) m.get( f"{coe_mirror.API_BASE}/api/v1/repos/TestOwner/test-repo/branches/main", json={"commit": {"id": "aaaabbbbccccddddeeeeffffgggghhhhiiiijjjj"}}, ) zip_buffer = io.BytesIO() with zipfile.ZipFile(zip_buffer, "w") as zf: zf.writestr("README.md", "test content here is longer") zip_buffer.seek(0) m.get( f"{coe_mirror.API_BASE}/TestOwner/test-repo/archive/aaaabbbbccccddddeeeeffffgggghhhhiiiijjjj.zip", content=zip_buffer.getvalue(), ) result = coe_mirror.main() assert result == 0 # Verify bad dst was removed bad_dst = mock_paths["pcloud_target"] / "TestOwner" / "test-repo_aaaabbbb.zip" assert not bad_dst.exists() # Verify src still in pending pending_src = mock_paths["pending_dir"] / "TestOwner__test-repo_aaaabbbb.zip" assert pending_src.exists() class TestExistenceCheckSkipsZeroByte: """Test that zero-byte file in pCloud target causes re-download.""" def test_existence_check_skips_when_pcloud_has_zero_byte_file(self, mock_paths, setup_logging, monkeypatch): """Pre-create zero-byte file, assert download still happens because st_size check rejects it.""" monkeypatch.setattr(coe_mirror, "acquire_lock", lambda: 999) monkeypatch.setattr(coe_mirror, "release_lock", lambda fd: None) monkeypatch.setattr(time, "sleep", lambda x: None) # Pre-create zero-byte file in pCloud target zero_file = mock_paths["pcloud_target"] / "TestOwner" / "test-repo_aaaabbbb.zip" zero_file.write_bytes(b"") assert zero_file.stat().st_size == 0 # Mock place_into_pcloud to succeed (make pCloud healthy) def mock_place(src, dst): shutil.copy2(src, dst) src.unlink() return True monkeypatch.setattr(coe_mirror, "place_into_pcloud", mock_place) with requests_mock.Mocker() as m: m.get( f"{coe_mirror.API_BASE}/api/v1/users/TestOwner/repos", json=[{"name": "test-repo", "default_branch": "main"}], ) m.get( f"{coe_mirror.API_BASE}/api/v1/repos/TestOwner/test-repo/branches/main", json={"commit": {"id": "aaaabbbbccccddddeeeeffffgggghhhhiiiijjjj"}}, ) zip_buffer = io.BytesIO() with zipfile.ZipFile(zip_buffer, "w") as zf: zf.writestr("README.md", "real content") zip_buffer.seek(0) m.get( f"{coe_mirror.API_BASE}/TestOwner/test-repo/archive/aaaabbbbccccddddeeeeffffgggghhhhiiiijjjj.zip", content=zip_buffer.getvalue(), ) result = coe_mirror.main() assert result == 0 # Verify zero-byte file was overwritten with real content assert zero_file.exists() assert zero_file.stat().st_size > 0 class TestLockContention: """Test lock contention exits gracefully.""" def test_lock_contention_exits_zero(self, mock_paths, setup_logging, monkeypatch): """Acquire lock externally, assert main() returns 0 without work.""" # Create lock file and acquire it xdg_runtime = os.getenv("XDG_RUNTIME_DIR") lock_dir = Path(xdg_runtime) if xdg_runtime else Path("/tmp") lock_file = lock_dir / coe_mirror.LOCK_NAME with open(lock_file, "w") as f: try: flock(f, LOCK_EX | LOCK_NB) # Lock acquired; now test that main() respects it result = coe_mirror.main() assert result == 0 finally: try: f.close() except: pass # Clean up try: lock_file.unlink() except: pass class TestUnsafeRepoNameRejected: """Test that unsafe repo names are rejected (Fix 1 - path traversal).""" def test_unsafe_repo_name_rejected(self, mock_paths, setup_logging, monkeypatch): """Mock API returning repo with unsafe name ../etc/passwd, assert rejected with WARN/ERROR log.""" monkeypatch.setattr(coe_mirror, "acquire_lock", lambda: 999) monkeypatch.setattr(coe_mirror, "release_lock", lambda fd: None) monkeypatch.setattr(time, "sleep", lambda x: None) # Mock pCloud probe to return healthy monkeypatch.setattr(coe_mirror, "probe_pcloud", lambda: True) with requests_mock.Mocker() as m: # Mock repo list returning unsafe repo name m.get( f"{coe_mirror.API_BASE}/api/v1/users/TestOwner/repos", json=[{"name": "../../../etc/passwd", "default_branch": "main"}], ) result = coe_mirror.main() assert result == 0 # Verify no file was written anywhere with traversal path assert not (mock_paths["pcloud_target"] / "etc").exists() assert not (mock_paths["pending_dir"] / "etc").exists() # Also check that no file matching the unsafe name exists unsafe_paths = list(mock_paths["pcloud_target"].glob("*..*.zip")) + list(mock_paths["pending_dir"].glob("*..*.zip")) assert len(unsafe_paths) == 0 class TestSkipWhenFileAlreadyInPcloud: """Test that file already in pCloud is skipped (Fix 3 - SameFileError).""" def test_skip_when_file_already_in_pcloud(self, mock_paths, setup_logging, monkeypatch): """Pre-create non-empty zip in pCloud, assert download_repo_zip not called and no duplicate placed.""" monkeypatch.setattr(coe_mirror, "acquire_lock", lambda: 999) monkeypatch.setattr(coe_mirror, "release_lock", lambda fd: None) monkeypatch.setattr(time, "sleep", lambda x: None) # Mock pCloud probe to return healthy monkeypatch.setattr(coe_mirror, "probe_pcloud", lambda: True) # Pre-create a non-empty file in pCloud existing_file = mock_paths["pcloud_target"] / "TestOwner" / "RepoX_abcdef12.zip" create_valid_zip(existing_file, "existing") # Spy on download_repo_zip to ensure it's not called original_download = coe_mirror.download_repo_zip download_calls = [] def spy_download(session, repo_name, full_sha): download_calls.append((repo_name, full_sha)) return original_download(session, repo_name, full_sha) monkeypatch.setattr(coe_mirror, "download_repo_zip", spy_download) with requests_mock.Mocker() as m: # Mock repo list m.get( f"{coe_mirror.API_BASE}/api/v1/users/TestOwner/repos", json=[{"name": "RepoX", "default_branch": "main"}], ) # Return matching SHA (first 8 chars = abcdef12) m.get( f"{coe_mirror.API_BASE}/api/v1/repos/TestOwner/RepoX/branches/main", json={"commit": {"id": "abcdef1234567890"}}, ) result = coe_mirror.main() assert result == 0 # Assert download_repo_zip was not called (file already exists, skipped at main level) assert len(download_calls) == 0 # Verify existing file is still there and unchanged assert existing_file.exists() assert existing_file.stat().st_size > 0 # Verify new_files does not contain this repo (no notification generated) # This is verified indirectly: notify should be called with empty list or list not containing RepoX # Hard to verify without capturing notify, but the skipped download is the key test class TestPlaceIntoPloudSameFileIsNoop: """Test that place_into_pcloud with same src/dst is a no-op (Fix 3 - belt and suspenders).""" def test_place_into_pcloud_same_file_is_noop(self, mock_paths, setup_logging): """Call place_into_pcloud(p, p) for existing path, assert returns True and file unchanged.""" # Create a test file test_file = mock_paths["pcloud_target"] / "test-file.zip" create_valid_zip(test_file, "content") original_mtime = test_file.stat().st_mtime # Call with same src and dst result = coe_mirror.place_into_pcloud(test_file, test_file) # Assert returns True (success) assert result is True # 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] == coe_mirror.COE_MIRROR_BOT_USER_KEY: return fake_result_user elif cmd[2] == coe_mirror.COE_MIRROR_BOT_PASSWORD_KEY: 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] == coe_mirror.COE_MIRROR_BOT_USER_KEY: return fake_result_user elif cmd[2] == coe_mirror.COE_MIRROR_BOT_PASSWORD_KEY: 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" class TestMultiOwner: """Multi-owner behavior: collisions, per-owner failure isolation.""" def test_same_repo_name_across_owners_does_not_collide(self, mock_paths, setup_logging, monkeypatch): """Two owners each have a repo named 'shared' — both land in their own subdirs.""" monkeypatch.setattr(coe_mirror, "OWNERS", ["OwnerA", "OwnerB"]) (mock_paths["pcloud_target"] / "OwnerA").mkdir(exist_ok=True) (mock_paths["pcloud_target"] / "OwnerB").mkdir(exist_ok=True) monkeypatch.setattr(coe_mirror, "acquire_lock", lambda: 999) monkeypatch.setattr(coe_mirror, "release_lock", lambda fd: None) monkeypatch.setattr(time, "sleep", lambda x: None) buf = io.BytesIO() with zipfile.ZipFile(buf, "w") as zf: zf.writestr("README.md", "hi") zip_bytes = buf.getvalue() sha_a = "a" * 40 sha_b = "b" * 40 with requests_mock.Mocker() as m: for owner, sha in (("OwnerA", sha_a), ("OwnerB", sha_b)): m.get( f"{coe_mirror.API_BASE}/api/v1/users/{owner}/repos", json=[{"name": "shared", "default_branch": "main"}], ) m.get( f"{coe_mirror.API_BASE}/api/v1/repos/{owner}/shared/branches/main", json={"commit": {"id": sha}}, ) m.get( f"{coe_mirror.API_BASE}/{owner}/shared/archive/{sha}.zip", content=zip_bytes, ) rc = coe_mirror.main() assert rc == 0 a_file = mock_paths["pcloud_target"] / "OwnerA" / f"shared_{sha_a[:8]}.zip" b_file = mock_paths["pcloud_target"] / "OwnerB" / f"shared_{sha_b[:8]}.zip" assert a_file.exists() and a_file.stat().st_size > 0 assert b_file.exists() and b_file.stat().st_size > 0 def test_one_owner_failure_does_not_block_others(self, mock_paths, setup_logging, monkeypatch): """OwnerA repo-list fetch 500s; OwnerB still processed and reported healthy.""" monkeypatch.setattr(coe_mirror, "OWNERS", ["OwnerA", "OwnerB"]) (mock_paths["pcloud_target"] / "OwnerA").mkdir(exist_ok=True) (mock_paths["pcloud_target"] / "OwnerB").mkdir(exist_ok=True) monkeypatch.setattr(coe_mirror, "acquire_lock", lambda: 999) monkeypatch.setattr(coe_mirror, "release_lock", lambda fd: None) monkeypatch.setattr(time, "sleep", lambda x: None) # Capture notify args captured = {} def spy_notify(summary, failed_owners=None): captured["summary"] = summary captured["failed_owners"] = failed_owners monkeypatch.setattr(coe_mirror, "notify", spy_notify) buf = io.BytesIO() with zipfile.ZipFile(buf, "w") as zf: zf.writestr("README.md", "hi") zip_bytes = buf.getvalue() sha_b = "b" * 40 with requests_mock.Mocker() as m: m.get(f"{coe_mirror.API_BASE}/api/v1/users/OwnerA/repos", status_code=500) m.get( f"{coe_mirror.API_BASE}/api/v1/users/OwnerB/repos", json=[{"name": "onlyone", "default_branch": "main"}], ) m.get( f"{coe_mirror.API_BASE}/api/v1/repos/OwnerB/onlyone/branches/main", json={"commit": {"id": sha_b}}, ) m.get( f"{coe_mirror.API_BASE}/OwnerB/onlyone/archive/{sha_b}.zip", content=zip_bytes, ) rc = coe_mirror.main() assert rc == 0 b_file = mock_paths["pcloud_target"] / "OwnerB" / f"onlyone_{sha_b[:8]}.zip" assert b_file.exists() assert captured["failed_owners"] == ["OwnerA"] # summary contains only the OwnerB entry assert any(owner == "OwnerB" and repo == "onlyone" for owner, repo, *_ in captured["summary"])