From ed17d27457fc5355c7f1563b2dcf2a4ae010e64f Mon Sep 17 00:00:00 2001 From: JKuijperM Date: Fri, 24 Jul 2026 09:32:11 +0200 Subject: [PATCH] Updated the tests for the project --- expenses_manager/expenses/tests/conftest.py | 31 +++ .../expenses/tests/test_accounts.py | 86 ++++++++ .../expenses/tests/test_categories.py | 101 +++++++++ .../expenses/tests/test_dashboard.py | 81 +++----- .../expenses/tests/test_deletes.py | 61 ++++++ .../expenses/tests/test_expense_list.py | 42 ++++ .../expenses/tests/test_expenses.py | 28 +-- expenses_manager/expenses/tests/test_fuel.py | 69 ++++++ expenses_manager/expenses/tests/test_goals.py | 196 ++++++++++++++++++ .../expenses/tests/test_income.py | 16 +- 10 files changed, 627 insertions(+), 84 deletions(-) create mode 100644 expenses_manager/expenses/tests/conftest.py create mode 100644 expenses_manager/expenses/tests/test_accounts.py create mode 100644 expenses_manager/expenses/tests/test_categories.py create mode 100644 expenses_manager/expenses/tests/test_deletes.py create mode 100644 expenses_manager/expenses/tests/test_expense_list.py create mode 100644 expenses_manager/expenses/tests/test_fuel.py create mode 100644 expenses_manager/expenses/tests/test_goals.py diff --git a/expenses_manager/expenses/tests/conftest.py b/expenses_manager/expenses/tests/conftest.py new file mode 100644 index 0000000..c2a1eff --- /dev/null +++ b/expenses_manager/expenses/tests/conftest.py @@ -0,0 +1,31 @@ +import pytest +from decimal import Decimal +from expenses.models import Account, Category + + +@pytest.fixture +def user(db, django_user_model): + return django_user_model.objects.create_user( + username="tester", password="testpass123" + ) + + +@pytest.fixture +def auth_client(client, user): + client.login(username=user.username, password="testpass123") + return client + + +@pytest.fixture +def account(user): + return Account.objects.create( + owner=user, + name="Cuenta principal", + initial_balance=Decimal("0"), + active=True, + ) + + +@pytest.fixture +def category(user): + return Category.objects.create(owner=user, name="General") diff --git a/expenses_manager/expenses/tests/test_accounts.py b/expenses_manager/expenses/tests/test_accounts.py new file mode 100644 index 0000000..d025ad6 --- /dev/null +++ b/expenses_manager/expenses/tests/test_accounts.py @@ -0,0 +1,86 @@ +import pytest +from datetime import date +from decimal import Decimal +from expenses.models import Account, Expense, Income, Category + +pytestmark = pytest.mark.django_db + + +def test_current_balance_reflects_income_and_expenses(user, category): + acc = Account.objects.create(owner=user, name="Cuenta", initial_balance=Decimal("100")) + Income.objects.create(owner=user, account=acc, name="Nomina", amount=Decimal("50"), date=date.today()) + Expense.objects.create(owner=user, account=acc, category=category, amount=Decimal("30"), date=date.today()) + + assert acc.current_balance() == Decimal("120") + + +def test_monthly_balance_returns_12_rows_in_order(user): + acc = Account.objects.create(owner=user, name="Cuenta", initial_balance=Decimal("100")) + + data = acc.monthly_balance(2020) + + assert len(data) == 12 + assert [row["month"] for row in data] == list(range(1, 13)) + assert all(row["balance"] == 100.0 for row in data) + + +def test_monthly_balance_is_cumulative_across_months(user, category): + acc = Account.objects.create(owner=user, name="Cuenta", initial_balance=Decimal("0")) + year = date.today().year - 1 # avoid the current-month live-balance patch + + Expense.objects.create(owner=user, account=acc, category=category, amount=Decimal("40"), date=date(year, 3, 1)) + Income.objects.create(owner=user, account=acc, name="Extra", amount=Decimal("100"), date=date(year, 6, 1)) + + data = acc.monthly_balance(year) + + assert data[2]["balance"] == -40.0 # month 3 + assert data[5]["balance"] == 60.0 # month 6, cumulative + assert data[11]["balance"] == 60.0 # carries forward to December + + +def test_monthly_balance_respects_year_param_and_excludes_other_years(user, category): + acc = Account.objects.create(owner=user, name="Cuenta", initial_balance=Decimal("0")) + + Expense.objects.create(owner=user, account=acc, category=category, amount=Decimal("50"), date=date(2023, 6, 15)) + Income.objects.create(owner=user, account=acc, name="Extra", amount=Decimal("30"), date=date(2024, 3, 10)) + + data_2023 = acc.monthly_balance(2023) + assert data_2023[5]["balance"] == -50.0 # month 6 + assert data_2023[11]["balance"] == -50.0 + + data_2024 = acc.monthly_balance(2024) + assert data_2024[1]["balance"] == -50.0 # month 2: prior year's expense folded into start + assert data_2024[2]["balance"] == -20.0 # month 3: 2024 income applied + + +def test_monthly_balance_current_month_patched_with_live_balance(user, category): + today = date.today() + acc = Account.objects.create(owner=user, name="Cuenta", initial_balance=Decimal("100")) + Expense.objects.create(owner=user, account=acc, category=category, amount=Decimal("30"), date=today) + + data = acc.monthly_balance(today.year) + + assert data[today.month - 1]["balance"] == float(acc.current_balance()) + + +def test_monthly_net_returns_income_minus_expense_per_month(user, category): + acc = Account.objects.create(owner=user, name="Cuenta", initial_balance=Decimal("0")) + + Income.objects.create(owner=user, account=acc, name="Nomina", amount=Decimal("100"), date=date(2024, 4, 5)) + Expense.objects.create(owner=user, account=acc, category=category, amount=Decimal("40"), date=date(2024, 4, 20)) + + data = acc.monthly_net(2024) + + assert len(data) == 12 + assert data[3]["net"] == 60.0 # month 4 + assert data[0]["net"] == 0.0 + + +def test_balance_until_only_counts_entries_on_or_before_date(user, category): + acc = Account.objects.create(owner=user, name="Cuenta", initial_balance=Decimal("0")) + + Expense.objects.create(owner=user, account=acc, category=category, amount=Decimal("20"), date=date(2024, 1, 10)) + Expense.objects.create(owner=user, account=acc, category=category, amount=Decimal("5"), date=date(2024, 1, 15)) + Income.objects.create(owner=user, account=acc, name="Later", amount=Decimal("50"), date=date(2024, 1, 20)) + + assert acc.balance_until(date(2024, 1, 15)) == Decimal("-25") diff --git a/expenses_manager/expenses/tests/test_categories.py b/expenses_manager/expenses/tests/test_categories.py new file mode 100644 index 0000000..a8622b0 --- /dev/null +++ b/expenses_manager/expenses/tests/test_categories.py @@ -0,0 +1,101 @@ +import pytest +from datetime import date +from decimal import Decimal +from django.contrib.messages import get_messages +from django.urls import reverse +from django.utils.text import slugify +from expenses.models import Category, Expense +from expenses.forms import CategoryForm + +pytestmark = pytest.mark.django_db + + +def test_slug_auto_generated_from_name_on_save(user): + cat = Category.objects.create(name="Ropa y Calzado", owner=user) + + assert cat.slug == slugify("Ropa y Calzado") + + +def test_slug_not_overwritten_if_explicitly_set(user): + cat = Category(name="Comida", slug="mi-slug-personalizado", owner=user) + cat.save() + + assert cat.slug == "mi-slug-personalizado" + + +def test_descendant_ids_includes_grandchildren(user): + parent = Category.objects.create(name="A", owner=user) + child = Category.objects.create(name="B", owner=user, parent=parent) + grandchild = Category.objects.create(name="C", owner=user, parent=child) + + ids = set(parent.descendant_ids()) + + assert ids == {parent.pk, child.pk, grandchild.pk} + + +def test_descendant_ids_can_exclude_self(user): + parent = Category.objects.create(name="A", owner=user) + child = Category.objects.create(name="B", owner=user, parent=parent) + + ids = parent.descendant_ids(include_self=False) + + assert parent.pk not in ids + assert child.pk in ids + + +def test_categoryform_parent_persists_on_save(auth_client, user): + other = Category.objects.create(name="Otra", owner=user) + + auth_client.post(reverse('category_list'), {'name': 'Nueva', 'parent': str(other.pk)}) + + created = Category.objects.get(name='Nueva', owner=user) + assert created.parent_id == other.pk + + +def test_categoryform_excludes_self_and_descendants_from_parent_queryset_when_editing(user): + parent = Category.objects.create(name="P", owner=user) + child = Category.objects.create(name="C", owner=user, parent=parent) + + form = CategoryForm(instance=parent, user=user) + queryset_ids = set(form.fields['parent'].queryset.values_list('pk', flat=True)) + + assert parent.pk not in queryset_ids + assert child.pk not in queryset_ids + + +def test_categoryform_clean_parent_rejects_self_as_parent(user): + cat = Category.objects.create(name="X", owner=user) + + form = CategoryForm(data={'name': 'X', 'parent': str(cat.pk)}, instance=cat, user=user) + + assert not form.is_valid() + assert 'parent' in form.errors + + +def test_categoryform_rejects_descendant_as_parent(user): + parent = Category.objects.create(name="P", owner=user) + child = Category.objects.create(name="C", owner=user, parent=parent) + + form = CategoryForm(data={'name': 'P', 'parent': str(child.pk)}, instance=parent, user=user) + + assert not form.is_valid() + + +def test_category_delete_with_expenses_is_protected(auth_client, user, account, category): + Expense.objects.create(owner=user, account=account, category=category, amount=Decimal("10"), date=date.today()) + + response = auth_client.post(reverse('category_delete', args=[category.pk])) + + assert response.status_code == 302 + assert Category.objects.filter(pk=category.pk).exists() + messages = [str(m) for m in get_messages(response.wsgi_request)] + assert any("gastos asociados" in m for m in messages) + + +def test_category_delete_without_expenses_succeeds(auth_client, user): + cat = Category.objects.create(name="Vacia", owner=user) + + response = auth_client.post(reverse('category_delete', args=[cat.pk])) + + assert response.status_code == 302 + assert not Category.objects.filter(pk=cat.pk).exists() diff --git a/expenses_manager/expenses/tests/test_dashboard.py b/expenses_manager/expenses/tests/test_dashboard.py index 2d19d89..3e0d2b7 100644 --- a/expenses_manager/expenses/tests/test_dashboard.py +++ b/expenses_manager/expenses/tests/test_dashboard.py @@ -2,7 +2,7 @@ import pytest from datetime import date from decimal import Decimal from django.urls import reverse -from expenses.models import Account, Expense, Category +from expenses.models import Expense, Category pytestmark = pytest.mark.django_db @@ -10,96 +10,73 @@ def test_dashboard_requires_login(client): url = reverse('dashboard') response = client.get(url) assert response.status_code == 302 - -def test_dashboard_logged_user_can_access(client, django_user_model): - user = django_user_model.objects.create_user( - username='test', - password='1234' - ) - client.login(username='test', password='1234') - + +def test_dashboard_logged_user_can_access(auth_client): url = reverse('dashboard') - response = client.get(url) - + response = auth_client.get(url) + assert response.status_code == 200 - -def test_dashboard_groups_by_category(client, django_user_model): - user = django_user_model.objects.create_user( - username='test', - password='1234' - ) - client.login(username='test', password='1234') - + +def test_dashboard_groups_by_category(auth_client, user, account): food = Category.objects.create(name='Food', owner=user) rent = Category.objects.create(name='Rent', owner=user) - - general_account = Account.objects.create(name='General', owner=user, initial_balance=300, active=True) - + Expense.objects.create( owner=user, category=food, amount=Decimal('10'), date=date(2024, 1, 1), - account=general_account, + account=account, ) Expense.objects.create( owner=user, category=food, amount=Decimal('5'), date=date(2024, 1, 2), - account=general_account, + account=account, ) Expense.objects.create( owner=user, category=rent, amount=Decimal('20'), date=date(2024, 1, 3), - account=general_account, + account=account, ) - - response = client.get( + + response = auth_client.get( reverse('dashboard'), {'year': 2024} ) - + data = list(response.context['by_category']) - + assert {'category__name': 'Food', 'total': Decimal('15')} in data assert {'category__name': 'Rent', 'total': Decimal('20')} in data -def test_dashboard_filters_by_year(client, django_user_model): - user = django_user_model.objects.create_user( - username='test', - password='1234' - ) - client.login(username='test', password='1234') - - cat = Category.objects.create(name='General', owner=user) - general_account = Account.objects.create(name='General', owner=user, initial_balance=300, active=True) - +def test_dashboard_filters_by_year(auth_client, user, account, category): Expense.objects.create( owner=user, - category=cat, + category=category, amount=10, date=date(2023, 5, 1), - account=general_account, + account=account, ) - + Expense.objects.create( owner=user, - category=cat, + category=category, amount=20, date=date(2024, 5, 1), - account=general_account, + account=account, ) - - response = client.get( + + response = auth_client.get( reverse('dashboard'), {'year': 2024}, ) - - by_month = list(response.context['by_month']) - - totals = [row['total'] for row in by_month] - assert Decimal('20') in totals - assert Decimal('10') not in totals \ No newline at end of file + + chart_data = response.context['chart_data'] + + assert len(chart_data) == 12 + assert chart_data[4] == 20.0 # May = month 5 -> index 4 + assert sum(chart_data) == 20.0 # only the 2024 expense contributes diff --git a/expenses_manager/expenses/tests/test_deletes.py b/expenses_manager/expenses/tests/test_deletes.py new file mode 100644 index 0000000..b4d8cf6 --- /dev/null +++ b/expenses_manager/expenses/tests/test_deletes.py @@ -0,0 +1,61 @@ +import pytest +from datetime import date +from decimal import Decimal +from django.urls import reverse +from expenses.models import Goal, Tag, Income + +pytestmark = pytest.mark.django_db + + +def test_goal_delete_get_does_not_delete(auth_client, user): + goal = Goal.objects.create(owner=user, name="Meta", target_amount=Decimal("100")) + + response = auth_client.get(reverse('goal_delete', args=[goal.pk])) + + assert response.status_code == 200 + assert Goal.objects.filter(pk=goal.pk).exists() + + +def test_goal_delete_post_deletes(auth_client, user): + goal = Goal.objects.create(owner=user, name="Meta", target_amount=Decimal("100")) + + response = auth_client.post(reverse('goal_delete', args=[goal.pk])) + + assert response.status_code == 302 + assert not Goal.objects.filter(pk=goal.pk).exists() + + +def test_tag_delete_get_does_not_delete(auth_client, user): + tag = Tag.objects.create(name="Food", owner=user) + + response = auth_client.get(reverse('tag_delete', args=[tag.pk])) + + assert response.status_code == 200 + assert Tag.objects.filter(pk=tag.pk).exists() + + +def test_tag_delete_post_deletes(auth_client, user): + tag = Tag.objects.create(name="Food", owner=user) + + response = auth_client.post(reverse('tag_delete', args=[tag.pk])) + + assert response.status_code == 302 + assert not Tag.objects.filter(pk=tag.pk).exists() + + +def test_income_delete_get_does_not_delete(auth_client, user, account): + income = Income.objects.create(owner=user, account=account, name="Nomina", amount=Decimal("500"), date=date.today()) + + response = auth_client.get(reverse('income_delete', args=[income.pk])) + + assert response.status_code == 200 + assert Income.objects.filter(pk=income.pk).exists() + + +def test_income_delete_post_deletes(auth_client, user, account): + income = Income.objects.create(owner=user, account=account, name="Nomina", amount=Decimal("500"), date=date.today()) + + response = auth_client.post(reverse('income_delete', args=[income.pk])) + + assert response.status_code == 302 + assert not Income.objects.filter(pk=income.pk).exists() diff --git a/expenses_manager/expenses/tests/test_expense_list.py b/expenses_manager/expenses/tests/test_expense_list.py new file mode 100644 index 0000000..4023a42 --- /dev/null +++ b/expenses_manager/expenses/tests/test_expense_list.py @@ -0,0 +1,42 @@ +import pytest +from datetime import date +from decimal import Decimal +from django.urls import reverse +from expenses.models import Expense, Tag + +pytestmark = pytest.mark.django_db + + +def test_tag_filter_with_non_numeric_value_returns_200(auth_client, user, account, category): + Expense.objects.create(owner=user, account=account, category=category, amount=Decimal("10"), date=date.today()) + + response = auth_client.get(reverse('expense_list'), {'tag': 'abc'}) + + assert response.status_code == 200 + assert len(response.context['page_obj']) == 1 + + +def test_tag_filter_with_valid_tag_returns_only_matching_expenses(auth_client, user, account, category): + tag = Tag.objects.create(name="Food", owner=user) + tagged = Expense.objects.create(owner=user, account=account, category=category, amount=Decimal("10"), date=date.today()) + tagged.tags.add(tag) + Expense.objects.create(owner=user, account=account, category=category, amount=Decimal("20"), date=date.today()) + + response = auth_client.get(reverse('expense_list'), {'tag': str(tag.pk)}) + + results = list(response.context['page_obj']) + assert results == [tagged] + + +def test_tag_filter_multiple_tags_are_ORed(auth_client, user, account, category): + tag1 = Tag.objects.create(name="Food", owner=user) + tag2 = Tag.objects.create(name="Transport", owner=user) + expense1 = Expense.objects.create(owner=user, account=account, category=category, amount=Decimal("10"), date=date.today()) + expense1.tags.add(tag1) + expense2 = Expense.objects.create(owner=user, account=account, category=category, amount=Decimal("20"), date=date.today()) + expense2.tags.add(tag2) + + response = auth_client.get(reverse('expense_list'), {'tag': [str(tag1.pk), str(tag2.pk)]}) + + results = set(response.context['page_obj']) + assert results == {expense1, expense2} diff --git a/expenses_manager/expenses/tests/test_expenses.py b/expenses_manager/expenses/tests/test_expenses.py index 9109c14..5f180c1 100644 --- a/expenses_manager/expenses/tests/test_expenses.py +++ b/expenses_manager/expenses/tests/test_expenses.py @@ -1,41 +1,29 @@ import pytest from datetime import date -from decimal import Decimal from django.urls import reverse -from expenses.models import Expense, Category, Tag, Account +from expenses.models import Expense, Tag pytestmark = pytest.mark.django_db -def test_expense_can_have_tags(client, django_user_model): - user = django_user_model.objects.create_user(username='test', password='1234') - client.login(username='test', password='1234') - +def test_expense_can_have_tags(user, category, account): tag = Tag.objects.create(name='Food', owner=user) - category = Category.objects.create(name='General', owner=user) - - general_account = Account.objects.create(name='General', owner=user, initial_balance=300, active=True) - + expense = Expense.objects.create( owner=user, category=category, amount=10, date=date.today(), - account=general_account, + account=account, ) expense.tags.add(tag) - + assert expense.tags.count() == 1 assert tag in expense.tags.all() -def test_user_can_create_tag(client, django_user_model): - user = django_user_model.objects.create_user( - username='test', password='1234' - ) - client.login(username='test', password='1234') - - response = client.post( +def test_user_can_create_tag(auth_client, user): + response = auth_client.post( reverse('tag_create'), {'name': 'Food'} ) - + assert Tag.objects.filter(owner=user, name='Food').exists() diff --git a/expenses_manager/expenses/tests/test_fuel.py b/expenses_manager/expenses/tests/test_fuel.py new file mode 100644 index 0000000..72be9a4 --- /dev/null +++ b/expenses_manager/expenses/tests/test_fuel.py @@ -0,0 +1,69 @@ +import pytest +from datetime import date +from decimal import Decimal +from django.urls import reverse +from expenses.models import Expense, FuelEntry + +pytestmark = pytest.mark.django_db + + +def _make_fuel_entry(user, account, category, odometer, liters, amount=Decimal("50")): + expense = Expense.objects.create( + owner=user, account=account, category=category, amount=amount, date=date.today() + ) + return FuelEntry.objects.create(expense=expense, odometer=odometer, liters=liters) + + +def test_price_per_liter_divides_amount_by_liters(user, account, category): + entry = _make_fuel_entry(user, account, category, odometer=1000, liters=Decimal("25"), amount=Decimal("50")) + + assert entry.price_per_liter() == Decimal("2") + + +def test_price_per_liter_zero_when_no_liters(user, account, category): + entry = _make_fuel_entry(user, account, category, odometer=1000, liters=Decimal("0")) + + assert entry.price_per_liter() == 0 + + +def test_km_since_previous_none_on_first_fillup(user, account, category): + entry = _make_fuel_entry(user, account, category, odometer=1000, liters=Decimal("20")) + + assert entry.km_since_previous() is None + + +def test_km_since_previous_computes_delta_from_prior_entry(user, account, category): + _make_fuel_entry(user, account, category, odometer=1000, liters=Decimal("20")) + second = _make_fuel_entry(user, account, category, odometer=1300, liters=Decimal("24")) + + assert second.km_since_previous() == 300 + + +def test_consumption_none_when_no_previous_entry(user, account, category): + entry = _make_fuel_entry(user, account, category, odometer=1000, liters=Decimal("20")) + + assert entry.consumption() is None + + +def test_consumption_computes_liters_per_100km(user, account, category): + _make_fuel_entry(user, account, category, odometer=1000, liters=Decimal("20")) + second = _make_fuel_entry(user, account, category, odometer=1300, liters=Decimal("24")) + + assert second.consumption() == Decimal("8") + + +def test_fuel_create_invalid_post_returns_200_not_500(auth_client): + response = auth_client.post(reverse('fuel_create'), {}) + + assert response.status_code == 200 + assert FuelEntry.objects.count() == 0 + + +def test_fuel_delete_post_cascades_to_fuelentry(auth_client, user, account, category): + entry = _make_fuel_entry(user, account, category, odometer=1000, liters=Decimal("20")) + expense = entry.expense + + response = auth_client.post(reverse('fuel_delete', args=[expense.pk])) + + assert not Expense.objects.filter(pk=expense.pk).exists() + assert not FuelEntry.objects.filter(pk=entry.pk).exists() diff --git a/expenses_manager/expenses/tests/test_goals.py b/expenses_manager/expenses/tests/test_goals.py new file mode 100644 index 0000000..2411c8e --- /dev/null +++ b/expenses_manager/expenses/tests/test_goals.py @@ -0,0 +1,196 @@ +import pytest +from datetime import date +from decimal import Decimal +from expenses.models import Account, Category, Expense, Goal +from expenses.forms import GoalForm + +pytestmark = pytest.mark.django_db + + +def _last_month_date(): + today = date.today() + if today.month == 1: + return date(today.year - 1, 12, 15) + return date(today.year, today.month - 1, 15) + + +def test_payment_goal_sums_expenses_since_start_date(user, account, category): + start = date(2024, 6, 1) + Expense.objects.create(owner=user, account=account, category=category, amount=Decimal("10"), date=date(2024, 5, 1)) + Expense.objects.create(owner=user, account=account, category=category, amount=Decimal("20"), date=date(2024, 6, 15)) + + goal = Goal.objects.create( + owner=user, name="Pago", kind=Goal.KIND_PAYMENT, category=category, + include_subcategories=False, start_date=start, target_amount=Decimal("100"), + ) + + assert goal.progress() == Decimal("20") + + +def test_payment_goal_include_subcategories_true_includes_child_expenses(user, account, category): + child = Category.objects.create(owner=user, name="Sub", parent=category) + Expense.objects.create(owner=user, account=account, category=child, amount=Decimal("15"), date=date.today()) + + goal = Goal.objects.create( + owner=user, name="Pago", kind=Goal.KIND_PAYMENT, category=category, + include_subcategories=True, target_amount=Decimal("100"), + ) + + assert goal.progress() == Decimal("15") + + +def test_payment_goal_include_subcategories_false_excludes_child_expenses(user, account, category): + child = Category.objects.create(owner=user, name="Sub", parent=category) + Expense.objects.create(owner=user, account=account, category=child, amount=Decimal("15"), date=date.today()) + + goal = Goal.objects.create( + owner=user, name="Pago", kind=Goal.KIND_PAYMENT, category=category, + include_subcategories=False, target_amount=Decimal("100"), + ) + + assert goal.progress() == Decimal("0") + + +def test_budget_goal_period_month_counts_only_current_month(user, account, category): + Expense.objects.create(owner=user, account=account, category=category, amount=Decimal("30"), date=date.today()) + Expense.objects.create(owner=user, account=account, category=category, amount=Decimal("999"), date=_last_month_date()) + + goal = Goal.objects.create( + owner=user, name="Presupuesto", kind=Goal.KIND_BUDGET, category=category, + period=Goal.PERIOD_MONTH, target_amount=Decimal("100"), + ) + + assert goal.progress() == Decimal("30") + + +def test_budget_goal_is_exceeded_true_over_target(user, account, category): + Expense.objects.create(owner=user, account=account, category=category, amount=Decimal("150"), date=date.today()) + + goal = Goal.objects.create( + owner=user, name="Presupuesto", kind=Goal.KIND_BUDGET, category=category, + period=Goal.PERIOD_MONTH, target_amount=Decimal("100"), + ) + + assert goal.is_exceeded() is True + + +def test_budget_goal_is_exceeded_false_under_target(user, account, category): + Expense.objects.create(owner=user, account=account, category=category, amount=Decimal("50"), date=date.today()) + + goal = Goal.objects.create( + owner=user, name="Presupuesto", kind=Goal.KIND_BUDGET, category=category, + period=Goal.PERIOD_MONTH, target_amount=Decimal("100"), + ) + + assert goal.is_exceeded() is False + + +def test_is_exceeded_always_false_for_non_budget_kind(user, account, category): + Expense.objects.create(owner=user, account=account, category=category, amount=Decimal("50"), date=date.today()) + + goal = Goal.objects.create( + owner=user, name="Pago", kind=Goal.KIND_PAYMENT, category=category, target_amount=Decimal("10"), + ) + + assert goal.progress() > goal.target_amount + assert goal.is_exceeded() is False + + +def test_saving_goal_progress_equals_account_balance(user): + acc = Account.objects.create(owner=user, name="Ahorros", initial_balance=Decimal("500")) + + goal = Goal.objects.create( + owner=user, name="Ahorro", kind=Goal.KIND_SAVING, account=acc, target_amount=Decimal("1000"), + ) + + assert goal.progress() == acc.current_balance() + + +def test_bar_width_caps_at_100_above_target(user): + acc = Account.objects.create(owner=user, name="Ahorros", initial_balance=Decimal("250")) + goal = Goal.objects.create(owner=user, name="Ahorro", kind=Goal.KIND_SAVING, account=acc, target_amount=Decimal("100")) + + assert goal.percentage() == Decimal("250") + assert goal.bar_width() == 100.0 + + +def test_bar_width_uncapped_below_100(user): + acc = Account.objects.create(owner=user, name="Ahorros", initial_balance=Decimal("40")) + goal = Goal.objects.create(owner=user, name="Ahorro", kind=Goal.KIND_SAVING, account=acc, target_amount=Decimal("100")) + + assert goal.bar_width() == 40.0 + + +@pytest.mark.parametrize("amount,expected_state", [ + (Decimal("50"), "ok"), + (Decimal("90"), "warning"), + (Decimal("150"), "danger"), +]) +def test_progress_state_budget_thresholds(user, account, category, amount, expected_state): + Expense.objects.create(owner=user, account=account, category=category, amount=amount, date=date.today()) + goal = Goal.objects.create( + owner=user, name="Presupuesto", kind=Goal.KIND_BUDGET, category=category, target_amount=Decimal("100"), + ) + + assert goal.progress_state() == expected_state + + +@pytest.mark.parametrize("amount,expected_state", [ + (Decimal("50"), "ok"), + (Decimal("100"), "complete"), + (Decimal("150"), "complete"), +]) +def test_progress_state_non_budget_thresholds(user, account, category, amount, expected_state): + Expense.objects.create(owner=user, account=account, category=category, amount=amount, date=date.today()) + goal = Goal.objects.create( + owner=user, name="Pago", kind=Goal.KIND_PAYMENT, category=category, target_amount=Decimal("100"), + ) + + assert goal.progress_state() == expected_state + + +def test_goalform_budget_requires_period(user, category): + data = {"name": "Presupuesto", "kind": Goal.KIND_BUDGET, "target_amount": "100", "category": str(category.pk), "period": Goal.PERIOD_NONE} + form = GoalForm(data=data, user=user) + + assert not form.is_valid() + assert "period" in form.errors + + +def test_goalform_saving_requires_account(user): + data = {"name": "Ahorro", "kind": Goal.KIND_SAVING, "target_amount": "500", "period": Goal.PERIOD_NONE} + form = GoalForm(data=data, user=user) + + assert not form.is_valid() + assert "account" in form.errors + + +def test_goalform_payment_requires_category(user): + data = {"name": "Pago", "kind": Goal.KIND_PAYMENT, "target_amount": "50", "period": Goal.PERIOD_NONE} + form = GoalForm(data=data, user=user) + + assert not form.is_valid() + assert "category" in form.errors + + +def test_goalform_budget_requires_category(user): + data = {"name": "Presupuesto", "kind": Goal.KIND_BUDGET, "target_amount": "50", "period": Goal.PERIOD_MONTH} + form = GoalForm(data=data, user=user) + + assert not form.is_valid() + assert "category" in form.errors + + +def test_goalform_valid_payment_saves_goal(user, category): + data = { + "name": "Ahorro viaje", "kind": Goal.KIND_PAYMENT, "target_amount": "200", + "category": str(category.pk), "period": Goal.PERIOD_NONE, + } + form = GoalForm(data=data, user=user) + + assert form.is_valid(), form.errors + goal = form.save(commit=False) + goal.owner = user + goal.save() + + assert Goal.objects.filter(pk=goal.pk, name="Ahorro viaje", kind=Goal.KIND_PAYMENT, category=category).exists() diff --git a/expenses_manager/expenses/tests/test_income.py b/expenses_manager/expenses/tests/test_income.py index c99f85e..e0e351d 100644 --- a/expenses_manager/expenses/tests/test_income.py +++ b/expenses_manager/expenses/tests/test_income.py @@ -1,20 +1,12 @@ import pytest from datetime import date -from decimal import Decimal -from django.urls import reverse from expenses.models import Income, Account pytestmark = pytest.mark.django_db -def test_income_increases_account_balance(client, django_user_model): - user = django_user_model.objects.create_user( - username='test', - password='1234' - ) - client.login(username='test', password='1234') - +def test_income_increases_account_balance(user): general_account = Account.objects.create(name='General', owner=user, initial_balance=1000, active=True) - + Income.objects.create( owner=user, account=general_account, @@ -22,5 +14,5 @@ def test_income_increases_account_balance(client, django_user_model): name='Nomina enero', date=date.today(), ) - - assert general_account.current_balance() == 1500 \ No newline at end of file + + assert general_account.current_balance() == 1500