Skip to content

Commit a801bb1

Browse files
committed
test(db): add comprehensive unit tests for connection module
- Test _get_async_url URL conversion for all dialects - Test DatabaseManager initialization and properties - Test async initialization, session management, and cleanup - Test global get_database and close_database functions - Test error handling for uninitialized connections Coverage: 19 tests for db/connection.py
1 parent 0ce465c commit a801bb1

1 file changed

Lines changed: 270 additions & 0 deletions

File tree

tests/unit/test_connection.py

Lines changed: 270 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,270 @@
1+
"""
2+
Unit tests for database connection management.
3+
"""
4+
5+
import pytest
6+
from unittest.mock import AsyncMock, MagicMock, patch
7+
8+
from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession
9+
10+
from app.exceptions import DatabaseConnectionException
11+
from db.connection import (
12+
DatabaseManager,
13+
_get_async_url,
14+
get_database,
15+
close_database,
16+
)
17+
18+
19+
class TestGetAsyncUrl:
20+
"""Tests for _get_async_url helper function."""
21+
22+
def test_postgresql_conversion(self) -> None:
23+
"""Test PostgreSQL URL conversion to asyncpg."""
24+
url = "postgresql://user:pass@localhost/db"
25+
result = _get_async_url(url)
26+
assert result == "postgresql+asyncpg://user:pass@localhost/db"
27+
28+
def test_mysql_conversion(self) -> None:
29+
"""Test MySQL URL conversion to aiomysql."""
30+
url = "mysql://user:pass@localhost/db"
31+
result = _get_async_url(url)
32+
assert result == "mysql+aiomysql://user:pass@localhost/db"
33+
34+
def test_sqlite_conversion(self) -> None:
35+
"""Test SQLite URL conversion to aiosqlite."""
36+
url = "sqlite:///./data/test.db"
37+
result = _get_async_url(url)
38+
assert result == "sqlite+aiosqlite:///./data/test.db"
39+
40+
def test_already_async_url(self) -> None:
41+
"""Test URL that already has async driver."""
42+
url = "postgresql+asyncpg://user:pass@localhost/db"
43+
result = _get_async_url(url)
44+
assert result == url
45+
46+
def test_unknown_dialect(self) -> None:
47+
"""Test unknown database dialect."""
48+
url = "oracle://user:pass@localhost/db"
49+
result = _get_async_url(url)
50+
assert result == url
51+
52+
53+
class TestDatabaseManager:
54+
"""Tests for DatabaseManager class."""
55+
56+
@pytest.fixture
57+
def mock_settings(self) -> MagicMock:
58+
"""Create mock settings."""
59+
settings = MagicMock()
60+
settings.database.url = "sqlite:///./test.db"
61+
settings.database.pool_size = 5
62+
settings.database.max_overflow = 10
63+
settings.database.pool_timeout = 30
64+
return settings
65+
66+
def test_init_with_defaults(self, mock_settings: MagicMock) -> None:
67+
"""Test initialization with default settings."""
68+
with patch("db.connection.get_settings", return_value=mock_settings):
69+
manager = DatabaseManager()
70+
71+
assert manager._url == "sqlite:///./test.db"
72+
assert manager._pool_size == 5
73+
assert manager._is_initialized is False
74+
75+
def test_init_with_custom_url(self, mock_settings: MagicMock) -> None:
76+
"""Test initialization with custom URL."""
77+
with patch("db.connection.get_settings", return_value=mock_settings):
78+
manager = DatabaseManager(url="postgresql://localhost/custom")
79+
80+
assert manager._url == "postgresql://localhost/custom"
81+
82+
def test_is_sqlite_property(self, mock_settings: MagicMock) -> None:
83+
"""Test is_sqlite property."""
84+
with patch("db.connection.get_settings", return_value=mock_settings):
85+
manager = DatabaseManager(url="sqlite:///test.db")
86+
assert manager.is_sqlite is True
87+
88+
manager2 = DatabaseManager(url="postgresql://localhost/db")
89+
assert manager2.is_sqlite is False
90+
91+
def test_dialect_property(self, mock_settings: MagicMock) -> None:
92+
"""Test dialect property for different database types."""
93+
with patch("db.connection.get_settings", return_value=mock_settings):
94+
# PostgreSQL
95+
manager = DatabaseManager(url="postgresql://localhost/db")
96+
assert manager.dialect == "postgresql"
97+
98+
# MySQL
99+
manager = DatabaseManager(url="mysql://localhost/db")
100+
assert manager.dialect == "mysql"
101+
102+
# SQLite
103+
manager = DatabaseManager(url="sqlite:///test.db")
104+
assert manager.dialect == "sqlite"
105+
106+
# Unknown
107+
manager = DatabaseManager(url="unknown://localhost/db")
108+
assert manager.dialect == "unknown"
109+
110+
@pytest.mark.asyncio
111+
async def test_initialize_success(self, mock_settings: MagicMock) -> None:
112+
"""Test successful database initialization."""
113+
with patch("db.connection.get_settings", return_value=mock_settings):
114+
with patch("db.connection.create_async_engine") as mock_engine:
115+
mock_engine.return_value = MagicMock(spec=AsyncEngine)
116+
117+
manager = DatabaseManager(url="sqlite:///test.db")
118+
await manager.initialize()
119+
120+
assert manager._is_initialized is True
121+
assert manager._engine is not None
122+
mock_engine.assert_called_once()
123+
124+
@pytest.mark.asyncio
125+
async def test_initialize_already_initialized(
126+
self, mock_settings: MagicMock
127+
) -> None:
128+
"""Test that initialize is idempotent."""
129+
with patch("db.connection.get_settings", return_value=mock_settings):
130+
with patch("db.connection.create_async_engine") as mock_engine:
131+
mock_engine.return_value = MagicMock(spec=AsyncEngine)
132+
133+
manager = DatabaseManager(url="sqlite:///test.db")
134+
await manager.initialize()
135+
await manager.initialize() # Second call should be no-op
136+
137+
# Should only be called once
138+
assert mock_engine.call_count == 1
139+
140+
@pytest.mark.asyncio
141+
async def test_initialize_failure(self, mock_settings: MagicMock) -> None:
142+
"""Test database initialization failure."""
143+
with patch("db.connection.get_settings", return_value=mock_settings):
144+
with patch(
145+
"db.connection.create_async_engine",
146+
side_effect=Exception("Connection failed"),
147+
):
148+
manager = DatabaseManager(url="sqlite:///test.db")
149+
150+
with pytest.raises(DatabaseConnectionException) as exc_info:
151+
await manager.initialize()
152+
153+
assert "Failed to initialize database" in str(exc_info.value.message)
154+
assert manager._is_initialized is False
155+
156+
@pytest.mark.asyncio
157+
async def test_close(self, mock_settings: MagicMock) -> None:
158+
"""Test closing database connection."""
159+
with patch("db.connection.get_settings", return_value=mock_settings):
160+
mock_engine = MagicMock(spec=AsyncEngine)
161+
mock_engine.dispose = AsyncMock()
162+
163+
manager = DatabaseManager(url="sqlite:///test.db")
164+
manager._engine = mock_engine
165+
manager._is_initialized = True
166+
167+
await manager.close()
168+
169+
mock_engine.dispose.assert_called_once()
170+
assert manager._engine is None
171+
assert manager._is_initialized is False
172+
173+
@pytest.mark.asyncio
174+
async def test_session_not_initialized(self, mock_settings: MagicMock) -> None:
175+
"""Test session access when not initialized."""
176+
with patch("db.connection.get_settings", return_value=mock_settings):
177+
manager = DatabaseManager(url="sqlite:///test.db")
178+
179+
with pytest.raises(DatabaseConnectionException) as exc_info:
180+
async with manager.session():
181+
pass
182+
183+
assert "not initialized" in str(exc_info.value.message)
184+
185+
@pytest.mark.asyncio
186+
async def test_engine_property_not_initialized(
187+
self, mock_settings: MagicMock
188+
) -> None:
189+
"""Test engine property access when not initialized."""
190+
with patch("db.connection.get_settings", return_value=mock_settings):
191+
manager = DatabaseManager(url="sqlite:///test.db")
192+
193+
with pytest.raises(DatabaseConnectionException) as exc_info:
194+
_ = manager.engine
195+
196+
assert "not initialized" in str(exc_info.value.message)
197+
198+
199+
class TestGlobalDatabaseFunctions:
200+
"""Tests for global database management functions."""
201+
202+
@pytest.fixture
203+
def mock_settings(self) -> MagicMock:
204+
"""Create mock settings."""
205+
settings = MagicMock()
206+
settings.database.url = "sqlite:///./test.db"
207+
settings.database.pool_size = 5
208+
settings.database.max_overflow = 10
209+
settings.database.pool_timeout = 30
210+
return settings
211+
212+
@pytest.fixture(autouse=True)
213+
def reset_global_manager(self) -> None:
214+
"""Reset global database manager between tests."""
215+
import db.connection
216+
217+
db.connection._db_manager = None
218+
yield
219+
db.connection._db_manager = None
220+
221+
@pytest.mark.asyncio
222+
async def test_get_database_creates_manager(
223+
self, mock_settings: MagicMock
224+
) -> None:
225+
"""Test get_database creates and initializes manager."""
226+
with patch("db.connection.get_settings", return_value=mock_settings):
227+
with patch("db.connection.create_async_engine") as mock_engine:
228+
mock_engine.return_value = MagicMock(spec=AsyncEngine)
229+
230+
manager = await get_database()
231+
232+
assert manager is not None
233+
assert manager._is_initialized is True
234+
235+
@pytest.mark.asyncio
236+
async def test_get_database_returns_same_instance(
237+
self, mock_settings: MagicMock
238+
) -> None:
239+
"""Test get_database returns the same instance."""
240+
with patch("db.connection.get_settings", return_value=mock_settings):
241+
with patch("db.connection.create_async_engine") as mock_engine:
242+
mock_engine.return_value = MagicMock(spec=AsyncEngine)
243+
244+
manager1 = await get_database()
245+
manager2 = await get_database()
246+
247+
assert manager1 is manager2
248+
249+
@pytest.mark.asyncio
250+
async def test_close_database(self, mock_settings: MagicMock) -> None:
251+
"""Test close_database closes the global manager."""
252+
with patch("db.connection.get_settings", return_value=mock_settings):
253+
with patch("db.connection.create_async_engine") as mock_engine:
254+
mock_engine_instance = MagicMock(spec=AsyncEngine)
255+
mock_engine_instance.dispose = AsyncMock()
256+
mock_engine.return_value = mock_engine_instance
257+
258+
await get_database()
259+
await close_database()
260+
261+
import db.connection
262+
263+
assert db.connection._db_manager is None
264+
265+
@pytest.mark.asyncio
266+
async def test_close_database_when_not_initialized(self) -> None:
267+
"""Test close_database when no manager exists."""
268+
# Should not raise any errors
269+
await close_database()
270+

0 commit comments

Comments
 (0)