""" Tests for app/celery_worker.py Targets all statements and branches in the Celery worker configuration module, including module-level task imports, beat schedule construction, the conditional IMAP / Uptime Kuma entries, and the None-entry filter. Because celery_worker.py fires ``check_credentials.apply_async()`` at import time (which requires a live Redis broker), every test mocks that call via ``importlib.reload`` so the module can be exercised without external services. """ import importlib from unittest.mock import patch import pytest from app.config import settings def _reload_celery_worker(): """Reload celery_worker with ``apply_async`` mocked to avoid Redis.""" with patch("app.tasks.check_credentials.check_credentials.apply_async"): import app.celery_worker importlib.reload(app.celery_worker) return app.celery_worker @pytest.mark.unit class TestCeleryWorkerConfig: """Test Celery worker configuration (imports, task routes, side effects).""" def test_test_task_function(self): """Test the test_task function returns expected value.""" mod = _reload_celery_worker() result = mod.test_task() assert result == "Celery is working!" def test_celery_instance_exists(self): """Test that celery instance exists in module.""" mod = _reload_celery_worker() assert hasattr(mod, "celery") assert mod.celery is not None def test_task_routes_configured(self): """Test that task routes are set.""" mod = _reload_celery_worker() routes = mod.celery.conf.task_routes assert routes is not None assert "app.tasks.*" in routes def test_all_task_imports_successful(self): """Test that all task modules are imported without errors.""" mod = _reload_celery_worker() assert mod is not None def test_all_task_modules_registered(self): """Test that every task module in app/tasks/ is imported in celery_worker. Dynamically discovers all task files in app/tasks/ and verifies that celery_worker imports from each one. This prevents new task modules from being accidentally left unregistered. """ import ast from pathlib import Path tasks_dir = Path("app/tasks") # Discover all task modules (excluding __init__.py and non-task helpers) task_modules = set() for py_file in sorted(tasks_dir.glob("*.py")): if py_file.name.startswith("_"): continue # Only include modules that define Celery tasks (use @celery.task decorator) source = py_file.read_text() tree = ast.parse(source) for node in ast.walk(tree): if isinstance(node, ast.FunctionDef): for decorator in node.decorator_list: decorator_src = ast.dump(decorator) if "celery" in decorator_src and "task" in decorator_src: task_modules.add(py_file.stem) break assert task_modules, "Should discover at least one task module" # Read celery_worker.py source and collect all 'from app.tasks.X import ...' worker_source = Path("app/celery_worker.py").read_text() worker_tree = ast.parse(worker_source) imported_modules = set() for node in ast.walk(worker_tree): if isinstance(node, ast.ImportFrom) and node.module and node.module.startswith("app.tasks."): # e.g. "app.tasks.convert_to_pdfa" -> "convert_to_pdfa" mod_name = node.module.split(".")[-1] imported_modules.add(mod_name) missing = task_modules - imported_modules assert not missing, ( f"Task modules not imported in celery_worker.py: {sorted(missing)}. " f"Add 'from app.tasks. import # noqa: F401' for each." ) def test_register_settings_reload_signal_called(self): """Test that register_settings_reload_signal is called at module load.""" with ( patch("app.tasks.check_credentials.check_credentials.apply_async"), patch("app.utils.settings_sync.register_settings_reload_signal") as mock_reg, ): import app.celery_worker importlib.reload(app.celery_worker) mock_reg.assert_called_once() def test_check_credentials_apply_async_called(self): """Test that check_credentials.apply_async is called at module load.""" with patch("app.tasks.check_credentials.check_credentials.apply_async") as mock_apply: import app.celery_worker importlib.reload(app.celery_worker) mock_apply.assert_called_once_with(countdown=10) @pytest.mark.unit class TestBeatScheduleConfiguration: """Test Celery beat schedule configuration (lines 56-97).""" def test_beat_schedule_always_present_entries(self): """Test that the three unconditional entries are always present.""" mod = _reload_celery_worker() schedule = mod.celery.conf.beat_schedule assert isinstance(schedule, dict) assert "check-credentials-regularly" in schedule assert "check-credentials-daily" in schedule assert "monitor-stalled-steps" in schedule def test_credential_check_regular_schedule(self): """Test 'check-credentials-regularly' entry.""" mod = _reload_celery_worker() entry = mod.celery.conf.beat_schedule["check-credentials-regularly"] assert entry["task"] == "app.tasks.check_credentials.check_credentials" assert "schedule" in entry assert entry["options"]["expires"] == 240 def test_credential_check_daily_schedule(self): """Test 'check-credentials-daily' entry.""" mod = _reload_celery_worker() entry = mod.celery.conf.beat_schedule["check-credentials-daily"] assert entry["task"] == "app.tasks.check_credentials.check_credentials" assert entry["options"]["expires"] == 3600 def test_monitor_stalled_steps_schedule(self): """Test 'monitor-stalled-steps' entry.""" mod = _reload_celery_worker() entry = mod.celery.conf.beat_schedule["monitor-stalled-steps"] assert entry["task"] == "app.tasks.monitor_stalled_steps.monitor_stalled_steps" assert entry["options"]["expires"] == 55 def test_no_none_entries_in_beat_schedule(self): """Test that None entries are filtered from beat schedule.""" mod = _reload_celery_worker() for key, value in mod.celery.conf.beat_schedule.items(): assert value is not None, f"Beat schedule entry '{key}' should not be None" # -- Conditional schedule entries ---------------------------------------- def test_imap_schedule_absent_when_not_configured(self): """Test IMAP polling absent when neither imap host is set.""" mod = _reload_celery_worker() assert "poll-inboxes-every-minute" not in mod.celery.conf.beat_schedule def test_uptime_kuma_schedule_absent_when_not_configured(self): """Test Uptime Kuma ping absent when url is not set.""" mod = _reload_celery_worker() assert "ping-uptime-kuma" not in mod.celery.conf.beat_schedule def test_imap_schedule_present_when_imap1_configured(self): """Test IMAP polling present when imap1_host is set.""" original = settings.imap1_host try: settings.imap1_host = "imap.example.com" mod = _reload_celery_worker() assert "poll-inboxes-every-minute" in mod.celery.conf.beat_schedule entry = mod.celery.conf.beat_schedule["poll-inboxes-every-minute"] assert entry["task"] == "app.tasks.imap_tasks.pull_all_inboxes" assert entry["options"]["expires"] == 55 finally: settings.imap1_host = original def test_imap_schedule_present_when_imap2_configured(self): """Test IMAP polling present when imap2_host is set.""" original = settings.imap2_host try: settings.imap2_host = "imap2.example.com" mod = _reload_celery_worker() assert "poll-inboxes-every-minute" in mod.celery.conf.beat_schedule finally: settings.imap2_host = original def test_uptime_kuma_schedule_present_when_configured(self): """Test Uptime Kuma ping present when uptime_kuma_url is set.""" orig_url = settings.uptime_kuma_url orig_interval = settings.uptime_kuma_ping_interval try: settings.uptime_kuma_url = "https://uptime.example.com/api/push/abc" settings.uptime_kuma_ping_interval = 3 mod = _reload_celery_worker() assert "ping-uptime-kuma" in mod.celery.conf.beat_schedule entry = mod.celery.conf.beat_schedule["ping-uptime-kuma"] assert entry["task"] == "app.tasks.uptime_kuma_tasks.ping_uptime_kuma" assert entry["options"]["expires"] == 55 finally: settings.uptime_kuma_url = orig_url settings.uptime_kuma_ping_interval = orig_interval