"""Tests for `expenses_handlers.py` — voice-driven personal finance tracker.""" import os import sys import time as _time import pytest sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) def _capture(eh): captured = [] eh.set_speak(captured.append) return captured def test_parse_expense_extracts_amount_and_category(isolated_state): eh = isolated_state('expenses_handlers', ['_PATH']) assert eh.parse_expense("потратил 500 на еду") == (500.0, "еду") assert eh.parse_expense("spent 12.50 on coffee") == (12.5, "coffee") # Comma decimal (RU keyboard) assert eh.parse_expense("потратил 12,50 на транспорт") == (12.5, "транспорт") # No category → default amount, cat = eh.parse_expense("потратил 100") assert amount == 100.0 assert cat == "разное" # Garbage assert eh.parse_expense("какая погода") is None assert eh.parse_expense("потратил bla") is None assert eh.parse_expense("потратил -100") is None assert eh.parse_expense("") is None def test_log_appends_entry(isolated_state): eh = isolated_state('expenses_handlers', ['_PATH']) captured = _capture(eh) eh.do_expenses_log(voice="потратил 250 на кофе") data = eh.load() assert len(data['entries']) == 1 e = data['entries'][0] assert e['amount'] == 250.0 assert e['category'] == "кофе" assert "250" in captured[0] assert "кофе" in captured[0] def test_log_ignores_unparseable(isolated_state): eh = isolated_state('expenses_handlers', ['_PATH']) captured = _capture(eh) eh.do_expenses_log(voice="ну блин") assert eh.load()['entries'] == [] assert "не понял" in captured[0].lower() def test_today_sums_only_today(isolated_state): eh = isolated_state('expenses_handlers', ['_PATH']) # Seed data: one entry today, one entry yesterday now = int(_time.time()) eh._save({'entries': [ {'ts': now, 'amount': 100.0, 'category': 'еда'}, {'ts': now - 86400, 'amount': 999.0, 'category': 'старое'}, ]}) captured = _capture(eh) eh.do_expenses_today() assert "100" in captured[0] assert "999" not in captured[0] def test_week_sums_last_7_days(isolated_state): eh = isolated_state('expenses_handlers', ['_PATH']) now = int(_time.time()) eh._save({'entries': [ {'ts': now - 3 * 86400, 'amount': 100.0, 'category': 'a'}, {'ts': now - 5 * 86400, 'amount': 200.0, 'category': 'b'}, {'ts': now - 10 * 86400, 'amount': 999.0, 'category': 'старое'}, ]}) captured = _capture(eh) eh.do_expenses_week() # 100 + 200 = 300 in last 7 days; 999 excluded assert "300" in captured[0] assert "999" not in captured[0] assert "2 трат" in captured[0] def test_breakdown_sorts_by_amount(isolated_state): eh = isolated_state('expenses_handlers', ['_PATH']) now = int(_time.time()) eh._save({'entries': [ {'ts': now, 'amount': 100, 'category': 'small'}, {'ts': now, 'amount': 500, 'category': 'big'}, {'ts': now, 'amount': 200, 'category': 'medium'}, ]}) captured = _capture(eh) eh.do_expenses_breakdown() text = captured[0] # big should appear before medium before small assert text.index("big") < text.index("medium") < text.index("small") def test_breakdown_collapses_tail(isolated_state): eh = isolated_state('expenses_handlers', ['_PATH']) now = int(_time.time()) eh._save({'entries': [ {'ts': now, 'amount': float(i + 1) * 10, 'category': f'c{i}'} for i in range(8) # 8 categories — top 5 spoken, 3 collapse ]}) captured = _capture(eh) eh.do_expenses_breakdown() assert "и другое" in captured[0] def test_today_when_empty_says_so(isolated_state): eh = isolated_state('expenses_handlers', ['_PATH']) captured = _capture(eh) eh.do_expenses_today() assert "ничего" in captured[0].lower()