feat: The Other Dude v9.0.1 — full-featured email system
ci: add GitHub Pages deployment workflow for docs site Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
211
backend/tests/test_restore_preview.py
Normal file
211
backend/tests/test_restore_preview.py
Normal file
@@ -0,0 +1,211 @@
|
||||
"""Tests for the preview-restore endpoint."""
|
||||
|
||||
import uuid
|
||||
from unittest.mock import AsyncMock, patch, MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
class TestPreviewRestoreEndpointExists:
|
||||
"""Verify the preview-restore route is registered on the config_backups router."""
|
||||
|
||||
def test_router_has_preview_restore_route(self):
|
||||
from app.routers.config_backups import router
|
||||
|
||||
paths = [r.path for r in router.routes]
|
||||
assert any("preview-restore" in p for p in paths), (
|
||||
f"No preview-restore route found. Routes: {paths}"
|
||||
)
|
||||
|
||||
def test_preview_restore_route_is_post(self):
|
||||
from app.routers.config_backups import router
|
||||
|
||||
for route in router.routes:
|
||||
if hasattr(route, "path") and "preview-restore" in route.path:
|
||||
assert "POST" in route.methods, (
|
||||
f"preview-restore route should be POST, got {route.methods}"
|
||||
)
|
||||
break
|
||||
else:
|
||||
pytest.fail("No preview-restore route found")
|
||||
|
||||
|
||||
class TestPreviewRestoreFunction:
|
||||
"""Test the preview_restore handler logic."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_preview_returns_impact_analysis(self):
|
||||
"""preview_restore should return diff, categories, warnings, validation."""
|
||||
from app.routers.config_backups import preview_restore, RestoreRequest
|
||||
|
||||
tenant_id = uuid.uuid4()
|
||||
device_id = uuid.uuid4()
|
||||
|
||||
current_export = "/ip address\nadd address=192.168.1.1/24 interface=ether1\n"
|
||||
target_export = "/ip address\nadd address=10.0.0.1/24 interface=ether1\n"
|
||||
|
||||
mock_db = AsyncMock()
|
||||
mock_user = MagicMock()
|
||||
mock_request = MagicMock()
|
||||
body = RestoreRequest(commit_sha="abc1234")
|
||||
|
||||
# Mock device query result
|
||||
mock_device = MagicMock()
|
||||
mock_device.ip_address = "192.168.88.1"
|
||||
mock_device.encrypted_credentials_transit = "vault:v1:abc"
|
||||
mock_device.encrypted_credentials = None
|
||||
mock_device.tenant_id = tenant_id
|
||||
|
||||
mock_scalar = MagicMock()
|
||||
mock_scalar.scalar_one_or_none.return_value = mock_device
|
||||
mock_db.execute.return_value = mock_scalar
|
||||
|
||||
with patch(
|
||||
"app.routers.config_backups._check_tenant_access",
|
||||
new_callable=AsyncMock,
|
||||
), patch(
|
||||
"app.routers.config_backups.limiter.enabled",
|
||||
False,
|
||||
), patch(
|
||||
"app.routers.config_backups.git_store.read_file",
|
||||
return_value=target_export.encode(),
|
||||
), patch(
|
||||
"app.routers.config_backups.backup_service.capture_export",
|
||||
new_callable=AsyncMock,
|
||||
return_value=current_export,
|
||||
), patch(
|
||||
"app.routers.config_backups.decrypt_credentials_hybrid",
|
||||
new_callable=AsyncMock,
|
||||
return_value='{"username": "admin", "password": "pass"}',
|
||||
), patch(
|
||||
"app.routers.config_backups.settings",
|
||||
):
|
||||
result = await preview_restore(
|
||||
request=mock_request,
|
||||
tenant_id=tenant_id,
|
||||
device_id=device_id,
|
||||
body=body,
|
||||
db=mock_db,
|
||||
current_user=mock_user,
|
||||
)
|
||||
|
||||
assert "diff" in result
|
||||
assert "categories" in result
|
||||
assert "warnings" in result
|
||||
assert "validation" in result
|
||||
# Both exports have /ip address with different commands
|
||||
assert isinstance(result["categories"], list)
|
||||
assert isinstance(result["diff"], dict)
|
||||
assert "added" in result["diff"]
|
||||
assert "removed" in result["diff"]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_preview_falls_back_to_latest_backup_when_device_unreachable(self):
|
||||
"""When live capture fails, preview should fall back to the latest backup."""
|
||||
from app.routers.config_backups import preview_restore, RestoreRequest
|
||||
|
||||
tenant_id = uuid.uuid4()
|
||||
device_id = uuid.uuid4()
|
||||
|
||||
current_export = "/ip address\nadd address=192.168.1.1/24 interface=ether1\n"
|
||||
target_export = "/ip address\nadd address=10.0.0.1/24 interface=ether1\n"
|
||||
|
||||
mock_db = AsyncMock()
|
||||
mock_user = MagicMock()
|
||||
mock_request = MagicMock()
|
||||
body = RestoreRequest(commit_sha="abc1234")
|
||||
|
||||
# Mock device query result
|
||||
mock_device = MagicMock()
|
||||
mock_device.ip_address = "192.168.88.1"
|
||||
mock_device.encrypted_credentials_transit = "vault:v1:abc"
|
||||
mock_device.encrypted_credentials = None
|
||||
mock_device.tenant_id = tenant_id
|
||||
|
||||
# First call: device query, second call: latest backup query
|
||||
mock_device_result = MagicMock()
|
||||
mock_device_result.scalar_one_or_none.return_value = mock_device
|
||||
|
||||
mock_latest_run = MagicMock()
|
||||
mock_latest_run.commit_sha = "latest123"
|
||||
mock_backup_result = MagicMock()
|
||||
mock_backup_result.scalar_one_or_none.return_value = mock_latest_run
|
||||
|
||||
mock_db.execute.side_effect = [mock_device_result, mock_backup_result]
|
||||
|
||||
def mock_read_file(tid, sha, did, filename):
|
||||
if sha == "abc1234":
|
||||
return target_export.encode()
|
||||
elif sha == "latest123":
|
||||
return current_export.encode()
|
||||
return b""
|
||||
|
||||
with patch(
|
||||
"app.routers.config_backups._check_tenant_access",
|
||||
new_callable=AsyncMock,
|
||||
), patch(
|
||||
"app.routers.config_backups.limiter.enabled",
|
||||
False,
|
||||
), patch(
|
||||
"app.routers.config_backups.git_store.read_file",
|
||||
side_effect=mock_read_file,
|
||||
), patch(
|
||||
"app.routers.config_backups.backup_service.capture_export",
|
||||
new_callable=AsyncMock,
|
||||
side_effect=ConnectionError("Device unreachable"),
|
||||
), patch(
|
||||
"app.routers.config_backups.decrypt_credentials_hybrid",
|
||||
new_callable=AsyncMock,
|
||||
return_value='{"username": "admin", "password": "pass"}',
|
||||
), patch(
|
||||
"app.routers.config_backups.settings",
|
||||
):
|
||||
result = await preview_restore(
|
||||
request=mock_request,
|
||||
tenant_id=tenant_id,
|
||||
device_id=device_id,
|
||||
body=body,
|
||||
db=mock_db,
|
||||
current_user=mock_user,
|
||||
)
|
||||
|
||||
assert "diff" in result
|
||||
assert "categories" in result
|
||||
assert "warnings" in result
|
||||
assert "validation" in result
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_preview_404_when_backup_not_found(self):
|
||||
"""preview_restore should return 404 when the target backup doesn't exist."""
|
||||
from app.routers.config_backups import preview_restore, RestoreRequest
|
||||
from fastapi import HTTPException
|
||||
|
||||
tenant_id = uuid.uuid4()
|
||||
device_id = uuid.uuid4()
|
||||
|
||||
mock_db = AsyncMock()
|
||||
mock_user = MagicMock()
|
||||
mock_request = MagicMock()
|
||||
body = RestoreRequest(commit_sha="nonexistent")
|
||||
|
||||
with patch(
|
||||
"app.routers.config_backups._check_tenant_access",
|
||||
new_callable=AsyncMock,
|
||||
), patch(
|
||||
"app.routers.config_backups.limiter.enabled",
|
||||
False,
|
||||
), patch(
|
||||
"app.routers.config_backups.git_store.read_file",
|
||||
side_effect=KeyError("not found"),
|
||||
):
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
await preview_restore(
|
||||
request=mock_request,
|
||||
tenant_id=tenant_id,
|
||||
device_id=device_id,
|
||||
body=body,
|
||||
db=mock_db,
|
||||
current_user=mock_user,
|
||||
)
|
||||
|
||||
assert exc_info.value.status_code == 404
|
||||
Reference in New Issue
Block a user