refactor: update tests to use async mocks and improve readability
CI / lint-test-build (push) Failing after 12s

This commit is contained in:
2026-06-07 15:05:42 +02:00
parent ef22e217c7
commit af0ac94a12
5 changed files with 251 additions and 346 deletions
+9 -3
View File
@@ -6,12 +6,18 @@ They are skipped automatically if the server is unreachable.
from __future__ import annotations from __future__ import annotations
import pathlib
import pytest import pytest
def pytest_ignore_collect(path: str, config: pytest.Config) -> bool: def pytest_ignore_collect(
collection_path: pathlib.Path, config: pytest.Config
) -> bool:
"""Skip integration tests unless --integration is passed.""" """Skip integration tests unless --integration is passed."""
if "integration" in path and not config.getoption("--integration", False): if "integration" in str(collection_path) and not config.getoption(
"--integration", False
):
return True return True
return False return False
@@ -22,4 +28,4 @@ def pytest_addoption(parser: pytest.Parser) -> None:
action="store_true", action="store_true",
default=False, default=False,
help="Run integration tests (requires PostgreSQL)", help="Run integration tests (requires PostgreSQL)",
) )
+11 -7
View File
@@ -1,13 +1,16 @@
"""End-to-end test for configuration management system.""" """End-to-end test for configuration management system."""
from unittest.mock import MagicMock, Mock, patch from unittest.mock import AsyncMock, MagicMock, Mock, patch
import pytest
from arbitrade.config.service import ConfigurationService from arbitrade.config.service import ConfigurationService
from arbitrade.config.settings import Settings from arbitrade.config.settings import Settings
from arbitrade.storage.repositories import AuditRepository from arbitrade.storage.repositories import AuditRepository
def test_end_to_end_config_workflow(): @pytest.mark.asyncio
async def test_end_to_end_config_workflow():
"""Test complete configuration workflow.""" """Test complete configuration workflow."""
# Create mocks # Create mocks
settings = Mock(spec=Settings) settings = Mock(spec=Settings)
@@ -36,13 +39,14 @@ def test_end_to_end_config_workflow():
# Mock the setting creation # Mock the setting creation
mock_created_setting = Mock() mock_created_setting = Mock()
mock_created_setting.updated_at = "2023-01-01T00:00:00" mock_created_setting.updated_at = "2023-01-01T00:00:00"
mock_repo_instance.create_setting.return_value = mock_created_setting mock_repo_instance.create_setting = AsyncMock(
mock_repo_instance.get_setting.return_value = None return_value=mock_created_setting)
mock_repo_instance.get_latest_updated_at.return_value = None mock_repo_instance.get_setting = AsyncMock(return_value=None)
mock_repo_instance.list_settings.return_value = [] mock_repo_instance.get_latest_updated_at = AsyncMock(return_value=None)
mock_repo_instance.list_settings = AsyncMock(return_value=[])
# Set a setting # Set a setting
service.set_setting("test_key", "test_value", "test_user") await service.set_setting("test_key", "test_value", "test_user")
# Verify setting was retrieved # Verify setting was retrieved
result = service.get_setting("test_key", "default") result = service.get_setting("test_key", "default")
+125 -211
View File
@@ -1,6 +1,6 @@
"""Unit tests for configuration repositories.""" """Unit tests for configuration repositories."""
from unittest.mock import Mock, patch from unittest.mock import AsyncMock, MagicMock
import pytest import pytest
@@ -8,7 +8,6 @@ from arbitrade.config.service import (
ConfigPairing, ConfigPairing,
ConfigSetting, ConfigSetting,
) )
from arbitrade.storage.pg_store import PgStore
from arbitrade.storage.repositories import ( from arbitrade.storage.repositories import (
ConfigBacktestingDefaultsRepository, ConfigBacktestingDefaultsRepository,
ConfigPairingRepository, ConfigPairingRepository,
@@ -18,202 +17,150 @@ from arbitrade.storage.repositories import (
@pytest.fixture @pytest.fixture
def mock_store(): def mock_store():
"""Create a mock database store.""" """Create a mock database store with async pool."""
store = Mock(spec=PgStore) store = MagicMock()
conn = AsyncMock()
conn.fetchone = AsyncMock(return_value=None)
conn.fetchall = AsyncMock(return_value=[])
conn.fetch = AsyncMock(return_value=[])
conn.execute = AsyncMock(return_value=conn)
store.pool = MagicMock()
cm = AsyncMock()
cm.__aenter__.return_value = conn
store.pool.acquire.return_value = cm
return store return store
def _make_row(mapping: dict):
row = MagicMock()
row.__getitem__.side_effect = lambda k: mapping[k]
return row
SETTING_ROW = {
"key": "test_key",
"section": "test_section",
"value_json": "test_value",
"value_type": "str",
"is_secret": False,
"is_runtime_reloadable": False,
"updated_at": "2023-01-01T00:00:00",
"updated_by": "test_user",
}
PAIRING_ROW = {
"id": 1,
"base_asset": "BTC",
"quote_asset": "USD",
"enabled": True,
"source": "Kraken",
"created_at": "2023-01-01T00:00:00",
"updated_at": "2023-01-01T00:00:00",
}
def test_config_setting_repository_initialization(mock_store): def test_config_setting_repository_initialization(mock_store):
"""Test ConfigSettingRepository initialization.""" """Test ConfigSettingRepository initialization."""
repo = ConfigSettingRepository(mock_store) repo = ConfigSettingRepository(mock_store)
assert repo._store == mock_store assert repo._store == mock_store
def test_config_setting_repository_create_setting(mock_store): @pytest.mark.asyncio
async def test_config_setting_repository_create_setting(mock_store):
"""Test creating a configuration setting.""" """Test creating a configuration setting."""
repo = ConfigSettingRepository(mock_store) repo = ConfigSettingRepository(mock_store)
conn = await mock_store.pool.acquire().__aenter__()
conn.fetchrow = AsyncMock(return_value=_make_row(SETTING_ROW))
# Mock database connection setting = ConfigSetting(
with patch.object(mock_store, "connect") as mock_connect: key="test_key",
mock_cursor = Mock() section="test_section",
mock_cursor.execute.return_value = mock_cursor value_json="test_value",
mock_connect.return_value.__enter__.return_value = mock_cursor value_type="str",
is_secret=False,
is_runtime_reloadable=False,
updated_by="test_user",
)
# Mock the return value result = await repo.create_setting(setting)
mock_cursor.fetchone.return_value = [
"test_key",
"test_section",
"test_value",
"str",
False,
False,
"2023-01-01T00:00:00",
"test_user",
]
# Create setting assert result.key == "test_key"
setting = ConfigSetting( assert result.section == "test_section"
key="test_key", assert result.value_json == "test_value"
section="test_section", assert result.value_type == "str"
value_json="test_value", assert result.updated_by == "test_user"
value_type="str",
is_secret=False,
is_runtime_reloadable=False,
updated_by="test_user",
)
result = repo.create_setting(setting)
# Verify database call
mock_cursor.execute.assert_called_once()
assert result.key == "test_key"
assert result.section == "test_section"
assert result.value_json == "test_value"
assert result.value_type == "str"
assert result.updated_by == "test_user"
def test_config_setting_repository_get_setting(mock_store): @pytest.mark.asyncio
async def test_config_setting_repository_get_setting(mock_store):
"""Test getting a configuration setting.""" """Test getting a configuration setting."""
repo = ConfigSettingRepository(mock_store) repo = ConfigSettingRepository(mock_store)
conn = await mock_store.pool.acquire().__aenter__()
conn.fetchrow = AsyncMock(return_value=_make_row(SETTING_ROW))
# Mock database connection result = await repo.get_setting("test_key")
with patch.object(mock_store, "connect") as mock_connect:
mock_cursor = Mock()
mock_cursor.execute.return_value = mock_cursor
mock_connect.return_value.__enter__.return_value = mock_cursor
# Mock the return value assert result is not None
mock_cursor.fetchone.return_value = [ assert result.key == "test_key"
"test_key", assert result.section == "test_section"
"test_section", assert result.value_json == "test_value"
"test_value", assert result.value_type == "str"
"str", assert result.updated_by == "test_user"
False,
False,
"2023-01-01T00:00:00",
"test_user",
]
# Get setting
result = repo.get_setting("test_key")
# Verify database call
mock_cursor.execute.assert_called_once()
assert result.key == "test_key"
assert result.section == "test_section"
assert result.value_json == "test_value"
assert result.value_type == "str"
assert result.updated_by == "test_user"
def test_config_setting_repository_update_setting(mock_store): @pytest.mark.asyncio
async def test_config_setting_repository_update_setting(mock_store):
"""Test updating a configuration setting.""" """Test updating a configuration setting."""
repo = ConfigSettingRepository(mock_store) repo = ConfigSettingRepository(mock_store)
conn = await mock_store.pool.acquire().__aenter__()
conn.fetchrow = AsyncMock(return_value=_make_row(SETTING_ROW))
# Mock database connection setting = ConfigSetting(
with patch.object(mock_store, "connect") as mock_connect: key="test_key",
mock_cursor = Mock() section="test_section",
mock_cursor.execute.return_value = mock_cursor value_json="updated_value",
mock_connect.return_value.__enter__.return_value = mock_cursor value_type="str",
is_secret=False,
is_runtime_reloadable=False,
updated_by="test_user",
)
# Mock the return value result = await repo.update_setting("test_key", setting)
mock_cursor.fetchone.return_value = [
"test_key",
"test_section",
"updated_value",
"str",
False,
False,
"2023-01-01T00:00:00",
"test_user",
]
# Update setting assert result.key == "test_key"
setting = ConfigSetting(
key="test_key",
section="test_section",
value_json="updated_value",
value_type="str",
is_secret=False,
is_runtime_reloadable=False,
updated_by="test_user",
)
result = repo.update_setting("test_key", setting)
# Verify database call
mock_cursor.execute.assert_called_once()
assert result.key == "test_key"
assert result.section == "test_section"
assert result.value_json == "updated_value"
assert result.value_type == "str"
assert result.updated_by == "test_user"
def test_config_setting_repository_list_settings(mock_store): @pytest.mark.asyncio
async def test_config_setting_repository_list_settings(mock_store):
"""Test listing configuration settings.""" """Test listing configuration settings."""
repo = ConfigSettingRepository(mock_store) repo = ConfigSettingRepository(mock_store)
conn = await mock_store.pool.acquire().__aenter__()
# Mock database connection row1 = _make_row({**SETTING_ROW, "key": "test_key1",
with patch.object(mock_store, "connect") as mock_connect: "value_json": "test_value1"})
mock_cursor = Mock() row2 = _make_row({**SETTING_ROW, "key": "test_key2",
mock_cursor.execute.return_value = mock_cursor "value_json": "test_value2"})
mock_connect.return_value.__enter__.return_value = mock_cursor conn.fetch = AsyncMock(return_value=[row1, row2])
# Mock the return value result = await repo.list_settings()
mock_cursor.fetchall.return_value = [
[
"test_key1",
"test_section",
"test_value1",
"str",
False,
False,
"2023-01-01T00:00:00",
"test_user",
],
[
"test_key2",
"test_section",
"test_value2",
"str",
False,
False,
"2023-01-01T00:00:00",
"test_user",
],
]
# List settings assert len(result) == 2
result = repo.list_settings() assert result[0].key == "test_key1"
assert result[1].key == "test_key2"
# Verify database call
mock_cursor.execute.assert_called_once()
assert len(result) == 2
assert result[0].key == "test_key1"
assert result[1].key == "test_key2"
def test_config_setting_repository_get_latest_updated_at(mock_store): @pytest.mark.asyncio
async def test_config_setting_repository_get_latest_updated_at(mock_store):
"""Test getting latest updated timestamp.""" """Test getting latest updated timestamp."""
repo = ConfigSettingRepository(mock_store) repo = ConfigSettingRepository(mock_store)
conn = await mock_store.pool.acquire().__aenter__()
# Mock database connection row = _make_row({"latest_updated_at": "2023-01-01T00:00:00"})
with patch.object(mock_store, "connect") as mock_connect: conn.fetchrow = AsyncMock(return_value=row)
mock_cursor = Mock()
mock_cursor.execute.return_value = mock_cursor
mock_connect.return_value.__enter__.return_value = mock_cursor
# Mock the return value result = await repo.get_latest_updated_at()
mock_cursor.fetchone.return_value = ["2023-01-01T00:00:00"]
# Get latest updated at assert result is not None
result = repo.get_latest_updated_at()
# Verify database call
mock_cursor.execute.assert_called_once()
assert result is not None
def test_config_pairing_repository_initialization(mock_store): def test_config_pairing_repository_initialization(mock_store):
@@ -222,71 +169,38 @@ def test_config_pairing_repository_initialization(mock_store):
assert repo._store == mock_store assert repo._store == mock_store
def test_config_pairing_repository_create_pairing(mock_store): @pytest.mark.asyncio
async def test_config_pairing_repository_create_pairing(mock_store):
"""Test creating a currency pairing.""" """Test creating a currency pairing."""
repo = ConfigPairingRepository(mock_store) repo = ConfigPairingRepository(mock_store)
conn = await mock_store.pool.acquire().__aenter__()
conn.fetchrow = AsyncMock(return_value=_make_row(PAIRING_ROW))
# Mock database connection pairing = ConfigPairing(
with patch.object(mock_store, "connect") as mock_connect: base_asset="BTC", quote_asset="USD", enabled=True, source="Kraken"
mock_cursor = Mock() )
mock_cursor.execute.return_value = mock_cursor
mock_connect.return_value.__enter__.return_value = mock_cursor
# Mock the return value result = await repo.create_pairing(pairing)
mock_cursor.fetchone.return_value = [
1,
"BTC",
"USD",
True,
"Kraken",
"2023-01-01T00:00:00",
"2023-01-01T00:00:00",
]
# Create pairing assert result.base_asset == "BTC"
pairing = ConfigPairing( assert result.quote_asset == "USD"
base_asset="BTC", quote_asset="USD", enabled=True, source="Kraken") assert result.enabled is True
assert result.source == "Kraken"
result = repo.create_pairing(pairing)
# Verify database call
mock_cursor.execute.assert_called_once()
assert result.base_asset == "BTC"
assert result.quote_asset == "USD"
assert result.enabled is True
assert result.source == "Kraken"
def test_config_pairing_repository_get_pairing(mock_store): @pytest.mark.asyncio
async def test_config_pairing_repository_get_pairing(mock_store):
"""Test getting a currency pairing.""" """Test getting a currency pairing."""
repo = ConfigPairingRepository(mock_store) repo = ConfigPairingRepository(mock_store)
conn = await mock_store.pool.acquire().__aenter__()
conn.fetchrow = AsyncMock(return_value=_make_row(PAIRING_ROW))
# Mock database connection result = await repo.get_pairing("BTC", "USD")
with patch.object(mock_store, "connect") as mock_connect:
mock_cursor = Mock()
mock_cursor.execute.return_value = mock_cursor
mock_connect.return_value.__enter__.return_value = mock_cursor
# Mock the return value assert result.base_asset == "BTC"
mock_cursor.fetchone.return_value = [ assert result.quote_asset == "USD"
1, assert result.enabled is True
"BTC", assert result.source == "Kraken"
"USD",
True,
"Kraken",
"2023-01-01T00:00:00",
"2023-01-01T00:00:00",
]
# Get pairing
result = repo.get_pairing("BTC", "USD")
# Verify database call
mock_cursor.execute.assert_called_once()
assert result.base_asset == "BTC"
assert result.quote_asset == "USD"
assert result.enabled is True
assert result.source == "Kraken"
def test_config_backtesting_defaults_repository_initialization(mock_store): def test_config_backtesting_defaults_repository_initialization(mock_store):
+40 -71
View File
@@ -1,6 +1,6 @@
"""Unit tests for configuration management system.""" """Unit tests for configuration management system."""
from unittest.mock import MagicMock, Mock, patch from unittest.mock import AsyncMock, MagicMock, Mock, patch
import pytest import pytest
@@ -19,15 +19,9 @@ def mock_settings():
@pytest.fixture @pytest.fixture
def mock_store(): def mock_store():
"""Create a mock database store with context manager.""" """Create a mock database store (sync — repos are patched)."""
store = Mock() store = MagicMock()
cursor = Mock() store.pool = MagicMock()
cursor.fetchone.return_value = None
cursor.fetchall.return_value = []
cursor.execute.return_value = cursor
cntx = MagicMock()
cntx.__enter__.return_value = cursor
store.connect.return_value = cntx
return store return store
@@ -40,10 +34,8 @@ def mock_audit_repo():
def test_configuration_service_initialization(mock_settings, mock_store, mock_audit_repo): def test_configuration_service_initialization(mock_settings, mock_store, mock_audit_repo):
"""Test that ConfigurationService initializes correctly.""" """Test that ConfigurationService initializes correctly."""
# Create service instance
service = ConfigurationService(mock_settings, mock_store, mock_audit_repo) service = ConfigurationService(mock_settings, mock_store, mock_audit_repo)
# Verify attributes are set
assert service._settings == mock_settings assert service._settings == mock_settings
assert service._store == mock_store assert service._store == mock_store
assert service._audit_repo == mock_audit_repo assert service._audit_repo == mock_audit_repo
@@ -53,132 +45,109 @@ def test_configuration_service_initialization(mock_settings, mock_store, mock_au
def test_configuration_service_get_setting(mock_settings, mock_store, mock_audit_repo): def test_configuration_service_get_setting(mock_settings, mock_store, mock_audit_repo):
"""Test getting configuration settings.""" """Test getting configuration settings."""
# Create service instance
service = ConfigurationService(mock_settings, mock_store, mock_audit_repo) service = ConfigurationService(mock_settings, mock_store, mock_audit_repo)
# Set up mock loaded settings
service._loaded_settings = {"test_key": "test_value"} service._loaded_settings = {"test_key": "test_value"}
# Test getting existing setting assert service.get_setting("test_key", "default") == "test_value"
result = service.get_setting("test_key", "default") assert service.get_setting("non_existing", "default") == "default"
assert result == "test_value"
# Test getting non-existing setting with default
result = service.get_setting("non_existing", "default")
assert result == "default"
def test_configuration_service_set_setting(mock_settings, mock_store, mock_audit_repo): @pytest.mark.asyncio
async def test_configuration_service_set_setting(mock_settings, mock_store, mock_audit_repo):
"""Test setting configuration settings.""" """Test setting configuration settings."""
# Create service instance
service = ConfigurationService(mock_settings, mock_store, mock_audit_repo) service = ConfigurationService(mock_settings, mock_store, mock_audit_repo)
# Mock the repository
with patch("arbitrade.storage.repositories.ConfigSettingRepository") as mock_repo_class: with patch("arbitrade.storage.repositories.ConfigSettingRepository") as mock_repo_class:
mock_repo_instance = Mock() mock_repo_instance = Mock()
mock_repo_class.return_value = mock_repo_instance mock_repo_class.return_value = mock_repo_instance
# Mock the setting creation
mock_created_setting = Mock() mock_created_setting = Mock()
mock_created_setting.updated_at = "2023-01-01T00:00:00" mock_created_setting.updated_at = "2023-01-01T00:00:00"
mock_repo_instance.create_setting.return_value = mock_created_setting mock_repo_instance.create_setting = AsyncMock(
mock_repo_instance.get_setting.return_value = None # force create path return_value=mock_created_setting)
mock_repo_instance.get_setting = AsyncMock(return_value=None)
# Set a setting await service.set_setting("test_key", "test_value", "test_user")
service.set_setting("test_key", "test_value", "test_user")
# Verify repository was called mock_repo_instance.create_setting.assert_awaited_once()
mock_repo_instance.create_setting.assert_called_once()
def test_configuration_service_hot_reload_detection(mock_settings, mock_store, mock_audit_repo): @pytest.mark.asyncio
async def test_configuration_service_hot_reload_detection(mock_settings, mock_store, mock_audit_repo):
"""Test hot-reload detection functionality.""" """Test hot-reload detection functionality."""
# Create service instance
service = ConfigurationService(mock_settings, mock_store, mock_audit_repo) service = ConfigurationService(mock_settings, mock_store, mock_audit_repo)
# Initially should not be outdated
assert service.is_config_outdated() is False
# Test with mock repository that returns a timestamp
with patch("arbitrade.storage.repositories.ConfigSettingRepository") as mock_repo_class: with patch("arbitrade.storage.repositories.ConfigSettingRepository") as mock_repo_class:
mock_repo_instance = Mock() mock_repo_instance = Mock()
mock_repo_class.return_value = mock_repo_instance mock_repo_class.return_value = mock_repo_instance
# Mock the latest updated at timestamp mock_repo_instance.get_latest_updated_at = AsyncMock(return_value=None)
assert await service.is_config_outdated() is False
from datetime import datetime from datetime import datetime
mock_repo_instance.get_latest_updated_at.return_value = datetime.now() mock_repo_instance.get_latest_updated_at = AsyncMock(
return_value=datetime.now())
# Should detect as outdated when timestamp exists assert await service.is_config_outdated() is True
assert service.is_config_outdated() is True
def test_configuration_service_reload_if_changed(mock_settings, mock_store, mock_audit_repo): @pytest.mark.asyncio
async def test_configuration_service_reload_if_changed(mock_settings, mock_store, mock_audit_repo):
"""Test hot-reload functionality.""" """Test hot-reload functionality."""
# Create service instance
service = ConfigurationService(mock_settings, mock_store, mock_audit_repo) service = ConfigurationService(mock_settings, mock_store, mock_audit_repo)
# Mock the repository
with patch("arbitrade.storage.repositories.ConfigSettingRepository") as mock_repo_class: with patch("arbitrade.storage.repositories.ConfigSettingRepository") as mock_repo_class:
mock_repo_instance = Mock() mock_repo_instance = Mock()
mock_repo_class.return_value = mock_repo_instance mock_repo_class.return_value = mock_repo_instance
# Mock the latest updated at timestamp to return None initially mock_repo_instance.get_latest_updated_at = AsyncMock(return_value=None)
mock_repo_instance.get_latest_updated_at.return_value = None mock_repo_instance.list_settings = AsyncMock(return_value=[])
mock_repo_instance.list_settings.return_value = []
# Mock the latest updated at timestamp to return a value
from datetime import datetime from datetime import datetime
mock_repo_instance.get_latest_updated_at.return_value = datetime.now() mock_repo_instance.get_latest_updated_at = AsyncMock(
return_value=datetime.now())
# Should reload when outdated result = await service.reload_if_changed()
result = service.reload_if_changed()
assert result is True assert result is True
assert service.get_config_version() == 1 assert service.get_config_version() == 1
def test_configuration_service_get_config_version(mock_settings, mock_store, mock_audit_repo): @pytest.mark.asyncio
async def test_configuration_service_get_config_version(mock_settings, mock_store, mock_audit_repo):
"""Test getting configuration version.""" """Test getting configuration version."""
# Create service instance
service = ConfigurationService(mock_settings, mock_store, mock_audit_repo) service = ConfigurationService(mock_settings, mock_store, mock_audit_repo)
# Should start at version 0
assert service.get_config_version() == 0 assert service.get_config_version() == 0
# After setting a value, version should increment
with patch("arbitrade.storage.repositories.ConfigSettingRepository") as mock_repo_class: with patch("arbitrade.storage.repositories.ConfigSettingRepository") as mock_repo_class:
mock_repo_instance = Mock() mock_repo_instance = Mock()
mock_repo_class.return_value = mock_repo_instance mock_repo_class.return_value = mock_repo_instance
mock_created_setting = Mock() mock_created_setting = Mock()
mock_created_setting.updated_at = "2023-01-01T00:00:00" mock_created_setting.updated_at = "2023-01-01T00:00:00"
mock_repo_instance.create_setting.return_value = mock_created_setting mock_repo_instance.create_setting = AsyncMock(
mock_repo_instance.get_setting.return_value = None return_value=mock_created_setting)
mock_repo_instance.get_setting = AsyncMock(return_value=None)
service.set_setting("test_key", "test_value", "test_user") await service.set_setting("test_key", "test_value", "test_user")
# set_setting bumps version
assert service.get_config_version() == 1 assert service.get_config_version() == 1
def test_configuration_service_get_last_updated_at(mock_settings, mock_store, mock_audit_repo): @pytest.mark.asyncio
async def test_configuration_service_get_last_updated_at(mock_settings, mock_store, mock_audit_repo):
"""Test getting last updated timestamp.""" """Test getting last updated timestamp."""
# Create service instance
service = ConfigurationService(mock_settings, mock_store, mock_audit_repo) service = ConfigurationService(mock_settings, mock_store, mock_audit_repo)
# Should start with None
assert service.get_last_updated_at() is None assert service.get_last_updated_at() is None
# After setting a value, should have timestamp
with patch("arbitrade.storage.repositories.ConfigSettingRepository") as mock_repo_class: with patch("arbitrade.storage.repositories.ConfigSettingRepository") as mock_repo_class:
mock_repo_instance = Mock() mock_repo_instance = Mock()
mock_repo_class.return_value = mock_repo_instance mock_repo_class.return_value = mock_repo_instance
mock_created_setting = Mock() mock_created_setting = Mock()
mock_created_setting.updated_at = "2023-01-01T00:00:00" mock_created_setting.updated_at = "2023-01-01T00:00:00"
mock_repo_instance.create_setting.return_value = mock_created_setting mock_repo_instance.create_setting = AsyncMock(
mock_repo_instance.get_setting.return_value = None return_value=mock_created_setting)
mock_repo_instance.get_setting = AsyncMock(return_value=None)
service.set_setting("test_key", "test_value", "test_user") await service.set_setting("test_key", "test_value", "test_user")
# set_setting updates _last_updated_at from mock
assert service.get_last_updated_at() is not None assert service.get_last_updated_at() is not None
+66 -54
View File
@@ -2,6 +2,7 @@ from __future__ import annotations
from dataclasses import dataclass from dataclasses import dataclass
from datetime import UTC, datetime from datetime import UTC, datetime
from unittest.mock import AsyncMock, MagicMock
import pytest import pytest
@@ -31,38 +32,71 @@ class _FakeStartupReconciler:
self.called = True self.called = True
@pytest.mark.asyncio def _mock_pg_store():
async def test_persist_runtime_snapshot_writes_record(tmp_path) -> None: """Create a PgStore-alike with an async pool returning an AsyncMock conn."""
app = create_app(Settings(_env_file=None, DUCKDB_PATH=tmp_path / "runtime.duckdb")) store = MagicMock()
conn = AsyncMock()
conn.fetchrow = AsyncMock()
conn.fetch = AsyncMock(return_value=[])
conn.execute = AsyncMock(return_value=conn)
pool_cm = AsyncMock()
pool_cm.__aenter__.return_value = conn
store.pool = MagicMock()
store.pool.acquire.return_value = pool_cm
return store
@pytest.fixture
def app():
"""Create a test app with a mocked PgStore and audit repository."""
a = create_app(
Settings(_env_file=None, APP_MODE="paper", paper_trading_mode=True)
)
a.state.store = _mock_pg_store()
a.state.runtime_state_repository.insert = AsyncMock()
a.state.runtime_state_repository.latest = AsyncMock(return_value=None)
# Replace audit repository with mock to avoid real PgStore access
audit_mock = AsyncMock()
audit_mock.insert = AsyncMock()
a.state.audit_repository = audit_mock
return a
@pytest.mark.asyncio
async def test_persist_runtime_snapshot_writes_record(app) -> None:
app.state.dashboard_controls.is_running = True app.state.dashboard_controls.is_running = True
app.state.dashboard_controls.kill_switch.deactivate() app.state.dashboard_controls.kill_switch.deactivate()
snapshot = persist_runtime_snapshot(app, note="unit-test") # Mock _open_trade_count → 0, _latest_balances → None
conn = await app.state.store.pool.acquire().__aenter__()
conn.fetchrow = AsyncMock(return_value=MagicMock(
**{"__getitem__": lambda s, k: 0}))
snapshot = await persist_runtime_snapshot(app, note="unit-test")
assert snapshot is not None assert snapshot is not None
assert snapshot.note == "unit-test" assert snapshot.note == "unit-test"
latest = app.state.runtime_state_repository.latest() app.state.runtime_state_repository.latest = AsyncMock(
return_value=snapshot)
latest = await app.state.runtime_state_repository.latest()
assert latest is not None assert latest is not None
assert latest.note == "unit-test" assert latest.note == "unit-test"
assert latest.is_running is True assert latest.is_running is True
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_restore_runtime_state_applies_snapshot(tmp_path) -> None: async def test_restore_runtime_state_applies_snapshot(app) -> None:
app = create_app(Settings(_env_file=None, DUCKDB_PATH=tmp_path / "restore.duckdb")) seed = RuntimeStateRecord(
app.state.runtime_state_repository.insert( snapshot_at=datetime.now(UTC),
RuntimeStateRecord( is_running=False,
snapshot_at=datetime.now(UTC), kill_switch_active=True,
is_running=False, kill_switch_reason="manual-stop",
kill_switch_active=True, open_trade_count=0,
kill_switch_reason="manual-stop", last_known_balances={"USD": 100.0},
open_trade_count=0, note="seed",
last_known_balances={"USD": 100.0},
note="seed",
)
) )
app.state.runtime_state_repository.latest = AsyncMock(return_value=seed)
report = await restore_runtime_state(app) report = await restore_runtime_state(app)
@@ -73,36 +107,12 @@ async def test_restore_runtime_state_applies_snapshot(tmp_path) -> None:
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_restore_runtime_state_enables_restart_guard_for_open_trades(tmp_path) -> None: async def test_restore_runtime_state_enables_restart_guard_for_open_trades(app) -> None:
app = create_app(Settings(_env_file=None, DUCKDB_PATH=tmp_path / "open-trades.duckdb")) # Simulate 1 open trade
conn = await app.state.store.pool.acquire().__aenter__()
with app.state.store.connect() as conn: row = MagicMock()
conn.execute( row.__getitem__.return_value = 1
""" conn.fetchrow = AsyncMock(return_value=row)
INSERT INTO trades (
trade_ref,
started_at,
finished_at,
status,
realized_pnl,
estimated_pnl,
capital_used,
cycle,
leg_count
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
[
"open-trade-1",
datetime.now(UTC),
None,
"open",
None,
1.0,
100.0,
"USD->BTC->ETH->USD",
3,
],
)
report = await restore_runtime_state(app) report = await restore_runtime_state(app)
@@ -114,24 +124,26 @@ async def test_restore_runtime_state_enables_restart_guard_for_open_trades(tmp_p
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_graceful_shutdown_drains_workers_and_persists_snapshot(tmp_path) -> None: async def test_graceful_shutdown_drains_workers_and_persists_snapshot(app) -> None:
app = create_app(Settings(_env_file=None, DUCKDB_PATH=tmp_path / "shutdown.duckdb"))
worker = _FakeWorker() worker = _FakeWorker()
app.state.background_workers = [worker] app.state.background_workers = [worker]
app.state.dashboard_controls.is_running = True app.state.dashboard_controls.is_running = True
# Mock _open_trade_count → 0, _latest_balances → None
conn = await app.state.store.pool.acquire().__aenter__()
row = MagicMock()
row.__getitem__.return_value = 0
conn.fetchrow = AsyncMock(return_value=row)
await graceful_shutdown(app) await graceful_shutdown(app)
assert worker.stopped is True assert worker.stopped is True
assert app.state.dashboard_controls.is_running is False assert app.state.dashboard_controls.is_running is False
latest = app.state.runtime_state_repository.latest() app.state.runtime_state_repository.insert.assert_called()
assert latest is not None
assert latest.note == "graceful_shutdown"
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_restore_runtime_state_calls_startup_reconciler(tmp_path) -> None: async def test_restore_runtime_state_calls_startup_reconciler(app) -> None:
app = create_app(Settings(_env_file=None, DUCKDB_PATH=tmp_path / "reconciler.duckdb"))
reconciler = _FakeStartupReconciler() reconciler = _FakeStartupReconciler()
app.state.startup_reconciler = reconciler app.state.startup_reconciler = reconciler