"""Tests for ledgerly.repository (schema, seeds, CRUD, reporting queries).""" import sqlite3 import pytest from ledgerly.errors import ConflictError from ledgerly.models import DEFAULT_CATEGORIES, EXPENSE, INCOME from ledgerly.repository import Repository, SqliteRepository @pytest.fixture() def repo(): r = SqliteRepository(":memory:") yield r r.close() def test_implements_protocol(): r = SqliteRepository(":memory:") assert isinstance(r, Repository) r.close() def test_seeds_default_categories(repo): cats = repo.list_categories() names = {name for name, _ in cats} assert "Salary" in names assert "Rent" in names assert len(cats) == len(DEFAULT_CATEGORIES) def test_seed_idempotent(tmp_path): path = tmp_path / "sub" / "t.db" first = SqliteRepository(path) count = len(first.list_categories()) first.close() second = SqliteRepository(path) assert len(second.list_categories()) == count second.close() def test_deleted_categories_do_not_resurrect(tmp_path): path = tmp_path / "t.db" first = SqliteRepository(path) for name, _kind in first.list_categories(): first.delete_category(first.find_category(name)[0]) first.close() second = SqliteRepository(path) assert second.list_categories() == [] second.close() def test_create_category_duplicate_raises(repo): with pytest.raises(ConflictError): repo.create_category("Groceries", EXPENSE) def test_find_category_case_insensitive(repo): _id, name, kind = repo.find_category("grocErIes") assert name == "Groceries" assert kind == EXPENSE def test_find_category_missing(repo): assert repo.find_category("Nope") is None def test_list_categories_income_first(repo): cats = repo.list_categories() kinds = [kind for _name, kind in cats] assert kinds.index(INCOME) < kinds.index(EXPENSE) def test_category_in_use_transitions(repo): rent_id = repo.find_category("Rent")[0] assert not repo.category_in_use(rent_id) tx_id = repo.insert_transaction(100, rent_id, "2026-10-01", None) assert repo.category_in_use(rent_id) repo.delete_transaction(tx_id) assert not repo.category_in_use(rent_id) def test_category_in_use_via_budget(repo): rent_id = repo.find_category("Rent")[0] repo.set_budget(rent_id, 1000) assert repo.category_in_use(rent_id) def test_delete_category(repo): cat_id = repo.find_category("Rent")[0] repo.delete_category(cat_id) assert repo.find_category("Rent") is None def test_insert_and_list_transactions(repo): cat_id = repo.find_category("Groceries")[0] repo.insert_transaction(8210, cat_id, "2026-10-02", "weekly shop") repo.insert_transaction(4500, cat_id, "2026-10-20", None) rows = repo.list_transactions(None, None, None) assert rows[0]["date"] == "2026-10-20" # newest first assert rows[0]["category"] == "Groceries" assert rows[0]["note"] is None def test_list_transactions_filters(repo): grocery_id = repo.find_category("Groceries")[0] salary_id = repo.find_category("Salary")[0] repo.insert_transaction(8210, grocery_id, "2026-10-02", None) repo.insert_transaction(4500, salary_id, "2026-11-01", None) assert len(repo.list_transactions("2026-10-01", "2026-10-31", None)) == 1 assert len(repo.list_transactions(None, None, grocery_id)) == 1 def test_transaction_exists_and_delete(repo): cat_id = repo.find_category("Rent")[0] tx_id = repo.insert_transaction(150000, cat_id, "2026-10-01", None) assert repo.transaction_exists(tx_id) repo.delete_transaction(tx_id) assert not repo.transaction_exists(tx_id) def test_insert_enforces_positive_amount(repo): cat_id = repo.find_category("Rent")[0] with pytest.raises(sqlite3.IntegrityError): repo.insert_transaction(0, cat_id, "2026-10-01", None) def test_insert_enforces_foreign_key(repo): with pytest.raises(sqlite3.IntegrityError): repo.insert_transaction(100, 99999, "2026-10-01", None) def test_set_budget_upsert(repo): cat_id = repo.find_category("Rent")[0] repo.set_budget(cat_id, 100000) assert repo.get_budget(cat_id) == 100000 repo.set_budget(cat_id, 120000) assert repo.get_budget(cat_id) == 120000 assert len(repo.list_budgets()) == 1 def test_delete_budget(repo): cat_id = repo.find_category("Rent")[0] assert not repo.delete_budget(cat_id) repo.set_budget(cat_id, 1000) assert repo.delete_budget(cat_id) assert repo.get_budget(cat_id) is None def test_list_budgets_shape(repo): repo.set_budget(repo.find_category("Rent")[0], 12345) row = repo.list_budgets()[0] assert row["category"] == "Rent" assert row["kind"] == EXPENSE assert row["amount_cents"] == 12345 def test_income_expense_totals_empty(repo): assert repo.income_expense_totals("2026-10-01", "2026-10-31") == (0, 0) def test_income_expense_totals(repo): salary_id = repo.find_category("Salary")[0] grocery_id = repo.find_category("Groceries")[0] repo.insert_transaction(300000, salary_id, "2026-10-01", None) repo.insert_transaction(50000, grocery_id, "2026-10-05", None) repo.insert_transaction(7777, grocery_id, "2026-09-05", None) # out of range assert repo.income_expense_totals("2026-10-01", "2026-10-31") == (300000, 50000) def test_category_totals_ordering(repo): salary_id = repo.find_category("Salary")[0] grocery_id = repo.find_category("Groceries")[0] rent_id = repo.find_category("Rent")[0] repo.insert_transaction(300000, salary_id, "2026-10-01", None) repo.insert_transaction(50000, grocery_id, "2026-10-05", None) repo.insert_transaction(99999, rent_id, "2026-10-03", None) rows = repo.category_totals("2026-10-01", "2026-10-31") assert [r["category"] for r in rows] == ["Salary", "Rent", "Groceries"] def test_context_manager_closes(tmp_path): path = tmp_path / "t.db" with SqliteRepository(path) as repo: assert repo is not None with pytest.raises(sqlite3.ProgrammingError): repo.list_categories()