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
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:
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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('<script>', 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')
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user