refactor: update tests to use async mocks and improve readability
CI / lint-test-build (push) Failing after 12s
CI / lint-test-build (push) Failing after 12s
This commit is contained in:
@@ -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)",
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user