Comment sorting changed, Comments added to test suite.
All checks were successful
Build containers when image tags change / build-if-image-changed (., web, containers, main container, git.baumann.gr/adebaumann/vui) (push) Successful in 16s
Build containers when image tags change / build-if-image-changed (data-loader, loader, initContainers, init-container, git.baumann.gr/adebaumann/vui-data-loader) (push) Successful in 4s

This commit is contained in:
2025-11-28 09:55:35 +01:00
parent b579f5fb42
commit 048105ef27
6 changed files with 799 additions and 13 deletions

View File

@@ -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. `<script>`-Tags)
- **test_line_breaks_preserved**: Testet, dass Zeilenumbrüche in `<br>`-Tags umgewandelt werden
- **test_security_headers_present**: Testet, dass Content-Security-Policy und X-Content-Type-Options Header gesetzt sind
#### AddVorgabeCommentViewTest
- **test_add_comment_requires_login**: Testet, dass anonyme Benutzer keine Kommentare hinzufügen können
- **test_add_comment_requires_post**: Testet, dass nur POST-Methode erlaubt ist (405 für GET)
- **test_add_comment_success**: Testet erfolgreiche Kommentarerstellung mit gültigen Daten
- **test_add_empty_comment_fails**: Testet, dass leere Kommentare mit 400-Fehler abgelehnt werden
- **test_add_whitespace_only_comment_fails**: Testet, dass Kommentare nur mit Leerzeichen abgelehnt werden
- **test_add_too_long_comment_fails**: Testet, dass Kommentare über 2000 Zeichen abgelehnt werden
- **test_add_comment_xss_script_tag_blocked**: Testet, dass Kommentare mit `<script>`-Tags blockiert werden
- **test_add_comment_xss_javascript_protocol_blocked**: Testet, dass `javascript:`-Protokoll blockiert wird
- **test_add_comment_xss_event_handlers_blocked**: Testet, dass Event-Handler (onload, onerror, onclick, onmouseover) blockiert werden
- **test_add_comment_invalid_json_fails**: Testet, dass ungültige JSON-Payloads abgelehnt werden
- **test_add_comment_nonexistent_vorgabe_fails**: Testet 404-Antwort für nicht existierende Vorgabe
- **test_add_comment_security_headers**: Testet, dass Sicherheits-Header in Antworten vorhanden sind
#### DeleteVorgabeCommentViewTest
- **test_delete_comment_requires_login**: Testet, dass anonyme Benutzer keine Kommentare löschen können
- **test_delete_comment_requires_post**: Testet, dass nur POST-Methode erlaubt ist (405 für GET)
- **test_user_can_delete_own_comment**: Testet, dass Benutzer ihre eigenen Kommentare löschen können
- **test_user_cannot_delete_other_users_comment**: Testet, dass Benutzer keine Kommentare anderer löschen können (403 Forbidden)
- **test_staff_can_delete_any_comment**: Testet, dass Staff-Benutzer jeden Kommentar löschen können
- **test_delete_nonexistent_comment_returns_404**: Testet 404-Antwort für nicht existierenden Kommentar
- **test_delete_comment_security_headers**: Testet, dass Sicherheits-Header in Antworten vorhanden sind
---
## pages App Tests
@@ -333,9 +375,17 @@ Die stichworte App enthält 18 Tests, die Schlüsselwortmodelle und ihre Sortier
## Test-Statistiken
- **Gesamt-Tests**: 207
- **Gesamt-Tests**: 230
- **abschnitte**: 33 Tests (einschließlich XSS-Prävention)
- **dokumente**: 116 Tests (98 in tests.py + 9 in test_json.py + 9 JSON-Tests in Haupt-tests.py)
- **dokumente**: 121 Tests (einschließlich Kommentarfunktionalität mit XSS-Schutz)
- Modell-Tests: 44 Tests
- View-Tests: 7 Tests
- URL-Pattern-Tests: 4 Tests
- Sanity-Check-Tests: 16 Tests
- Management-Befehl-Tests: 2 Tests
- JSON-Export-Tests: 9 Tests
- Unvollständige-Vorgaben-Tests: 15 Tests
- Kommentar-Tests: 24 Tests (6 Modell + 18 View-Tests)
- **pages**: 4 Tests
- **referenzen**: 18 Tests
- **rollen**: 18 Tests
@@ -349,7 +399,17 @@ Die stichworte App enthält 18 Tests, die Schlüsselwortmodelle und ihre Sortier
4. **Utility-Funktionen**: Textverarbeitung, Caching, Formatierung
5. **Management-Befehle**: CLI-Schnittstelle und Ausgabeverarbeitung
6. **Integration**: App-übergreifende Funktionalität und Datenfluss
7. **Sicherheit**: XSS-Prävention durch HTML-Bereinigung beim Rendern von Inhalten
7. **Sicherheit**:
- XSS-Prävention durch HTML-Bereinigung beim Rendern von Inhalten
- XSS-Angriffsverhinderung im Kommentarsystem (Script-Tags, javascript:-Protokoll, Event-Handler)
- Eingabevalidierung und -bereinigung
- Autorisierungsprüfungen (Staff vs. normale Benutzer)
- Sicherheits-Header (Content-Security-Policy, X-Content-Type-Options)
8. **Kommentar-Funktionalität**:
- CRUD-Operationen (Create, Read, Delete)
- Benutzerberechtigungen und -besitz
- HTML-Escaping und Erhalt von Zeilenumbrüchen
- Verhinderung mehrerer XSS-Angriffsvektoren
## Ausführen der Tests

