232 lines
8.5 KiB
Python
232 lines
8.5 KiB
Python
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()
|
|
|
|
|
|
def _edit_payload(account, edit_date, amount, odometer, liters, next_url=None):
|
|
payload = {
|
|
"date": edit_date.strftime("%Y-%m-%d"),
|
|
"amount": str(amount),
|
|
"account": account.pk,
|
|
"odometer": str(odometer),
|
|
"liters": str(liters),
|
|
}
|
|
if next_url is not None:
|
|
payload["next"] = next_url
|
|
return payload
|
|
|
|
|
|
def test_fuel_edit_post_updates_expense_and_fuelentry(auth_client, user, account, category):
|
|
entry = _make_fuel_entry(user, account, category, odometer=1000, liters=Decimal("20"), amount=Decimal("50"))
|
|
expense = entry.expense
|
|
new_date = date(2024, 3, 1)
|
|
|
|
response = auth_client.post(
|
|
reverse('fuel_edit', args=[expense.pk]),
|
|
_edit_payload(account, new_date, Decimal("45.50"), 1200, Decimal("22.5")),
|
|
)
|
|
|
|
assert response.status_code == 302
|
|
|
|
expense.refresh_from_db()
|
|
entry.refresh_from_db()
|
|
|
|
assert expense.amount == Decimal("45.50")
|
|
assert expense.date == new_date
|
|
assert expense.account_id == account.pk
|
|
assert entry.odometer == 1200
|
|
assert entry.liters == Decimal("22.5")
|
|
|
|
|
|
def test_fuel_edit_invalid_post_returns_200_and_does_not_modify(auth_client, user, account, category):
|
|
entry = _make_fuel_entry(user, account, category, odometer=1000, liters=Decimal("20"), amount=Decimal("50"))
|
|
expense = entry.expense
|
|
original_date = expense.date
|
|
|
|
payload = _edit_payload(account, date(2024, 3, 1), Decimal("45.50"), 1200, Decimal("22.5"))
|
|
payload["liters"] = ""
|
|
|
|
response = auth_client.post(reverse('fuel_edit', args=[expense.pk]), payload)
|
|
|
|
assert response.status_code == 200
|
|
|
|
expense.refresh_from_db()
|
|
entry.refresh_from_db()
|
|
|
|
assert expense.amount == Decimal("50")
|
|
assert expense.date == original_date
|
|
assert entry.odometer == 1000
|
|
assert entry.liters == Decimal("20")
|
|
|
|
|
|
def test_fuel_edit_redirects_to_next_when_relative(auth_client, user, account, category):
|
|
entry = _make_fuel_entry(user, account, category, odometer=1000, liters=Decimal("20"))
|
|
expense = entry.expense
|
|
next_url = reverse('expense_list')
|
|
|
|
response = auth_client.post(
|
|
reverse('fuel_edit', args=[expense.pk]),
|
|
_edit_payload(account, date(2024, 3, 1), Decimal("45.50"), 1200, Decimal("22.5"), next_url=next_url),
|
|
)
|
|
|
|
assert response.status_code == 302
|
|
assert response.url == next_url
|
|
|
|
|
|
def test_fuel_edit_ignores_external_next(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_edit', args=[expense.pk]),
|
|
_edit_payload(
|
|
account, date(2024, 3, 1), Decimal("45.50"), 1200, Decimal("22.5"),
|
|
next_url="https://evil.com",
|
|
),
|
|
)
|
|
|
|
assert response.status_code == 302
|
|
assert response.url == reverse('expense_list')
|
|
|
|
|
|
def test_fuel_delete_get_does_not_delete(auth_client, user, account, category):
|
|
entry = _make_fuel_entry(user, account, category, odometer=1000, liters=Decimal("20"))
|
|
expense = entry.expense
|
|
|
|
response = auth_client.get(reverse('fuel_delete', args=[expense.pk]))
|
|
|
|
assert response.status_code == 200
|
|
assert Expense.objects.filter(pk=expense.pk).exists()
|
|
assert FuelEntry.objects.filter(pk=entry.pk).exists()
|
|
|
|
|
|
def test_fuel_delete_get_cancel_link_uses_next_when_provided(auth_client, user, account, category):
|
|
entry = _make_fuel_entry(user, account, category, odometer=1000, liters=Decimal("20"))
|
|
expense = entry.expense
|
|
next_url = reverse('expense_list')
|
|
|
|
response = auth_client.get(reverse('fuel_delete', args=[expense.pk]), {"next": next_url})
|
|
|
|
assert response.status_code == 200
|
|
assert f'<a class="btn btn-secondary" href="{next_url}">Cancelar</a>'.encode() in response.content
|
|
|
|
|
|
def test_fuel_delete_get_cancel_link_falls_back_to_fuel_list_without_next(auth_client, user, account, category):
|
|
entry = _make_fuel_entry(user, account, category, odometer=1000, liters=Decimal("20"))
|
|
expense = entry.expense
|
|
fuel_list_url = reverse('fuel_list')
|
|
|
|
response = auth_client.get(reverse('fuel_delete', args=[expense.pk]))
|
|
|
|
assert response.status_code == 200
|
|
assert f'<a class="btn btn-secondary" href="{fuel_list_url}">Cancelar</a>'.encode() in response.content
|
|
|
|
|
|
def test_fuel_delete_post_redirects_to_next_when_relative(auth_client, user, account, category):
|
|
entry = _make_fuel_entry(user, account, category, odometer=1000, liters=Decimal("20"))
|
|
expense = entry.expense
|
|
next_url = reverse('expense_list')
|
|
|
|
response = auth_client.post(reverse('fuel_delete', args=[expense.pk]), {"next": next_url})
|
|
|
|
assert response.status_code == 302
|
|
assert response.url == next_url
|
|
|
|
|
|
def test_fuel_delete_post_ignores_external_next(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]), {"next": "https://evil.com"})
|
|
|
|
assert response.status_code == 302
|
|
assert response.url == reverse('fuel_list')
|
|
|
|
|
|
def test_fuel_delete_get_ignores_unsafe_next_in_cancel_link(auth_client, user, account, category):
|
|
entry = _make_fuel_entry(user, account, category, odometer=1000, liters=Decimal("20"))
|
|
expense = entry.expense
|
|
fuel_list_url = reverse('fuel_list')
|
|
|
|
response = auth_client.get(reverse('fuel_delete', args=[expense.pk]), {"next": "javascript:alert(1)"})
|
|
|
|
assert response.status_code == 200
|
|
assert b"javascript:" not in response.content
|
|
assert f'<a class="btn btn-secondary" href="{fuel_list_url}">Cancelar</a>'.encode() in response.content
|
|
|
|
|
|
def test_fuel_edit_get_ignores_unsafe_next_in_cancel_link(auth_client, user, account, category):
|
|
entry = _make_fuel_entry(user, account, category, odometer=1000, liters=Decimal("20"))
|
|
expense = entry.expense
|
|
|
|
response = auth_client.get(reverse('fuel_edit', args=[expense.pk]), {"next": "javascript:alert(1)"})
|
|
|
|
assert response.status_code == 200
|
|
assert b"javascript:" not in response.content
|