121 lines
3 KiB
Python
121 lines
3 KiB
Python
"""Tests for ledgerly.models (sanitizers, Month, read models)."""
|
|
|
|
from dataclasses import FrozenInstanceError
|
|
from datetime import date
|
|
|
|
import pytest
|
|
|
|
from ledgerly.errors import ValidationError
|
|
from ledgerly.models import (
|
|
BudgetLine,
|
|
Category,
|
|
Month,
|
|
MonthSummary,
|
|
clean_name,
|
|
clean_note,
|
|
parse_date,
|
|
validate_kind,
|
|
)
|
|
from ledgerly.money import Money
|
|
|
|
|
|
def test_clean_name_ok():
|
|
assert clean_name(" Rent ") == "Rent"
|
|
|
|
|
|
def test_clean_name_rejects_blank():
|
|
with pytest.raises(ValidationError):
|
|
clean_name(" ")
|
|
with pytest.raises(ValidationError):
|
|
clean_name(None)
|
|
|
|
|
|
def test_clean_name_rejects_long():
|
|
with pytest.raises(ValidationError, match="50"):
|
|
clean_name("x" * 51)
|
|
|
|
|
|
def test_clean_note():
|
|
assert clean_note(None) is None
|
|
assert clean_note(" ") is None
|
|
assert clean_note("a note") == "a note"
|
|
|
|
|
|
def test_clean_note_rejects_long():
|
|
with pytest.raises(ValidationError, match="200"):
|
|
clean_note("n" * 201)
|
|
|
|
|
|
def test_parse_date_ok():
|
|
assert parse_date("2026-10-26") == date(2026, 10, 26)
|
|
|
|
|
|
def test_parse_date_rejects_bad():
|
|
for bad in ("2026-13-01", "2026-02-30", "25-10-01", "not-a-date", 123):
|
|
with pytest.raises(ValidationError):
|
|
parse_date(bad)
|
|
|
|
|
|
def test_parse_date_rejects_out_of_range():
|
|
with pytest.raises(ValidationError):
|
|
parse_date("1999-12-31")
|
|
with pytest.raises(ValidationError):
|
|
parse_date("2101-01-01")
|
|
|
|
|
|
def test_validate_kind():
|
|
assert validate_kind("income") == "income"
|
|
with pytest.raises(ValidationError):
|
|
validate_kind("asset")
|
|
|
|
|
|
def test_month_from_str_ok():
|
|
month = Month.from_str("2026-10")
|
|
assert (month.year, month.month) == (2026, 10)
|
|
assert month.label == "2026-10"
|
|
assert month.first_day == date(2026, 10, 1)
|
|
assert month.last_day == date(2026, 10, 31)
|
|
|
|
|
|
def test_month_from_str_rejects_bad():
|
|
for bad in ("2026", "26-10", "2026-13", "2026-", "2026-1-3", "garbage", 2026):
|
|
with pytest.raises(ValidationError):
|
|
Month.from_str(bad)
|
|
|
|
|
|
def test_month_constructor_validation():
|
|
with pytest.raises(ValidationError):
|
|
Month(2026, 13)
|
|
with pytest.raises(ValidationError):
|
|
Month(1999, 5)
|
|
|
|
|
|
def test_month_range_leap_year():
|
|
month = Month(year=2024, month=2)
|
|
assert month.range == ("2024-02-01", "2024-02-29")
|
|
|
|
|
|
def test_month_now_is_sane():
|
|
month = Month.now()
|
|
assert 1 <= month.month <= 12
|
|
|
|
|
|
def test_month_summary_net():
|
|
summary = MonthSummary(month=Month.now(), income=Money(3000), expenses=Money(3100))
|
|
assert summary.net == Money(-100)
|
|
|
|
|
|
def test_budget_line_over_and_under():
|
|
over = BudgetLine(category="Rent", budget=Money(1000), spent=Money(1200))
|
|
assert over.is_over
|
|
assert over.remaining == Money(-200)
|
|
under = BudgetLine(category="Rent", budget=Money(1000), spent=Money(800))
|
|
assert not under.is_over
|
|
assert under.remaining == Money(200)
|
|
|
|
|
|
def test_entities_are_frozen():
|
|
cat = Category(name="Rent", kind="expense")
|
|
with pytest.raises(FrozenInstanceError):
|
|
cat.name = "Other"
|