finance-telegram-bot/tests/test_database.py
2026-07-14 12:21:12 +07:00

257 lines
9.6 KiB
Python

from datetime import datetime, timedelta
from zoneinfo import ZoneInfo
import pytest
from app.database import Database
from app.database import FamilyJoinError
from app.categories import category_label
from app.domain import Category, TransactionKind
@pytest.mark.asyncio
async def test_transaction_lifecycle_and_totals(tmp_path):
database = Database(tmp_path / "finance.db")
await database.initialize()
now = datetime(2026, 7, 13, 12, tzinfo=ZoneInfo("UTC"))
await database.upsert_user(
user_id=10,
chat_id=10,
username="tester",
full_name="Test User",
timezone="UTC",
report_time="21:00",
now=now,
)
await database.upsert_user(
user_id=20,
chat_id=20,
username=None,
full_name="Other User",
timezone="UTC",
report_time="21:00",
now=now,
)
expense = await database.add_transaction(
user_id=10,
amount_kopecks=30_000,
description="бензин",
kind=TransactionKind.EXPENSE,
category=Category.CAR,
occurred_at=now,
)
await database.add_transaction(
user_id=10,
amount_kopecks=100_000,
description="подработка",
kind=TransactionKind.INCOME,
category=Category.INCOME,
occurred_at=now,
)
totals = await database.totals(10, now - timedelta(hours=1), now + timedelta(hours=1))
assert totals.income == 100_000
assert totals.expense == 30_000
assert totals.balance == 70_000
assert await database.category_totals(
10, now - timedelta(hours=1), now + timedelta(hours=1)
) == {category_label(Category.CAR): 30_000}
assert await database.category_totals(10) == {category_label(Category.CAR): 30_000}
# Другой пользователь не может изменить или удалить чужую запись.
assert not await database.update_category(expense.id, 20, Category.OTHER)
assert not await database.delete_transaction(expense.id, 20)
assert await database.update_category(expense.id, 10, Category.OTHER)
assert (await database.get_transaction(expense.id, 10)).category is Category.OTHER
assert await database.delete_transaction(expense.id, 10)
@pytest.mark.asyncio
async def test_daily_report_idempotency(tmp_path):
database = Database(tmp_path / "finance.db")
await database.initialize()
now = datetime(2026, 7, 13, 21, tzinfo=ZoneInfo("UTC"))
await database.upsert_user(
user_id=10,
chat_id=10,
username=None,
full_name="Test User",
timezone="UTC",
report_time="21:00",
now=now,
)
assert not await database.report_was_sent(10, "2026-07-13")
await database.mark_report_sent(10, "2026-07-13", now)
await database.mark_report_sent(10, "2026-07-13", now)
assert await database.report_was_sent(10, "2026-07-13")
@pytest.mark.asyncio
async def test_family_budget_shares_history_and_new_transactions(tmp_path):
database = Database(tmp_path / "finance.db")
await database.initialize()
now = datetime(2026, 7, 13, 20, tzinfo=ZoneInfo("UTC"))
for user_id, name in ((10, "Алексей"), (20, "Мария"), (30, "Посторонний")):
await database.upsert_user(
user_id=user_id,
chat_id=user_id,
username=None,
full_name=name,
timezone="UTC",
report_time="21:00",
now=now,
)
await database.add_transaction(
user_id=10,
amount_kopecks=30_000,
description="бензин",
kind=TransactionKind.EXPENSE,
category=Category.CAR,
occurred_at=now,
)
await database.add_transaction(
user_id=20,
amount_kopecks=10_000,
description="ярче",
kind=TransactionKind.EXPENSE,
category=Category.PRODUCTS,
occurred_at=now,
)
family = await database.create_family(10, now)
request = await database.request_family_join(20, family.invite_code.lower(), now)
assert request.owner_chat_id == 10
with pytest.raises(FamilyJoinError, match="обработан"):
await database.resolve_family_join(
owner_user_id=30,
candidate_user_id=20,
approve=True,
now=now,
)
approved, candidate_chat_id, joined = await database.resolve_family_join(
owner_user_id=10,
candidate_user_id=20,
approve=True,
now=now,
)
assert approved
assert candidate_chat_id == 20
assert joined is not None
assert joined.household_id == family.household_id
assert joined.members == ("Алексей", "Мария")
for user_id in (10, 20):
totals = await database.totals(user_id)
assert totals.expense == 40_000
assert await database.category_totals(user_id) == {
category_label(Category.CAR): 30_000,
category_label(Category.PRODUCTS): 10_000,
}
recent = await database.recent_transactions(user_id)
assert {item.author_name for item in recent} == {"Алексей", "Мария"}
recipients = await database.family_recipients(10)
assert [(row["user_id"], row["chat_id"]) for row in recipients] == [(20, 20)]
with pytest.raises(FamilyJoinError, match="создатель"):
await database.bind_family_chat(20, -100500, "Наш бюджет")
bound = await database.bind_family_chat(10, -100500, "Наш бюджет")
assert bound.notification_chat_id == -100500
assert await database.family_notification_chats(10) == [20, -100500]
assert await database.family_report_target(20) == (family.household_id, -100500)
assert not await database.family_report_was_sent(family.household_id, "2026-07-13")
await database.mark_family_report_sent(family.household_id, "2026-07-13", now)
assert await database.family_report_was_sent(family.household_id, "2026-07-13")
with pytest.raises(FamilyJoinError, match="создатель"):
await database.set_family_debt(20, 53_071_300, now)
await database.set_family_debt(10, 53_071_300, now)
salary = await database.add_transaction(
user_id=20,
amount_kopecks=40_000_000,
description="зарплата",
kind=TransactionKind.INCOME,
category=Category.INCOME,
occurred_at=now + timedelta(seconds=1),
)
assert await database.family_debt(10) == 13_071_300
purchase = await database.add_transaction(
user_id=10,
amount_kopecks=2_000_000,
description="покупка",
kind=TransactionKind.EXPENSE,
category=Category.OTHER,
occurred_at=now + timedelta(seconds=2),
)
assert await database.family_debt(20) == 15_071_300
assert await database.delete_transaction(purchase.id, 10)
assert await database.family_debt(10) == 13_071_300
assert salary.author_name == "Мария"
categories = await database.budget_categories(10)
fuel = next(category for category in categories if category.system_key == "car_fuel")
assert fuel.parent_name == "Машина"
health = await database.create_budget_category(
user_id=20,
name="Здоровье",
parent_id=None,
now=now,
)
doctors = await database.create_budget_category(
user_id=10,
name="Врачи",
parent_id=health.id,
now=now,
)
other = await database.add_transaction(
user_id=10,
amount_kopecks=50_000,
description="консультация",
kind=TransactionKind.EXPENSE,
category=Category.OTHER,
occurred_at=now + timedelta(seconds=3),
)
assert await database.update_transaction_budget_category(other.id, 10, doctors.id)
moved = await database.get_transaction(other.id, 10)
assert moved.parent_category_name == "Здоровье"
assert moved.budget_category_name == "Врачи"
analytics = await database.category_totals(20)
assert analytics["📁 Здоровье → 📁 Врачи"] == 50_000
with pytest.raises(FamilyJoinError, match="два участника"):
await database.request_family_join(30, family.invite_code, now)
@pytest.mark.asyncio
async def test_existing_database_is_migrated_for_family_budget(tmp_path):
import aiosqlite
path = tmp_path / "old.db"
async with aiosqlite.connect(path) as db:
await db.executescript(
"""
CREATE TABLE users (
user_id INTEGER PRIMARY KEY, chat_id INTEGER NOT NULL, username TEXT,
full_name TEXT NOT NULL, timezone TEXT NOT NULL, report_time TEXT NOT NULL,
created_at TEXT NOT NULL, updated_at TEXT NOT NULL
);
CREATE TABLE transactions (
id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL,
amount_kopecks INTEGER NOT NULL, description TEXT NOT NULL,
kind TEXT NOT NULL, category TEXT NOT NULL,
occurred_at TEXT NOT NULL, created_at TEXT NOT NULL
);
"""
)
await db.commit()
database = Database(path)
await database.initialize()
async with aiosqlite.connect(path) as db:
cursor = await db.execute("PRAGMA table_info(transactions)")
columns = {row[1] for row in await cursor.fetchall()}
assert "household_id" in columns
async with aiosqlite.connect(path) as db:
cursor = await db.execute("PRAGMA table_info(households)")
household_columns = {row[1] for row in await cursor.fetchall()}
assert {"debt_base_kopecks", "debt_started_at"} <= household_columns