236 lines
7.6 KiB
Python
236 lines
7.6 KiB
Python
|
|
"""End-to-end CLI tests: main() with a temp database, captured stdout/stderr."""
|
||
|
|
|
||
|
|
import pytest
|
||
|
|
|
||
|
|
from ledgerly import __version__
|
||
|
|
from ledgerly.cli import build_parser, default_db_path, main
|
||
|
|
from ledgerly.models import DEFAULT_CATEGORIES
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.fixture()
|
||
|
|
def db(tmp_path):
|
||
|
|
return str(tmp_path / "test.db")
|
||
|
|
|
||
|
|
|
||
|
|
def run(db, *argv):
|
||
|
|
return main(list(argv), db_path=db)
|
||
|
|
|
||
|
|
|
||
|
|
# -- basics --------------------------------------------------------------------
|
||
|
|
|
||
|
|
|
||
|
|
def test_version(db, capsys):
|
||
|
|
with pytest.raises(SystemExit) as exc:
|
||
|
|
main(["--version"], db_path=db)
|
||
|
|
assert exc.value.code == 0
|
||
|
|
assert __version__ in capsys.readouterr().out
|
||
|
|
|
||
|
|
|
||
|
|
def test_missing_command(db):
|
||
|
|
with pytest.raises(SystemExit) as exc:
|
||
|
|
main([], db_path=db)
|
||
|
|
assert exc.value.code == 2
|
||
|
|
|
||
|
|
|
||
|
|
def test_default_db_path_under_home():
|
||
|
|
path = default_db_path()
|
||
|
|
assert path.name == "ledgerly.db"
|
||
|
|
assert ".ledgerly" in path.parts
|
||
|
|
|
||
|
|
|
||
|
|
def test_parser_exposes_all_commands():
|
||
|
|
parser = build_parser()
|
||
|
|
assert parser.prog == "ledgerly"
|
||
|
|
|
||
|
|
|
||
|
|
# -- add / list / delete ---------------------------------------------------------
|
||
|
|
|
||
|
|
|
||
|
|
def test_add_and_list(db, capsys):
|
||
|
|
assert run(db, "add", "82.10", "--category", "Groceries",
|
||
|
|
"--date", "2026-10-02", "--note", "weekly") == 0
|
||
|
|
out = capsys.readouterr().out
|
||
|
|
assert "#1" in out and "$82.10" in out and "weekly" in out
|
||
|
|
assert run(db, "list", "--month", "2026-10") == 0
|
||
|
|
assert "Groceries" in capsys.readouterr().out
|
||
|
|
|
||
|
|
|
||
|
|
def test_add_default_date_and_month(db, capsys):
|
||
|
|
assert run(db, "add", "10.00", "--category", "Rent") == 0
|
||
|
|
assert run(db, "list") == 0 # current month by default
|
||
|
|
assert "Rent" in capsys.readouterr().out
|
||
|
|
|
||
|
|
|
||
|
|
def test_add_invalid_amount(db):
|
||
|
|
with pytest.raises(SystemExit) as exc:
|
||
|
|
run(db, "add", "abc", "--category", "Rent")
|
||
|
|
assert exc.value.code == 2
|
||
|
|
|
||
|
|
|
||
|
|
def test_add_invalid_date(db):
|
||
|
|
with pytest.raises(SystemExit) as exc:
|
||
|
|
run(db, "add", "1.00", "--category", "Rent", "--date", "nope")
|
||
|
|
assert exc.value.code == 2
|
||
|
|
|
||
|
|
|
||
|
|
def test_add_unknown_category_domain_error(db, capsys):
|
||
|
|
assert run(db, "add", "1.00", "--category", "Nope") == 1
|
||
|
|
assert "unknown category" in capsys.readouterr().err
|
||
|
|
|
||
|
|
|
||
|
|
def test_add_zero_amount(db, capsys):
|
||
|
|
assert run(db, "add", "0", "--category", "Rent") == 1
|
||
|
|
assert "greater than zero" in capsys.readouterr().err
|
||
|
|
|
||
|
|
|
||
|
|
def test_list_all(db, capsys):
|
||
|
|
run(db, "add", "5.00", "--category", "Rent", "--date", "2025-01-05")
|
||
|
|
run(db, "add", "6.00", "--category", "Rent", "--date", "2026-12-01")
|
||
|
|
assert run(db, "list", "--all") == 0
|
||
|
|
out = capsys.readouterr().out
|
||
|
|
assert "2025-01-05" in out and "2026-12-01" in out
|
||
|
|
|
||
|
|
|
||
|
|
def test_list_no_transactions(db, capsys):
|
||
|
|
assert run(db, "list", "--month", "2001-01") == 0
|
||
|
|
assert "no transactions" in capsys.readouterr().out
|
||
|
|
|
||
|
|
|
||
|
|
def test_list_by_category(db, capsys):
|
||
|
|
run(db, "add", "5.00", "--category", "Rent", "--date", "2026-10-05")
|
||
|
|
run(db, "add", "6.00", "--category", "Groceries", "--date", "2026-10-06")
|
||
|
|
capsys.readouterr() # discard add output; keep the list output clean
|
||
|
|
assert run(db, "list", "--month", "2026-10", "--category", "rent") == 0
|
||
|
|
out = capsys.readouterr().out
|
||
|
|
assert "Rent" in out and "Groceries" not in out
|
||
|
|
|
||
|
|
|
||
|
|
def test_delete(db, capsys):
|
||
|
|
assert run(db, "add", "5.00", "--category", "Rent", "--date", "2026-01-05") == 0
|
||
|
|
out = capsys.readouterr().out
|
||
|
|
tx_id = int(out.split("#", 1)[1].split(":", 1)[0].strip())
|
||
|
|
assert run(db, "delete", str(tx_id)) == 0
|
||
|
|
assert "deleted" in capsys.readouterr().out
|
||
|
|
|
||
|
|
|
||
|
|
def test_delete_unknown(db, capsys):
|
||
|
|
assert run(db, "delete", "999") == 1
|
||
|
|
assert "unknown transaction" in capsys.readouterr().err
|
||
|
|
|
||
|
|
|
||
|
|
# -- categories ------------------------------------------------------------------
|
||
|
|
|
||
|
|
|
||
|
|
def test_categories(db, capsys):
|
||
|
|
assert run(db, "categories") == 0
|
||
|
|
out = capsys.readouterr().out
|
||
|
|
assert "Salary" in out and "Rent" in out
|
||
|
|
|
||
|
|
|
||
|
|
def test_categories_empty(db, capsys):
|
||
|
|
for name, _kind in DEFAULT_CATEGORIES:
|
||
|
|
assert run(db, "category-del", name) == 0
|
||
|
|
assert run(db, "categories") == 0
|
||
|
|
assert "no categories" in capsys.readouterr().out
|
||
|
|
|
||
|
|
|
||
|
|
def test_category_add_and_del(db, capsys):
|
||
|
|
assert run(db, "category-add", "Pets", "--type", "expense") == 0
|
||
|
|
assert "Pets" in capsys.readouterr().out
|
||
|
|
assert run(db, "category-add", "pets", "--type", "income") == 1
|
||
|
|
assert "already exists" in capsys.readouterr().err
|
||
|
|
assert run(db, "category-del", "Pets") == 0
|
||
|
|
assert "deleted category" in capsys.readouterr().out
|
||
|
|
|
||
|
|
|
||
|
|
def test_category_add_bad_type(db):
|
||
|
|
with pytest.raises(SystemExit) as exc:
|
||
|
|
run(db, "category-add", "Pets", "--type", "asset")
|
||
|
|
assert exc.value.code == 2
|
||
|
|
|
||
|
|
|
||
|
|
def test_category_del_in_use(db, capsys):
|
||
|
|
run(db, "add", "5.00", "--category", "Rent", "--date", "2026-01-05")
|
||
|
|
assert run(db, "category-del", "Rent") == 1
|
||
|
|
assert "transactions or a budget" in capsys.readouterr().err
|
||
|
|
|
||
|
|
|
||
|
|
# -- budgets -----------------------------------------------------------------------
|
||
|
|
|
||
|
|
|
||
|
|
def test_budget_set_and_del(db, capsys):
|
||
|
|
assert run(db, "budget-set", "Rent", "1500") == 0
|
||
|
|
assert "$1,500.00" in capsys.readouterr().out
|
||
|
|
assert run(db, "budget-set", "rent", "2000") == 0 # replace
|
||
|
|
assert "$2,000.00" in capsys.readouterr().out
|
||
|
|
assert run(db, "budget-del", "Rent") == 0
|
||
|
|
assert "removed budget" in capsys.readouterr().out
|
||
|
|
|
||
|
|
|
||
|
|
def test_budget_set_income_category(db, capsys):
|
||
|
|
assert run(db, "budget-set", "Salary", "100") == 1
|
||
|
|
assert "expense" in capsys.readouterr().err
|
||
|
|
|
||
|
|
|
||
|
|
def test_budget_set_invalid_amount(db):
|
||
|
|
with pytest.raises(SystemExit) as exc:
|
||
|
|
run(db, "budget-set", "Rent", "not-money")
|
||
|
|
assert exc.value.code == 2
|
||
|
|
|
||
|
|
|
||
|
|
def test_budget_del_not_set(db, capsys):
|
||
|
|
assert run(db, "budget-del", "Rent") == 1
|
||
|
|
assert "no budget" in capsys.readouterr().err
|
||
|
|
|
||
|
|
|
||
|
|
def test_budget_show_with_overage(db, capsys):
|
||
|
|
run(db, "budget-set", "Rent", "1500")
|
||
|
|
run(db, "add", "1600.00", "--category", "Rent", "--date", "2026-10-05")
|
||
|
|
assert run(db, "budget-show", "--month", "2026-10") == 0
|
||
|
|
out = capsys.readouterr().out
|
||
|
|
assert "Rent" in out and "OVER" in out and "budgets for 2026-10" in out
|
||
|
|
|
||
|
|
|
||
|
|
def test_budget_show_flags_unbudgeted_spending(db, capsys):
|
||
|
|
run(db, "add", "40.00", "--category", "Groceries", "--date", "2026-10-02")
|
||
|
|
assert run(db, "budget-show", "--month", "2026-10") == 0
|
||
|
|
out = capsys.readouterr().out
|
||
|
|
assert "Groceries" in out and "OVER" in out
|
||
|
|
|
||
|
|
|
||
|
|
def test_budget_show_no_budgets_no_spending(db, capsys):
|
||
|
|
assert run(db, "budget-show", "--month", "2026-10") == 0
|
||
|
|
assert "no budgets set" in capsys.readouterr().out
|
||
|
|
|
||
|
|
|
||
|
|
# -- report ---------------------------------------------------------------------------
|
||
|
|
|
||
|
|
|
||
|
|
def test_report(db, capsys):
|
||
|
|
run(db, "add", "3000", "--category", "Salary", "--date", "2026-10-01")
|
||
|
|
run(db, "add", "1200", "--category", "Rent", "--date", "2026-10-02")
|
||
|
|
run(db, "add", "250", "--category", "Groceries", "--date", "2026-10-03")
|
||
|
|
assert run(db, "report", "--month", "2026-10") == 0
|
||
|
|
out = capsys.readouterr().out
|
||
|
|
assert "report for 2026-10" in out
|
||
|
|
assert "income" in out and "$3,000.00" in out
|
||
|
|
assert "expenses" in out and "$1,450.00" in out
|
||
|
|
assert "net" in out and "$1,550.00" in out
|
||
|
|
assert "Salary" in out and "Groceries" in out
|
||
|
|
|
||
|
|
|
||
|
|
def test_report_empty(db, capsys):
|
||
|
|
assert run(db, "report", "--month", "2001-01") == 0
|
||
|
|
assert "no transactions" in capsys.readouterr().out
|
||
|
|
|
||
|
|
|
||
|
|
def test_report_default_month(db, capsys):
|
||
|
|
assert run(db, "report") == 0
|
||
|
|
assert "report for" in capsys.readouterr().out
|
||
|
|
|
||
|
|
|
||
|
|
def test_invalid_month(db):
|
||
|
|
with pytest.raises(SystemExit) as exc:
|
||
|
|
run(db, "report", "--month", "2026-99")
|
||
|
|
assert exc.value.code == 2
|