From 700d35b5d51367566e52ba845aceed9556594203 Mon Sep 17 00:00:00 2001 From: Herculino Trotta Date: Sun, 28 Dec 2025 13:36:21 -0300 Subject: [PATCH] feat(tests): add tests for monthly summaries --- app/apps/monthly_overview/tests/__init__.py | 0 .../monthly_overview/tests/test_summary.py | 331 ++++++++++++++++++ 2 files changed, 331 insertions(+) create mode 100644 app/apps/monthly_overview/tests/__init__.py create mode 100644 app/apps/monthly_overview/tests/test_summary.py diff --git a/app/apps/monthly_overview/tests/__init__.py b/app/apps/monthly_overview/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/apps/monthly_overview/tests/test_summary.py b/app/apps/monthly_overview/tests/test_summary.py new file mode 100644 index 0000000..590cab6 --- /dev/null +++ b/app/apps/monthly_overview/tests/test_summary.py @@ -0,0 +1,331 @@ +from datetime import date +from decimal import Decimal + +from django.contrib.auth import get_user_model +from django.test import TestCase, override_settings + +from apps.accounts.models import Account, AccountGroup +from apps.currencies.models import Currency +from apps.transactions.models import ( + Transaction, + TransactionCategory, + TransactionTag, +) + + +@override_settings( + STORAGES={ + "default": {"BACKEND": "django.core.files.storage.FileSystemStorage"}, + "staticfiles": { + "BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage" + }, + }, + WHITENOISE_AUTOREFRESH=True, +) +class MonthlySummaryFilterBehaviorTests(TestCase): + """Tests for monthly summary views filter behavior. + + These tests verify that: + 1. Views work correctly without any filters + 2. Views work correctly with filters applied + 3. The filter detection logic properly uses different querysets + 4. Calculated values reflect the applied filters + """ + + def setUp(self): + """Set up test data""" + User = get_user_model() + self.user = User.objects.create_user( + email="testuser@test.com", password="testpass123" + ) + self.client.login(username="testuser@test.com", password="testpass123") + + self.currency = Currency.objects.create( + code="USD", name="US Dollar", decimal_places=2, prefix="$ " + ) + self.account_group = AccountGroup.objects.create(name="Test Group") + self.account = Account.objects.create( + name="Test Account", + group=self.account_group, + currency=self.currency, + is_asset=False, + ) + self.category = TransactionCategory.objects.create( + name="Test Category", owner=self.user + ) + self.tag = TransactionTag.objects.create(name="TestTag", owner=self.user) + + # Create test transactions for December 2025 + # Income: 1000 (paid) + self.income_transaction = Transaction.objects.create( + account=self.account, + type=Transaction.Type.INCOME, + is_paid=True, + date=date(2025, 12, 10), + reference_date=date(2025, 12, 1), + amount=Decimal("1000.00"), + description="December Income", + owner=self.user, + ) + + # Expense: 200 (paid) + self.expense_transaction = Transaction.objects.create( + account=self.account, + type=Transaction.Type.EXPENSE, + is_paid=True, + date=date(2025, 12, 15), + reference_date=date(2025, 12, 1), + amount=Decimal("200.00"), + description="December Expense", + category=self.category, + owner=self.user, + ) + self.expense_transaction.tags.add(self.tag) + + # Expense: 150 (projected/unpaid) + self.projected_expense = Transaction.objects.create( + account=self.account, + type=Transaction.Type.EXPENSE, + is_paid=False, + date=date(2025, 12, 20), + reference_date=date(2025, 12, 1), + amount=Decimal("150.00"), + description="Projected Expense", + owner=self.user, + ) + + def _get_currency_data(self, context_dict): + """Helper to extract data for our test currency from context dict. + + The context dict is keyed by currency ID, so we need to find + the entry for our currency. + """ + if not context_dict: + return None + for currency_id, data in context_dict.items(): + if data.get("currency", {}).get("code") == "USD": + return data + return None + + # --- monthly_summary view tests --- + + def test_monthly_summary_no_filter_returns_200(self): + """Test that monthly_summary returns 200 without filters""" + response = self.client.get( + "/monthly/12/2025/summary/", + HTTP_HX_REQUEST="true", + ) + self.assertEqual(response.status_code, 200) + + def test_monthly_summary_no_filter_includes_all_transactions(self): + """Without filters, summary should include all transactions""" + response = self.client.get( + "/monthly/12/2025/summary/", + HTTP_HX_REQUEST="true", + ) + context = response.context + + # income_current should have the income: 1000 + income_current = context.get("income_current", {}) + usd_data = self._get_currency_data(income_current) + self.assertIsNotNone(usd_data) + self.assertEqual(usd_data["income_current"], Decimal("1000.00")) + + # expense_current should have paid expense: 200 + expense_current = context.get("expense_current", {}) + usd_data = self._get_currency_data(expense_current) + self.assertIsNotNone(usd_data) + self.assertEqual(usd_data["expense_current"], Decimal("200.00")) + + # expense_projected should have unpaid expense: 150 + expense_projected = context.get("expense_projected", {}) + usd_data = self._get_currency_data(expense_projected) + self.assertIsNotNone(usd_data) + self.assertEqual(usd_data["expense_projected"], Decimal("150.00")) + + def test_monthly_summary_type_filter_only_income(self): + """With type=IN filter, summary should only include income""" + response = self.client.get( + "/monthly/12/2025/summary/?type=IN", + HTTP_HX_REQUEST="true", + ) + context = response.context + + # income_current should still have 1000 + income_current = context.get("income_current", {}) + usd_data = self._get_currency_data(income_current) + self.assertIsNotNone(usd_data) + self.assertEqual(usd_data["income_current"], Decimal("1000.00")) + + # expense_current should be empty/zero (filtered out) + expense_current = context.get("expense_current", {}) + usd_data = self._get_currency_data(expense_current) + if usd_data: + self.assertEqual(usd_data.get("expense_current", 0), Decimal("0")) + + # expense_projected should be empty/zero (filtered out) + expense_projected = context.get("expense_projected", {}) + usd_data = self._get_currency_data(expense_projected) + if usd_data: + self.assertEqual(usd_data.get("expense_projected", 0), Decimal("0")) + + def test_monthly_summary_type_filter_only_expenses(self): + """With type=EX filter, summary should only include expenses""" + response = self.client.get( + "/monthly/12/2025/summary/?type=EX", + HTTP_HX_REQUEST="true", + ) + context = response.context + + # income_current should be empty/zero (filtered out) + income_current = context.get("income_current", {}) + usd_data = self._get_currency_data(income_current) + if usd_data: + self.assertEqual(usd_data.get("income_current", 0), Decimal("0")) + + # expense_current should have 200 + expense_current = context.get("expense_current", {}) + usd_data = self._get_currency_data(expense_current) + self.assertIsNotNone(usd_data) + self.assertEqual(usd_data["expense_current"], Decimal("200.00")) + + # expense_projected should have 150 + expense_projected = context.get("expense_projected", {}) + usd_data = self._get_currency_data(expense_projected) + self.assertIsNotNone(usd_data) + self.assertEqual(usd_data["expense_projected"], Decimal("150.00")) + + def test_monthly_summary_is_paid_filter_only_paid(self): + """With is_paid=1 filter, summary should only include paid transactions""" + response = self.client.get( + "/monthly/12/2025/summary/?is_paid=1", + HTTP_HX_REQUEST="true", + ) + context = response.context + + # income_current should have 1000 (paid) + income_current = context.get("income_current", {}) + usd_data = self._get_currency_data(income_current) + self.assertIsNotNone(usd_data) + self.assertEqual(usd_data["income_current"], Decimal("1000.00")) + + # expense_current should have 200 (paid) + expense_current = context.get("expense_current", {}) + usd_data = self._get_currency_data(expense_current) + self.assertIsNotNone(usd_data) + self.assertEqual(usd_data["expense_current"], Decimal("200.00")) + + # expense_projected should be empty/zero (filtered out - unpaid) + expense_projected = context.get("expense_projected", {}) + usd_data = self._get_currency_data(expense_projected) + if usd_data: + self.assertEqual(usd_data.get("expense_projected", 0), Decimal("0")) + + def test_monthly_summary_is_paid_filter_only_unpaid(self): + """With is_paid=0 filter, summary should only include unpaid transactions""" + response = self.client.get( + "/monthly/12/2025/summary/?is_paid=0", + HTTP_HX_REQUEST="true", + ) + context = response.context + + # income_current should be empty/zero (filtered out - paid) + income_current = context.get("income_current", {}) + usd_data = self._get_currency_data(income_current) + if usd_data: + self.assertEqual(usd_data.get("income_current", 0), Decimal("0")) + + # expense_current should be empty/zero (filtered out - paid) + expense_current = context.get("expense_current", {}) + usd_data = self._get_currency_data(expense_current) + if usd_data: + self.assertEqual(usd_data.get("expense_current", 0), Decimal("0")) + + # expense_projected should have 150 (unpaid) + expense_projected = context.get("expense_projected", {}) + usd_data = self._get_currency_data(expense_projected) + self.assertIsNotNone(usd_data) + self.assertEqual(usd_data["expense_projected"], Decimal("150.00")) + + def test_monthly_summary_description_filter(self): + """With description filter, summary should only include matching transactions""" + response = self.client.get( + "/monthly/12/2025/summary/?description=Income", + HTTP_HX_REQUEST="true", + ) + context = response.context + + # Only income matches "Income" description + income_current = context.get("income_current", {}) + usd_data = self._get_currency_data(income_current) + self.assertIsNotNone(usd_data) + self.assertEqual(usd_data["income_current"], Decimal("1000.00")) + + # Expenses should be filtered out + expense_current = context.get("expense_current", {}) + usd_data = self._get_currency_data(expense_current) + if usd_data: + self.assertEqual(usd_data.get("expense_current", 0), Decimal("0")) + + def test_monthly_summary_amount_filter(self): + """With amount filter, summary should only include transactions in range""" + # Filter to only get transactions between 100 and 250 (should get 200 and 150) + response = self.client.get( + "/monthly/12/2025/summary/?from_amount=100&to_amount=250", + HTTP_HX_REQUEST="true", + ) + context = response.context + + # Income (1000) should be filtered out + income_current = context.get("income_current", {}) + usd_data = self._get_currency_data(income_current) + if usd_data: + self.assertEqual(usd_data.get("income_current", 0), Decimal("0")) + + # expense_current should have 200 + expense_current = context.get("expense_current", {}) + usd_data = self._get_currency_data(expense_current) + self.assertIsNotNone(usd_data) + self.assertEqual(usd_data["expense_current"], Decimal("200.00")) + + # expense_projected should have 150 + expense_projected = context.get("expense_projected", {}) + usd_data = self._get_currency_data(expense_projected) + self.assertIsNotNone(usd_data) + self.assertEqual(usd_data["expense_projected"], Decimal("150.00")) + + # --- monthly_account_summary view tests --- + + def test_monthly_account_summary_no_filter_returns_200(self): + """Test that monthly_account_summary returns 200 without filters""" + response = self.client.get( + "/monthly/12/2025/summary/accounts/", + HTTP_HX_REQUEST="true", + ) + self.assertEqual(response.status_code, 200) + + def test_monthly_account_summary_with_filter_returns_200(self): + """Test that monthly_account_summary returns 200 with filter""" + response = self.client.get( + "/monthly/12/2025/summary/accounts/?type=IN", + HTTP_HX_REQUEST="true", + ) + self.assertEqual(response.status_code, 200) + + # --- monthly_currency_summary view tests --- + + def test_monthly_currency_summary_no_filter_returns_200(self): + """Test that monthly_currency_summary returns 200 without filters""" + response = self.client.get( + "/monthly/12/2025/summary/currencies/", + HTTP_HX_REQUEST="true", + ) + self.assertEqual(response.status_code, 200) + + def test_monthly_currency_summary_with_filter_returns_200(self): + """Test that monthly_currency_summary returns 200 with filter""" + response = self.client.get( + "/monthly/12/2025/summary/currencies/?type=EX", + HTTP_HX_REQUEST="true", + ) + self.assertEqual(response.status_code, 200)