Files
lfnovo--open-notebook/tests/test_startup_migration_retry.py
2026-07-13 12:10:23 +08:00

113 lines
3.7 KiB
Python

"""Tests for API startup migration retry behavior."""
from unittest.mock import AsyncMock
import pytest
from api import main as api_main
class FakeMigrationManager:
def __init__(
self,
ping_side_effect=None,
current_versions=None,
needs_migration=True,
migration_side_effect=None,
):
self.ping = AsyncMock(side_effect=ping_side_effect)
self.get_current_version = AsyncMock(side_effect=current_versions or [14])
self.needs_migration = AsyncMock(return_value=needs_migration)
self.run_migration_up = AsyncMock(side_effect=migration_side_effect)
@pytest.fixture
def no_retry_delay(monkeypatch):
sleep = AsyncMock()
monkeypatch.setattr(api_main, "DATABASE_STARTUP_RETRY_ATTEMPTS", 3)
monkeypatch.setattr(api_main, "DATABASE_STARTUP_RETRY_INITIAL_DELAY_SECONDS", 0)
monkeypatch.setattr(api_main, "DATABASE_STARTUP_RETRY_MAX_DELAY_SECONDS", 0)
monkeypatch.setattr(api_main.asyncio, "sleep", sleep)
return sleep
@pytest.mark.asyncio
async def test_database_reachable_on_first_probe_runs_migrations_once(
monkeypatch, no_retry_delay
):
manager = FakeMigrationManager(
ping_side_effect=[None],
current_versions=[13, 14],
needs_migration=True,
)
monkeypatch.setattr(api_main, "AsyncMigrationManager", lambda: manager)
await api_main._run_database_migrations()
manager.ping.assert_awaited_once()
manager.run_migration_up.assert_awaited_once()
assert manager.get_current_version.await_count == 2
no_retry_delay.assert_not_awaited()
@pytest.mark.asyncio
async def test_database_retry_succeeds_after_initial_probe_failures(
monkeypatch, no_retry_delay
):
temporary_error = OSError("Temporary failure in name resolution")
manager = FakeMigrationManager(
ping_side_effect=[temporary_error, temporary_error, None],
current_versions=[13, 14],
needs_migration=True,
)
monkeypatch.setattr(api_main, "AsyncMigrationManager", lambda: manager)
await api_main._run_database_migrations()
assert manager.ping.await_count == 3
assert no_retry_delay.await_count == 2
manager.run_migration_up.assert_awaited_once()
@pytest.mark.asyncio
async def test_lifespan_raises_after_database_retry_budget_exhausted(
monkeypatch, no_retry_delay
):
manager = FakeMigrationManager(
ping_side_effect=OSError(-3, "Temporary failure in name resolution"),
)
monkeypatch.setattr(api_main, "AsyncMigrationManager", lambda: manager)
with pytest.raises(RuntimeError, match="Failed to run database migrations"):
# lifespan never touches its app argument, so None keeps the test
# isolated from the real FastAPI app.
async with api_main.lifespan(None): # type: ignore[arg-type]
pass
assert manager.ping.await_count == 3
assert no_retry_delay.await_count == 2
manager.run_migration_up.assert_not_awaited()
@pytest.mark.asyncio
async def test_lifespan_does_not_retry_real_migration_errors(
monkeypatch, no_retry_delay
):
manager = FakeMigrationManager(
ping_side_effect=[None],
current_versions=[13],
needs_migration=True,
migration_side_effect=ValueError("bad migration"),
)
monkeypatch.setattr(api_main, "AsyncMigrationManager", lambda: manager)
with pytest.raises(RuntimeError, match="Failed to run database migrations"):
# lifespan never touches its app argument, so None keeps the test
# isolated from the real FastAPI app.
async with api_main.lifespan(None): # type: ignore[arg-type]
pass
manager.ping.assert_awaited_once()
no_retry_delay.assert_not_awaited()
manager.run_migration_up.assert_awaited_once()