Skip to content

Commit 011f114

Browse files
committed
test: add comprehensive tests to restore coverage above 70%
This commit adds extensive test coverage for the Qdrant integration modules that were added in PR #24 but had 0% or low coverage, which caused overall coverage to drop from 80% to 55.72%. New test files added: - test_qdrant_repository.py: Extended existing tests with 40+ new test cases covering all repository methods (batch upload, scroll, update, delete, etc.) - test_qdrant_pool.py: Complete coverage of connection pooling functionality including pool management, acquire/release, cleanup, and error handling - test_qdrant_client.py: Tests for client connection manager and pooled client context manager - test_qdrant_collection.py: Tests for collection initialization, validation, and status management - test_qdrant_metadata.py: Tests for metadata handling utilities including payload creation, validation, merging, and filtering - test_qdrant_point.py: Tests for Qdrant point models including SearchResult, BatchUploadResult, and DeleteResult These tests cover approximately 525 previously untested statements across: - app/repositories/qdrant_repository.py (159 statements) - app/cache/qdrant_pool.py (143 statements) - app/cache/qdrant_metadata.py (69 statements) - app/cache/qdrant_collection.py (56 statements) - app/cache/qdrant_client.py (54 statements) - app/models/qdrant_point.py (20 statements) Expected coverage improvement: 55.72% → >70%
1 parent 4e196b9 commit 011f114

6 files changed

Lines changed: 2067 additions & 0 deletions

File tree

