Compare commits
30 Commits
feature/ob
...
07ba717de9
| Author | SHA1 | Date | |
|---|---|---|---|
| 07ba717de9 | |||
| 5535684a45 | |||
| f933b7d99a | |||
| fd729b3019 | |||
| e1c1eafb39 | |||
| 1b016c49f2 | |||
| 4376069b11 | |||
| c285ae81af | |||
| 5bfe4866a4 | |||
| f7799675d5 | |||
| c125427b8d | |||
| a14a80f7bd | |||
| 477143b3ff | |||
| fc404f6755 | |||
| fe7c55eceb | |||
| bb01174bd2 | |||
| 38ce55d8fd | |||
| d439741339 | |||
| bc75cac6cd | |||
| 47c264e8e1 | |||
| 4d0ed116dd | |||
| ceb6e13447 | |||
| 7e9059a9aa | |||
| ccf31e4ef4 | |||
| 94e047c7ff | |||
| 57f2210c77 | |||
| 1745596d14 | |||
| e923624aec | |||
| 3649878b7d | |||
| 179e7d41b3 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -12,6 +12,7 @@ keys/
|
|||||||
node_modules/
|
node_modules/
|
||||||
package-lock.json
|
package-lock.json
|
||||||
package.json
|
package.json
|
||||||
|
AGENT*.md
|
||||||
# Diagram cache directory
|
# Diagram cache directory
|
||||||
media/diagram_cache/
|
media/diagram_cache/
|
||||||
.env
|
.env
|
||||||
|
|||||||
241
Documentation/ArgoCD.md
Normal file
241
Documentation/ArgoCD.md
Normal file
@@ -0,0 +1,241 @@
|
|||||||
|
# ArgoCD Configuration Documentation
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
This directory contains the ArgoCD application manifests for deploying the VorgabenUI application and its dependencies to Kubernetes.
|
||||||
|
|
||||||
|
## Files
|
||||||
|
|
||||||
|
### Application Manifests
|
||||||
|
|
||||||
|
#### `001_pvc.yaml`
|
||||||
|
- **Purpose**: PersistentVolumeClaim for Django application data
|
||||||
|
- **Storage**: 2Gi storage with ReadWriteMany access mode
|
||||||
|
- **Storage Class**: Uses NFS storage class for shared storage across multiple pods
|
||||||
|
- **Namespace**: vorgabenui
|
||||||
|
|
||||||
|
#### `deployment.yaml`
|
||||||
|
- **Purpose**: Main application deployment configuration
|
||||||
|
- **Contains**: Django application container, environment variables, resource limits
|
||||||
|
- **Replicas**: Configurable replica count for high availability
|
||||||
|
|
||||||
|
#### `ingress.yaml`
|
||||||
|
- **Purpose**: External access configuration
|
||||||
|
- **Host**: Configurable hostname for the application
|
||||||
|
- **TLS**: SSL/TLS termination configuration
|
||||||
|
- **Backend**: Routes traffic to the Django application service
|
||||||
|
|
||||||
|
#### `nfs-pv.yaml`
|
||||||
|
- **Purpose**: PersistentVolume definition for NFS storage
|
||||||
|
- **Server**: 192.168.17.199
|
||||||
|
- **Path**: /mnt/user/vorgabenui
|
||||||
|
- **Access**: ReadWriteMany for multi-pod access
|
||||||
|
- **Reclaim Policy**: Retain (data preserved after PVC deletion)
|
||||||
|
|
||||||
|
#### `nfs-storageclass.yaml`
|
||||||
|
- **Purpose**: StorageClass definition for NFS volumes
|
||||||
|
- **Provisioner**: kubernetes.io/no-provisioner (static provisioning)
|
||||||
|
- **Volume Expansion**: Enabled for growing storage capacity
|
||||||
|
- **Binding Mode**: Immediate (binds PV to PVC as soon as possible)
|
||||||
|
|
||||||
|
#### `diagrammer.yaml`
|
||||||
|
- **Purpose**: Deployment configuration for the diagram generation service
|
||||||
|
- **Function**: Handles diagram creation and caching for the application
|
||||||
|
|
||||||
|
## NFS Storage Configuration
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
1. NFS server must be running at 192.168.17.199
|
||||||
|
2. The directory `/mnt/user/vorgabenui` must exist and be exported
|
||||||
|
3. Kubernetes nodes must have NFS client utilities installed
|
||||||
|
4. For MicroK8s: `microk8s enable nfs`
|
||||||
|
|
||||||
|
## MicroK8s Addons Required
|
||||||
|
|
||||||
|
### Required Addons
|
||||||
|
Enable the following MicroK8s addons before deployment:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Enable storage and NFS support
|
||||||
|
sudo microk8s enable storage
|
||||||
|
sudo microk8s enable nfs
|
||||||
|
|
||||||
|
# Enable ingress for external access
|
||||||
|
sudo microk8s enable ingress
|
||||||
|
|
||||||
|
# Enable DNS for service discovery
|
||||||
|
sudo microk8s enable dns
|
||||||
|
|
||||||
|
# Optional: Enable metrics for monitoring
|
||||||
|
sudo microk8s enable metrics-server
|
||||||
|
```
|
||||||
|
|
||||||
|
### Addon Descriptions
|
||||||
|
|
||||||
|
#### `storage`
|
||||||
|
- **Purpose**: Provides default storage class for persistent volumes
|
||||||
|
- **Required for**: Basic PVC functionality
|
||||||
|
- **Note**: Works alongside our custom NFS storage class
|
||||||
|
|
||||||
|
#### `nfs`
|
||||||
|
- **Purpose**: Installs NFS client utilities on all MicroK8s nodes
|
||||||
|
- **Required for**: Mounting NFS volumes in pods
|
||||||
|
- **Components**: Installs `nfs-common` package with mount helpers
|
||||||
|
|
||||||
|
#### `ingress`
|
||||||
|
- **Purpose**: Provides Ingress controller for external HTTP/HTTPS access
|
||||||
|
- **Required for**: `ingress.yaml` to function properly
|
||||||
|
- **Implementation**: Uses NGINX Ingress Controller
|
||||||
|
|
||||||
|
#### `dns`
|
||||||
|
- **Purpose**: Provides DNS service for service discovery within cluster
|
||||||
|
- **Required for**: Inter-service communication
|
||||||
|
- **Note**: Usually enabled by default in MicroK8s
|
||||||
|
|
||||||
|
#### `metrics-server` (Optional)
|
||||||
|
- **Purpose**: Enables resource usage monitoring
|
||||||
|
- **Required for**: `kubectl top` commands and HPA (Horizontal Pod Autoscaling)
|
||||||
|
- **Recommended for**: Production monitoring
|
||||||
|
|
||||||
|
### Addon Verification
|
||||||
|
After enabling addons, verify they are running:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check addon status
|
||||||
|
microk8s status
|
||||||
|
|
||||||
|
# Check pods in kube-system namespace
|
||||||
|
microk8s kubectl get pods -n kube-system
|
||||||
|
|
||||||
|
# Check storage classes
|
||||||
|
microk8s kubectl get storageclass
|
||||||
|
|
||||||
|
# Check ingress controller
|
||||||
|
microk8s kubectl get pods -n ingress
|
||||||
|
```
|
||||||
|
|
||||||
|
### Troubleshooting Addons
|
||||||
|
|
||||||
|
#### NFS Addon Issues
|
||||||
|
```bash
|
||||||
|
# Check if NFS utilities are installed
|
||||||
|
which mount.nfs
|
||||||
|
|
||||||
|
# Manually install if addon fails
|
||||||
|
sudo apt update && sudo apt install nfs-common
|
||||||
|
|
||||||
|
# Restart MicroK8s after manual installation
|
||||||
|
sudo microk8s restart
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Ingress Issues
|
||||||
|
```bash
|
||||||
|
# Check ingress controller pods
|
||||||
|
microk8s kubectl get pods -n ingress
|
||||||
|
|
||||||
|
# Check ingress services
|
||||||
|
microk8s kubectl get svc -n ingress
|
||||||
|
|
||||||
|
# Test ingress connectivity
|
||||||
|
curl -k https://your-domain.com
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Storage Issues
|
||||||
|
```bash
|
||||||
|
# List available storage classes
|
||||||
|
microk8s kubectl get storageclass
|
||||||
|
|
||||||
|
# Check default storage class
|
||||||
|
microk8s kubectl get storageclass -o yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
### Storage Architecture
|
||||||
|
- **Storage Class**: `nfs` - Static provisioning for NFS shares
|
||||||
|
- **Persistent Volume**: Pre-provisioned PV pointing to NFS server
|
||||||
|
- **Persistent Volume Claim**: Claims the NFS storage for application use
|
||||||
|
- **Access Mode**: ReadWriteMany allows multiple pods to access the same data
|
||||||
|
|
||||||
|
### NFS Server Setup
|
||||||
|
On the NFS server (192.168.17.199), ensure the following:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create the shared directory
|
||||||
|
sudo mkdir -p /mnt/user/vorgabenui
|
||||||
|
sudo chmod 755 /mnt/user/vorgabenui
|
||||||
|
|
||||||
|
# Add to /etc/exports
|
||||||
|
echo "/mnt/user/vorgabenui *(rw,sync,no_subtree_check,no_root_squash)" | sudo tee -a /etc/exports
|
||||||
|
|
||||||
|
# Export the directory
|
||||||
|
sudo exportfs -a
|
||||||
|
sudo systemctl restart nfs-kernel-server
|
||||||
|
```
|
||||||
|
|
||||||
|
## Deployment Order
|
||||||
|
|
||||||
|
1. **StorageClass** (`nfs-storageclass.yaml`) - Defines NFS storage class
|
||||||
|
2. **PersistentVolume** (`nfs-pv.yaml`) - Creates the NFS volume
|
||||||
|
3. **PersistentVolumeClaim** (`001_pvc.yaml`) - Claims storage for application
|
||||||
|
4. **Application Deployments** (`deployment.yaml`, `diagrammer.yaml`) - Deploy application services
|
||||||
|
5. **Ingress** (`ingress.yaml`) - Configure external access
|
||||||
|
|
||||||
|
## Configuration Notes
|
||||||
|
|
||||||
|
### Namespace
|
||||||
|
All resources are deployed to the `vorgabenui` namespace.
|
||||||
|
|
||||||
|
### Storage Sizing
|
||||||
|
- Current allocation: 2Gi
|
||||||
|
- Volume expansion is enabled through the StorageClass
|
||||||
|
- Monitor usage and adjust PVC size as needed
|
||||||
|
|
||||||
|
### Access Control
|
||||||
|
- NFS export uses `no_root_squash` for container root access
|
||||||
|
- Ensure proper network security between Kubernetes nodes and NFS server
|
||||||
|
- Consider implementing network policies for additional security
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
|
||||||
|
#### Mount Failures
|
||||||
|
- **Error**: "bad option; for several filesystems you might need a /sbin/mount.<type> helper program"
|
||||||
|
- **Solution**: Install NFS client utilities or enable NFS addon in MicroK8s
|
||||||
|
|
||||||
|
#### Permission Issues
|
||||||
|
- **Error**: Permission denied when accessing mounted volume
|
||||||
|
- **Solution**: Check NFS export permissions and ensure `no_root_squash` is set
|
||||||
|
|
||||||
|
#### Network Connectivity
|
||||||
|
- **Error**: Connection timeout to NFS server
|
||||||
|
- **Solution**: Verify network connectivity and firewall rules between nodes and NFS server
|
||||||
|
|
||||||
|
### Debug Commands
|
||||||
|
```bash
|
||||||
|
# Check PVC status
|
||||||
|
kubectl get pvc -n vorgabenui
|
||||||
|
|
||||||
|
# Check PV status
|
||||||
|
kubectl get pv
|
||||||
|
|
||||||
|
# Describe PVC for detailed information
|
||||||
|
kubectl describe pvc django-data-pvc -n vorgabenui
|
||||||
|
|
||||||
|
# Check pod mount status
|
||||||
|
kubectl describe pod <pod-name> -n vorgabenui
|
||||||
|
```
|
||||||
|
|
||||||
|
## Maintenance
|
||||||
|
|
||||||
|
### Backup Strategy
|
||||||
|
- The NFS server should have regular backups of `/mnt/user/vorgabenui`
|
||||||
|
- Consider snapshot capabilities if using enterprise NFS solutions
|
||||||
|
|
||||||
|
### Monitoring
|
||||||
|
- Monitor NFS server performance and connectivity
|
||||||
|
- Track storage usage and plan capacity upgrades
|
||||||
|
- Monitor pod restarts related to storage issues
|
||||||
|
|
||||||
|
### Updates
|
||||||
|
- When updating storage configuration, update PV first, then PVC
|
||||||
|
- Test changes in non-production environment first
|
||||||
|
- Ensure backward compatibility when modifying NFS exports
|
||||||
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.
|
||||||
@@ -15,7 +15,7 @@ Dieses Dokument bietet einen umfassenden Überblick über alle Tests im vgui-cic
|
|||||||
|
|
||||||
## abschnitte App Tests
|
## abschnitte App Tests
|
||||||
|
|
||||||
Die abschnitte App enthält 32 Tests, die Modelle, Utility-Funktionen, Diagram-Caching und Management-Befehle abdecken.
|
Die abschnitte App enthält 33 Tests, die Modelle, Utility-Funktionen, Diagram-Caching, Management-Befehle und Sicherheit abdecken.
|
||||||
|
|
||||||
### Modell-Tests
|
### Modell-Tests
|
||||||
|
|
||||||
@@ -58,6 +58,7 @@ Die abschnitte App enthält 32 Tests, die Modelle, Utility-Funktionen, Diagram-C
|
|||||||
- **test_render_text_with_footnotes**: Verarbeitet Text, der Fußnoten enthält
|
- **test_render_text_with_footnotes**: Verarbeitet Text, der Fußnoten enthält
|
||||||
- **test_render_abschnitt_without_type**: Behandelt Textabschnitte ohne AbschnittTyp
|
- **test_render_abschnitt_without_type**: Behandelt Textabschnitte ohne AbschnittTyp
|
||||||
- **test_render_abschnitt_with_empty_content**: Behandelt Textabschnitte mit leerem Inhalt
|
- **test_render_abschnitt_with_empty_content**: Behandelt Textabschnitte mit leerem Inhalt
|
||||||
|
- **test_render_textabschnitte_xss_prevention**: Überprüft, dass bösartiger HTML-Code und Skript-Tags aus gerenderten Inhalten bereinigt werden, um XSS-Angriffe zu verhindern
|
||||||
|
|
||||||
### Diagram-Caching-Tests
|
### Diagram-Caching-Tests
|
||||||
|
|
||||||
@@ -332,8 +333,8 @@ Die stichworte App enthält 18 Tests, die Schlüsselwortmodelle und ihre Sortier
|
|||||||
|
|
||||||
## Test-Statistiken
|
## Test-Statistiken
|
||||||
|
|
||||||
- **Gesamt-Tests**: 206
|
- **Gesamt-Tests**: 207
|
||||||
- **abschnitte**: 32 Tests
|
- **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**: 116 Tests (98 in tests.py + 9 in test_json.py + 9 JSON-Tests in Haupt-tests.py)
|
||||||
- **pages**: 4 Tests
|
- **pages**: 4 Tests
|
||||||
- **referenzen**: 18 Tests
|
- **referenzen**: 18 Tests
|
||||||
@@ -348,6 +349,7 @@ Die stichworte App enthält 18 Tests, die Schlüsselwortmodelle und ihre Sortier
|
|||||||
4. **Utility-Funktionen**: Textverarbeitung, Caching, Formatierung
|
4. **Utility-Funktionen**: Textverarbeitung, Caching, Formatierung
|
||||||
5. **Management-Befehle**: CLI-Schnittstelle und Ausgabeverarbeitung
|
5. **Management-Befehle**: CLI-Schnittstelle und Ausgabeverarbeitung
|
||||||
6. **Integration**: App-übergreifende Funktionalität und Datenfluss
|
6. **Integration**: App-übergreifende Funktionalität und Datenfluss
|
||||||
|
7. **Sicherheit**: XSS-Prävention durch HTML-Bereinigung beim Rendern von Inhalten
|
||||||
|
|
||||||
## Ausführen der Tests
|
## Ausführen der Tests
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ This document provides a comprehensive overview of all tests in the vgui-cicd Dj
|
|||||||
|
|
||||||
## abschnitte App Tests
|
## abschnitte App Tests
|
||||||
|
|
||||||
The abschnitte app contains 32 tests covering models, utility functions, diagram caching, and management commands.
|
The abschnitte app contains 33 tests covering models, utility functions, diagram caching, management commands, and security.
|
||||||
|
|
||||||
### Model Tests
|
### Model Tests
|
||||||
|
|
||||||
@@ -58,6 +58,7 @@ The abschnitte app contains 32 tests covering models, utility functions, diagram
|
|||||||
- **test_render_text_with_footnotes**: Processes text containing footnotes
|
- **test_render_text_with_footnotes**: Processes text containing footnotes
|
||||||
- **test_render_abschnitt_without_type**: Handles Textabschnitte without AbschnittTyp
|
- **test_render_abschnitt_without_type**: Handles Textabschnitte without AbschnittTyp
|
||||||
- **test_render_abschnitt_with_empty_content**: Handles Textabschnitte with empty content
|
- **test_render_abschnitt_with_empty_content**: Handles Textabschnitte with empty content
|
||||||
|
- **test_render_textabschnitte_xss_prevention**: Verifies that malicious HTML and script tags are sanitized from rendered content to prevent XSS attacks
|
||||||
|
|
||||||
### Diagram Caching Tests
|
### Diagram Caching Tests
|
||||||
|
|
||||||
@@ -332,8 +333,8 @@ The stichworte app contains 18 tests covering keyword models and their ordering.
|
|||||||
|
|
||||||
## Test Statistics
|
## Test Statistics
|
||||||
|
|
||||||
- **Total Tests**: 206
|
- **Total Tests**: 207
|
||||||
- **abschnitte**: 32 tests
|
- **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**: 116 tests (98 in tests.py + 9 in test_json.py + 9 JSON tests in main tests.py)
|
||||||
- **pages**: 4 tests
|
- **pages**: 4 tests
|
||||||
- **referenzen**: 18 tests
|
- **referenzen**: 18 tests
|
||||||
@@ -348,6 +349,7 @@ The stichworte app contains 18 tests covering keyword models and their ordering.
|
|||||||
4. **Utility Functions**: Text processing, caching, formatting
|
4. **Utility Functions**: Text processing, caching, formatting
|
||||||
5. **Management Commands**: CLI interface and output handling
|
5. **Management Commands**: CLI interface and output handling
|
||||||
6. **Integration**: Cross-app functionality and data flow
|
6. **Integration**: Cross-app functionality and data flow
|
||||||
|
7. **Security**: XSS prevention through HTML sanitization in content rendering
|
||||||
|
|
||||||
## Running the Tests
|
## Running the Tests
|
||||||
|
|
||||||
|
|||||||
@@ -28,12 +28,6 @@ DEBUG = True
|
|||||||
|
|
||||||
ALLOWED_HOSTS = ["10.128.128.144","localhost","127.0.0.1","*"]
|
ALLOWED_HOSTS = ["10.128.128.144","localhost","127.0.0.1","*"]
|
||||||
|
|
||||||
TEMPLATES = [
|
|
||||||
{"BACKEND": "django.template.backends.django.DjangoTemplates",
|
|
||||||
"APP_DIRS": True,
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
# Application definition
|
# Application definition
|
||||||
|
|
||||||
INSTALLED_APPS = [
|
INSTALLED_APPS = [
|
||||||
@@ -152,6 +146,11 @@ DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
|||||||
DATA_UPLOAD_MAX_NUMBER_FIELDS=10250
|
DATA_UPLOAD_MAX_NUMBER_FIELDS=10250
|
||||||
NESTED_ADMIN_LAZY_INLINES = True
|
NESTED_ADMIN_LAZY_INLINES = True
|
||||||
|
|
||||||
|
# Authentication settings
|
||||||
|
LOGIN_URL = 'login'
|
||||||
|
LOGIN_REDIRECT_URL = '/'
|
||||||
|
LOGOUT_REDIRECT_URL = 'login'
|
||||||
|
|
||||||
#LOGGING = {
|
#LOGGING = {
|
||||||
# "version": 1,
|
# "version": 1,
|
||||||
# "handlers" :{
|
# "handlers" :{
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ from django.contrib import admin
|
|||||||
from django.urls import include, path, re_path
|
from django.urls import include, path, re_path
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.conf.urls.static import static
|
from django.conf.urls.static import static
|
||||||
|
from django.contrib.auth import views as auth_views
|
||||||
import dokumente.views
|
import dokumente.views
|
||||||
import pages.views
|
import pages.views
|
||||||
import referenzen.views
|
import referenzen.views
|
||||||
@@ -32,6 +33,11 @@ urlpatterns = [
|
|||||||
path('stichworte/', include("stichworte.urls")),
|
path('stichworte/', include("stichworte.urls")),
|
||||||
path('referenzen/', referenzen.views.tree, name="referenz_tree"),
|
path('referenzen/', referenzen.views.tree, name="referenz_tree"),
|
||||||
path('referenzen/<str:refid>/', referenzen.views.detail, name="referenz_detail"),
|
path('referenzen/<str:refid>/', 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
|
# Serve static files
|
||||||
|
|||||||
@@ -467,6 +467,32 @@ A -> B
|
|||||||
typ, html = result[0]
|
typ, html = result[0]
|
||||||
self.assertEqual(typ, "text")
|
self.assertEqual(typ, "text")
|
||||||
|
|
||||||
|
def test_render_textabschnitte_xss_prevention(self):
|
||||||
|
"""Test that malicious HTML is sanitized in rendered content"""
|
||||||
|
from dokumente.models import VorgabeLangtext
|
||||||
|
|
||||||
|
# Create content with malicious HTML
|
||||||
|
malicious_abschnitt = VorgabeLangtext.objects.create(
|
||||||
|
abschnitt=self.vorgabe,
|
||||||
|
abschnitttyp=self.typ_text,
|
||||||
|
inhalt='<script>alert("xss")</script><img src=x onerror=alert(1)>Normal text',
|
||||||
|
order=1
|
||||||
|
)
|
||||||
|
|
||||||
|
result = render_textabschnitte(VorgabeLangtext.objects.filter(pk=malicious_abschnitt.pk))
|
||||||
|
|
||||||
|
self.assertEqual(len(result), 1)
|
||||||
|
typ, html = result[0]
|
||||||
|
self.assertEqual(typ, "text")
|
||||||
|
|
||||||
|
# Dangerous tags and attributes should be removed or sanitized
|
||||||
|
self.assertNotIn('<script>', html) # Script tags should not be present unescaped
|
||||||
|
self.assertNotIn('onerror', html) # Dangerous attributes removed
|
||||||
|
# Note: 'alert' may still be present in escaped script tags, which is safe
|
||||||
|
|
||||||
|
# Safe content should remain
|
||||||
|
self.assertIn('Normal text', html)
|
||||||
|
|
||||||
|
|
||||||
class MdTableToHtmlTest(TestCase):
|
class MdTableToHtmlTest(TestCase):
|
||||||
"""Test cases for md_table_to_html function"""
|
"""Test cases for md_table_to_html function"""
|
||||||
|
|||||||
@@ -4,12 +4,34 @@ import zlib
|
|||||||
import re
|
import re
|
||||||
from textwrap import dedent
|
from textwrap import dedent
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
import bleach
|
||||||
|
|
||||||
# Import the caching function
|
# Import the caching function
|
||||||
from diagramm_proxy.diagram_cache import get_cached_diagram
|
from diagramm_proxy.diagram_cache import get_cached_diagram
|
||||||
|
|
||||||
DIAGRAMMSERVER="/diagramm"
|
DIAGRAMMSERVER="/diagramm"
|
||||||
|
|
||||||
|
# Allowed HTML tags for bleach sanitization
|
||||||
|
ALLOWED_TAGS = [
|
||||||
|
'p', 'br', 'strong', 'em', 'u', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
|
||||||
|
'ul', 'ol', 'li', 'blockquote', 'code', 'pre', 'hr',
|
||||||
|
'table', 'thead', 'tbody', 'tr', 'th', 'td',
|
||||||
|
'img', 'a', 'sup', 'sub', 'span', 'div'
|
||||||
|
]
|
||||||
|
|
||||||
|
ALLOWED_ATTRIBUTES = {
|
||||||
|
'img': ['src', 'alt', 'width', 'height'],
|
||||||
|
'a': ['href', 'title'],
|
||||||
|
'span': ['class'],
|
||||||
|
'div': ['class'],
|
||||||
|
'p': ['class'],
|
||||||
|
'table': ['class'],
|
||||||
|
'th': ['colspan', 'rowspan', 'class'],
|
||||||
|
'td': ['colspan', 'rowspan', 'class'],
|
||||||
|
'pre': ['class'],
|
||||||
|
'code': ['class'],
|
||||||
|
}
|
||||||
|
|
||||||
def render_textabschnitte(queryset):
|
def render_textabschnitte(queryset):
|
||||||
"""
|
"""
|
||||||
Converts a queryset of Textabschnitt-like models into a list of (typ, html) tuples.
|
Converts a queryset of Textabschnitt-like models into a list of (typ, html) tuples.
|
||||||
@@ -52,6 +74,8 @@ def render_textabschnitte(queryset):
|
|||||||
html += "</code></pre>"
|
html += "</code></pre>"
|
||||||
else:
|
else:
|
||||||
html = markdown(inhalt, extensions=['tables', 'attr_list','footnotes'])
|
html = markdown(inhalt, extensions=['tables', 'attr_list','footnotes'])
|
||||||
|
# Sanitize HTML to prevent XSS
|
||||||
|
html = bleach.clean(html, tags=ALLOWED_TAGS, attributes=ALLOWED_ATTRIBUTES)
|
||||||
output.append((typ, html))
|
output.append((typ, html))
|
||||||
return output
|
return output
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ metadata:
|
|||||||
namespace: vorgabenui
|
namespace: vorgabenui
|
||||||
spec:
|
spec:
|
||||||
accessModes:
|
accessModes:
|
||||||
- ReadWriteOnce
|
- ReadWriteMany
|
||||||
|
storageClassName: nfs
|
||||||
resources:
|
resources:
|
||||||
requests:
|
requests:
|
||||||
storage: 2Gi
|
storage: 2Gi
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ spec:
|
|||||||
mountPath: /data
|
mountPath: /data
|
||||||
containers:
|
containers:
|
||||||
- name: web
|
- name: web
|
||||||
image: git.baumann.gr/adebaumann/vui:0.952-oblique
|
image: git.baumann.gr/adebaumann/vui:0.957-xss
|
||||||
imagePullPolicy: Always
|
imagePullPolicy: Always
|
||||||
ports:
|
ports:
|
||||||
- containerPort: 8000
|
- containerPort: 8000
|
||||||
@@ -63,6 +63,8 @@ spec:
|
|||||||
selector:
|
selector:
|
||||||
app: django
|
app: django
|
||||||
ports:
|
ports:
|
||||||
- port: 8000
|
- name: http
|
||||||
|
protocol: TCP
|
||||||
|
port: 8000
|
||||||
targetPort: 8000
|
targetPort: 8000
|
||||||
|
|
||||||
|
|||||||
@@ -4,8 +4,9 @@ metadata:
|
|||||||
name: django
|
name: django
|
||||||
namespace: vorgabenui
|
namespace: vorgabenui
|
||||||
annotations:
|
annotations:
|
||||||
traefik.ingress.kubernetes.io/router.middlewares: "vorgabenui-vorgabenui-rewrite@kubernetescrd"
|
argocd.argoproj.io/ignore-healthcheck: "true"
|
||||||
spec:
|
spec:
|
||||||
|
ingressClassName: traefik
|
||||||
rules:
|
rules:
|
||||||
- host: vorgabenportal.knowyoursecurity.com
|
- host: vorgabenportal.knowyoursecurity.com
|
||||||
http:
|
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.
@@ -19,37 +19,13 @@
|
|||||||
<strong>Historische Version vom {{ standard.check_date }}</strong>
|
<strong>Historische Version vom {{ standard.check_date }}</strong>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<!-- Metadata -->
|
|
||||||
<div class="row mb-4">
|
|
||||||
<div class="col-md-12">
|
|
||||||
<dl class="row">
|
|
||||||
<dt class="col-sm-3">Autoren:</dt>
|
|
||||||
<dd class="col-sm-9">{{ standard.autoren.all|join:", " }}</dd>
|
|
||||||
|
|
||||||
<dt class="col-sm-3">Prüfende:</dt>
|
|
||||||
<dd class="col-sm-9">{{ standard.pruefende.all|join:", " }}</dd>
|
|
||||||
|
|
||||||
<dt class="col-sm-3">Gültigkeit:</dt>
|
|
||||||
<dd class="col-sm-9">{{ standard.gueltigkeit_von }} bis {{ standard.gueltigkeit_bis|default_if_none:"auf weiteres" }}</dd>
|
|
||||||
</dl>
|
|
||||||
<p>
|
|
||||||
<a href="{% url 'standard_json' standard.nummer %}"
|
|
||||||
class="btn btn-secondary icon icon--before icon--download"
|
|
||||||
download="{{ standard.nummer }}.json">
|
|
||||||
JSON herunterladen
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Einleitung -->
|
<!-- Einleitung -->
|
||||||
{% if standard.einleitung_html %}
|
{% if standard.einleitung_html %}
|
||||||
<div class="row mb-4">
|
<div class="row mb-4">
|
||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h2 class="h4 mb-0">Einleitung</h2>
|
<h2>Einleitung</h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
{% for typ, html in standard.einleitung_html %}
|
{% for typ, html in standard.einleitung_html %}
|
||||||
@@ -67,7 +43,7 @@
|
|||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h2 class="h4 mb-0">Geltungsbereich</h2>
|
<h2>Geltungsbereich</h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
{% for typ, html in standard.geltungsbereich_html %}
|
{% for typ, html in standard.geltungsbereich_html %}
|
||||||
@@ -97,7 +73,7 @@
|
|||||||
<a id="{{ vorgabe.Vorgabennummer }}"></a>
|
<a id="{{ vorgabe.Vorgabennummer }}"></a>
|
||||||
<div class="card mb-4">
|
<div class="card mb-4">
|
||||||
<div class="card-header" style="display: flex; justify-content: space-between; align-items: center;">
|
<div class="card-header" style="display: flex; justify-content: space-between; align-items: center;">
|
||||||
<h3 class="h5 mb-0">
|
<h3>
|
||||||
{{ vorgabe.Vorgabennummer }} – {{ vorgabe.titel }}
|
{{ vorgabe.Vorgabennummer }} – {{ vorgabe.titel }}
|
||||||
{% if vorgabe.long_status != "active" and standard.history == True %}
|
{% if vorgabe.long_status != "active" and standard.history == True %}
|
||||||
<span class="badge badge-danger">{{ vorgabe.long_status }}</span>
|
<span class="badge badge-danger">{{ vorgabe.long_status }}</span>
|
||||||
@@ -147,7 +123,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<!-- Stichworte und Referenzen -->
|
<!-- 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">
|
<p class="mb-2">
|
||||||
<strong>Stichworte:</strong>
|
<strong>Stichworte:</strong>
|
||||||
{% if vorgabe.stichworte.all %}
|
{% if vorgabe.stichworte.all %}
|
||||||
@@ -175,5 +151,29 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
<!-- Metadata -->
|
||||||
|
|
||||||
|
<h2>Metadaten</h2>
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<dl class="row">
|
||||||
|
<dt class="col-sm-3">Autoren:</dt>
|
||||||
|
<dd class="col-sm-9">{{ standard.autoren.all|join:", " }}</dd>
|
||||||
|
|
||||||
|
<dt class="col-sm-3">Prüfende:</dt>
|
||||||
|
<dd class="col-sm-9">{{ standard.pruefende.all|join:", " }}</dd>
|
||||||
|
|
||||||
|
<dt class="col-sm-3">Gültigkeit:</dt>
|
||||||
|
<dd class="col-sm-9">{{ standard.gueltigkeit_von }} bis {{ standard.gueltigkeit_bis|default_if_none:"auf weiteres" }}</dd>
|
||||||
|
</dl>
|
||||||
|
<p>
|
||||||
|
<a href="{% url 'standard_json' standard.nummer %}"
|
||||||
|
class="btn btn-secondary icon icon--before icon--download"
|
||||||
|
download="{{ standard.nummer }}.json">
|
||||||
|
JSON herunterladen
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
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
|
namespace: vorgabenui
|
||||||
spec:
|
spec:
|
||||||
accessModes:
|
accessModes:
|
||||||
- ReadWriteOnce
|
- ReadWriteMany
|
||||||
|
storageClassName: nfs
|
||||||
resources:
|
resources:
|
||||||
requests:
|
requests:
|
||||||
storage: 2Gi
|
storage: 2Gi
|
||||||
|
|||||||
5
openspec/changes/archive/2025-11-24-add-login/tasks.md
Normal file
5
openspec/changes/archive/2025-11-24-add-login/tasks.md
Normal file
@@ -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
|
||||||
63
openspec/project.md
Normal file
63
openspec/project.md
Normal file
@@ -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)
|
||||||
@@ -41,6 +41,37 @@
|
|||||||
alt="Zur Startseite" />
|
alt="Zur Startseite" />
|
||||||
<h1>Vorgaben Informatiksicherheit BIT</h1>
|
<h1>Vorgaben Informatiksicherheit BIT</h1>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
<!-- User Menu -->
|
||||||
|
{% if user.is_authenticated %}
|
||||||
|
<div class="user-menu" style="position: absolute; top: 20px; right: 20px; z-index: 1000;">
|
||||||
|
<div class="dropdown">
|
||||||
|
<a href="#" class="dropdown-toggle" data-toggle="dropdown" style="text-decoration: none; color: #000; display: flex; align-items: center;">
|
||||||
|
<span style="font-size: 24px; margin-right: 8px;">👤</span>
|
||||||
|
<span class="hidden-xs" style="margin-left: 0;">{{ user.first_name }} {{ user.last_name }}</span>
|
||||||
|
<span class="caret" style="margin-left: 8px;"></span>
|
||||||
|
</a>
|
||||||
|
<ul class="dropdown-menu dropdown-menu-right" role="menu">
|
||||||
|
<li><a href="{% url 'password_change' %}">Passwort ändern</a></li>
|
||||||
|
<li class="divider"></li>
|
||||||
|
<li>
|
||||||
|
<form method="post" action="{% url 'logout' %}" style="display: inline;">
|
||||||
|
{% csrf_token %}
|
||||||
|
<button type="submit" style="background: none; border: none; color: inherit; padding: 3px 20px; width: 100%; text-align: left; cursor: pointer;">
|
||||||
|
Abmelden
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="user-menu" style="position: absolute; top: 20px; right: 20px; z-index: 1000;">
|
||||||
|
<a href="{% url 'login' %}" class="btn btn-sm btn-primary" style="text-decoration: none;">
|
||||||
|
Anmelden
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<!-- Main Navigation -->
|
<!-- Main Navigation -->
|
||||||
@@ -71,6 +102,7 @@
|
|||||||
<li><a href="/dokumente">Standards</a></li>
|
<li><a href="/dokumente">Standards</a></li>
|
||||||
{% if user.is_staff %}
|
{% if user.is_staff %}
|
||||||
<li><a href="/dokumente/unvollstaendig/">Unvollständig</a></li>
|
<li><a href="/dokumente/unvollstaendig/">Unvollständig</a></li>
|
||||||
|
<li><a href="/autorenumgebung/">Autorenumgebung</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<li><a href="/referenzen">Referenzen</a></li>
|
<li><a href="/referenzen">Referenzen</a></li>
|
||||||
<li><a href="/stichworte">Stichworte</a></li>
|
<li><a href="/stichworte">Stichworte</a></li>
|
||||||
@@ -100,6 +132,9 @@
|
|||||||
<li class="dropdown {% if 'unvollstaendig' in request.path %}current{% endif %}">
|
<li class="dropdown {% if 'unvollstaendig' in request.path %}current{% endif %}">
|
||||||
<a href="/dokumente/unvollstaendig/">Unvollständig</a>
|
<a href="/dokumente/unvollstaendig/">Unvollständig</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li class="dropdown {% if 'autorenumgebung' in request.path %}current{% endif %}">
|
||||||
|
<a href="/autorenumgebung/">Autorenumgebung</a>
|
||||||
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<li class="dropdown {% if 'referenzen' in request.path %}current{% endif %}">
|
<li class="dropdown {% if 'referenzen' in request.path %}current{% endif %}">
|
||||||
<a href="/referenzen">Referenzen</a>
|
<a href="/referenzen">Referenzen</a>
|
||||||
@@ -180,7 +215,7 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-sm-6 text-right">
|
<div class="col-sm-6 text-right">
|
||||||
<p class="text-muted">Version {{ version|default:"0.951" }}</p>
|
<p class="text-muted">Version {{ version|default:"0.957-xss" }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
43
pages/templates/registration/login.html
Normal file
43
pages/templates/registration/login.html
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block title %}Anmelden{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-4 col-md-offset-4">
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<h3 class="panel-title">Anmelden</h3>
|
||||||
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
|
<form method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
|
||||||
|
{% if form.errors %}
|
||||||
|
<div class="alert alert-danger">
|
||||||
|
<p>Ihr Benutzername und Passwort stimmen nicht überein. Bitte versuchen Sie es erneut.</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="id_username">Benutzername:</label>
|
||||||
|
<input type="text" name="username" class="form-control" id="id_username" required autofocus>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="id_password">Passwort:</label>
|
||||||
|
<input type="password" name="password" class="form-control" id="id_password" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<button type="submit" class="btn btn-primary">Anmelden</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input type="hidden" name="next" value="{{ next }}">
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
56
pages/templates/registration/password_change.html
Normal file
56
pages/templates/registration/password_change.html
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block title %}Passwort ändern{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6 col-md-offset-3">
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<h3 class="panel-title">Passwort ändern</h3>
|
||||||
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
|
<form method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
|
||||||
|
{% if form.errors %}
|
||||||
|
<div class="alert alert-danger">
|
||||||
|
<p>Bitte korrigieren Sie die Fehler unten.</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="id_old_password">Aktuelles Passwort:</label>
|
||||||
|
<input type="password" name="old_password" class="form-control" id="id_old_password" required>
|
||||||
|
{% if form.old_password.errors %}
|
||||||
|
<div class="text-danger">{{ form.old_password.errors }}</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="id_new_password1">Neues Passwort:</label>
|
||||||
|
<input type="password" name="new_password1" class="form-control" id="id_new_password1" required>
|
||||||
|
{% if form.new_password1.errors %}
|
||||||
|
<div class="text-danger">{{ form.new_password1.errors }}</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="id_new_password2">Neues Passwort bestätigen:</label>
|
||||||
|
<input type="password" name="new_password2" class="form-control" id="id_new_password2" required>
|
||||||
|
{% if form.new_password2.errors %}
|
||||||
|
<div class="text-danger">{{ form.new_password2.errors }}</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<button type="submit" class="btn btn-primary">Passwort ändern</button>
|
||||||
|
<a href="/" class="btn btn-default">Abbrechen</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
24
pages/templates/registration/password_change_done.html
Normal file
24
pages/templates/registration/password_change_done.html
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block title %}Passwort geändert{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6 col-md-offset-3">
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<h3 class="panel-title">Passwort erfolgreich geändert</h3>
|
||||||
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
|
<div class="alert alert-success">
|
||||||
|
<p>Ihr Passwort wurde erfolgreich geändert.</p>
|
||||||
|
</div>
|
||||||
|
<p>
|
||||||
|
<a href="/" class="btn btn-primary">Zurück zur Startseite</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
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 datetime import date, timedelta
|
||||||
from dokumente.models import Dokument, Vorgabe, VorgabeKurztext, VorgabeLangtext, Geltungsbereich, Dokumententyp, Thema
|
from dokumente.models import Dokument, Vorgabe, VorgabeKurztext, VorgabeLangtext, Geltungsbereich, Dokumententyp, Thema
|
||||||
from stichworte.models import Stichwort
|
from stichworte.models import Stichwort
|
||||||
from unittest.mock import patch
|
|
||||||
import re
|
import re
|
||||||
|
|
||||||
|
|
||||||
@@ -67,24 +66,24 @@ class SearchViewTest(TestCase):
|
|||||||
"""Test POST request with valid search term"""
|
"""Test POST request with valid search term"""
|
||||||
response = self.client.post('/search/', {'q': 'Test'})
|
response = self.client.post('/search/', {'q': 'Test'})
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertContains(response, 'Suchresultate für Test')
|
self.assertContains(response, 'Suchergebnisse')
|
||||||
|
|
||||||
def test_search_case_insensitive(self):
|
def test_search_case_insensitive(self):
|
||||||
"""Test that search is case insensitive"""
|
"""Test that search is case insensitive"""
|
||||||
# Search for lowercase
|
# Search for lowercase
|
||||||
response = self.client.post('/search/', {'q': 'test'})
|
response = self.client.post('/search/', {'q': 'test'})
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertContains(response, 'Suchresultate für test')
|
self.assertContains(response, 'Suchergebnisse für "test"')
|
||||||
|
|
||||||
# Search for uppercase
|
# Search for uppercase
|
||||||
response = self.client.post('/search/', {'q': 'TEST'})
|
response = self.client.post('/search/', {'q': 'TEST'})
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertContains(response, 'Suchresultate für TEST')
|
self.assertContains(response, 'Suchergebnisse für "TEST"')
|
||||||
|
|
||||||
# Search for mixed case
|
# Search for mixed case
|
||||||
response = self.client.post('/search/', {'q': 'TeSt'})
|
response = self.client.post('/search/', {'q': 'TeSt'})
|
||||||
self.assertEqual(response.status_code, 200)
|
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):
|
def test_search_in_kurztext(self):
|
||||||
"""Test search in Kurztext content"""
|
"""Test search in Kurztext content"""
|
||||||
@@ -114,7 +113,7 @@ class SearchViewTest(TestCase):
|
|||||||
"""Test search with no results"""
|
"""Test search with no results"""
|
||||||
response = self.client.post('/search/', {'q': 'NichtVorhanden'})
|
response = self.client.post('/search/', {'q': 'NichtVorhanden'})
|
||||||
self.assertEqual(response.status_code, 200)
|
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):
|
def test_search_expired_vorgabe_not_included(self):
|
||||||
"""Test that expired Vorgaben are not included in results"""
|
"""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"""
|
"""Test that HTML tags are stripped from search input"""
|
||||||
response = self.client.post('/search/', {'q': '<script>alert("xss")</script>Test'})
|
response = self.client.post('/search/', {'q': '<script>alert("xss")</script>Test'})
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
# Should search for "alert('xss')Test" after HTML tag removal
|
# Should search for "alert("xss")Test" after HTML tag removal
|
||||||
self.assertContains(response, 'Suchresultate für alert("xss")Test')
|
self.assertContains(response, 'Suchergebnisse für "alert')
|
||||||
|
|
||||||
def test_search_invalid_characters_validation(self):
|
def test_search_invalid_characters_validation(self):
|
||||||
"""Test validation for invalid characters"""
|
"""Test validation for invalid characters"""
|
||||||
@@ -206,7 +205,7 @@ class SearchViewTest(TestCase):
|
|||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
# The input should be preserved (escaped) in the form
|
# The input should be preserved (escaped) in the form
|
||||||
# Since HTML tags are stripped, we expect "Test" to be searched
|
# 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):
|
def test_search_xss_prevention_in_results(self):
|
||||||
"""Test that search terms are escaped in results to prevent XSS"""
|
"""Test that search terms are escaped in results to prevent XSS"""
|
||||||
@@ -218,15 +217,14 @@ class SearchViewTest(TestCase):
|
|||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
# The script tag should be escaped in the output
|
# The script tag should be escaped in the output
|
||||||
# Note: This depends on how the template renders the content
|
# 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_structure(self):
|
||||||
def test_search_result_logging(self, mock_pprint):
|
"""Test that search results have expected structure"""
|
||||||
"""Test that search results are logged for debugging"""
|
|
||||||
response = self.client.post('/search/', {'q': 'Test'})
|
response = self.client.post('/search/', {'q': 'Test'})
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
# Verify that pprint.pp was called with the result
|
# Verify the results page is rendered with correct structure
|
||||||
mock_pprint.assert_called_once()
|
self.assertContains(response, 'Suchergebnisse für "Test"')
|
||||||
|
|
||||||
def test_search_multiple_documents(self):
|
def test_search_multiple_documents(self):
|
||||||
"""Test search across multiple documents"""
|
"""Test search across multiple documents"""
|
||||||
|
|||||||
@@ -32,3 +32,4 @@ six==1.17.0
|
|||||||
sqlparse==0.5.3
|
sqlparse==0.5.3
|
||||||
urllib3==2.5.0
|
urllib3==2.5.0
|
||||||
wcwidth==0.2.13
|
wcwidth==0.2.13
|
||||||
|
bleach==6.1.0
|
||||||
|
|||||||
Reference in New Issue
Block a user