Skip to content

Commit 338722a

Browse files
committed
feat: add tests for optimized member enrichment function in group service
1 parent da9383e commit 338722a

File tree

2 files changed

+332
-0
lines changed

2 files changed

+332
-0
lines changed

backend/tests/expenses/test_expense_service.py

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2109,5 +2109,122 @@ async def test_get_group_analytics_group_not_found(expense_service):
21092109
mock_db.users.find_one.assert_not_called()
21102110

21112111

2112+
@pytest.mark.asyncio
2113+
async def test_get_friends_balance_summary_aggregation_error(expense_service):
2114+
"""Test friends balance summary when aggregation fails"""
2115+
user_id_str = str(ObjectId())
2116+
2117+
with patch("app.expenses.service.mongodb") as mock_mongodb:
2118+
mock_db = MagicMock()
2119+
mock_mongodb.database = mock_db
2120+
2121+
# Mock groups
2122+
mock_groups = [
2123+
{
2124+
"_id": ObjectId(),
2125+
"name": "Test Group",
2126+
"members": [{"userId": user_id_str}, {"userId": str(ObjectId())}],
2127+
}
2128+
]
2129+
mock_groups_cursor = AsyncMock()
2130+
mock_groups_cursor.to_list.return_value = mock_groups
2131+
mock_db.groups.find.return_value = mock_groups_cursor
2132+
2133+
# Mock aggregation failure
2134+
mock_agg_cursor = AsyncMock()
2135+
mock_agg_cursor.to_list.side_effect = Exception("Aggregation failed")
2136+
mock_db.settlements.aggregate.return_value = mock_agg_cursor
2137+
2138+
result = await expense_service.get_friends_balance_summary(user_id_str)
2139+
2140+
# Should return empty results on error
2141+
assert len(result["friendsBalance"]) == 0
2142+
assert result["summary"]["totalOwedToYou"] == 0
2143+
assert result["summary"]["totalYouOwe"] == 0
2144+
assert result["summary"]["friendCount"] == 0
2145+
2146+
2147+
@pytest.mark.asyncio
2148+
async def test_get_friends_balance_summary_user_fetch_error(expense_service):
2149+
"""Test friends balance summary when fetching user details fails"""
2150+
user_id_str = str(ObjectId())
2151+
friend_id_str = str(ObjectId())
2152+
2153+
with patch("app.expenses.service.mongodb") as mock_mongodb:
2154+
mock_db = MagicMock()
2155+
mock_mongodb.database = mock_db
2156+
2157+
# Mock groups
2158+
mock_groups = [
2159+
{
2160+
"_id": ObjectId(),
2161+
"name": "Test Group",
2162+
"members": [{"userId": user_id_str}, {"userId": friend_id_str}],
2163+
}
2164+
]
2165+
mock_groups_cursor = AsyncMock()
2166+
mock_groups_cursor.to_list.return_value = mock_groups
2167+
mock_db.groups.find.return_value = mock_groups_cursor
2168+
2169+
# Mock aggregation success
2170+
mock_agg_cursor = AsyncMock()
2171+
mock_agg_cursor.to_list.return_value = [
2172+
{
2173+
"_id": friend_id_str,
2174+
"totalBalance": 50.0,
2175+
"groups": [{"groupId": str(mock_groups[0]["_id"]), "balance": 50.0}],
2176+
}
2177+
]
2178+
mock_db.settlements.aggregate.return_value = mock_agg_cursor
2179+
2180+
# Mock user fetch failure
2181+
mock_users_cursor = AsyncMock()
2182+
mock_users_cursor.to_list.side_effect = Exception("User fetch failed")
2183+
mock_db.users.find.return_value = mock_users_cursor
2184+
2185+
result = await expense_service.get_friends_balance_summary(user_id_str)
2186+
2187+
# Should still return results but with "Unknown" for user names
2188+
assert len(result["friendsBalance"]) == 1
2189+
assert result["friendsBalance"][0]["userName"] == "Unknown"
2190+
assert result["friendsBalance"][0]["netBalance"] == 50.0
2191+
2192+
2193+
@pytest.mark.asyncio
2194+
async def test_get_friends_balance_summary_zero_balance_filtering(expense_service):
2195+
"""Test that friends with zero balance are filtered out"""
2196+
user_id_str = str(ObjectId())
2197+
2198+
with patch("app.expenses.service.mongodb") as mock_mongodb:
2199+
mock_db = MagicMock()
2200+
mock_mongodb.database = mock_db
2201+
2202+
# Mock groups
2203+
mock_groups = [
2204+
{
2205+
"_id": ObjectId(),
2206+
"name": "Test Group",
2207+
"members": [{"userId": user_id_str}],
2208+
}
2209+
]
2210+
mock_groups_cursor = AsyncMock()
2211+
mock_groups_cursor.to_list.return_value = mock_groups
2212+
mock_db.groups.find.return_value = mock_groups_cursor
2213+
2214+
# Mock aggregation returns no results (all filtered by zero balance)
2215+
mock_agg_cursor = AsyncMock()
2216+
mock_agg_cursor.to_list.return_value = []
2217+
mock_db.settlements.aggregate.return_value = mock_agg_cursor
2218+
2219+
result = await expense_service.get_friends_balance_summary(user_id_str)
2220+
2221+
# Should return empty friend balance
2222+
assert len(result["friendsBalance"]) == 0
2223+
assert result["summary"]["totalOwedToYou"] == 0
2224+
assert result["summary"]["totalYouOwe"] == 0
2225+
assert result["summary"]["friendCount"] == 0
2226+
assert result["summary"]["activeGroups"] == 1
2227+
2228+
21122229
if __name__ == "__main__":
21132230
pytest.main([__file__])
Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
"""Tests for the optimized _enrich_members_with_user_details function"""
2+
3+
from unittest.mock import AsyncMock, MagicMock, patch
4+
5+
import pytest
6+
from app.groups.service import GroupService
7+
from bson import ObjectId
8+
9+
10+
class TestEnrichMembersOptimized:
11+
"""Test cases for _enrich_members_with_user_details optimized function"""
12+
13+
def setup_method(self):
14+
"""Setup for each test method"""
15+
self.service = GroupService()
16+
17+
@pytest.mark.asyncio
18+
async def test_enrich_members_with_user_details_success(self):
19+
"""Test successful enrichment of members with user details"""
20+
user_id_1 = str(ObjectId())
21+
user_id_2 = str(ObjectId())
22+
user_id_3 = str(ObjectId())
23+
24+
members = [
25+
{"userId": user_id_1, "role": "admin", "joinedAt": "2023-01-01"},
26+
{"userId": user_id_2, "role": "member", "joinedAt": "2023-01-02"},
27+
{"userId": user_id_3, "role": "member", "joinedAt": "2023-01-03"},
28+
]
29+
30+
mock_users = [
31+
{"_id": ObjectId(user_id_1), "name": "Admin User", "imageUrl": "admin.jpg"},
32+
{
33+
"_id": ObjectId(user_id_2),
34+
"name": "Member One",
35+
"imageUrl": "member1.jpg",
36+
},
37+
{"_id": ObjectId(user_id_3), "name": "Member Two", "imageUrl": None},
38+
]
39+
40+
mock_db = MagicMock()
41+
mock_users_collection = MagicMock()
42+
mock_db.users = mock_users_collection
43+
44+
# Mock the find operation
45+
mock_cursor = AsyncMock()
46+
mock_cursor.to_list.return_value = mock_users
47+
mock_users_collection.find.return_value = mock_cursor
48+
49+
with patch.object(self.service, "get_db", return_value=mock_db):
50+
enriched = await self.service._enrich_members_with_user_details(members)
51+
52+
assert len(enriched) == 3
53+
assert enriched[0]["userId"] == user_id_1
54+
assert enriched[0]["user"]["name"] == "Admin User"
55+
assert enriched[0]["user"]["imageUrl"] == "admin.jpg"
56+
assert enriched[0]["role"] == "admin"
57+
58+
assert enriched[1]["userId"] == user_id_2
59+
assert enriched[1]["user"]["name"] == "Member One"
60+
assert enriched[1]["user"]["imageUrl"] == "member1.jpg"
61+
62+
assert enriched[2]["userId"] == user_id_3
63+
assert enriched[2]["user"]["name"] == "Member Two"
64+
assert enriched[2]["user"]["imageUrl"] is None
65+
66+
# Verify the query was made correctly with $in operator
67+
mock_users_collection.find.assert_called_once()
68+
call_args = mock_users_collection.find.call_args
69+
assert "_id" in call_args[0][0]
70+
assert "$in" in call_args[0][0]["_id"]
71+
72+
@pytest.mark.asyncio
73+
async def test_enrich_members_empty_list(self):
74+
"""Test enrichment with empty members list"""
75+
mock_db = MagicMock()
76+
77+
with patch.object(self.service, "get_db", return_value=mock_db):
78+
enriched = await self.service._enrich_members_with_user_details([])
79+
80+
assert enriched == []
81+
# Verify no database call was made
82+
mock_db.users.find.assert_not_called()
83+
84+
@pytest.mark.asyncio
85+
async def test_enrich_members_missing_user_data(self):
86+
"""Test enrichment when some users are not found in database"""
87+
user_id_1 = str(ObjectId())
88+
user_id_2 = str(ObjectId())
89+
90+
members = [
91+
{"userId": user_id_1, "role": "admin", "joinedAt": "2023-01-01"},
92+
{"userId": user_id_2, "role": "member", "joinedAt": "2023-01-02"},
93+
]
94+
95+
# Only return data for user_id_1, not user_id_2
96+
mock_users = [
97+
{"_id": ObjectId(user_id_1), "name": "Admin User", "imageUrl": "admin.jpg"},
98+
]
99+
100+
mock_db = MagicMock()
101+
mock_users_collection = MagicMock()
102+
mock_db.users = mock_users_collection
103+
104+
mock_cursor = AsyncMock()
105+
mock_cursor.to_list.return_value = mock_users
106+
mock_users_collection.find.return_value = mock_cursor
107+
108+
with patch.object(self.service, "get_db", return_value=mock_db):
109+
enriched = await self.service._enrich_members_with_user_details(members)
110+
111+
assert len(enriched) == 2
112+
assert enriched[0]["user"]["name"] == "Admin User"
113+
# Missing user should have fallback name
114+
assert "User" in enriched[1]["user"]["name"] # Will be "User <last4digits>"
115+
116+
@pytest.mark.asyncio
117+
async def test_enrich_members_database_error(self):
118+
"""Test enrichment when database query fails"""
119+
user_id_1 = str(ObjectId())
120+
121+
members = [
122+
{"userId": user_id_1, "role": "admin", "joinedAt": "2023-01-01"},
123+
]
124+
125+
mock_db = MagicMock()
126+
mock_users_collection = MagicMock()
127+
mock_db.users = mock_users_collection
128+
129+
# Simulate database error
130+
mock_cursor = AsyncMock()
131+
mock_cursor.to_list.side_effect = Exception("Database connection error")
132+
mock_users_collection.find.return_value = mock_cursor
133+
134+
with patch.object(self.service, "get_db", return_value=mock_db):
135+
enriched = await self.service._enrich_members_with_user_details(members)
136+
137+
# Should still return members with fallback user data
138+
assert len(enriched) == 1
139+
assert "User" in enriched[0]["user"]["name"] # Fallback name
140+
assert enriched[0]["user"]["imageUrl"] is None
141+
142+
@pytest.mark.asyncio
143+
async def test_enrich_members_preserves_member_fields(self):
144+
"""Test that enrichment preserves all original member fields"""
145+
user_id_1 = str(ObjectId())
146+
147+
members = [
148+
{
149+
"userId": user_id_1,
150+
"role": "admin",
151+
"joinedAt": "2023-01-01",
152+
"customField": "custom_value",
153+
},
154+
]
155+
156+
mock_users = [
157+
{"_id": ObjectId(user_id_1), "name": "Admin User", "imageUrl": "admin.jpg"},
158+
]
159+
160+
mock_db = MagicMock()
161+
mock_users_collection = MagicMock()
162+
mock_db.users = mock_users_collection
163+
164+
mock_cursor = AsyncMock()
165+
mock_cursor.to_list.return_value = mock_users
166+
mock_users_collection.find.return_value = mock_cursor
167+
168+
with patch.object(self.service, "get_db", return_value=mock_db):
169+
enriched = await self.service._enrich_members_with_user_details(members)
170+
171+
# Verify all fields are preserved
172+
assert enriched[0]["userId"] == user_id_1
173+
assert enriched[0]["role"] == "admin"
174+
assert enriched[0]["joinedAt"] == "2023-01-01"
175+
# Note: customField won't be in the output as the function creates a new structure
176+
# It only preserves userId, role, and joinedAt
177+
assert enriched[0]["user"]["name"] == "Admin User"
178+
assert enriched[0]["user"]["imageUrl"] == "admin.jpg"
179+
180+
@pytest.mark.asyncio
181+
async def test_enrich_members_batch_query_optimization(self):
182+
"""Test that the function uses a single batch query instead of N queries"""
183+
# Create 10 members
184+
members = []
185+
user_ids = []
186+
for i in range(10):
187+
user_id = str(ObjectId())
188+
user_ids.append(user_id)
189+
members.append(
190+
{"userId": user_id, "role": "member", "joinedAt": f"2023-01-{i+1:02d}"}
191+
)
192+
193+
mock_users = [
194+
{"_id": ObjectId(uid), "name": f"User {i}", "imageUrl": None}
195+
for i, uid in enumerate(user_ids)
196+
]
197+
198+
mock_db = MagicMock()
199+
mock_users_collection = MagicMock()
200+
mock_db.users = mock_users_collection
201+
202+
mock_cursor = AsyncMock()
203+
mock_cursor.to_list.return_value = mock_users
204+
mock_users_collection.find.return_value = mock_cursor
205+
206+
with patch.object(self.service, "get_db", return_value=mock_db):
207+
enriched = await self.service._enrich_members_with_user_details(members)
208+
209+
# Verify only ONE database call was made (batch query)
210+
assert mock_users_collection.find.call_count == 1
211+
212+
# Verify all 10 members were enriched
213+
assert len(enriched) == 10
214+
for i, member in enumerate(enriched):
215+
assert member["user"]["name"] == f"User {i}"

0 commit comments

Comments
 (0)