From 3649878b7d1b6da31940b3b444286b68e81f2e7a Mon Sep 17 00:00:00 2001 From: "Adrian A. Baumann" Date: Tue, 18 Nov 2025 11:36:06 +0100 Subject: [PATCH 1/9] Ignore file maintenance --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index f19cdcf..da8a3d8 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ keys/ node_modules/ package-lock.json package.json +AGENT*.md # Diagram cache directory media/diagram_cache/ .env From e923624aec21b887ac7ad4839d298e41c9f00a88 Mon Sep 17 00:00:00 2001 From: "Adrian A. Baumann" Date: Wed, 19 Nov 2025 13:49:45 +0100 Subject: [PATCH 2/9] Metadata moved to end of document --- .../templates/standards/standard_detail.html | 48 +++++++++---------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/dokumente/templates/standards/standard_detail.html b/dokumente/templates/standards/standard_detail.html index 4a95baa..a648123 100644 --- a/dokumente/templates/standards/standard_detail.html +++ b/dokumente/templates/standards/standard_detail.html @@ -19,30 +19,6 @@ Historische Version vom {{ standard.check_date }} {% endif %} - - -
-
-
-
Autoren:
-
{{ standard.autoren.all|join:", " }}
- -
Prüfende:
-
{{ standard.pruefende.all|join:", " }}
- -
Gültigkeit:
-
{{ standard.gueltigkeit_von }} bis {{ standard.gueltigkeit_bis|default_if_none:"auf weiteres" }}
-
-

- - JSON herunterladen - -

-
-
- {% if standard.einleitung_html %}
@@ -175,5 +151,29 @@
{% endif %} {% endfor %} + + +

Metadaten

+
+
+
+
Autoren:
+
{{ standard.autoren.all|join:", " }}
+ +
Prüfende:
+
{{ standard.pruefende.all|join:", " }}
+ +
Gültigkeit:
+
{{ standard.gueltigkeit_von }} bis {{ standard.gueltigkeit_bis|default_if_none:"auf weiteres" }}
+
+

+ + JSON herunterladen + +

+
+
{% endblock %} From 1745596d14361da7eba4c24cb10dac86b56f6e99 Mon Sep 17 00:00:00 2001 From: "Adrian A. Baumann" Date: Thu, 20 Nov 2025 15:32:24 +0100 Subject: [PATCH 3/9] Deploy after Kubernetes fuckup --- argocd/deployment.yaml | 2 +- pages/templates/base.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/argocd/deployment.yaml b/argocd/deployment.yaml index 111c31d..c5444d2 100644 --- a/argocd/deployment.yaml +++ b/argocd/deployment.yaml @@ -25,7 +25,7 @@ spec: mountPath: /data containers: - name: web - image: git.baumann.gr/adebaumann/vui:0.952-oblique + image: git.baumann.gr/adebaumann/vui:0.953-development imagePullPolicy: Always ports: - containerPort: 8000 diff --git a/pages/templates/base.html b/pages/templates/base.html index 8848e01..c1a3e91 100644 --- a/pages/templates/base.html +++ b/pages/templates/base.html @@ -180,7 +180,7 @@

-

Version {{ version|default:"0.951" }}

+

Version {{ version|default:"0.953" }}

