diff --git a/jdav_web/contrib/tests.py b/jdav_web/contrib/tests.py index 7ce503c..41e3726 100644 --- a/jdav_web/contrib/tests.py +++ b/jdav_web/contrib/tests.py @@ -1,3 +1,56 @@ from django.test import TestCase +from django.contrib.auth import get_user_model +from contrib.models import CommonModel +from contrib.rules import has_global_perm -# Create your tests here. +User = get_user_model() + +class CommonModelTestCase(TestCase): + def test_common_model_abstract_base(self): + """Test that CommonModel provides the correct meta attributes""" + meta = CommonModel._meta + self.assertTrue(meta.abstract) + expected_permissions = ( + 'add_global', 'change_global', 'view_global', 'delete_global', 'list_global', 'view', + ) + self.assertEqual(meta.default_permissions, expected_permissions) + + def test_common_model_inheritance(self): + """Test that CommonModel has rules mixin functionality""" + # Test that CommonModel has the expected functionality + # Since it's abstract, we can't instantiate it directly + # but we can check its metaclass and mixins + from rules.contrib.models import RulesModelMixin, RulesModelBase + + self.assertTrue(issubclass(CommonModel, RulesModelMixin)) + self.assertEqual(CommonModel.__class__, RulesModelBase) + + +class GlobalPermissionRulesTestCase(TestCase): + def setUp(self): + self.user = User.objects.create_user( + username='testuser', + email='test@example.com', + password='testpass123' + ) + + def test_has_global_perm_predicate_creation(self): + """Test that has_global_perm creates a predicate function""" + # has_global_perm is a decorator factory, not a direct predicate + predicate = has_global_perm('auth.add_user') + self.assertTrue(callable(predicate)) + + def test_has_global_perm_with_superuser(self): + """Test that superusers have global permissions""" + self.user.is_superuser = True + self.user.save() + + predicate = has_global_perm('auth.add_user') + result = predicate(self.user, None) + self.assertTrue(result) + + def test_has_global_perm_with_regular_user(self): + """Test that regular users don't automatically have global permissions""" + predicate = has_global_perm('auth.add_user') + result = predicate(self.user, None) + self.assertFalse(result) diff --git a/jdav_web/finance/tests.py b/jdav_web/finance/tests.py index 949b124..b9f5eea 100644 --- a/jdav_web/finance/tests.py +++ b/jdav_web/finance/tests.py @@ -2,6 +2,7 @@ from unittest import skip from django.test import TestCase from django.utils import timezone from django.conf import settings +from decimal import Decimal from .models import Statement, StatementUnSubmitted, StatementSubmitted, Bill, Ledger, Transaction,\ StatementUnSubmittedManager, StatementSubmittedManager, StatementConfirmedManager,\ StatementConfirmed, TransactionIssue, StatementManager @@ -67,115 +68,115 @@ class StatementTestCase(TestCase): email=settings.TEST_MAIL, gender=DIVERSE) mol = NewMemberOnList.objects.create(member=m, memberlist=ex) ex.membersonlist.add(mol) - + base = timezone.now() ex = Freizeit.objects.create(name='Wild trip with old people', kilometers_traveled=self.kilometers_traveled, tour_type=GEMEINSCHAFTS_TOUR, tour_approach=MUSKELKRAFT_ANREISE, difficulty=2, date=timezone.datetime(2024, 1, 2, 8, 0, 0, tzinfo=base.tzinfo), end=timezone.datetime(2024, 1, 5, 17, 0, 0, tzinfo=base.tzinfo) ) - + settings.EXCURSION_ORG_FEE = 20 settings.LJP_TAX = 0.2 settings.LJP_CONTRIBUTION_PER_DAY = 20 - + self.st5 = Statement.objects.create(night_cost=self.night_cost, excursion=ex) - + for i in range(9): m = Member.objects.create(prename='Peter {}'.format(i), lastname='Walter', birth_date=timezone.now().date() - relativedelta(years=i+21), email=settings.TEST_MAIL, gender=DIVERSE) mol = NewMemberOnList.objects.create(member=m, memberlist=ex) - ex.membersonlist.add(mol) - + ex.membersonlist.add(mol) + ljpproposal = LJPProposal.objects.create( - title='Test proposal', + title='Test proposal', category=LJPProposal.LJP_STAFF_TRAINING, goal=LJPProposal.LJP_ENVIRONMENT, goal_strategy='my strategy', not_bw_reason=LJPProposal.NOT_BW_ROOMS, excursion=self.st5.excursion) - + for i in range(3): int = Intervention.objects.create( - date_start=timezone.datetime(2024, 1, 2+i, 12, 0, 0, tzinfo=base.tzinfo), - duration = 2+i, + date_start=timezone.datetime(2024, 1, 2+i, 12, 0, 0, tzinfo=base.tzinfo), + duration = 2+i, activity = 'hi', ljp_proposal=ljpproposal ) - + self.b1 = Bill.objects.create( - statement=self.st5, - short_description='covered bill', - explanation='hi', - amount='300', - paid_by=self.fritz, - costs_covered=True, + statement=self.st5, + short_description='covered bill', + explanation='hi', + amount='300', + paid_by=self.fritz, + costs_covered=True, refunded=False ) self.b2 = Bill.objects.create( - statement=self.st5, - short_description='non-covered bill', - explanation='hi', - amount='900', - paid_by=self.fritz, - costs_covered=False, + statement=self.st5, + short_description='non-covered bill', + explanation='hi', + amount='900', + paid_by=self.fritz, + costs_covered=False, refunded=False ) - + def test_org_fee(self): # org fee should be collected if participants are older than 26 self.assertEqual(self.st5.excursion.old_participant_count, 3, 'Calculation of number of old people in excursion is incorrect.') - + total_org = 4 * 3 * 20 # 4 days, 3 old people, 20€ per day - + self.assertEqual(self.st5.total_org_fee_theoretical, total_org, 'Theoretical org_fee should equal to amount per day per person * n_persons * n_days if there are old people.') self.assertEqual(self.st5.total_org_fee, 0, 'Paid org fee should be 0 if no allowance and subsidies are paid if there are old people.') - + self.assertIsNone(self.st5.org_fee_payant) - + # now collect subsidies self.st5.subsidy_to = self.fritz self.assertEqual(self.st5.total_org_fee, total_org, 'Paid org fee should equal to amount per day per person * n_persons * n_days if subsidies are paid.') - + # now collect allowances self.st5.allowance_to.add(self.fritz) self.st5.subsidy_to = None self.assertEqual(self.st5.total_org_fee, total_org, 'Paid org fee should equal to amount per day per person * n_persons * n_days if allowances are paid.') - + # now collect both self.st5.subsidy_to = self.fritz self.assertEqual(self.st5.total_org_fee, total_org, 'Paid org fee should equal to amount per day per person * n_persons * n_days if subsidies and allowances are paid.') - + self.assertEqual(self.st5.org_fee_payant, self.fritz, 'Org fee payant should be the receiver allowances and subsidies.') # return to previous state self.st5.subsidy_to = None self.st5.allowance_to.remove(self.fritz) - - + + def test_ljp_payment(self): - + expected_intervention_hours = 2 + 3 + 4 expected_seminar_days = 0 + 0.5 + 0.5 # >=2.5h = 0.5days, >=5h = 1.0day - expected_ljp = (1-settings.LJP_TAX) * expected_seminar_days * settings.LJP_CONTRIBUTION_PER_DAY * 9 - # (1 - 20% tax) * 1 seminar day * 20€ * 9 participants - + expected_ljp = (1-settings.LJP_TAX) * expected_seminar_days * settings.LJP_CONTRIBUTION_PER_DAY * 9 + # (1 - 20% tax) * 1 seminar day * 20€ * 9 participants + self.assertEqual(self.st5.excursion.total_intervention_hours, expected_intervention_hours, 'Calculation of total intervention hours is incorrect.') self.assertEqual(self.st5.excursion.total_seminar_days, expected_seminar_days, 'Calculation of total seminar days is incorrect.') - + self.assertEqual(self.st5.paid_ljp_contributions, 0, 'No LJP contributions should be paid if no receiver is set.') - + # now we want to pay out the LJP contributions self.st5.ljp_to = self.fritz self.assertEqual(self.st5.paid_ljp_contributions, expected_ljp, 'LJP contributions should be paid if a receiver is set.') - + # now the total costs paid by trip organisers is lower than expected ljp contributions, should be reduced automatically self.b2.amount=100 self.b2.save() - + self.assertEqual(self.st5.total_bills_not_covered, 100, 'Changes in bills should be reflected in the total costs paid by trip organisers') self.assertGreaterEqual(self.st5.total_bills_not_covered, self.st5.paid_ljp_contributions, 'LJP contributions should be less than or equal to the costs paid by trip organisers') - + self.st5.ljp_to = None def test_staff_count(self): @@ -371,6 +372,41 @@ class StatementTestCase(TestCase): bills = self.st2.grouped_bills() self.assertTrue('amount' in bills[0]) + def test_euro_per_km_no_excursion(self): + """Test euro_per_km when no excursion is associated""" + statement = Statement.objects.create( + short_description="Test Statement", + explanation="Test explanation", + night_cost=25 + ) + self.assertEqual(statement.euro_per_km, 0) + + def test_submit_workflow(self): + """Test statement submission workflow""" + statement = Statement.objects.create( + short_description="Test Statement", + explanation="Test explanation", + night_cost=25, + created_by=self.fritz + ) + + self.assertFalse(statement.submitted) + self.assertIsNone(statement.submitted_by) + self.assertIsNone(statement.submitted_date) + + # Test submission - submit method doesn't return a value, just changes state + statement.submit(submitter=self.fritz) + self.assertTrue(statement.submitted) + self.assertEqual(statement.submitted_by, self.fritz) + self.assertIsNotNone(statement.submitted_date) + + def test_template_context_with_excursion(self): + """Test statement template context when excursion is present""" + # Use existing excursion from setUp + context = self.st3.template_context() + self.assertIn('euro_per_km', context) + self.assertIsInstance(context['euro_per_km'], (int, float, Decimal)) + class LedgerTestCase(TestCase): def setUp(self): @@ -431,9 +467,20 @@ class TransactionTestCase(TestCase): self.assertTrue(str(self.trans.pk) in str(self.trans)) def test_escape_reference(self): - self.assertEqual(Transaction.escape_reference('harmless'), 'harmless') - self.assertEqual(Transaction.escape_reference('äöüÄÖÜß'), 'aeoeueAeOeUess') - self.assertEqual(Transaction.escape_reference('ha@r!?mless+09'), 'har?mless+09') + """Test transaction reference escaping with various special characters""" + test_cases = [ + ('harmless', 'harmless'), + ('äöüÄÖÜß', 'aeoeueAeOeUess'), + ('ha@r!?mless+09', 'har?mless+09'), + ("simple", "simple"), + ("test@email.com", "testemail.com"), + ("ref!with#special$chars%", "refwithspecialchars"), + ("normal_text-123", "normaltext-123"), # underscores are removed + ] + + for input_ref, expected in test_cases: + result = Transaction.escape_reference(input_ref) + self.assertEqual(result, expected) def test_code(self): self.trans.amount = 0 @@ -446,6 +493,35 @@ class TransactionTestCase(TestCase): self.fritz.iban = 'DE89370400440532013000' self.assertNotEqual(self.trans.code(), '') + def test_code_with_zero_amount(self): + """Test transaction code generation with zero amount""" + transaction = Transaction.objects.create( + reference="test-ref", + amount=Decimal('0.00'), + member=self.fritz, + ledger=self.personal_account, + statement=self.st + ) + + # Zero amount should return empty code + self.assertEqual(transaction.code(), '') + + def test_code_with_invalid_iban(self): + """Test transaction code generation with invalid IBAN""" + self.fritz.iban = "INVALID_IBAN" + self.fritz.save() + + transaction = Transaction.objects.create( + reference="test-ref", + amount=Decimal('100.00'), + member=self.fritz, + ledger=self.personal_account, + statement=self.st + ) + + # Invalid IBAN should return empty code + self.assertEqual(transaction.code(), '') + class BillTestCase(TestCase): def setUp(self): @@ -461,6 +537,30 @@ class BillTestCase(TestCase): def test_pretty_amount(self): self.assertTrue('€' in self.bill.pretty_amount()) + def test_pretty_amount_formatting(self): + """Test bill pretty_amount formatting with specific values""" + bill = Bill.objects.create( + statement=self.st, + short_description="Test Bill", + amount=Decimal('42.50') + ) + + pretty = bill.pretty_amount() + self.assertIn("42.50", pretty) + self.assertIn("€", pretty) + + def test_zero_amount(self): + """Test bill with zero amount""" + bill = Bill.objects.create( + statement=self.st, + short_description="Zero Bill", + amount=Decimal('0.00') + ) + + self.assertEqual(bill.amount, Decimal('0.00')) + pretty = bill.pretty_amount() + self.assertIn("0.00", pretty) + class TransactionIssueTestCase(TestCase): def setUp(self): diff --git a/jdav_web/material/tests.py b/jdav_web/material/tests.py index 7ce503c..53ee4ec 100644 --- a/jdav_web/material/tests.py +++ b/jdav_web/material/tests.py @@ -1,3 +1,114 @@ from django.test import TestCase +from django.utils import timezone +from datetime import date +from decimal import Decimal +from material.models import MaterialCategory, MaterialPart, Ownership +from members.models import Member, MALE, FEMALE, DIVERSE -# Create your tests here. + +class MaterialCategoryTestCase(TestCase): + def setUp(self): + self.category = MaterialCategory.objects.create(name="Climbing Gear") + + def test_str(self): + """Test string representation of MaterialCategory""" + self.assertEqual(str(self.category), "Climbing Gear") + + def test_verbose_names(self): + """Test verbose names are set correctly""" + meta = MaterialCategory._meta + self.assertTrue(hasattr(meta, 'verbose_name')) + self.assertTrue(hasattr(meta, 'verbose_name_plural')) + + +class MaterialPartTestCase(TestCase): + def setUp(self): + self.category = MaterialCategory.objects.create(name="Ropes") + self.material_part = MaterialPart.objects.create( + name="Dynamic Rope 10mm", + description="60m dynamic climbing rope", + quantity=5, + buy_date=date(2020, 1, 15), + lifetime=Decimal('8') + ) + self.material_part.material_cat.add(self.category) + + self.member = Member.objects.create( + prename="John", + lastname="Doe", + birth_date=date(1990, 1, 1), + email="john@example.com", + gender=MALE + ) + + def test_str(self): + """Test string representation of MaterialPart""" + self.assertEqual(str(self.material_part), "Dynamic Rope 10mm") + + def test_quantity_real_no_ownership(self): + """Test quantity_real when no ownership exists""" + result = self.material_part.quantity_real() + self.assertEqual(result, "0/5") + + def test_quantity_real_with_ownership(self): + """Test quantity_real with ownership records""" + Ownership.objects.create( + material=self.material_part, + owner=self.member, + count=3 + ) + Ownership.objects.create( + material=self.material_part, + owner=self.member, + count=1 + ) + result = self.material_part.quantity_real() + self.assertEqual(result, "4/5") + + def test_verbose_names(self): + """Test field verbose names""" + # Just test that verbose names exist, since they might be translated + field_names = ['name', 'description', 'quantity', 'buy_date', 'lifetime', 'photo', 'material_cat'] + + for field_name in field_names: + field = self.material_part._meta.get_field(field_name) + self.assertTrue(hasattr(field, 'verbose_name')) + self.assertIsNotNone(field.verbose_name) + + +class OwnershipTestCase(TestCase): + def setUp(self): + self.category = MaterialCategory.objects.create(name="Hardware") + self.material_part = MaterialPart.objects.create( + name="Carabiner Set", + description="Lightweight aluminum carabiners", + quantity=10, + buy_date=date(2021, 6, 1), + lifetime=Decimal('10') + ) + + self.member = Member.objects.create( + prename="Alice", + lastname="Smith", + birth_date=date(1985, 3, 15), + email="alice@example.com", + gender=FEMALE + ) + + self.ownership = Ownership.objects.create( + material=self.material_part, + owner=self.member, + count=6 + ) + + def test_ownership_creation(self): + """Test ownership record creation""" + self.assertEqual(self.ownership.material, self.material_part) + self.assertEqual(self.ownership.owner, self.member) + self.assertEqual(self.ownership.count, 6) + + def test_material_part_relationship(self): + """Test relationship between MaterialPart and Ownership""" + ownerships = Ownership.objects.filter(material=self.material_part) + self.assertEqual(ownerships.count(), 1) + self.assertEqual(ownerships.first(), self.ownership)