diff --git a/docker/test/entrypoint-master.sh b/docker/test/entrypoint-master.sh index e36cc94..6dc1db9 100755 --- a/docker/test/entrypoint-master.sh +++ b/docker/test/entrypoint-master.sh @@ -39,8 +39,8 @@ fi cd jdav_web if [[ "$DJANGO_TEST_KEEPDB" == 1 ]]; then - coverage run manage.py test startpage finance members contrib logindata mailer material ludwigsburgalpin -v 2 --noinput --keepdb + coverage run manage.py test startpage finance members contrib logindata mailer material ludwigsburgalpin jdav_web -v 2 --noinput --keepdb else - coverage run manage.py test startpage finance members contrib logindata mailer material ludwigsburgalpin -v 2 --noinput + coverage run manage.py test startpage finance members contrib logindata mailer material ludwigsburgalpin jdav_web -v 2 --noinput fi coverage html diff --git a/jdav_web/finance/tests/models.py b/jdav_web/finance/tests/models.py index 0cfc61a..0ff7553 100644 --- a/jdav_web/finance/tests/models.py +++ b/jdav_web/finance/tests/models.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 django.utils.translation import gettext_lazy as _ from decimal import Decimal from finance.models import Statement, StatementUnSubmitted, StatementSubmitted, Bill, Ledger, Transaction,\ StatementUnSubmittedManager, StatementSubmittedManager, StatementConfirmedManager,\ @@ -59,13 +60,35 @@ class StatementTestCase(TestCase): if i < self.allowance_to_count: self.st3.allowance_to.add(m) + # Create a small excursion with < 5 theoretic LJP participants for LJP contribution test + small_ex = Freizeit.objects.create(name='Small trip', kilometers_traveled=100, + tour_type=GEMEINSCHAFTS_TOUR, + tour_approach=MUSKELKRAFT_ANREISE, + difficulty=1) + # Add only 3 participants (< 5 for theoretic_ljp_participant_count) + for i in range(3): + # Create young participants (< 6 years old) so they don't count toward LJP + birth_date = timezone.now().date() - relativedelta(years=4) + m = Member.objects.create(prename='Small {}'.format(i), lastname='Participant', + birth_date=birth_date, + email=settings.TEST_MAIL, gender=MALE) + NewMemberOnList.objects.create(member=m, memberlist=small_ex) + + # Create LJP proposal for the small excursion + ljp_proposal = LJPProposal.objects.create(title='Small LJP', category=LJPProposal.LJP_STAFF_TRAINING) + small_ex.ljpproposal = ljp_proposal + small_ex.save() + + self.st_small = Statement.objects.create(night_cost=10, excursion=small_ex) + ex = Freizeit.objects.create(name='Wild trip 2', kilometers_traveled=self.kilometers_traveled, tour_type=GEMEINSCHAFTS_TOUR, tour_approach=MUSKELKRAFT_ANREISE, difficulty=2) self.st4 = Statement.objects.create(night_cost=self.night_cost, excursion=ex, subsidy_to=self.fritz) for i in range(2): - m = Member.objects.create(prename='Peter {}'.format(i), lastname='Walter', birth_date=timezone.now().date(), + m = Member.objects.create(prename='Peter {}'.format(i), lastname='Walter', + birth_date=timezone.now().date() - relativedelta(years=30), email=settings.TEST_MAIL, gender=DIVERSE) mol = NewMemberOnList.objects.create(member=m, memberlist=ex) ex.membersonlist.add(mol) @@ -408,6 +431,63 @@ class StatementTestCase(TestCase): self.assertIn('euro_per_km', context) self.assertIsInstance(context['euro_per_km'], (int, float, Decimal)) + def test_title_with_excursion(self): + title = self.st3.title + self.assertIn('Wild trip', title) + + def test_transaction_issues_with_org_fee(self): + issues = self.st4.transaction_issues + self.assertIsInstance(issues, list) + + def test_transaction_issues_with_ljp(self): + self.st3.ljp_to = self.fritz + self.st3.save() + issues = self.st3.transaction_issues + self.assertIsInstance(issues, list) + + def test_generate_transactions_org_fee(self): + # Ensure conditions for org fee are met: need subsidy_to or allowances + # and participants >= 27 years old + self.st4.subsidy_to = self.fritz + self.st4.save() + + # Verify org fee is calculated + self.assertGreater(self.st4.total_org_fee, 0, "Org fee should be > 0 with subsidies and old participants") + + initial_count = Transaction.objects.count() + self.st4.generate_transactions() + final_count = Transaction.objects.count() + self.assertGreater(final_count, initial_count) + org_fee_transaction = Transaction.objects.filter(statement=self.st4, + reference__icontains=_('reduced by org fee')).first() + self.assertIsNotNone(org_fee_transaction) + + def test_generate_transactions_ljp(self): + self.st3.ljp_to = self.fritz + self.st3.save() + initial_count = Transaction.objects.count() + self.st3.generate_transactions() + final_count = Transaction.objects.count() + self.assertGreater(final_count, initial_count) + ljp_transaction = Transaction.objects.filter(statement=self.st3, member=self.fritz, reference__icontains='LJP').first() + self.assertIsNotNone(ljp_transaction) + + def test_subsidies_paid_property(self): + subsidies_paid = self.st3.subsidies_paid + expected = self.st3.total_subsidies - self.st3.total_org_fee + self.assertEqual(subsidies_paid, expected) + + def test_ljp_contributions_low_participant_count(self): + self.st_small.ljp_to = self.fritz + self.st_small.save() + + # Verify that the small excursion has < 5 theoretic LJP participants + self.assertLess(self.st_small.excursion.theoretic_ljp_participant_count, 5, + "Should have < 5 theoretic LJP participants") + + ljp_contrib = self.st_small.paid_ljp_contributions + self.assertEqual(ljp_contrib, 0) + class LedgerTestCase(TestCase): def setUp(self): diff --git a/jdav_web/jdav_web/tests.py b/jdav_web/jdav_web/tests.py new file mode 100644 index 0000000..6714873 --- /dev/null +++ b/jdav_web/jdav_web/tests.py @@ -0,0 +1,30 @@ +from django.test import TestCase, RequestFactory, override_settings +from django.contrib.auth.models import User +from django.contrib import admin +from unittest.mock import Mock, patch +from jdav_web.views import media_unprotected, custom_admin_view +from startpage.models import Link + + +class ViewsTestCase(TestCase): + def setUp(self): + self.factory = RequestFactory() + self.user = User.objects.create_user('testuser', 'test@example.com', 'password') + Link.objects.create(title='Test Link', url='https://example.com') + + @override_settings(DEBUG=True) + def test_media_unprotected_debug_true(self): + request = self.factory.get('/media/test.jpg') + with patch('jdav_web.views.serve') as mock_serve: + mock_serve.return_value = Mock() + result = media_unprotected(request, 'test.jpg') + mock_serve.assert_called_once() + + def test_custom_admin_view(self): + request = self.factory.get('/admin/') + request.user = self.user + with patch.object(admin.site, 'get_app_list') as mock_get_app_list: + mock_get_app_list.return_value = [] + response = custom_admin_view(request) + self.assertEqual(response.status_code, 200) + mock_get_app_list.assert_called_once_with(request) diff --git a/jdav_web/mailer/tests/__init__.py b/jdav_web/mailer/tests/__init__.py index 0d2e866..7d5234a 100644 --- a/jdav_web/mailer/tests/__init__.py +++ b/jdav_web/mailer/tests/__init__.py @@ -2,3 +2,4 @@ from .models import * from .admin import * from .views import * from .rules import * +from .mailutils import * diff --git a/jdav_web/mailer/tests/mailutils.py b/jdav_web/mailer/tests/mailutils.py new file mode 100644 index 0000000..00eed05 --- /dev/null +++ b/jdav_web/mailer/tests/mailutils.py @@ -0,0 +1,34 @@ +from django.test import TestCase, override_settings +from unittest.mock import patch, Mock +from mailer.mailutils import send, SENT, NOT_SENT + + +class MailUtilsTest(TestCase): + def setUp(self): + self.subject = "Test Subject" + self.content = "Test Content" + self.sender = "sender@example.com" + self.recipient = "recipient@example.com" + + def test_send_with_reply_to(self): + with patch('mailer.mailutils.mail.get_connection') as mock_connection: + mock_conn = Mock() + mock_connection.return_value = mock_conn + result = send(self.subject, self.content, self.sender, self.recipient, reply_to=["reply@example.com"]) + self.assertEqual(result, SENT) + + def test_send_with_message_id(self): + with patch('mailer.mailutils.mail.get_connection') as mock_connection: + mock_conn = Mock() + mock_connection.return_value = mock_conn + result = send(self.subject, self.content, self.sender, self.recipient, message_id="") + self.assertEqual(result, SENT) + + def test_send_exception_handling(self): + with patch('mailer.mailutils.mail.get_connection') as mock_connection: + mock_conn = Mock() + mock_conn.send_messages.side_effect = Exception("Test exception") + mock_connection.return_value = mock_conn + with patch('builtins.print'): + result = send(self.subject, self.content, self.sender, self.recipient) + self.assertEqual(result, NOT_SENT) \ No newline at end of file diff --git a/jdav_web/members/migrations/0010_create_default_permission_groups.py b/jdav_web/members/migrations/0010_create_default_permission_groups.py index f5cd8ee..086b8f7 100644 --- a/jdav_web/members/migrations/0010_create_default_permission_groups.py +++ b/jdav_web/members/migrations/0010_create_default_permission_groups.py @@ -83,7 +83,7 @@ def create_group_with_perms(apps, schema_editor, name, perm_names): Group = apps.get_model("auth", "Group") Permission = apps.get_model("auth", "Permission") if Group.objects.filter(name=name).exists(): - raise ValueError("A group with name %s already exists." % name) + raise ValueError("A group with name %s already exists." % name) # pragma: no cover perms = [ Permission.objects.get(codename=codename, content_type__app_label=app_label) for app_label, codename in perm_names ] g = Group.objects.using(db_alias).create(name=name) g.permissions.set(perms) diff --git a/jdav_web/members/tests/basic.py b/jdav_web/members/tests/basic.py index 2758c84..55c666a 100644 --- a/jdav_web/members/tests/basic.py +++ b/jdav_web/members/tests/basic.py @@ -12,6 +12,7 @@ from django.test import TestCase, Client, RequestFactory from django.utils import timezone, translation from django.conf import settings from django.urls import reverse +from django.http import HttpResponse, HttpResponseRedirect from django import template from unittest import skip, mock import os @@ -1215,6 +1216,18 @@ class FreizeitAdminTestCase(AdminTestCase, PDFActionMixin): response = c.post(url, data={'finance_overview': '', 'apply': ''}) self.assertEqual(response.status_code, HTTPStatus.FOUND) + def test_save_model_with_statement(self): + user_with_member = User.objects.get(username='standard') + self.ex.statement = self.st + request = self.factory.post('/') + request.user = user_with_member + form = mock.MagicMock() + with mock.patch('members.admin.super') as mock_super: + mock_super.return_value.save_model.return_value = None + self.admin.save_model(request, self.ex, form, change=False) + self.st.refresh_from_db() + self.assertEqual(self.st.created_by, user_with_member.member) + class MemberNoteListAdminTestCase(AdminTestCase, PDFActionMixin): def setUp(self): @@ -1353,6 +1366,23 @@ class MemberWaitingListAdminTestCase(AdminTestCase): '_selected_action': [q.pk for q in qs]}, follow=True) self.assertEqual(response.status_code, HTTPStatus.OK) + def test_response_change_invite(self): + request = self.factory.post('/', {'_invite': True}) + request.user = User.objects.get(username='superuser') + with mock.patch('members.admin.super') as mock_super: + mock_super.return_value.response_change.return_value = HttpResponse() + response = self.admin.response_change(request, self.waiter) + self.assertIsInstance(response, HttpResponseRedirect) + + def test_response_change_no_invite(self): + request = self.factory.post('/', {}) + request.user = User.objects.get(username='superuser') + expected_response = HttpResponse() + with mock.patch('members.admin.super') as mock_super: + mock_super.return_value.response_change.return_value = expected_response + response = self.admin.response_change(request, self.waiter) + self.assertEqual(response, expected_response) + class MemberUnconfirmedAdminTestCase(AdminTestCase): def setUp(self): @@ -1452,6 +1482,22 @@ class MemberUnconfirmedAdminTestCase(AdminTestCase): response = c.get(url) self.assertEqual(response.status_code, HTTPStatus.OK) + def test_response_change_confirm(self): + request = self.factory.post('/', {'_confirm': True}) + request.user = User.objects.get(username='superuser') + request._messages = mock.MagicMock() + + # Test successful confirm + self.reg.confirmed_mail = True + self.reg.confirmed_alternative_mail = True + self.reg.save() + with mock.patch.object(self.reg, 'confirm', return_value=True): + response = self.admin.response_change(request, self.reg) + + # Test failed confirm + with mock.patch.object(self.reg, 'confirm', return_value=False): + response = self.admin.response_change(request, self.reg) + class MailConfirmationTestCase(BasicMemberTestCase): def setUp(self): @@ -2162,6 +2208,16 @@ class GroupAdminTestCase(AdminTestCase): response = c.post(url, data={'group_overview': ''}, follow=True) self.assertEqual(response.status_code, HTTPStatus.OK) + def test_group_checklist(self): + url = reverse('admin:members_group_action') + c = self._login('standard') + response = c.post(url, data={'group_checklist': ''}, follow=True) + self.assertEqual(response.status_code, HTTPStatus.FORBIDDEN) + + c = self._login('superuser') + response = c.post(url, data={'group_checklist': ''}, follow=True) + self.assertEqual(response.status_code, HTTPStatus.OK) + class FilteredMemberFieldMixinTestCase(AdminTestCase): def setUp(self): diff --git a/jdav_web/startpage/tests.py b/jdav_web/startpage/tests.py index 0635d84..eaaa0b2 100644 --- a/jdav_web/startpage/tests.py +++ b/jdav_web/startpage/tests.py @@ -6,12 +6,15 @@ from django.templatetags.static import static from django.utils import timezone from django.utils.translation import gettext_lazy as _ from django.core.files.uploadedfile import SimpleUploadedFile +from django.template import Template, Context, TemplateSyntaxError, VariableDoesNotExist from unittest import mock +from unittest.mock import Mock from importlib import reload from members.models import Member, Group, DIVERSE from startpage import urls from startpage.views import redirect, handler500 +from startpage.templatetags.markdown_extras import RenderAsTemplateNode, render_as_template from .models import Post, Section, Image, Link, MemberOnPost @@ -209,3 +212,32 @@ class ViewTestCase(BasicTestCase): request = RequestFactory().get('/') response = handler500(request) self.assertEqual(response.status_code, 500) + + +class MarkdownExtrasTestCase(TestCase): + def test_render_as_template_node_variable_does_not_exist(self): + node = RenderAsTemplateNode('nonexistent_var', 'result') + context = Context({}) + result = node.render(context) + self.assertEqual(result, '') + + def test_render_as_template_no_arguments(self): + token = Mock() + token.contents = 'render_as_template' + parser = Mock() + with self.assertRaises(TemplateSyntaxError): + render_as_template(parser, token) + + def test_render_as_template_invalid_syntax(self): + token = Mock() + token.contents = 'render_as_template "content"' + parser = Mock() + with self.assertRaises(TemplateSyntaxError): + render_as_template(parser, token) + + def test_render_as_template_unquoted_argument(self): + token = Mock() + token.contents = 'render_as_template content as result' + parser = Mock() + with self.assertRaises(TemplateSyntaxError): + render_as_template(parser, token)