From 57f2210c779b48d6a0f587159c06aca866cd2771 Mon Sep 17 00:00:00 2001 From: "Adrian A. Baumann" Date: Fri, 21 Nov 2025 16:20:37 +0100 Subject: [PATCH 4/9] Rootmismatch - redeploying --- argocd/deployment.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/argocd/deployment.yaml b/argocd/deployment.yaml index c5444d2..8b09d42 100644 --- a/argocd/deployment.yaml +++ b/argocd/deployment.yaml @@ -25,7 +25,7 @@ spec: mountPath: /data containers: - name: web - image: git.baumann.gr/adebaumann/vui:0.953-development + image: git.baumann.gr/adebaumann/vui:0.953 imagePullPolicy: Always ports: - containerPort: 8000 From 94e047c7ffb90b42d0da7fa3f58ec50e2aff61de Mon Sep 17 00:00:00 2001 From: "Adrian A. Baumann" Date: Fri, 21 Nov 2025 16:39:32 +0100 Subject: [PATCH 5/9] Ingress troubleshooting --- argocd/deployment.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/argocd/deployment.yaml b/argocd/deployment.yaml index 8b09d42..0b1f9c1 100644 --- a/argocd/deployment.yaml +++ b/argocd/deployment.yaml @@ -25,7 +25,7 @@ spec: mountPath: /data containers: - name: web - image: git.baumann.gr/adebaumann/vui:0.953 + image: git.baumann.gr/adebaumann/vui:0.953-ingressfixed imagePullPolicy: Always ports: - containerPort: 8000 From 7e9059a9aa2d9a2d6ba356021c03990ea4648d2d Mon Sep 17 00:00:00 2001 From: "Adrian A. Baumann" Date: Mon, 24 Nov 2025 10:37:23 +0100 Subject: [PATCH 6/9] feat: implement user authentication with login/logout functionality - Add user login screen with German interface - Add user icon and dropdown menu in header for authenticated users - Add password change functionality with proper redirects - Configure authentication URLs and settings - Ensure all auth functions redirect to main page instead of admin - Complete openspec change proposal for login feature --- VorgabenUI/settings.py | 5 ++ VorgabenUI/urls.py | 6 ++ openspec/changes/add-login/tasks.md | 5 ++ openspec/project.md | 63 +++++++++++++++++++ pages/templates/base.html | 24 +++++++ pages/templates/registration/login.html | 43 +++++++++++++ .../registration/password_change.html | 56 +++++++++++++++++ .../registration/password_change_done.html | 24 +++++++ 8 files changed, 226 insertions(+) create mode 100644 openspec/changes/add-login/tasks.md create mode 100644 openspec/project.md create mode 100644 pages/templates/registration/login.html create mode 100644 pages/templates/registration/password_change.html create mode 100644 pages/templates/registration/password_change_done.html diff --git a/VorgabenUI/settings.py b/VorgabenUI/settings.py index 80cc333..f32e2f8 100644 --- a/VorgabenUI/settings.py +++ b/VorgabenUI/settings.py @@ -152,6 +152,11 @@ DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' DATA_UPLOAD_MAX_NUMBER_FIELDS=10250 NESTED_ADMIN_LAZY_INLINES = True +# Authentication settings +LOGIN_URL = 'login' +LOGIN_REDIRECT_URL = '/' +LOGOUT_REDIRECT_URL = 'login' + #LOGGING = { # "version": 1, # "handlers" :{ diff --git a/VorgabenUI/urls.py b/VorgabenUI/urls.py index d072398..5c568c7 100644 --- a/VorgabenUI/urls.py +++ b/VorgabenUI/urls.py @@ -18,6 +18,7 @@ from django.contrib import admin from django.urls import include, path, re_path from django.conf import settings from django.conf.urls.static import static +from django.contrib.auth import views as auth_views import dokumente.views import pages.views import referenzen.views @@ -32,6 +33,11 @@ urlpatterns = [ path('stichworte/', include("stichworte.urls")), path('referenzen/', referenzen.views.tree, name="referenz_tree"), path('referenzen//', referenzen.views.detail, name="referenz_detail"), + # Authentication URLs + path('login/', auth_views.LoginView.as_view(template_name='registration/login.html'), name='login'), + path('logout/', auth_views.LogoutView.as_view(next_page='/'), name='logout'), + path('password_change/', auth_views.PasswordChangeView.as_view(template_name='registration/password_change.html', success_url='/'), name='password_change'), + path('password_change/done/', auth_views.PasswordChangeDoneView.as_view(template_name='registration/password_change_done.html'), name='password_change_done'), ] # Serve static files diff --git a/openspec/changes/add-login/tasks.md b/openspec/changes/add-login/tasks.md new file mode 100644 index 0000000..e554e71 --- /dev/null +++ b/openspec/changes/add-login/tasks.md @@ -0,0 +1,5 @@ +## 1. Add user login functionality +- [x] add a login screen for users +- [x] add an icon for logged in user on the top right corner of all page +- [x] add a menu to log out and change password on the user icon +- [x] all functions should go back to the main page, not the django admin page diff --git a/openspec/project.md b/openspec/project.md new file mode 100644 index 0000000..50ff634 --- /dev/null +++ b/openspec/project.md @@ -0,0 +1,63 @@ +# Project Context + +## Purpose +This is a Django-based document management system for regulatory documents (Dokumente) and their provisions (Vorgaben). It manages validity periods, conflicts between overlapping provisions, references, keywords, and roles. The system supports importing documents, checking for compliance, and maintaining changelogs. + +## Tech Stack +- Python 3.x +- Django 5.2.5 +- SQLite (development), PostgreSQL (production) +- Django MPTT for tree structures +- Django Nested Admin for inline editing +- Kubernetes for deployment +- ArgoCD for continuous deployment +- Traefik for ingress +- Gunicorn for WSGI server + +## Project Conventions + +### Code Style +- Language: German for user-facing strings and model names, English for code comments and internal naming +- Imports: Standard library first, then Django, then third-party, then local apps +- Model naming: German nouns (Dokument, Vorgabe, Person) +- Field naming: German for field names, English Django conventions +- Class naming: PascalCase for models, snake_case for functions/variables +- All models have __str__ methods returning meaningful German strings +- Use verbose_name and verbose_name_plural in Meta classes (German) + +### Architecture Patterns +- Django apps: abschnitte, dokumente, referenzen, rollen, stichworte, pages +- MPTT for hierarchical text sections +- Foreign keys with on_delete=models.PROTECT for important relationships +- Many-to-many with descriptive related_name +- Proxy models for different views (e.g., VorgabenTable) +- Management commands for data operations + +### Testing Strategy +- Django test framework +- Test class names in English, methods in English +- Comprehensive model tests +- Test both success and error cases +- Run with `python manage.py test` + +### Git Workflow +- Standard Git workflow +- Commits in English +- Use Gitea workflows for CI/CD + +## Domain Context +The system manages regulatory documents with numbered provisions that have validity dates. Provisions can conflict if they have overlapping date ranges for the same document, theme, and number. The system includes sanity checks for conflicts, diagram caching for visualization, and JSON export functionality. + +## Important Constraints +- German language for all user interfaces and data +- Strict validation of date ranges to prevent overlapping provisions +- Documents have types, authors, reviewers, and validity periods +- Provisions linked to themes, references, keywords, and relevant roles +- Active/inactive status for documents + +## External Dependencies +- Django ecosystem: MPTT, nested-admin, revproxy +- Kubernetes cluster for deployment +- ArgoCD for GitOps +- Traefik for load balancing +- External diagram services (diagramm_proxy) diff --git a/pages/templates/base.html b/pages/templates/base.html index c1a3e91..fed2831 100644 --- a/pages/templates/base.html +++ b/pages/templates/base.html @@ -41,6 +41,30 @@ alt="Zur Startseite" />

