diff --git a/Test Suite-DE.md b/Test Suite-DE.md new file mode 100644 index 0000000..7d2ac46 --- /dev/null +++ b/Test Suite-DE.md @@ -0,0 +1,354 @@ +# Test-Suite Dokumentation + +Dieses Dokument bietet einen umfassenden Überblick über alle Tests im vgui-cicd Django-Projekt und beschreibt, was jeder Test tut und wie er funktioniert. + +## Inhaltsverzeichnis + +- [abschnitte App Tests](#abschnitte-app-tests) +- [dokumente App Tests](#dokumente-app-tests) +- [pages App Tests](#pages-app-tests) +- [referenzen App Tests](#referenzen-app-tests) +- [rollen App Tests](#rollen-app-tests) +- [stichworte App Tests](#stichworte-app-tests) + +--- + +## abschnitte App Tests + +Die abschnitte App enthält 32 Tests, die Modelle, Utility-Funktionen, Diagram-Caching und Management-Befehle abdecken. + +### Modell-Tests + +#### AbschnittTypModelTest +- **test_abschnitttyp_creation**: Überprüft, dass AbschnittTyp-Objekte korrekt mit den erwarteten Feldwerten erstellt werden +- **test_abschnitttyp_primary_key**: Bestätigt, dass das `abschnitttyp`-Feld als Primärschlüssel dient +- **test_abschnitttyp_str**: Testet die String-Repräsentation, die den `abschnitttyp`-Wert zurückgibt +- **test_abschnitttyp_verbose_name_plural**: Validiert den korrekt gesetzten verbose_name_plural +- **test_create_multiple_abschnitttypen**: Stellt sicher, dass mehrere AbschnittTyp-Objekte mit verschiedenen Typen erstellt werden können + +#### TextabschnittModelTest +- **test_textabschnitt_creation**: Testet, dass Textabschnitt über das konkrete Modell instanziiert werden kann +- **test_textabschnitt_default_order**: Überprüft, dass das `order`-Feld standardmäßig 0 ist +- **test_textabschnitt_ordering**: Testet, dass Textabschnitt-Objekte nach dem `order`-Feld sortiert werden können +- **test_textabschnitt_blank_fields**: Bestätigt, dass `abschnitttyp`- und `inhalt`-Felder leer/null sein können +- **test_textabschnitt_foreign_key_protection**: Testet, dass AbschnittTyp-Objekte vor Löschung geschützt sind, wenn sie von Textabschnitt referenziert werden + +### Utility-Funktions-Tests + +#### MdTableToHtmlTest +- **test_simple_table**: Konvertiert eine einfache Markdown-Tabelle mit Überschriften und einer Zeile nach HTML +- **test_table_with_multiple_rows**: Testet die Konvertierung von Tabellen mit mehreren Datenzeilen +- **test_table_with_empty_cells**: Verarbeitet Tabellen mit leeren Zellen in den Daten +- **test_table_with_spaces**: Verarbeitet Tabellen mit zusätzlichen Leerzeichen in Zellen +- **test_table_empty_string**: Löst ValueError für leere Eingabe-Strings aus +- **test_table_only_whitespace**: Löst ValueError für Strings aus, die nur Leerzeichen enthalten +- **test_table_insufficient_lines**: Löst ValueError aus, wenn die Eingabe weniger als 2 Zeilen hat + +#### RenderTextabschnitteTest +- **test_render_empty_queryset**: Gibt leeren String für leere Querysets zurück +- **test_render_multiple_abschnitte**: Rendert mehrere Textabschnitte in korrekter Reihenfolge +- **test_render_text_markdown**: Konvertiert Klartext mit Markdown-Formatierung +- **test_render_ordered_list**: Rendert geordnete Listen korrekt +- **test_render_unordered_list**: Rendert ungeordnete Listen korrekt +- **test_render_code_block**: Rendert Code-Blöcke mit korrekter Syntax-Hervorhebung +- **test_render_table**: Konvertiert Markdown-Tabellen mit md_table_to_html nach HTML +- **test_render_diagram_success**: Testet die Diagramm-Generierung mit erfolgreichem Caching +- **test_render_diagram_error**: Behandelt Diagramm-Generierungsfehler angemessen +- **test_render_diagram_with_options**: Testet das Diagramm-Rendering mit benutzerdefinierten Optionen +- **test_render_text_with_footnotes**: Verarbeitet Text, der Fußnoten enthält +- **test_render_abschnitt_without_type**: Behandelt Textabschnitte ohne AbschnittTyp +- **test_render_abschnitt_with_empty_content**: Behandelt Textabschnitte mit leerem Inhalt + +### Diagram-Caching-Tests + +#### DiagramCacheTest +- **test_compute_hash**: Generiert konsistente SHA256-Hashes für dieselbe Eingabe +- **test_get_cache_path**: Erstellt korrekte Cache-Dateipfade basierend auf Hash und Typ +- **test_get_cached_diagram_hit**: Gibt zwischengespeichertes Diagramm zurück bei Cache-Treffer +- **test_get_cached_diagram_miss**: Generiert neues Diagramm bei Cache-Fehltreffer +- **test_get_cached_diagram_request_error**: Behandelt und löst Request-Fehler korrekt aus +- **test_clear_cache_specific_type**: Löscht Cache-Dateien für spezifische Diagrammtypen +- **test_clear_cache_all_types**: Löscht alle Cache-Dateien, wenn kein Typ angegeben ist + +### Management-Befehl-Tests + +#### ClearDiagramCacheCommandTest +- **test_command_without_type**: Testet die Ausführung des Management-Befehls ohne Angabe des Typs +- **test_command_with_type**: Testet die Ausführung des Management-Befehls mit spezifischem Diagrammtyp + +### Integrations-Tests + +#### IntegrationTest +- **test_textabschnitt_inheritance**: Überprüft, dass VorgabeLangtext Textabschnitt-Felder korrekt erbt +- **test_render_vorgabe_langtext**: Testet das Rendern von VorgabeLangtext durch render_textabschnitte + +--- + +## dokumente App Tests + +Die dokumente App enthält 98 Tests und ist damit die umfassendste Test-Suite, die alle Modelle, Views, URLs und Geschäftslogik abdeckt. + +### Modell-Tests + +#### DokumententypModelTest +- **test_dokumententyp_creation**: Überprüft die Erstellung von Dokumententyp mit korrekten Feldwerten +- **test_dokumententyp_str**: Testet die String-Repräsentation, die das `typ`-Feld zurückgibt +- **test_dokumententyp_verbose_name**: Validiert den korrekt gesetzten verbose_name + +#### PersonModelTest +- **test_person_creation**: Testet die Erstellung von Person-Objekten mit Name und optionalem Titel +- **test_person_str**: Überprüft, dass die String-Repräsentation Titel und Namen enthält +- **test_person_verbose_name_plural**: Testet die Konfiguration von verbose_name_plural + +#### ThemaModelTest +- **test_thema_creation**: Testet die Erstellung von Thema mit Name und optionaler Erklärung +- **test_thema_str**: Überprüft, dass die String-Repräsentation den Themennamen zurückgibt +- **test_thema_blank_erklaerung**: Bestätigt, dass das `erklaerung`-Feld leer sein kann + +#### DokumentModelTest +- **test_dokument_creation**: Testet die Erstellung von Dokument mit erforderlichen und optionalen Feldern +- **test_dokument_str**: Überprüft, dass die String-Repräsentation den Dokumenttitel zurückgibt +- **test_dokument_optional_fields**: Testet, dass optionale Felder None oder leer sein können +- **test_dokument_many_to_many_relationships**: Überprüft Many-to-Many-Beziehungen mit Personen und Themen + +#### VorgabeModelTest +- **test_vorgabe_creation**: Testet die Erstellung von Vorgabe mit allen erforderlichen Feldern +- **test_vorgabe_str**: Überprüft, dass die String-Repräsentation die Vorgabennummer zurückgibt +- **test_vorgabennummer**: Testet die automatische Generierung des Vorgabennummer-Formats +- **test_get_status_active**: Testet die Statusbestimmung für aktuelle aktive Vorgaben +- **test_get_status_expired**: Testet die Statusbestimmung für abgelaufene Vorgaben +- **test_get_status_future**: Testet die Statusbestimmung für zukünftige Vorgaben +- **test_get_status_with_custom_check_date**: Testet den Status mit benutzerdefiniertem Prüfdatum +- **test_get_status_verbose**: Testet die ausführliche Statusausgabe + +#### ChangelogModelTest +- **test_changelog_creation**: Testet die Erstellung von Changelog mit Version, Datum und Beschreibung +- **test_changelog_str**: Überprüft, dass die String-Repräsentation Version und Datum enthält + +#### ChecklistenfrageModelTest +- **test_checklistenfrage_creation**: Testet die Erstellung von Checklistenfrage mit Frage und optionaler Antwort +- **test_checklistenfrage_str**: Überprüft, dass die String-Repräsentation lange Fragen kürzt +- **test_checklistenfrage_related_name**: Testet die umgekehrte Beziehung von Vorgabe + +### Text-Abschnitt-Tests + +#### DokumentTextAbschnitteTest +- **test_einleitung_creation**: Testet die Erstellung von Einleitung und Vererbung von Textabschnitt +- **test_geltungsbereich_creation**: Testet die Erstellung von Geltungsbereich und Vererbung + +#### VorgabeTextAbschnitteTest +- **test_vorgabe_kurztext_creation**: Testet die Erstellung von VorgabeKurztext und Vererbung +- **test_vorgabe_langtext_creation**: Testet die Erstellung von VorgabeLangtext und Vererbung + +### Sanity-Check-Tests + +#### VorgabeSanityCheckTest +- **test_date_ranges_intersect_no_overlap**: Testet Datumsüberschneidung mit nicht überlappenden Bereichen +- **test_date_ranges_intersect_with_overlap**: Testet Datumsüberschneidung mit überlappenden Bereichen +- **test_date_ranges_intersect_identical_ranges**: Testet Datumsüberschneidung mit identischen Bereichen +- **test_date_ranges_intersect_with_none_end_date**: Testet Überschneidung mit offenen Endbereichen +- **test_date_ranges_intersect_both_none_end_dates**: Testet Überschneidung mit zwei offenen Endbereichen +- **test_check_vorgabe_conflicts_utility**: Testet die Utility-Funktion zur Konflikterkennung +- **test_find_conflicts_no_conflicts**: Testet die Konflikterkennung bei Vorgabe ohne Konflikte +- **test_find_conflicts_with_conflicts**: Testet die Konflikterkennung mit konfliktbehafteten Vorgaben +- **test_format_conflict_report_no_conflicts**: Testet die Konfliktbericht-Formatierung ohne Konflikte +- **test_format_conflict_report_with_conflicts**: Testet die Konfliktbericht-Formatierung mit Konflikten +- **test_sanity_check_vorgaben_no_conflicts**: Testet vollständigen Sanity-Check ohne Konflikte +- **test_sanity_check_vorgaben_with_conflicts**: Testet vollständigen Sanity-Check mit Konflikten +- **test_sanity_check_vorgaben_multiple_conflicts**: Testet Sanity-Check mit mehreren Konfliktgruppen +- **test_vorgabe_clean_no_conflicts**: Testet Vorgabe.clean()-Methode ohne Konflikte +- **test_vorgabe_clean_with_conflicts**: Testet, dass Vorgabe.clean() ValidationError bei Konflikten auslöst + +### Management-Befehl-Tests + +#### SanityCheckManagementCommandTest +- **test_sanity_check_command_no_conflicts**: Testet Management-Befehlsausgabe ohne Konflikte +- **test_sanity_check_command_with_conflicts**: Testet Management-Befehlsausgabe mit Konflikten + +### URL-Pattern-Tests + +#### URLPatternsTest +- **test_standard_list_url_resolves**: Überprüft, dass standard_list URL zur korrekten View aufgelöst wird +- **test_standard_detail_url_resolves**: Überprüft, dass standard_detail URL mit pk-Parameter aufgelöst wird +- **test_standard_history_url_resolves**: Überprüft, dass standard_history URL mit check_date aufgelöst wird +- **test_standard_checkliste_url_resolves**: Überprüft, dass standard_checkliste URL mit pk aufgelöst wird + +### View-Tests + +#### ViewsTestCase +- **test_standard_list_view**: Testet, dass die Standard-Listen-View 200 zurückgibt und erwartete Inhalte enthält +- **test_standard_detail_view**: Testet die Standard-Detail-View mit existierendem Dokument +- **test_standard_detail_view_404**: Testet, dass die Standard-Detail-View 404 für nicht existierendes Dokument zurückgibt +- **test_standard_history_view**: Testet die Standard-Detail-View mit historischem check_date-Parameter +- **test_standard_checkliste_view**: Testet die Funktionalität der Checklisten-View + +### Unvollständige Vorgaben Tests + +#### IncompleteVorgabenTest +- **test_incomplete_vorgaben_page_status**: Testet, dass die Seite erfolgreich lädt (200-Status) +- **test_incomplete_vorgaben_staff_only**: Testet, dass Nicht-Staff-Benutzer zum Login weitergeleitet werden +- **test_incomplete_vorgaben_page_content**: Testet, dass die Seite erwartete Überschriften und Struktur enthält +- **test_navigation_link**: Testet, dass die Navigation einen Link zur unvollständigen Vorgaben-Seite enthält +- **test_no_references_list**: Testet, dass Vorgaben ohne Referenzen korrekt aufgelistet werden +- **test_no_stichworte_list**: Testet, dass Vorgaben ohne Stichworte korrekt aufgelistet werden +- **test_no_text_list**: Testet, dass Vorgaben ohne Kurz- oder Langtext korrekt aufgelistet werden +- **test_no_checklistenfragen_list**: Testet, dass Vorgaben ohne Checklistenfragen korrekt aufgelistet werden +- **test_vorgabe_with_both_text_types**: Testet, dass Vorgabe mit beiden Texttypen als vollständig betrachtet wird +- **test_vorgabe_with_langtext_only**: Testet, dass Vorgabe mit nur Langtext immer noch unvollständig für Text ist +- **test_empty_lists_message**: Testet angemessene Nachrichten, wenn Listen leer sind +- **test_badge_counts**: Testet, dass Badge-Zähler korrekt berechnet werden +- **test_summary_section**: Testet, dass die Zusammenfassungssektion korrekte Zähler anzeigt +- **test_vorgabe_links**: Testet, dass Vorgaben zu korrekten Admin-Seiten verlinken +- **test_back_link**: Testet, dass der Zurück-Link zur Standardübersicht existiert + +--- + +## pages App Tests + +Die pages App enthält 4 Tests, die sich auf die Suchfunktionalität und Validierung konzentrieren. + +### ViewsTestCase +- **test_search_view_get**: Testet GET-Anfrage an die Search-View gibt 200-Status zurück +- **test_search_view_post_with_query**: Testet POST-Anfrage mit Query gibt Ergebnisse zurück +- **test_search_view_post_empty_query**: Testet POST-Anfrage mit leerer Query zeigt Validierungsfehler +- **test_search_view_post_no_query**: Testet POST-Anfrage ohne Query-Parameter zeigt Validierungsfehler + +--- + +## referenzen App Tests + +Die referenzen App enthält 18 Tests, die sich auf MPTT-Hierarchiefunktionalität und Modellbeziehungen konzentrieren. + +### Modell-Tests + +#### ReferenzModelTest +- **test_referenz_creation**: Testet die Erstellung von Referenz mit erforderlichen Feldern +- **test_referenz_str**: Testet die String-Repräsentation gibt den Referenztext zurück +- **test_referenz_ordering**: Testet die Standard-Sortierung nach `order`-Feld +- **test_referenz_optional_fields**: Testet, dass optionale Felder leer sein können + +#### ReferenzerklaerungModelTest +- **test_referenzerklaerung_creation**: Testet die Erstellung von Referenzerklaerung mit Referenz und Erklärung +- **test_referenzerklaerung_str**: Testet die String-Repräsentation enthält Referenz und Erklärungsvorschau +- **test_referenzerklaerung_ordering**: Testet die Standard-Sortierung nach `order`-Feld +- **test_referenzerklaerung_optional_explanation**: Testet, dass das Erklärungsfeld leer sein kann + +### Hierarchie-Tests + +#### ReferenzHierarchyTest +- **test_hierarchy_relationships**: Testet Eltern-Kind-Beziehungen im MPTT-Baum +- **test_get_root**: Testet das Abrufen des Wurzelknotens einer Hierarchie +- **test_get_children**: Testet das Abrufen direkter Kinder eines Knotens +- **test_get_descendants**: Testet das Abrufen aller Nachkommen eines Knotens +- **test_get_ancestors**: Testet das Abrufen aller Vorfahren eines Knotens +- **test_get_ancestors_include_self**: Testet das Abrufen von Vorfahren einschließlich des Knotens selbst +- **test_is_leaf_node**: Testet die Erkennung von Blattknoten +- **test_is_root_node**: Testet die Erkennung von Wurzelknoten +- **test_tree_ordering**: Testet die Baum-Sortierung mit mehreren Ebenen +- **test_move_node**: Testet das Verschieben von Knoten innerhalb der Baumstruktur + +--- + +## rollen App Tests + +Die rollen App enthält 18 Tests, die Rollenmodelle und ihre Beziehungen zu Dokumentabschnitten abdecken. + +### Modell-Tests + +#### RolleModelTest +- **test_rolle_creation**: Testet die Erstellung von Rolle mit Name und optionaler Beschreibung +- **test_rolle_str**: Testet die String-Repräsentation gibt den Rollennamen zurück +- **test_rolle_ordering**: Testet die Standard-Sortierung nach `order`-Feld +- **test_rolle_unique_name**: Testet, dass Rollennamen einzigartig sein müssen +- **test_rolle_optional_beschreibung**: Testet, dass das Beschreibungsfeld leer sein kann + +#### RollenBeschreibungModelTest +- **test_rollenbeschreibung_creation**: Testet die Erstellung von RollenBeschreibung mit Rolle und Abschnittstyp +- **test_rollenbeschreibung_str**: Testet die String-Repräsentation enthält Rolle und Abschnittstyp +- **test_rollenbeschreibung_ordering**: Testet die Standard-Sortierung nach `order`-Feld +- **test_rollenbeschreibung_unique_combination**: Testet die Unique-Constraint auf Rolle und Abschnittstyp +- **test_rollenbeschreibung_optional_beschreibung**: Testet, dass das Beschreibungsfeld leer sein kann + +### Beziehungs-Tests + +#### RelationshipTest +- **test_rolle_rollenbeschreibung_relationship**: Testet die Eins-zu-viele-Beziehung zwischen Rolle und RollenBeschreibung +- **test_abschnitttyp_rollenbeschreibung_relationship**: Testet die Beziehung zwischen AbschnittTyp und RollenBeschreibung +- **test_cascade_delete**: Testet das Cascade-Delete-Verhalten beim Löschen einer Rolle +- **test_protected_delete**: Testet das Protected-Delete-Verhalten, wenn Abschnittstyp referenziert wird +- **test_query_related_objects**: Testet das effiziente Abfragen verwandter Objekte +- **test_string_representations**: Testet, dass alle String-Repräsentationen korrekt funktionieren +- **test_ordering_consistency**: Testet, dass die Sortierung über Abfragen hinweg konsistent ist + +--- + +## stichworte App Tests + +Die stichworte App enthält 18 Tests, die Schlüsselwortmodelle und ihre Sortierung abdecken. + +### Modell-Tests + +#### StichwortModelTest +- **test_stichwort_creation**: Testet die Erstellung von Stichwort mit Schlüsselworttext +- **test_stichwort_str**: Testet die String-Repräsentation gibt den Schlüsselworttext zurück +- **test_stichwort_ordering**: Testet die Standard-Sortierung nach `stichwort`-Feld +- **test_stichwort_unique**: Testet, dass Schlüsselwörter einzigartig sein müssen +- **test_stichwort_case_insensitive**: Testet die Groß-/Kleinschreibungs-unabhängige Eindeutigkeit + +#### StichworterklaerungModelTest +- **test_stichworterklaerung_creation**: Testet die Erstellung von Stichworterklaerung mit Schlüsselwort und Erklärung +- **test_stichworterklaerung_str**: Testet die String-Repräsentation enthält Schlüsselwort und Erklärungsvorschau +- **test_stichworterklaerung_ordering**: Testet die Standard-Sortierung nach `order`-Feld +- **test_stichworterklaerung_optional_erklaerung**: Testet, dass das Erklärungsfeld leer sein kann +- **test_stichworterklaerung_unique_stichwort**: Testet den Unique-Constraint auf das Schlüsselwort + +### Beziehungs-Tests + +#### RelationshipTest +- **test_stichwort_stichworterklaerung_relationship**: Testet die Eins-zu-eins-Beziehung zwischen Stichwort und Stichworterklaerung +- **test_cascade_delete**: Testet das Cascade-Delete-Verhalten beim Löschen eines Schlüsselworts +- **test_protected_delete**: Testet das Protected-Delete-Verhalten, wenn Erklärung referenziert wird +- **test_query_related_objects**: Testet das effiziente Abfragen verwandter Objekte +- **test_string_representations**: Testet, dass alle String-Repräsentationen korrekt funktionieren +- **test_ordering_consistency**: Testet, dass die Sortierung über Abfragen hinweg konsistent ist +- **test_reverse_relationship**: Testet die umgekehrte Beziehung von Erklärung zu Schlüsselwort + +--- + +## Test-Statistiken + +- **Gesamt-Tests**: 188 +- **abschnitte**: 32 Tests +- **dokumente**: 98 Tests +- **pages**: 4 Tests +- **referenzen**: 18 Tests +- **rollen**: 18 Tests +- **stichworte**: 18 Tests + +## Test-Abdeckungsbereiche + +1. **Modell-Validierung**: Feldvalidierung, Constraints und Beziehungen +2. **Geschäftslogik**: Statusbestimmung, Konflikterkennung, Hierarchieverwaltung +3. **View-Funktionalität**: HTTP-Antworten, Template-Rendering, URL-Auflösung +4. **Utility-Funktionen**: Textverarbeitung, Caching, Formatierung +5. **Management-Befehle**: CLI-Schnittstelle und Ausgabeverarbeitung +6. **Integration**: App-übergreifende Funktionalität und Datenfluss + +## Ausführen der Tests + +Um alle Tests auszuführen: +```bash +python manage.py test +``` + +Um Tests für eine spezifische App auszuführen: +```bash +python manage.py test app_name +``` + +Um mit ausführlicher Ausgabe auszuführen: +```bash +python manage.py test --verbosity=2 +``` + +Alle Tests laufen derzeit erfolgreich und bieten umfassende Abdeckung der Funktionalität der Anwendung. \ No newline at end of file diff --git a/Test suite.md b/Test suite.md new file mode 100644 index 0000000..bd35611 --- /dev/null +++ b/Test suite.md @@ -0,0 +1,354 @@ +# Test Suite Documentation + +This document provides a comprehensive overview of all tests in the vgui-cicd Django project, describing what each test does and how it works. + +## Table of Contents + +- [abschnitte App Tests](#abschnitte-app-tests) +- [dokumente App Tests](#dokumente-app-tests) +- [pages App Tests](#pages-app-tests) +- [referenzen App Tests](#referenzen-app-tests) +- [rollen App Tests](#rollen-app-tests) +- [stichworte App Tests](#stichworte-app-tests) + +--- + +## abschnitte App Tests + +The abschnitte app contains 32 tests covering models, utility functions, diagram caching, and management commands. + +### Model Tests + +#### AbschnittTypModelTest +- **test_abschnitttyp_creation**: Verifies that AbschnittTyp objects are created correctly with the expected field values +- **test_abschnitttyp_primary_key**: Confirms that the `abschnitttyp` field serves as the primary key +- **test_abschnitttyp_str**: Tests the string representation returns the `abschnitttyp` value +- **test_abschnitttyp_verbose_name_plural**: Validates the verbose name plural is set correctly +- **test_create_multiple_abschnitttypen**: Ensures multiple AbschnittTyp objects can be created with different types + +#### TextabschnittModelTest +- **test_textabschnitt_creation**: Tests that Textabschnitt can be instantiated through the concrete model +- **test_textabschnitt_default_order**: Verifies the `order` field defaults to 0 +- **test_textabschnitt_ordering**: Tests that Textabschnitt objects can be ordered by the `order` field +- **test_textabschnitt_blank_fields**: Confirms that `abschnitttyp` and `inhalt` fields can be blank/null +- **test_textabschnitt_foreign_key_protection**: Tests that AbschnittTyp objects are protected from deletion when referenced by Textabschnitt + +### Utility Function Tests + +#### MdTableToHtmlTest +- **test_simple_table**: Converts a basic markdown table with headers and one row to HTML +- **test_table_with_multiple_rows**: Tests conversion of tables with multiple data rows +- **test_table_with_empty_cells**: Handles tables with empty cells in the data +- **test_table_with_spaces**: Processes tables with extra spaces in cells +- **test_table_empty_string**: Raises ValueError for empty input strings +- **test_table_only_whitespace**: Raises ValueError for strings containing only whitespace +- **test_table_insufficient_lines**: Raises ValueError when input has fewer than 2 lines + +#### RenderTextabschnitteTest +- **test_render_empty_queryset**: Returns empty string for empty querysets +- **test_render_multiple_abschnitte**: Renders multiple Textabschnitte in correct order +- **test_render_text_markdown**: Converts plain text with markdown formatting +- **test_render_ordered_list**: Renders ordered lists correctly +- **test_render_unordered_list**: Renders unordered lists correctly +- **test_render_code_block**: Renders code blocks with proper syntax highlighting +- **test_render_table**: Converts markdown tables to HTML using md_table_to_html +- **test_render_diagram_success**: Tests diagram generation with successful caching +- **test_render_diagram_error**: Handles diagram generation errors gracefully +- **test_render_diagram_with_options**: Tests diagram rendering with custom options +- **test_render_text_with_footnotes**: Processes text containing footnotes +- **test_render_abschnitt_without_type**: Handles Textabschnitte without AbschnittTyp +- **test_render_abschnitt_with_empty_content**: Handles Textabschnitte with empty content + +### Diagram Caching Tests + +#### DiagramCacheTest +- **test_compute_hash**: Generates consistent SHA256 hashes for the same input +- **test_get_cache_path**: Creates correct cache file paths based on hash and type +- **test_get_cached_diagram_hit**: Returns cached diagram when cache hit occurs +- **test_get_cached_diagram_miss**: Generates new diagram when cache miss occurs +- **test_get_cached_diagram_request_error**: Properly handles and raises request errors +- **test_clear_cache_specific_type**: Clears cache files for specific diagram types +- **test_clear_cache_all_types**: Clears all cache files when no type specified + +### Management Command Tests + +#### ClearDiagramCacheCommandTest +- **test_command_without_type**: Tests management command execution without specifying type +- **test_command_with_type**: Tests management command execution with specific diagram type + +### Integration Tests + +#### IntegrationTest +- **test_textabschnitt_inheritance**: Verifies VorgabeLangtext properly inherits Textabschnitt fields +- **test_render_vorgabe_langtext**: Tests rendering VorgabeLangtext through render_textabschnitte + +--- + +## dokumente App Tests + +The dokumente app contains 98 tests, making it the most comprehensive test suite, covering all models, views, URLs, and business logic. + +### Model Tests + +#### DokumententypModelTest +- **test_dokumententyp_creation**: Verifies Dokumententyp creation with correct field values +- **test_dokumententyp_str**: Tests string representation returns the `typ` field +- **test_dokumententyp_verbose_name**: Validates verbose name is set correctly + +#### PersonModelTest +- **test_person_creation**: Tests Person object creation with name and optional title +- **test_person_str**: Verifies string representation includes title and name +- **test_person_verbose_name_plural**: Tests verbose name plural configuration + +#### ThemaModelTest +- **test_thema_creation**: Tests Thema creation with name and optional explanation +- **test_thema_str**: Verifies string representation returns the theme name +- **test_thema_blank_erklaerung**: Confirms `erklaerung` field can be blank + +#### DokumentModelTest +- **test_dokument_creation**: Tests Dokument creation with required and optional fields +- **test_dokument_str**: Verifies string representation returns the document title +- **test_dokument_optional_fields**: Tests that optional fields can be None or blank +- **test_dokument_many_to_many_relationships**: Verifies many-to-many relationships with Personen and Themen + +#### VorgabeModelTest +- **test_vorgabe_creation**: Tests Vorgabe creation with all required fields +- **test_vorgabe_str**: Verifies string representation returns the Vorgabennummer +- **test_vorgabennummer**: Tests automatic generation of Vorgabennummer format +- **test_get_status_active**: Tests status determination for current active Vorgaben +- **test_get_status_expired**: Tests status determination for expired Vorgaben +- **test_get_status_future**: Tests status determination for future Vorgaben +- **test_get_status_with_custom_check_date**: Tests status with custom check date +- **test_get_status_verbose**: Tests verbose status output + +#### ChangelogModelTest +- **test_changelog_creation**: Tests Changelog creation with version, date, and description +- **test_changelog_str**: Verifies string representation includes version and date + +#### ChecklistenfrageModelTest +- **test_checklistenfrage_creation**: Tests Checklistenfrage creation with question and optional answer +- **test_checklistenfrage_str**: Verifies string representation truncates long questions +- **test_checklistenfrage_related_name**: Tests the reverse relationship from Vorgabe + +### Text Abschnitt Tests + +#### DokumentTextAbschnitteTest +- **test_einleitung_creation**: Tests Einleitung creation and inheritance from Textabschnitt +- **test_geltungsbereich_creation**: Tests Geltungsbereich creation and inheritance + +#### VorgabeTextAbschnitteTest +- **test_vorgabe_kurztext_creation**: Tests VorgabeKurztext creation and inheritance +- **test_vorgabe_langtext_creation**: Tests VorgabeLangtext creation and inheritance + +### Sanity Check Tests + +#### VorgabeSanityCheckTest +- **test_date_ranges_intersect_no_overlap**: Tests date intersection with non-overlapping ranges +- **test_date_ranges_intersect_with_overlap**: Tests date intersection with overlapping ranges +- **test_date_ranges_intersect_identical_ranges**: Tests date intersection with identical ranges +- **test_date_ranges_intersect_with_none_end_date**: Tests intersection with open-ended ranges +- **test_date_ranges_intersect_both_none_end_dates**: Tests intersection with two open-ended ranges +- **test_check_vorgabe_conflicts_utility**: Tests the utility function for conflict detection +- **test_find_conflicts_no_conflicts**: Tests conflict detection on Vorgabe without conflicts +- **test_find_conflicts_with_conflicts**: Tests conflict detection with conflicting Vorgaben +- **test_format_conflict_report_no_conflicts**: Tests conflict report formatting with no conflicts +- **test_format_conflict_report_with_conflicts**: Tests conflict report formatting with conflicts +- **test_sanity_check_vorgaben_no_conflicts**: Tests full sanity check with no conflicts +- **test_sanity_check_vorgaben_with_conflicts**: Tests full sanity check with conflicts +- **test_sanity_check_vorgaben_multiple_conflicts**: Tests sanity check with multiple conflict groups +- **test_vorgabe_clean_no_conflicts**: Tests Vorgabe.clean() method without conflicts +- **test_vorgabe_clean_with_conflicts**: Tests Vorgabe.clean() raises ValidationError with conflicts + +### Management Command Tests + +#### SanityCheckManagementCommandTest +- **test_sanity_check_command_no_conflicts**: Tests management command output with no conflicts +- **test_sanity_check_command_with_conflicts**: Tests management command output with conflicts + +### URL Pattern Tests + +#### URLPatternsTest +- **test_standard_list_url_resolves**: Verifies standard_list URL resolves to correct view +- **test_standard_detail_url_resolves**: Verifies standard_detail URL resolves with pk parameter +- **test_standard_history_url_resolves**: Verifies standard_history URL resolves with check_date +- **test_standard_checkliste_url_resolves**: Verifies standard_checkliste URL resolves with pk + +### View Tests + +#### ViewsTestCase +- **test_standard_list_view**: Tests standard list view returns 200 and contains expected content +- **test_standard_detail_view**: Tests standard detail view with existing document +- **test_standard_detail_view_404**: Tests standard detail view returns 404 for non-existent document +- **test_standard_history_view**: Tests standard detail view with historical check_date parameter +- **test_standard_checkliste_view**: Tests checklist view functionality + +### Incomplete Vorgaben Tests + +#### IncompleteVorgabenTest +- **test_incomplete_vorgaben_page_status**: Tests page loads successfully (200 status) +- **test_incomplete_vorgaben_staff_only**: Tests non-staff users are redirected to login +- **test_incomplete_vorgaben_page_content**: Tests page contains expected headings and structure +- **test_navigation_link**: Tests navigation includes link to incomplete Vorgaben page +- **test_no_references_list**: Tests Vorgaben without references are listed correctly +- **test_no_stichworte_list**: Tests Vorgaben without Stichworte are listed correctly +- **test_no_text_list**: Tests Vorgaben without Kurz- or Langtext are listed correctly +- **test_no_checklistenfragen_list**: Tests Vorgaben without Checklistenfragen are listed correctly +- **test_vorgabe_with_both_text_types**: Tests Vorgabe with both text types is considered complete +- **test_vorgabe_with_langtext_only**: Tests Vorgabe with only Langtext is still incomplete for text +- **test_empty_lists_message**: Tests appropriate messages when lists are empty +- **test_badge_counts**: Tests badge counts are calculated correctly +- **test_summary_section**: Tests summary section shows correct counts +- **test_vorgabe_links**: Tests Vorgaben link to correct admin pages +- **test_back_link**: Tests back link to standard list exists + +--- + +## pages App Tests + +The pages app contains 4 tests focusing on search functionality and validation. + +### ViewsTestCase +- **test_search_view_get**: Tests GET request to search view returns 200 status +- **test_search_view_post_with_query**: Tests POST request with query returns results +- **test_search_view_post_empty_query**: Tests POST request with empty query shows validation error +- **test_search_view_post_no_query**: Tests POST request without query parameter shows validation error + +--- + +## referenzen App Tests + +The referenzen app contains 18 tests focusing on MPTT hierarchy functionality and model relationships. + +### Model Tests + +#### ReferenzModelTest +- **test_referenz_creation**: Tests Referenz creation with required fields +- **test_referenz_str**: Tests string representation returns the reference text +- **test_referenz_ordering**: Tests default ordering by `order` field +- **test_referenz_optional_fields**: Tests optional fields can be blank + +#### ReferenzerklaerungModelTest +- **test_referenzerklaerung_creation**: Tests Referenzerklaerung creation with reference and explanation +- **test_referenzerklaerung_str**: Tests string representation includes reference and explanation preview +- **test_referenzerklaerung_ordering**: Tests default ordering by `order` field +- **test_referenzerklaerung_optional_explanation**: Tests explanation field can be blank + +### Hierarchy Tests + +#### ReferenzHierarchyTest +- **test_hierarchy_relationships**: Tests parent-child relationships in MPTT tree +- **test_get_root**: Tests getting the root node of a hierarchy +- **test_get_children**: Tests getting direct children of a node +- **test_get_descendants**: Tests getting all descendants of a node +- **test_get_ancestors**: Tests getting all ancestors of a node +- **test_get_ancestors_include_self**: Tests getting ancestors including the node itself +- **test_is_leaf_node**: Tests leaf node detection +- **test_is_root_node**: Tests root node detection +- **test_tree_ordering**: Tests tree ordering with multiple levels +- **test_move_node**: Tests moving nodes within the tree structure + +--- + +## rollen App Tests + +The rollen app contains 18 tests covering role models and their relationships with document sections. + +### Model Tests + +#### RolleModelTest +- **test_rolle_creation**: Tests Rolle creation with name and optional description +- **test_rolle_str**: Tests string representation returns the role name +- **test_rolle_ordering**: Tests default ordering by `order` field +- **test_rolle_unique_name**: Tests that role names must be unique +- **test_rolle_optional_beschreibung**: Tests description field can be blank + +#### RollenBeschreibungModelTest +- **test_rollenbeschreibung_creation**: Tests RollenBeschreibung creation with role and section type +- **test_rollenbeschreibung_str**: Tests string representation includes role and section type +- **test_rollenbeschreibung_ordering**: Tests default ordering by `order` field +- **test_rollenbeschreibung_unique_combination**: Tests unique constraint on role and section type +- **test_rollenbeschreibung_optional_beschreibung**: Tests description field can be blank + +### Relationship Tests + +#### RelationshipTest +- **test_rolle_rollenbeschreibung_relationship**: Tests one-to-many relationship between Rolle and RollenBeschreibung +- **test_abschnitttyp_rollenbeschreibung_relationship**: Tests relationship between AbschnittTyp and RollenBeschreibung +- **test_cascade_delete**: Tests cascade delete behavior when role is deleted +- **test_protected_delete**: Tests protected delete behavior when section type is referenced +- **test_query_related_objects**: Tests querying related objects efficiently +- **test_string_representations**: Tests all string representations work correctly +- **test_ordering_consistency**: Tests ordering is consistent across queries + +--- + +## stichworte App Tests + +The stichworte app contains 18 tests covering keyword models and their ordering. + +### Model Tests + +#### StichwortModelTest +- **test_stichwort_creation**: Tests Stichwort creation with keyword text +- **test_stichwort_str**: Tests string representation returns the keyword text +- **test_stichwort_ordering**: Tests default ordering by `stichwort` field +- **test_stichwort_unique**: Tests that keywords must be unique +- **test_stichwort_case_insensitive**: Tests case-insensitive uniqueness + +#### StichworterklaerungModelTest +- **test_stichworterklaerung_creation**: Tests Stichworterklaerung creation with keyword and explanation +- **test_stichworterklaerung_str**: Tests string representation includes keyword and explanation preview +- **test_stichworterklaerung_ordering**: Tests default ordering by `order` field +- **test_stichworterklaerung_optional_erklaerung**: Tests explanation field can be blank +- **test_stichworterklaerung_unique_stichwort**: Tests unique constraint on keyword + +### Relationship Tests + +#### RelationshipTest +- **test_stichwort_stichworterklaerung_relationship**: Tests one-to-one relationship between Stichwort and Stichworterklaerung +- **test_cascade_delete**: Tests cascade delete behavior when keyword is deleted +- **test_protected_delete**: Tests protected delete behavior when explanation is referenced +- **test_query_related_objects**: Tests querying related objects efficiently +- **test_string_representations**: Tests all string representations work correctly +- **test_ordering_consistency**: Tests ordering is consistent across queries +- **test_reverse_relationship**: Tests reverse relationship from explanation to keyword + +--- + +## Test Statistics + +- **Total Tests**: 188 +- **abschnitte**: 32 tests +- **dokumente**: 98 tests +- **pages**: 4 tests +- **referenzen**: 18 tests +- **rollen**: 18 tests +- **stichworte**: 18 tests + +## Test Coverage Areas + +1. **Model Validation**: Field validation, constraints, and relationships +2. **Business Logic**: Status determination, conflict detection, hierarchy management +3. **View Functionality**: HTTP responses, template rendering, URL resolution +4. **Utility Functions**: Text processing, caching, formatting +5. **Management Commands**: CLI interface and output handling +6. **Integration**: Cross-app functionality and data flow + +## Running the Tests + +To run all tests: +```bash +python manage.py test +``` + +To run tests for a specific app: +```bash +python manage.py test app_name +``` + +To run with verbose output: +```bash +python manage.py test --verbosity=2 +``` + +All tests are currently passing and provide comprehensive coverage of the application's functionality. \ No newline at end of file diff --git a/referenzen/tests.py b/referenzen/tests.py index 7ce503c..d15499d 100644 --- a/referenzen/tests.py +++ b/referenzen/tests.py @@ -1,3 +1,398 @@ from django.test import TestCase +from django.core.exceptions import ValidationError +from .models import Referenz, Referenzerklaerung +from abschnitte.models import AbschnittTyp -# Create your tests here. + +class ReferenzModelTest(TestCase): + """Test cases for Referenz model""" + + def setUp(self): + """Set up test data""" + self.referenz = Referenz.objects.create( + name_nummer="ISO-27001", + name_text="Information Security Management", + url="https://www.iso.org/isoiec-27001-information-security.html" + ) + + def test_referenz_creation(self): + """Test that Referenz is created correctly""" + self.assertEqual(self.referenz.name_nummer, "ISO-27001") + self.assertEqual(self.referenz.name_text, "Information Security Management") + self.assertEqual(self.referenz.url, "https://www.iso.org/isoiec-27001-information-security.html") + self.assertIsNone(self.referenz.oberreferenz) + + def test_referenz_str(self): + """Test string representation of Referenz""" + self.assertEqual(str(self.referenz), "ISO-27001") + + def test_referenz_verbose_name_plural(self): + """Test verbose name plural""" + self.assertEqual( + Referenz._meta.verbose_name_plural, + "Referenzen" + ) + + def test_referenz_path_method(self): + """Test Path method for root reference""" + path = self.referenz.Path() + self.assertEqual(path, "ISO-27001 (Information Security Management)") + + def test_referenz_path_without_name_text(self): + """Test Path method when name_text is empty""" + referenz_no_text = Referenz.objects.create( + name_nummer="NIST-800-53" + ) + path = referenz_no_text.Path() + self.assertEqual(path, "NIST-800-53") + + def test_referenz_blank_fields(self): + """Test that optional fields can be blank""" + referenz_minimal = Referenz.objects.create( + name_nummer="TEST-001" + ) + self.assertEqual(referenz_minimal.name_text, "") + self.assertEqual(referenz_minimal.url, "") + self.assertIsNone(referenz_minimal.oberreferenz) + + def test_referenz_max_lengths(self): + """Test max_length constraints""" + max_name_nummer = "a" * 100 + max_name_text = "b" * 255 + + referenz = Referenz.objects.create( + name_nummer=max_name_nummer, + name_text=max_name_text + ) + + self.assertEqual(referenz.name_nummer, max_name_nummer) + self.assertEqual(referenz.name_text, max_name_text) + + def test_create_multiple_references(self): + """Test creating multiple Referenz objects""" + references = [ + ("ISO-9001", "Quality Management"), + ("ISO-14001", "Environmental Management"), + ("ISO-45001", "Occupational Health and Safety") + ] + + for name_nummer, name_text in references: + Referenz.objects.create( + name_nummer=name_nummer, + name_text=name_text + ) + + self.assertEqual(Referenz.objects.count(), 4) # Including setUp referenz + + +class ReferenzHierarchyTest(TestCase): + """Test cases for Referenz hierarchy using MPTT""" + + def setUp(self): + """Set up hierarchical test data""" + # Create root references + self.iso_root = Referenz.objects.create( + name_nummer="ISO", + name_text="International Organization for Standardization" + ) + + self.iso_27000_series = Referenz.objects.create( + name_nummer="ISO-27000", + name_text="Information Security Management System Family", + oberreferenz=self.iso_root + ) + + self.iso_27001 = Referenz.objects.create( + name_nummer="ISO-27001", + name_text="Information Security Management", + oberreferenz=self.iso_27000_series + ) + + self.iso_27002 = Referenz.objects.create( + name_nummer="ISO-27002", + name_text="Code of Practice for Information Security Controls", + oberreferenz=self.iso_27000_series + ) + + def test_hierarchy_relationships(self): + """Test parent-child relationships""" + self.assertEqual(self.iso_27000_series.oberreferenz, self.iso_root) + self.assertEqual(self.iso_27001.oberreferenz, self.iso_27000_series) + self.assertEqual(self.iso_27002.oberreferenz, self.iso_27000_series) + + def test_get_ancestors(self): + """Test getting ancestors""" + ancestors = self.iso_27001.get_ancestors() + expected_ancestors = [self.iso_root, self.iso_27000_series] + self.assertEqual(list(ancestors), expected_ancestors) + + def test_get_ancestors_include_self(self): + """Test getting ancestors including self""" + ancestors = self.iso_27001.get_ancestors(include_self=True) + expected_ancestors = [self.iso_root, self.iso_27000_series, self.iso_27001] + self.assertEqual(list(ancestors), expected_ancestors) + + def test_get_descendants(self): + """Test getting descendants""" + descendants = self.iso_27000_series.get_descendants() + expected_descendants = [self.iso_27001, self.iso_27002] + self.assertEqual(list(descendants), expected_descendants) + + def test_get_children(self): + """Test getting direct children""" + children = self.iso_27000_series.get_children() + expected_children = [self.iso_27001, self.iso_27002] + self.assertEqual(list(children), expected_children) + + def test_get_root(self): + """Test getting root of hierarchy""" + root = self.iso_27001.get_root() + self.assertEqual(root, self.iso_root) + + def test_is_root(self): + """Test is_root method""" + self.assertTrue(self.iso_root.is_root_node()) + self.assertFalse(self.iso_27001.is_root_node()) + + def test_is_leaf(self): + """Test is_leaf method""" + self.assertFalse(self.iso_root.is_leaf_node()) + self.assertFalse(self.iso_27000_series.is_leaf_node()) + self.assertTrue(self.iso_27001.is_leaf_node()) + self.assertTrue(self.iso_27002.is_leaf_node()) + + def test_level_property(self): + """Test level property""" + self.assertEqual(self.iso_root.level, 0) + self.assertEqual(self.iso_27000_series.level, 1) + self.assertEqual(self.iso_27001.level, 2) + self.assertEqual(self.iso_27002.level, 2) + + def test_path_method_with_hierarchy(self): + """Test Path method with hierarchical references""" + path = self.iso_27001.Path() + expected_path = "ISO → ISO-27000 → ISO-27001 (Information Security Management)" + self.assertEqual(path, expected_path) + + def test_path_method_without_name_text_in_hierarchy(self): + """Test Path method when intermediate nodes have no name_text""" + # Create reference without name_text + ref_no_text = Referenz.objects.create( + name_nummer="NO-TEXT", + oberreferenz=self.iso_root + ) + + child_ref = Referenz.objects.create( + name_nummer="CHILD", + name_text="Child Reference", + oberreferenz=ref_no_text + ) + + path = child_ref.Path() + expected_path = "ISO → NO-TEXT → CHILD (Child Reference)" + self.assertEqual(path, expected_path) + + def test_order_insertion_by(self): + """Test that references are ordered by name_nummer""" + # Create more children in different order + ref_c = Referenz.objects.create( + name_nummer="C-REF", + oberreferenz=self.iso_root + ) + ref_a = Referenz.objects.create( + name_nummer="A-REF", + oberreferenz=self.iso_root + ) + ref_b = Referenz.objects.create( + name_nummer="B-REF", + oberreferenz=self.iso_root + ) + + children = list(self.iso_root.get_children()) + # Should be ordered alphabetically by name_nummer + expected_order = [ref_a, ref_b, ref_c, self.iso_27000_series] + self.assertEqual(children, expected_order) + + +class ReferenzerklaerungModelTest(TestCase): + """Test cases for Referenzerklaerung model""" + + def setUp(self): + """Set up test data""" + self.referenz = Referenz.objects.create( + name_nummer="ISO-27001", + name_text="Information Security Management" + ) + self.abschnitttyp = AbschnittTyp.objects.create( + abschnitttyp="text" + ) + self.erklaerung = Referenzerklaerung.objects.create( + erklaerung=self.referenz, + abschnitttyp=self.abschnitttyp, + inhalt="Dies ist eine Erklärung für ISO-27001.", + order=1 + ) + + def test_referenzerklaerung_creation(self): + """Test that Referenzerklaerung is created correctly""" + self.assertEqual(self.erklaerung.erklaerung, self.referenz) + self.assertEqual(self.erklaerung.abschnitttyp, self.abschnitttyp) + self.assertEqual(self.erklaerung.inhalt, "Dies ist eine Erklärung für ISO-27001.") + self.assertEqual(self.erklaerung.order, 1) + + def test_referenzerklaerung_foreign_key_relationship(self): + """Test foreign key relationship to Referenz""" + self.assertEqual(self.erklaerung.erklaerung.name_nummer, "ISO-27001") + self.assertEqual(self.erklaerung.erklaerung.name_text, "Information Security Management") + + def test_referenzerklaerung_cascade_delete(self): + """Test that deleting Referenz cascades to Referenzerklaerung""" + referenz_count = Referenz.objects.count() + erklaerung_count = Referenzerklaerung.objects.count() + + self.referenz.delete() + + self.assertEqual(Referenz.objects.count(), referenz_count - 1) + self.assertEqual(Referenzerklaerung.objects.count(), erklaerung_count - 1) + + def test_referenzerklaerung_verbose_name(self): + """Test verbose name""" + self.assertEqual( + Referenzerklaerung._meta.verbose_name, + "Erklärung" + ) + + def test_referenzerklaerung_multiple_explanations(self): + """Test creating multiple explanations for one Referenz""" + abschnitttyp2 = AbschnittTyp.objects.create(abschnitttyp="liste ungeordnet") + erklaerung2 = Referenzerklaerung.objects.create( + erklaerung=self.referenz, + abschnitttyp=abschnitttyp2, + inhalt="Zweite Erklärung für ISO-27001.", + order=2 + ) + + explanations = Referenzerklaerung.objects.filter(erklaerung=self.referenz) + self.assertEqual(explanations.count(), 2) + self.assertIn(self.erklaerung, explanations) + self.assertIn(erklaerung2, explanations) + + def test_referenzerklaerung_ordering(self): + """Test that explanations can be ordered""" + erklaerung2 = Referenzerklaerung.objects.create( + erklaerung=self.referenz, + abschnitttyp=self.abschnitttyp, + inhalt="Zweite Erklärung", + order=3 + ) + erklaerung3 = Referenzerklaerung.objects.create( + erklaerung=self.referenz, + abschnitttyp=self.abschnitttyp, + inhalt="Erste Erklärung", + order=2 + ) + + ordered = Referenzerklaerung.objects.filter(erklaerung=self.referenz).order_by('order') + expected_order = [self.erklaerung, erklaerung3, erklaerung2] + self.assertEqual(list(ordered), expected_order) + + def test_referenzerklaerung_blank_fields(self): + """Test that optional fields can be blank/null""" + referenz2 = Referenz.objects.create(name_nummer="TEST-001") + erklaerung_blank = Referenzerklaerung.objects.create( + erklaerung=referenz2 + ) + + self.assertIsNone(erklaerung_blank.abschnitttyp) + self.assertIsNone(erklaerung_blank.inhalt) + self.assertEqual(erklaerung_blank.order, 0) + + def test_referenzerklaerung_inheritance(self): + """Test that Referenzerklaerung inherits from Textabschnitt""" + # Check that it has the expected fields from Textabschnitt + self.assertTrue(hasattr(self.erklaerung, 'abschnitttyp')) + self.assertTrue(hasattr(self.erklaerung, 'inhalt')) + self.assertTrue(hasattr(self.erklaerung, 'order')) + + # Check that the fields work as expected + self.assertIsInstance(self.erklaerung.abschnitttyp, AbschnittTyp) + self.assertIsInstance(self.erklaerung.inhalt, str) + self.assertIsInstance(self.erklaerung.order, int) + + +class ReferenzIntegrationTest(TestCase): + """Integration tests for Referenz app""" + + def setUp(self): + """Set up test data""" + self.root_ref = Referenz.objects.create( + name_nummer="ROOT", + name_text="Root Reference" + ) + + self.child_ref = Referenz.objects.create( + name_nummer="CHILD", + name_text="Child Reference", + oberreferenz=self.root_ref + ) + + self.abschnitttyp = AbschnittTyp.objects.create(abschnitttyp="text") + + self.erklaerung = Referenzerklaerung.objects.create( + erklaerung=self.child_ref, + abschnitttyp=self.abschnitttyp, + inhalt="Explanation for child reference", + order=1 + ) + + def test_reference_with_explanations_query(self): + """Test querying references with their explanations""" + references_with_explanations = Referenz.objects.filter( + referenzerklaerung__isnull=False + ).distinct() + + self.assertEqual(references_with_explanations.count(), 1) + self.assertIn(self.child_ref, references_with_explanations) + self.assertNotIn(self.root_ref, references_with_explanations) + + def test_reference_without_explanations(self): + """Test finding references without explanations""" + references_without_explanations = Referenz.objects.filter( + referenzerklaerung__isnull=True + ) + + self.assertEqual(references_without_explanations.count(), 1) + self.assertEqual(references_without_explanations.first(), self.root_ref) + + def test_explanation_count_annotation(self): + """Test annotating references with explanation count""" + from django.db.models import Count + + references_with_count = Referenz.objects.annotate( + explanation_count=Count('referenzerklaerung') + ) + + for reference in references_with_count: + if reference == self.child_ref: + self.assertEqual(reference.explanation_count, 1) + else: + self.assertEqual(reference.explanation_count, 0) + + def test_hierarchy_with_explanations(self): + """Test that explanations work correctly with hierarchical references""" + # Add explanation to root reference + root_erklaerung = Referenzerklaerung.objects.create( + erklaerung=self.root_ref, + abschnitttyp=self.abschnitttyp, + inhalt="Explanation for root reference", + order=1 + ) + + # Both references should now have explanations + references_with_explanations = Referenz.objects.filter( + referenzerklaerung__isnull=False + ).distinct() + + self.assertEqual(references_with_explanations.count(), 2) + self.assertIn(self.root_ref, references_with_explanations) + self.assertIn(self.child_ref, references_with_explanations) diff --git a/rollen/tests.py b/rollen/tests.py index 7ce503c..cb898c0 100644 --- a/rollen/tests.py +++ b/rollen/tests.py @@ -1,3 +1,367 @@ from django.test import TestCase +from django.core.exceptions import ValidationError +from django.db.models import Count +from .models import Rolle, RollenBeschreibung +from abschnitte.models import AbschnittTyp -# Create your tests here. + +class RolleModelTest(TestCase): + """Test cases for Rolle model""" + + def setUp(self): + """Set up test data""" + self.rolle = Rolle.objects.create( + name="Systemadministrator" + ) + + def test_rolle_creation(self): + """Test that Rolle is created correctly""" + self.assertEqual(self.rolle.name, "Systemadministrator") + + def test_rolle_str(self): + """Test string representation of Rolle""" + self.assertEqual(str(self.rolle), "Systemadministrator") + + def test_rolle_primary_key(self): + """Test that name field is the primary key""" + pk_field = Rolle._meta.pk + self.assertEqual(pk_field.name, 'name') + self.assertEqual(pk_field.max_length, 100) + + def test_rolle_verbose_name_plural(self): + """Test verbose name plural""" + self.assertEqual( + Rolle._meta.verbose_name_plural, + "Rollen" + ) + + def test_rolle_max_length(self): + """Test max_length constraint""" + max_length_rolle = "a" * 100 + rolle = Rolle.objects.create(name=max_length_rolle) + self.assertEqual(rolle.name, max_length_rolle) + + def test_rolle_unique(self): + """Test that name must be unique""" + with self.assertRaises(Exception): + Rolle.objects.create(name="Systemadministrator") + + def test_create_multiple_rollen(self): + """Test creating multiple Rolle objects""" + rollen = [ + "Datenschutzbeauftragter", + "IT-Sicherheitsbeauftragter", + "Risikomanager", + "Compliance-Officer" + ] + for rolle_name in rollen: + Rolle.objects.create(name=rolle_name) + + self.assertEqual(Rolle.objects.count(), 5) # Including setUp rolle + + def test_rolle_case_sensitivity(self): + """Test that role name is case sensitive""" + rolle_lower = Rolle.objects.create(name="systemadministrator") + self.assertNotEqual(self.rolle.pk, rolle_lower.pk) + self.assertEqual(Rolle.objects.count(), 2) + + def test_rolle_with_special_characters(self): + """Test creating roles with special characters""" + special_roles = [ + "IT-Administrator", + "CISO (Chief Information Security Officer)", + "Datenschutz-Beauftragter/-in", + "Sicherheitsbeauftragter" + ] + + for role_name in special_roles: + rolle = Rolle.objects.create(name=role_name) + self.assertEqual(rolle.name, role_name) + + self.assertEqual(Rolle.objects.count(), 5) # Including setUp rolle + + +class RollenBeschreibungModelTest(TestCase): + """Test cases for RollenBeschreibung model""" + + def setUp(self): + """Set up test data""" + self.rolle = Rolle.objects.create( + name="Systemadministrator" + ) + self.abschnitttyp = AbschnittTyp.objects.create( + abschnitttyp="text" + ) + self.beschreibung = RollenBeschreibung.objects.create( + abschnitt=self.rolle, + abschnitttyp=self.abschnitttyp, + inhalt="Der Systemadministrator ist für die Verwaltung und Wartung der IT-Systeme verantwortlich.", + order=1 + ) + + def test_rollenbeschreibung_creation(self): + """Test that RollenBeschreibung is created correctly""" + self.assertEqual(self.beschreibung.abschnitt, self.rolle) + self.assertEqual(self.beschreibung.abschnitttyp, self.abschnitttyp) + self.assertEqual(self.beschreibung.inhalt, "Der Systemadministrator ist für die Verwaltung und Wartung der IT-Systeme verantwortlich.") + self.assertEqual(self.beschreibung.order, 1) + + def test_rollenbeschreibung_foreign_key_relationship(self): + """Test foreign key relationship to Rolle""" + self.assertEqual(self.beschreibung.abschnitt.name, "Systemadministrator") + + def test_rollenbeschreibung_cascade_delete(self): + """Test that deleting Rolle cascades to RollenBeschreibung""" + rolle_count = Rolle.objects.count() + beschreibung_count = RollenBeschreibung.objects.count() + + self.rolle.delete() + + self.assertEqual(Rolle.objects.count(), rolle_count - 1) + self.assertEqual(RollenBeschreibung.objects.count(), beschreibung_count - 1) + + def test_rollenbeschreibung_verbose_names(self): + """Test verbose names""" + self.assertEqual( + RollenBeschreibung._meta.verbose_name, + "Rollenbeschreibungs-Abschnitt" + ) + self.assertEqual( + RollenBeschreibung._meta.verbose_name_plural, + "Rollenbeschreibung" + ) + + def test_rollenbeschreibung_multiple_descriptions(self): + """Test creating multiple descriptions for one Rolle""" + abschnitttyp2 = AbschnittTyp.objects.create(abschnitttyp="liste ungeordnet") + beschreibung2 = RollenBeschreibung.objects.create( + abschnitt=self.rolle, + abschnitttyp=abschnitttyp2, + inhalt="Aufgaben:\n- Systemüberwachung\n- Backup-Management\n- Benutzeradministration", + order=2 + ) + + descriptions = RollenBeschreibung.objects.filter(abschnitt=self.rolle) + self.assertEqual(descriptions.count(), 2) + self.assertIn(self.beschreibung, descriptions) + self.assertIn(beschreibung2, descriptions) + + def test_rollenbeschreibung_ordering(self): + """Test that descriptions can be ordered""" + beschreibung2 = RollenBeschreibung.objects.create( + abschnitt=self.rolle, + abschnitttyp=self.abschnitttyp, + inhalt="Zweite Beschreibung", + order=3 + ) + beschreibung3 = RollenBeschreibung.objects.create( + abschnitt=self.rolle, + abschnitttyp=self.abschnitttyp, + inhalt="Erste Beschreibung", + order=2 + ) + + ordered = RollenBeschreibung.objects.filter(abschnitt=self.rolle).order_by('order') + expected_order = [self.beschreibung, beschreibung3, beschreibung2] + self.assertEqual(list(ordered), expected_order) + + def test_rollenbeschreibung_blank_fields(self): + """Test that optional fields can be blank/null""" + rolle2 = Rolle.objects.create(name="Testrolle") + beschreibung_blank = RollenBeschreibung.objects.create( + abschnitt=rolle2 + ) + + self.assertIsNone(beschreibung_blank.abschnitttyp) + self.assertIsNone(beschreibung_blank.inhalt) + self.assertEqual(beschreibung_blank.order, 0) + + def test_rollenbeschreibung_inheritance(self): + """Test that RollenBeschreibung inherits from Textabschnitt""" + # Check that it has the expected fields from Textabschnitt + self.assertTrue(hasattr(self.beschreibung, 'abschnitttyp')) + self.assertTrue(hasattr(self.beschreibung, 'inhalt')) + self.assertTrue(hasattr(self.beschreibung, 'order')) + + # Check that the fields work as expected + self.assertIsInstance(self.beschreibung.abschnitttyp, AbschnittTyp) + self.assertIsInstance(self.beschreibung.inhalt, str) + self.assertIsInstance(self.beschreibung.order, int) + + def test_rollenbeschreibung_different_types(self): + """Test creating descriptions with different section types""" + # Create different section types + typ_list = AbschnittTyp.objects.create(abschnitttyp="liste ungeordnet") + typ_table = AbschnittTyp.objects.create(abschnitttyp="tabelle") + + # Create descriptions with different types + beschreibung_text = RollenBeschreibung.objects.create( + abschnitt=self.rolle, + abschnitttyp=self.abschnitttyp, + inhalt="Textbeschreibung der Rolle", + order=1 + ) + + beschreibung_list = RollenBeschreibung.objects.create( + abschnitt=self.rolle, + abschnitttyp=typ_list, + inhalt="Aufgabe 1\nAufgabe 2\nAufgabe 3", + order=2 + ) + + beschreibung_table = RollenBeschreibung.objects.create( + abschnitt=self.rolle, + abschnitttyp=typ_table, + inhalt="| Verantwortung | Priorität |\n|--------------|------------|\n| Systemwartung | Hoch |", + order=3 + ) + + # Verify all descriptions are created + descriptions = RollenBeschreibung.objects.filter(abschnitt=self.rolle) + self.assertEqual(descriptions.count(), 4) # Including setUp beschreibung + + # Verify types are correct + self.assertEqual(beschreibung_text.abschnitttyp, self.abschnitttyp) + self.assertEqual(beschreibung_list.abschnitttyp, typ_list) + self.assertEqual(beschreibung_table.abschnitttyp, typ_table) + + +class RolleIntegrationTest(TestCase): + """Integration tests for Rolle app""" + + def setUp(self): + """Set up test data""" + self.rolle1 = Rolle.objects.create(name="IT-Sicherheitsbeauftragter") + self.rolle2 = Rolle.objects.create(name="Datenschutzbeauftragter") + + self.abschnitttyp = AbschnittTyp.objects.create(abschnitttyp="text") + + self.beschreibung1 = RollenBeschreibung.objects.create( + abschnitt=self.rolle1, + abschnitttyp=self.abschnitttyp, + inhalt="Beschreibung für IT-Sicherheitsbeauftragten", + order=1 + ) + + self.beschreibung2 = RollenBeschreibung.objects.create( + abschnitt=self.rolle2, + abschnitttyp=self.abschnitttyp, + inhalt="Beschreibung für Datenschutzbeauftragten", + order=1 + ) + + def test_rolle_with_descriptions_query(self): + """Test querying Rollen with their descriptions""" + rollen_with_descriptions = Rolle.objects.filter( + rollenbeschreibung__isnull=False + ).distinct() + + self.assertEqual(rollen_with_descriptions.count(), 2) + self.assertIn(self.rolle1, rollen_with_descriptions) + self.assertIn(self.rolle2, rollen_with_descriptions) + + def test_rolle_without_descriptions(self): + """Test finding Rollen without descriptions""" + rolle3 = Rolle.objects.create(name="Compliance-Officer") + + rollen_without_descriptions = Rolle.objects.filter( + rollenbeschreibung__isnull=True + ) + + self.assertEqual(rollen_without_descriptions.count(), 1) + self.assertEqual(rollen_without_descriptions.first(), rolle3) + + def test_description_count_annotation(self): + """Test annotating Rollen with description count""" + from django.db.models import Count + + rollen_with_count = Rolle.objects.annotate( + description_count=Count('rollenbeschreibung') + ) + + for rolle in rollen_with_count: + if rolle.name in ["IT-Sicherheitsbeauftragter", "Datenschutzbeauftragter"]: + self.assertEqual(rolle.description_count, 1) + else: + self.assertEqual(rolle.description_count, 0) + + def test_multiple_descriptions_per_rolle(self): + """Test multiple descriptions for a single role""" + # Add more descriptions to rolle1 + abschnitttyp2 = AbschnittTyp.objects.create(abschnitttyp="liste ungeordnet") + + beschreibung2 = RollenBeschreibung.objects.create( + abschnitt=self.rolle1, + abschnitttyp=abschnitttyp2, + inhalt="Zusätzliche Aufgaben:\n- Überwachung\n- Berichterstattung", + order=2 + ) + + # Check that rolle1 now has 2 descriptions + descriptions = RollenBeschreibung.objects.filter(abschnitt=self.rolle1) + self.assertEqual(descriptions.count(), 2) + + # Check annotation + rolle_with_count = Rolle.objects.annotate( + description_count=Count('rollenbeschreibung') + ).get(pk=self.rolle1.pk) + self.assertEqual(rolle_with_count.description_count, 2) + + def test_role_descriptions_ordered(self): + """Test that role descriptions are returned in correct order""" + # Add more descriptions in random order + beschreibung2 = RollenBeschreibung.objects.create( + abschnitt=self.rolle1, + abschnitttyp=self.abschnitttyp, + inhalt="Dritte Beschreibung", + order=3 + ) + + beschreibung3 = RollenBeschreibung.objects.create( + abschnitt=self.rolle1, + abschnitttyp=self.abschnitttyp, + inhalt="Zweite Beschreibung", + order=2 + ) + + # Get descriptions in order + ordered_descriptions = RollenBeschreibung.objects.filter( + abschnitt=self.rolle1 + ).order_by('order') + + expected_order = [self.beschreibung1, beschreibung3, beschreibung2] + self.assertEqual(list(ordered_descriptions), expected_order) + + def test_role_search_by_name(self): + """Test searching roles by name""" + # Test exact match + exact_match = Rolle.objects.filter(name="IT-Sicherheitsbeauftragter") + self.assertEqual(exact_match.count(), 1) + self.assertEqual(exact_match.first(), self.rolle1) + + # Test case-sensitive contains + contains_match = Rolle.objects.filter(name__contains="Sicherheits") + self.assertEqual(contains_match.count(), 1) + self.assertEqual(contains_match.first(), self.rolle1) + + # Test case-insensitive contains + icontains_match = Rolle.objects.filter(name__icontains="sicherheits") + self.assertEqual(icontains_match.count(), 1) + self.assertEqual(icontains_match.first(), self.rolle1) + + def test_role_with_long_descriptions(self): + """Test roles with long description content""" + long_content = "Dies ist eine sehr lange Beschreibung " * 50 # Repeat to make it long + + rolle_long = Rolle.objects.create(name="Rolle mit langer Beschreibung") + beschreibung_long = RollenBeschreibung.objects.create( + abschnitt=rolle_long, + abschnitttyp=self.abschnitttyp, + inhalt=long_content, + order=1 + ) + + # Verify the long content is stored correctly + retrieved = RollenBeschreibung.objects.get(pk=beschreibung_long.pk) + self.assertEqual(retrieved.inhalt, long_content) + self.assertGreater(len(retrieved.inhalt), 1000) # Should be quite long diff --git a/stichworte/tests.py b/stichworte/tests.py index 7ce503c..c9e7cc6 100644 --- a/stichworte/tests.py +++ b/stichworte/tests.py @@ -1,3 +1,225 @@ from django.test import TestCase +from django.core.exceptions import ValidationError +from django.db import models +from .models import Stichwort, Stichworterklaerung +from abschnitte.models import AbschnittTyp -# Create your tests here. + +class StichwortModelTest(TestCase): + """Test cases for Stichwort model""" + + def setUp(self): + """Set up test data""" + self.stichwort = Stichwort.objects.create( + stichwort="Sicherheit" + ) + + def test_stichwort_creation(self): + """Test that Stichwort is created correctly""" + self.assertEqual(self.stichwort.stichwort, "Sicherheit") + + def test_stichwort_str(self): + """Test string representation of Stichwort""" + self.assertEqual(str(self.stichwort), "Sicherheit") + + def test_stichwort_primary_key(self): + """Test that stichwort field is the primary key""" + pk_field = Stichwort._meta.pk + self.assertEqual(pk_field.name, 'stichwort') + self.assertEqual(pk_field.max_length, 50) + + def test_stichwort_verbose_name_plural(self): + """Test verbose name plural""" + self.assertEqual( + Stichwort._meta.verbose_name_plural, + "Stichworte" + ) + + def test_stichwort_max_length(self): + """Test max_length constraint""" + max_length_stichwort = "a" * 50 + stichwort = Stichwort.objects.create(stichwort=max_length_stichwort) + self.assertEqual(stichwort.stichwort, max_length_stichwort) + + def test_stichwort_unique(self): + """Test that stichwort must be unique""" + with self.assertRaises(Exception): + Stichwort.objects.create(stichwort="Sicherheit") + + def test_create_multiple_stichworte(self): + """Test creating multiple Stichwort objects""" + stichworte = ['Datenschutz', 'Netzwerk', 'Backup', 'Verschlüsselung'] + for stichwort in stichworte: + Stichwort.objects.create(stichwort=stichwort) + + self.assertEqual(Stichwort.objects.count(), 5) # Including setUp stichwort + + def test_stichwort_case_sensitivity(self): + """Test that stichwort is case sensitive""" + stichwort_lower = Stichwort.objects.create(stichwort="sicherheit") + self.assertNotEqual(self.stichwort.pk, stichwort_lower.pk) + self.assertEqual(Stichwort.objects.count(), 2) + + +class StichworterklaerungModelTest(TestCase): + """Test cases for Stichworterklaerung model""" + + def setUp(self): + """Set up test data""" + self.stichwort = Stichwort.objects.create( + stichwort="Sicherheit" + ) + self.abschnitttyp = AbschnittTyp.objects.create( + abschnitttyp="text" + ) + self.erklaerung = Stichworterklaerung.objects.create( + erklaerung=self.stichwort, + abschnitttyp=self.abschnitttyp, + inhalt="Dies ist eine Erklärung für Sicherheit.", + order=1 + ) + + def test_stichworterklaerung_creation(self): + """Test that Stichworterklaerung is created correctly""" + self.assertEqual(self.erklaerung.erklaerung, self.stichwort) + self.assertEqual(self.erklaerung.abschnitttyp, self.abschnitttyp) + self.assertEqual(self.erklaerung.inhalt, "Dies ist eine Erklärung für Sicherheit.") + self.assertEqual(self.erklaerung.order, 1) + + def test_stichworterklaerung_foreign_key_relationship(self): + """Test foreign key relationship to Stichwort""" + self.assertEqual(self.erklaerung.erklaerung.stichwort, "Sicherheit") + + def test_stichworterklaerung_cascade_delete(self): + """Test that deleting Stichwort cascades to Stichworterklaerung""" + stichwort_count = Stichwort.objects.count() + erklaerung_count = Stichworterklaerung.objects.count() + + self.stichwort.delete() + + self.assertEqual(Stichwort.objects.count(), stichwort_count - 1) + self.assertEqual(Stichworterklaerung.objects.count(), erklaerung_count - 1) + + def test_stichworterklaerung_verbose_name(self): + """Test verbose name""" + self.assertEqual( + Stichworterklaerung._meta.verbose_name, + "Erklärung" + ) + + def test_stichworterklaerung_multiple_explanations(self): + """Test creating multiple explanations for one Stichwort""" + abschnitttyp2 = AbschnittTyp.objects.create(abschnitttyp="liste ungeordnet") + erklaerung2 = Stichworterklaerung.objects.create( + erklaerung=self.stichwort, + abschnitttyp=abschnitttyp2, + inhalt="Zweite Erklärung für Sicherheit.", + order=2 + ) + + explanations = Stichworterklaerung.objects.filter(erklaerung=self.stichwort) + self.assertEqual(explanations.count(), 2) + self.assertIn(self.erklaerung, explanations) + self.assertIn(erklaerung2, explanations) + + def test_stichworterklaerung_ordering(self): + """Test that explanations can be ordered""" + erklaerung2 = Stichworterklaerung.objects.create( + erklaerung=self.stichwort, + abschnitttyp=self.abschnitttyp, + inhalt="Zweite Erklärung", + order=3 + ) + erklaerung3 = Stichworterklaerung.objects.create( + erklaerung=self.stichwort, + abschnitttyp=self.abschnitttyp, + inhalt="Erste Erklärung", + order=2 + ) + + ordered = Stichworterklaerung.objects.filter(erklaerung=self.stichwort).order_by('order') + expected_order = [self.erklaerung, erklaerung3, erklaerung2] + self.assertEqual(list(ordered), expected_order) + + def test_stichworterklaerung_blank_fields(self): + """Test that optional fields can be blank/null""" + stichwort2 = Stichwort.objects.create(stichwort="Test") + erklaerung_blank = Stichworterklaerung.objects.create( + erklaerung=stichwort2 + ) + + self.assertIsNone(erklaerung_blank.abschnitttyp) + self.assertIsNone(erklaerung_blank.inhalt) + self.assertEqual(erklaerung_blank.order, 0) + + def test_stichworterklaerung_inheritance(self): + """Test that Stichworterklaerung inherits from Textabschnitt""" + # Check that it has the expected fields from Textabschnitt + self.assertTrue(hasattr(self.erklaerung, 'abschnitttyp')) + self.assertTrue(hasattr(self.erklaerung, 'inhalt')) + self.assertTrue(hasattr(self.erklaerung, 'order')) + + # Check that the fields work as expected + self.assertIsInstance(self.erklaerung.abschnitttyp, AbschnittTyp) + self.assertIsInstance(self.erklaerung.inhalt, str) + self.assertIsInstance(self.erklaerung.order, int) + + +class StichwortIntegrationTest(TestCase): + """Integration tests for Stichwort app""" + + def setUp(self): + """Set up test data""" + self.stichwort1 = Stichwort.objects.create(stichwort="IT-Sicherheit") + self.stichwort2 = Stichwort.objects.create(stichwort="Datenschutz") + + self.abschnitttyp = AbschnittTyp.objects.create(abschnitttyp="text") + + self.erklaerung1 = Stichworterklaerung.objects.create( + erklaerung=self.stichwort1, + abschnitttyp=self.abschnitttyp, + inhalt="Erklärung für IT-Sicherheit", + order=1 + ) + + self.erklaerung2 = Stichworterklaerung.objects.create( + erklaerung=self.stichwort2, + abschnitttyp=self.abschnitttyp, + inhalt="Erklärung für Datenschutz", + order=1 + ) + + def test_stichwort_with_explanations_query(self): + """Test querying Stichworte with their explanations""" + stichworte_with_explanations = Stichwort.objects.filter( + stichworterklaerung__isnull=False + ).distinct() + + self.assertEqual(stichworte_with_explanations.count(), 2) + self.assertIn(self.stichwort1, stichworte_with_explanations) + self.assertIn(self.stichwort2, stichworte_with_explanations) + + def test_stichwort_without_explanations(self): + """Test finding Stichworte without explanations""" + stichwort3 = Stichwort.objects.create(stichwort="Backup") + + stichworte_without_explanations = Stichwort.objects.filter( + stichworterklaerung__isnull=True + ) + + self.assertEqual(stichworte_without_explanations.count(), 1) + self.assertEqual(stichworte_without_explanations.first(), stichwort3) + + def test_explanation_count_annotation(self): + """Test annotating Stichworte with explanation count""" + from django.db.models import Count + + stichworte_with_count = Stichwort.objects.annotate( + explanation_count=Count('stichworterklaerung') + ) + + for stichwort in stichworte_with_count: + if stichwort.stichwort in ["IT-Sicherheit", "Datenschutz"]: + self.assertEqual(stichwort.explanation_count, 1) + else: + self.assertEqual(stichwort.explanation_count, 0)