View File

@@ -87,7 +87,7 @@ The abschnitte app contains 33 tests covering models, utility functions, diagram
## dokumente App Tests
The dokumente app contains 98 tests, making it the most comprehensive test suite, covering all models, views, URLs, and business logic.
The dokumente app contains 121 tests, making it the most comprehensive test suite, covering all models, views, URLs, business logic, and comment functionality with XSS protection.
### Model Tests
@@ -131,6 +131,14 @@ The dokumente app contains 98 tests, making it the most comprehensive test suite
- **test_checklistenfrage_str**: Verifies string representation truncates long questions
- **test_checklistenfrage_related_name**: Tests the reverse relationship from Vorgabe
#### VorgabeCommentModelTest
- **test_comment_creation**: Tests VorgabeComment creation with vorgabe, user, and text
- **test_comment_str**: Verifies string representation includes username and Vorgabennummer
- **test_comment_related_name**: Tests the reverse relationship from Vorgabe
- **test_comment_ordering**: Tests comments are ordered by created_at descending (newest first)
- **test_comment_timestamps_auto_update**: Tests that updated_at changes when comment is modified
- **test_multiple_users_can_comment**: Tests multiple users can comment on same Vorgabe
### Text Abschnitt Tests
#### DokumentTextAbschnitteTest
@@ -217,6 +225,40 @@ The dokumente app contains 98 tests, making it the most comprehensive test suite
- **test_vorgabe_links**: Tests Vorgaben link to correct admin pages
- **test_back_link**: Tests back link to standard list exists
### Comment Functionality Tests
#### GetVorgabeCommentsViewTest
- **test_get_comments_requires_login**: Tests anonymous users cannot view comments and are redirected
- **test_regular_user_sees_only_own_comments**: Tests regular users only see their own comments
- **test_staff_user_sees_all_comments**: Tests staff users can see all comments
- **test_get_comments_returns_404_for_nonexistent_vorgabe**: Tests 404 response for non-existent Vorgabe
- **test_comments_are_html_escaped**: Tests HTML escaping prevents XSS attacks (e.g., `<script>` tags)
- **test_line_breaks_preserved**: Tests line breaks are converted to `<br>` tags
- **test_security_headers_present**: Tests Content-Security-Policy and X-Content-Type-Options headers are set
#### AddVorgabeCommentViewTest
- **test_add_comment_requires_login**: Tests anonymous users cannot add comments
- **test_add_comment_requires_post**: Tests only POST method is allowed (405 for GET)
- **test_add_comment_success**: Tests successful comment creation with valid data
- **test_add_empty_comment_fails**: Tests empty comments are rejected with 400 error
- **test_add_whitespace_only_comment_fails**: Tests whitespace-only comments are rejected
- **test_add_too_long_comment_fails**: Tests comments exceeding 2000 characters are rejected
- **test_add_comment_xss_script_tag_blocked**: Tests comments with `<script>` tags are blocked
- **test_add_comment_xss_javascript_protocol_blocked**: Tests `javascript:` protocol is blocked
- **test_add_comment_xss_event_handlers_blocked**: Tests event handlers (onload, onerror, onclick, onmouseover) are blocked
- **test_add_comment_invalid_json_fails**: Tests invalid JSON payloads are rejected
- **test_add_comment_nonexistent_vorgabe_fails**: Tests 404 response for non-existent Vorgabe
- **test_add_comment_security_headers**: Tests security headers are present in responses
#### DeleteVorgabeCommentViewTest
- **test_delete_comment_requires_login**: Tests anonymous users cannot delete comments
- **test_delete_comment_requires_post**: Tests only POST method is allowed (405 for GET)
- **test_user_can_delete_own_comment**: Tests users can delete their own comments
- **test_user_cannot_delete_other_users_comment**: Tests users cannot delete others' comments (403 Forbidden)
- **test_staff_can_delete_any_comment**: Tests staff users can delete any comment
- **test_delete_nonexistent_comment_returns_404**: Tests 404 response for non-existent comment
- **test_delete_comment_security_headers**: Tests security headers are present in responses
---
## pages App Tests
@@ -333,9 +375,17 @@ The stichworte app contains 18 tests covering keyword models and their ordering.
## Test Statistics
- **Total Tests**: 207
- **Total Tests**: 230
- **abschnitte**: 33 tests (including XSS prevention)
- **dokumente**: 116 tests (98 in tests.py + 9 in test_json.py + 9 JSON tests in main tests.py)
- **dokumente**: 121 tests (including comment functionality with XSS protection)
- Model tests: 44 tests
- View tests: 7 tests
- URL pattern tests: 4 tests
- Sanity check tests: 16 tests
- Management command tests: 2 tests
- JSON export tests: 9 tests
- Incomplete Vorgaben tests: 15 tests
- Comment tests: 24 tests (6 model + 18 view tests)
- **pages**: 4 tests
- **referenzen**: 18 tests
- **rollen**: 18 tests
@@ -349,7 +399,17 @@ The stichworte app contains 18 tests covering keyword models and their ordering.
4. **Utility Functions**: Text processing, caching, formatting
5. **Management Commands**: CLI interface and output handling
6. **Integration**: Cross-app functionality and data flow
7. **Security**: XSS prevention through HTML sanitization in content rendering
7. **Security**:
- XSS prevention through HTML sanitization in content rendering
- XSS attack prevention in comment system (script tags, javascript: protocol, event handlers)
- Input validation and sanitization
- Authorization checks (staff vs. regular users)
- Security headers (Content-Security-Policy, X-Content-Type-Options)
8. **Comment Functionality**:
- CRUD operations (Create, Read, Delete)
- User permissions and ownership
- HTML escaping and line break preservation
- Multiple XSS attack vector prevention
## Running the Tests