Vorgaben Informatiksicherheit BIT

+ + + {% if user.is_authenticated %} + + {% else %} + + {% endif %} diff --git a/pages/templates/registration/login.html b/pages/templates/registration/login.html new file mode 100644 index 0000000..c4cfe54 --- /dev/null +++ b/pages/templates/registration/login.html @@ -0,0 +1,43 @@ +{% extends "base.html" %} +{% load static %} + +{% block title %}Anmelden{% endblock %} + +{% block content %} +
+
+
+
+

Anmelden

+
+
+
+ {% csrf_token %} + + {% if form.errors %} +
+

Ihr Benutzername und Passwort stimmen nicht überein. Bitte versuchen Sie es erneut.

+
+ {% endif %} + +
+ + +
+ +
+ + +
+ +
+ +
+ + +
+
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/pages/templates/registration/password_change.html b/pages/templates/registration/password_change.html new file mode 100644 index 0000000..1a9645e --- /dev/null +++ b/pages/templates/registration/password_change.html @@ -0,0 +1,56 @@ +{% extends "base.html" %} +{% load static %} + +{% block title %}Passwort ändern{% endblock %} + +{% block content %} +
+
+
+
+

Passwort ändern

+
+
+
+ {% csrf_token %} + + {% if form.errors %} +
+

