Files
WYGIWYH/app/apps/transactions/tests.py
google-labs-jules[bot] 02f6bb0c29 Add initial Django tests for multiple apps
This commit introduces Django tests for several applications within your project. My goal was to cover the most important elements of each app.

Work Performed:

I analyzed and added tests for the following apps:
- apps.users: User authentication and profile management.
- apps.transactions: CRUD operations for transactions, categories, tags, entities, installment plans, and recurring transactions.
- apps.currencies: Management of currencies, exchange rates, and exchange rate services.
- apps.accounts: CRUD operations for accounts and account groups, including sharing.
- apps.common: Various utilities like custom fields, template tags, decorators, and management commands.
- apps.net_worth: Net worth calculation logic and display views.
- apps.import_app: Import profile validation, import service logic, and basic file processing.
- apps.export_app: Data export functionality using ModelResources and view logic for CSV/ZIP.
- apps.api: Core API endpoints for transactions and accounts, including permissions.

I also planned to cover:
- apps.rules
- apps.calendar_view
- apps.dca
2025-06-15 20:12:37 +00:00

524 lines
25 KiB
Python

import datetime
from decimal import Decimal
from datetime import date, timedelta
from django.test import TestCase, Client
from django.urls import reverse
from django.contrib.auth import get_user_model
from django.core.exceptions import ValidationError
from django.utils import timezone
from decimal import Decimal
import datetime # Import was missing
from apps.transactions.models import (
TransactionCategory,
TransactionTag,
TransactionEntity, # Added
Transaction,
InstallmentPlan,
RecurringTransaction,
)
from apps.accounts.models import Account, AccountGroup
from apps.currencies.models import Currency, ExchangeRate
User = get_user_model()
class BaseTransactionAppTest(TestCase):
def setUp(self):
self.user = User.objects.create_user(email="testuser@example.com", password="password")
self.other_user = User.objects.create_user(email="otheruser@example.com", password="password")
self.client = Client()
self.client.login(email="testuser@example.com", password="password")
self.currency = Currency.objects.create(
code="USD", name="US Dollar", decimal_places=2, prefix="$ "
)
self.account_group = AccountGroup.objects.create(name="Test Group", owner=self.user)
self.account = Account.objects.create(
name="Test Account", group=self.account_group, currency=self.currency, owner=self.user
)
class TransactionCategoryTests(BaseTransactionAppTest):
def test_category_creation(self):
"""Test basic category creation by a user."""
category = TransactionCategory.objects.create(name="Groceries", owner=self.user)
self.assertEqual(str(category), "Groceries")
self.assertFalse(category.mute)
self.assertTrue(category.active)
self.assertEqual(category.owner, self.user)
def test_category_creation_view(self):
response = self.client.post(reverse("category_add"), {"name": "Utilities", "active": "on"})
self.assertEqual(response.status_code, 204) # HTMX success, no content
self.assertTrue(TransactionCategory.objects.filter(name="Utilities", owner=self.user).exists())
def test_category_edit_view(self):
category = TransactionCategory.objects.create(name="Initial Name", owner=self.user)
response = self.client.post(reverse("category_edit", args=[category.id]), {"name": "Updated Name", "mute": "on", "active": "on"})
self.assertEqual(response.status_code, 204)
category.refresh_from_db()
self.assertEqual(category.name, "Updated Name")
self.assertTrue(category.mute)
def test_category_delete_view(self):
category = TransactionCategory.objects.create(name="To Delete", owner=self.user)
response = self.client.delete(reverse("category_delete", args=[category.id]))
self.assertEqual(response.status_code, 204)
self.assertFalse(TransactionCategory.all_objects.filter(id=category.id).exists()) # all_objects to check even if soft deleted by mistake
def test_other_user_cannot_edit_category(self):
category = TransactionCategory.objects.create(name="User1s Category", owner=self.user)
self.client.logout()
self.client.login(email="otheruser@example.com", password="password")
response = self.client.post(reverse("category_edit", args=[category.id]), {"name": "Attempted Update"})
# This should return a 204 with a message, not a 403, as per view logic for owned objects
self.assertEqual(response.status_code, 204)
category.refresh_from_db()
self.assertEqual(category.name, "User1s Category") # Name should not change
def test_category_sharing_and_visibility(self):
category = TransactionCategory.objects.create(name="Shared Cat", owner=self.user, visibility=TransactionCategory.Visibility.SHARED)
category.shared_with.add(self.other_user)
# Other user should be able to see it (though not directly tested here, view logic would permit)
# Test that owner can still edit
response = self.client.post(reverse("category_edit", args=[category.id]), {"name": "Owner Edited Shared Cat", "active":"on"})
self.assertEqual(response.status_code, 204)
category.refresh_from_db()
self.assertEqual(category.name, "Owner Edited Shared Cat")
# Test other user cannot delete if not owner
self.client.logout()
self.client.login(email="otheruser@example.com", password="password")
response = self.client.delete(reverse("category_delete", args=[category.id])) # This removes user from shared_with
self.assertEqual(response.status_code, 204)
category.refresh_from_db()
self.assertTrue(TransactionCategory.all_objects.filter(id=category.id).exists())
self.assertNotIn(self.other_user, category.shared_with.all())
class TransactionTagTests(BaseTransactionAppTest):
def test_tag_creation(self):
"""Test basic tag creation by a user."""
tag = TransactionTag.objects.create(name="Essential", owner=self.user)
self.assertEqual(str(tag), "Essential")
self.assertTrue(tag.active)
self.assertEqual(tag.owner, self.user)
def test_tag_creation_view(self):
response = self.client.post(reverse("tag_add"), {"name": "Vacation", "active": "on"})
self.assertEqual(response.status_code, 204)
self.assertTrue(TransactionTag.objects.filter(name="Vacation", owner=self.user).exists())
def test_tag_edit_view(self):
tag = TransactionTag.objects.create(name="Old Tag", owner=self.user)
response = self.client.post(reverse("tag_edit", args=[tag.id]), {"name": "New Tag", "active": "on"})
self.assertEqual(response.status_code, 204)
tag.refresh_from_db()
self.assertEqual(tag.name, "New Tag")
def test_tag_delete_view(self):
tag = TransactionTag.objects.create(name="Delete Me Tag", owner=self.user)
response = self.client.delete(reverse("tag_delete", args=[tag.id]))
self.assertEqual(response.status_code, 204)
self.assertFalse(TransactionTag.all_objects.filter(id=tag.id).exists())
class TransactionEntityTests(BaseTransactionAppTest):
def test_entity_creation(self):
"""Test basic entity creation by a user."""
entity = TransactionEntity.objects.create(name="Supermarket X", owner=self.user)
self.assertEqual(str(entity), "Supermarket X")
self.assertTrue(entity.active)
self.assertEqual(entity.owner, self.user)
def test_entity_creation_view(self):
response = self.client.post(reverse("entity_add"), {"name": "Online Store", "active": "on"})
self.assertEqual(response.status_code, 204)
self.assertTrue(TransactionEntity.objects.filter(name="Online Store", owner=self.user).exists())
def test_entity_edit_view(self):
entity = TransactionEntity.objects.create(name="Local Shop", owner=self.user)
response = self.client.post(reverse("entity_edit", args=[entity.id]), {"name": "Local Shop Inc.", "active": "on"})
self.assertEqual(response.status_code, 204)
entity.refresh_from_db()
self.assertEqual(entity.name, "Local Shop Inc.")
def test_entity_delete_view(self):
entity = TransactionEntity.objects.create(name="To Be Removed Entity", owner=self.user)
response = self.client.delete(reverse("entity_delete", args=[entity.id]))
self.assertEqual(response.status_code, 204)
self.assertFalse(TransactionEntity.all_objects.filter(id=entity.id).exists())
class TransactionTests(BaseTransactionAppTest): # Inherit from BaseTransactionAppTest
def setUp(self):
super().setUp() # Call BaseTransactionAppTest's setUp
"""Set up test data"""
# self.category is already created in BaseTransactionAppTest if needed,
# or create specific ones here.
self.category = TransactionCategory.objects.create(name="Test Category", owner=self.user)
self.tag = TransactionTag.objects.create(name="Test Tag", owner=self.user)
self.entity = TransactionEntity.objects.create(name="Test Entity", owner=self.user)
def test_transaction_creation(self):
"""Test basic transaction creation with required fields"""
transaction = Transaction.objects.create(
account=self.account,
owner=self.user, # Assign owner
type=Transaction.Type.EXPENSE,
date=timezone.now().date(),
amount=Decimal("100.00"),
description="Test transaction",
category=self.category,
)
transaction.tags.add(self.tag)
transaction.entities.add(self.entity)
self.assertTrue(transaction.is_paid)
self.assertEqual(transaction.type, Transaction.Type.EXPENSE)
self.assertEqual(transaction.account.currency.code, "USD")
self.assertEqual(transaction.owner, self.user)
self.assertIn(self.tag, transaction.tags.all())
self.assertIn(self.entity, transaction.entities.all())
def test_transaction_creation_view(self):
data = {
"account": self.account.id,
"type": Transaction.Type.INCOME,
"is_paid": "on",
"date": timezone.now().date().isoformat(),
"amount": "250.75",
"description": "Freelance Gig",
"category": self.category.id,
"tags": [self.tag.name], # Dynamic fields expect names for creation/selection
"entities": [self.entity.name]
}
response = self.client.post(reverse("transaction_add"), data)
self.assertEqual(response.status_code, 204, response.content.decode() if response.content else "No content")
self.assertTrue(
Transaction.objects.filter(description="Freelance Gig", owner=self.user, amount=Decimal("250.75")).exists()
)
# Check that tag and entity were associated (or created if DynamicModel...Field handled it)
created_transaction = Transaction.objects.get(description="Freelance Gig")
self.assertIn(self.tag, created_transaction.tags.all())
self.assertIn(self.entity, created_transaction.entities.all())
def test_transaction_edit_view(self):
transaction = Transaction.objects.create(
account=self.account, owner=self.user, type=Transaction.Type.EXPENSE,
date=timezone.now().date(), amount=Decimal("50.00"), description="Initial"
)
updated_description = "Updated Description"
updated_amount = "75.25"
response = self.client.post(
reverse("transaction_edit", args=[transaction.id]),
{
"account": self.account.id, "type": Transaction.Type.EXPENSE, "is_paid": "on",
"date": transaction.date.isoformat(), "amount": updated_amount,
"description": updated_description, "category": self.category.id
}
)
self.assertEqual(response.status_code, 204)
transaction.refresh_from_db()
self.assertEqual(transaction.description, updated_description)
self.assertEqual(transaction.amount, Decimal(updated_amount))
def test_transaction_soft_delete_view(self):
transaction = Transaction.objects.create(
account=self.account, owner=self.user, type=Transaction.Type.EXPENSE,
date=timezone.now().date(), amount=Decimal("10.00"), description="To Soft Delete"
)
response = self.client.delete(reverse("transaction_delete", args=[transaction.id]))
self.assertEqual(response.status_code, 204)
transaction.refresh_from_db()
self.assertTrue(transaction.deleted)
self.assertIsNotNone(transaction.deleted_at)
self.assertTrue(Transaction.deleted_objects.filter(id=transaction.id).exists())
self.assertFalse(Transaction.objects.filter(id=transaction.id).exists()) # Default manager should not find it
def test_transaction_hard_delete_after_soft_delete(self):
# First soft delete
transaction = Transaction.objects.create(
account=self.account, owner=self.user, type=Transaction.Type.EXPENSE,
date=timezone.now().date(), amount=Decimal("15.00"), description="To Hard Delete"
)
transaction.delete() # Soft delete via model method
self.assertTrue(Transaction.deleted_objects.filter(id=transaction.id).exists())
# Then hard delete via view (which calls model's delete again on an already soft-deleted item)
response = self.client.delete(reverse("transaction_delete", args=[transaction.id]))
self.assertEqual(response.status_code, 204)
self.assertFalse(Transaction.all_objects.filter(id=transaction.id).exists())
def test_transaction_undelete_view(self):
transaction = Transaction.objects.create(
account=self.account, owner=self.user, type=Transaction.Type.EXPENSE,
date=timezone.now().date(), amount=Decimal("20.00"), description="To Undelete"
)
transaction.delete() # Soft delete
transaction.refresh_from_db()
self.assertTrue(transaction.deleted)
response = self.client.get(reverse("transaction_undelete", args=[transaction.id]))
self.assertEqual(response.status_code, 204)
transaction.refresh_from_db()
self.assertFalse(transaction.deleted)
self.assertIsNone(transaction.deleted_at)
self.assertTrue(Transaction.objects.filter(id=transaction.id).exists())
def test_transaction_with_exchange_currency(self):
"""Test transaction with exchange currency"""
eur = Currency.objects.create(
code="EUR", name="Euro", decimal_places=2, prefix="", owner=self.user
)
self.account.exchange_currency = eur
self.account.save()
# Create exchange rate
ExchangeRate.objects.create(
from_currency=self.currency,
to_currency=eur,
rate=Decimal("0.85"),
date=timezone.now().date(), # Ensure date matches transaction or is general
owner=self.user
)
transaction = Transaction.objects.create(
account=self.account,
owner=self.user,
type=Transaction.Type.EXPENSE,
date=timezone.now().date(),
amount=Decimal("100.00"),
description="Test transaction",
)
exchanged = transaction.exchanged_amount()
self.assertIsNotNone(exchanged)
self.assertEqual(exchanged["prefix"], "")
# Depending on exact conversion logic, you might want to check the amount too
# self.assertEqual(exchanged["amount"], Decimal("85.00"))
def test_truncating_amount(self):
"""Test amount truncating based on account.currency decimal places"""
transaction = Transaction.objects.create(
account=self.account,
type=Transaction.Type.EXPENSE,
date=timezone.now().date(),
amount=Decimal(
"100.0100001"
), # account currency has two decimal places, the last 1 should be removed
description="Test transaction",
)
self.assertEqual(transaction.amount, Decimal("100.0100000"))
def test_automatic_reference_date(self):
"""Test reference_date from date"""
transaction = Transaction.objects.create(
account=self.account,
owner=self.user,
type=Transaction.Type.EXPENSE,
date=datetime.datetime(day=20, month=1, year=2000).date(),
amount=Decimal("100"),
description="Test transaction",
)
self.assertEqual(
transaction.reference_date,
datetime.datetime(day=1, month=1, year=2000).date(),
)
def test_reference_date_is_always_on_first_day(self):
"""Test reference_date is always on the first day"""
transaction = Transaction.objects.create(
account=self.account,
owner=self.user,
type=Transaction.Type.EXPENSE,
date=datetime.datetime(day=20, month=1, year=2000).date(),
reference_date=datetime.datetime(day=20, month=2, year=2000).date(),
amount=Decimal("100"),
description="Test transaction",
)
self.assertEqual(
transaction.reference_date,
datetime.datetime(day=1, month=2, year=2000).date(),
)
def test_transaction_transfer_view(self):
other_account = Account.objects.create(
name="Other Account", group=self.account_group, currency=self.currency, owner=self.user
)
data = {
"from_account": self.account.id,
"to_account": other_account.id,
"from_amount": "100.00",
"to_amount": "100.00", # Assuming same currency for simplicity
"date": timezone.now().date().isoformat(),
"description": "Test Transfer",
}
response = self.client.post(reverse("transactions_transfer"), data)
self.assertEqual(response.status_code, 204)
self.assertTrue(
Transaction.objects.filter(account=self.account, type=Transaction.Type.EXPENSE, amount="100.00").exists()
)
self.assertTrue(
Transaction.objects.filter(account=other_account, type=Transaction.Type.INCOME, amount="100.00").exists()
)
def test_transaction_bulk_edit_view(self):
t1 = Transaction.objects.create(
account=self.account, owner=self.user, type=Transaction.Type.EXPENSE,
date=timezone.now().date(), amount=Decimal("10.00"), description="Bulk 1"
)
t2 = Transaction.objects.create(
account=self.account, owner=self.user, type=Transaction.Type.EXPENSE,
date=timezone.now().date(), amount=Decimal("20.00"), description="Bulk 2"
)
new_category = TransactionCategory.objects.create(name="Bulk Category", owner=self.user)
data = {
"transactions": [t1.id, t2.id],
"category": new_category.id,
"is_paid": "true", # NullBoolean can be 'true', 'false', or empty for no change
}
response = self.client.post(reverse("transactions_bulk_edit"), data)
self.assertEqual(response.status_code, 204)
t1.refresh_from_db()
t2.refresh_from_db()
self.assertEqual(t1.category, new_category)
self.assertEqual(t2.category, new_category)
self.assertTrue(t1.is_paid)
self.assertTrue(t2.is_paid)
class InstallmentPlanTests(BaseTransactionAppTest): # Inherit from BaseTransactionAppTest
def setUp(self):
super().setUp() # Call BaseTransactionAppTest's setUp
# self.currency and self.account are available from base
self.category = TransactionCategory.objects.create(name="Installments", owner=self.user)
def test_installment_plan_creation_and_transaction_generation(self):
"""Test basic installment plan creation and its transaction generation."""
start_date = timezone.now().date()
plan = InstallmentPlan.objects.create(
account=self.account,
owner=self.user,
type=Transaction.Type.EXPENSE,
description="Test Plan",
number_of_installments=3,
start_date=start_date,
installment_amount=Decimal("100.00"),
recurrence=InstallmentPlan.Recurrence.MONTHLY,
category=self.category,
)
plan.create_transactions() # Manually call as it's not in save in the form
self.assertEqual(plan.transactions.count(), 3)
first_transaction = plan.transactions.order_by('date').first()
self.assertEqual(first_transaction.amount, Decimal("100.00"))
self.assertEqual(first_transaction.date, start_date)
self.assertEqual(first_transaction.category, self.category)
def test_installment_plan_update_transactions(self):
start_date = timezone.now().date()
plan = InstallmentPlan.objects.create(
account=self.account, owner=self.user, type=Transaction.Type.EXPENSE,
description="Initial Plan", number_of_installments=2, start_date=start_date,
installment_amount=Decimal("50.00"), recurrence=InstallmentPlan.Recurrence.MONTHLY,
)
plan.create_transactions()
self.assertEqual(plan.transactions.count(), 2)
plan.description = "Updated Plan Description"
plan.installment_amount = Decimal("60.00")
plan.number_of_installments = 3 # Increase installments
plan.save() # This should trigger _calculate_end_date and _calculate_installment_total_number
plan.update_transactions() # Manually call as it's not in save in the form
self.assertEqual(plan.transactions.count(), 3)
updated_transaction = plan.transactions.order_by('date').first()
self.assertEqual(updated_transaction.description, "Updated Plan Description")
# Amount should not change if already paid, but these are created as unpaid
self.assertEqual(updated_transaction.amount, Decimal("60.00"))
def test_installment_plan_delete_with_transactions(self):
plan = InstallmentPlan.objects.create(
account=self.account, owner=self.user, type=Transaction.Type.EXPENSE,
description="Plan to Delete", number_of_installments=2, start_date=timezone.now().date(),
installment_amount=Decimal("25.00"), recurrence=InstallmentPlan.Recurrence.MONTHLY,
)
plan.create_transactions()
plan_id = plan.id
self.assertTrue(Transaction.objects.filter(installment_plan_id=plan_id).exists())
plan.delete() # This should also delete related transactions as per model's delete
self.assertFalse(InstallmentPlan.all_objects.filter(id=plan_id).exists())
self.assertFalse(Transaction.all_objects.filter(installment_plan_id=plan_id).exists())
class RecurringTransactionTests(BaseTransactionAppTest): # Inherit
def setUp(self):
super().setUp()
self.category = TransactionCategory.objects.create(name="Recurring Category", owner=self.user)
def test_recurring_transaction_creation_and_upcoming_generation(self):
"""Test basic recurring transaction creation and initial upcoming transaction generation."""
start_date = timezone.now().date()
recurring = RecurringTransaction.objects.create(
account=self.account,
owner=self.user,
type=Transaction.Type.INCOME,
amount=Decimal("200.00"),
description="Monthly Salary",
start_date=start_date,
recurrence_type=RecurringTransaction.RecurrenceType.MONTH,
recurrence_interval=1,
category=self.category,
)
recurring.create_upcoming_transactions() # Manually call
# It should create a few transactions (e.g., for next 5 occurrences or up to end_date)
self.assertTrue(recurring.transactions.count() > 0)
first_upcoming = recurring.transactions.order_by('date').first()
self.assertEqual(first_upcoming.amount, Decimal("200.00"))
self.assertEqual(first_upcoming.date, start_date) # First one should be on start_date
self.assertFalse(first_upcoming.is_paid)
def test_recurring_transaction_update_unpaid(self):
recurring = RecurringTransaction.objects.create(
account=self.account, owner=self.user, type=Transaction.Type.EXPENSE,
amount=Decimal("30.00"), description="Subscription", start_date=timezone.now().date(),
recurrence_type=RecurringTransaction.RecurrenceType.MONTH, recurrence_interval=1
)
recurring.create_upcoming_transactions()
unpaid_transaction = recurring.transactions.filter(is_paid=False).first()
self.assertIsNotNone(unpaid_transaction)
recurring.amount = Decimal("35.00")
recurring.description = "Updated Subscription"
recurring.save()
recurring.update_unpaid_transactions() # Manually call
unpaid_transaction.refresh_from_db()
self.assertEqual(unpaid_transaction.amount, Decimal("35.00"))
self.assertEqual(unpaid_transaction.description, "Updated Subscription")
def test_recurring_transaction_delete_unpaid(self):
recurring = RecurringTransaction.objects.create(
account=self.account, owner=self.user, type=Transaction.Type.EXPENSE,
amount=Decimal("40.00"), description="Service Fee", start_date=timezone.now().date() + timedelta(days=5), # future start
recurrence_type=RecurringTransaction.RecurrenceType.MONTH, recurrence_interval=1
)
recurring.create_upcoming_transactions()
self.assertTrue(recurring.transactions.filter(is_paid=False).exists())
recurring.delete_unpaid_transactions() # Manually call
# This method in the model deletes transactions with date > today
self.assertFalse(recurring.transactions.filter(is_paid=False, date__gt=timezone.now().date()).exists())