From 048105ef27b18d8157a69015527d0513cb8494e9 Mon Sep 17 00:00:00 2001
From: "Adrian A. Baumann"
Date: Fri, 28 Nov 2025 09:55:35 +0100
Subject: [PATCH] Comment sorting changed, Comments added to test suite.
---
Test Suite-DE.md | 68 +++-
Test suite.md | 68 +++-
argocd/deployment.yaml | 2 +-
dokumente/tests.py | 668 +++++++++++++++++++++++++++++++++++++-
dokumente/views.py | 4 +-
pages/templates/base.html | 2 +-
6 files changed, 799 insertions(+), 13 deletions(-)
diff --git a/Test Suite-DE.md b/Test Suite-DE.md
index 071189c..92c697d 100644
--- a/Test Suite-DE.md
+++ b/Test Suite-DE.md
@@ -87,7 +87,7 @@ Die abschnitte App enthält 33 Tests, die Modelle, Utility-Funktionen, Diagram-C
## dokumente App Tests
-Die dokumente App enthält 98 Tests und ist damit die umfassendste Test-Suite, die alle Modelle, Views, URLs und Geschäftslogik abdeckt.
+Die dokumente App enthält 121 Tests und ist damit die umfassendste Test-Suite, die alle Modelle, Views, URLs, Geschäftslogik und Kommentarfunktionalität mit XSS-Schutz abdeckt.
### Modell-Tests
@@ -131,6 +131,14 @@ Die dokumente App enthält 98 Tests und ist damit die umfassendste Test-Suite, d
- **test_checklistenfrage_str**: Überprüft, dass die String-Repräsentation lange Fragen kürzt
- **test_checklistenfrage_related_name**: Testet die umgekehrte Beziehung von Vorgabe
+#### VorgabeCommentModelTest
+- **test_comment_creation**: Testet die Erstellung von VorgabeComment mit Vorgabe, Benutzer und Text
+- **test_comment_str**: Überprüft, dass die String-Repräsentation Benutzername und Vorgabennummer enthält
+- **test_comment_related_name**: Testet die umgekehrte Beziehung von Vorgabe
+- **test_comment_ordering**: Testet, dass Kommentare nach created_at absteigend sortiert sind (neueste zuerst)
+- **test_comment_timestamps_auto_update**: Testet, dass sich updated_at ändert, wenn ein Kommentar geändert wird
+- **test_multiple_users_can_comment**: Testet, dass mehrere Benutzer zur selben Vorgabe kommentieren können
+
### Text-Abschnitt-Tests
#### DokumentTextAbschnitteTest
@@ -217,6 +225,40 @@ Die dokumente App enthält 98 Tests und ist damit die umfassendste Test-Suite, d
- **test_vorgabe_links**: Testet, dass Vorgaben zu korrekten Admin-Seiten verlinken
- **test_back_link**: Testet, dass der Zurück-Link zur Standardübersicht existiert
+### Kommentar-Funktionalität Tests
+
+#### GetVorgabeCommentsViewTest
+- **test_get_comments_requires_login**: Testet, dass anonyme Benutzer keine Kommentare sehen können und weitergeleitet werden
+- **test_regular_user_sees_only_own_comments**: Testet, dass normale Benutzer nur ihre eigenen Kommentare sehen
+- **test_staff_user_sees_all_comments**: Testet, dass Staff-Benutzer alle Kommentare sehen können
+- **test_get_comments_returns_404_for_nonexistent_vorgabe**: Testet 404-Antwort für nicht existierende Vorgabe
+- **test_comments_are_html_escaped**: Testet HTML-Escaping zur Verhinderung von XSS-Angriffen (z.B. ` comment"
+ )
+
+ self.client.login(username='regularuser', password='testpass123')
+
+ url = reverse('get_vorgabe_comments', kwargs={'vorgabe_id': self.vorgabe.id})
+ response = self.client.get(url)
+
+ import json
+ data = json.loads(response.content)
+
+ # Find the comment with script tag
+ script_comment = [c for c in data['comments'] if 'script' in c['text'].lower()][0]
+
+ # Should be escaped
+ self.assertIn('<script>', script_comment['text'])
+ self.assertNotIn(' comment"}',
+ content_type='application/json'
+ )
+
+ self.assertEqual(response.status_code, 400)
+
+ import json
+ data = json.loads(response.content)
+
+ self.assertIn('error', data)
+ self.assertIn('ungültige', data['error'].lower())
+
+ # No comment should be created
+ self.assertEqual(VorgabeComment.objects.count(), 0)
+
+ def test_add_comment_xss_javascript_protocol_blocked(self):
+ """Test that comments with javascript: protocol are blocked"""
+ self.client.login(username='testuser', password='testpass123')
+
+ url = reverse('add_vorgabe_comment', kwargs={'vorgabe_id': self.vorgabe.id})
+ response = self.client.post(url,
+ data='{"text": "Click here"}',
+ content_type='application/json'
+ )
+
+ self.assertEqual(response.status_code, 400)
+ self.assertEqual(VorgabeComment.objects.count(), 0)
+
+ def test_add_comment_xss_event_handlers_blocked(self):
+ """Test that comments with event handlers are blocked"""
+ dangerous_inputs = [
+ 'Test onload=alert(1) comment',
+ 'Test onerror=alert(1) comment',
+ 'Test onclick=alert(1) comment',
+ 'Test onmouseover=alert(1) comment'
+ ]
+
+ self.client.login(username='testuser', password='testpass123')
+ url = reverse('add_vorgabe_comment', kwargs={'vorgabe_id': self.vorgabe.id})
+
+ for dangerous_input in dangerous_inputs:
+ response = self.client.post(url,
+ data=f'{{"text": "{dangerous_input}"}}',
+ content_type='application/json'
+ )
+
+ self.assertEqual(response.status_code, 400)
+
+ # No comments should be created
+ self.assertEqual(VorgabeComment.objects.count(), 0)
+
+ def test_add_comment_invalid_json_fails(self):
+ """Test that invalid JSON is rejected"""
+ self.client.login(username='testuser', password='testpass123')
+
+ url = reverse('add_vorgabe_comment', kwargs={'vorgabe_id': self.vorgabe.id})
+ response = self.client.post(url,
+ data='invalid json',
+ content_type='application/json'
+ )
+
+ self.assertEqual(response.status_code, 400)
+
+ import json
+ data = json.loads(response.content)
+
+ self.assertIn('error', data)
+ self.assertIn('Ungültige', data['error'])
+
+ def test_add_comment_nonexistent_vorgabe_fails(self):
+ """Test that adding comment to non-existent Vorgabe returns 404"""
+ self.client.login(username='testuser', password='testpass123')
+
+ url = reverse('add_vorgabe_comment', kwargs={'vorgabe_id': 99999})
+ response = self.client.post(url,
+ data='{"text": "Test comment"}',
+ content_type='application/json'
+ )
+
+ self.assertEqual(response.status_code, 404)
+
+ def test_add_comment_security_headers(self):
+ """Test that security headers are present in response"""
+ self.client.login(username='testuser', password='testpass123')
+
+ url = reverse('add_vorgabe_comment', kwargs={'vorgabe_id': self.vorgabe.id})
+ response = self.client.post(url,
+ data='{"text": "Test comment"}',
+ content_type='application/json'
+ )
+
+ self.assertIn('Content-Security-Policy', response)
+ self.assertIn('X-Content-Type-Options', response)
+ self.assertEqual(response['X-Content-Type-Options'], 'nosniff')
+
+
+class DeleteVorgabeCommentViewTest(TestCase):
+ """Test cases for delete_vorgabe_comment view"""
+
+ def setUp(self):
+ """Set up test data"""
+ self.client = Client()
+
+ self.user = User.objects.create_user(
+ username='testuser',
+ password='testpass123'
+ )
+
+ self.other_user = User.objects.create_user(
+ username='otheruser',
+ password='testpass123'
+ )
+
+ self.staff_user = User.objects.create_user(
+ username='staffuser',
+ password='testpass123'
+ )
+ self.staff_user.is_staff = True
+ self.staff_user.save()
+
+ self.dokumententyp = Dokumententyp.objects.create(
+ name="Test Typ",
+ verantwortliche_ve="Test VE"
+ )
+
+ self.thema = Thema.objects.create(name="Test Thema")
+
+ self.dokument = Dokument.objects.create(
+ nummer="COMM-001",
+ dokumententyp=self.dokumententyp,
+ name="Comment Test",
+ aktiv=True
+ )
+
+ self.vorgabe = Vorgabe.objects.create(
+ order=1,
+ nummer=1,
+ dokument=self.dokument,
+ thema=self.thema,
+ titel="Test Vorgabe",
+ gueltigkeit_von=date.today()
+ )
+
+ self.comment = VorgabeComment.objects.create(
+ vorgabe=self.vorgabe,
+ user=self.user,
+ text="Test comment to delete"
+ )
+
+ def test_delete_comment_requires_login(self):
+ """Test that anonymous users cannot delete comments"""
+ url = reverse('delete_vorgabe_comment', kwargs={'comment_id': self.comment.id})
+ response = self.client.post(url)
+
+ # Should redirect to login
+ self.assertEqual(response.status_code, 302)
+
+ # Comment should still exist
+ self.assertTrue(VorgabeComment.objects.filter(id=self.comment.id).exists())
+
+ def test_delete_comment_requires_post(self):
+ """Test that only POST method is allowed"""
+ self.client.login(username='testuser', password='testpass123')
+
+ url = reverse('delete_vorgabe_comment', kwargs={'comment_id': self.comment.id})
+ response = self.client.get(url)
+
+ # Should return method not allowed
+ self.assertEqual(response.status_code, 405)
+
+ def test_user_can_delete_own_comment(self):
+ """Test that users can delete their own comments"""
+ self.client.login(username='testuser', password='testpass123')
+
+ url = reverse('delete_vorgabe_comment', kwargs={'comment_id': self.comment.id})
+ response = self.client.post(url)
+
+ self.assertEqual(response.status_code, 200)
+
+ import json
+ data = json.loads(response.content)
+
+ self.assertTrue(data['success'])
+
+ # Comment should be deleted
+ self.assertFalse(VorgabeComment.objects.filter(id=self.comment.id).exists())
+
+ def test_user_cannot_delete_other_users_comment(self):
+ """Test that users cannot delete other users' comments"""
+ self.client.login(username='otheruser', password='testpass123')
+
+ url = reverse('delete_vorgabe_comment', kwargs={'comment_id': self.comment.id})
+ response = self.client.post(url)
+
+ self.assertEqual(response.status_code, 403)
+
+ import json
+ data = json.loads(response.content)
+
+ self.assertIn('error', data)
+ self.assertIn('Berechtigung', data['error'])
+
+ # Comment should still exist
+ self.assertTrue(VorgabeComment.objects.filter(id=self.comment.id).exists())
+
+ def test_staff_can_delete_any_comment(self):
+ """Test that staff users can delete any comment"""
+ self.client.login(username='staffuser', password='testpass123')
+
+ url = reverse('delete_vorgabe_comment', kwargs={'comment_id': self.comment.id})
+ response = self.client.post(url)
+
+ self.assertEqual(response.status_code, 200)
+
+ import json
+ data = json.loads(response.content)
+
+ self.assertTrue(data['success'])
+
+ # Comment should be deleted
+ self.assertFalse(VorgabeComment.objects.filter(id=self.comment.id).exists())
+
+ def test_delete_nonexistent_comment_returns_404(self):
+ """Test that deleting non-existent comment returns 404"""
+ self.client.login(username='testuser', password='testpass123')
+
+ url = reverse('delete_vorgabe_comment', kwargs={'comment_id': 99999})
+ response = self.client.post(url)
+
+ self.assertEqual(response.status_code, 404)
+
+ def test_delete_comment_security_headers(self):
+ """Test that security headers are present in response"""
+ self.client.login(username='testuser', password='testpass123')
+
+ url = reverse('delete_vorgabe_comment', kwargs={'comment_id': self.comment.id})
+ response = self.client.post(url)
+
+ self.assertIn('Content-Security-Policy', response)
+ self.assertIn('X-Content-Type-Options', response)
+ self.assertEqual(response['X-Content-Type-Options'], 'nosniff')
diff --git a/dokumente/views.py b/dokumente/views.py
index a413894..0b535dd 100644
--- a/dokumente/views.py
+++ b/dokumente/views.py
@@ -259,10 +259,10 @@ def get_vorgabe_comments(request, vorgabe_id):
if request.user.is_staff:
# Staff can see all comments
- comments = vorgabe.comments.all().select_related('user')
+ comments = vorgabe.comments.all().select_related('user').order_by('created_at')
else:
# Regular users can only see their own comments
- comments = vorgabe.comments.filter(user=request.user).select_related('user')
+ comments = vorgabe.comments.filter(user=request.user).select_related('user').order_by('created_at')
comments_data = []
for comment in comments:
diff --git a/pages/templates/base.html b/pages/templates/base.html
index 27b84a4..fd19f62 100644
--- a/pages/templates/base.html
+++ b/pages/templates/base.html
@@ -215,7 +215,7 @@
-
Version {{ version|default:"0.959" }}
+
Version {{ version|default:"0.960" }}