Files
ss-tools/backend/tests/integration/test_maintenance_lifecycle_e2e.py
root 632b730fff chore: migrate GRACE-Poly anchors to hierarchical dotted naming
Systematic rename of all semantic anchors (#region, [DEF], @RELATION)
across 1400+ files — backend Python, frontend Svelte/TS, specs, docs:
- Flat anchors become Namespace.Module.Entity
- @RELATION references updated to match new anchor paths
- Zero business logic changes
2026-07-22 11:48:15 +03:00

598 lines
28 KiB
Python

# #region Test.Integration.MaintenanceLifecycleE2E [C:5] [TYPE Module] [SEMANTICS test,integration,maintenance,e2e,superset,testcontainers]
# @BRIEF Full maintenance lifecycle E2E tests: seed PG table, register in Superset, create dashboard,
# exercise start_maintenance/end_maintenance orchestrators, verify postconditions in BOTH DB and Superset.
# @RELATION BINDS_TO -> [Services.Orchestrators.MaintenanceOrchestrators]
# @RELATION BINDS_TO -> [Core.DashboardsWrite.SupersetDashboardsWriteMixin]
# @RELATION BINDS_TO -> [Services.DashboardScanner.MaintenanceDashboardScanner]
# @RELATION BINDS_TO -> [Models.Maintenance.MaintenanceModels]
# @RELATION DEPENDS_ON -> [Test.Conftest.IntegrationTestConftest]
#
# @TEST_CONTRACT MaintenanceLifecycle ->
# {
# scenarios: [
# "start creates exactly one banner and updates dashboard layout",
# "repeated start call on already-processed event is idempotent",
# "end removes banner and transitions event to COMPLETED",
# "end restores dashboard layout after banner removal"
# ]
# }
# @TEST_EDGE: repeated_start_idempotent -> start_maintenance on ACTIVE event returns current status
# @TEST_EDGE: missing_superset_dataset -> start_maintenance returns "no_match" gracefully
# @TEST_EDGE: end_maintenance_after_removed -> idempotent end call returns "already_completed"
#
# @TEST_INVARIANT event_lifecycle -> VERIFIED_BY: [test_start_creates_banner_and_updates_layout, test_end_removes_banner_and_restores_state]
# @TEST_INVARIANT idempotent_start -> VERIFIED_BY: [Test.Integration.TestRepeatedStartIsIdempotent]
# @TEST_INVARIANT banner_uniqueness -> VERIFIED_BY: [Test.Integration.TestStartCreatesBannerAndUpdatesLayout]
#
# @PRE Docker daemon running; testcontainers PostgreSQL + Superset containers can start.
# @POST All test resources cleaned up (PG tables, Superset databases/datasets/dashboards/charts).
# @SIDE_EFFECT Creates and removes real Superset and PostgreSQL resources.
# @RATIONALE
# Previous integration tests only verify ORM models in isolation (test_maintenance_integration.py).
# This test drives the actual production orchestrator code path (start_maintenance / end_maintenance)
# with a real Superset instance and real PostgreSQL, verifying postconditions in both systems.
#
# The narrowest real production entrypoint is the orchestrator functions, not the API routes
# (which require TaskManager + auth). Calling start_maintenance() directly exercises:
# - find_affected_dashboards() — scans real Superset datasets
# - ensure_banner_chart() — creates real markdown chart in Superset
# - update_dashboard_layout() — modifies real dashboard position_json
# - _process_states_for_end() — removes chart and restores layout
#
# @REJECTED
# Calling the API route directly rejected — requires TaskManager auth, JWT/permission setup,
# and async TaskManager processing which adds ~30s per test without additional coverage.
# Mocking SupersetClient rejected — would test the orchestrator's SQL logic but not the
# actual Superset API integration.
from contextlib import suppress
from datetime import UTC, datetime, timedelta
import json
import pytest
from uuid import uuid4
from sqlalchemy import text
from src.models.maintenance import (
MaintenanceDashboardBanner,
MaintenanceDashboardBannerStatus,
MaintenanceDashboardState,
MaintenanceDashboardStateStatus,
MaintenanceEvent,
MaintenanceEventStatus,
MaintenanceSettings,
)
from src.services.maintenance._orchestrators import end_maintenance, start_maintenance
# #region Test.Integration.MaintenanceLifecycleE2E.Test [C:4] [TYPE Class]
# @BRIEF E2E tests for maintenance lifecycle with real Superset + PostgreSQL.
class TestMaintenanceLifecycleE2E:
"""Full maintenance lifecycle: seed, start, verify, end, verify."""
# #region Test.Integration.TestStartCreatesBannerAndUpdatesLayout [C:3] [TYPE Function]
# @BRIEF Seed PG table, register in Superset, create dashboard, start maintenance — verify banner + layout.
# @TEST_SCENARIO: start_creates_banner -> MaintenanceEvent transitions to ACTIVE,
# MaintenanceDashboardBanner created with ACTIVE status,
# MaintenanceDashboardState created with ACTIVE status,
# Superset dashboard layout includes MARKDOWN-banner- element.
@pytest.mark.integration
@pytest.mark.asyncio
async def test_start_creates_banner_and_updates_layout( # noqa: C901
self,
superset_client,
superset_db_url,
pg_engine,
db_session,
):
suffix = uuid4().hex[:10]
table_name = f"maint_e2e_start_{suffix}"
dashboard_title = f"Maintenance E2E Start {suffix}"
database_id = None
dataset_id = None
chart_id = None
dashboard_id = None
event_id = None
banner_id = None
# ── Step 1: Seed physical PostgreSQL table ──
with pg_engine.begin() as conn:
conn.execute(
text(f'CREATE TABLE public."{table_name}" (id INTEGER PRIMARY KEY, label VARCHAR(64))')
)
conn.execute(
text(f"""INSERT INTO public."{table_name}" VALUES (1, 'alpha'), (2, 'beta'), (3, 'gamma')""")
)
try:
# ── Step 2: Register database in Superset ──
from urllib.parse import urlparse
parsed = urlparse(superset_db_url)
database_uri = (
f"postgresql://test:test@{parsed.hostname}:{parsed.port}/test_translate"
)
db_resp = await superset_client.create_database(
database_name=f"Maint E2E Source {suffix}",
sqlalchemy_uri=database_uri,
)
database_id = int(
db_resp.get("id") or db_resp.get("result", {}).get("id", 0)
)
# ── Step 3: Create dataset ──
ds_resp = await superset_client.create_dataset(
table_name=table_name,
database=database_id,
schema_name="public",
)
dataset_id = int(
ds_resp.get("id") or ds_resp.get("result", {}).get("id", 0)
)
# ── Step 4: Create dashboard ──
dash_resp = await superset_client.create_dashboard(
dashboard_title=dashboard_title,
slug=f"maint-e2e-start-{suffix}",
published=True,
)
dashboard_id = int(
dash_resp.get("id") or dash_resp.get("result", {}).get("id", 0)
)
# ── Step 5: Create chart and add to dashboard ──
chart_resp = await superset_client.client.request(
method="POST",
endpoint="/chart/",
data={
"dashboards": [dashboard_id],
"datasource_id": dataset_id,
"datasource_type": "table",
"slice_name": f"Maint E2E Chart {suffix}",
"viz_type": "table",
"params": json.dumps({
"datasource": f"{dataset_id}__table",
"viz_type": "table",
"all_columns": ["id", "label"],
"row_limit": 100,
}),
},
)
chart_id = int(
chart_resp.get("id") or chart_resp.get("result", {}).get("id", 0)
)
await superset_client.client.request(
method="PUT",
endpoint=f"/dashboard/{dashboard_id}",
data={
"position_json": json.dumps({
f"CHART-{chart_id}": {
"id": f"CHART-{chart_id}",
"type": "CHART",
"meta": {"chartId": chart_id, "width": 12, "height": 50},
}
})
},
)
# ── Step 6: Ensure MaintenanceSettings exist ──
settings = db_session.query(MaintenanceSettings).filter(
MaintenanceSettings.id == "default"
).first()
if not settings:
settings = MaintenanceSettings(
id="default",
target_environment_id="test_env",
banner_template="Maintenance: {message} ({start_time} - {end_time})",
)
db_session.add(settings)
db_session.commit()
# ── Step 7: Create PENDING MaintenanceEvent ──
now = datetime.now(UTC)
event = MaintenanceEvent(
tables=[f"public.{table_name}"],
start_time=now,
end_time=now + timedelta(hours=2),
message="Scheduled maintenance - E2E test",
status=MaintenanceEventStatus.PENDING,
environment_id="test_env",
)
db_session.add(event)
db_session.commit()
event_id = event.id
# ── Step 8: Call start_maintenance orchestrator ──
result = await start_maintenance(event_id, db_session, superset_client)
db_session.commit()
# ── Verify postconditions ──
# 8a: Result indicates success
assert result["status"] in ("active", "partial"), f"Expected active/partial, got {result['status']}"
assert result["affected_dashboards"] >= 1, f"Expected >=1 dashboard, got {result['affected_dashboards']}"
# 8b: Event transitions to ACTIVE
db_session.expire_all()
updated_event = db_session.query(MaintenanceEvent).filter(
MaintenanceEvent.id == event_id
).first()
assert updated_event is not None
assert updated_event.status == MaintenanceEventStatus.ACTIVE, \
f"Expected ACTIVE, got {updated_event.status}"
# 8c: DashboardBanner created with ACTIVE status
banners = db_session.query(MaintenanceDashboardBanner).filter(
MaintenanceDashboardBanner.environment_id == "test_env",
MaintenanceDashboardBanner.status == MaintenanceDashboardBannerStatus.ACTIVE,
).all()
assert len(banners) >= 1, "Expected at least one active banner"
banner = banners[0]
banner_id = banner.id
assert banner.dashboard_id == dashboard_id, f"Expected dashboard_id={dashboard_id}, got {banner.dashboard_id}"
assert banner.chart_id is not None, "Expected chart_id to be set"
# 8d: DashboardState created with ACTIVE status
states = db_session.query(MaintenanceDashboardState).filter(
MaintenanceDashboardState.event_id == event_id,
MaintenanceDashboardState.dashboard_id == dashboard_id,
).all()
assert len(states) >= 1, "Expected at least one dashboard state"
assert states[0].status == MaintenanceDashboardStateStatus.ACTIVE, \
f"Expected ACTIVE, got {states[0].status}"
# 8e: Superset dashboard layout includes MARKDOWN-banner element
dashboard_detail = await superset_client.client.request(
method="GET",
endpoint=f"/dashboard/{dashboard_id}",
)
result_data = dashboard_detail.get("result", dashboard_detail)
position_json = json.loads(result_data.get("position_json", "{}"))
markdown_keys = [k for k in position_json if "MARKDOWN-banner" in k]
assert len(markdown_keys) >= 1, \
f"Expected MARKDOWN-banner key in layout, got keys: {list(position_json.keys())[:10]}"
# The chart_id is encoded in the key name: MARKDOWN-banner-{chart_id}
chart_ids_in_layout = set()
for k in position_json:
if k.startswith("MARKDOWN-banner-"):
try:
cid = int(k.split("-")[-1])
chart_ids_in_layout.add(cid)
except (ValueError, IndexError):
pass
assert banner.chart_id in chart_ids_in_layout, \
f"Layout MARKDOWN references chart_ids {chart_ids_in_layout}, expected {banner.chart_id}"
# 8f: Only ONE active banner for this (env, dashboard) pair
active_count = db_session.query(MaintenanceDashboardBanner).filter(
MaintenanceDashboardBanner.environment_id == "test_env",
MaintenanceDashboardBanner.dashboard_id == dashboard_id,
MaintenanceDashboardBanner.status == MaintenanceDashboardBannerStatus.ACTIVE,
).count()
assert active_count == 1, f"Expected exactly 1 active banner, got {active_count}"
finally:
# ── Cleanup ──
if event_id:
with suppress(Exception):
# End maintenance if still active
await end_maintenance(event_id, db_session, superset_client)
db_session.commit()
if banner_id:
with suppress(Exception):
b = db_session.query(MaintenanceDashboardBanner).filter(
MaintenanceDashboardBanner.id == banner_id
).first()
if b:
b.status = MaintenanceDashboardBannerStatus.REMOVED
db_session.commit()
if chart_id:
with suppress(Exception):
await superset_client.delete_chart(chart_id)
if dashboard_id:
with suppress(Exception):
await superset_client.delete_dashboard(dashboard_id)
if dataset_id:
with suppress(Exception):
await superset_client.delete_dataset(dataset_id)
if database_id:
with suppress(Exception):
await superset_client.delete_database(database_id)
with pg_engine.begin() as conn:
conn.execute(text(f'DROP TABLE IF EXISTS public."{table_name}"'))
# #endregion Test.Integration.TestStartCreatesBannerAndUpdatesLayout
# #region Test.Integration.TestRepeatedStartIsIdempotent [C:3] [TYPE Function]
# @BRIEF Call start_maintenance on an already-ACTIVE event — verify it returns current status.
# @TEST_EDGE: repeated_start_idempotent -> VERIFIED_BY: test_repeated_start_is_idempotent
@pytest.mark.integration
@pytest.mark.asyncio
async def test_repeated_start_is_idempotent(
self,
superset_client,
superset_db_url,
pg_engine,
db_session,
):
suffix = uuid4().hex[:10]
table_name = f"maint_e2e_idem_{suffix}"
dashboard_title = f"Maintenance E2E Idemp {suffix}"
# ── Setup: seed table, create Superset resources, create PENDING event, start once ──
with pg_engine.begin() as conn:
conn.execute(
text(f'CREATE TABLE public."{table_name}" (id INTEGER PRIMARY KEY, label VARCHAR(64))')
)
conn.execute(text(f"""INSERT INTO public."{table_name}" VALUES (1, 'x')"""))
database_id = dataset_id = chart_id = dashboard_id = event_id = None
try:
from urllib.parse import urlparse
parsed = urlparse(superset_db_url)
database_uri = f"postgresql://test:test@{parsed.hostname}:{parsed.port}/test_translate"
db_resp = await superset_client.create_database(
database_name=f"Maint Idem {suffix}", sqlalchemy_uri=database_uri,
)
database_id = int(db_resp.get("id") or db_resp.get("result", {}).get("id", 0))
ds_resp = await superset_client.create_dataset(
table_name=table_name, database=database_id, schema_name="public",
)
dataset_id = int(ds_resp.get("id") or ds_resp.get("result", {}).get("id", 0))
dash_resp = await superset_client.create_dashboard(
dashboard_title=dashboard_title, slug=f"maint-idem-{suffix}", published=True,
)
dashboard_id = int(dash_resp.get("id") or dash_resp.get("result", {}).get("id", 0))
chart_resp = await superset_client.client.request(
method="POST", endpoint="/chart/",
data={
"dashboards": [dashboard_id],
"datasource_id": dataset_id,
"datasource_type": "table",
"slice_name": f"Maint Idem Chart {suffix}",
"viz_type": "table",
"params": json.dumps({"datasource": f"{dataset_id}__table", "viz_type": "table", "row_limit": 100}),
},
)
chart_id = int(chart_resp.get("id") or chart_resp.get("result", {}).get("id", 0))
await superset_client.client.request(
method="PUT", endpoint=f"/dashboard/{dashboard_id}",
data={
"position_json": json.dumps({
f"CHART-{chart_id}": {
"id": f"CHART-{chart_id}", "type": "CHART",
"meta": {"chartId": chart_id, "width": 12, "height": 50},
}
})
},
)
# Create PENDING event
now = datetime.now(UTC)
event = MaintenanceEvent(
tables=[f"public.{table_name}"],
start_time=now, end_time=now + timedelta(hours=2),
status=MaintenanceEventStatus.PENDING, environment_id="test_env",
)
db_session.add(event)
db_session.commit()
event_id = event.id
# First call: start
result1 = await start_maintenance(event_id, db_session, superset_client)
db_session.commit()
assert result1["status"] in ("active", "partial"), f"First start failed: {result1}"
# Second call: start again — should be idempotent
result2 = await start_maintenance(event_id, db_session, superset_client)
db_session.commit()
# Verify idempotency: returns current status without re-creating
assert result2["status"] in ("active", "partial"), \
f"Idempotent start returned unexpected status: {result2['status']}"
# Verify still exactly ONE active banner
active_count = db_session.query(MaintenanceDashboardBanner).filter(
MaintenanceDashboardBanner.environment_id == "test_env",
MaintenanceDashboardBanner.dashboard_id == dashboard_id,
MaintenanceDashboardBanner.status == MaintenanceDashboardBannerStatus.ACTIVE,
).count()
assert active_count == 1, \
f"Expected exactly 1 active banner after idempotent start, got {active_count}"
# Verify event status unchanged (still ACTIVE)
db_session.expire_all()
ev = db_session.query(MaintenanceEvent).filter(MaintenanceEvent.id == event_id).first()
assert ev is not None
assert ev.status == MaintenanceEventStatus.ACTIVE, \
f"Expected ACTIVE after idempotent start, got {ev.status}"
finally:
if event_id:
with suppress(Exception):
await end_maintenance(event_id, db_session, superset_client)
db_session.commit()
for cid in ([chart_id] if chart_id else []):
with suppress(Exception):
await superset_client.delete_chart(cid)
for did in ([dashboard_id] if dashboard_id else []):
with suppress(Exception):
await superset_client.delete_dashboard(did)
for dsid in ([dataset_id] if dataset_id else []):
with suppress(Exception):
await superset_client.delete_dataset(dsid)
for dbid in ([database_id] if database_id else []):
with suppress(Exception):
await superset_client.delete_database(dbid)
with pg_engine.begin() as conn:
conn.execute(text(f'DROP TABLE IF EXISTS public."{table_name}"'))
# #endregion Test.Integration.TestRepeatedStartIsIdempotent
# #region Test.Integration.TestEndRemovesBannerAndRestoresState [C:3] [TYPE Function]
# @BRIEF Full lifecycle: start then end — verify COMPLETED event, REMOVED banner, restored state.
# @TEST_SCENARIO: end_removes_banner -> Event transitions to COMPLETED,
# Banner transitions to REMOVED, DashboardState transitions to REMOVED,
# MARKDOWN-banner element removed from Superset dashboard layout.
@pytest.mark.integration
@pytest.mark.asyncio
async def test_end_removes_banner_and_restores_state(
self,
superset_client,
superset_db_url,
pg_engine,
db_session,
):
suffix = uuid4().hex[:10]
table_name = f"maint_e2e_end_{suffix}"
with pg_engine.begin() as conn:
conn.execute(
text(f'CREATE TABLE public."{table_name}" (id INTEGER PRIMARY KEY, label VARCHAR(64))')
)
conn.execute(text(f"""INSERT INTO public."{table_name}" VALUES (1, 'end-test')"""))
database_id = dataset_id = chart_id = dashboard_id = event_id = None
try:
from urllib.parse import urlparse
parsed = urlparse(superset_db_url)
database_uri = f"postgresql://test:test@{parsed.hostname}:{parsed.port}/test_translate"
db_resp = await superset_client.create_database(
database_name=f"Maint End {suffix}", sqlalchemy_uri=database_uri,
)
database_id = int(db_resp.get("id") or db_resp.get("result", {}).get("id", 0))
ds_resp = await superset_client.create_dataset(
table_name=table_name, database=database_id, schema_name="public",
)
dataset_id = int(ds_resp.get("id") or ds_resp.get("result", {}).get("id", 0))
dash_resp = await superset_client.create_dashboard(
dashboard_title=f"Maintenance E2E End {suffix}",
slug=f"maint-end-{suffix}", published=True,
)
dashboard_id = int(dash_resp.get("id") or dash_resp.get("result", {}).get("id", 0))
chart_resp = await superset_client.client.request(
method="POST", endpoint="/chart/",
data={
"dashboards": [dashboard_id],
"datasource_id": dataset_id,
"datasource_type": "table",
"slice_name": f"Maint End Chart {suffix}",
"viz_type": "table",
"params": json.dumps({"datasource": f"{dataset_id}__table", "viz_type": "table", "row_limit": 100}),
},
)
chart_id = int(chart_resp.get("id") or chart_resp.get("result", {}).get("id", 0))
await superset_client.client.request(
method="PUT", endpoint=f"/dashboard/{dashboard_id}",
data={
"position_json": json.dumps({
f"CHART-{chart_id}": {
"id": f"CHART-{chart_id}", "type": "CHART",
"meta": {"chartId": chart_id, "width": 12, "height": 50},
}
})
},
)
now = datetime.now(UTC)
event = MaintenanceEvent(
tables=[f"public.{table_name}"],
start_time=now, end_time=now + timedelta(hours=2),
status=MaintenanceEventStatus.PENDING, environment_id="test_env",
)
db_session.add(event)
db_session.commit()
event_id = event.id
# ── Start ──
start_result = await start_maintenance(event_id, db_session, superset_client)
db_session.commit()
assert start_result["status"] in ("active", "partial"), f"Start failed: {start_result}"
assert start_result["affected_dashboards"] >= 1
# Capture banner_id before end
banner = db_session.query(MaintenanceDashboardBanner).filter(
MaintenanceDashboardBanner.environment_id == "test_env",
MaintenanceDashboardBanner.status == MaintenanceDashboardBannerStatus.ACTIVE,
).first()
assert banner is not None
banner_id = banner.id
# Verify banner chart exists in Superset
if banner.chart_id:
try:
chart_data = await superset_client.get_chart(banner.chart_id)
assert chart_data is not None, "Banner chart not found in Superset"
except Exception as e:
pytest.fail(f"Banner chart {banner.chart_id} not found in Superset: {e}")
# ── End ──
end_result = await end_maintenance(event_id, db_session, superset_client)
db_session.commit()
assert end_result["status"] == "completed", f"End failed: {end_result}"
assert end_result["removed_from"] >= 1, f"Expected >=1 removed, got {end_result}"
# ── Verify postconditions ──
# Event is COMPLETED
db_session.expire_all()
ev = db_session.query(MaintenanceEvent).filter(MaintenanceEvent.id == event_id).first()
assert ev is not None
assert ev.status == MaintenanceEventStatus.COMPLETED, \
f"Expected COMPLETED, got {ev.status}"
# Banner is REMOVED
b = db_session.query(MaintenanceDashboardBanner).filter(
MaintenanceDashboardBanner.id == banner_id
).first()
assert b is not None
assert b.status == MaintenanceDashboardBannerStatus.REMOVED, \
f"Expected REMOVED banner, got {b.status}"
# DashboardState is REMOVED
states = db_session.query(MaintenanceDashboardState).filter(
MaintenanceDashboardState.event_id == event_id,
).all()
assert len(states) >= 1
assert all(
s.status == MaintenanceDashboardStateStatus.REMOVED for s in states
), f"Not all states are REMOVED: {[s.status for s in states]}"
# MARKDOWN-banner element removed from dashboard layout
dash_detail = await superset_client.client.request(
method="GET", endpoint=f"/dashboard/{dashboard_id}",
)
result_data = dash_detail.get("result", dash_detail)
position_json = json.loads(result_data.get("position_json", "{}"))
markdown_keys = [k for k in position_json if "MARKDOWN-banner" in k]
assert len(markdown_keys) == 0, \
f"Expected no MARKDOWN-banner keys after end, got {markdown_keys}"
finally:
with suppress(Exception):
await end_maintenance(event_id, db_session, superset_client) if event_id else None
db_session.commit()
for cid in ([chart_id] if chart_id else []):
with suppress(Exception):
await superset_client.delete_chart(cid)
for did in ([dashboard_id] if dashboard_id else []):
with suppress(Exception):
await superset_client.delete_dashboard(did)
for dsid in ([dataset_id] if dataset_id else []):
with suppress(Exception):
await superset_client.delete_dataset(dsid)
for dbid in ([database_id] if database_id else []):
with suppress(Exception):
await superset_client.delete_database(dbid)
with pg_engine.begin() as conn:
conn.execute(text(f'DROP TABLE IF EXISTS public."{table_name}"'))
# #endregion Test.Integration.TestEndRemovesBannerAndRestoresState
# #endregion Test.Integration.MaintenanceLifecycleE2E.Test
# #endregion Test.Integration.MaintenanceLifecycleE2E