From b21b975252c8659054629d4a0c14d87dd8b60d98 Mon Sep 17 00:00:00 2001 From: Christian Merten Date: Wed, 27 Aug 2025 10:25:21 +0200 Subject: [PATCH 1/7] chore(startpage/tests): test uncovered lines --- jdav_web/startpage/models.py | 2 +- jdav_web/startpage/tests.py | 51 +++++++++++++++++++++++++++++++----- 2 files changed, 46 insertions(+), 7 deletions(-) diff --git a/jdav_web/startpage/models.py b/jdav_web/startpage/models.py index d252b64..a04bd08 100644 --- a/jdav_web/startpage/models.py +++ b/jdav_web/startpage/models.py @@ -88,7 +88,7 @@ class Image(models.Model): max_upload_size=10) def __str__(self): - return os.path.basename(self.f.name) if self.f.name else _("Empty") + return os.path.basename(self.f.name) if self.f.name else str(_("Empty")) class Meta: verbose_name = _('image') diff --git a/jdav_web/startpage/tests.py b/jdav_web/startpage/tests.py index 94ab953..ea76e0d 100644 --- a/jdav_web/startpage/tests.py +++ b/jdav_web/startpage/tests.py @@ -1,8 +1,10 @@ +import os from django.test import TestCase, Client -from django.urls import reverse +from django.urls import reverse, NoReverseMatch from django.conf import settings 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 unittest import mock from importlib import reload @@ -10,7 +12,7 @@ from importlib import reload from members.models import Member, Group, DIVERSE from startpage import urls -from .models import Post, Section, Image +from .models import Post, Section, Image, Link, MemberOnPost class BasicTestCase(TestCase): @@ -25,7 +27,7 @@ class BasicTestCase(TestCase): file = SimpleUploadedFile("post_image.jpg", b"file_content", content_type="image/jpeg") staff_post = Post.objects.create(title='Staff', urlname='staff', website_text='This is our staff: Peter.', section=orga) - Image.objects.create(post=staff_post, f=file) + self.image_with_file = Image.objects.create(post=staff_post, f=file) file = SimpleUploadedFile("member_image.jpg", b"file_content", content_type="image/jpeg") m = Member.objects.create(prename='crazy', lastname='cool', birth_date=timezone.now().date(), email=settings.TEST_MAIL, gender=DIVERSE, @@ -39,6 +41,10 @@ class BasicTestCase(TestCase): crazy_post.groups.add(crazy_group) crazy_post.save() + self.post_no_section = Post.objects.create(title='No Section', urlname='no-section', section=None) + self.image_no_file = Image.objects.create(post=staff_post) + self.test_link = Link.objects.create(title='Test Link', url='https://example.com') + class ModelsTestCase(BasicTestCase): def test_str(self): @@ -66,6 +72,41 @@ class ModelsTestCase(BasicTestCase): '/de/{name}/last-trip'.format(name=settings.REPORTS_SECTION)) self.assertEqual(post3.absolute_urlname(), reverse('startpage:post', args=(reports.urlname, 'last-trip'))) + def test_post_absolute_section_none(self): + """Test Post.absolute_section when section is None""" + self.assertEqual(self.post_no_section.absolute_section(), 'Aktuelles') + + def test_post_absolute_urlname_no_section(self): + """Test Post.absolute_urlname when section is None""" + expected_url = reverse('startpage:post', args=('aktuelles', 'no-section')) + self.assertEqual(self.post_no_section.absolute_urlname(), expected_url) + + def test_image_str_without_file(self): + """Test Image.__str__ when no file is associated""" + self.assertEqual(str(self.image_no_file), str(_('Empty'))) + + def test_image_str_with_file(self): + """Test Image.__str__ when file is associated""" + # The str should return basename of the file + expected = os.path.basename(self.image_with_file.f.name) + self.assertEqual(str(self.image_with_file), expected) + + def test_link_str(self): + """Test Link.__str__ method""" + self.assertEqual(str(self.test_link), 'Test Link') + + def test_section_absolute_urlname_no_reverse_match(self): + """Test Section.absolute_urlname when NoReverseMatch occurs""" + section = Section.objects.get(urlname='orga') + with mock.patch('startpage.models.reverse', side_effect=NoReverseMatch): + self.assertEqual(section.absolute_urlname(), str(_('deactivated'))) + + def test_post_absolute_urlname_no_reverse_match(self): + """Test Post.absolute_urlname when NoReverseMatch occurs""" + post = Post.objects.get(urlname='staff') + with mock.patch('startpage.models.reverse', side_effect=NoReverseMatch): + self.assertEqual(post.absolute_urlname(), str(_('deactivated'))) + class ViewTestCase(BasicTestCase): def test_index(self): @@ -137,9 +178,7 @@ class ViewTestCase(BasicTestCase): def test_post_image(self): c = Client() - staff_post = Post.objects.get(urlname='staff') - img = Image.objects.get(post=staff_post) - url = img.f.url + url = self.image_with_file.f.url response = c.get('/de' + url) self.assertEqual(response.status_code, 200, 'Images on posts should be visible without login.') From 205b72d8e215f52579cd6eb840412062e8b35364 Mon Sep 17 00:00:00 2001 From: Christian Merten Date: Thu, 28 Aug 2025 11:04:56 +0200 Subject: [PATCH 2/7] chore(startpage/tests): cover missing views --- jdav_web/startpage/tests.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/jdav_web/startpage/tests.py b/jdav_web/startpage/tests.py index ea76e0d..0635d84 100644 --- a/jdav_web/startpage/tests.py +++ b/jdav_web/startpage/tests.py @@ -1,5 +1,5 @@ import os -from django.test import TestCase, Client +from django.test import TestCase, Client, RequestFactory from django.urls import reverse, NoReverseMatch from django.conf import settings from django.templatetags.static import static @@ -11,6 +11,7 @@ from importlib import reload from members.models import Member, Group, DIVERSE from startpage import urls +from startpage.views import redirect, handler500 from .models import Post, Section, Image, Link, MemberOnPost @@ -194,3 +195,17 @@ class ViewTestCase(BasicTestCase): url_names = [pattern.name for pattern in urls.urlpatterns if hasattr(pattern, 'name')] self.assertIn('index', url_names) self.assertEqual(len(urls.urlpatterns), 2) # Should have index and impressum only + + def test_redirect_view(self): + """Test redirect view functionality""" + request = RequestFactory().get('/') + with mock.patch.object(settings, 'STARTPAGE_REDIRECT_URL', 'https://example.com'): + response = redirect(request) + self.assertEqual(response.status_code, 302) + self.assertEqual(response.url, 'https://example.com') + + def test_handler500(self): + """Test custom 500 error handler""" + request = RequestFactory().get('/') + response = handler500(request) + self.assertEqual(response.status_code, 500) From e81efe0f83eaef4d9da3459967bc4ccf854384c2 Mon Sep 17 00:00:00 2001 From: Christian Merten Date: Thu, 28 Aug 2025 12:51:40 +0200 Subject: [PATCH 3/7] chore(contrib/tests): add missing utils tests --- jdav_web/contrib/tests.py | 88 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 85 insertions(+), 3 deletions(-) diff --git a/jdav_web/contrib/tests.py b/jdav_web/contrib/tests.py index 99493e1..45adca4 100644 --- a/jdav_web/contrib/tests.py +++ b/jdav_web/contrib/tests.py @@ -1,13 +1,18 @@ -from django.test import TestCase +from datetime import datetime, timedelta +from decimal import Decimal +from django.test import TestCase, RequestFactory from django.contrib.auth import get_user_model from django.contrib import admin from django.db import models -from django.test import RequestFactory -from unittest.mock import Mock +from django.core.exceptions import ValidationError +from django.core.files.uploadedfile import SimpleUploadedFile +from django.utils.translation import gettext_lazy as _ +from unittest.mock import Mock, patch from rules.contrib.models import RulesModelMixin, RulesModelBase from contrib.models import CommonModel from contrib.rules import has_global_perm from contrib.admin import CommonAdminMixin +from utils import file_size_validator, RestrictedFileField, cvt_to_decimal, get_member, normalize_name, normalize_filename, coming_midnight, mondays_until_nth User = get_user_model() @@ -91,3 +96,80 @@ class CommonAdminMixinTestCase(TestCase): # Verify that the formfield_overrides were used self.assertIsNotNone(result) + + +class UtilsTestCase(TestCase): + def setUp(self): + self.user = User.objects.create_user( + username='testuser', + email='test@example.com', + password='testpass123' + ) + + def test_file_size_validator_exceeds_limit(self): + """Test file_size_validator when file exceeds size limit""" + validator = file_size_validator(1) # 1MB limit + + # Create a mock file that exceeds the limit (2MB) + mock_file = Mock() + mock_file.size = 2 * 1024 * 1024 # 2MB + + with self.assertRaises(ValidationError) as cm: + validator(mock_file) + + # Check for the translated error message + expected_message = str(_('Please keep filesize under {} MiB. Current filesize: {:10.2f} MiB.').format(1, 2.00)) + self.assertIn(expected_message, str(cm.exception)) + + def test_restricted_file_field_content_type_not_supported(self): + """Test RestrictedFileField when content type is not supported""" + field = RestrictedFileField(content_types=['image/jpeg']) + + # Create mock data with unsupported content type + mock_data = Mock() + mock_data.file = Mock() + mock_data.file.content_type = "text/plain" + + # Mock the super().clean() to return our mock data + with patch.object(models.FileField, 'clean', return_value=mock_data): + with self.assertRaises(ValidationError) as cm: + field.clean("dummy") + + # Check for the translated error message + expected_message = str(_('Filetype not supported.')) + self.assertIn(expected_message, str(cm.exception)) + + def test_restricted_file_field_size_exceeds_limit(self): + """Test RestrictedFileField when file size exceeds limit""" + field = RestrictedFileField(max_upload_size=1) # 1 byte limit + + # Create mock data with file that exceeds size limit + mock_data = Mock() + mock_data.file = Mock() + mock_data.file.content_type = "text/plain" + mock_data.file._size = 2 # 2 bytes, exceeds limit + + # Mock the super().clean() to return our mock data + with patch.object(models.FileField, 'clean', return_value=mock_data): + with self.assertRaises(ValidationError) as cm: + field.clean("dummy") + + # Check for the translated error message + expected_message = str(_('Please keep filesize under {}. Current filesize: {}').format(1, 2)) + self.assertIn(expected_message, str(cm.exception)) + + def test_mondays_until_nth(self): + """Test mondays_until_nth function""" + # Test with n=2 to get 3 Mondays (including the 0th) + result = mondays_until_nth(2) + + # Should return a list of 3 dates + self.assertEqual(len(result), 3) + + # All dates should be Mondays (weekday 0) + for date in result: + self.assertEqual(date.weekday(), 0) # Monday is 0 + + # Dates should be consecutive weeks + self.assertEqual(result[1] - result[0], timedelta(days=7)) + self.assertEqual(result[2] - result[1], timedelta(days=7)) From a784957b7fd4d31132eae7e190517729adef93eb Mon Sep 17 00:00:00 2001 From: Christian Merten Date: Fri, 29 Aug 2025 02:33:19 +0200 Subject: [PATCH 4/7] chore(mailer/tests): remove unused method --- jdav_web/mailer/tests/admin.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/jdav_web/mailer/tests/admin.py b/jdav_web/mailer/tests/admin.py index 79032cf..52b0dab 100644 --- a/jdav_web/mailer/tests/admin.py +++ b/jdav_web/mailer/tests/admin.py @@ -38,13 +38,6 @@ class AdminTestCase(TestCase): standard = create_custom_user('standard', ['Standard'], 'Paul', 'Wulter') trainer = create_custom_user('trainer', ['Standard', 'Trainings'], 'Lise', 'Lotte') - def _login(self, name): - c = Client() - res = c.login(username=name, password='secret') - # make sure we logged in - assert res - return c - def _add_middleware(self, request): """Add required middleware to request.""" # Session middleware From c7eb07ddd17fb7592a4db933cc434a4baf271fc0 Mon Sep 17 00:00:00 2001 From: Christian Merten Date: Fri, 29 Aug 2025 02:39:28 +0200 Subject: [PATCH 5/7] chore(members/tests): cover templatetags --- jdav_web/members/tests/__init__.py | 1 + jdav_web/members/tests/templatetags.py | 55 ++++++++++++++++++++++++++ 2 files changed, 56 insertions(+) create mode 100644 jdav_web/members/tests/templatetags.py diff --git a/jdav_web/members/tests/__init__.py b/jdav_web/members/tests/__init__.py index 35a6867..4614a2f 100644 --- a/jdav_web/members/tests/__init__.py +++ b/jdav_web/members/tests/__init__.py @@ -2,3 +2,4 @@ from .basic import * from .views import * from .tasks import * from .rules import * +from .templatetags import * diff --git a/jdav_web/members/tests/templatetags.py b/jdav_web/members/tests/templatetags.py new file mode 100644 index 0000000..a8c414d --- /dev/null +++ b/jdav_web/members/tests/templatetags.py @@ -0,0 +1,55 @@ +from django.test import TestCase +from django.template import Context, Template +from datetime import datetime, date, timedelta +from members.templatetags.tex_extras import index, datetime_short, date_short, date_vs, time_short, add, plus + + +class TexExtrasTestCase(TestCase): + def setUp(self): + self.test_date = date(2024, 3, 15) + self.test_datetime = datetime(2024, 3, 15, 14, 30) + self.test_list = ['a', 'b', 'c'] + + def test_index_valid_position(self): + result = index(self.test_list, 1) + self.assertEqual(result, 'b') + + def test_index_invalid_position(self): + result = index(self.test_list, 5) + self.assertEqual(result, '') + + def test_index_type_error(self): + result = index(123, 1) + self.assertEqual(result, '') + + def test_datetime_short(self): + result = datetime_short(self.test_datetime) + self.assertEqual(result, '15.03.2024 14:30') + + def test_date_short(self): + result = date_short(self.test_date) + self.assertEqual(result, '15.03.24') + + def test_date_vs(self): + result = date_vs(self.test_date) + self.assertEqual(result, '15.03.') + + def test_time_short(self): + result = time_short(self.test_datetime) + self.assertEqual(result, '14:30') + + def test_add_with_days(self): + result = add(self.test_date, 5) + self.assertEqual(result, date(2024, 3, 20)) + + def test_add_without_days(self): + result = add(self.test_date, None) + self.assertEqual(result, self.test_date) + + def test_plus_with_second_number(self): + result = plus(10, 5) + self.assertEqual(result, 15) + + def test_plus_without_second_number(self): + result = plus(10, None) + self.assertEqual(result, 10) From f5e3769aae635df7de2dd692733298dcd7c7f9ac Mon Sep 17 00:00:00 2001 From: Christian Merten Date: Fri, 29 Aug 2025 02:46:16 +0200 Subject: [PATCH 6/7] chore(members/tests): cover templatetags/overview_extras --- jdav_web/members/tests/templatetags.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/jdav_web/members/tests/templatetags.py b/jdav_web/members/tests/templatetags.py index a8c414d..46f121f 100644 --- a/jdav_web/members/tests/templatetags.py +++ b/jdav_web/members/tests/templatetags.py @@ -2,6 +2,7 @@ from django.test import TestCase from django.template import Context, Template from datetime import datetime, date, timedelta from members.templatetags.tex_extras import index, datetime_short, date_short, date_vs, time_short, add, plus +from members.templatetags.overview_extras import blToColor, render_bool class TexExtrasTestCase(TestCase): @@ -53,3 +54,27 @@ class TexExtrasTestCase(TestCase): def test_plus_without_second_number(self): result = plus(10, None) self.assertEqual(result, 10) + + +class OverviewExtrasTestCase(TestCase): + def test_blToColor_truthy_value(self): + result = blToColor(True) + self.assertEqual(result, 'green') + + def test_blToColor_falsy_value(self): + result = blToColor(False) + self.assertEqual(result, 'red') + + def test_render_bool_non_boolean_value(self): + with self.assertRaises(ValueError): + render_bool("not_a_boolean") + + def test_render_bool_true(self): + result = render_bool(True) + self.assertIn('#bcd386', result) + self.assertIn('icon-tick', result) + + def test_render_bool_false(self): + result = render_bool(False) + self.assertIn('#dba4a4', result) + self.assertIn('icon-cross', result) From 162b9a46eaa935811c422c8334dcc803fc968c75 Mon Sep 17 00:00:00 2001 From: Christian Merten Date: Fri, 29 Aug 2025 02:48:09 +0200 Subject: [PATCH 7/7] chore(docker/test): add test texts config --- docker/test/config/texts.toml | 12 ++++++++++++ jdav_web/jdav_web/settings/__init__.py | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 docker/test/config/texts.toml diff --git a/docker/test/config/texts.toml b/docker/test/config/texts.toml new file mode 100644 index 0000000..2e04743 --- /dev/null +++ b/docker/test/config/texts.toml @@ -0,0 +1,12 @@ +confirm_mail = """ +Hiho custom test test {name}, + +du hast bei der JDAV %(SEKTION)s eine E-Mail Adresse hinterlegt. Da bei uns alle Kommunikation +per Email funktioniert, brauchen wir eine Bestätigung {whattoconfirm}. + +Custom! + +{link} + +Test test +Deine JDAV test test %(SEKTION)s""" diff --git a/jdav_web/jdav_web/settings/__init__.py b/jdav_web/jdav_web/settings/__init__.py index 248f7d1..e474d0e 100644 --- a/jdav_web/jdav_web/settings/__init__.py +++ b/jdav_web/jdav_web/settings/__init__.py @@ -25,7 +25,7 @@ if os.path.exists(os.path.join(CONFIG_DIR_PATH, TEXTS_FILE)): with open(os.path.join(CONFIG_DIR_PATH, TEXTS_FILE), 'rb') as f: texts = tomli.load(f) else: - texts = {} + texts = {} # pragma: no cover def get_var(*keys, default='', dictionary=config):