Skip to content

Commit 58a4ada

Browse files
committed
test: add unit tests to increase coverage
- Budget service business logic and calculations - Configuration management and validation - HTTP client API communication and error handling - Entry point transport configuration - Pydantic model validation rules - Server initialization and authentication setup - Utility functions for assets and HTTP routes
1 parent 5b4ae85 commit 58a4ada

File tree

7 files changed

+1487
-0
lines changed

7 files changed

+1487
-0
lines changed

tests/unit/test_budgets_service.py

Lines changed: 328 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,328 @@
1+
"""Unit tests for BudgetService."""
2+
3+
from datetime import date
4+
from unittest.mock import AsyncMock, MagicMock, patch
5+
6+
import pytest
7+
8+
from lampyrid.models.lampyrid_models import (
9+
GetAvailableBudgetRequest,
10+
GetBudgetSpendingRequest,
11+
GetBudgetSummaryRequest,
12+
)
13+
from lampyrid.services.budgets import BudgetService
14+
15+
16+
class TestBudgetService:
17+
"""Test cases for BudgetService class."""
18+
19+
@pytest.fixture
20+
def mock_service(self):
21+
"""Create a BudgetService with mocked FireflyClient."""
22+
with patch('lampyrid.services.budgets.FireflyClient') as mock_firefly:
23+
mock_client_instance = AsyncMock()
24+
mock_firefly.return_value = mock_client_instance
25+
26+
service = BudgetService(mock_client_instance)
27+
return service, mock_client_instance
28+
29+
@pytest.mark.asyncio
30+
async def test_get_budget_spending_with_spent_entries(self, mock_service):
31+
"""Test get_budget_spending when spent entries exist."""
32+
service, mock_client = mock_service
33+
34+
# Mock response with spent entries
35+
mock_budget_single = MagicMock()
36+
mock_budget_single.data.id = '1'
37+
mock_budget_single.data.attributes.name = 'Test Budget'
38+
39+
mock_limits_response = MagicMock()
40+
mock_limits_response.data = [
41+
MagicMock(
42+
attributes=MagicMock(
43+
spent=[MagicMock(sum='50.0'), MagicMock(sum='25.0')], amount='200.0'
44+
)
45+
)
46+
]
47+
48+
mock_client.get_budget.return_value = mock_budget_single
49+
mock_client.get_budget_limits.return_value = mock_limits_response
50+
51+
req = GetBudgetSpendingRequest(
52+
budget_id='1', start_date=date(2023, 1, 1), end_date=date(2023, 12, 31)
53+
)
54+
55+
result = await service.get_budget_spending(req)
56+
57+
# Verify calculations
58+
assert result.budget_id == '1'
59+
assert result.budget_name == 'Test Budget'
60+
assert result.spent == 75.0 # 50.0 + 25.0
61+
assert result.budgeted == 200.0
62+
assert result.remaining == 125.0
63+
assert result.percentage_spent == 37.5 # (75.0 / 200.0) * 100
64+
65+
@pytest.mark.asyncio
66+
async def test_get_budget_spending_without_spent_entries(self, mock_service):
67+
"""Test get_budget_spending when no spent entries exist."""
68+
service, mock_client = mock_service
69+
70+
# Mock response without spent entries but with amount
71+
mock_budget_single = MagicMock()
72+
mock_budget_single.data.id = '1'
73+
mock_budget_single.data.attributes.name = 'Test Budget'
74+
75+
mock_limits_response = MagicMock()
76+
mock_limits_response.data = [MagicMock(attributes=MagicMock(spent=None, amount='150.0'))]
77+
78+
mock_client.get_budget.return_value = mock_budget_single
79+
mock_client.get_budget_limits.return_value = mock_limits_response
80+
81+
req = GetBudgetSpendingRequest(
82+
budget_id='1', start_date=date(2023, 1, 1), end_date=date(2023, 12, 31)
83+
)
84+
85+
result = await service.get_budget_spending(req)
86+
87+
# Verify calculations
88+
assert result.budget_id == '1'
89+
assert result.budget_name == 'Test Budget'
90+
assert result.spent == 0.0
91+
assert result.budgeted == 150.0
92+
assert result.remaining == 150.0
93+
assert result.percentage_spent == 0.0
94+
95+
@pytest.mark.asyncio
96+
async def test_get_budget_spending_without_amount(self, mock_service):
97+
"""Test get_budget_spending when no amount is set."""
98+
service, mock_client = mock_service
99+
100+
# Mock response with spent entries but no amount
101+
mock_budget_single = MagicMock()
102+
mock_budget_single.data.id = '1'
103+
mock_budget_single.data.attributes.name = 'Test Budget'
104+
105+
mock_limits_response = MagicMock()
106+
mock_limits_response.data = [
107+
MagicMock(attributes=MagicMock(spent=[MagicMock(sum='30.0')], amount=None))
108+
]
109+
110+
mock_client.get_budget.return_value = mock_budget_single
111+
mock_client.get_budget_limits.return_value = mock_limits_response
112+
113+
req = GetBudgetSpendingRequest(
114+
budget_id='1', start_date=date(2023, 1, 1), end_date=date(2023, 12, 31)
115+
)
116+
117+
result = await service.get_budget_spending(req)
118+
119+
# Verify calculations when no amount is set
120+
assert result.budget_id == '1'
121+
assert result.budget_name == 'Test Budget'
122+
assert result.spent == 30.0
123+
assert result.budgeted is None
124+
assert result.remaining is None
125+
assert result.percentage_spent is None
126+
127+
@pytest.mark.asyncio
128+
async def test_get_budget_summary_with_budgeted_amounts(self, mock_service):
129+
"""Test get_budget_summary when budgets have budgeted amounts."""
130+
service, mock_client = mock_service
131+
132+
# Mock budgets response
133+
mock_budgets_response = MagicMock()
134+
mock_budgets_response.data = [MagicMock(id='1'), MagicMock(id='2')]
135+
136+
# Mock individual budget spending calls
137+
async def mock_get_budget_limits(budget_id, start_date, end_date):
138+
if budget_id == '1':
139+
mock_response = MagicMock()
140+
mock_response.data = [
141+
MagicMock(
142+
attributes=MagicMock(
143+
spent=[MagicMock(sum='50.0'), MagicMock(sum='0.0')], amount='100.0'
144+
)
145+
)
146+
]
147+
return mock_response
148+
else:
149+
mock_response = MagicMock()
150+
mock_response.data = [
151+
MagicMock(
152+
attributes=MagicMock(
153+
spent=[MagicMock(sum='25.0'), MagicMock(sum='0.0')], amount='50.0'
154+
)
155+
)
156+
]
157+
return mock_response
158+
159+
# Mock individual budget calls for budget names
160+
def mock_get_budget(budget_id):
161+
mock_budget = MagicMock()
162+
if budget_id == '1':
163+
mock_budget.data.attributes.name = 'Budget 1'
164+
else:
165+
mock_budget.data.attributes.name = 'Budget 2'
166+
return mock_budget
167+
168+
mock_client.get_budgets.return_value = mock_budgets_response
169+
mock_client.get_budget.side_effect = mock_get_budget
170+
mock_client.get_budget_limits.side_effect = mock_get_budget_limits
171+
172+
req = GetBudgetSummaryRequest(start_date=date(2023, 1, 1), end_date=date(2023, 12, 31))
173+
174+
result = await service.get_budget_summary(req)
175+
176+
# Verify summary calculations
177+
assert len(result.budgets) == 2
178+
assert result.total_spent == 75.0 # 50.0 + 25.0
179+
assert result.total_budgeted == 150.0 # 100.0 + 50.0
180+
assert result.total_remaining == 75.0
181+
assert result.available_budget is None
182+
183+
@pytest.mark.asyncio
184+
async def test_get_budget_summary_without_budgeted_amounts(self, mock_service):
185+
"""Test get_budget_summary when budgets have no budgeted amounts."""
186+
service, mock_client = mock_service
187+
188+
# Mock budgets response
189+
mock_budgets_response = MagicMock()
190+
mock_budgets_response.data = [MagicMock(id='1'), MagicMock(id='2')]
191+
192+
# Mock individual budget spending with no budgeted amounts
193+
async def mock_get_budget_limits(budget_id, start_date, end_date):
194+
if budget_id == '1':
195+
mock_response = MagicMock()
196+
mock_response.data = [
197+
MagicMock(attributes=MagicMock(spent=[MagicMock(sum='30.0')], amount=None))
198+
]
199+
return mock_response
200+
else:
201+
mock_response = MagicMock()
202+
mock_response.data = [
203+
MagicMock(attributes=MagicMock(spent=[MagicMock(sum='20.0')], amount=None))
204+
]
205+
return mock_response
206+
207+
# Mock individual budget calls for budget names
208+
def mock_get_budget(budget_id):
209+
mock_budget = MagicMock()
210+
if budget_id == '1':
211+
mock_budget.data.attributes.name = 'Budget 1'
212+
else:
213+
mock_budget.data.attributes.name = 'Budget 2'
214+
return mock_budget
215+
216+
mock_client.get_budgets.return_value = mock_budgets_response
217+
mock_client.get_budget.side_effect = mock_get_budget
218+
mock_client.get_budget_limits.side_effect = mock_get_budget_limits
219+
220+
req = GetBudgetSummaryRequest(start_date=date(2023, 1, 1), end_date=date(2023, 12, 31))
221+
222+
result = await service.get_budget_summary(req)
223+
224+
# Verify summary calculations
225+
assert len(result.budgets) == 2
226+
assert result.total_spent == 50.0 # 30.0 + 20.0
227+
assert result.total_budgeted is None
228+
assert result.total_remaining is None
229+
assert result.available_budget is None
230+
231+
@pytest.mark.asyncio
232+
async def test_get_available_budget_with_data(self, mock_service):
233+
"""Test get_available_budget when data is available."""
234+
service, mock_client = mock_service
235+
236+
# Mock available budgets response
237+
mock_available_response = MagicMock()
238+
mock_budget = MagicMock()
239+
mock_budget.attributes.amount = '500.0'
240+
mock_budget.attributes.currency_code = 'USD'
241+
mock_budget.attributes.start = MagicMock()
242+
mock_budget.attributes.start.date.return_value = date(2023, 1, 1)
243+
mock_budget.attributes.end = MagicMock()
244+
mock_budget.attributes.end.date.return_value = date(2023, 12, 31)
245+
246+
mock_available_response.data = [mock_budget]
247+
248+
mock_client.get_available_budgets.return_value = mock_available_response
249+
250+
req = GetAvailableBudgetRequest(start_date=date(2023, 1, 1), end_date=date(2023, 12, 31))
251+
252+
result = await service.get_available_budget(req)
253+
254+
# Verify result with available data
255+
assert result.amount == 500.0
256+
assert result.currency_code == 'USD'
257+
assert result.start_date == date(2023, 1, 1)
258+
assert result.end_date == date(2023, 12, 31)
259+
260+
@pytest.mark.asyncio
261+
async def test_get_available_budget_without_data(self, mock_service):
262+
"""Test get_available_budget when no data is available."""
263+
service, mock_client = mock_service
264+
265+
# Mock empty available budgets response
266+
mock_available_response = MagicMock()
267+
mock_available_response.data = []
268+
269+
mock_client.get_available_budgets.return_value = mock_available_response
270+
271+
req = GetAvailableBudgetRequest(start_date=date(2023, 1, 1), end_date=date(2023, 12, 31))
272+
273+
result = await service.get_available_budget(req)
274+
275+
# Verify default result when no data
276+
assert result.amount == 0.0
277+
assert result.currency_code == 'USD'
278+
assert result.start_date == date(2023, 1, 1)
279+
assert result.end_date == date(2023, 12, 31)
280+
281+
@pytest.mark.asyncio
282+
async def test_get_available_budget_uses_default_dates(self, mock_service):
283+
"""Test get_available_budget uses default dates when not provided."""
284+
service, mock_client = mock_service
285+
286+
# Mock empty available budgets response
287+
mock_available_response = MagicMock()
288+
mock_available_response.data = []
289+
290+
mock_client.get_available_budgets.return_value = mock_available_response
291+
292+
# Request with no dates
293+
req = GetAvailableBudgetRequest()
294+
295+
result = await service.get_available_budget(req)
296+
297+
# Verify default dates are used
298+
today = date.today()
299+
assert result.start_date == today.replace(day=1)
300+
assert result.end_date == today
301+
302+
@pytest.mark.asyncio
303+
async def test_create_budget(self, mock_service):
304+
"""Test creating a new budget."""
305+
service, mock_client = mock_service
306+
307+
# Mock response
308+
mock_budget_single = MagicMock()
309+
mock_budget_single.data.id = '123'
310+
mock_budget_single.data.attributes.name = 'New Budget'
311+
mock_budget_single.data.attributes.active = True
312+
mock_budget_single.data.attributes.notes = None
313+
mock_budget_single.data.attributes.order = 0
314+
315+
mock_client.create_budget.return_value = mock_budget_single
316+
317+
# Mock BudgetStore to avoid complex required fields
318+
budget_store = MagicMock()
319+
320+
result = await service.create_budget(budget_store)
321+
322+
# Verify the client was called
323+
mock_client.create_budget.assert_called_once_with(budget_store)
324+
325+
# Verify the result is converted correctly
326+
assert result.id == '123'
327+
assert result.name == 'New Budget'
328+
assert result.active is True

0 commit comments

Comments
 (0)