From 8654471042027b64633be2a53f86ca3bc1554803 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 11 Sep 2025 22:25:50 +0000 Subject: [PATCH] Add tests for configurable paths and finalize implementation Co-authored-by: elisiariocouto <818914+elisiariocouto@users.noreply.github.com> --- tests/conftest.py | 28 +- tests/unit/test_configurable_paths.py | 162 +++++++++++ tests/unit/test_sqlite_database.py | 84 +++--- tests/unit/test_sqlite_database.py.bak | 364 +++++++++++++++++++++++++ 4 files changed, 576 insertions(+), 62 deletions(-) create mode 100644 tests/unit/test_configurable_paths.py create mode 100644 tests/unit/test_sqlite_database.py.bak diff --git a/tests/conftest.py b/tests/conftest.py index 58c3460..f1821ba 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -86,23 +86,17 @@ def api_client(fastapi_app): @pytest.fixture def mock_db_path(temp_db_path): """Mock the database path to use temporary database for testing.""" - from pathlib import Path - - # Create the expected directory structure - temp_home = temp_db_path.parent - config_dir = temp_home / ".config" / "leggen" - config_dir.mkdir(parents=True, exist_ok=True) - - # Create the expected database path - expected_db_path = config_dir / "leggen.db" - - # Mock Path.home to return our temp directory - def mock_home(): - return temp_home - - # Patch Path.home in the main pathlib module - with patch.object(Path, "home", staticmethod(mock_home)): - yield expected_db_path + from leggen.utils.paths import path_manager + + # Set the path manager to use the temporary database + original_database_path = path_manager._database_path + path_manager.set_database_path(temp_db_path) + + try: + yield temp_db_path + finally: + # Restore original path + path_manager._database_path = original_database_path @pytest.fixture diff --git a/tests/unit/test_configurable_paths.py b/tests/unit/test_configurable_paths.py new file mode 100644 index 0000000..13ab5c2 --- /dev/null +++ b/tests/unit/test_configurable_paths.py @@ -0,0 +1,162 @@ +"""Integration tests for configurable paths.""" + +import pytest +import tempfile +import os +from pathlib import Path +from unittest.mock import patch + +from leggen.utils.paths import path_manager +from leggen.database.sqlite import persist_balances, get_balances + + +class MockContext: + """Mock context for testing.""" + pass + + +@pytest.mark.unit +class TestConfigurablePaths: + """Test configurable path management.""" + + def test_default_paths(self): + """Test that default paths are correctly set.""" + # Reset path manager + original_config = path_manager._config_dir + original_db = path_manager._database_path + + try: + path_manager._config_dir = None + path_manager._database_path = None + + # Test defaults + config_dir = path_manager.get_config_dir() + db_path = path_manager.get_database_path() + + assert config_dir == Path.home() / ".config" / "leggen" + assert db_path == Path.home() / ".config" / "leggen" / "leggen.db" + finally: + path_manager._config_dir = original_config + path_manager._database_path = original_db + + def test_environment_variables(self): + """Test that environment variables override defaults.""" + with tempfile.TemporaryDirectory() as tmpdir: + test_config_dir = Path(tmpdir) / "test-config" + test_db_path = Path(tmpdir) / "test.db" + + with patch.dict(os.environ, { + 'LEGGEN_CONFIG_DIR': str(test_config_dir), + 'LEGGEN_DATABASE_PATH': str(test_db_path) + }): + # Reset path manager to pick up environment variables + original_config = path_manager._config_dir + original_db = path_manager._database_path + + try: + path_manager._config_dir = None + path_manager._database_path = None + + config_dir = path_manager.get_config_dir() + db_path = path_manager.get_database_path() + + assert config_dir == test_config_dir + assert db_path == test_db_path + finally: + path_manager._config_dir = original_config + path_manager._database_path = original_db + + def test_explicit_path_setting(self): + """Test explicitly setting paths.""" + with tempfile.TemporaryDirectory() as tmpdir: + test_config_dir = Path(tmpdir) / "explicit-config" + test_db_path = Path(tmpdir) / "explicit.db" + + # Save original paths + original_config = path_manager._config_dir + original_db = path_manager._database_path + + try: + # Set explicit paths + path_manager.set_config_dir(test_config_dir) + path_manager.set_database_path(test_db_path) + + assert path_manager.get_config_dir() == test_config_dir + assert path_manager.get_database_path() == test_db_path + assert path_manager.get_config_file_path() == test_config_dir / "config.toml" + assert path_manager.get_auth_file_path() == test_config_dir / "auth.json" + finally: + # Restore original paths + path_manager._config_dir = original_config + path_manager._database_path = original_db + + def test_database_operations_with_custom_path(self): + """Test that database operations work with custom paths.""" + with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as tmp_file: + test_db_path = Path(tmp_file.name) + + # Save original database path + original_db = path_manager._database_path + + try: + # Set custom database path + path_manager.set_database_path(test_db_path) + + # Test database operations + ctx = MockContext() + balance = { + "account_id": "test-account", + "bank": "TEST_BANK", + "status": "active", + "iban": "TEST_IBAN", + "amount": 1000.0, + "currency": "EUR", + "type": "available", + "timestamp": "2023-01-01T00:00:00", + } + + # Persist balance + persist_balances(ctx, balance) + + # Retrieve balances + balances = get_balances() + + assert len(balances) == 1 + assert balances[0]["account_id"] == "test-account" + assert balances[0]["amount"] == 1000.0 + + # Verify database file exists at custom location + assert test_db_path.exists() + + finally: + # Restore original path and cleanup + path_manager._database_path = original_db + if test_db_path.exists(): + test_db_path.unlink() + + def test_directory_creation(self): + """Test that directories are created as needed.""" + with tempfile.TemporaryDirectory() as tmpdir: + test_config_dir = Path(tmpdir) / "new" / "config" / "dir" + test_db_path = Path(tmpdir) / "new" / "db" / "dir" / "test.db" + + # Save original paths + original_config = path_manager._config_dir + original_db = path_manager._database_path + + try: + # Set paths to non-existent directories + path_manager.set_config_dir(test_config_dir) + path_manager.set_database_path(test_db_path) + + # Ensure directories are created + path_manager.ensure_config_dir_exists() + path_manager.ensure_database_dir_exists() + + assert test_config_dir.exists() + assert test_db_path.parent.exists() + + finally: + # Restore original paths + path_manager._config_dir = original_config + path_manager._database_path = original_db \ No newline at end of file diff --git a/tests/unit/test_sqlite_database.py b/tests/unit/test_sqlite_database.py index 740edc4..0d0da67 100644 --- a/tests/unit/test_sqlite_database.py +++ b/tests/unit/test_sqlite_database.py @@ -21,14 +21,18 @@ def temp_db_path(): @pytest.fixture def mock_home_db_path(temp_db_path): - """Mock the home database path to use temp file.""" - config_dir = temp_db_path.parent / ".config" / "leggen" - config_dir.mkdir(parents=True, exist_ok=True) - db_file = config_dir / "leggen.db" - - with patch("pathlib.Path.home") as mock_home: - mock_home.return_value = temp_db_path.parent - yield db_file + """Mock the database path to use temp file.""" + from leggen.utils.paths import path_manager + + # Set the path manager to use the temporary database + original_database_path = path_manager._database_path + path_manager.set_database_path(temp_db_path) + + try: + yield temp_db_path + finally: + # Restore original path + path_manager._database_path = original_database_path @pytest.fixture @@ -90,18 +94,14 @@ class TestSQLiteDatabase: """Test persisting transactions to database.""" ctx = MockContext() - # Mock the database path - with patch("pathlib.Path.home") as mock_home: - mock_home.return_value = mock_home_db_path.parent / ".." + # Persist transactions + new_transactions = sqlite_db.persist_transactions( + ctx, "test-account-123", sample_transactions + ) - # Persist transactions - new_transactions = sqlite_db.persist_transactions( - ctx, "test-account-123", sample_transactions - ) - - # Should return all transactions as new - assert len(new_transactions) == 2 - assert new_transactions[0]["internalTransactionId"] == "txn-001" + # Should return all transactions as new + assert len(new_transactions) == 2 + assert new_transactions[0]["internalTransactionId"] == "txn-001" def test_persist_transactions_duplicates( self, mock_home_db_path, sample_transactions @@ -109,40 +109,34 @@ class TestSQLiteDatabase: """Test handling duplicate transactions.""" ctx = MockContext() - with patch("pathlib.Path.home") as mock_home: - mock_home.return_value = mock_home_db_path.parent / ".." + # Insert transactions twice + new_transactions_1 = sqlite_db.persist_transactions( + ctx, "test-account-123", sample_transactions + ) + new_transactions_2 = sqlite_db.persist_transactions( + ctx, "test-account-123", sample_transactions + ) - # Insert transactions twice - new_transactions_1 = sqlite_db.persist_transactions( - ctx, "test-account-123", sample_transactions - ) - new_transactions_2 = sqlite_db.persist_transactions( - ctx, "test-account-123", sample_transactions - ) - - # First time should return all as new - assert len(new_transactions_1) == 2 - # Second time should also return all (INSERT OR REPLACE behavior with composite key) - assert len(new_transactions_2) == 2 + # First time should return all as new + assert len(new_transactions_1) == 2 + # Second time should also return all (INSERT OR REPLACE behavior with composite key) + assert len(new_transactions_2) == 2 def test_get_transactions_all(self, mock_home_db_path, sample_transactions): """Test retrieving all transactions.""" ctx = MockContext() - with patch("pathlib.Path.home") as mock_home: - mock_home.return_value = mock_home_db_path.parent / ".." + # Insert test data + sqlite_db.persist_transactions(ctx, "test-account-123", sample_transactions) - # Insert test data - sqlite_db.persist_transactions(ctx, "test-account-123", sample_transactions) + # Get all transactions + transactions = sqlite_db.get_transactions() - # Get all transactions - transactions = sqlite_db.get_transactions() - - assert len(transactions) == 2 - assert ( - transactions[0]["internalTransactionId"] == "txn-002" - ) # Ordered by date DESC - assert transactions[1]["internalTransactionId"] == "txn-001" + assert len(transactions) == 2 + assert ( + transactions[0]["internalTransactionId"] == "txn-002" + ) # Ordered by date DESC + assert transactions[1]["internalTransactionId"] == "txn-001" def test_get_transactions_filtered_by_account( self, mock_home_db_path, sample_transactions diff --git a/tests/unit/test_sqlite_database.py.bak b/tests/unit/test_sqlite_database.py.bak new file mode 100644 index 0000000..0d0da67 --- /dev/null +++ b/tests/unit/test_sqlite_database.py.bak @@ -0,0 +1,364 @@ +"""Tests for SQLite database functions.""" + +import pytest +import tempfile +from pathlib import Path +from unittest.mock import patch +from datetime import datetime + +import leggen.database.sqlite as sqlite_db + + +@pytest.fixture +def temp_db_path(): + """Create a temporary database file for testing.""" + import uuid + + with tempfile.TemporaryDirectory() as tmpdir: + db_path = Path(tmpdir) / f"test_{uuid.uuid4().hex}.db" + yield db_path + + +@pytest.fixture +def mock_home_db_path(temp_db_path): + """Mock the database path to use temp file.""" + from leggen.utils.paths import path_manager + + # Set the path manager to use the temporary database + original_database_path = path_manager._database_path + path_manager.set_database_path(temp_db_path) + + try: + yield temp_db_path + finally: + # Restore original path + path_manager._database_path = original_database_path + + +@pytest.fixture +def sample_transactions(): + """Sample transaction data for testing.""" + return [ + { + "transactionId": "bank-txn-001", # NEW: stable bank-provided ID + "internalTransactionId": "txn-001", + "institutionId": "REVOLUT_REVOLT21", + "iban": "LT313250081177977789", + "transactionDate": datetime(2025, 9, 1, 9, 30), + "description": "Coffee Shop Payment", + "transactionValue": -10.50, + "transactionCurrency": "EUR", + "transactionStatus": "booked", + "accountId": "test-account-123", + "rawTransaction": {"transactionId": "bank-txn-001", "some": "data"}, + }, + { + "transactionId": "bank-txn-002", # NEW: stable bank-provided ID + "internalTransactionId": "txn-002", + "institutionId": "REVOLUT_REVOLT21", + "iban": "LT313250081177977789", + "transactionDate": datetime(2025, 9, 2, 14, 15), + "description": "Grocery Store", + "transactionValue": -45.30, + "transactionCurrency": "EUR", + "transactionStatus": "booked", + "accountId": "test-account-123", + "rawTransaction": {"transactionId": "bank-txn-002", "other": "data"}, + }, + ] + + +@pytest.fixture +def sample_balance(): + """Sample balance data for testing.""" + return { + "account_id": "test-account-123", + "bank": "REVOLUT_REVOLT21", + "status": "active", + "iban": "LT313250081177977789", + "amount": 1000.00, + "currency": "EUR", + "type": "interimAvailable", + "timestamp": datetime.now(), + } + + +class MockContext: + """Mock context for testing.""" + + +class TestSQLiteDatabase: + """Test SQLite database operations.""" + + def test_persist_transactions(self, mock_home_db_path, sample_transactions): + """Test persisting transactions to database.""" + ctx = MockContext() + + # Persist transactions + new_transactions = sqlite_db.persist_transactions( + ctx, "test-account-123", sample_transactions + ) + + # Should return all transactions as new + assert len(new_transactions) == 2 + assert new_transactions[0]["internalTransactionId"] == "txn-001" + + def test_persist_transactions_duplicates( + self, mock_home_db_path, sample_transactions + ): + """Test handling duplicate transactions.""" + ctx = MockContext() + + # Insert transactions twice + new_transactions_1 = sqlite_db.persist_transactions( + ctx, "test-account-123", sample_transactions + ) + new_transactions_2 = sqlite_db.persist_transactions( + ctx, "test-account-123", sample_transactions + ) + + # First time should return all as new + assert len(new_transactions_1) == 2 + # Second time should also return all (INSERT OR REPLACE behavior with composite key) + assert len(new_transactions_2) == 2 + + def test_get_transactions_all(self, mock_home_db_path, sample_transactions): + """Test retrieving all transactions.""" + ctx = MockContext() + + # Insert test data + sqlite_db.persist_transactions(ctx, "test-account-123", sample_transactions) + + # Get all transactions + transactions = sqlite_db.get_transactions() + + assert len(transactions) == 2 + assert ( + transactions[0]["internalTransactionId"] == "txn-002" + ) # Ordered by date DESC + assert transactions[1]["internalTransactionId"] == "txn-001" + + def test_get_transactions_filtered_by_account( + self, mock_home_db_path, sample_transactions + ): + """Test filtering transactions by account ID.""" + ctx = MockContext() + + # Add transaction for different account + other_account_transaction = sample_transactions[0].copy() + other_account_transaction["internalTransactionId"] = "txn-003" + other_account_transaction["accountId"] = "other-account" + + all_transactions = sample_transactions + [other_account_transaction] + + with patch("pathlib.Path.home") as mock_home: + mock_home.return_value = mock_home_db_path.parent / ".." + + sqlite_db.persist_transactions(ctx, "test-account-123", all_transactions) + + # Filter by account + transactions = sqlite_db.get_transactions(account_id="test-account-123") + + assert len(transactions) == 2 + for txn in transactions: + assert txn["accountId"] == "test-account-123" + + def test_get_transactions_with_pagination( + self, mock_home_db_path, sample_transactions + ): + """Test transaction pagination.""" + ctx = MockContext() + + with patch("pathlib.Path.home") as mock_home: + mock_home.return_value = mock_home_db_path.parent / ".." + + sqlite_db.persist_transactions(ctx, "test-account-123", sample_transactions) + + # Get first page + transactions_page1 = sqlite_db.get_transactions(limit=1, offset=0) + assert len(transactions_page1) == 1 + + # Get second page + transactions_page2 = sqlite_db.get_transactions(limit=1, offset=1) + assert len(transactions_page2) == 1 + + # Should be different transactions + assert ( + transactions_page1[0]["internalTransactionId"] + != transactions_page2[0]["internalTransactionId"] + ) + + def test_get_transactions_with_amount_filter( + self, mock_home_db_path, sample_transactions + ): + """Test filtering transactions by amount.""" + ctx = MockContext() + + with patch("pathlib.Path.home") as mock_home: + mock_home.return_value = mock_home_db_path.parent / ".." + + sqlite_db.persist_transactions(ctx, "test-account-123", sample_transactions) + + # Filter by minimum amount (should exclude coffee shop payment) + transactions = sqlite_db.get_transactions(min_amount=-20.0) + assert len(transactions) == 1 + assert transactions[0]["transactionValue"] == -10.50 + + def test_get_transactions_with_search(self, mock_home_db_path, sample_transactions): + """Test searching transactions by description.""" + ctx = MockContext() + + with patch("pathlib.Path.home") as mock_home: + mock_home.return_value = mock_home_db_path.parent / ".." + + sqlite_db.persist_transactions(ctx, "test-account-123", sample_transactions) + + # Search for "Coffee" + transactions = sqlite_db.get_transactions(search="Coffee") + assert len(transactions) == 1 + assert "Coffee" in transactions[0]["description"] + + def test_get_transactions_empty_database(self, mock_home_db_path): + """Test getting transactions from empty database.""" + with patch("pathlib.Path.home") as mock_home: + mock_home.return_value = mock_home_db_path.parent / ".." + + transactions = sqlite_db.get_transactions() + assert transactions == [] + + def test_get_transactions_nonexistent_database(self): + """Test getting transactions when database doesn't exist.""" + with patch("pathlib.Path.home") as mock_home: + mock_home.return_value = Path("/nonexistent") + + transactions = sqlite_db.get_transactions() + assert transactions == [] + + def test_persist_balances(self, mock_home_db_path, sample_balance): + """Test persisting balance data.""" + ctx = MockContext() + + with patch("pathlib.Path.home") as mock_home: + mock_home.return_value = mock_home_db_path.parent / ".." + + result = sqlite_db.persist_balances(ctx, sample_balance) + + # Should return the balance data + assert result["account_id"] == "test-account-123" + + def test_get_balances(self, mock_home_db_path, sample_balance): + """Test retrieving balances.""" + ctx = MockContext() + + with patch("pathlib.Path.home") as mock_home: + mock_home.return_value = mock_home_db_path.parent / ".." + + # Insert test balance + sqlite_db.persist_balances(ctx, sample_balance) + + # Get balances + balances = sqlite_db.get_balances() + + assert len(balances) == 1 + assert balances[0]["account_id"] == "test-account-123" + assert balances[0]["amount"] == 1000.00 + + def test_get_balances_filtered_by_account(self, mock_home_db_path, sample_balance): + """Test filtering balances by account ID.""" + ctx = MockContext() + + # Create balance for different account + other_balance = sample_balance.copy() + other_balance["account_id"] = "other-account" + + with patch("pathlib.Path.home") as mock_home: + mock_home.return_value = mock_home_db_path.parent / ".." + + sqlite_db.persist_balances(ctx, sample_balance) + sqlite_db.persist_balances(ctx, other_balance) + + # Filter by account + balances = sqlite_db.get_balances(account_id="test-account-123") + + assert len(balances) == 1 + assert balances[0]["account_id"] == "test-account-123" + + def test_get_account_summary(self, mock_home_db_path, sample_transactions): + """Test getting account summary from transactions.""" + ctx = MockContext() + + with patch("pathlib.Path.home") as mock_home: + mock_home.return_value = mock_home_db_path.parent / ".." + + sqlite_db.persist_transactions(ctx, "test-account-123", sample_transactions) + + summary = sqlite_db.get_account_summary("test-account-123") + + assert summary is not None + assert summary["accountId"] == "test-account-123" + assert summary["institutionId"] == "REVOLUT_REVOLT21" + assert summary["iban"] == "LT313250081177977789" + + def test_get_account_summary_nonexistent(self, mock_home_db_path): + """Test getting summary for nonexistent account.""" + with patch("pathlib.Path.home") as mock_home: + mock_home.return_value = mock_home_db_path.parent / ".." + + summary = sqlite_db.get_account_summary("nonexistent") + assert summary is None + + def test_get_transaction_count(self, mock_home_db_path, sample_transactions): + """Test getting transaction count.""" + ctx = MockContext() + + with patch("pathlib.Path.home") as mock_home: + mock_home.return_value = mock_home_db_path.parent / ".." + + sqlite_db.persist_transactions(ctx, "test-account-123", sample_transactions) + + # Get total count + count = sqlite_db.get_transaction_count() + assert count == 2 + + # Get count for specific account + count_filtered = sqlite_db.get_transaction_count( + account_id="test-account-123" + ) + assert count_filtered == 2 + + # Get count for nonexistent account + count_none = sqlite_db.get_transaction_count(account_id="nonexistent") + assert count_none == 0 + + def test_get_transaction_count_with_filters( + self, mock_home_db_path, sample_transactions + ): + """Test getting transaction count with filters.""" + ctx = MockContext() + + with patch("pathlib.Path.home") as mock_home: + mock_home.return_value = mock_home_db_path.parent / ".." + + sqlite_db.persist_transactions(ctx, "test-account-123", sample_transactions) + + # Filter by search + count = sqlite_db.get_transaction_count(search="Coffee") + assert count == 1 + + # Filter by amount + count = sqlite_db.get_transaction_count(min_amount=-20.0) + assert count == 1 + + def test_database_indexes_created(self, mock_home_db_path, sample_transactions): + """Test that database indexes are created properly.""" + ctx = MockContext() + + with patch("pathlib.Path.home") as mock_home: + mock_home.return_value = mock_home_db_path.parent / ".." + + # Persist transactions to create tables and indexes + sqlite_db.persist_transactions(ctx, "test-account-123", sample_transactions) + + # Get transactions to ensure we can query the table (indexes working) + transactions = sqlite_db.get_transactions(account_id="test-account-123") + assert len(transactions) == 2