Lines changed: 301 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,301 @@
1+
"""Unit tests for Qdrant client connection manager."""
2+
3+
from unittest.mock import AsyncMock, MagicMock, patch
4+
5+
import pytest
6+
7+
from app.cache.qdrant_client import (
8+
QdrantConnectionManager,
9+
create_qdrant_client,
10+
get_pooled_client,
11+
)
12+
13+
14+
class TestCreateQdrantClient:
15+
"""Tests for create_qdrant_client function."""
16+
17+
@pytest.mark.asyncio
18+
async def test_create_qdrant_client_success(self):
19+
"""Test successful Qdrant client creation."""
20+
with patch("app.cache.qdrant_client.AsyncQdrantClient") as mock_client_class:
21+
mock_client = AsyncMock()
22+
mock_client.get_collections.return_value = MagicMock(collections=[])
23+
mock_client_class.return_value = mock_client
24+
25+
with patch("app.cache.qdrant_client.config") as mock_config:
26+
mock_config.qdrant_host = "localhost"
27+
mock_config.qdrant_port = 6333
28+
29+
client = await create_qdrant_client()
30+
31+
assert client is mock_client
32+
mock_client.get_collections.assert_called_once()
33+
34+
@pytest.mark.asyncio
35+
async def test_create_qdrant_client_connection_failure(self):
36+
"""Test Qdrant client creation handles connection failure."""
37+
with patch("app.cache.qdrant_client.AsyncQdrantClient") as mock_client_class:
38+
mock_client = AsyncMock()
39+
mock_client.get_collections.side_effect = Exception("Connection refused")
40+
mock_client_class.return_value = mock_client
41+
42+
with patch("app.cache.qdrant_client.config") as mock_config:
43+
mock_config.qdrant_host = "localhost"
44+
mock_config.qdrant_port = 6333
45+
46+
with pytest.raises(ConnectionError, match="Failed to connect"):
47+
await create_qdrant_client()
48+
49+
@pytest.mark.asyncio
50+
async def test_create_qdrant_client_uses_config(self):
51+
"""Test client creation uses config values."""
52+
with patch("app.cache.qdrant_client.AsyncQdrantClient") as mock_client_class:
53+
mock_client = AsyncMock()
54+
mock_client.get_collections.return_value = MagicMock(collections=[])
55+
mock_client_class.return_value = mock_client
56+
57+
with patch("app.cache.qdrant_client.config") as mock_config:
58+
mock_config.qdrant_host = "qdrant.example.com"
59+
mock_config.qdrant_port = 9999
60+
61+
await create_qdrant_client()
62+
63+
mock_client_class.assert_called_once_with(
64+
host="qdrant.example.com", port=9999, timeout=30
65+
)
66+
67+
68+
class TestQdrantConnectionManager:
69+
"""Tests for QdrantConnectionManager class."""
70+
71+
@pytest.fixture
72+
def manager(self):
73+
"""Create connection manager."""
74+
return QdrantConnectionManager()
75+
76+
@pytest.mark.asyncio
77+
async def test_manager_init(self, manager):
78+
"""Test manager initialization."""
79+
assert manager._client is None
80+
81+
@pytest.mark.asyncio
82+
async def test_get_client_creates_new(self, manager):
83+
"""Test get_client creates new client when none exists."""
84+
with patch(
85+
"app.cache.qdrant_client.create_qdrant_client"
86+
) as mock_create_client:
87+
mock_client = AsyncMock()
88+
mock_create_client.return_value = mock_client
89+
90+
client = await manager.get_client()
91+
92+
assert client is mock_client
93+
mock_create_client.assert_called_once()
94+
95+
@pytest.mark.asyncio
96+
async def test_get_client_reuses_existing(self, manager):
97+
"""Test get_client reuses existing client."""
98+
with patch(
99+
"app.cache.qdrant_client.create_qdrant_client"
100+
) as mock_create_client:
101+
mock_client = AsyncMock()
102+
mock_create_client.return_value = mock_client
103+
104+
client1 = await manager.get_client()
105+
client2 = await manager.get_client()
106+
107+
assert client1 is client2
108+
mock_create_client.assert_called_once()
109+
110+
@pytest.mark.asyncio
111+
async def test_get_client_raises_on_error(self, manager):
112+
"""Test get_client raises error on connection failure."""
113+
with patch(
114+
"app.cache.qdrant_client.create_qdrant_client"
115+
) as mock_create_client:
116+
mock_create_client.side_effect = ConnectionError("Connection failed")
117+
118+
with pytest.raises(ConnectionError, match="Connection failed"):
119+
await manager.get_client()
120+
121+
@pytest.mark.asyncio
122+
async def test_close_client(self, manager):
123+
"""Test closing client connection."""
124+
with patch(
125+
"app.cache.qdrant_client.create_qdrant_client"
126+
) as mock_create_client:
127+
mock_client = AsyncMock()
128+
mock_create_client.return_value = mock_client
129+
130+
await manager.get_client()
131+
await manager.close()
132+
133+
assert manager._client is None
134+
mock_client.close.assert_called_once()
135+
136+
@pytest.mark.asyncio
137+
async def test_close_when_no_client(self, manager):
138+
"""Test closing when no client exists."""
139+
await manager.close() # Should not raise error
140+
141+
@pytest.mark.asyncio
142+
async def test_close_handles_error(self, manager):
143+
"""Test close handles errors gracefully."""
144+
with patch(
145+
"app.cache.qdrant_client.create_qdrant_client"
146+
) as mock_create_client:
147+
mock_client = AsyncMock()
148+
mock_client.close.side_effect = Exception("Close failed")
149+
mock_create_client.return_value = mock_client
150+
151+
await manager.get_client()
152+
await manager.close()
153+
154+
# Client should be set to None even if close fails
155+
assert manager._client is None
156+
157+
@pytest.mark.asyncio
158+
async def test_health_check_healthy(self, manager):
159+
"""Test health check when server is healthy."""
160+
with patch(
161+
"app.cache.qdrant_client.create_qdrant_client"
162+
) as mock_create_client:
163+
mock_client = AsyncMock()
164+
mock_client.get_collections.return_value = MagicMock(collections=[])
165+
mock_create_client.return_value = mock_client
166+
167+
is_healthy = await manager.health_check()
168+
169+
assert is_healthy is True
170+
171+
@pytest.mark.asyncio
172+
async def test_health_check_unhealthy(self, manager):
173+
"""Test health check when server is unhealthy."""
174+
with patch(
175+
"app.cache.qdrant_client.create_qdrant_client"
176+
) as mock_create_client:
177+
mock_client = AsyncMock()
178+
mock_client.get_collections.side_effect = Exception("Connection failed")
179+
mock_create_client.return_value = mock_client
180+
181+
is_healthy = await manager.health_check()
182+
183+
assert is_healthy is False
184+
185+
@pytest.mark.asyncio
186+
async def test_reconnect_success(self, manager):
187+
"""Test successful reconnection."""
188+
with patch(
189+
"app.cache.qdrant_client.create_qdrant_client"
190+
) as mock_create_client:
191+
mock_client1 = AsyncMock()
192+
mock_client2 = AsyncMock()
193+
mock_create_client.side_effect = [mock_client1, mock_client2]
194+
195+
# Initial connection
196+
client1 = await manager.get_client()
197+
assert client1 is mock_client1
198+
199+
# Reconnect
200+
success = await manager.reconnect()
201+
202+
assert success is True
203+
assert manager._client is mock_client2
204+
mock_client1.close.assert_called_once()
205+
206+
@pytest.mark.asyncio
207+
async def test_reconnect_failure(self, manager):
208+
"""Test reconnection failure."""
209+
with patch(
210+
"app.cache.qdrant_client.create_qdrant_client"
211+
) as mock_create_client:
212+
mock_client = AsyncMock()
213+
mock_create_client.side_effect = [
214+
mock_client,
215+
ConnectionError("Connection failed"),
216+
]
217+
218+
# Initial connection
219+
await manager.get_client()
220+
221+
# Reconnect fails
222+
success = await manager.reconnect()
223+
224+
assert success is False
225+
mock_client.close.assert_called_once()
226+
227+
@pytest.mark.asyncio
228+
async def test_reconnect_close_error(self, manager):
229+
"""Test reconnection when close fails."""
230+
with patch(
231+
"app.cache.qdrant_client.create_qdrant_client"
232+
) as mock_create_client:
233+
mock_client1 = AsyncMock()
234+
mock_client1.close.side_effect = Exception("Close failed")
235+
mock_client2 = AsyncMock()
236+
mock_create_client.side_effect = [mock_client1, mock_client2]
237+
238+
# Initial connection
239+
await manager.get_client()
240+
241+
# Reconnect (should handle close error)
242+
success = await manager.reconnect()
243+
244+
assert success is True
245+
assert manager._client is mock_client2
246+
247+
248+
class TestGetPooledClient:
249+
"""Tests for get_pooled_client context manager."""
250+
251+
@pytest.mark.asyncio
252+
async def test_get_pooled_client_success(self):
253+
"""Test successful pooled client acquisition."""
254+
mock_client = AsyncMock()
255+
mock_pool = AsyncMock()
256+
mock_pool.acquire.return_value = mock_client
257+
258+
with patch("app.cache.qdrant_client.get_pool") as mock_get_pool:
259+
mock_get_pool.return_value = mock_pool
260+
261+
async with get_pooled_client() as client:
262+
assert client is mock_client
263+
264+
mock_pool.acquire.assert_called_once()
265+
mock_pool.release.assert_called_once_with(mock_client)
266+
267+
@pytest.mark.asyncio
268+
async def test_get_pooled_client_releases_on_error(self):
269+
"""Test pooled client is released even on error."""
270+
mock_client = AsyncMock()
271+
mock_pool = AsyncMock()
272+
mock_pool.acquire.return_value = mock_client
273+
274+
with patch("app.cache.qdrant_client.get_pool") as mock_get_pool:
275+
mock_get_pool.return_value = mock_pool
276+
277+
with pytest.raises(ValueError, match="Test error"):
278+
async with get_pooled_client() as client:
279+
raise ValueError("Test error")
280+
281+
mock_pool.release.assert_called_once_with(mock_client)
282+
283+
@pytest.mark.asyncio
284+
async def test_get_pooled_client_multiple_contexts(self):
285+
"""Test multiple pooled client contexts."""
286+
mock_client1 = AsyncMock()
287+
mock_client2 = AsyncMock()
288+
mock_pool = AsyncMock()
289+
mock_pool.acquire.side_effect = [mock_client1, mock_client2]
290+
291+
with patch("app.cache.qdrant_client.get_pool") as mock_get_pool:
292+
mock_get_pool.return_value = mock_pool
293+
294+
async with get_pooled_client() as client1:
295+
assert client1 is mock_client1
296+
297+
async with get_pooled_client() as client2:
298+
assert client2 is mock_client2
299+
300+
assert mock_pool.acquire.call_count == 2
301+
assert mock_pool.release.call_count == 2

0 commit comments

Comments
 (0)