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" }}