from pathlib import Path import pytest from app.bot import ( parse_cancellation, parse_category, parse_transaction, webapp_url_for_user, ) from app.config import Settings from app.db import Database def make_database(tmp_path: Path) -> Database: database = Database(tmp_path / "test.sqlite3") database.initialize() return database def add_user(database: Database, user_id: int, name: str) -> None: database.upsert_user( user_id=user_id, first_name=name, last_name=None, username=None, ) def test_transaction_and_leaderboard(tmp_path: Path) -> None: database = make_database(tmp_path) add_user(database, 1, "Аня") item = parse_transaction( '{"v":1,"type":"transaction","kind":"income","amount":"1000.50","category":"Зарплата","occurred_at":"2026-01-01T00:00:00Z"}' ) database.add_transaction(1, item) expense = parse_transaction( '{"v":1,"type":"transaction","kind":"expense","amount":"250.25","category":"Еда","occurred_at":"2026-01-02T00:00:00Z"}' ) database.add_transaction(1, expense) summary = database.summary(1) assert summary is not None assert summary["income_cents"] == 100_050 assert summary["expense_cents"] == 25_025 assert summary["saved_percent"] == 75.0 assert database.leaderboard()[0]["nickname"] == "Аня" def test_friendship_is_symmetric_and_idempotent(tmp_path: Path) -> None: database = make_database(tmp_path) add_user(database, 3, "Три") add_user(database, 7, "Семь") assert database.add_friendship(7, 3) assert not database.add_friendship(3, 7) assert database.friend_ids(3) == [7] assert database.friend_ids(7) == [3] def test_filtered_leaderboard(tmp_path: Path) -> None: database = make_database(tmp_path) for user_id in (1, 2, 3): add_user(database, user_id, str(user_id)) database.add_transaction( 1, parse_transaction( '{"v":1,"type":"transaction","kind":"income","amount":"100","category":"Работа"}' ), ) database.add_transaction( 3, parse_transaction( '{"v":1,"type":"transaction","kind":"expense","amount":"40","category":"Еда"}' ), ) result = database.leaderboard(user_ids=[1, 3]) assert [item["user_id"] for item in result] == [1, 3] filtered = database.leaderboard(user_ids=[1, 3], categories=["Еда"]) assert filtered[0]["user_id"] == 1 assert filtered[0]["balance_cents"] == 0 assert filtered[1]["user_id"] == 3 assert filtered[1]["balance_cents"] == -4_000 @pytest.mark.parametrize( "payload", [ "not-json", '{"v":1,"type":"transaction","kind":"other","amount":"10","category":"Еда"}', '{"v":1,"type":"transaction","kind":"income","amount":"-1","category":"Зарплата"}', '{"v":1,"type":"transaction","kind":"income","amount":"10","category":""}', ], ) def test_rejects_invalid_webapp_payload(payload: str) -> None: with pytest.raises(ValueError): parse_transaction(payload) def test_personalized_keyboard_webapp_url(tmp_path: Path) -> None: settings = Settings( telegram_token="token", base_app_url="https://example.com/", data_dir=tmp_path, ) assert webapp_url_for_user(settings, 123456) == ( "https://example.com/app/?user_id=123456&mode=keyboard" ) def test_custom_category_and_cancellation(tmp_path: Path) -> None: database = make_database(tmp_path) add_user(database, 10, "Владелец") add_user(database, 11, "Другой") kind, name = parse_category( '{"v":1,"type":"category_create","kind":"expense","category":"Подписки"}' ) assert database.add_category(10, kind, name) assert not database.add_category(10, kind, name) assert database.user_categories(10) == [{"kind": "expense", "name": "Подписки"}] transaction_id = database.add_transaction( 10, parse_transaction( '{"v":1,"type":"transaction","kind":"expense","amount":"499","category":"Подписки"}' ), ) assert parse_cancellation( f'{{"v":1,"type":"transaction_cancel","transaction_id":{transaction_id}}}' ) == transaction_id assert not database.cancel_transaction(11, transaction_id) assert database.cancel_transaction(10, transaction_id) assert not database.cancel_transaction(10, transaction_id) assert database.transactions(10) == [] assert database.summary(10)["expense_cents"] == 0 assert database.user_categories(10) == [{"kind": "expense", "name": "Подписки"}]