Add tests for goal links and collection goals
This commit is contained in:
parent
a5f1c7b590
commit
c50d416560
749
expenses_manager/expenses/tests/test_goal_links.py
Normal file
749
expenses_manager/expenses/tests/test_goal_links.py
Normal file
@ -0,0 +1,749 @@
|
||||
"""Vinculación de movimientos a objetivos y el tipo "cobro pendiente".
|
||||
|
||||
El test central es el del doble conteo: con la selección por categoría y la
|
||||
vinculación a mano conviviendo, un gasto que esté en las dos tiene que contar
|
||||
una sola vez. No da error si se cuenta dos veces, da un número mal, que es peor.
|
||||
"""
|
||||
|
||||
from datetime import date
|
||||
from decimal import Decimal
|
||||
|
||||
import pytest
|
||||
from django.db import connection
|
||||
from django.test.utils import CaptureQueriesContext
|
||||
from django.urls import reverse
|
||||
|
||||
from expenses.forms import ExpenseForm, GoalForm, IncomeForm
|
||||
from expenses.models import Account, Category, Expense, Goal, Income, Transfer
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def savings(user):
|
||||
return Account.objects.create(
|
||||
owner=user, name="Ahorro", initial_balance=Decimal("0"), active=True
|
||||
)
|
||||
|
||||
|
||||
def make_payment_goal(user, category=None, target="1000", start=date(2024, 1, 1)):
|
||||
return Goal.objects.create(
|
||||
owner=user,
|
||||
name="Pago",
|
||||
kind=Goal.KIND_PAYMENT,
|
||||
category=category,
|
||||
include_subcategories=False,
|
||||
start_date=start,
|
||||
target_amount=Decimal(target),
|
||||
)
|
||||
|
||||
|
||||
def make_budget_goal(user, category, target="100", period=Goal.PERIOD_MONTH):
|
||||
return Goal.objects.create(
|
||||
owner=user,
|
||||
name="Presupuesto",
|
||||
kind=Goal.KIND_BUDGET,
|
||||
category=category,
|
||||
include_subcategories=False,
|
||||
period=period,
|
||||
target_amount=Decimal(target),
|
||||
)
|
||||
|
||||
|
||||
def make_collection_goal(user, target="1000", start=date(2024, 1, 1)):
|
||||
return Goal.objects.create(
|
||||
owner=user,
|
||||
name="Cobro",
|
||||
kind=Goal.KIND_COLLECTION,
|
||||
start_date=start,
|
||||
target_amount=Decimal(target),
|
||||
)
|
||||
|
||||
|
||||
def make_expense(user, account, category, amount="10", on=date(2024, 5, 11)):
|
||||
return Expense.objects.create(
|
||||
owner=user,
|
||||
account=account,
|
||||
category=category,
|
||||
amount=Decimal(amount),
|
||||
date=on,
|
||||
)
|
||||
|
||||
|
||||
def make_income(user, account, amount="10", on=date(2024, 5, 11), name="Devolución"):
|
||||
return Income.objects.create(
|
||||
owner=user, account=account, name=name, amount=Decimal(amount), date=on
|
||||
)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# El doble conteo: el test central de la fase
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_an_expense_in_the_category_and_linked_by_hand_counts_once(
|
||||
user, account, category
|
||||
):
|
||||
expense = make_expense(user, account, category, amount="100")
|
||||
goal = make_payment_goal(user, category=category)
|
||||
|
||||
goal.linked_expenses.add(expense)
|
||||
|
||||
assert goal.progress == Decimal("100")
|
||||
|
||||
|
||||
def test_the_category_and_the_links_add_up_without_overlapping(
|
||||
user, account, category
|
||||
):
|
||||
other_category = Category.objects.create(owner=user, name="Otra")
|
||||
in_category = make_expense(user, account, category, amount="100")
|
||||
linked_outside = make_expense(user, account, other_category, amount="30")
|
||||
goal = make_payment_goal(user, category=category)
|
||||
|
||||
# El de la categoría también vinculado a mano, para que el solape exista.
|
||||
goal.linked_expenses.add(in_category, linked_outside)
|
||||
|
||||
assert goal.progress == Decimal("130")
|
||||
|
||||
|
||||
def test_a_linked_expense_counted_in_several_goals_is_not_inflated(
|
||||
user, account, category
|
||||
):
|
||||
expense = make_expense(user, account, category, amount="100")
|
||||
goal = make_payment_goal(user, category=category)
|
||||
other_goal = make_payment_goal(user, category=category)
|
||||
|
||||
# El JOIN del M2M devuelve una fila por objetivo vinculado: si se suma sin
|
||||
# deduplicar, este gasto cuenta tantas veces como objetivos tenga.
|
||||
goal.linked_expenses.add(expense)
|
||||
other_goal.linked_expenses.add(expense)
|
||||
|
||||
assert goal.progress == Decimal("100")
|
||||
|
||||
|
||||
def test_a_goal_with_links_and_no_category_still_measures(user, account, category):
|
||||
expense = make_expense(user, account, category, amount="40")
|
||||
goal = make_payment_goal(user, category=None)
|
||||
|
||||
goal.linked_expenses.add(expense)
|
||||
|
||||
assert goal.progress == Decimal("40")
|
||||
|
||||
|
||||
def test_links_respect_the_start_date(user, account, category):
|
||||
old = make_expense(user, account, category, amount="99", on=date(2023, 5, 1))
|
||||
goal = make_payment_goal(user, category=None, start=date(2024, 1, 1))
|
||||
|
||||
goal.linked_expenses.add(old)
|
||||
|
||||
assert goal.progress == Decimal("0")
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Presupuesto: el caso con más riesgo de duplicar, porque siempre tiene
|
||||
# categoría y además admite vinculaciones
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_a_budget_counts_a_linked_expense_of_its_own_category_once(
|
||||
user, account, category
|
||||
):
|
||||
# Un presupuesto siempre lleva categoría, así que el solape con la
|
||||
# vinculación a mano es el caso normal, no el raro.
|
||||
expense = make_expense(user, account, category, amount="30", on=date.today())
|
||||
goal = make_budget_goal(user, category, target="100")
|
||||
|
||||
goal.linked_expenses.add(expense)
|
||||
|
||||
assert goal.progress == Decimal("30")
|
||||
|
||||
|
||||
def test_a_budget_adds_a_linked_expense_from_another_category(
|
||||
user, account, category
|
||||
):
|
||||
other_category = Category.objects.create(owner=user, name="Ocio")
|
||||
make_expense(user, account, category, amount="30", on=date.today())
|
||||
outside = make_expense(user, account, other_category, amount="20", on=date.today())
|
||||
goal = make_budget_goal(user, category, target="100")
|
||||
|
||||
# El caso que motiva vincular en un presupuesto: un gasto de otra categoría
|
||||
# que quieres que compute aquí.
|
||||
goal.linked_expenses.add(outside)
|
||||
|
||||
assert goal.progress == Decimal("50")
|
||||
|
||||
|
||||
def test_a_budget_blown_by_a_linked_expense_is_still_flagged(user, account, category):
|
||||
other_category = Category.objects.create(owner=user, name="Ocio")
|
||||
outside = make_expense(user, account, other_category, amount="120", on=date.today())
|
||||
goal = make_budget_goal(user, category, target="100")
|
||||
|
||||
goal.linked_expenses.add(outside)
|
||||
|
||||
assert goal.progress == Decimal("120")
|
||||
assert goal.is_exceeded() is True
|
||||
assert goal.progress_state() == "danger"
|
||||
|
||||
|
||||
def test_a_budget_accepts_links(user, category):
|
||||
assert make_budget_goal(user, category).accepts_links is True
|
||||
|
||||
|
||||
def test_only_the_budget_inverts_the_criterion(user, account, category):
|
||||
# Pasado del objetivo: el presupuesto lo marca como excedido, el pago lo
|
||||
# considera conseguido. Solo el presupuesto invierte el criterio.
|
||||
over_budget = make_budget_goal(user, category, target="100")
|
||||
over_budget.linked_expenses.add(
|
||||
make_expense(user, account, category, amount="120", on=date.today())
|
||||
)
|
||||
over_payment = make_payment_goal(user, category=None, target="100")
|
||||
over_payment.linked_expenses.add(
|
||||
make_expense(user, account, category, amount="120")
|
||||
)
|
||||
|
||||
assert over_budget.is_exceeded() is True
|
||||
assert over_budget.progress_state() == "danger"
|
||||
assert over_payment.is_exceeded() is False
|
||||
assert over_payment.progress_state() == "complete"
|
||||
|
||||
|
||||
def test_a_budget_at_eighty_percent_warns(user, account, category):
|
||||
goal = make_budget_goal(user, category, target="100")
|
||||
goal.linked_expenses.add(
|
||||
make_expense(user, account, category, amount="80", on=date.today())
|
||||
)
|
||||
|
||||
assert goal.progress_state() == "warning"
|
||||
assert goal.is_exceeded() is False
|
||||
|
||||
|
||||
def test_a_linked_expense_outside_the_budget_period_does_not_count(
|
||||
user, account, category
|
||||
):
|
||||
other_category = Category.objects.create(owner=user, name="Ocio")
|
||||
old = make_expense(
|
||||
user, account, other_category, amount="50", on=date(2020, 1, 15)
|
||||
)
|
||||
goal = make_budget_goal(user, category, target="100")
|
||||
|
||||
# El presupuesto se reinicia cada mes: vincular algo viejo no lo resucita.
|
||||
goal.linked_expenses.add(old)
|
||||
|
||||
assert goal.progress == Decimal("0")
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Cobro pendiente
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_a_collection_goal_sums_its_linked_incomes(user, account):
|
||||
income = make_income(user, account, amount="250")
|
||||
goal = make_collection_goal(user)
|
||||
|
||||
goal.linked_incomes.add(income)
|
||||
|
||||
assert goal.progress == Decimal("250")
|
||||
|
||||
|
||||
def test_an_income_outside_the_collection_goal_does_not_count(user, account):
|
||||
make_income(user, account, amount="250")
|
||||
goal = make_collection_goal(user)
|
||||
|
||||
assert goal.progress == Decimal("0")
|
||||
|
||||
|
||||
def test_a_collection_goal_respects_the_start_date(user, account):
|
||||
old = make_income(user, account, amount="250", on=date(2023, 1, 1))
|
||||
goal = make_collection_goal(user, start=date(2024, 1, 1))
|
||||
|
||||
goal.linked_incomes.add(old)
|
||||
|
||||
assert goal.progress == Decimal("0")
|
||||
|
||||
|
||||
def test_a_collection_goal_at_half_way_is_not_flagged(user, account):
|
||||
goal = make_collection_goal(user, target="100")
|
||||
goal.linked_incomes.add(make_income(user, account, amount="50"))
|
||||
|
||||
assert goal.percentage() == Decimal("50")
|
||||
# Acercarse al 100% es bueno aquí, como en pago y ahorro: solo el
|
||||
# presupuesto invierte el criterio.
|
||||
assert goal.progress_state() == "ok"
|
||||
assert goal.is_exceeded() is False
|
||||
|
||||
|
||||
def test_a_fully_collected_goal_is_complete(user, account):
|
||||
goal = make_collection_goal(user, target="100")
|
||||
goal.linked_incomes.add(make_income(user, account, amount="100"))
|
||||
|
||||
assert goal.progress_state() == "complete"
|
||||
assert goal.is_exceeded() is False
|
||||
|
||||
|
||||
def test_the_new_kind_is_offered_in_the_goal_list_filter(auth_client, user):
|
||||
make_collection_goal(user)
|
||||
make_payment_goal(user)
|
||||
|
||||
response = auth_client.get(reverse("goal_list"), {"kind": Goal.KIND_COLLECTION})
|
||||
|
||||
assert response.status_code == 200
|
||||
assert [g.kind for g in response.context["goals"]] == [Goal.KIND_COLLECTION]
|
||||
|
||||
|
||||
def test_a_collection_goal_needs_a_start_date(user):
|
||||
form = GoalForm(
|
||||
{
|
||||
"name": "Cobro",
|
||||
"kind": Goal.KIND_COLLECTION,
|
||||
"target_amount": "100",
|
||||
"period": Goal.PERIOD_NONE,
|
||||
},
|
||||
user=user,
|
||||
)
|
||||
|
||||
assert not form.is_valid()
|
||||
assert "start_date" in form.errors
|
||||
|
||||
|
||||
def test_a_collection_goal_needs_neither_category_nor_account(user, category):
|
||||
form = GoalForm(
|
||||
{
|
||||
"name": "Cobro",
|
||||
"kind": Goal.KIND_COLLECTION,
|
||||
"target_amount": "100",
|
||||
"start_date": "2024-01-01",
|
||||
"period": Goal.PERIOD_NONE,
|
||||
},
|
||||
user=user,
|
||||
)
|
||||
|
||||
assert form.is_valid(), form.errors
|
||||
goal = form.save(commit=False)
|
||||
assert goal.category is None
|
||||
assert goal.account is None
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Qué es vinculable: las patas del traspaso no, la comisión sí
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_a_transfer_leg_is_not_a_candidate(user, account, savings, category):
|
||||
transfer = Transfer(
|
||||
owner=user,
|
||||
date=date(2024, 5, 11),
|
||||
amount=Decimal("400"),
|
||||
from_account=account,
|
||||
to_account=savings,
|
||||
)
|
||||
transfer.sync()
|
||||
goal = make_payment_goal(user, category=category)
|
||||
|
||||
candidates = list(goal.candidate_movements())
|
||||
|
||||
# Mover dinero entre cuentas propias no es ni un pago ni un cobro.
|
||||
assert transfer.expense_leg not in candidates
|
||||
|
||||
|
||||
def test_the_income_leg_is_not_a_candidate_for_a_collection_goal(
|
||||
user, account, savings
|
||||
):
|
||||
transfer = Transfer(
|
||||
owner=user,
|
||||
date=date(2024, 5, 11),
|
||||
amount=Decimal("400"),
|
||||
from_account=account,
|
||||
to_account=savings,
|
||||
)
|
||||
transfer.sync()
|
||||
goal = make_collection_goal(user)
|
||||
|
||||
assert transfer.income_leg not in list(goal.candidate_movements())
|
||||
|
||||
|
||||
def test_the_transfer_fee_is_a_candidate(user, account, savings, category):
|
||||
transfer = Transfer(
|
||||
owner=user,
|
||||
date=date(2024, 5, 11),
|
||||
amount=Decimal("400"),
|
||||
from_account=account,
|
||||
to_account=savings,
|
||||
fee_amount=Decimal("1.50"),
|
||||
)
|
||||
transfer.sync()
|
||||
goal = make_payment_goal(user, category=category)
|
||||
|
||||
# La comisión es gasto real: va con is_transfer=False y cuenta en análisis.
|
||||
assert transfer.fee_expense in list(goal.candidate_movements())
|
||||
|
||||
|
||||
def test_a_saving_goal_does_not_offer_linking(user, savings):
|
||||
goal = Goal.objects.create(
|
||||
owner=user,
|
||||
name="Ahorro",
|
||||
kind=Goal.KIND_SAVING,
|
||||
account=savings,
|
||||
target_amount=Decimal("100"),
|
||||
)
|
||||
|
||||
assert goal.accepts_links is False
|
||||
|
||||
|
||||
def test_the_linking_screen_turns_away_a_saving_goal(auth_client, user, savings):
|
||||
goal = Goal.objects.create(
|
||||
owner=user,
|
||||
name="Ahorro",
|
||||
kind=Goal.KIND_SAVING,
|
||||
account=savings,
|
||||
target_amount=Decimal("100"),
|
||||
)
|
||||
|
||||
response = auth_client.get(reverse("goal_links", args=[goal.pk]))
|
||||
|
||||
assert response.status_code == 302
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Vía 1: el formulario del movimiento
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_the_expense_form_only_offers_goals_that_take_expenses(
|
||||
user, savings, category
|
||||
):
|
||||
payment = make_payment_goal(user, category=category)
|
||||
budget = make_budget_goal(user, category)
|
||||
collection = make_collection_goal(user)
|
||||
saving = Goal.objects.create(
|
||||
owner=user,
|
||||
name="Ahorro",
|
||||
kind=Goal.KIND_SAVING,
|
||||
account=savings,
|
||||
target_amount=Decimal("100"),
|
||||
)
|
||||
|
||||
offered = list(ExpenseForm(user=user).fields["goals"].queryset)
|
||||
|
||||
assert payment in offered
|
||||
assert budget in offered
|
||||
assert collection not in offered
|
||||
assert saving not in offered
|
||||
|
||||
|
||||
def test_the_goal_list_offers_linking_on_a_budget(auth_client, user, category):
|
||||
goal = make_budget_goal(user, category)
|
||||
|
||||
response = auth_client.get(reverse("goal_list"))
|
||||
|
||||
assert reverse("goal_links", args=[goal.pk]) in response.content.decode()
|
||||
|
||||
|
||||
def test_the_income_form_only_offers_collection_goals(user, category):
|
||||
collection = make_collection_goal(user)
|
||||
payment = make_payment_goal(user, category=category)
|
||||
|
||||
offered = list(IncomeForm(user=user).fields["goals"].queryset)
|
||||
|
||||
assert collection in offered
|
||||
assert payment not in offered
|
||||
|
||||
|
||||
def test_the_expense_form_does_not_offer_another_users_goals(
|
||||
user, category, django_user_model
|
||||
):
|
||||
intruder = django_user_model.objects.create_user(username="otro", password="x")
|
||||
theirs = make_payment_goal(intruder, category=None)
|
||||
|
||||
assert theirs not in list(ExpenseForm(user=user).fields["goals"].queryset)
|
||||
|
||||
|
||||
def test_creating_an_expense_links_it_to_the_chosen_goal(
|
||||
auth_client, user, account, category
|
||||
):
|
||||
goal = make_payment_goal(user, category=None)
|
||||
|
||||
response = auth_client.post(
|
||||
reverse("expense_create"),
|
||||
{
|
||||
"date": "2024-05-11",
|
||||
"amount": "75",
|
||||
"category": category.pk,
|
||||
"account": account.pk,
|
||||
"description": "Compra",
|
||||
"goals": [goal.pk],
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 302
|
||||
assert goal.progress == Decimal("75")
|
||||
|
||||
|
||||
def test_creating_an_income_links_it_to_the_chosen_goal(auth_client, user, account):
|
||||
goal = make_collection_goal(user)
|
||||
|
||||
response = auth_client.post(
|
||||
reverse("income_create"),
|
||||
{
|
||||
"date": "2024-05-11",
|
||||
"amount": "75",
|
||||
"account": account.pk,
|
||||
"name": "Devolución",
|
||||
"goals": [goal.pk],
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 302
|
||||
assert goal.progress == Decimal("75")
|
||||
|
||||
|
||||
def test_editing_an_expense_can_unlink_it(auth_client, user, account, category):
|
||||
expense = make_expense(user, account, category, amount="75")
|
||||
goal = make_payment_goal(user, category=None)
|
||||
goal.linked_expenses.add(expense)
|
||||
|
||||
auth_client.post(
|
||||
reverse("expense_edit", args=[expense.pk]),
|
||||
{
|
||||
"date": "2024-05-11",
|
||||
"amount": "75",
|
||||
"category": category.pk,
|
||||
"account": account.pk,
|
||||
"description": "",
|
||||
# Sin "goals": se queda sin vincular.
|
||||
},
|
||||
)
|
||||
|
||||
assert goal.linked_expenses.count() == 0
|
||||
|
||||
|
||||
def test_the_expense_form_shows_the_current_links_when_editing(
|
||||
user, account, category
|
||||
):
|
||||
expense = make_expense(user, account, category)
|
||||
goal = make_payment_goal(user, category=None)
|
||||
goal.linked_expenses.add(expense)
|
||||
|
||||
form = ExpenseForm(instance=expense, user=user)
|
||||
|
||||
assert form.initial["goals"] == [goal.pk]
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Vía 2: la pantalla del objetivo
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_the_linking_screen_lists_the_candidates(auth_client, user, account, category):
|
||||
expense = make_expense(user, account, category)
|
||||
goal = make_payment_goal(user, category=None)
|
||||
|
||||
response = auth_client.get(reverse("goal_links", args=[goal.pk]))
|
||||
|
||||
assert response.status_code == 200
|
||||
assert expense in list(response.context["page_obj"])
|
||||
|
||||
|
||||
def test_linking_from_the_goal_screen_saves(auth_client, user, account, category):
|
||||
expense = make_expense(user, account, category, amount="60")
|
||||
goal = make_payment_goal(user, category=None)
|
||||
|
||||
auth_client.post(
|
||||
reverse("goal_links", args=[goal.pk]),
|
||||
{"shown": [expense.pk], "movements": [expense.pk]},
|
||||
)
|
||||
|
||||
assert list(goal.linked_expenses.all()) == [expense]
|
||||
|
||||
|
||||
def test_both_ways_of_linking_end_up_the_same(auth_client, user, account, category):
|
||||
first = make_expense(user, account, category, amount="60")
|
||||
second = make_expense(user, account, category, amount="60")
|
||||
by_screen = make_payment_goal(user, category=None)
|
||||
by_form = make_payment_goal(user, category=None)
|
||||
|
||||
auth_client.post(
|
||||
reverse("goal_links", args=[by_screen.pk]),
|
||||
{"shown": [first.pk], "movements": [first.pk]},
|
||||
)
|
||||
auth_client.post(
|
||||
reverse("expense_edit", args=[second.pk]),
|
||||
{
|
||||
"date": second.date.isoformat(),
|
||||
"amount": "60",
|
||||
"category": category.pk,
|
||||
"account": account.pk,
|
||||
"description": "",
|
||||
"goals": [by_form.pk],
|
||||
},
|
||||
)
|
||||
|
||||
assert by_screen.progress == by_form.progress == Decimal("60")
|
||||
|
||||
|
||||
def test_unchecking_on_the_goal_screen_unlinks(auth_client, user, account, category):
|
||||
expense = make_expense(user, account, category)
|
||||
goal = make_payment_goal(user, category=None)
|
||||
goal.linked_expenses.add(expense)
|
||||
|
||||
auth_client.post(
|
||||
reverse("goal_links", args=[goal.pk]),
|
||||
{"shown": [expense.pk]},
|
||||
)
|
||||
|
||||
assert goal.linked_expenses.count() == 0
|
||||
|
||||
|
||||
def test_a_get_on_the_linking_screen_saves_nothing(
|
||||
auth_client, user, account, category
|
||||
):
|
||||
expense = make_expense(user, account, category)
|
||||
goal = make_payment_goal(user, category=None)
|
||||
|
||||
auth_client.get(
|
||||
reverse("goal_links", args=[goal.pk]),
|
||||
{"shown": [expense.pk], "movements": [expense.pk]},
|
||||
)
|
||||
|
||||
assert goal.linked_expenses.count() == 0
|
||||
|
||||
|
||||
def test_saving_one_page_leaves_the_other_pages_alone(
|
||||
auth_client, user, account, category
|
||||
):
|
||||
# La pantalla pagina de 10 en 10: un set() a secas con lo marcado en una
|
||||
# página desvincularía todo lo de las demás.
|
||||
expenses = [
|
||||
make_expense(user, account, category, on=date(2024, 5, day))
|
||||
for day in range(1, 15)
|
||||
]
|
||||
goal = make_payment_goal(user, category=None)
|
||||
goal.linked_expenses.set(expenses)
|
||||
|
||||
first_page = list(auth_client.get(reverse("goal_links", args=[goal.pk])).context["page_obj"])
|
||||
# Se reenvía la primera página sin ninguna marca: solo esos se desvinculan.
|
||||
auth_client.post(
|
||||
reverse("goal_links", args=[goal.pk]),
|
||||
{"shown": [m.pk for m in first_page]},
|
||||
)
|
||||
|
||||
assert goal.linked_expenses.count() == len(expenses) - len(first_page)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Aislamiento por usuario
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_another_users_movement_cannot_be_linked_by_post(
|
||||
auth_client, user, django_user_model
|
||||
):
|
||||
intruder = django_user_model.objects.create_user(username="otro", password="x")
|
||||
their_account = Account.objects.create(
|
||||
owner=intruder, name="Ajena", initial_balance=Decimal("0"), active=True
|
||||
)
|
||||
their_category = Category.objects.create(owner=intruder, name="Ajena")
|
||||
theirs = make_expense(intruder, their_account, their_category, amount="500")
|
||||
goal = make_payment_goal(user, category=None)
|
||||
|
||||
response = auth_client.post(
|
||||
reverse("goal_links", args=[goal.pk]),
|
||||
{"shown": [theirs.pk], "movements": [theirs.pk]},
|
||||
)
|
||||
|
||||
assert response.status_code == 302
|
||||
assert goal.linked_expenses.count() == 0
|
||||
assert goal.progress == Decimal("0")
|
||||
|
||||
|
||||
def test_another_users_goal_cannot_be_opened(auth_client, django_user_model):
|
||||
intruder = django_user_model.objects.create_user(username="otro", password="x")
|
||||
theirs = make_payment_goal(intruder, category=None)
|
||||
|
||||
response = auth_client.get(reverse("goal_links", args=[theirs.pk]))
|
||||
|
||||
assert response.status_code == 404
|
||||
|
||||
|
||||
def test_another_users_goal_cannot_be_chosen_from_the_expense_form(
|
||||
auth_client, user, account, category, django_user_model
|
||||
):
|
||||
intruder = django_user_model.objects.create_user(username="otro", password="x")
|
||||
theirs = make_payment_goal(intruder, category=None)
|
||||
|
||||
auth_client.post(
|
||||
reverse("expense_create"),
|
||||
{
|
||||
"date": "2024-05-11",
|
||||
"amount": "75",
|
||||
"category": category.pk,
|
||||
"account": account.pk,
|
||||
"description": "Compra",
|
||||
"goals": [theirs.pk],
|
||||
},
|
||||
)
|
||||
|
||||
assert theirs.linked_expenses.count() == 0
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Borrado y rendimiento
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_deleting_a_goal_keeps_the_linked_movements(user, account, category):
|
||||
expense = make_expense(user, account, category)
|
||||
goal = make_payment_goal(user, category=None)
|
||||
goal.linked_expenses.add(expense)
|
||||
|
||||
goal.delete()
|
||||
|
||||
# Se limpia la tabla intermedia y el gasto sigue ahí.
|
||||
expense.refresh_from_db()
|
||||
assert Expense.objects.filter(pk=expense.pk).exists()
|
||||
|
||||
|
||||
def test_deleting_a_linked_movement_leaves_the_goal_alone(user, account, category):
|
||||
expense = make_expense(user, account, category)
|
||||
goal = make_payment_goal(user, category=None)
|
||||
goal.linked_expenses.add(expense)
|
||||
|
||||
expense.delete()
|
||||
goal.refresh_from_db()
|
||||
|
||||
assert goal.linked_expenses.count() == 0
|
||||
assert goal.progress == Decimal("0")
|
||||
|
||||
|
||||
def _queries_painting(auth_client, user, account, category, goals, links_each):
|
||||
"""Consultas que cuesta pintar goal_list con esos objetivos y vinculaciones."""
|
||||
Goal.objects.filter(owner=user).delete()
|
||||
for _ in range(goals):
|
||||
goal = make_payment_goal(user, category=None)
|
||||
goal.linked_expenses.set(
|
||||
[make_expense(user, account, category) for _ in range(links_each)]
|
||||
)
|
||||
|
||||
with CaptureQueriesContext(connection) as captured:
|
||||
assert auth_client.get(reverse("goal_list")).status_code == 200
|
||||
|
||||
return len(captured)
|
||||
|
||||
|
||||
def test_linked_movements_do_not_cost_a_query_each(
|
||||
auth_client, user, account, category
|
||||
):
|
||||
# El N+1 que reaparece con el M2M: una consulta por movimiento vinculado al
|
||||
# pintar la lista. Triplicar las vinculaciones no debe mover el número.
|
||||
few = _queries_painting(auth_client, user, account, category, 2, 1)
|
||||
many = _queries_painting(auth_client, user, account, category, 2, 3)
|
||||
|
||||
assert few == many
|
||||
|
||||
|
||||
def test_each_extra_goal_costs_a_single_query(auth_client, user, account, category):
|
||||
# Cada objetivo cuesta el agregado de su progress(), y nada más.
|
||||
two = _queries_painting(auth_client, user, account, category, 2, 2)
|
||||
four = _queries_painting(auth_client, user, account, category, 4, 2)
|
||||
|
||||
assert four - two == 2
|
||||
Loading…
Reference in New Issue
Block a user