623 lines
24 KiB
Python
623 lines
24 KiB
Python
# #region test_maintenance_service [C:3] [TYPE TestModule] [SEMANTICS test, maintenance, service, pytest, integration]
|
|
# @BRIEF Integration tests for MaintenanceService — T018, T026, T026a.
|
|
# Tests the C3/C4 orchestrators: start_maintenance, end_maintenance, end_all_maintenance,
|
|
# and helpers: find_affected_dashboards, ensure_banner_chart, build_banner_text, rebuild_banner.
|
|
# @LAYER Test
|
|
# @RELATION BINDS_TO -> [EXT:frontend:MaintenanceServiceModule]
|
|
# @TEST_CONTRACT: start_maintenance(str, Session, SupersetClient) -> dict
|
|
# @TEST_CONTRACT: end_maintenance(str, Session, SupersetClient) -> dict
|
|
# @TEST_CONTRACT: end_all_maintenance(Session, SupersetClient) -> dict
|
|
# @TEST_EDGE: event_not_found -> returns failed status with error message
|
|
# @TEST_EDGE: no_matching_dashboards -> returns "no_match" status
|
|
# @TEST_EDGE: superset_api_failure -> returns partial/failed with error details
|
|
import uuid
|
|
from datetime import datetime, timezone
|
|
from typing import Any, cast
|
|
from unittest.mock import MagicMock, patch, PropertyMock
|
|
|
|
import pytest
|
|
|
|
from src.models.maintenance import (
|
|
DashboardScope,
|
|
MaintenanceDashboardBanner,
|
|
MaintenanceDashboardBannerStatus,
|
|
MaintenanceDashboardState,
|
|
MaintenanceDashboardStateStatus,
|
|
MaintenanceEvent,
|
|
MaintenanceEventStatus,
|
|
MaintenanceSettings,
|
|
)
|
|
from src.services.maintenance import (
|
|
build_banner_text,
|
|
build_idempotency_key,
|
|
end_all_maintenance,
|
|
end_maintenance,
|
|
ensure_banner_chart,
|
|
find_affected_dashboards,
|
|
rebuild_banner,
|
|
start_maintenance,
|
|
)
|
|
|
|
|
|
# ── Fixtures ──────────────────────────────────────────────────
|
|
|
|
@pytest.fixture
|
|
def mock_superset():
|
|
"""Create a mock SupersetClient for testing."""
|
|
client = MagicMock()
|
|
# Default: no datasets
|
|
client.get_datasets.return_value = (0, [])
|
|
client.get_dashboards.return_value = (0, [])
|
|
client.get_dataset_detail.return_value = {"linked_dashboards": []}
|
|
client.create_markdown_chart.return_value = 12345
|
|
client.get_chart.return_value = {"id": 12345, "result": {"viz_type": "markdown"}}
|
|
client.update_banner_on_dashboard.return_value = {"result": "ok"}
|
|
client.update_dashboard_layout.return_value = {"result": "ok"}
|
|
client.remove_chart_from_layout.return_value = {"result": "ok"}
|
|
client.delete_chart.return_value = {"result": "ok"}
|
|
return client
|
|
|
|
|
|
@pytest.fixture
|
|
def db_session():
|
|
"""Create an in-memory SQLite session with tables."""
|
|
from sqlalchemy import create_engine
|
|
from sqlalchemy.orm import sessionmaker
|
|
from src.models.mapping import Base
|
|
|
|
engine = create_engine("sqlite:///:memory:", echo=False)
|
|
Base.metadata.create_all(engine)
|
|
Session = sessionmaker(bind=engine)
|
|
session = Session()
|
|
|
|
# Create default settings
|
|
settings = MaintenanceSettings(
|
|
id="default",
|
|
target_environment_id="test-env",
|
|
display_timezone="UTC",
|
|
banner_template="Maintenance: {message} ({start_time} - {end_time})",
|
|
dashboard_scope=DashboardScope.PUBLISHED_ONLY,
|
|
excluded_dashboard_ids=[],
|
|
forced_dashboard_ids=[],
|
|
)
|
|
session.add(settings)
|
|
session.commit()
|
|
return session
|
|
|
|
|
|
@pytest.fixture
|
|
def pending_event(db_session):
|
|
"""Create a PENDING maintenance event in the DB."""
|
|
event = MaintenanceEvent(
|
|
tables=["raw.sales", "raw.inventory"],
|
|
start_time=datetime.now(timezone.utc),
|
|
end_time=datetime.now(timezone.utc),
|
|
message="Test maintenance",
|
|
status=MaintenanceEventStatus.PENDING,
|
|
environment_id="test-env",
|
|
)
|
|
db_session.add(event)
|
|
db_session.commit()
|
|
return event
|
|
|
|
|
|
# ── T017: build_idempotency_key tests ─────────────────────────
|
|
|
|
class TestBuildIdempotencyKey:
|
|
"""Tests for build_idempotency_key helper."""
|
|
|
|
# #region test_basic_key [C:2] [TYPE Function]
|
|
# @BRIEF Basic idempotency key with tables and times.
|
|
def test_basic_key(self):
|
|
tables = ["raw.sales", "RAW.INVENTORY"]
|
|
start = datetime(2026, 5, 21, 22, 0, 0, tzinfo=timezone.utc)
|
|
end = datetime(2026, 5, 22, 2, 0, 0, tzinfo=timezone.utc)
|
|
key = build_idempotency_key(tables, start, end)
|
|
assert key[0] == frozenset({"raw.inventory", "raw.sales"})
|
|
assert key[1] == "2026-05-21T22:00:00+00:00"
|
|
assert key[2] == "2026-05-22T02:00:00+00:00"
|
|
# #endregion test_basic_key
|
|
|
|
# #region test_key_without_end_time [C:2] [TYPE Function]
|
|
# @BRIEF Idempotency key with None end_time.
|
|
def test_key_without_end_time(self):
|
|
tables = ["raw.sales"]
|
|
start = datetime(2026, 5, 21, 22, 0, 0, tzinfo=timezone.utc)
|
|
key = build_idempotency_key(tables, start, None)
|
|
assert key[0] == frozenset({"raw.sales"})
|
|
assert key[1] == "2026-05-21T22:00:00+00:00"
|
|
assert key[2] is None
|
|
# #endregion test_key_without_end_time
|
|
|
|
# #region test_key_table_sorting [C:2] [TYPE Function]
|
|
# @BRIEF Tables are sorted alphabetically in frozenset.
|
|
def test_key_table_sorting(self):
|
|
key1 = build_idempotency_key(["z.table", "a.table"], None, None)
|
|
key2 = build_idempotency_key(["a.table", "z.table"], None, None)
|
|
assert key1 == key2
|
|
# #endregion test_key_table_sorting
|
|
|
|
|
|
# ── T019: find_affected_dashboards tests ──────────────────────
|
|
|
|
class TestFindAffectedDashboards:
|
|
"""Tests for find_affected_dashboards."""
|
|
|
|
# #region test_empty_tables [C:2] [TYPE Function]
|
|
# @BRIEF Empty tables list returns empty.
|
|
def test_empty_tables(self, mock_superset):
|
|
result = find_affected_dashboards([], mock_superset)
|
|
assert result == []
|
|
# #endregion test_empty_tables
|
|
|
|
# #region test_table_based_match [C:2] [TYPE Function]
|
|
# @BRIEF Table-based dataset matching.
|
|
def test_table_based_match(self, mock_superset):
|
|
mock_superset.get_datasets.return_value = (2, [
|
|
{"id": 1, "schema": "raw", "table_name": "sales", "sql": None},
|
|
{"id": 2, "schema": "raw", "table_name": "inventory", "sql": None},
|
|
])
|
|
mock_superset.get_dataset_detail.side_effect = [
|
|
{"linked_dashboards": [{"id": 101}]},
|
|
{"linked_dashboards": [{"id": 102}]},
|
|
]
|
|
|
|
result = find_affected_dashboards(["raw.sales"], mock_superset)
|
|
assert 101 in result
|
|
assert 102 not in result
|
|
# #endregion test_table_based_match
|
|
|
|
# #region test_virtual_dataset_match [C:2] [TYPE Function]
|
|
# @BRIEF Virtual SQL dataset matching via SqlTableExtractor.
|
|
def test_virtual_dataset_match(self, mock_superset):
|
|
mock_superset.get_datasets.return_value = (1, [
|
|
{
|
|
"id": 1,
|
|
"schema": "raw",
|
|
"table_name": "sales_view",
|
|
"sql": "SELECT * FROM raw.sales",
|
|
"is_sqllab_view": True,
|
|
},
|
|
])
|
|
mock_superset.get_dataset_detail.return_value = {
|
|
"linked_dashboards": [{"id": 101}],
|
|
}
|
|
|
|
result = find_affected_dashboards(["raw.sales"], mock_superset)
|
|
assert 101 in result
|
|
# #endregion test_virtual_dataset_match
|
|
|
|
# #region test_case_insensitive_matching [C:2] [TYPE Function]
|
|
# @BRIEF Matching is case-insensitive.
|
|
def test_case_insensitive_matching(self, mock_superset):
|
|
mock_superset.get_datasets.return_value = (1, [
|
|
{"id": 1, "schema": "RAW", "table_name": "SALES", "sql": None},
|
|
])
|
|
mock_superset.get_dataset_detail.return_value = {
|
|
"linked_dashboards": [{"id": 101}],
|
|
}
|
|
|
|
result = find_affected_dashboards(["raw.sales"], mock_superset)
|
|
assert 101 in result
|
|
# #endregion test_case_insensitive_matching
|
|
|
|
# #region test_superset_api_failure [C:2] [TYPE Function]
|
|
# @BRIEF Superset API failure propagates.
|
|
def test_superset_api_failure(self, mock_superset):
|
|
mock_superset.get_datasets.side_effect = Exception("Superset API error")
|
|
with pytest.raises(Exception, match="Superset API error"):
|
|
find_affected_dashboards(["raw.sales"], mock_superset)
|
|
# #endregion test_superset_api_failure
|
|
|
|
|
|
# ── T019a: ensure_banner_chart tests ──────────────────────────
|
|
|
|
class TestEnsureBannerChart:
|
|
"""Tests for ensure_banner_chart."""
|
|
|
|
# #region test_creates_new_banner [C:2] [TYPE Function]
|
|
# @BRIEF No existing banner — creates new chart and banner row.
|
|
def test_creates_new_banner(self, mock_superset, db_session):
|
|
banner = ensure_banner_chart(
|
|
101, "test-env", mock_superset, db_session
|
|
)
|
|
assert banner.dashboard_id == 101
|
|
assert banner.chart_id == 12345
|
|
assert banner.status == MaintenanceDashboardBannerStatus.ACTIVE
|
|
mock_superset.create_markdown_chart.assert_called_once_with(101, "*Maintenance banner placeholder*")
|
|
mock_superset.update_dashboard_layout.assert_called_once_with(101, 12345)
|
|
# #endregion test_creates_new_banner
|
|
|
|
# #region test_returns_existing_banner [C:2] [TYPE Function]
|
|
# @BRIEF Existing active banner — returns it.
|
|
def test_returns_existing_banner(self, mock_superset, db_session):
|
|
existing = MaintenanceDashboardBanner(
|
|
environment_id="test-env",
|
|
dashboard_id=101,
|
|
chart_id=999,
|
|
banner_text="Existing",
|
|
status=MaintenanceDashboardBannerStatus.ACTIVE,
|
|
)
|
|
db_session.add(existing)
|
|
db_session.commit()
|
|
|
|
banner = ensure_banner_chart(
|
|
101, "test-env", mock_superset, db_session
|
|
)
|
|
assert banner.id == existing.id
|
|
assert banner.chart_id == 999
|
|
mock_superset.create_markdown_chart.assert_not_called()
|
|
# #endregion test_returns_existing_banner
|
|
|
|
# #region test_chart_creation_failure [C:2] [TYPE Function]
|
|
# @BRIEF Chart creation failure propagates.
|
|
def test_chart_creation_failure(self, mock_superset, db_session):
|
|
mock_superset.create_markdown_chart.side_effect = Exception("Chart error")
|
|
with pytest.raises(Exception, match="Chart error"):
|
|
ensure_banner_chart(101, "test-env", mock_superset, db_session)
|
|
# #endregion test_chart_creation_failure
|
|
|
|
|
|
# ── T020: build_banner_text tests ─────────────────────────────
|
|
|
|
class TestBuildBannerText:
|
|
"""Tests for build_banner_text."""
|
|
|
|
# #region test_single_event [C:2] [TYPE Function]
|
|
# @BRIEF Single event — direct substitution.
|
|
def test_single_event(self):
|
|
events = [
|
|
{"start_time": "2026-05-21T22:00:00+00:00",
|
|
"end_time": "2026-05-22T02:00:00+00:00",
|
|
"message": "Updating tables"}
|
|
]
|
|
template = "Banner: {message} ({start_time} - {end_time})"
|
|
result = build_banner_text(events, template)
|
|
assert "Updating tables" in result
|
|
assert "2026-05-21 22:00" in result
|
|
assert "2026-05-22 02:00" in result
|
|
# #endregion test_single_event
|
|
|
|
# #region test_multiple_events [C:2] [TYPE Function]
|
|
# @BRIEF Multiple events — combined message.
|
|
def test_multiple_events(self):
|
|
events = [
|
|
{"start_time": "2026-05-21T22:00:00+00:00",
|
|
"end_time": "2026-05-22T02:00:00+00:00",
|
|
"message": "Event 1"},
|
|
{"start_time": "2026-05-23T22:00:00+00:00",
|
|
"end_time": "2026-05-24T02:00:00+00:00",
|
|
"message": "Event 2"},
|
|
]
|
|
template = "Banner: {message} ({start_time} - {end_time})"
|
|
result = build_banner_text(events, template)
|
|
assert "Event 1" in result
|
|
assert "Event 2" in result
|
|
# #endregion test_multiple_events
|
|
|
|
# #region test_empty_events [C:2] [TYPE Function]
|
|
# @BRIEF Empty events list returns empty string.
|
|
def test_empty_events(self):
|
|
result = build_banner_text([], "Template: {message}")
|
|
assert result == ""
|
|
# #endregion test_empty_events
|
|
|
|
# #region test_missing_variables [C:2] [TYPE Function]
|
|
# @BRIEF Missing variables replaced with empty string.
|
|
def test_missing_variables(self):
|
|
events = [{"start_time": None, "end_time": None, "message": "Hi"}]
|
|
template = "Start: {start_time} - End: {end_time} - Msg: {message}"
|
|
result = build_banner_text(events, template)
|
|
assert "уточняется" in result
|
|
assert "Hi" in result
|
|
# #endregion test_missing_variables
|
|
|
|
# #region test_html_escaping [C:2] [TYPE Function]
|
|
# @BRIEF Message is HTML-escaped.
|
|
def test_html_escaping(self):
|
|
events = [{
|
|
"start_time": "2026-05-21T22:00:00+00:00",
|
|
"end_time": "2026-05-22T02:00:00+00:00",
|
|
"message": "<script>alert('xss')</script>",
|
|
}]
|
|
template = "{message}"
|
|
result = build_banner_text(events, template)
|
|
assert "<script>" not in result
|
|
assert "<script>" in result
|
|
# #endregion test_html_escaping
|
|
|
|
|
|
# ── T018/T021: start_maintenance tests ────────────────────────
|
|
|
|
class TestStartMaintenance:
|
|
"""Tests for start_maintenance C4 orchestrator."""
|
|
|
|
# #region test_happy_path [C:2] [TYPE Function]
|
|
# @BRIEF Happy path: event with matching dashboards.
|
|
def test_happy_path(self, mock_superset, db_session, pending_event):
|
|
# Mock dataset with linked dashboard
|
|
mock_superset.get_datasets.return_value = (1, [
|
|
{"id": 1, "schema": "raw", "table_name": "sales", "sql": None},
|
|
])
|
|
mock_superset.get_dataset_detail.return_value = {
|
|
"linked_dashboards": [{"id": 101, "title": "Sales Dashboard"}],
|
|
}
|
|
# Mock dashboard list for title resolution
|
|
mock_superset.get_dashboards.return_value = (1, [
|
|
{"id": 101, "dashboard_title": "Sales Dashboard", "published": True},
|
|
])
|
|
|
|
result = start_maintenance(pending_event.id, db_session, mock_superset)
|
|
|
|
assert result["maintenance_id"] == pending_event.id
|
|
assert result["status"] in ("active", "partial")
|
|
assert result["affected_dashboards"] >= 1
|
|
|
|
# Verify event status updated
|
|
db_session.refresh(pending_event)
|
|
assert pending_event.status == MaintenanceEventStatus.ACTIVE
|
|
# #endregion test_happy_path
|
|
|
|
# #region test_no_matching_dashboards [C:2] [TYPE Function]
|
|
# @BRIEF No dashboards match the tables.
|
|
def test_no_matching_dashboards(self, mock_superset, db_session, pending_event):
|
|
# No datasets
|
|
mock_superset.get_datasets.return_value = (0, [])
|
|
|
|
result = start_maintenance(pending_event.id, db_session, mock_superset)
|
|
|
|
assert result["status"] == "no_match"
|
|
assert result["affected_dashboards"] == 0
|
|
# #endregion test_no_matching_dashboards
|
|
|
|
# #region test_event_not_found [C:2] [TYPE Function]
|
|
# @BRIEF Event ID not in DB.
|
|
def test_event_not_found(self, mock_superset, db_session):
|
|
result = start_maintenance("nonexistent-id", db_session, mock_superset)
|
|
assert result["status"] == "failed"
|
|
assert "error" in result
|
|
# #endregion test_event_not_found
|
|
|
|
# #region test_already_processed_event [C:2] [TYPE Function]
|
|
# @BRIEF Event already ACTIVE — idempotent.
|
|
def test_already_processed_event(self, mock_superset, db_session, pending_event):
|
|
# First run succeeds
|
|
mock_superset.get_datasets.return_value = (0, [])
|
|
result1 = start_maintenance(pending_event.id, db_session, mock_superset)
|
|
assert result1["status"] == "no_match"
|
|
|
|
# Second run — event is already ACTIVE
|
|
result2 = start_maintenance(pending_event.id, db_session, mock_superset)
|
|
assert result2["status"] == "active"
|
|
# #endregion test_already_processed_event
|
|
|
|
|
|
# ── T027: rebuild_banner tests ────────────────────────────────
|
|
|
|
class TestRebuildBanner:
|
|
"""Tests for rebuild_banner."""
|
|
|
|
# #region test_rebuild_active_banner [C:2] [TYPE Function]
|
|
# @BRIEF Active banner with active state — rebuilds text.
|
|
def test_rebuild_active_banner(self, mock_superset, db_session):
|
|
banner = MaintenanceDashboardBanner(
|
|
environment_id="test-env",
|
|
dashboard_id=101,
|
|
chart_id=12345,
|
|
banner_text="Old text",
|
|
status=MaintenanceDashboardBannerStatus.ACTIVE,
|
|
)
|
|
db_session.add(banner)
|
|
db_session.flush()
|
|
|
|
event = MaintenanceEvent(
|
|
tables=["raw.sales"],
|
|
start_time=datetime(2026, 5, 21, 22, 0, 0, tzinfo=timezone.utc),
|
|
end_time=datetime(2026, 5, 22, 2, 0, 0, tzinfo=timezone.utc),
|
|
message="Test",
|
|
status=MaintenanceEventStatus.ACTIVE,
|
|
environment_id="test-env",
|
|
)
|
|
db_session.add(event)
|
|
db_session.flush()
|
|
|
|
state = MaintenanceDashboardState(
|
|
event_id=event.id,
|
|
dashboard_id=101,
|
|
banner_id=banner.id,
|
|
status=MaintenanceDashboardStateStatus.ACTIVE,
|
|
)
|
|
db_session.add(state)
|
|
db_session.commit()
|
|
|
|
result = rebuild_banner(banner.id, db_session, mock_superset)
|
|
assert result is True
|
|
mock_superset.update_banner_on_dashboard.assert_called_once()
|
|
# #endregion test_rebuild_active_banner
|
|
|
|
# #region test_banner_not_found [C:2] [TYPE Function]
|
|
# @BRIEF Banner ID not found.
|
|
def test_banner_not_found(self, mock_superset, db_session):
|
|
result = rebuild_banner("nonexistent", db_session, mock_superset)
|
|
assert result is False
|
|
# #endregion test_banner_not_found
|
|
|
|
|
|
# ── T026/T028: end_maintenance tests ──────────────────────────
|
|
|
|
class TestEndMaintenance:
|
|
"""Tests for end_maintenance C4 orchestrator."""
|
|
|
|
# #region test_end_active_event_single_banner [C:2] [TYPE Function]
|
|
# @BRIEF End event with single active dashboard — banner removed entirely.
|
|
def test_end_active_event_single_banner(self, mock_superset, db_session, pending_event):
|
|
# Set up: create banner and state
|
|
banner = MaintenanceDashboardBanner(
|
|
environment_id="test-env",
|
|
dashboard_id=101,
|
|
chart_id=12345,
|
|
banner_text="Test banner",
|
|
status=MaintenanceDashboardBannerStatus.ACTIVE,
|
|
)
|
|
db_session.add(banner)
|
|
db_session.flush()
|
|
|
|
state = MaintenanceDashboardState(
|
|
event_id=pending_event.id,
|
|
dashboard_id=101,
|
|
banner_id=banner.id,
|
|
status=MaintenanceDashboardStateStatus.ACTIVE,
|
|
)
|
|
db_session.add(state)
|
|
pending_event.status = MaintenanceEventStatus.ACTIVE
|
|
db_session.commit()
|
|
|
|
result = end_maintenance(pending_event.id, db_session, mock_superset)
|
|
|
|
assert result["maintenance_id"] == pending_event.id
|
|
assert result["removed_from"] >= 1
|
|
|
|
# Verify chart was deleted
|
|
mock_superset.remove_chart_from_layout.assert_called()
|
|
mock_superset.delete_chart.assert_called_with(12345)
|
|
|
|
# Verify event status
|
|
db_session.refresh(pending_event)
|
|
assert pending_event.status == MaintenanceEventStatus.COMPLETED
|
|
# #endregion test_end_active_event_single_banner
|
|
|
|
# #region test_end_event_shared_banner [C:2] [TYPE Function]
|
|
# @BRIEF Two events, same dashboard — ending one rebuilds banner, doesn't delete.
|
|
def test_end_event_shared_banner(self, mock_superset, db_session, pending_event):
|
|
# Banner shared by two events
|
|
banner = MaintenanceDashboardBanner(
|
|
environment_id="test-env",
|
|
dashboard_id=101,
|
|
chart_id=12345,
|
|
banner_text="Shared banner",
|
|
status=MaintenanceDashboardBannerStatus.ACTIVE,
|
|
)
|
|
db_session.add(banner)
|
|
db_session.flush()
|
|
|
|
# Event 1 (pending_event) state
|
|
state1 = MaintenanceDashboardState(
|
|
event_id=pending_event.id,
|
|
dashboard_id=101,
|
|
banner_id=banner.id,
|
|
status=MaintenanceDashboardStateStatus.ACTIVE,
|
|
)
|
|
db_session.add(state1)
|
|
|
|
# Event 2
|
|
event2 = MaintenanceEvent(
|
|
tables=["raw.products"],
|
|
start_time=datetime.now(timezone.utc),
|
|
message="Second event",
|
|
status=MaintenanceEventStatus.ACTIVE,
|
|
environment_id="test-env",
|
|
)
|
|
db_session.add(event2)
|
|
db_session.flush()
|
|
|
|
state2 = MaintenanceDashboardState(
|
|
event_id=event2.id,
|
|
dashboard_id=101,
|
|
banner_id=banner.id,
|
|
status=MaintenanceDashboardStateStatus.ACTIVE,
|
|
)
|
|
db_session.add(state2)
|
|
|
|
pending_event.status = MaintenanceEventStatus.ACTIVE
|
|
db_session.commit()
|
|
|
|
# End event1
|
|
result = end_maintenance(pending_event.id, db_session, mock_superset)
|
|
|
|
assert result["removed_from"] >= 1
|
|
|
|
# Chart should NOT be deleted (event2 still active)
|
|
mock_superset.delete_chart.assert_not_called()
|
|
# But rebuild should be called
|
|
mock_superset.update_banner_on_dashboard.assert_called()
|
|
|
|
# Verify event1 completed
|
|
db_session.refresh(pending_event)
|
|
assert pending_event.status == MaintenanceEventStatus.COMPLETED
|
|
# #endregion test_end_event_shared_banner
|
|
|
|
# #region test_end_already_completed_event [C:2] [TYPE Function]
|
|
# @BRIEF Already completed event — idempotent.
|
|
def test_end_already_completed_event(self, mock_superset, db_session, pending_event):
|
|
pending_event.status = MaintenanceEventStatus.COMPLETED
|
|
db_session.commit()
|
|
|
|
result = end_maintenance(pending_event.id, db_session, mock_superset)
|
|
assert result["status"] == "already_completed"
|
|
# #endregion test_end_already_completed_event
|
|
|
|
# #region test_event_not_found [C:2] [TYPE Function]
|
|
# @BRIEF Nonexistent event.
|
|
def test_event_not_found(self, mock_superset, db_session):
|
|
result = end_maintenance("nonexistent", db_session, mock_superset)
|
|
assert result["status"] == "failed"
|
|
# #endregion test_event_not_found
|
|
|
|
|
|
# ── T026a/T030: end_all_maintenance tests ─────────────────────
|
|
|
|
class TestEndAllMaintenance:
|
|
"""Tests for end_all_maintenance."""
|
|
|
|
# #region test_end_all_active_events [C:2] [TYPE Function]
|
|
# @BRIEF End all active events.
|
|
def test_end_all_active_events(self, mock_superset, db_session):
|
|
# Create two active events
|
|
for i in range(2):
|
|
event = MaintenanceEvent(
|
|
tables=[f"raw.table{i}"],
|
|
start_time=datetime.now(timezone.utc),
|
|
status=MaintenanceEventStatus.ACTIVE,
|
|
environment_id="test-env",
|
|
)
|
|
db_session.add(event)
|
|
db_session.commit()
|
|
|
|
result = end_all_maintenance(db_session, mock_superset)
|
|
assert result["closed_events"] == 2
|
|
assert result["status"] == "completed"
|
|
# #endregion test_end_all_active_events
|
|
|
|
# #region test_no_active_events [C:2] [TYPE Function]
|
|
# @BRIEF No active events.
|
|
def test_no_active_events(self, mock_superset, db_session):
|
|
result = end_all_maintenance(db_session, mock_superset)
|
|
assert result["closed_events"] == 0
|
|
assert result["status"] == "completed"
|
|
# #endregion test_no_active_events
|
|
|
|
# #region test_mixed_active_and_completed [C:2] [TYPE Function]
|
|
# @BRIEF Mix of active and completed events — only active get ended.
|
|
def test_mixed_active_and_completed(self, mock_superset, db_session):
|
|
active = MaintenanceEvent(
|
|
tables=["raw.active"],
|
|
start_time=datetime.now(timezone.utc),
|
|
status=MaintenanceEventStatus.ACTIVE,
|
|
environment_id="test-env",
|
|
)
|
|
db_session.add(active)
|
|
|
|
completed = MaintenanceEvent(
|
|
tables=["raw.done"],
|
|
start_time=datetime.now(timezone.utc),
|
|
status=MaintenanceEventStatus.COMPLETED,
|
|
environment_id="test-env",
|
|
)
|
|
db_session.add(completed)
|
|
db_session.commit()
|
|
|
|
result = end_all_maintenance(db_session, mock_superset)
|
|
assert result["closed_events"] == 1
|
|
# #endregion test_mixed_active_and_completed
|
|
# #endregion test_maintenance_service
|