test: 5 final agents — fix 40+ failures, llm_analysis 80%+, git_plugin 90%+, routes 90-98%, services 98-100%, core 90-100%. Coverage: real 87%, target 95%+
This commit is contained in:
362
backend/tests/api/test_maintenance_routes_edge.py
Normal file
362
backend/tests/api/test_maintenance_routes_edge.py
Normal file
@@ -0,0 +1,362 @@
|
||||
# #region Test.Maintenance.Routes.Edge [C:3] [TYPE Module] [SEMANTICS test,maintenance,routes,edge,coverage]
|
||||
# @BRIEF Edge-case tests for Maintenance Banner API routes — covers uncovered branches from _routes.py.
|
||||
# @RELATION BINDS_TO -> [MaintenanceRoutesModule]
|
||||
# @TEST_EDGE: settings_not_found -> GET /settings returns 404 when no settings row exists
|
||||
# @TEST_EDGE: settings_upsert -> PUT /settings creates new settings when none exist
|
||||
# @TEST_EDGE: settings_all_fields -> PUT /settings with all fields updates correctly
|
||||
# @TEST_EDGE: end_already_completed -> POST /{id}/end returns idempotent success for already-completed
|
||||
# @TEST_EDGE: start_past_time -> POST /start with start_time too far in past returns 400
|
||||
# @TEST_EDGE: start_html_message -> POST /start with HTML in message is accepted
|
||||
# @TEST_EDGE: end_all_with_env_body -> POST /end-all with environment_id in body
|
||||
# @TEST_EDGE: banners_with_linked_states -> GET /dashboard-banners with linked states/events
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from unittest.mock import MagicMock
|
||||
import pytest
|
||||
|
||||
# ── Shared fixtures ────────────────────────────────────────────
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def client():
|
||||
from fastapi.testclient import TestClient
|
||||
from src.app import app
|
||||
return TestClient(app)
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def auth_mock(client, mock_db):
|
||||
from src.app import app
|
||||
from src.core.auth.api_key import generate_api_key
|
||||
from src.dependencies import get_current_user
|
||||
from src.models.api_key import APIKey
|
||||
|
||||
raw, prefix, key_hash = generate_api_key()
|
||||
api_key = APIKey(
|
||||
key_hash=key_hash, prefix=prefix, name="Test Key",
|
||||
permissions=["maintenance:start", "maintenance:end", "maintenance:end_all"],
|
||||
active=True,
|
||||
)
|
||||
mock_db.add(api_key)
|
||||
mock_db.commit()
|
||||
client.headers.update({"X-API-Key": raw})
|
||||
|
||||
mock_role = MagicMock()
|
||||
mock_role.name = "Admin"
|
||||
mock_role.permissions = []
|
||||
|
||||
mock_user = MagicMock()
|
||||
mock_user.username = "test-admin"
|
||||
mock_user.roles = [mock_role]
|
||||
|
||||
async def _mock_get_current_user():
|
||||
return mock_user
|
||||
|
||||
app.dependency_overrides[get_current_user] = _mock_get_current_user
|
||||
|
||||
yield
|
||||
|
||||
client.headers.pop("X-API-Key", None)
|
||||
app.dependency_overrides = {}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_db():
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from sqlalchemy.pool import StaticPool
|
||||
from src.dependencies import get_db
|
||||
from src.models.mapping import Base
|
||||
|
||||
engine = create_engine(
|
||||
"sqlite:///:memory:",
|
||||
poolclass=StaticPool,
|
||||
connect_args={"check_same_thread": False},
|
||||
)
|
||||
Base.metadata.create_all(engine)
|
||||
Session = sessionmaker(bind=engine)
|
||||
session = Session()
|
||||
|
||||
def _get_db_override():
|
||||
db = Session()
|
||||
try:
|
||||
yield db
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
from src.app import app
|
||||
app.dependency_overrides[get_db] = _get_db_override
|
||||
yield session
|
||||
app.dependency_overrides.pop(get_db, None)
|
||||
session.close()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_task_manager():
|
||||
from src.dependencies import get_task_manager
|
||||
|
||||
from unittest.mock import AsyncMock
|
||||
mock_tm = MagicMock()
|
||||
mock_task = MagicMock()
|
||||
mock_task.id = "test-task-id-edge"
|
||||
mock_tm.create_task = AsyncMock(return_value=mock_task)
|
||||
|
||||
from src.app import app
|
||||
app.dependency_overrides[get_task_manager] = lambda: mock_tm
|
||||
yield mock_tm
|
||||
app.dependency_overrides.pop(get_task_manager, None)
|
||||
|
||||
|
||||
# ── GET /api/maintenance/settings — not found ─────────────────
|
||||
|
||||
|
||||
class TestSettingsNotFound:
|
||||
"""GET /api/maintenance/settings when no settings exist."""
|
||||
|
||||
# #region test_get_settings_not_found [C:2] [TYPE Function]
|
||||
# @BRIEF No MaintenanceSettings row -> 404.
|
||||
# @TEST_EDGE: settings_not_found -> GET /settings returns 404
|
||||
def test_get_settings_not_found(self, client, mock_db, mock_task_manager):
|
||||
# Remove the default settings created by the fixture
|
||||
from src.models.maintenance import MaintenanceSettings
|
||||
mock_db.query(MaintenanceSettings).delete()
|
||||
mock_db.commit()
|
||||
|
||||
response = client.get("/api/maintenance/settings")
|
||||
assert response.status_code == 404
|
||||
assert "not configured" in response.json()["detail"]
|
||||
# #endregion test_get_settings_not_found
|
||||
|
||||
|
||||
# ── PUT /api/maintenance/settings — upsert (no existing) ──────
|
||||
|
||||
|
||||
class TestSettingsUpsert:
|
||||
"""PUT /api/maintenance/settings when no settings row exists."""
|
||||
|
||||
# #region test_upsert_settings [C:2] [TYPE Function]
|
||||
# @BRIEF No settings row -> upsert creates one.
|
||||
def test_upsert_settings(self, client, mock_db, mock_task_manager):
|
||||
from src.models.maintenance import MaintenanceSettings
|
||||
mock_db.query(MaintenanceSettings).delete()
|
||||
mock_db.commit()
|
||||
|
||||
response = client.put(
|
||||
"/api/maintenance/settings",
|
||||
json={"target_environment_id": "new-env", "display_timezone": "Asia/Tokyo"},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["target_environment_id"] == "new-env"
|
||||
assert data["display_timezone"] == "Asia/Tokyo"
|
||||
# #endregion test_upsert_settings
|
||||
|
||||
# #region test_upsert_settings_all_fields [C:2] [TYPE Function]
|
||||
# @BRIEF All fields provided -> upsert with complete data.
|
||||
def test_upsert_settings_all_fields(self, client, mock_db, mock_task_manager):
|
||||
from src.models.maintenance import MaintenanceSettings
|
||||
mock_db.query(MaintenanceSettings).delete()
|
||||
mock_db.commit()
|
||||
|
||||
response = client.put(
|
||||
"/api/maintenance/settings",
|
||||
json={
|
||||
"target_environment_id": "env-2",
|
||||
"display_timezone": "Europe/London",
|
||||
"banner_template": "Custom: {message}",
|
||||
"dashboard_scope": "all",
|
||||
"excluded_dashboard_ids": [1, 2],
|
||||
"forced_dashboard_ids": [3],
|
||||
},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["target_environment_id"] == "env-2"
|
||||
assert data["display_timezone"] == "Europe/London"
|
||||
assert data["banner_template"] == "Custom: {message}"
|
||||
assert data["dashboard_scope"] == "all"
|
||||
assert 1 in data["excluded_dashboard_ids"]
|
||||
assert 3 in data["forced_dashboard_ids"]
|
||||
# #endregion test_upsert_settings_all_fields
|
||||
|
||||
|
||||
# ── POST /api/maintenance/{id}/end — already completed ────────
|
||||
|
||||
|
||||
class TestEndAlreadyCompleted:
|
||||
"""POST /api/maintenance/{id}/end when event is already COMPLETED."""
|
||||
|
||||
# #region test_end_idempotent_completed [C:2] [TYPE Function]
|
||||
# @BRIEF Event already COMPLETED -> returns 202 with already_completed status.
|
||||
def test_end_idempotent_completed(self, client, mock_db, mock_task_manager):
|
||||
from src.models.maintenance import MaintenanceEvent, MaintenanceEventStatus
|
||||
|
||||
event = MaintenanceEvent(
|
||||
tables=["raw.sales"],
|
||||
start_time=datetime.now(UTC),
|
||||
status=MaintenanceEventStatus.COMPLETED,
|
||||
environment_id="test-env",
|
||||
task_id="existing-task",
|
||||
)
|
||||
mock_db.add(event)
|
||||
mock_db.commit()
|
||||
|
||||
response = client.post(f"/api/maintenance/{event.id}/end")
|
||||
assert response.status_code == 202
|
||||
data = response.json()
|
||||
assert data["status"] == "already_completed"
|
||||
assert data["task_id"] == "existing-task"
|
||||
# #endregion test_end_idempotent_completed
|
||||
|
||||
|
||||
# ── POST /api/maintenance/start — edge cases ──────────────────
|
||||
|
||||
|
||||
class TestStartEdgeCases:
|
||||
"""POST /api/maintenance/start — edge paths."""
|
||||
|
||||
# #region test_start_too_far_past [C:2] [TYPE Function]
|
||||
# @BRIEF start_time more than 1h in the past -> 400.
|
||||
def test_start_too_far_past(self, client, mock_db, mock_task_manager):
|
||||
response = client.post(
|
||||
"/api/maintenance/start",
|
||||
json={
|
||||
"tables": ["raw.sales"],
|
||||
"start_time": (datetime.now(UTC) - timedelta(hours=5)).isoformat(),
|
||||
"environment_id": "test-env",
|
||||
},
|
||||
)
|
||||
assert response.status_code == 400
|
||||
assert "too far in the past" in response.json()["detail"]
|
||||
# #endregion test_start_too_far_past
|
||||
|
||||
# #region test_start_with_html_message [C:2] [TYPE Function]
|
||||
# @BRIEF Message with HTML content -> accepted (HTML will be escaped at render).
|
||||
def test_start_with_html_message(self, client, mock_db, mock_task_manager):
|
||||
response = client.post(
|
||||
"/api/maintenance/start",
|
||||
json={
|
||||
"tables": ["raw.sales"],
|
||||
"start_time": (datetime.now(UTC) + timedelta(hours=1)).isoformat(),
|
||||
"end_time": (datetime.now(UTC) + timedelta(hours=3)).isoformat(),
|
||||
"message": "<script>alert('xss')</script>",
|
||||
"environment_id": "test-env",
|
||||
},
|
||||
)
|
||||
# HTML in message is accepted — rendering layer escapes it
|
||||
assert response.status_code == 202
|
||||
# #endregion test_start_with_html_message
|
||||
|
||||
|
||||
# ── POST /api/maintenance/end-all — with env in body ──────────
|
||||
|
||||
|
||||
class TestEndAllWithEnv:
|
||||
"""POST /api/maintenance/end-all with environment_id in body."""
|
||||
|
||||
# #region test_end_all_with_env_body [C:2] [TYPE Function]
|
||||
# @BRIEF environment_id in JSON body -> passed to task params.
|
||||
def test_end_all_with_env_body(self, client, mock_db, mock_task_manager):
|
||||
response = client.post(
|
||||
"/api/maintenance/end-all",
|
||||
json={"environment_id": "env-42"},
|
||||
)
|
||||
assert response.status_code == 202
|
||||
data = response.json()
|
||||
assert data["status"] == "pending"
|
||||
# Verify environment_id was passed through
|
||||
call_kwargs = mock_task_manager.create_task.call_args[1]
|
||||
assert call_kwargs["params"]["environment_id"] == "env-42"
|
||||
# #endregion test_end_all_with_env_body
|
||||
|
||||
|
||||
# ── GET /api/maintenance/dashboard-banners — with linked states ─
|
||||
|
||||
|
||||
class TestBannersWithLinkedStates:
|
||||
"""GET /api/maintenance/dashboard-banners with linked dashboard states and events."""
|
||||
|
||||
# #region test_banners_with_linked_states [C:2] [TYPE Function]
|
||||
# @BRIEF Banners with linked MaintenanceDashboardState records -> events aggregated.
|
||||
def test_banners_with_linked_states(self, client, mock_db, mock_task_manager):
|
||||
from src.models.maintenance import (
|
||||
MaintenanceDashboardBanner,
|
||||
MaintenanceDashboardBannerStatus,
|
||||
MaintenanceDashboardState,
|
||||
MaintenanceDashboardStateStatus,
|
||||
MaintenanceEvent,
|
||||
MaintenanceEventStatus,
|
||||
)
|
||||
|
||||
event = MaintenanceEvent(
|
||||
tables=["raw.test"],
|
||||
start_time=datetime.now(UTC),
|
||||
end_time=datetime.now(UTC) + timedelta(hours=2),
|
||||
status=MaintenanceEventStatus.ACTIVE,
|
||||
environment_id="test-env",
|
||||
)
|
||||
mock_db.add(event)
|
||||
mock_db.flush()
|
||||
|
||||
banner = MaintenanceDashboardBanner(
|
||||
environment_id="test-env",
|
||||
dashboard_id=201,
|
||||
chart_id=67890,
|
||||
banner_text="Linked test",
|
||||
status=MaintenanceDashboardBannerStatus.ACTIVE,
|
||||
)
|
||||
mock_db.add(banner)
|
||||
mock_db.flush()
|
||||
|
||||
state = MaintenanceDashboardState(
|
||||
banner_id=banner.id,
|
||||
event_id=event.id,
|
||||
dashboard_id=201,
|
||||
status=MaintenanceDashboardStateStatus.ACTIVE,
|
||||
)
|
||||
mock_db.add(state)
|
||||
mock_db.commit()
|
||||
|
||||
response = client.get("/api/maintenance/dashboard-banners")
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert isinstance(data, list)
|
||||
assert len(data) >= 1
|
||||
banners_for_dash = [b for b in data if b["dashboard_id"] == 201]
|
||||
assert len(banners_for_dash) == 1
|
||||
assert len(banners_for_dash[0]["events"]) == 1
|
||||
assert banners_for_dash[0]["active"] is True
|
||||
# #endregion test_banners_with_linked_states
|
||||
|
||||
|
||||
# ── GET /api/maintenance/dashboard-banners — no linked states ─
|
||||
|
||||
|
||||
class TestBannersNoLinkedStates:
|
||||
"""GET /api/maintenance/dashboard-banners with no linked states (just banner)."""
|
||||
|
||||
# #region test_banners_no_linked_states [C:2] [TYPE Function]
|
||||
# @BRIEF Active banner without linked states -> event lists are empty.
|
||||
def test_banners_no_linked_states(self, client, mock_db, mock_task_manager):
|
||||
from src.models.maintenance import (
|
||||
MaintenanceDashboardBanner,
|
||||
MaintenanceDashboardBannerStatus,
|
||||
)
|
||||
|
||||
banner = MaintenanceDashboardBanner(
|
||||
environment_id="test-env",
|
||||
dashboard_id=301,
|
||||
chart_id=11111,
|
||||
banner_text="No linked state",
|
||||
status=MaintenanceDashboardBannerStatus.ACTIVE,
|
||||
)
|
||||
mock_db.add(banner)
|
||||
mock_db.commit()
|
||||
|
||||
response = client.get("/api/maintenance/dashboard-banners")
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
banners_for_dash = [b for b in data if b["dashboard_id"] == 301]
|
||||
assert len(banners_for_dash) == 1
|
||||
assert banners_for_dash[0]["events"] == []
|
||||
# #endregion test_banners_no_linked_states
|
||||
# #endregion Test.Maintenance.Routes.Edge
|
||||
Reference in New Issue
Block a user