Files
ss-tools/backend/tests/api/test_assistant_tool_maintenance.py

385 lines
15 KiB
Python

# #region Test.Assistant.ToolMaintenance [C:2] [TYPE Module] [SEMANTICS test,assistant,maintenance,events]
# @BRIEF Tests for _tool_maintenance.py — maintenance handlers.
# @RELATION BINDS_TO -> [AssistantToolMaintenance]
# @TEST_EDGE: list_maintenance_events -> Lists active and completed events
# @TEST_EDGE: list_maintenance_no_events -> Empty maintenance response
# @TEST_EDGE: start_maintenance -> Start with tables and start_time
# @TEST_EDGE: start_maintenance_missing_params -> 422 on missing params
# @TEST_EDGE: start_maintenance_invalid_time -> 422 on bad datetime
# @TEST_EDGE: start_maintenance_too_many_tables -> 422 on >100 tables
# @TEST_EDGE: end_maintenance_by_id -> End specific event
# @TEST_EDGE: end_maintenance_not_found -> 404 on missing event
# @TEST_EDGE: end_maintenance_all_active -> End all active events
from pathlib import Path
import sys
sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
from datetime import UTC, datetime, timedelta
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from fastapi import HTTPException
def _make_maintenance_event(event_id="evt-1", status="ACTIVE", tables=None, environment_id="env-1"):
ev = MagicMock()
ev.id = event_id
ev.status = status
ev.tables = tables or ["users"]
ev.environment_id = environment_id
ev.created_at = datetime.now(UTC)
ev.end_time = None
return ev
class TestHandleListMaintenanceEvents:
"""handle_list_maintenance_events — listing events."""
@pytest.fixture
def intent(self):
return {"entities": {}}
@pytest.fixture
def current_user(self):
from src.schemas.auth import User
return User(id="user-1", username="test", email="test@x.com", is_active=True,
auth_source="internal", created_at=datetime.now(UTC), last_login=datetime.now(UTC), roles=[])
@pytest.mark.asyncio
async def test_lists_active_and_completed(self, intent, current_user):
from src.api.routes.assistant._tool_maintenance import handle_list_maintenance_events
from src.models.maintenance import MaintenanceEventStatus
task_manager = MagicMock()
task_manager.create_task = AsyncMock(return_value=MagicMock(id="task-99"))
config_manager = MagicMock()
db = MagicMock()
active_events = [_make_maintenance_event("evt-1", MaintenanceEventStatus.ACTIVE)]
completed_events = [_make_maintenance_event("evt-2", MaintenanceEventStatus.COMPLETED)]
# Mock the queries
db.query.return_value.filter.return_value = MagicMock()
db.query.return_value.filter.return_value.filter.return_value.all.return_value = []
# Separate mocks for each query
def query_side_effect(model):
q = MagicMock()
if model.__name__ == "MaintenanceEvent":
# Active events
q.filter.return_value = MagicMock()
q.filter.return_value.filter.return_value.all.side_effect = [
[], # expired
active_events, # active
]
q.filter.return_value.filter.return_value.order_by.return_value.limit.return_value.all.return_value = completed_events
q.filter.return_value.filter.return_value.order_by.return_value.all.return_value = active_events
q.filter.return_value.in_.return_value = q.filter.return_value
q.filter.return_value.isnot.return_value = q.filter.return_value
return q
db.query.side_effect = query_side_effect
# Mock dashboard state count
db.query.return_value.filter.return_value.filter.return_value = MagicMock()
db.query.return_value.filter.return_value.filter.return_value.count.return_value = 3
text, task_id, actions = await handle_list_maintenance_events(
intent, current_user, task_manager, config_manager, db
)
assert "Активные" in text or "Завершённые" in text
assert actions is not None
@pytest.mark.asyncio
async def test_no_events(self, intent, current_user):
from src.api.routes.assistant._tool_maintenance import handle_list_maintenance_events
task_manager = MagicMock()
task_manager.create_task = AsyncMock(return_value=MagicMock(id="task-99"))
config_manager = MagicMock()
db = MagicMock()
def query_side_effect(model):
q = MagicMock()
if model.__name__ == "MaintenanceEvent":
q.filter.return_value = MagicMock()
q.filter.return_value.filter.return_value.all.return_value = []
q.filter.return_value.filter.return_value.order_by.return_value.all.return_value = []
q.filter.return_value.filter.return_value.order_by.return_value.limit.return_value.all.return_value = []
else:
q.filter.return_value.filter.return_value.count.return_value = 0
return q
db.query.side_effect = query_side_effect
text, task_id, actions = await handle_list_maintenance_events(
intent, current_user, task_manager, config_manager, db
)
assert "Нет событий" in text
class TestHandleStartMaintenance:
"""handle_start_maintenance — starting maintenance events."""
@pytest.fixture
def intent(self):
return {"entities": {}}
@pytest.fixture
def current_user(self):
from src.schemas.auth import User
return User(id="user-1", username="test", email="test@x.com", is_active=True,
auth_source="internal", created_at=datetime.now(UTC), last_login=datetime.now(UTC), roles=[])
@pytest.mark.asyncio
async def test_start_success(self, intent, current_user):
from src.api.routes.assistant._tool_maintenance import handle_start_maintenance
task_manager = MagicMock()
task_manager.create_task = AsyncMock(return_value=MagicMock(id="task-42"))
config_manager = MagicMock()
db = MagicMock()
intent["entities"] = {
"tables": ["users", "orders"],
"start_time": "2025-06-15T10:00",
"end_time": "2025-06-15T12:00",
"message": "Scheduled maintenance",
}
text, task_id, actions = await handle_start_maintenance(
intent, current_user, task_manager, config_manager, db
)
assert "Обслуживание запущено" in text
assert task_id == "task-42"
@pytest.mark.asyncio
async def test_start_without_end_time(self, intent, current_user):
from src.api.routes.assistant._tool_maintenance import handle_start_maintenance
task_manager = MagicMock()
task_manager.create_task = AsyncMock(return_value=MagicMock(id="task-43"))
config_manager = MagicMock()
db = MagicMock()
intent["entities"] = {
"tables": ["users"],
"start_time": "2025-06-15T10:00",
}
text, task_id, actions = await handle_start_maintenance(
intent, current_user, task_manager, config_manager, db
)
assert "Обслуживание запущено" in text
@pytest.mark.asyncio
async def test_start_missing_params_raises_422(self, intent, current_user):
from src.api.routes.assistant._tool_maintenance import handle_start_maintenance
task_manager = MagicMock()
config_manager = MagicMock()
db = MagicMock()
intent["entities"] = {}
with pytest.raises(HTTPException) as exc:
await handle_start_maintenance(
intent, current_user, task_manager, config_manager, db
)
assert exc.value.status_code == 422
@pytest.mark.asyncio
async def test_start_missing_tables_raises_422(self, intent, current_user):
from src.api.routes.assistant._tool_maintenance import handle_start_maintenance
task_manager = MagicMock()
config_manager = MagicMock()
db = MagicMock()
intent["entities"] = {"start_time": "2025-06-15T10:00"}
with pytest.raises(HTTPException) as exc:
await handle_start_maintenance(
intent, current_user, task_manager, config_manager, db
)
assert exc.value.status_code == 422
@pytest.mark.asyncio
async def test_start_invalid_time_raises_422(self, intent, current_user):
from src.api.routes.assistant._tool_maintenance import handle_start_maintenance
task_manager = MagicMock()
config_manager = MagicMock()
db = MagicMock()
intent["entities"] = {
"tables": ["users"],
"start_time": "not-a-date",
}
with pytest.raises(HTTPException) as exc:
await handle_start_maintenance(
intent, current_user, task_manager, config_manager, db
)
assert exc.value.status_code == 422
assert "start_time" in exc.value.detail.lower()
@pytest.mark.asyncio
async def test_start_invalid_end_time_raises_422(self, intent, current_user):
from src.api.routes.assistant._tool_maintenance import handle_start_maintenance
task_manager = MagicMock()
config_manager = MagicMock()
db = MagicMock()
intent["entities"] = {
"tables": ["users"],
"start_time": "2025-06-15T10:00",
"end_time": "not-a-date",
}
with pytest.raises(HTTPException) as exc:
await handle_start_maintenance(
intent, current_user, task_manager, config_manager, db
)
assert exc.value.status_code == 422
assert "end_time" in exc.value.detail.lower()
@pytest.mark.asyncio
async def test_start_too_many_tables_raises_422(self, intent, current_user):
from src.api.routes.assistant._tool_maintenance import handle_start_maintenance
task_manager = MagicMock()
config_manager = MagicMock()
db = MagicMock()
intent["entities"] = {
"tables": [f"table_{i}" for i in range(101)],
"start_time": "2025-06-15T10:00",
}
with pytest.raises(HTTPException) as exc:
await handle_start_maintenance(
intent, current_user, task_manager, config_manager, db
)
assert exc.value.status_code == 422
assert "100" in exc.value.detail
@pytest.mark.asyncio
async def test_start_with_tables_as_string(self, intent, current_user):
from src.api.routes.assistant._tool_maintenance import handle_start_maintenance
task_manager = MagicMock()
task_manager.create_task = AsyncMock(return_value=MagicMock(id="task-44"))
config_manager = MagicMock()
db = MagicMock()
intent["entities"] = {
"tables": "users,orders,products",
"start_time": "2025-06-15T10:00",
}
text, task_id, actions = await handle_start_maintenance(
intent, current_user, task_manager, config_manager, db
)
assert "Обслуживание запущено" in text
# Should have split the string
task_params = task_manager.create_task.call_args[1]["params"]
assert task_params["tables"] == ["users", "orders", "products"]
class TestHandleEndMaintenance:
"""handle_end_maintenance — ending maintenance events."""
@pytest.fixture
def intent(self):
return {"entities": {}}
@pytest.fixture
def current_user(self):
from src.schemas.auth import User
return User(id="user-1", username="test", email="test@x.com", is_active=True,
auth_source="internal", created_at=datetime.now(UTC), last_login=datetime.now(UTC), roles=[])
@pytest.mark.asyncio
async def test_end_by_id(self, intent, current_user):
from src.api.routes.assistant._tool_maintenance import handle_end_maintenance
task_manager = MagicMock()
task_manager.create_task = AsyncMock(return_value=MagicMock(id="task-45"))
config_manager = MagicMock()
db = MagicMock()
event = _make_maintenance_event("evt-1")
db.query.return_value.filter.return_value.first.return_value = event
intent["entities"] = {"event_id": "evt-1"}
text, task_id, actions = await handle_end_maintenance(
intent, current_user, task_manager, config_manager, db
)
assert "Завершение" in text
assert task_id == "task-45"
@pytest.mark.asyncio
async def test_end_not_found_raises_404(self, intent, current_user):
from src.api.routes.assistant._tool_maintenance import handle_end_maintenance
task_manager = MagicMock()
config_manager = MagicMock()
db = MagicMock()
db.query.return_value.filter.return_value.first.return_value = None
intent["entities"] = {"event_id": "evt-nonexistent"}
with pytest.raises(HTTPException) as exc:
await handle_end_maintenance(
intent, current_user, task_manager, config_manager, db
)
assert exc.value.status_code == 404
@pytest.mark.asyncio
async def test_end_all_active_success(self, intent, current_user):
from src.api.routes.assistant._tool_maintenance import handle_end_maintenance
from src.models.maintenance import MaintenanceEventStatus
task_manager = MagicMock()
task_manager.create_task = AsyncMock(return_value=MagicMock(id="task-46"))
config_manager = MagicMock()
db = MagicMock()
active_events = [
_make_maintenance_event("evt-1", MaintenanceEventStatus.ACTIVE),
_make_maintenance_event("evt-2", MaintenanceEventStatus.PARTIAL),
]
db.query.return_value.filter.return_value.filter.return_value.all.return_value = active_events
intent["entities"] = {} # No event_id -> end all
text, task_id, actions = await handle_end_maintenance(
intent, current_user, task_manager, config_manager, db
)
assert "Завершение" in text
assert "2" in text # Should mention 2 events
@pytest.mark.asyncio
async def test_end_all_no_active(self, intent, current_user):
from src.api.routes.assistant._tool_maintenance import handle_end_maintenance
task_manager = MagicMock()
config_manager = MagicMock()
db = MagicMock()
db.query.return_value.filter.return_value.filter.return_value.all.return_value = []
intent["entities"] = {} # No event_id
text, task_id, actions = await handle_end_maintenance(
intent, current_user, task_manager, config_manager, db
)
assert "Нет активных" in text
assert task_id is None
# #endregion Test.Assistant.ToolMaintenance