Files
ss-tools/backend/tests/test_maintenance_api.py

687 lines
28 KiB
Python

# #region Test.MaintenanceApi.TestMaintenanceApi [C:3] [TYPE TestModule] [SEMANTICS test, maintenance, api, fastapi, pytest]
# @BRIEF Contract tests for Maintenance Banner API endpoints — T016, T025.
# Tests POST /start, POST /{id}/end, POST /end-all, GET /events, GET /dashboard-banners,
# GET /settings, PUT /settings.
# @LAYER Test
# @RELATION BINDS_TO -> [Api.Routes.MaintenanceRoutesModule]
# @TEST_CONTRACT: POST /api/maintenance/start -> 202 {task_id, maintenance_id, status}
# @TEST_CONTRACT: POST /api/maintenance/{id}/end -> 202 {task_id, status}
# @TEST_CONTRACT: POST /api/maintenance/end-all -> 202 {task_id, status}
# @TEST_EDGE: missing_tables -> 422 validation error
# @TEST_EDGE: no_prod_environment -> 422 validation error
# @TEST_EDGE: end_time_before_start_time -> 400 validation error
# @TEST_EDGE: non_existent_event_end -> 404 not found
from datetime import UTC, datetime, timedelta
import pytest
# ── Patch GitService at module level to prevent /app/storage/repositories error ──
# Only patch git_service (5-line shim), NOT git._base (233 lines — breaks other tests).
# See test_api_key_routes.py for identical pattern.
import sys
from unittest.mock import AsyncMock, MagicMock
_SAVED_GIT_SERVICE = sys.modules.get('src.services.git_service')
_mock_git_svc_cls = MagicMock()
_mock_git_svc_cls.return_value = MagicMock()
sys.modules['src.services.git_service'] = MagicMock(GitService=_mock_git_svc_cls)
@pytest.fixture(scope="module", autouse=True)
def _restore_git_modules():
"""Restore real git_service module after this module's tests finish."""
yield
if _SAVED_GIT_SERVICE is not None:
sys.modules['src.services.git_service'] = _SAVED_GIT_SERVICE
else:
sys.modules.pop('src.services.git_service', None)
# ── Fixtures ──────────────────────────────────────────────────
@pytest.fixture
def client():
"""Create a TestClient with all dependencies mocked."""
from fastapi.testclient import TestClient
from src.app import app
return TestClient(app)
@pytest.fixture(autouse=True)
def auth_mock(client, mock_db):
"""Create a valid API key with maintenance permissions and mock JWT user."""
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
# Create API key for mutation endpoints
raw, prefix, key_hash = generate_api_key()
api_key = APIKey(
key_hash=key_hash,
prefix=prefix,
name="Test API 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 JWT user for read-only endpoints
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():
"""Patch get_db dependency to return a clean in-memory session.
Uses StaticPool so all Session() instances share the same connection,
preventing SQLite threading issues where each :memory: connection
creates a separate database.
Creates all tables on the shared Base.metadata.
"""
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from sqlalchemy.pool import StaticPool
from src.dependencies import get_db
from src.models.maintenance import DashboardScope, MaintenanceSettings
from src.models.mapping import Base
# StaticPool ensures all sessions share the same in-memory connection
engine = create_engine(
"sqlite:///:memory:",
poolclass=StaticPool,
connect_args={"check_same_thread": False},
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="Test: {message} ({start_time}-{end_time})",
dashboard_scope=DashboardScope.PUBLISHED_ONLY,
excluded_dashboard_ids=[],
forced_dashboard_ids=[],
)
session.add(settings)
session.commit()
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_config_manager():
"""Patch get_config_manager to expose a test environment for start validation."""
from src.dependencies import get_config_manager
from src.core.config_manager import ConfigManager
from src.core.config_models import Environment
mock_cm = MagicMock(spec=ConfigManager)
test_env = Environment(
id="test-env",
name="Test Env",
url="https://test.example.com",
username="test",
password="test",
)
mock_cm.get_environments.return_value = [test_env]
from src.app import app
app.dependency_overrides[get_config_manager] = lambda: mock_cm
yield mock_cm
app.dependency_overrides.pop(get_config_manager, None)
@pytest.fixture
def mock_task_manager():
"""Patch get_task_manager to return a mock."""
from src.dependencies import get_task_manager
mock_tm = MagicMock()
mock_task = MagicMock()
mock_task.id = "test-task-id-123"
mock_tm.create_task = AsyncMock(return_value=mock_task)
mock_tm.broadcast_maintenance_event = AsyncMock()
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)
# ── T016: POST /api/maintenance/start ─────────────────────────
class TestStartEndpoint:
"""Contract tests for POST /api/maintenance/start."""
# #region Test.MaintenanceApi.TestStartReturns202 [C:2] [TYPE Function]
# @BRIEF Valid request returns 202 with task_id and maintenance_id.
def test_start_returns_202(self, client, mock_db, mock_task_manager, mock_config_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": "Test maintenance",
"environment_id": "test-env",
},
)
assert response.status_code == 202
data = response.json()
assert "task_id" in data
assert "maintenance_id" in data
assert data["status"] == "pending"
# #endregion Test.MaintenanceApi.TestStartReturns202
# #region Test.MaintenanceApi.TestStartMissingTables [C:2] [TYPE Function]
# @BRIEF Missing tables field returns 422.
def test_start_missing_tables(self, client, mock_db, mock_task_manager, mock_config_manager):
response = client.post(
"/api/maintenance/start",
json={
"start_time": (datetime.now(UTC) + timedelta(hours=1)).isoformat(),
},
)
assert response.status_code == 422
# #endregion Test.MaintenanceApi.TestStartMissingTables
# #region Test.MaintenanceApi.TestStartWithoutProdEnvironment [C:2] [TYPE Function]
# @BRIEF An environment-less start is rejected safely when no PROD target exists.
# @TEST_EDGE: no_prod_environment -> 422 validation error
def test_start_without_environment_requires_configured_prod(
self, client, mock_db, mock_task_manager, mock_config_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": "Test maintenance",
},
)
assert response.status_code == 422
assert response.json()["detail"] == "No production environments are configured for maintenance"
mock_task_manager.create_task.assert_not_awaited()
# #endregion Test.MaintenanceApi.TestStartWithoutProdEnvironment
# #region Test.MaintenanceApi.TestStartAllProductionEnvironments [C:3] [TYPE Function]
# @BRIEF Omitting environment_id starts one event per configured PROD environment.
def test_start_without_environment_fans_out_to_all_prod(
self, client, mock_db, mock_task_manager, mock_config_manager
):
from src.core.config_models import Environment
mock_config_manager.get_environments.return_value = [
Environment(id="dev", name="DEV", url="https://dev.example.com", username="test", password="test"),
Environment(id="prod-a", name="PROD A", url="https://prod-a.example.com", username="test", password="test", stage="PROD"),
Environment(id="prod-b", name="PROD B", url="https://prod-b.example.com", username="test", password="test", is_production=True),
]
mock_task_manager.create_task.side_effect = [
MagicMock(id="task-prod-a"),
MagicMock(id="task-prod-b"),
]
response = client.post(
"/api/maintenance/start",
json={
"tables": ["raw.sales"],
"start_time": (datetime.now(UTC) + timedelta(hours=1)).isoformat(),
"message": "Scheduled data update",
},
)
assert response.status_code == 202
data = response.json()
assert data["status"] == "pending"
assert [(item["environment_id"], item["task_id"]) for item in data["events"]] == [
("prod-a", "task-prod-a"),
("prod-b", "task-prod-b"),
]
assert mock_task_manager.create_task.await_count == 2
assert [call.kwargs["params"]["environment_id"] for call in mock_task_manager.create_task.await_args_list] == [
"prod-a",
"prod-b",
]
# #endregion Test.MaintenanceApi.TestStartAllProductionEnvironments
def test_start_idempotency_is_scoped_to_environment(
self, client, mock_db, mock_task_manager, mock_config_manager
):
from src.core.config_models import Environment
mock_config_manager.get_environments.return_value = [
Environment(id="test-env", name="Test", url="https://test.example.com", username="test", password="test"),
Environment(id="other-env", name="Other", url="https://other.example.com", username="test", password="test"),
]
payload = {
"tables": ["raw.sales"],
"start_time": (datetime.now(UTC) + timedelta(hours=1)).isoformat(),
}
assert client.post("/api/maintenance/start", json={**payload, "environment_id": "test-env"}).status_code == 202
assert client.post("/api/maintenance/start", json={**payload, "environment_id": "other-env"}).status_code == 202
assert mock_task_manager.create_task.await_count == 2
def test_start_openapi_includes_single_and_all_prod_examples(self, client):
operation = client.get("/openapi.json").json()["paths"]["/api/maintenance/start"]["post"]
examples = operation["requestBody"]["content"]["application/json"]["examples"]
assert "single_environment" in examples
assert examples["single_environment"]["value"]["environment_id"] == "ss-preprod"
assert "environment_id" not in examples["all_production_environments"]["value"]
# #region Test.MaintenanceApi.TestStartEndTimeBeforeStart [C:2] [TYPE Function]
# @BRIEF end_time before start_time returns 400.
def test_start_end_time_before_start(self, client, mock_db, mock_task_manager, mock_config_manager):
response = client.post(
"/api/maintenance/start",
json={
"tables": ["raw.sales"],
"start_time": (datetime.now(UTC) + timedelta(hours=3)).isoformat(),
"end_time": (datetime.now(UTC) + timedelta(hours=1)).isoformat(),
"environment_id": "test-env",
},
)
assert response.status_code == 400
# #endregion Test.MaintenanceApi.TestStartEndTimeBeforeStart
# #region Test.MaintenanceApi.TestStartTooManyTables [C:2] [TYPE Function]
# @BRIEF More than 100 tables returns 422 (Pydantic validation).
def test_start_too_many_tables(self, client, mock_db, mock_task_manager, mock_config_manager):
tables = [f"raw.table{i}" for i in range(101)]
response = client.post(
"/api/maintenance/start",
json={
"tables": tables,
"start_time": (datetime.now(UTC) + timedelta(hours=1)).isoformat(),
"environment_id": "test-env",
},
)
assert response.status_code == 422
# #endregion Test.MaintenanceApi.TestStartTooManyTables
# #region Test.MaintenanceApi.TestStartIdempotency [C:2] [TYPE Function]
# @BRIEF Same (tables, start, end) returns already_active.
def test_start_idempotency(self, client, mock_db, mock_task_manager, mock_config_manager):
from src.models.maintenance import MaintenanceEvent, MaintenanceEventStatus
payload = {
"tables": ["raw.sales"],
"start_time": (datetime.now(UTC) + timedelta(hours=1)).isoformat(),
"end_time": (datetime.now(UTC) + timedelta(hours=3)).isoformat(),
"environment_id": "test-env",
}
# First call creates event
response1 = client.post("/api/maintenance/start", json=payload)
assert response1.status_code == 202
# Manually set event to ACTIVE so idempotency check finds it
maintenance_id = response1.json()["maintenance_id"]
event = mock_db.query(MaintenanceEvent).filter(
MaintenanceEvent.id == maintenance_id
).first()
if event:
event.status = MaintenanceEventStatus.ACTIVE
mock_db.commit()
# Second call with same params — idempotent
response2 = client.post("/api/maintenance/start", json=payload)
assert response2.status_code == 202
data = response2.json()
# Should have maintenance_id and status
assert data.get("maintenance_id") or data.get("status")
# #endregion Test.MaintenanceApi.TestStartIdempotency
# #region Test.MaintenanceApi.TestStartUnknownEnvironment [C:2] [TYPE Function]
# @BRIEF Unknown environment_id returns 404 synchronously (no stuck PENDING event).
# @TEST_EDGE: invalid_env -> start returns 404, no event row created.
def test_start_unknown_environment(self, client, mock_db, mock_task_manager, mock_config_manager):
from src.models.maintenance import MaintenanceEvent
payload = {
"tables": ["raw.sales"],
"start_time": (datetime.now(UTC) + timedelta(hours=1)).isoformat(),
"environment_id": "does-not-exist",
}
response = client.post("/api/maintenance/start", json=payload)
assert response.status_code == 404
# No event row should have been created
assert mock_db.query(MaintenanceEvent).count() == 0
# #endregion Test.MaintenanceApi.TestStartUnknownEnvironment
# ── T025: POST /api/maintenance/{id}/end ──────────────────────
class TestEndEndpoint:
"""Contract tests for POST /api/maintenance/{id}/end."""
# #region Test.MaintenanceApi.TestEndReturns202 [C:2] [TYPE Function]
# @BRIEF Valid maintenance_id returns 202.
def test_end_returns_202(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.ACTIVE,
environment_id="test-env",
)
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 "task_id" in data
# #endregion Test.MaintenanceApi.TestEndReturns202
# #region Test.MaintenanceApi.TestEndNotFound [C:2] [TYPE Function]
# @BRIEF Non-existent maintenance_id returns 404.
def test_end_not_found(self, client, mock_db, mock_task_manager):
response = client.post("/api/maintenance/nonexistent-id/end")
assert response.status_code == 404
# #endregion Test.MaintenanceApi.TestEndNotFound
# ── POST /api/maintenance/end-all ─────────────────────────────
class TestEndAllEndpoint:
"""Contract tests for POST /api/maintenance/end-all."""
# #region Test.MaintenanceApi.TestEndAllReturns202 [C:2] [TYPE Function]
# @BRIEF end-all returns 202.
def test_end_all_returns_202(self, client, mock_db, mock_task_manager):
response = client.post("/api/maintenance/end-all")
assert response.status_code == 202
data = response.json()
assert "task_id" in data
assert data["status"] == "pending"
# #endregion Test.MaintenanceApi.TestEndAllReturns202
# ── GET /api/maintenance/events ───────────────────────────────
class TestEventsEndpoint:
"""Contract tests for GET /api/maintenance/events."""
# #region Test.MaintenanceApi.TestListEvents [C:2] [TYPE Function]
# @BRIEF Returns active and completed events.
def test_list_events(self, client, mock_db, mock_task_manager):
from src.models.maintenance import MaintenanceEvent, MaintenanceEventStatus
active = MaintenanceEvent(
tables=["raw.active"],
start_time=datetime.now(UTC),
status=MaintenanceEventStatus.ACTIVE,
environment_id="test-env",
)
mock_db.add(active)
completed = MaintenanceEvent(
tables=["raw.done"],
start_time=datetime.now(UTC),
status=MaintenanceEventStatus.COMPLETED,
environment_id="test-env",
)
mock_db.add(completed)
mock_db.commit()
response = client.get("/api/maintenance/events")
assert response.status_code == 200
data = response.json()
assert "active" in data
assert "completed" in data
# #endregion Test.MaintenanceApi.TestListEvents
# #region Test.MaintenanceApi.TestHistoricalDashboardCounts [C:2] [TYPE Function]
# @BRIEF Terminal events retain every linked dashboard state while active events expose only live banners.
def test_historical_dashboard_counts_include_removed_states(self, client, mock_db, mock_task_manager, monkeypatch):
from src.api.routes.maintenance import _routes
from src.models.maintenance import (
MaintenanceDashboardState,
MaintenanceDashboardStateStatus,
MaintenanceEvent,
MaintenanceEventStatus,
)
active = MaintenanceEvent(
tables=["raw.active"], start_time=datetime.now(UTC), status=MaintenanceEventStatus.ACTIVE, environment_id="test-env"
)
completed = MaintenanceEvent(
tables=["raw.completed"], start_time=datetime.now(UTC), status=MaintenanceEventStatus.COMPLETED, environment_id="test-env"
)
mock_db.add_all([active, completed])
mock_db.flush()
mock_db.add_all([
MaintenanceDashboardState(event_id=active.id, dashboard_id=10, status=MaintenanceDashboardStateStatus.ACTIVE),
MaintenanceDashboardState(event_id=active.id, dashboard_id=11, status=MaintenanceDashboardStateStatus.REMOVED),
MaintenanceDashboardState(event_id=completed.id, dashboard_id=20, status=MaintenanceDashboardStateStatus.REMOVED),
MaintenanceDashboardState(event_id=completed.id, dashboard_id=21, status=MaintenanceDashboardStateStatus.REMOVAL_FAILED),
])
mock_db.commit()
monkeypatch.setattr(_routes, "get_config_manager", lambda: (_ for _ in ()).throw(AssertionError("list must not call Superset")))
response = client.get("/api/maintenance/events")
assert response.status_code == 200
payload = response.json()
assert payload["active"][0]["affected_count"] == 1
assert payload["completed"][0]["affected_count"] == 2
assert payload["completed"][0]["dashboards"] == []
# #endregion Test.MaintenanceApi.TestHistoricalDashboardCounts
# #region Test.MaintenanceApi.TestEventDashboardDetails [C:2] [TYPE Function]
# @BRIEF Expanding a terminal event returns its historical dashboard IDs on demand.
def test_event_dashboard_details_include_terminal_states(self, client, mock_db, mock_task_manager, monkeypatch):
from src.api.routes.maintenance import _routes
from src.models.maintenance import (
MaintenanceDashboardState,
MaintenanceDashboardStateStatus,
MaintenanceEvent,
MaintenanceEventStatus,
)
event = MaintenanceEvent(
tables=["raw.completed"], start_time=datetime.now(UTC), status=MaintenanceEventStatus.COMPLETED, environment_id="test-env"
)
mock_db.add(event)
mock_db.flush()
mock_db.add_all([
MaintenanceDashboardState(event_id=event.id, dashboard_id=20, status=MaintenanceDashboardStateStatus.REMOVED),
MaintenanceDashboardState(event_id=event.id, dashboard_id=21, status=MaintenanceDashboardStateStatus.REMOVAL_FAILED),
])
mock_db.commit()
monkeypatch.setattr(_routes, "get_config_manager", lambda: MagicMock(get_environments=lambda: []))
response = client.get(f"/api/maintenance/events/{event.id}/dashboards")
assert response.status_code == 200
assert response.json() == [{"id": 20, "title": "20"}, {"id": 21, "title": "21"}]
# #endregion Test.MaintenanceApi.TestEventDashboardDetails
# #region Test.MaintenanceApi.TestExpiredEventAutoEnds [C:2] [TYPE Function]
# @BRIEF Event past end_time with ACTIVE status → create_task called with auto-end params.
def test_expired_event_auto_ends(self, client, mock_db, mock_task_manager):
from datetime import timedelta
from src.models.maintenance import MaintenanceEvent, MaintenanceEventStatus
event = MaintenanceEvent(
tables=["raw.sales"],
start_time=datetime.now(UTC) - timedelta(hours=2),
end_time=datetime.now(UTC) - timedelta(hours=1),
message="Expired maintenance",
status=MaintenanceEventStatus.ACTIVE,
environment_id="test-env",
# Auto-expiry honors the documented invariant: only auto_end=True events
# are auto-ended; end_time alone stays informational.
auto_end=True,
)
mock_db.add(event)
mock_db.commit()
response = client.get("/api/maintenance/events")
assert response.status_code == 200
mock_task_manager.create_task.assert_called_once_with(
plugin_id="maintenance_banner_apply",
params={
"operation": "end",
"event_id": event.id,
"environment_id": "test-env",
"user": "system",
},
)
# #endregion Test.MaintenanceApi.TestExpiredEventAutoEnds
# #region Test.MaintenanceApi.TestFutureEndTimeSkipped [C:2] [TYPE Function]
# @BRIEF Event with future end_time → no task created, skipped by < now filter.
def test_future_end_time_skipped(self, client, mock_db, mock_task_manager):
from datetime import timedelta
from src.models.maintenance import MaintenanceEvent, MaintenanceEventStatus
event = MaintenanceEvent(
tables=["raw.sales"],
start_time=datetime.now(UTC) + timedelta(hours=1),
end_time=datetime.now(UTC) + timedelta(hours=3),
message="Future maintenance",
status=MaintenanceEventStatus.ACTIVE,
environment_id="test-env",
)
mock_db.add(event)
mock_db.commit()
response = client.get("/api/maintenance/events")
assert response.status_code == 200
mock_task_manager.create_task.assert_not_called()
# #endregion Test.MaintenanceApi.TestFutureEndTimeSkipped
# #region Test.MaintenanceApi.TestNoEndTimeSkipped [C:2] [TYPE Function]
# @BRIEF Event with end_time=None → no task created, skipped by isnot(None) filter.
def test_no_end_time_skipped(self, client, mock_db, mock_task_manager):
from datetime import timedelta
from src.models.maintenance import MaintenanceEvent, MaintenanceEventStatus
event = MaintenanceEvent(
tables=["raw.sales"],
start_time=datetime.now(UTC) - timedelta(hours=2),
end_time=None,
message="No end time",
status=MaintenanceEventStatus.ACTIVE,
environment_id="test-env",
)
mock_db.add(event)
mock_db.commit()
response = client.get("/api/maintenance/events")
assert response.status_code == 200
mock_task_manager.create_task.assert_not_called()
# #endregion Test.MaintenanceApi.TestNoEndTimeSkipped
# ── GET /api/maintenance/dashboard-banners ────────────────────
class TestDashboardBannersEndpoint:
"""Contract tests for GET /api/maintenance/dashboard-banners."""
# #region Test.MaintenanceApi.TestListBanners [C:2] [TYPE Function]
# @BRIEF Returns list of active banners.
def test_list_banners(self, client, mock_db, mock_task_manager):
from src.models.maintenance import (
MaintenanceDashboardBanner,
MaintenanceDashboardBannerStatus,
)
banner = MaintenanceDashboardBanner(
environment_id="test-env",
dashboard_id=101,
chart_id=12345,
banner_text="Test",
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()
assert isinstance(data, list)
# #endregion Test.MaintenanceApi.TestListBanners
# ── GET /api/maintenance/settings ─────────────────────────────
class TestSettingsEndpoint:
"""Contract tests for GET/PUT /api/maintenance/settings."""
# #region Test.MaintenanceApi.TestGetSettings [C:2] [TYPE Function]
# @BRIEF GET returns settings.
def test_get_settings(self, client, mock_db, mock_task_manager):
response = client.get("/api/maintenance/settings")
assert response.status_code == 200
data = response.json()
assert "target_environment_id" in data
assert data["target_environment_id"] == "test-env"
assert data["default_message"]
# #endregion Test.MaintenanceApi.TestGetSettings
# #region Test.MaintenanceApi.TestPutSettings [C:2] [TYPE Function]
# @BRIEF PUT updates settings.
def test_put_settings(self, client, mock_db, mock_task_manager):
response = client.put(
"/api/maintenance/settings",
json={
"display_timezone": "Europe/Moscow",
"default_message": "Выполняется обновление витрин.",
},
)
assert response.status_code == 200
data = response.json()
assert data["display_timezone"] == "Europe/Moscow"
assert data["default_message"] == "Выполняется обновление витрин."
# #endregion Test.MaintenanceApi.TestPutSettings
# #endregion Test.MaintenanceApi.TestMaintenanceApi