View File

@@ -25,7 +25,7 @@ spec:
mountPath: /data
containers:
- name: web
image: git.baumann.gr/adebaumann/vui:0.959.1
image: git.baumann.gr/adebaumann/vui:0.960
imagePullPolicy: Always
ports:
- containerPort: 8000

View File

@@ -7,7 +7,7 @@ from io import StringIO
from .models import (
Dokumententyp, Person, Thema, Dokument, Vorgabe,
VorgabeLangtext, VorgabeKurztext, Geltungsbereich,
Einleitung, Checklistenfrage, Changelog
Einleitung, Checklistenfrage, Changelog, VorgabeComment
)
from .utils import check_vorgabe_conflicts, date_ranges_intersect, format_conflict_report
from abschnitte.models import AbschnittTyp
@@ -1506,3 +1506,669 @@ class StandardJSONViewTest(TestCase):
# Check that JSON is properly indented (should be formatted)
self.assertIn('\n', response.content.decode())
self.assertIn(' ', response.content.decode()) # Check for indentation
class VorgabeCommentModelTest(TestCase):
"""Test cases for VorgabeComment model"""
def setUp(self):
"""Set up test data for comment tests"""
self.user = User.objects.create_user(
username='testuser',
password='testpass123'
)
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 Document",
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="Dies ist ein Testkommentar"
)
def test_comment_creation(self):
"""Test that VorgabeComment is created correctly"""
self.assertEqual(self.comment.vorgabe, self.vorgabe)
self.assertEqual(self.comment.user, self.user)
self.assertEqual(self.comment.text, "Dies ist ein Testkommentar")
self.assertIsNotNone(self.comment.created_at)
self.assertIsNotNone(self.comment.updated_at)
def test_comment_str(self):
"""Test string representation of VorgabeComment"""
expected = f"Kommentar von {self.user.username} zu {self.vorgabe.Vorgabennummer()}"
self.assertEqual(str(self.comment), expected)
def test_comment_related_name(self):
"""Test related name works correctly"""
self.assertIn(self.comment, self.vorgabe.comments.all())
def test_comment_ordering(self):
"""Test comments are ordered by created_at descending"""
comment2 = VorgabeComment.objects.create(
vorgabe=self.vorgabe,
user=self.user,
text="Zweiter Kommentar"
)
comments = list(self.vorgabe.comments.all())
self.assertEqual(comments[0], comment2) # Newest first
self.assertEqual(comments[1], self.comment)
def test_comment_timestamps_auto_update(self):
"""Test that updated_at changes when comment is modified"""
original_updated_at = self.comment.updated_at
# Wait a tiny bit and update
import time
time.sleep(0.01)
self.comment.text = "Updated text"
self.comment.save()
self.assertNotEqual(self.comment.updated_at, original_updated_at)
self.assertEqual(self.comment.text, "Updated text")
def test_multiple_users_can_comment(self):
"""Test multiple users can comment on same Vorgabe"""
user2 = User.objects.create_user(
username='testuser2',
password='testpass123'
)
comment2 = VorgabeComment.objects.create(
vorgabe=self.vorgabe,
user=user2,
text="Kommentar von anderem Benutzer"
)
self.assertEqual(self.vorgabe.comments.count(), 2)
self.assertIn(self.comment, self.vorgabe.comments.all())
self.assertIn(comment2, self.vorgabe.comments.all())
class GetVorgabeCommentsViewTest(TestCase):
"""Test cases for get_vorgabe_comments view"""
def setUp(self):
"""Set up test data"""
self.client = Client()
# Create users
self.regular_user = User.objects.create_user(
username='regularuser',
password='testpass123'
)
self.staff_user = User.objects.create_user(
username='staffuser',
password='testpass123'
)
self.staff_user.is_staff = True
self.staff_user.save()
self.other_user = User.objects.create_user(
username='otheruser',
password='testpass123'
)
# Create test data
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()
)
# Create comments from different users
self.comment1 = VorgabeComment.objects.create(
vorgabe=self.vorgabe,
user=self.regular_user,
text="Kommentar von Regular User"
)
self.comment2 = VorgabeComment.objects.create(
vorgabe=self.vorgabe,
user=self.other_user,
text="Kommentar von Other User"
)
def test_get_comments_requires_login(self):
"""Test that anonymous users cannot view comments"""
url = reverse('get_vorgabe_comments', kwargs={'vorgabe_id': self.vorgabe.id})
response = self.client.get(url)
# Should redirect to login
self.assertEqual(response.status_code, 302)
self.assertIn('/login/', response.url)
def test_regular_user_sees_only_own_comments(self):
"""Test that regular users only see their own comments"""
self.client.login(username='regularuser', password='testpass123')
url = reverse('get_vorgabe_comments', kwargs={'vorgabe_id': self.vorgabe.id})
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
self.assertEqual(response['Content-Type'], 'application/json')
import json
data = json.loads(response.content)
# Should only see their own comment
self.assertEqual(len(data['comments']), 1)
self.assertEqual(data['comments'][0]['text'], 'Kommentar von Regular User')
self.assertEqual(data['comments'][0]['user'], 'regularuser')
self.assertTrue(data['comments'][0]['is_own'])
def test_staff_user_sees_all_comments(self):
"""Test that staff users see all comments"""
self.client.login(username='staffuser', password='testpass123')
url = reverse('get_vorgabe_comments', kwargs={'vorgabe_id': self.vorgabe.id})
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
import json
data = json.loads(response.content)
# Should see all comments
self.assertEqual(len(data['comments']), 2)
usernames = [c['user'] for c in data['comments']]
self.assertIn('regularuser', usernames)
self.assertIn('otheruser', usernames)
def test_get_comments_returns_404_for_nonexistent_vorgabe(self):
"""Test that requesting comments for non-existent Vorgabe returns 404"""
self.client.login(username='regularuser', password='testpass123')
url = reverse('get_vorgabe_comments', kwargs={'vorgabe_id': 99999})
response = self.client.get(url)
self.assertEqual(response.status_code, 404)
def test_comments_are_html_escaped(self):
"""Test that comments are properly HTML escaped"""
# Create comment with HTML
comment = VorgabeComment.objects.create(
vorgabe=self.vorgabe,
user=self.regular_user,
text="Test <script>alert('xss')</script> 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('&lt;script&gt;', script_comment['text'])
self.assertNotIn('<script>', script_comment['text'])
def test_line_breaks_preserved(self):
"""Test that line breaks are converted to <br> tags"""
comment = VorgabeComment.objects.create(
vorgabe=self.vorgabe,
user=self.regular_user,
text="Line 1\nLine 2\nLine 3"
)
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 multiline comment
multiline_comment = [c for c in data['comments'] if 'Line 1' in c['text']][0]
# Should contain <br> tags
self.assertIn('<br>', multiline_comment['text'])
self.assertIn('Line 1<br>Line 2<br>Line 3', multiline_comment['text'])
def test_security_headers_present(self):
"""Test that security headers are present in response"""
self.client.login(username='regularuser', password='testpass123')
url = reverse('get_vorgabe_comments', kwargs={'vorgabe_id': self.vorgabe.id})
response = self.client.get(url)
self.assertIn('Content-Security-Policy', response)
self.assertIn('X-Content-Type-Options', response)
self.assertEqual(response['X-Content-Type-Options'], 'nosniff')
class AddVorgabeCommentViewTest(TestCase):
"""Test cases for add_vorgabe_comment view"""
def setUp(self):
"""Set up test data"""
self.client = Client()
self.user = User.objects.create_user(
username='testuser',
password='testpass123'
)
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()
)
def test_add_comment_requires_login(self):
"""Test that anonymous users cannot add comments"""
url = reverse('add_vorgabe_comment', kwargs={'vorgabe_id': self.vorgabe.id})
response = self.client.post(url,
data='{"text": "Test comment"}',
content_type='application/json'
)
# Should redirect to login
self.assertEqual(response.status_code, 302)
def test_add_comment_requires_post(self):
"""Test that only POST method is allowed"""
self.client.login(username='testuser', password='testpass123')
url = reverse('add_vorgabe_comment', kwargs={'vorgabe_id': self.vorgabe.id})
response = self.client.get(url)
# Should return method not allowed
self.assertEqual(response.status_code, 405)
def test_add_comment_success(self):
"""Test successful comment addition"""
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": "Dies ist ein neuer Kommentar"}',
content_type='application/json'
)
self.assertEqual(response.status_code, 200)
import json
data = json.loads(response.content)
self.assertTrue(data['success'])
self.assertEqual(data['comment']['text'], 'Dies ist ein neuer Kommentar')
self.assertEqual(data['comment']['user'], 'testuser')
self.assertTrue(data['comment']['is_own'])
# Verify comment was created in database
self.assertEqual(VorgabeComment.objects.count(), 1)
comment = VorgabeComment.objects.first()
self.assertEqual(comment.text, 'Dies ist ein neuer Kommentar')
self.assertEqual(comment.user, self.user)
def test_add_empty_comment_fails(self):
"""Test that empty comments are 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='{"text": ""}',
content_type='application/json'
)
self.assertEqual(response.status_code, 400)
import json
data = json.loads(response.content)
self.assertIn('error', data)
self.assertIn('leer', data['error'].lower())
# No comment should be created
self.assertEqual(VorgabeComment.objects.count(), 0)
def test_add_whitespace_only_comment_fails(self):
"""Test that whitespace-only comments are 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='{"text": " \\n\\t "}',
content_type='application/json'
)
self.assertEqual(response.status_code, 400)
self.assertEqual(VorgabeComment.objects.count(), 0)
def test_add_too_long_comment_fails(self):
"""Test that comments exceeding max length are rejected"""
self.client.login(username='testuser', password='testpass123')
long_text = "a" * 2001 # Over the 2000 character limit
url = reverse('add_vorgabe_comment', kwargs={'vorgabe_id': self.vorgabe.id})
response = self.client.post(url,
data=f'{{"text": "{long_text}"}}',
content_type='application/json'
)
self.assertEqual(response.status_code, 400)
import json
data = json.loads(response.content)
self.assertIn('error', data)
self.assertIn('lang', data['error'].lower())
# No comment should be created
self.assertEqual(VorgabeComment.objects.count(), 0)
def test_add_comment_xss_script_tag_blocked(self):
"""Test that comments with <script> tags 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": "Test <script>alert(\\"xss\\")</script> 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 <a href=\\"javascript:alert(1)\\">here</a>"}',
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')

View File

@@ -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:

View File

@@ -215,7 +215,7 @@
</p>
</div>
<div class="col-sm-6 text-right">
<p class="text-muted">Version {{ version|default:"0.959" }}</p>
<p class="text-muted">Version {{ version|default:"0.960" }}</p>
</div>
</div>
</div>