2026-10-06 13:04:16 +02:00
"""Tests für die Eigentümersuche im Serienbriefexport (#56).
Alle Namen und Anschriften sind erfunden.
"""
import contextlib
import io
import types
import unittest
from unittest.mock import patch
from sn_verfahrensgebiet.functions import eigentuemer_suche as es
from sn_verfahrensgebiet.functions.eigentuemer_suche import Eigentuemer
KOPF = [
" EIG_KONTO" , "PER_ID" , "PER_VERSTORBEN" , "PER_NR" , "Anrede" , "Titel" , "Vorname" , "Nachname" ,
"OT" , "STRASSE" , "PLZ" , "WOHNORT" , "Email" , "PER_KONTOINHABER" , "PER_RECHTSPERSON" ,
"EIG_GEMARKUNG" , "EIG_BLATT" , "LISTE_ZGBBL" , "ANZAHL_PERSONEN" , "EIG_ID" ,
]
class _Null :
"""Nachbildung von QGIS-NULL."""
def isNull ( self ):
return True
class _Feature ( dict ):
def __init__ ( self , fid , werte ):
super () . __init__ ( werte )
self . _fid = fid
def id ( self ):
return self . _fid
class _Layer :
def __init__ ( self , zeilen , kopf = KOPF , name = "Serienbrief" , layer_id = "csv1" ):
self . _kopf = list ( kopf )
self . _name = name
self . _id = layer_id
self . _features = [
_Feature ( i + 1 , { spalte : zeile . get ( spalte . lstrip ( " " ), "" ) for spalte in self . _kopf })
for i , zeile in enumerate ( zeilen )
]
def name ( self ):
return self . _name
def id ( self ):
return self . _id
def fields ( self ):
return types . SimpleNamespace ( names = lambda : list ( self . _kopf ))
def getFeatures ( self ):
return iter ( self . _features )
def _zeile ( nachname , vorname , strasse = "Hauptstraße 1" , plz = "01234" , ort = "Musterstadt" , ** weitere ):
zeile = { "Nachname" : nachname , "Vorname" : vorname , "STRASSE" : strasse , "PLZ" : plz , "WOHNORT" : ort }
zeile . update ( weitere )
return zeile
class TestLayererkennung ( unittest . TestCase ):
def test_layer_mit_name_und_vorname_ist_eigentuemerlayer ( self ):
"""Prüft, dass ein Layer mit den Spalten Nachname und Vorname als Eigentümerdatei erkannt wird, auch mit BOM und anderer Schreibweise."""
self . assertTrue ( es . ist_eigentuemerlayer ( _Layer ([])))
self . assertTrue ( es . ist_eigentuemerlayer ( _Layer ([], kopf = [ " NACHNAME" , "vorname" ])))
self . assertFalse ( es . ist_eigentuemerlayer ( _Layer ([], kopf = [ "flurstueckskennzeichen" ])))
self . assertFalse ( es . ist_eigentuemerlayer ( object ()))
def test_eigentuemerlayer_im_projekt ( self ):
"""Prüft, dass aus dem Projekt nur passende Layer kommen, nach Namen sortiert."""
b = _Layer ([], name = "b-Export" , layer_id = "b" )
a = _Layer ([], name = "A-Export" , layer_id = "a" )
flst = _Layer ([], kopf = [ "flurstueckskennzeichen" ], name = "Flurstücke" , layer_id = "f" )
projekt = types . SimpleNamespace ( mapLayers = lambda : { "b" : b , "f" : flst , "a" : a })
with patch . object ( es , "QgsProject" , types . SimpleNamespace ( instance = lambda : projekt )):
self . assertEqual ( es . eigentuemerlayer_im_projekt (), [ a , b ])
class TestSuche ( unittest . TestCase ):
def test_ohne_eingabe_fehler ( self ):
"""Prüft, dass ohne Name und Vorname nicht die ganze Datei gelistet, sondern um eine Eingabe gebeten wird."""
with self . assertRaisesRegex ( es . EigentuemerSucheFehler , "Namen" ):
es . suche_eigentuemer ( _Layer ([ _zeile ( "Beispiel" , "Anna" )]), " " , "" )
def test_layer_ohne_pflichtspalten ( self ):
"""Prüft, dass ein Layer ohne Nachname/Vorname mit Hinweis auf den Serienbriefexport abgelehnt wird."""
with self . assertRaisesRegex ( es . EigentuemerSucheFehler , "Serienbriefexport" ):
es . suche_eigentuemer ( _Layer ([], kopf = [ "flurstueckskennzeichen" ]), "Beispiel" , "" )
def test_teilstring_ohne_gross_kleinschreibung ( self ):
"""Prüft, dass Name und Vorname als Teilstring ohne Rücksicht auf Groß-/Kleinschreibung gesucht werden."""
layer = _Layer ([
_zeile ( "Beispiel" , "Anna" ),
_zeile ( "Beispiel-Muster" , "Bernd" ),
_zeile ( "Anders" , "Anna" ),
])
self . assertEqual ([ e . nachname for e in es . suche_eigentuemer ( layer , "beispiel" , "" )],
[ "Beispiel" , "Beispiel-Muster" ])
self . assertEqual ([ e . nachname for e in es . suche_eigentuemer ( layer , "" , "ANNA" )], [ "Anders" , "Beispiel" ])
self . assertEqual ([ e . vorname for e in es . suche_eigentuemer ( layer , "beisp" , "an" )], [ "Anna" ])
def test_umlaute_und_eszett ( self ):
"""Prüft, dass „Müller“ auch „Mueller“ findet und „Strauss“ auch „Strauß“."""
layer = _Layer ([ _zeile ( "Mueller" , "Jörg" ), _zeile ( "Strauß" , "Erika" , ort = "Anderswo" )])
self . assertEqual ( len ( es . suche_eigentuemer ( layer , "Müller" , "Joerg" )), 1 )
self . assertEqual ( len ( es . suche_eigentuemer ( layer , "Strauss" , "" )), 1 )
def test_gleiche_person_gleiche_anschrift_zusammengefasst ( self ):
"""Prüft, dass Zeilen mit gleichem Namen und gleicher Anschrift ein Treffer werden und Konten, Personen-IDs und E-Mails gesammelt werden."""
layer = _Layer ([
_zeile ( "Beispiel" , "Anna" , EIG_KONTO = "100" , PER_ID = "7" , Email = "anna@example.org" ),
_zeile ( "beispiel " , " Anna" , strasse = "Hauptstraße 1" , EIG_KONTO = "200" , PER_ID = "7" ,
Titel = "Dr." , Email = "anna@example.org" ),
_zeile ( "Beispiel" , "Anna" , EIG_KONTO = "300" , PER_ID = "8" , Email = "a.b@example.org" ),
])
treffer = es . suche_eigentuemer ( layer , "Beispiel" , "Anna" )
self . assertEqual ( len ( treffer ), 1 )
e = treffer [ 0 ]
self . assertEqual ( e . konten , ( "100" , "200" , "300" ))
self . assertEqual ( e . per_ids , ( "7" , "8" ))
self . assertEqual ( e . emails , ( "anna@example.org" , "a.b@example.org" ))
self . assertEqual ( e . feature_ids , ( 1 , 2 , 3 ))
self . assertEqual ( e . titel , "Dr." )
def test_andere_anschrift_eigener_treffer ( self ):
"""Prüft, dass dieselbe Person mit anderer Anschrift ein eigener Treffer bleibt, sortiert nach Wohnort."""
layer = _Layer ([
_zeile ( "Beispiel" , "Anna" , ort = "Zettlitz" ),
_zeile ( "Beispiel" , "Anna" , ort = "Altdorf" , plz = "09999" ),
])
treffer = es . suche_eigentuemer ( layer , "Beispiel" , "Anna" )
self . assertEqual ([ e . wohnort for e in treffer ], [ "Altdorf" , "Zettlitz" ])
2026-10-06 13:13:50 +02:00
def test_kennzeichen_verstorben ( self ):
"""Prüft, dass „verstorben“ beim Exportwert 1 (auch als Zahl gelesen) gesetzt ist und bei 0, Leerem und NULL nicht."""
ja = [ "1" , 1 ]
nein = [ "0" , 0 , "" , None , _Null ()]
zeilen = [ _zeile ( f "Ja { i } " , "Test" , PER_VERSTORBEN = w ) for i , w in enumerate ( ja )]
zeilen += [ _zeile ( f "Nein { i } " , "Test" , PER_VERSTORBEN = w ) for i , w in enumerate ( nein )]
2026-10-06 13:04:16 +02:00
for e in es . suche_eigentuemer ( _Layer ( zeilen ), "" , "Test" ):
erwartet = e . nachname . startswith ( "Ja" )
with self . subTest ( name = e . nachname ):
self . assertEqual ( e . verstorben , erwartet )
2026-10-06 13:13:50 +02:00
self . assertEqual ( e . hinweise , "verstorben" if erwartet else "" )
def test_rechtsperson_als_rohwert ( self ):
"""Prüft, dass PER_RECHTSPERSON unverändert angezeigt wird (0, 1, 5, 9; noch nicht entschlüsselt) und abweichende Werte einer Person gesammelt werden."""
layer = _Layer ([
_zeile ( "Beispiel" , "Anna" , PER_RECHTSPERSON = "0" ),
_zeile ( "Gemeinde Musterstadt" , "" , strasse = "Markt 1" , PER_RECHTSPERSON = 5 ),
_zeile ( "Muster" , "Bernd" , PER_RECHTSPERSON = "1" ),
_zeile ( "Muster" , "Bernd" , PER_RECHTSPERSON = "9" ),
_zeile ( "Ohnewert" , "Clara" , PER_RECHTSPERSON = _Null ()),
])
werte = { e . nachname : e . rechtsperson_text for e in es . suche_eigentuemer ( layer , "e" , "" )}
self . assertEqual ( werte , { "Beispiel" : "0" , "Gemeinde Musterstadt" : "5" , "Muster" : "1, 9" , "Ohnewert" : "" })
2026-10-06 13:04:16 +02:00
def test_plz_mit_fuehrender_null ( self ):
"""Prüft, dass eine von QGIS als Zahl gelesene PLZ wieder fünfstellig mit führender Null erscheint und Zeilen dadurch zusammenfinden."""
layer = _Layer ([
_zeile ( "Beispiel" , "Anna" , plz = 1067 ),
_zeile ( "Beispiel" , "Anna" , plz = "01067" ),
_zeile ( "Muster" , "Bernd" , plz = "1067.0" ),
])
treffer = es . suche_eigentuemer ( layer , "e" , "" )
self . assertEqual ([( e . nachname , e . plz ) for e in treffer ], [( "Beispiel" , "01067" ), ( "Muster" , "01067" )])
def test_suche_gibt_nichts_aus ( self ):
"""Prüft, dass die Suche keine Personendaten auf die Konsole schreibt (Datenschutz)."""
ausgabe = io . StringIO ()
with contextlib . redirect_stdout ( ausgabe ), contextlib . redirect_stderr ( ausgabe ):
es . suche_eigentuemer ( _Layer ([ _zeile ( "Beispiel" , "Anna" )]), "Beispiel" , "" )
self . assertEqual ( ausgabe . getvalue (), "" )
class TestAnzeigeformen ( unittest . TestCase ):
def test_name_und_anschrift ( self ):
"""Prüft, dass Name mit Titel und einzeilige Anschrift mit Ortsteil lesbar zusammengesetzt werden."""
e = Eigentuemer ( titel = "Dr." , vorname = "Anna" , nachname = "Beispiel" , strasse = "Hauptstraße 1" ,
plz = "01234" , wohnort = "Musterstadt" , ortsteil = "Unterdorf" )
self . assertEqual ( e . name , "Beispiel, Dr. Anna" )
self . assertEqual ( e . anschrift , "Hauptstraße 1, 01234 Musterstadt, OT Unterdorf" )
self . assertEqual ( Eigentuemer ( nachname = "Beispiel" , ortsteil = "OT Unterdorf" ) . anschrift , "OT Unterdorf" )
self . assertEqual ( Eigentuemer ( nachname = "Gemeinde Musterstadt" ) . name , "Gemeinde Musterstadt" )
if __name__ == "__main__" :
unittest . main ()