7a0da7932b
OSV-Scanner (Scheduled) / scan-scheduled (push) Failing after 0s
Create Release / test-gate (push) Has been cancelled
Create Release / release-gate (push) Has been cancelled
Create Release / ci-gate (push) Has been cancelled
Create Release / version-check (push) Has been cancelled
Create Release / e2e-test-gate (push) Has been cancelled
Create Release / responsive-test-gate (push) Has been cancelled
Create Release / compat-test-gate (push) Has been cancelled
Create Release / compose-integration-gate (push) Has been cancelled
Create Release / vulture-gate (push) Has been cancelled
Create Release / build (push) Has been cancelled
Create Release / provenance (push) Has been cancelled
Create Release / prerelease-docker (push) Has been cancelled
Create Release / publish-docker (push) Has been cancelled
Create Release / create-release (push) Has been cancelled
Create Release / cleanup-changelog (push) Has been cancelled
Create Release / trigger-pypi (push) Has been cancelled
Create Release / monitor-pypi (push) Has been cancelled
Create Release / Clean up orphan prerelease tags and signatures (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-form] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-metrics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-workflow] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-core] (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [history-news] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [library] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [link-analytics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-core] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-lifecycle] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [error-benchmark] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) (push) Has been cancelled
Docker Tests (Consolidated) / Accessibility Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Unit Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Example Tests (push) Has been cancelled
Docker Tests (Consolidated) / Production Image Smoke Test (push) Has been cancelled
Docker Tests (Consolidated) / Infrastructure Tests (push) Has been cancelled
OSSF Scorecard / OSSF Security Scorecard Analysis (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [mobile] (push) Has been cancelled
Backwards Compatibility / Verify Encryption Constants (push) Has been cancelled
Backwards Compatibility / PyPI Version Compatibility (push) Has been cancelled
Backwards Compatibility / Database Migration Tests (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Docker Tests (Consolidated) / detect-changes (push) Has been cancelled
Docker Tests (Consolidated) / Build Test Image (push) Has been cancelled
Docker Tests (Consolidated) / All Pytest Tests + Coverage (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [accessibility] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [api-crud] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-login] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-register] (push) Has been cancelled
461 lines
16 KiB
Python
461 lines
16 KiB
Python
"""
|
|
Deep behavioral tests for BackgroundJobScheduler user operations.
|
|
Tests update_user_info, unregister_user, start/stop, and singleton management.
|
|
"""
|
|
|
|
from datetime import datetime, UTC
|
|
from unittest.mock import Mock, patch
|
|
|
|
import pytest
|
|
from apscheduler.jobstores.base import JobLookupError
|
|
|
|
from local_deep_research.scheduler.background import (
|
|
DocumentSchedulerSettings,
|
|
BackgroundJobScheduler,
|
|
)
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def reset_scheduler():
|
|
"""Reset the singleton before and after each test."""
|
|
BackgroundJobScheduler._instance = None
|
|
yield
|
|
BackgroundJobScheduler._instance = None
|
|
|
|
|
|
# --- update_user_info ---
|
|
|
|
|
|
class TestUpdateUserInfo:
|
|
"""Tests for update_user_info behavior."""
|
|
|
|
def test_does_nothing_when_not_running(self):
|
|
s = BackgroundJobScheduler()
|
|
s.is_running = False
|
|
s.update_user_info("user1", "pass1")
|
|
assert "user1" not in s.user_sessions
|
|
|
|
def test_creates_new_session(self):
|
|
s = BackgroundJobScheduler()
|
|
s.is_running = True
|
|
with patch.object(s, "_schedule_user_subscriptions"):
|
|
s.update_user_info("user1", "pass1")
|
|
assert "user1" in s.user_sessions
|
|
|
|
def test_stores_password_in_credential_store(self):
|
|
s = BackgroundJobScheduler()
|
|
s.is_running = True
|
|
with patch.object(s, "_schedule_user_subscriptions"):
|
|
s.update_user_info("user1", "mypass")
|
|
assert "password" not in s.user_sessions["user1"]
|
|
assert s._credential_store.retrieve("user1") == "mypass"
|
|
|
|
def test_sets_last_activity(self):
|
|
s = BackgroundJobScheduler()
|
|
s.is_running = True
|
|
with patch.object(s, "_schedule_user_subscriptions"):
|
|
s.update_user_info("user1", "pass1")
|
|
assert "last_activity" in s.user_sessions["user1"]
|
|
assert isinstance(s.user_sessions["user1"]["last_activity"], datetime)
|
|
|
|
def test_initializes_empty_scheduled_jobs(self):
|
|
s = BackgroundJobScheduler()
|
|
s.is_running = True
|
|
with patch.object(s, "_schedule_user_subscriptions"):
|
|
s.update_user_info("user1", "pass1")
|
|
assert s.user_sessions["user1"]["scheduled_jobs"] == set()
|
|
|
|
def test_calls_schedule_user_subscriptions(self):
|
|
s = BackgroundJobScheduler()
|
|
s.is_running = True
|
|
with patch.object(s, "_schedule_user_subscriptions") as mock_sched:
|
|
s.update_user_info("user1", "pass1")
|
|
mock_sched.assert_called_once_with("user1")
|
|
|
|
def test_updates_existing_password(self):
|
|
s = BackgroundJobScheduler()
|
|
s.is_running = True
|
|
with patch.object(s, "_schedule_user_subscriptions"):
|
|
s.update_user_info("user1", "pass1")
|
|
s.update_user_info("user1", "pass2")
|
|
assert s._credential_store.retrieve("user1") == "pass2"
|
|
|
|
def test_updates_last_activity_for_existing(self):
|
|
s = BackgroundJobScheduler()
|
|
s.is_running = True
|
|
with patch.object(s, "_schedule_user_subscriptions"):
|
|
s.update_user_info("user1", "pass1")
|
|
first_activity = s.user_sessions["user1"]["last_activity"]
|
|
s.update_user_info("user1", "pass1")
|
|
second_activity = s.user_sessions["user1"]["last_activity"]
|
|
assert second_activity >= first_activity
|
|
|
|
def test_reschedules_for_existing_user(self):
|
|
s = BackgroundJobScheduler()
|
|
s.is_running = True
|
|
with patch.object(s, "_schedule_user_subscriptions") as mock_sched:
|
|
s.update_user_info("user1", "pass1")
|
|
s.update_user_info("user1", "pass1")
|
|
assert mock_sched.call_count == 2
|
|
|
|
def test_multiple_users(self):
|
|
s = BackgroundJobScheduler()
|
|
s.is_running = True
|
|
with patch.object(s, "_schedule_user_subscriptions"):
|
|
s.update_user_info("user1", "pass1")
|
|
s.update_user_info("user2", "pass2")
|
|
assert "user1" in s.user_sessions
|
|
assert "user2" in s.user_sessions
|
|
|
|
|
|
# --- unregister_user ---
|
|
|
|
|
|
class TestUnregisterUser:
|
|
"""Tests for unregister_user behavior."""
|
|
|
|
def test_removes_from_sessions(self):
|
|
s = BackgroundJobScheduler()
|
|
s.user_sessions["user1"] = {
|
|
"last_activity": datetime.now(UTC),
|
|
"scheduled_jobs": set(),
|
|
}
|
|
s._credential_store.store("user1", "pass")
|
|
s.unregister_user("user1")
|
|
assert "user1" not in s.user_sessions
|
|
|
|
def test_removes_scheduled_jobs(self):
|
|
s = BackgroundJobScheduler()
|
|
mock_scheduler = Mock()
|
|
s.scheduler = mock_scheduler
|
|
s.user_sessions["user1"] = {
|
|
"last_activity": datetime.now(UTC),
|
|
"scheduled_jobs": {"job1", "job2"},
|
|
}
|
|
s._credential_store.store("user1", "pass")
|
|
s.unregister_user("user1")
|
|
assert mock_scheduler.remove_job.call_count == 2
|
|
|
|
def test_handles_job_lookup_error(self):
|
|
s = BackgroundJobScheduler()
|
|
mock_scheduler = Mock()
|
|
mock_scheduler.remove_job.side_effect = JobLookupError("job1")
|
|
s.scheduler = mock_scheduler
|
|
s.user_sessions["user1"] = {
|
|
"last_activity": datetime.now(UTC),
|
|
"scheduled_jobs": {"job1"},
|
|
}
|
|
s._credential_store.store("user1", "pass")
|
|
# Should not raise
|
|
s.unregister_user("user1")
|
|
|
|
def test_invalidates_settings_cache(self):
|
|
s = BackgroundJobScheduler()
|
|
s._settings_cache["user1"] = DocumentSchedulerSettings()
|
|
s.user_sessions["user1"] = {
|
|
"last_activity": datetime.now(UTC),
|
|
"scheduled_jobs": set(),
|
|
}
|
|
s._credential_store.store("user1", "pass")
|
|
s.unregister_user("user1")
|
|
assert "user1" not in s._settings_cache
|
|
|
|
def test_handles_nonexistent_user(self):
|
|
s = BackgroundJobScheduler()
|
|
# Should not raise
|
|
s.unregister_user("nonexistent")
|
|
|
|
def test_does_not_affect_other_users(self):
|
|
s = BackgroundJobScheduler()
|
|
s.user_sessions["user1"] = {
|
|
"last_activity": datetime.now(UTC),
|
|
"scheduled_jobs": set(),
|
|
}
|
|
s._credential_store.store("user1", "pass1")
|
|
s.user_sessions["user2"] = {
|
|
"last_activity": datetime.now(UTC),
|
|
"scheduled_jobs": set(),
|
|
}
|
|
s._credential_store.store("user2", "pass2")
|
|
s.unregister_user("user1")
|
|
assert "user2" in s.user_sessions
|
|
|
|
|
|
# --- start ---
|
|
|
|
|
|
class TestNewsSchedulerStart:
|
|
"""Tests for start method."""
|
|
|
|
def test_disabled_config_does_not_start(self):
|
|
s = BackgroundJobScheduler()
|
|
s.config["enabled"] = False
|
|
s.start()
|
|
assert s.is_running is False
|
|
|
|
def test_already_running_does_not_restart(self):
|
|
s = BackgroundJobScheduler()
|
|
s.is_running = True
|
|
mock_scheduler = Mock()
|
|
s.scheduler = mock_scheduler
|
|
s.start()
|
|
mock_scheduler.start.assert_not_called()
|
|
|
|
|
|
# --- stop ---
|
|
|
|
|
|
class TestNewsSchedulerStop:
|
|
"""Tests for stop method."""
|
|
|
|
def test_clears_user_sessions(self):
|
|
s = BackgroundJobScheduler()
|
|
s.is_running = True
|
|
s.user_sessions["user1"] = {"data": "test"}
|
|
mock_scheduler = Mock()
|
|
s.scheduler = mock_scheduler
|
|
s.stop()
|
|
assert s.user_sessions == {}
|
|
|
|
def test_sets_is_running_false(self):
|
|
s = BackgroundJobScheduler()
|
|
s.is_running = True
|
|
mock_scheduler = Mock()
|
|
s.scheduler = mock_scheduler
|
|
s.stop()
|
|
assert s.is_running is False
|
|
|
|
def test_calls_scheduler_shutdown(self):
|
|
s = BackgroundJobScheduler()
|
|
s.is_running = True
|
|
mock_scheduler = Mock()
|
|
s.scheduler = mock_scheduler
|
|
s.stop()
|
|
mock_scheduler.shutdown.assert_called_once_with(wait=True)
|
|
|
|
def test_not_running_does_nothing(self):
|
|
s = BackgroundJobScheduler()
|
|
s.is_running = False
|
|
mock_scheduler = Mock()
|
|
s.scheduler = mock_scheduler
|
|
s.stop()
|
|
mock_scheduler.shutdown.assert_not_called()
|
|
|
|
|
|
# --- _get_document_scheduler_settings ---
|
|
|
|
|
|
class TestGetDocumentSchedulerSettings:
|
|
"""Tests for document scheduler settings retrieval with caching."""
|
|
|
|
def test_returns_defaults_without_session(self):
|
|
s = BackgroundJobScheduler()
|
|
result = s._get_document_scheduler_settings("unknown_user")
|
|
assert result == DocumentSchedulerSettings.defaults()
|
|
|
|
def test_returns_cached_settings(self):
|
|
s = BackgroundJobScheduler()
|
|
cached = DocumentSchedulerSettings(enabled=False)
|
|
s._settings_cache["user1"] = cached
|
|
result = s._get_document_scheduler_settings("user1")
|
|
assert result is cached
|
|
|
|
def test_force_refresh_bypasses_cache(self):
|
|
s = BackgroundJobScheduler()
|
|
cached = DocumentSchedulerSettings(enabled=False)
|
|
s._settings_cache["user1"] = cached
|
|
# Without a session, force_refresh returns defaults (can't query DB)
|
|
result = s._get_document_scheduler_settings("user1", force_refresh=True)
|
|
assert result == DocumentSchedulerSettings.defaults()
|
|
|
|
def test_returns_defaults_on_no_session(self):
|
|
s = BackgroundJobScheduler()
|
|
result = s._get_document_scheduler_settings("user1")
|
|
assert result.enabled is True
|
|
assert result.interval_seconds == 1800
|
|
|
|
|
|
# --- get_document_scheduler_status ---
|
|
|
|
|
|
class TestGetDocumentSchedulerStatus:
|
|
"""Tests for document scheduler status retrieval."""
|
|
|
|
def test_unknown_user_returns_disabled(self):
|
|
s = BackgroundJobScheduler()
|
|
result = s.get_document_scheduler_status("unknown")
|
|
assert result["enabled"] is False
|
|
assert "not found" in result["message"].lower()
|
|
|
|
def test_known_user_returns_settings(self):
|
|
s = BackgroundJobScheduler()
|
|
s.user_sessions["user1"] = {
|
|
"last_activity": datetime.now(UTC),
|
|
"scheduled_jobs": set(),
|
|
}
|
|
s._credential_store.store("user1", "pass")
|
|
s._settings_cache["user1"] = DocumentSchedulerSettings()
|
|
result = s.get_document_scheduler_status("user1")
|
|
assert result["enabled"] is True
|
|
assert result["interval_seconds"] == 1800
|
|
|
|
def test_includes_processing_options(self):
|
|
s = BackgroundJobScheduler()
|
|
s.user_sessions["user1"] = {
|
|
"last_activity": datetime.now(UTC),
|
|
"scheduled_jobs": set(),
|
|
}
|
|
s._credential_store.store("user1", "pass")
|
|
s._settings_cache["user1"] = DocumentSchedulerSettings(
|
|
download_pdfs=True, extract_text=True, generate_rag=False
|
|
)
|
|
result = s.get_document_scheduler_status("user1")
|
|
assert result["processing_options"]["download_pdfs"] is True
|
|
assert result["processing_options"]["extract_text"] is True
|
|
assert result["processing_options"]["generate_rag"] is False
|
|
|
|
def test_has_scheduled_job_true(self):
|
|
s = BackgroundJobScheduler()
|
|
s.user_sessions["user1"] = {
|
|
"last_activity": datetime.now(UTC),
|
|
"scheduled_jobs": {"user1_document_processing"},
|
|
}
|
|
s._credential_store.store("user1", "pass")
|
|
s._settings_cache["user1"] = DocumentSchedulerSettings()
|
|
result = s.get_document_scheduler_status("user1")
|
|
assert result["has_scheduled_job"] is True
|
|
|
|
def test_has_scheduled_job_false(self):
|
|
s = BackgroundJobScheduler()
|
|
s.user_sessions["user1"] = {
|
|
"last_activity": datetime.now(UTC),
|
|
"scheduled_jobs": set(),
|
|
}
|
|
s._credential_store.store("user1", "pass")
|
|
s._settings_cache["user1"] = DocumentSchedulerSettings()
|
|
result = s.get_document_scheduler_status("user1")
|
|
assert result["has_scheduled_job"] is False
|
|
|
|
def test_user_active_status(self):
|
|
s = BackgroundJobScheduler()
|
|
s.user_sessions["user1"] = {
|
|
"last_activity": datetime.now(UTC),
|
|
"scheduled_jobs": set(),
|
|
}
|
|
s._credential_store.store("user1", "pass")
|
|
s._settings_cache["user1"] = DocumentSchedulerSettings()
|
|
result = s.get_document_scheduler_status("user1")
|
|
assert result["user_active"] is True
|
|
|
|
|
|
# --- trigger_document_processing ---
|
|
|
|
|
|
class TestTriggerDocumentProcessing:
|
|
"""Tests for manual document processing trigger."""
|
|
|
|
def test_returns_false_for_unknown_user(self):
|
|
s = BackgroundJobScheduler()
|
|
s.is_running = True
|
|
result = s.trigger_document_processing("unknown")
|
|
assert result is False
|
|
|
|
def test_returns_false_when_not_running(self):
|
|
s = BackgroundJobScheduler()
|
|
s.is_running = False
|
|
s.user_sessions["user1"] = {
|
|
"last_activity": datetime.now(UTC),
|
|
"scheduled_jobs": set(),
|
|
}
|
|
s._credential_store.store("user1", "pass")
|
|
result = s.trigger_document_processing("user1")
|
|
assert result is False
|
|
|
|
def test_schedules_immediate_job(self):
|
|
s = BackgroundJobScheduler()
|
|
s.is_running = True
|
|
mock_scheduler = Mock()
|
|
mock_job = Mock()
|
|
mock_job.next_run_time = datetime.now(UTC)
|
|
mock_scheduler.get_job.return_value = mock_job
|
|
s.scheduler = mock_scheduler
|
|
s.user_sessions["user1"] = {
|
|
"last_activity": datetime.now(UTC),
|
|
"scheduled_jobs": set(),
|
|
}
|
|
s._credential_store.store("user1", "pass")
|
|
result = s.trigger_document_processing("user1")
|
|
assert result is True
|
|
mock_scheduler.add_job.assert_called_once()
|
|
|
|
def test_returns_false_on_job_verification_failure(self):
|
|
s = BackgroundJobScheduler()
|
|
s.is_running = True
|
|
mock_scheduler = Mock()
|
|
mock_scheduler.get_job.return_value = None # Verification fails
|
|
s.scheduler = mock_scheduler
|
|
s.user_sessions["user1"] = {
|
|
"last_activity": datetime.now(UTC),
|
|
"scheduled_jobs": set(),
|
|
}
|
|
s._credential_store.store("user1", "pass")
|
|
result = s.trigger_document_processing("user1")
|
|
assert result is False
|
|
|
|
def test_handles_exception(self):
|
|
s = BackgroundJobScheduler()
|
|
s.is_running = True
|
|
mock_scheduler = Mock()
|
|
mock_scheduler.add_job.side_effect = RuntimeError("scheduler error")
|
|
s.scheduler = mock_scheduler
|
|
s.user_sessions["user1"] = {
|
|
"last_activity": datetime.now(UTC),
|
|
"scheduled_jobs": set(),
|
|
}
|
|
s._credential_store.store("user1", "pass")
|
|
result = s.trigger_document_processing("user1")
|
|
assert result is False
|
|
|
|
|
|
# --- initialize_with_settings ---
|
|
|
|
|
|
class TestInitializeWithSettings:
|
|
"""Tests for settings initialization."""
|
|
|
|
def test_stores_settings_manager(self):
|
|
s = BackgroundJobScheduler()
|
|
sm = Mock()
|
|
sm.get_setting = Mock(return_value=True)
|
|
s.initialize_with_settings(sm)
|
|
assert s.settings_manager is sm
|
|
|
|
def test_loads_config_from_settings(self):
|
|
s = BackgroundJobScheduler()
|
|
sm = Mock()
|
|
|
|
def mock_get(key, default=None):
|
|
return {
|
|
"news.scheduler.enabled": True,
|
|
"news.scheduler.retention_hours": 72,
|
|
"news.scheduler.cleanup_interval_hours": 2,
|
|
"news.scheduler.max_jitter_seconds": 600,
|
|
"news.scheduler.max_concurrent_jobs": 20,
|
|
"news.scheduler.batch_size": 10,
|
|
"news.scheduler.activity_check_interval": 10,
|
|
}.get(key, default)
|
|
|
|
sm.get_setting = mock_get
|
|
s.initialize_with_settings(sm)
|
|
assert s.config["retention_hours"] == 72
|
|
assert s.config["cleanup_interval_hours"] == 2
|
|
assert s.config["max_jitter_seconds"] == 600
|
|
|
|
def test_handles_settings_error(self):
|
|
s = BackgroundJobScheduler()
|
|
sm = Mock()
|
|
sm.get_setting.side_effect = RuntimeError("DB error")
|
|
# Should not raise, keeps default config
|
|
s.initialize_with_settings(sm)
|
|
assert s.config["enabled"] is True # Default still intact
|