236 lines
11 KiB
Python
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
|