Files
ss-tools/backend/tests/services/maintenance/test_orchestrators.py

236 lines
11 KiB
Python

# #region Test.Maintenance.Orchestrators [C:4] [TYPE Module] [SEMANTICS test, maintenance, orchestration, coverage, edge]
# @BRIEF Tests for maintenance orchestrators — start_maintenance, end_maintenance, end_all_maintenance.
# @RELATION BINDS_TO -> [MaintenanceOrchestrators]
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent / "src"))
from unittest.mock import AsyncMock, MagicMock, patch, PropertyMock
import pytest
from src.models.maintenance import (
MaintenanceDashboardState,
MaintenanceEvent,
MaintenanceEventStatus,
MaintenanceSettings,
)
# ── start_maintenance coverage ──
class TestStartMaintenance:
"""start_maintenance — event start orchestration."""
@pytest.mark.asyncio
async def test_dashboard_discovery_failure(self):
"""find_affected_dashboards raises → FAILED status."""
from src.services.maintenance._orchestrators import start_maintenance
db = MagicMock()
event = MaintenanceEvent(
id="evt1", status=MaintenanceEventStatus.PENDING,
tables=["table_a"], environment_id="env1",
)
# Chain for MaintenanceEvent query
db.query.return_value.filter.return_value.with_for_update.return_value.first.return_value = event
# Chain for MaintenanceSettings query
db.query.return_value.filter.return_value.first.return_value = MaintenanceSettings(
id="default", target_environment_id="env1", banner_template="Tpl", display_timezone="UTC"
)
superset = AsyncMock()
with patch("src.services.maintenance._orchestrators.find_affected_dashboards",
new_callable=AsyncMock, side_effect=Exception("Discovery failed")):
result = await start_maintenance("evt1", db, superset)
assert result["status"] == "failed"
assert "Discovery failed" in result["error"]
@pytest.mark.asyncio
async def test_partial_success_some_failures(self):
"""Some dashboards fail → PARTIAL status."""
from src.services.maintenance._orchestrators import start_maintenance
db = MagicMock()
event = MaintenanceEvent(
id="evt1", status=MaintenanceEventStatus.PENDING,
tables=["table_a"], environment_id="env1",
)
db.query.return_value.filter.return_value.with_for_update.return_value.first.return_value = event
db.query.return_value.filter.return_value.first.return_value = MaintenanceSettings(
id="default", target_environment_id="env1", banner_template="Tpl", display_timezone="UTC"
)
superset = AsyncMock()
with patch("src.services.maintenance._orchestrators.find_affected_dashboards",
new_callable=AsyncMock, return_value=["dash1", "dash2"]), \
patch("src.services.maintenance._orchestrators._process_dashboards_for_start",
new_callable=AsyncMock, return_value=(
["dash1"], # successful
["dash2"], # failed
[], # unmatched
)):
result = await start_maintenance("evt1", db, superset)
assert result["status"] == "partial"
assert result["affected_dashboards"] == 1
assert len(result["failed_dashboards"]) == 1
@pytest.mark.asyncio
async def test_all_failures(self):
"""All dashboards fail → FAILED."""
from src.services.maintenance._orchestrators import start_maintenance
db = MagicMock()
event = MaintenanceEvent(
id="evt1", status=MaintenanceEventStatus.PENDING,
tables=["table_a"], environment_id="env1",
)
db.query.return_value.filter.return_value.with_for_update.return_value.first.return_value = event
db.query.return_value.filter.return_value.first.return_value = MaintenanceSettings(
id="default", target_environment_id="env1", banner_template="Tpl", display_timezone="UTC"
)
superset = AsyncMock()
with patch("src.services.maintenance._orchestrators.find_affected_dashboards",
new_callable=AsyncMock, return_value=["dash1"]), \
patch("src.services.maintenance._orchestrators._process_dashboards_for_start",
new_callable=AsyncMock, return_value=(
[], # successful
["dash1"], # failed
[], # unmatched
)):
result = await start_maintenance("evt1", db, superset)
assert result["status"] == "failed"
assert result["affected_dashboards"] == 0
# ── end_maintenance ──
class TestEndMaintenance:
"""end_maintenance — event end orchestration."""
@pytest.mark.asyncio
async def test_event_not_found(self):
"""Event missing → failed."""
from src.services.maintenance._orchestrators import end_maintenance
db = MagicMock()
db.query.return_value.filter.return_value.with_for_update.return_value.first.return_value = None
result = await end_maintenance("nonexistent", db, AsyncMock())
assert result["status"] == "failed"
@pytest.mark.asyncio
async def test_already_completed(self):
"""Already completed → idempotent status."""
from src.services.maintenance._orchestrators import end_maintenance
db = MagicMock()
event = MaintenanceEvent(id="evt1", status=MaintenanceEventStatus.COMPLETED)
db.query.return_value.filter.return_value.with_for_update.return_value.first.return_value = event
result = await end_maintenance("evt1", db, AsyncMock())
assert result["status"] == "already_completed"
@pytest.mark.asyncio
async def test_success(self):
"""Successful end → completed status."""
from src.services.maintenance._orchestrators import end_maintenance
db = MagicMock()
event = MaintenanceEvent(id="evt1", status=MaintenanceEventStatus.ACTIVE)
db.query.return_value.filter.return_value.with_for_update.return_value.first.return_value = event
db.query.return_value.filter.return_value.all.return_value = []
with patch("src.services.maintenance._orchestrators._process_states_for_end",
new_callable=AsyncMock, return_value=(0, [])):
result = await end_maintenance("evt1", db, AsyncMock())
assert result["status"] == "completed"
@pytest.mark.asyncio
async def test_partial_failed_removals(self):
"""Some removals fail → partial status."""
from src.services.maintenance._orchestrators import end_maintenance
db = MagicMock()
event = MaintenanceEvent(id="evt1", status=MaintenanceEventStatus.ACTIVE)
db.query.return_value.filter.return_value.with_for_update.return_value.first.return_value = event
db.query.return_value.filter.return_value.all.return_value = []
with patch("src.services.maintenance._orchestrators._process_states_for_end",
new_callable=AsyncMock, return_value=(1, [{"id": 5, "title": "dash5", "error": "removal failed"}])):
result = await end_maintenance("evt1", db, AsyncMock())
assert result["status"] == "partial"
assert len(result["failed_removals"]) == 1
# ── end_all_maintenance ──
class TestEndAllMaintenance:
"""end_all_maintenance — bulk end."""
@pytest.mark.asyncio
async def test_no_active_events(self):
"""No active events → immediate completion."""
from src.services.maintenance._orchestrators import end_all_maintenance
db = MagicMock()
db.query.return_value.filter.return_value.in_.return_value.all.return_value = []
result = await end_all_maintenance(db, AsyncMock())
assert result["status"] == "completed"
assert result["closed_events"] == 0
@pytest.mark.asyncio
async def test_one_event_ends_cleanly(self):
"""One active event → ended cleanly."""
from src.services.maintenance._orchestrators import end_all_maintenance
db = MagicMock()
event = MaintenanceEvent(id="evt1", status=MaintenanceEventStatus.ACTIVE)
db.query.return_value.filter.return_value.all.return_value = [event]
with patch("src.services.maintenance._orchestrators.end_maintenance",
new_callable=AsyncMock, return_value={"removed_from": 3, "failed_removals": []}):
result = await end_all_maintenance(db, AsyncMock())
assert result["status"] == "completed"
assert result["closed_events"] == 1
assert result["removed_from"] == 3
@pytest.mark.asyncio
async def test_end_maintenance_raises_exception(self):
"""end_maintenance raises → event skipped with failed_removals entry."""
from src.services.maintenance._orchestrators import end_all_maintenance
db = MagicMock()
event = MaintenanceEvent(id="evt_fail", status=MaintenanceEventStatus.ACTIVE)
db.query.return_value.filter.return_value.all.return_value = [event]
with patch("src.services.maintenance._orchestrators.end_maintenance",
new_callable=AsyncMock, side_effect=Exception("end failure")):
result = await end_all_maintenance(db, AsyncMock())
assert result["status"] == "partial"
assert result["closed_events"] == 0
assert len(result["failed_removals"]) == 1
assert result["failed_removals"][0]["title"] == "evt_fail"
assert "end failure" in result["failed_removals"][0]["error"]
@pytest.mark.asyncio
async def test_mixed_success_failure(self):
"""Multiple events: some succeed, some fail."""
from src.services.maintenance._orchestrators import end_all_maintenance
db = MagicMock()
evt_ok = MaintenanceEvent(id="evt_ok", status=MaintenanceEventStatus.ACTIVE)
evt_fail = MaintenanceEvent(id="evt_fail", status=MaintenanceEventStatus.PARTIAL)
db.query.return_value.filter.return_value.all.return_value = [evt_ok, evt_fail]
call_count = [0]
async def mock_end_maintenance(event_id, db_session, superset):
call_count[0] += 1
if call_count[0] == 1:
return {"removed_from": 2, "failed_removals": []}
raise Exception("second failed")
with patch("src.services.maintenance._orchestrators.end_maintenance",
new_callable=AsyncMock, side_effect=mock_end_maintenance):
result = await end_all_maintenance(db, AsyncMock())
assert result["status"] == "partial"
assert result["closed_events"] == 1
assert result["removed_from"] == 2
assert len(result["failed_removals"]) == 1
# #endregion Test.Maintenance.Orchestrators