Compare commits
16 Commits
ceb6e13447
...
feature/nf
| Author | SHA1 | Date | |
|---|---|---|---|
| 4376069b11 | |||
| c285ae81af | |||
| 5bfe4866a4 | |||
| f7799675d5 | |||
| c125427b8d | |||
| a14a80f7bd | |||
| 477143b3ff | |||
| fc404f6755 | |||
| fe7c55eceb | |||
| bb01174bd2 | |||
| 38ce55d8fd | |||
| d439741339 | |||
| bc75cac6cd | |||
| 47c264e8e1 | |||
| 4d0ed116dd | |||
| ccf31e4ef4 |
92
Documentation/modelle_dokumente.md
Normal file
92
Documentation/modelle_dokumente.md
Normal file
@@ -0,0 +1,92 @@
|
||||
# Modelle (App: dokumente)
|
||||
|
||||
Kurzbeschreibungen der Modelle in dokumente/models.py.
|
||||
|
||||
## Dokumententyp
|
||||
- Zweck: Kategorisierung von Dokumenten (z. B. Richtlinie, Verfahren).
|
||||
- Wichtige Felder: `name` (CharField, PK), `verantwortliche_ve` (CharField).
|
||||
- Besonderheiten: `__str__()` gibt `name` zurück.
|
||||
- Meta: `verbose_name` und `verbose_name_plural` gesetzt.
|
||||
|
||||
## Person
|
||||
- Zweck: Repräsentiert Personen (Autoren, Prüfer).
|
||||
- Wichtige Felder: `name` (CharField, PK), `funktion` (CharField).
|
||||
- Beziehungen: Many-to-many mit Dokument über `autoren` und `pruefende`.
|
||||
- Besonderheiten: `__str__()` gibt `name` zurück; `ordering = ['name']`.
|
||||
- Meta: `verbose_name_plural = "Personen"`.
|
||||
|
||||
## Thema
|
||||
- Zweck: Thematische Einordnung von Vorgaben.
|
||||
- Wichtige Felder: `name` (CharField, PK), `erklaerung` (TextField, optional).
|
||||
- Besonderheiten: `__str__()` gibt `name` zurück.
|
||||
|
||||
## Dokument
|
||||
- Zweck: Hauptobjekt; ein einzelnes Dokument mit Metadaten.
|
||||
- Wichtige Felder:
|
||||
- `nummer` (CharField, PK)
|
||||
- `dokumententyp` (FK → Dokumententyp, on_delete=PROTECT)
|
||||
- `name` (CharField)
|
||||
- `autoren`, `pruefende` (ManyToManyField → Person)
|
||||
- `gueltigkeit_von`, `gueltigkeit_bis` (DateField, optional)
|
||||
- `aktiv` (BooleanField)
|
||||
- `signatur_cso`, `anhaenge` (Metadaten)
|
||||
- Besonderheiten: `__str__()` formatiert als "nummer – name".
|
||||
- Meta: `verbose_name` / `verbose_name_plural`.
|
||||
|
||||
## Vorgabe
|
||||
- Zweck: Einzelne Vorgabe / Anforderung innerhalb eines Dokuments.
|
||||
- Wichtige Felder:
|
||||
- `order` (IntegerField) — Sortierreihenfolge
|
||||
- `nummer` (IntegerField) — Nummer innerhalb Thema/Dokument
|
||||
- `dokument` (FK → Dokument, CASCADE, related_name='vorgaben')
|
||||
- `thema` (FK → Thema, PROTECT)
|
||||
- `titel` (CharField)
|
||||
- `referenzen` (M2M → Referenz, optional)
|
||||
- `stichworte` (M2M → Stichwort, optional)
|
||||
- `relevanz` (M2M → Rolle, optional)
|
||||
- `gueltigkeit_von`, `gueltigkeit_bis` (Datum/Felder)
|
||||
- Beziehungen: zu Dokument, Thema, Referenzen, Stichworte, Rollen.
|
||||
- Wichtige Methoden:
|
||||
- `Vorgabennummer()` — generiert eine lesbare Kennung (z. B. "DOK. T. N").
|
||||
- `get_status(check_date, verbose)` — liefert "future", "active" oder "expired" oder eine deutsche Statusbeschreibung, abhängig von Gültigkeitsdaten.
|
||||
- `sanity_check_vorgaben()` (static) — findet Konflikte zwischen Vorgaben mit gleicher Nummer/Thema/Dokument, deren Zeiträume sich überschneiden.
|
||||
- `clean()` — ruft `find_conflicts()` auf und wirft ValidationError bei Konflikten.
|
||||
- `find_conflicts()` — prüft Konflikte mit bestehenden Vorgaben (ohne sich selbst).
|
||||
- `_date_ranges_intersect(...)` (static) — prüft, ob sich zwei Datumsbereiche überschneiden (None = offen).
|
||||
- Besonderheiten: `__str__()` gibt "Vorgabennummer: titel" zurück.
|
||||
- Meta: `ordering = ['order']`, `verbose_name_plural = "Vorgaben"`.
|
||||
|
||||
## VorgabeLangtext, VorgabeKurztext
|
||||
- Zweck: Textabschnitts-Modelle, erben von `Textabschnitt` (aus abschnitte.models).
|
||||
- Wichtige Felder: je ein FK `abschnitt` → Vorgabe.
|
||||
- Besonderheit: konkrete Untertypen für Lang- und Kurztexte; Meta-`verbose_name` gesetzt.
|
||||
|
||||
## Geltungsbereich, Einleitung
|
||||
- Zweck: Dokumentbezogene Textabschnitte (erben von `Textabschnitt`).
|
||||
- Wichtige Felder: FK zum `Dokument` (`geltungsbereich` bzw. `einleitung`).
|
||||
- Meta: `verbose_name`/`verbose_name_plural` gesetzt.
|
||||
|
||||
## Checklistenfrage
|
||||
- Zweck: Einzelne Frage für Checklisten zu einer Vorgabe.
|
||||
- Wichtige Felder: `vorgabe` (FK → Vorgabe, related_name="checklistenfragen"), `frage` (CharField).
|
||||
- Besonderheiten: `__str__()` gibt `frage` zurück.
|
||||
|
||||
## VorgabenTable
|
||||
- Zweck: Proxy-Modell für Vorgabe zur Darstellung (Tabellenansicht).
|
||||
- Besonderheiten: kein eigenes Schema; nur Meta-Attribute (`proxy = True`, `verbose_name`).
|
||||
|
||||
## Changelog
|
||||
- Zweck: Änderungsverzeichnis-Eintrag für ein Dokument.
|
||||
- Wichtige Felder:
|
||||
- `dokument` (FK → Dokument, related_name='changelog')
|
||||
- `autoren` (M2M → Person)
|
||||
- `datum` (DateField)
|
||||
- `aenderung` (TextField)
|
||||
- Besonderheiten: `__str__()` formatiert als "datum – dokumentnummer".
|
||||
- Meta: `verbose_name` / `verbose_name_plural`.
|
||||
|
||||
Hinweise zur Pflege
|
||||
- Wichtige Relationen nutzen häufig on_delete=PROTECT, um versehentliche Löschungen zu vermeiden.
|
||||
- Viele Modelle haben CharField-Primärschlüssel (z. B. `nummer`, `name`).
|
||||
- Validierungslogik für zeitliche Konflikte ist in Vorgabe implementiert (clean / find_conflicts).
|
||||
- Textabschnitt-Modelle erben Verhalten aus `abschnitte.models.Textabschnitt` — dort sind Anzeige- und Inhaltsregeln definiert.
|
||||
@@ -5,7 +5,8 @@ metadata:
|
||||
namespace: vorgabenui
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
- ReadWriteMany
|
||||
storageClassName: nfs
|
||||
resources:
|
||||
requests:
|
||||
storage: 2Gi
|
||||
|
||||
@@ -25,7 +25,7 @@ spec:
|
||||
mountPath: /data
|
||||
containers:
|
||||
- name: web
|
||||
image: git.baumann.gr/adebaumann/vui:0.953-ingressfixed
|
||||
image: git.baumann.gr/adebaumann/vui:0.957
|
||||
imagePullPolicy: Always
|
||||
ports:
|
||||
- containerPort: 8000
|
||||
@@ -63,6 +63,8 @@ spec:
|
||||
selector:
|
||||
app: django
|
||||
ports:
|
||||
- port: 8000
|
||||
- name: http
|
||||
protocol: TCP
|
||||
port: 8000
|
||||
targetPort: 8000
|
||||
|
||||
|
||||
@@ -4,8 +4,9 @@ metadata:
|
||||
name: django
|
||||
namespace: vorgabenui
|
||||
annotations:
|
||||
traefik.ingress.kubernetes.io/router.middlewares: "vorgabenui-vorgabenui-rewrite@kubernetescrd"
|
||||
argocd.argoproj.io/ignore-healthcheck: "true"
|
||||
spec:
|
||||
ingressClassName: traefik
|
||||
rules:
|
||||
- host: vorgabenportal.knowyoursecurity.com
|
||||
http:
|
||||
|
||||
15
argocd/nfs-pv.yaml
Normal file
15
argocd/nfs-pv.yaml
Normal file
@@ -0,0 +1,15 @@
|
||||
apiVersion: v1
|
||||
kind: PersistentVolume
|
||||
metadata:
|
||||
name: django-data-pv
|
||||
namespace: vorgabenui
|
||||
spec:
|
||||
capacity:
|
||||
storage: 2Gi
|
||||
accessModes:
|
||||
- ReadWriteMany
|
||||
persistentVolumeReclaimPolicy: Retain
|
||||
storageClassName: nfs
|
||||
nfs:
|
||||
server: 192.168.17.199
|
||||
path: /mnt/user/vorgabenui
|
||||
8
argocd/nfs-storageclass.yaml
Normal file
8
argocd/nfs-storageclass.yaml
Normal file
@@ -0,0 +1,8 @@
|
||||
apiVersion: storage.k8s.io/v1
|
||||
kind: StorageClass
|
||||
metadata:
|
||||
name: nfs
|
||||
provisioner: kubernetes.io/no-provisioner
|
||||
allowVolumeExpansion: true
|
||||
reclaimPolicy: Retain
|
||||
volumeBindingMode: Immediate
|
||||
@@ -1,9 +0,0 @@
|
||||
apiVersion: traefik.containo.us/v1alpha1
|
||||
kind: Middleware
|
||||
metadata:
|
||||
name: vorgabenui-rewrite
|
||||
namespace: vorgabenui
|
||||
spec:
|
||||
stripPrefix:
|
||||
prefixes:
|
||||
- "/"
|
||||
BIN
data/db.sqlite3
BIN
data/db.sqlite3
Binary file not shown.
@@ -25,7 +25,7 @@
|
||||
<div class="col-md-12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h2 class="h4 mb-0">Einleitung</h2>
|
||||
<h2>Einleitung</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% for typ, html in standard.einleitung_html %}
|
||||
@@ -43,7 +43,7 @@
|
||||
<div class="col-md-12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h2 class="h4 mb-0">Geltungsbereich</h2>
|
||||
<h2>Geltungsbereich</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% for typ, html in standard.geltungsbereich_html %}
|
||||
@@ -73,7 +73,7 @@
|
||||
<a id="{{ vorgabe.Vorgabennummer }}"></a>
|
||||
<div class="card mb-4">
|
||||
<div class="card-header" style="display: flex; justify-content: space-between; align-items: center;">
|
||||
<h3 class="h5 mb-0">
|
||||
<h3>
|
||||
{{ vorgabe.Vorgabennummer }} – {{ vorgabe.titel }}
|
||||
{% if vorgabe.long_status != "active" and standard.history == True %}
|
||||
<span class="badge badge-danger">{{ vorgabe.long_status }}</span>
|
||||
@@ -123,7 +123,7 @@
|
||||
{% endif %}
|
||||
|
||||
<!-- Stichworte und Referenzen -->
|
||||
<div class="mt-4 p-3" style="background-color: #f8f9fa; border-left: 3px solid #dee2e6;">
|
||||
<div class="mt-4 p-3" style="background-color: #f8f9fa; border-left: 3px solid #dee2e6; padding-left: 0.5en;">
|
||||
<p class="mb-2">
|
||||
<strong>Stichworte:</strong>
|
||||
{% if vorgabe.stichworte.all %}
|
||||
|
||||
15
k8s/nfs-pv.yaml
Normal file
15
k8s/nfs-pv.yaml
Normal file
@@ -0,0 +1,15 @@
|
||||
apiVersion: v1
|
||||
kind: PersistentVolume
|
||||
metadata:
|
||||
name: django-data-pv
|
||||
namespace: vorgabenui
|
||||
spec:
|
||||
capacity:
|
||||
storage: 2Gi
|
||||
accessModes:
|
||||
- ReadWriteMany
|
||||
persistentVolumeReclaimPolicy: Retain
|
||||
storageClassName: nfs
|
||||
nfs:
|
||||
server: 192.168.17.199
|
||||
path: /mnt/user/vorgabenui
|
||||
8
k8s/nfs-storageclass.yaml
Normal file
8
k8s/nfs-storageclass.yaml
Normal file
@@ -0,0 +1,8 @@
|
||||
apiVersion: storage.k8s.io/v1
|
||||
kind: StorageClass
|
||||
metadata:
|
||||
name: nfs
|
||||
provisioner: kubernetes.io/no-provisioner
|
||||
allowVolumeExpansion: true
|
||||
reclaimPolicy: Retain
|
||||
volumeBindingMode: Immediate
|
||||
@@ -5,7 +5,8 @@ metadata:
|
||||
namespace: vorgabenui
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
- ReadWriteMany
|
||||
storageClassName: nfs
|
||||
resources:
|
||||
requests:
|
||||
storage: 2Gi
|
||||
|
||||
@@ -211,7 +211,7 @@
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-sm-6 text-right">
|
||||
<p class="text-muted">Version {{ version|default:"0.953" }}</p>
|
||||
<p class="text-muted">Version {{ version|default:"0.957" }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
266
pages/test_auth.py
Normal file
266
pages/test_auth.py
Normal file
@@ -0,0 +1,266 @@
|
||||
from django.test import TestCase, Client
|
||||
from django.contrib.auth.models import User
|
||||
from django.urls import reverse
|
||||
from django.core.exceptions import ValidationError
|
||||
|
||||
|
||||
class AuthenticationTest(TestCase):
|
||||
"""Test login, logout, and password change functionality"""
|
||||
|
||||
def setUp(self):
|
||||
self.client = Client()
|
||||
self.test_user = User.objects.create_user(
|
||||
username='testuser',
|
||||
password='testpass123',
|
||||
email='test@example.com'
|
||||
)
|
||||
self.staff_user = User.objects.create_user(
|
||||
username='staffuser',
|
||||
password='staffpass123',
|
||||
email='staff@example.com'
|
||||
)
|
||||
self.staff_user.is_staff = True
|
||||
self.staff_user.save()
|
||||
|
||||
def test_login_page_loads(self):
|
||||
"""Test that login page loads correctly"""
|
||||
response = self.client.get(reverse('login'))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, 'Anmelden')
|
||||
self.assertContains(response, 'Benutzername:')
|
||||
self.assertContains(response, 'Passwort:')
|
||||
|
||||
def test_login_valid_credentials(self):
|
||||
"""Test successful login with valid credentials"""
|
||||
response = self.client.post(reverse('login'), {
|
||||
'username': 'testuser',
|
||||
'password': 'testpass123'
|
||||
})
|
||||
self.assertEqual(response.status_code, 302) # Redirect after login
|
||||
self.assertRedirects(response, '/') # Should redirect to main page
|
||||
|
||||
# Check user is logged in
|
||||
response = self.client.get('/')
|
||||
self.assertContains(response, 'testuser') # Username should appear in header
|
||||
|
||||
def test_login_invalid_credentials(self):
|
||||
"""Test login with invalid credentials shows error"""
|
||||
response = self.client.post(reverse('login'), {
|
||||
'username': 'testuser',
|
||||
'password': 'wrongpassword'
|
||||
})
|
||||
self.assertEqual(response.status_code, 200) # Stay on login page
|
||||
self.assertContains(response, 'Ihr Benutzername und Passwort stimmen nicht überein')
|
||||
|
||||
def test_login_empty_credentials(self):
|
||||
"""Test login with empty credentials"""
|
||||
response = self.client.post(reverse('login'), {
|
||||
'username': '',
|
||||
'password': ''
|
||||
})
|
||||
self.assertEqual(response.status_code, 200) # Stay on login page
|
||||
# Django's form validation should handle this
|
||||
|
||||
def test_logout_functionality(self):
|
||||
"""Test logout functionality"""
|
||||
# First login
|
||||
self.client.login(username='testuser', password='testpass123')
|
||||
|
||||
# Verify user is logged in
|
||||
response = self.client.get('/')
|
||||
self.assertContains(response, 'testuser')
|
||||
|
||||
# Logout using POST
|
||||
response = self.client.post(reverse('logout'))
|
||||
self.assertEqual(response.status_code, 302) # Redirect after logout
|
||||
self.assertRedirects(response, '/') # Should redirect to main page
|
||||
|
||||
# Verify user is logged out
|
||||
response = self.client.get('/')
|
||||
self.assertNotContains(response, 'testuser')
|
||||
self.assertContains(response, 'Anmelden') # Should show login link
|
||||
|
||||
def test_logout_requires_post(self):
|
||||
"""Test that logout requires POST method"""
|
||||
# Login first
|
||||
self.client.login(username='testuser', password='testpass123')
|
||||
|
||||
# Try GET logout (should fail with 405)
|
||||
response = self.client.get(reverse('logout'))
|
||||
self.assertEqual(response.status_code, 405) # Method Not Allowed
|
||||
|
||||
# User should still be logged in
|
||||
response = self.client.get('/')
|
||||
self.assertContains(response, 'testuser')
|
||||
|
||||
def test_password_change_page_loads(self):
|
||||
"""Test that password change page loads for authenticated users"""
|
||||
self.client.login(username='testuser', password='testpass123')
|
||||
|
||||
response = self.client.get(reverse('password_change'))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, 'Passwort ändern')
|
||||
self.assertContains(response, 'Aktuelles Passwort:')
|
||||
self.assertContains(response, 'Neues Passwort:')
|
||||
self.assertContains(response, 'Neues Passwort bestätigen:')
|
||||
|
||||
def test_password_change_requires_authentication(self):
|
||||
"""Test that password change page requires authentication"""
|
||||
response = self.client.get(reverse('password_change'))
|
||||
self.assertEqual(response.status_code, 302) # Redirect to login
|
||||
|
||||
# Should redirect to login page
|
||||
self.assertIn(reverse('login'), response.url)
|
||||
|
||||
def test_password_change_valid(self):
|
||||
"""Test successful password change"""
|
||||
self.client.login(username='testuser', password='testpass123')
|
||||
|
||||
response = self.client.post(reverse('password_change'), {
|
||||
'old_password': 'testpass123',
|
||||
'new_password1': 'newpass456',
|
||||
'new_password2': 'newpass456'
|
||||
})
|
||||
self.assertEqual(response.status_code, 302) # Redirect after success
|
||||
self.assertRedirects(response, '/') # Should redirect to main page
|
||||
|
||||
# Verify new password works
|
||||
self.client.logout()
|
||||
response = self.client.post(reverse('login'), {
|
||||
'username': 'testuser',
|
||||
'password': 'newpass456'
|
||||
})
|
||||
self.assertEqual(response.status_code, 302) # Successful login
|
||||
|
||||
def test_password_change_wrong_old_password(self):
|
||||
"""Test password change with wrong old password"""
|
||||
self.client.login(username='testuser', password='testpass123')
|
||||
|
||||
response = self.client.post(reverse('password_change'), {
|
||||
'old_password': 'wrongpassword',
|
||||
'new_password1': 'newpass456',
|
||||
'new_password2': 'newpass456'
|
||||
})
|
||||
self.assertEqual(response.status_code, 200) # Stay on form page
|
||||
self.assertContains(response, 'Bitte korrigieren Sie die Fehler unten')
|
||||
|
||||
def test_password_change_mismatched_new_passwords(self):
|
||||
"""Test password change with mismatched new passwords"""
|
||||
self.client.login(username='testuser', password='testpass123')
|
||||
|
||||
response = self.client.post(reverse('password_change'), {
|
||||
'old_password': 'testpass123',
|
||||
'new_password1': 'newpass456',
|
||||
'new_password2': 'differentpass789'
|
||||
})
|
||||
self.assertEqual(response.status_code, 200) # Stay on form page
|
||||
self.assertContains(response, 'Bitte korrigieren Sie die Fehler unten')
|
||||
|
||||
def test_password_change_same_as_old_password(self):
|
||||
"""Test password change with same password as old"""
|
||||
self.client.login(username='testuser', password='testpass123')
|
||||
|
||||
response = self.client.post(reverse('password_change'), {
|
||||
'old_password': 'testpass123',
|
||||
'new_password1': 'testpass123',
|
||||
'new_password2': 'testpass123'
|
||||
})
|
||||
# Django's default validators don't prevent same password, so it should succeed
|
||||
self.assertEqual(response.status_code, 302) # Redirect after success
|
||||
self.assertRedirects(response, '/') # Should redirect to main page
|
||||
|
||||
def test_password_change_cancel_button(self):
|
||||
"""Test password change cancel button"""
|
||||
self.client.login(username='testuser', password='testpass123')
|
||||
|
||||
response = self.client.get(reverse('password_change'))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, 'Abbrechen')
|
||||
# The cancel button should link to main page
|
||||
self.assertContains(response, 'href="/"')
|
||||
|
||||
def test_user_menu_display_for_authenticated_user(self):
|
||||
"""Test that user menu displays correctly for authenticated users"""
|
||||
self.client.login(username='testuser', password='testpass123')
|
||||
|
||||
response = self.client.get('/')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, 'testuser') # Username in menu
|
||||
self.assertContains(response, 'Passwort ändern') # Password change link
|
||||
self.assertContains(response, 'Abmelden') # Logout link
|
||||
self.assertNotContains(response, 'Anmelden') # Should not show login link
|
||||
|
||||
def test_login_link_display_for_anonymous_user(self):
|
||||
"""Test that login link displays for anonymous users"""
|
||||
response = self.client.get('/')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, 'Anmelden') # Should show login link
|
||||
self.assertNotContains(response, 'testuser') # Should not show username
|
||||
self.assertNotContains(response, 'Passwort ändern') # Should not show password change
|
||||
self.assertNotContains(response, 'Abmelden') # Should not show logout
|
||||
|
||||
def test_staff_user_menu(self):
|
||||
"""Test that staff users see appropriate menu"""
|
||||
self.client.login(username='staffuser', password='staffpass123')
|
||||
|
||||
response = self.client.get('/')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, 'staffuser') # Username in menu
|
||||
self.assertContains(response, 'Passwort ändern') # Password change link
|
||||
self.assertContains(response, 'Abmelden') # Logout link
|
||||
|
||||
def test_login_redirect_to_main_page(self):
|
||||
"""Test that successful login redirects to main page"""
|
||||
response = self.client.post(reverse('login'), {
|
||||
'username': 'testuser',
|
||||
'password': 'testpass123'
|
||||
}, follow=True)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
# Should end up on main page
|
||||
self.assertContains(response, 'Vorgaben Informatiksicherheit')
|
||||
|
||||
def test_logout_redirect_to_main_page(self):
|
||||
"""Test that logout redirects to main page"""
|
||||
self.client.login(username='testuser', password='testpass123')
|
||||
|
||||
response = self.client.post(reverse('logout'), follow=True)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
# Should end up on main page
|
||||
self.assertContains(response, 'Vorgaben Informatiksicherheit')
|
||||
# Should show login link for anonymous users
|
||||
self.assertContains(response, 'Anmelden')
|
||||
|
||||
def test_password_change_redirect_to_main_page(self):
|
||||
"""Test that successful password change redirects to main page"""
|
||||
self.client.login(username='testuser', password='testpass123')
|
||||
|
||||
response = self.client.post(reverse('password_change'), {
|
||||
'old_password': 'testpass123',
|
||||
'new_password1': 'newpass456',
|
||||
'new_password2': 'newpass456'
|
||||
}, follow=True)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
# Should end up on main page
|
||||
self.assertContains(response, 'Vorgaben Informatiksicherheit')
|
||||
|
||||
def test_csrf_token_present_in_forms(self):
|
||||
"""Test that CSRF tokens are present in authentication forms"""
|
||||
# Login form
|
||||
response = self.client.get(reverse('login'))
|
||||
self.assertContains(response, 'csrfmiddlewaretoken')
|
||||
|
||||
# Password change form
|
||||
self.client.login(username='testuser', password='testpass123')
|
||||
response = self.client.get(reverse('password_change'))
|
||||
self.assertContains(response, 'csrfmiddlewaretoken')
|
||||
|
||||
def test_login_with_next_parameter(self):
|
||||
"""Test login with next parameter for redirect"""
|
||||
response = self.client.post(reverse('login'), {
|
||||
'username': 'testuser',
|
||||
'password': 'testpass123',
|
||||
'next': '/dokumente/'
|
||||
})
|
||||
self.assertEqual(response.status_code, 302)
|
||||
# Should redirect to the specified next page
|
||||
self.assertRedirects(response, '/dokumente/')
|
||||
@@ -4,7 +4,6 @@ from django.utils import timezone
|
||||
from datetime import date, timedelta
|
||||
from dokumente.models import Dokument, Vorgabe, VorgabeKurztext, VorgabeLangtext, Geltungsbereich, Dokumententyp, Thema
|
||||
from stichworte.models import Stichwort
|
||||
from unittest.mock import patch
|
||||
import re
|
||||
|
||||
|
||||
@@ -67,24 +66,24 @@ class SearchViewTest(TestCase):
|
||||
"""Test POST request with valid search term"""
|
||||
response = self.client.post('/search/', {'q': 'Test'})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, 'Suchresultate für Test')
|
||||
self.assertContains(response, 'Suchergebnisse')
|
||||
|
||||
def test_search_case_insensitive(self):
|
||||
"""Test that search is case insensitive"""
|
||||
# Search for lowercase
|
||||
response = self.client.post('/search/', {'q': 'test'})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, 'Suchresultate für test')
|
||||
self.assertContains(response, 'Suchergebnisse für "test"')
|
||||
|
||||
# Search for uppercase
|
||||
response = self.client.post('/search/', {'q': 'TEST'})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, 'Suchresultate für TEST')
|
||||
self.assertContains(response, 'Suchergebnisse für "TEST"')
|
||||
|
||||
# Search for mixed case
|
||||
response = self.client.post('/search/', {'q': 'TeSt'})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, 'Suchresultate für TeSt')
|
||||
self.assertContains(response, 'Suchergebnisse für "TeSt"')
|
||||
|
||||
def test_search_in_kurztext(self):
|
||||
"""Test search in Kurztext content"""
|
||||
@@ -114,7 +113,7 @@ class SearchViewTest(TestCase):
|
||||
"""Test search with no results"""
|
||||
response = self.client.post('/search/', {'q': 'NichtVorhanden'})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, 'Keine Resultate für "NichtVorhanden"')
|
||||
self.assertContains(response, 'Keine Ergebnisse gefunden')
|
||||
|
||||
def test_search_expired_vorgabe_not_included(self):
|
||||
"""Test that expired Vorgaben are not included in results"""
|
||||
@@ -160,8 +159,8 @@ class SearchViewTest(TestCase):
|
||||
"""Test that HTML tags are stripped from search input"""
|
||||
response = self.client.post('/search/', {'q': '<script>alert("xss")</script>Test'})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
# Should search for "alert('xss')Test" after HTML tag removal
|
||||
self.assertContains(response, 'Suchresultate für alert("xss")Test')
|
||||
# Should search for "alert("xss")Test" after HTML tag removal
|
||||
self.assertContains(response, 'Suchergebnisse für "alert')
|
||||
|
||||
def test_search_invalid_characters_validation(self):
|
||||
"""Test validation for invalid characters"""
|
||||
@@ -206,7 +205,7 @@ class SearchViewTest(TestCase):
|
||||
self.assertEqual(response.status_code, 200)
|
||||
# The input should be preserved (escaped) in the form
|
||||
# Since HTML tags are stripped, we expect "Test" to be searched
|
||||
self.assertContains(response, 'Suchresultate für Test')
|
||||
self.assertContains(response, 'Suchergebnisse für "Test"')
|
||||
|
||||
def test_search_xss_prevention_in_results(self):
|
||||
"""Test that search terms are escaped in results to prevent XSS"""
|
||||
@@ -218,15 +217,14 @@ class SearchViewTest(TestCase):
|
||||
self.assertEqual(response.status_code, 200)
|
||||
# The script tag should be escaped in the output
|
||||
# Note: This depends on how the template renders the content
|
||||
self.assertContains(response, 'Suchresultate für term')
|
||||
self.assertContains(response, 'Suchergebnisse für "term"')
|
||||
|
||||
@patch('pages.views.pprint.pp')
|
||||
def test_search_result_logging(self, mock_pprint):
|
||||
"""Test that search results are logged for debugging"""
|
||||
def test_search_result_structure(self):
|
||||
"""Test that search results have expected structure"""
|
||||
response = self.client.post('/search/', {'q': 'Test'})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
# Verify that pprint.pp was called with the result
|
||||
mock_pprint.assert_called_once()
|
||||
# Verify the results page is rendered with correct structure
|
||||
self.assertContains(response, 'Suchergebnisse für "Test"')
|
||||
|
||||
def test_search_multiple_documents(self):
|
||||
"""Test search across multiple documents"""
|
||||
|
||||
Reference in New Issue
Block a user