Bitte korrigieren Sie die Fehler unten.

+
+ {% endif %} + +
+ + + {% if form.old_password.errors %} +
{{ form.old_password.errors }}
+ {% endif %} +
+ +
+ + + {% if form.new_password1.errors %} +
{{ form.new_password1.errors }}
+ {% endif %} +
+ +
+ + + {% if form.new_password2.errors %} +
{{ form.new_password2.errors }}
+ {% endif %} +
+ +
+ + Abbrechen +
+
+
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/pages/templates/registration/password_change_done.html b/pages/templates/registration/password_change_done.html new file mode 100644 index 0000000..15cacda --- /dev/null +++ b/pages/templates/registration/password_change_done.html @@ -0,0 +1,24 @@ +{% extends "base.html" %} +{% load static %} + +{% block title %}Passwort geändert{% endblock %} + +{% block content %} +
+
+
+
+

Passwort erfolgreich geändert

+
+
+
+

Ihr Passwort wurde erfolgreich geändert.

+
+

+ Zurück zur Startseite +

+
+
+
+
+{% endblock %} \ No newline at end of file From ceb6e1344722b897a55225eb5c7e7d54d524d541 Mon Sep 17 00:00:00 2001 From: "Adrian A. Baumann" Date: Mon, 24 Nov 2025 10:39:40 +0100 Subject: [PATCH 7/9] fix: resolve logout 405 error by using POST method - Change logout link from GET anchor to POST form - Add CSRF token for security - Style button to match dropdown menu appearance --- pages/templates/base.html | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/pages/templates/base.html b/pages/templates/base.html index fed2831..585b0a5 100644 --- a/pages/templates/base.html +++ b/pages/templates/base.html @@ -54,7 +54,14 @@ From 4d0ed116ddb25ecc8f49cc49ea118f154b8241db Mon Sep 17 00:00:00 2001 From: "Adrian A. Baumann" Date: Mon, 24 Nov 2025 10:46:10 +0100 Subject: [PATCH 8/9] test: add comprehensive authentication test suite - Add 21 test cases covering login, logout, and password change functionality - Test both success and failure scenarios for authentication flows - Verify proper redirects to main page instead of admin - Test user menu display for authenticated vs anonymous users - Test CSRF protection and POST requirement for logout - Test password validation and error handling - All tests passing, ensuring authentication feature works correctly --- pages/test_auth.py | 266 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 266 insertions(+) create mode 100644 pages/test_auth.py diff --git a/pages/test_auth.py b/pages/test_auth.py new file mode 100644 index 0000000..28e4a4d --- /dev/null +++ b/pages/test_auth.py @@ -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/') \ No newline at end of file From 47c264e8e19d8c2ae3e68c1909143882c1f384c9 Mon Sep 17 00:00:00 2001 From: "Adrian A. Baumann" Date: Mon, 24 Nov 2025 11:19:09 +0100 Subject: [PATCH 9/9] fix: update search tests to match actual template output - Change expected 'Suchresultate' to 'Suchergebnisse' matching results.html - Change expected 'Keine Resultate' to 'Keine Ergebnisse gefunden' - Replace test_search_result_logging with test_search_result_structure - Remove unused unittest.mock import --- pages/tests.py | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/pages/tests.py b/pages/tests.py index 792e605..2c2d742 100644 --- a/pages/tests.py +++ b/pages/tests.py @@ -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': '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"""