Add ruff config to exclude alembic E402, SQLAlchemy F821, and pre-existing E501 line-length issues. Auto-fix 69 unused imports and 2 f-strings without placeholders. Manually fix 8 unused variables. Apply ruff format to 127 files. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
157 lines
4.4 KiB
Python
157 lines
4.4 KiB
Python
"""Tests for push rollback NATS subscriber."""
|
|
|
|
import pytest
|
|
from unittest.mock import AsyncMock, patch
|
|
from uuid import uuid4
|
|
|
|
from app.services.push_rollback_subscriber import (
|
|
handle_push_rollback,
|
|
handle_push_alert,
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_rollback_triggers_restore():
|
|
"""Push rollback should call restore_config with the pre-push commit SHA."""
|
|
event = {
|
|
"device_id": str(uuid4()),
|
|
"tenant_id": str(uuid4()),
|
|
"push_operation_id": str(uuid4()),
|
|
"pre_push_commit_sha": "abc1234",
|
|
}
|
|
|
|
mock_session = AsyncMock()
|
|
mock_cm = AsyncMock()
|
|
mock_cm.__aenter__ = AsyncMock(return_value=mock_session)
|
|
mock_cm.__aexit__ = AsyncMock(return_value=False)
|
|
|
|
with (
|
|
patch(
|
|
"app.services.push_rollback_subscriber.restore_service.restore_config",
|
|
new_callable=AsyncMock,
|
|
return_value={"status": "committed"},
|
|
) as mock_restore,
|
|
patch(
|
|
"app.services.push_rollback_subscriber.AdminAsyncSessionLocal",
|
|
return_value=mock_cm,
|
|
),
|
|
):
|
|
await handle_push_rollback(event)
|
|
|
|
mock_restore.assert_called_once()
|
|
call_kwargs = mock_restore.call_args[1]
|
|
assert call_kwargs["device_id"] == event["device_id"]
|
|
assert call_kwargs["tenant_id"] == event["tenant_id"]
|
|
assert call_kwargs["commit_sha"] == "abc1234"
|
|
assert call_kwargs["db_session"] is mock_session
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_rollback_missing_fields_skips():
|
|
"""Rollback with missing fields should log warning and return."""
|
|
event = {"device_id": str(uuid4())} # missing tenant_id and commit_sha
|
|
|
|
with patch(
|
|
"app.services.push_rollback_subscriber.restore_service.restore_config",
|
|
new_callable=AsyncMock,
|
|
) as mock_restore:
|
|
await handle_push_rollback(event)
|
|
|
|
mock_restore.assert_not_called()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_rollback_failure_creates_alert():
|
|
"""When restore_config raises, an alert should be created."""
|
|
event = {
|
|
"device_id": str(uuid4()),
|
|
"tenant_id": str(uuid4()),
|
|
"pre_push_commit_sha": "abc1234",
|
|
}
|
|
|
|
mock_session = AsyncMock()
|
|
mock_cm = AsyncMock()
|
|
mock_cm.__aenter__ = AsyncMock(return_value=mock_session)
|
|
mock_cm.__aexit__ = AsyncMock(return_value=False)
|
|
|
|
with (
|
|
patch(
|
|
"app.services.push_rollback_subscriber.restore_service.restore_config",
|
|
new_callable=AsyncMock,
|
|
side_effect=RuntimeError("SSH failed"),
|
|
),
|
|
patch(
|
|
"app.services.push_rollback_subscriber.AdminAsyncSessionLocal",
|
|
return_value=mock_cm,
|
|
),
|
|
patch(
|
|
"app.services.push_rollback_subscriber._create_push_alert",
|
|
new_callable=AsyncMock,
|
|
) as mock_alert,
|
|
):
|
|
await handle_push_rollback(event)
|
|
|
|
mock_alert.assert_called_once_with(
|
|
event["device_id"],
|
|
event["tenant_id"],
|
|
"template (auto-rollback failed)",
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_alert_creates_alert_record():
|
|
"""Editor push alert should create a high-priority alert."""
|
|
event = {
|
|
"device_id": str(uuid4()),
|
|
"tenant_id": str(uuid4()),
|
|
"push_type": "editor",
|
|
}
|
|
|
|
with patch(
|
|
"app.services.push_rollback_subscriber._create_push_alert",
|
|
new_callable=AsyncMock,
|
|
) as mock_alert:
|
|
await handle_push_alert(event)
|
|
|
|
mock_alert.assert_called_once_with(
|
|
event["device_id"],
|
|
event["tenant_id"],
|
|
"editor",
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_alert_missing_fields_skips():
|
|
"""Alert with missing fields should skip."""
|
|
event = {"device_id": str(uuid4())} # missing tenant_id
|
|
|
|
with patch(
|
|
"app.services.push_rollback_subscriber._create_push_alert",
|
|
new_callable=AsyncMock,
|
|
) as mock_alert:
|
|
await handle_push_alert(event)
|
|
|
|
mock_alert.assert_not_called()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_alert_defaults_to_editor_push_type():
|
|
"""Alert without push_type should default to 'editor'."""
|
|
event = {
|
|
"device_id": str(uuid4()),
|
|
"tenant_id": str(uuid4()),
|
|
# no push_type
|
|
}
|
|
|
|
with patch(
|
|
"app.services.push_rollback_subscriber._create_push_alert",
|
|
new_callable=AsyncMock,
|
|
) as mock_alert:
|
|
await handle_push_alert(event)
|
|
|
|
mock_alert.assert_called_once_with(
|
|
event["device_id"],
|
|
event["tenant_id"],
|
|
"editor",
|
|
)
|