feat(kunde): Validierung, KundeService + DP-Schnittstelle (Phase 3-5)

pull/1/head
3027248 2026-06-25 15:19:52 +02:00
parent 9eaa2b6790
commit 8fd72c2691
21 changed files with 603 additions and 92 deletions

7
.gitignore vendored
View File

@ -301,3 +301,10 @@ TSWLatexianTemp*
# Uncomment the next line to have this generated file ignored.
#*Notes.bib
# ---> Java / Maven
target/
*.class
# ---> JetBrains (IntelliJ IDEA)
.idea/
*.iml

8
.idea/.gitignore vendored
View File

@ -1,8 +0,0 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

View File

@ -1,9 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

View File

@ -1,15 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CheckStyle-IDEA" serialisationVersion="2">
<checkstyleVersion>12.3.1</checkstyleVersion>
<scanScope>JavaOnly</scanScope>
<option name="thirdPartyClasspath" />
<option name="activeLocationIds" />
<option name="locations">
<list>
<ConfigurationLocation id="bundled-sun-checks" type="BUNDLED" scope="All" description="Sun Checks">(bundled)</ConfigurationLocation>
<ConfigurationLocation id="bundled-google-checks" type="BUNDLED" scope="All" description="Google Checks">(bundled)</ConfigurationLocation>
</list>
</option>
</component>
</project>

View File

@ -1,13 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<annotationProcessing>
<profile name="Maven default annotation processors profile" enabled="true">
<sourceOutputDir name="target/generated-sources/annotations" />
<sourceTestOutputDir name="target/generated-test-sources/test-annotations" />
<outputRelativeToContentRoot value="true" />
<module name="fakturierungssystem" />
</profile>
</annotationProcessing>
</component>
</project>

View File

@ -1,7 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding">
<file url="file://$PROJECT_DIR$/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/src/main/resources" charset="UTF-8" />
</component>
</project>

View File

@ -1,20 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RemoteRepositoriesConfiguration">
<remote-repository>
<option name="id" value="central" />
<option name="name" value="Central Repository" />
<option name="url" value="https://repo.maven.apache.org/maven2" />
</remote-repository>
<remote-repository>
<option name="id" value="central" />
<option name="name" value="Maven Central repository" />
<option name="url" value="https://repo1.maven.org/maven2" />
</remote-repository>
<remote-repository>
<option name="id" value="jboss.community" />
<option name="name" value="JBoss Community repository" />
<option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" />
</remote-repository>
</component>
</project>

View File

@ -1,14 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="MavenProjectsManager">
<option name="originalFiles">
<list>
<option value="$PROJECT_DIR$/pom.xml" />
</list>
</option>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_23" default="true" project-jdk-name="openjdk-23" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

View File

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

View File

@ -0,0 +1,91 @@
package de.hsmannheim.faktura.kunde.model;
import java.util.Objects;
/**
* Datenobjekt "Kunde" gemäß Pflichtenheft Kap. 6.1.2.
*
* Reiner Datentyp (POJO) ohne fachliche Validierung die Prüfung von
* Pflichtfeldern, der Bedingung firmenname||nachname und des E-Mail-Formats
* erfolgt in Phase 3 im KundeService, nicht in dieser Klasse.
*/
public class Kunde {
private long kundeId; // Pflicht: fortlaufender, eindeutiger Primärschlüssel
private String firmenname; // Pflicht*: sofern kein Nachname (Constraint in Phase 3)
private String nachname; // Pflicht*: sofern kein Firmenname
private String vorname; // Optional
private String strasse; // Pflicht: Straße + Hausnummer
private String plz; // Pflicht: String, um führende Nullen zu erhalten!
private String ort; // Pflicht
private String telefon; // Optional
private String email; // Optional: Formatprüfung in Phase 3
private String ustIdNr; // Optional
private String lieferadresse; // Optional: abweichende Lieferadresse
private String ansprechpartner; // Optional
/** No-arg-Konstruktor von Jackson für die JSON-Deserialisierung benötigt (P1-3). */
public Kunde() {
}
// --- Getter und Setter (P1-2) ---
public long getKundeId() { return kundeId; }
public void setKundeId(long kundeId) { this.kundeId = kundeId; }
public String getFirmenname() { return firmenname; }
public void setFirmenname(String firmenname) { this.firmenname = firmenname; }
public String getNachname() { return nachname; }
public void setNachname(String nachname) { this.nachname = nachname; }
public String getVorname() { return vorname; }
public void setVorname(String vorname) { this.vorname = vorname; }
public String getStrasse() { return strasse; }
public void setStrasse(String strasse) { this.strasse = strasse; }
public String getPlz() { return plz; }
public void setPlz(String plz) { this.plz = plz; }
public String getOrt() { return ort; }
public void setOrt(String ort) { this.ort = ort; }
public String getTelefon() { return telefon; }
public void setTelefon(String telefon) { this.telefon = telefon; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public String getUstIdNr() { return ustIdNr; }
public void setUstIdNr(String ustIdNr) { this.ustIdNr = ustIdNr; }
public String getLieferadresse() { return lieferadresse; }
public void setLieferadresse(String lieferadresse) { this.lieferadresse = lieferadresse; }
public String getAnsprechpartner() { return ansprechpartner; }
public void setAnsprechpartner(String ansprechpartner) { this.ansprechpartner = ansprechpartner; }
// --- Identität über den Primärschlüssel kundeId (P1-2) ---
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Kunde other = (Kunde) o;
return kundeId == other.kundeId;
}
@Override
public int hashCode() {
return Objects.hash(kundeId);
}
@Override
public String toString() {
return "Kunde{kundeId=" + kundeId
+ ", firmenname='" + firmenname + '\''
+ ", nachname='" + nachname + '\''
+ ", ort='" + ort + '\'' + '}';
}
}

View File

@ -0,0 +1,24 @@
package de.hsmannheim.faktura.kunde.repository;
import de.hsmannheim.faktura.kunde.model.Kunde;
import java.util.ArrayList;
import java.util.List;
/**
* Reiner Persistenz-Container: bündelt die gespeicherten Kunden und den
* fortlaufenden ID-Zähler in einem JSON-Objekt. Der mitgespeicherte Zähler
* garantiert, dass vergebene IDs nach einem Neustart nicht wiederverwendet
* werden (P2-3).
*/
public class KundeContainer {
private List<Kunde> kunden = new ArrayList<>();
private long naechsteId = 1;
public List<Kunde> getKunden() { return kunden; }
public void setKunden(List<Kunde> kunden) { this.kunden = kunden; }
public long getNaechsteId() { return naechsteId; }
public void setNaechsteId(long naechsteId) { this.naechsteId = naechsteId; }
}

View File

@ -0,0 +1,22 @@
package de.hsmannheim.faktura.kunde.repository;
import de.hsmannheim.faktura.kunde.model.Kunde;
import java.util.List;
import java.util.Optional;
/**
* Abstrakte Datenhaltung für Kunden (IF-02, NF-ARCH-02).
* Kapselt die Speichertechnologie, sodass sie austauschbar ist.
*/
public interface KundeRepository {
/** Legt neu an (kundeId == 0) oder aktualisiert; liefert die (ggf. neu vergebene) kundeId. */
long speichern(Kunde kunde);
Optional<Kunde> findeById(long kundeId);
List<Kunde> findeAlle();
void loeschen(long kundeId);
}

View File

@ -0,0 +1,89 @@
package de.hsmannheim.faktura.kunde.repository;
import com.fasterxml.jackson.databind.ObjectMapper;
import de.hsmannheim.faktura.kunde.model.Kunde;
import java.io.File;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
/**
* JSON-basierte Umsetzung von KundeRepository (NF-ARCH-01).
* Hält die Kunden im Speicher und schreibt nach jeder Änderung in eine
* lokale JSON-Datei. Beim Erzeugen wird eine vorhandene Datei geladen.
*/
public class KundeRepositoryJson implements KundeRepository {
private final File datei;
private final ObjectMapper mapper = new ObjectMapper();
private final Map<Long, Kunde> kunden = new LinkedHashMap<>();
private long naechsteId = 1;
public KundeRepositoryJson(File datei) {
this.datei = datei;
laden();
}
@Override
public long speichern(Kunde kunde) {
if (kunde.getKundeId() == 0) {
// Neuanlage: nächste freie ID vergeben, Zähler hochzählen (kein Reuse)
kunde.setKundeId(naechsteId);
naechsteId++;
}
kunden.put(kunde.getKundeId(), kunde);
speichernInDatei();
return kunde.getKundeId();
}
@Override
public Optional<Kunde> findeById(long kundeId) {
return Optional.ofNullable(kunden.get(kundeId));
}
@Override
public List<Kunde> findeAlle() {
return new ArrayList<>(kunden.values());
}
@Override
public void loeschen(long kundeId) {
kunden.remove(kundeId);
speichernInDatei();
}
// --- interne Persistenz-Hilfen ---
private void laden() {
if (!datei.exists()) {
return; // erster Start: leeres Repository
}
try {
KundeContainer container = mapper.readValue(datei, KundeContainer.class);
kunden.clear();
for (Kunde k : container.getKunden()) {
kunden.put(k.getKundeId(), k);
}
naechsteId = container.getNaechsteId();
} catch (IOException e) {
throw new UncheckedIOException("Kundendaten konnten nicht geladen werden: " + datei, e);
}
}
private void speichernInDatei() {
try {
KundeContainer container = new KundeContainer();
container.setKunden(new ArrayList<>(kunden.values()));
container.setNaechsteId(naechsteId);
mapper.writerWithDefaultPrettyPrinter().writeValue(datei, container);
} catch (IOException e) {
throw new UncheckedIOException("Kundendaten konnten nicht gespeichert werden: " + datei, e);
}
}
}

View File

@ -0,0 +1,12 @@
package de.hsmannheim.faktura.kunde.service;
/**
* Schmale Abstraktion über die Belegseite (Gruppe F) für die GR-05-Löschsperre.
* Liefert true, wenn der Kunde in mindestens einem Beleg referenziert ist.
* Die echte Umsetzung liefert Gruppe F (BelegRepository, OP-1/P0-4); bis dahin
* und in Tests wird gegen dieses Interface programmiert.
*/
@FunctionalInterface
public interface BelegReferenzPruefer {
boolean istKundeReferenziert(long kundeId);
}

View File

@ -0,0 +1,89 @@
package de.hsmannheim.faktura.kunde.service;
import de.hsmannheim.faktura.kunde.model.Kunde;
import de.hsmannheim.faktura.kunde.repository.KundeRepository;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
/**
* Fachliche Logik der Kundenverwaltung (F-KV-0104 / BA-KV-0104).
* Orchestriert Validierung (Phase 3) und Persistenz (Phase 2); enthält
* selbst keine Speicher- oder Prüf-Details (NF-MAINT-03, Schichtentrennung).
* Die vier öffentlichen Methoden entsprechen exakt dem Schnittstellenvertrag K1.
*/
public class KundeService {
private final KundeRepository repository;
private final KundeValidator validator;
private final BelegReferenzPruefer belegPruefer;
public KundeService(KundeRepository repository,
KundeValidator validator,
BelegReferenzPruefer belegPruefer) {
this.repository = repository;
this.validator = validator;
this.belegPruefer = belegPruefer;
}
/** P4-1 · F-KV-01 / BA-KV-01: validieren, ID vergeben lassen, persistieren. */
public long anlegen(Kunde kunde) {
validator.pruefe(kunde); // wirft bei Verstoß -> speichern wird nie erreicht (P3-4)
kunde.setKundeId(0); // Neuanlage erzwingen: Repository vergibt die nächste ID
return repository.speichern(kunde);
}
/** P4-2 · F-KV-02 / BA-KV-02: vorhandenen Kunden laden, validieren, persistieren. */
public void bearbeiten(Kunde kunde) {
if (repository.findeById(kunde.getKundeId()).isEmpty()) {
throw new ValidierungsException(
"Kunde mit ID " + kunde.getKundeId() + " existiert nicht.");
}
validator.pruefe(kunde);
repository.speichern(kunde);
}
/** P4-3 · F-KV-03 / BA-KV-03: Treffer über Firmenname/Nachname UND Kundennummer. */
public List<Kunde> suchen(String suchbegriff) {
if (suchbegriff == null || suchbegriff.isBlank()) {
return List.of();
}
String begriff = suchbegriff.trim().toLowerCase();
return repository.findeAlle().stream()
.filter(k -> passt(k, begriff))
.collect(Collectors.toList());
}
/** P5-1 · K3: Kundenauflösung per ID für Gruppe F (Ersatz für deren FakeCustomerLookup). */
public Optional<Kunde> finde(long kundeId) {
return repository.findeById(kundeId);
}
/** P4-4 / P4-5 · F-KV-04 / BA-KV-04 / GR-05 / NF-SEC-02: nur löschen, wenn unreferenziert. */
public void loeschen(long kundeId) {
Optional<Kunde> vorhanden = repository.findeById(kundeId);
if (vorhanden.isEmpty()) {
throw new ValidierungsException(
"Kunde mit ID " + kundeId + " existiert nicht.");
}
if (belegPruefer.istKundeReferenziert(kundeId)) {
throw new LoeschsperreException(
"Kunde " + kundeId + " ist in mindestens einem Beleg referenziert "
+ "und darf nicht gelöscht werden (GR-05).");
}
repository.loeschen(kundeId);
}
// --- interne Suchhilfen ---
private boolean passt(Kunde k, String begriff) {
if (enthaelt(k.getFirmenname(), begriff)) return true;
if (enthaelt(k.getNachname(), begriff)) return true;
return String.valueOf(k.getKundeId()).equals(begriff); // Suche über Kundennummer
}
private boolean enthaelt(String feld, String begriff) {
return feld != null && feld.toLowerCase().contains(begriff);
}
}

View File

@ -0,0 +1,52 @@
package de.hsmannheim.faktura.kunde.service;
import de.hsmannheim.faktura.kunde.model.Kunde;
import java.util.regex.Pattern;
/**
* Prüft einen Kunden gegen die fachlichen Regeln aus Pflichtenheft Kap. 6.1.2.
* Wirft bei Verstoß eine ValidierungsException der aufrufende Service
* persistiert dann nicht (P3-4).
*/
public class KundeValidator {
// Pragmatische Prüfung: etwas vor dem @, etwas danach, ein Punkt mit Endung.
// Lehnt z. B. "kunde@" ab (MT-KV-04), ohne RFC-Vollständigkeit zu erzwingen.
private static final Pattern EMAIL_MUSTER =
Pattern.compile("^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$");
public void pruefe(Kunde kunde) {
if (kunde == null) {
throw new ValidierungsException("Kunde darf nicht null sein.");
}
// P3-2: mindestens Firmenname ODER Nachname
if (istLeer(kunde.getFirmenname()) && istLeer(kunde.getNachname())) {
throw new ValidierungsException(
"Entweder Firmenname oder Nachname muss angegeben sein.");
}
// P3-1: Pflichtfelder der Adresse
if (istLeer(kunde.getStrasse())) {
throw new ValidierungsException("Straße ist ein Pflichtfeld.");
}
if (istLeer(kunde.getPlz())) {
throw new ValidierungsException("PLZ ist ein Pflichtfeld.");
}
if (istLeer(kunde.getOrt())) {
throw new ValidierungsException("Ort ist ein Pflichtfeld.");
}
// P3-3: E-Mail nur prüfen, wenn überhaupt angegeben
if (!istLeer(kunde.getEmail())
&& !EMAIL_MUSTER.matcher(kunde.getEmail()).matches()) {
throw new ValidierungsException(
"E-Mail-Adresse hat kein gültiges Format: " + kunde.getEmail());
}
}
private boolean istLeer(String wert) {
return wert == null || wert.isBlank();
}
}

View File

@ -0,0 +1,11 @@
package de.hsmannheim.faktura.kunde.service;
/**
* Wird geworfen, wenn ein Kunde wegen Beleg-Referenz (GR-05) nicht
* gelöscht werden darf.
*/
public class LoeschsperreException extends RuntimeException {
public LoeschsperreException(String message) {
super(message);
}
}

View File

@ -0,0 +1,13 @@
package de.hsmannheim.faktura.kunde.service;
/**
* Wird geworfen, wenn ein Kunde gegen eine fachliche Validierungsregel
* verstößt (Pflichtenheft Kap. 6.1.2). Unchecked, damit die im
* Schnittstellenvertrag K1 festgelegten Service-Signaturen ohne
* throws-Klausel auskommen.
*/
public class ValidierungsException extends RuntimeException {
public ValidierungsException(String message) {
super(message);
}
}

View File

@ -0,0 +1,40 @@
package de.hsmannheim.faktura.kunde.repository;
import de.hsmannheim.faktura.kunde.model.Kunde;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.io.File;
import java.nio.file.Path;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
class KundeRepositoryJsonTest {
@Test
void persistenz_ueberlebt_neustart(@TempDir Path tempDir) {
File datei = tempDir.resolve("kunden.json").toFile();
// 1. Repository anlegen und einen Kunden speichern
KundeRepository repo1 = new KundeRepositoryJson(datei);
Kunde kunde = new Kunde();
kunde.setNachname("Mustermann");
kunde.setStrasse("Hauptstr. 1");
kunde.setPlz("01067"); // führende Null!
kunde.setOrt("Dresden");
long id = repo1.speichern(kunde);
// 2. Neues Repository auf dieselbe Datei = simulierter Neustart
KundeRepository repo2 = new KundeRepositoryJson(datei);
Optional<Kunde> geladen = repo2.findeById(id);
// 3. Kunde ist unverändert wiederhergestellt
assertTrue(geladen.isPresent(), "Kunde sollte nach Neustart vorhanden sein");
assertEquals("Mustermann", geladen.get().getNachname());
assertEquals("01067", geladen.get().getPlz(),
"Führende Null der PLZ muss erhalten bleiben");
assertEquals("Dresden", geladen.get().getOrt());
}
}

View File

@ -0,0 +1,99 @@
package de.hsmannheim.faktura.kunde.service;
import de.hsmannheim.faktura.kunde.model.Kunde;
import de.hsmannheim.faktura.kunde.repository.KundeRepository;
import de.hsmannheim.faktura.kunde.repository.KundeRepositoryJson;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.io.File;
import java.nio.file.Path;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
class KundeServiceTest {
private KundeService service;
private boolean kundeIstReferenziert; // steuert den BelegReferenzPruefer im Test
@BeforeEach
void setUp(@TempDir Path tempDir) {
File datei = tempDir.resolve("kunden.json").toFile();
KundeRepository repository = new KundeRepositoryJson(datei);
KundeValidator validator = new KundeValidator();
BelegReferenzPruefer belegPruefer = kundeId -> kundeIstReferenziert;
service = new KundeService(repository, validator, belegPruefer);
kundeIstReferenziert = false;
}
private Kunde gueltigerKunde() {
Kunde k = new Kunde();
k.setNachname("Mustermann");
k.setStrasse("Hauptstr. 1");
k.setPlz("68159");
k.setOrt("Mannheim");
return k;
}
@Test
void anlegen_vergibtIdUndPersistiert() { // BA-KV-01
long id = service.anlegen(gueltigerKunde());
assertTrue(id > 0);
assertEquals(1, service.suchen("Mustermann").size());
}
@Test
void anlegen_ungueltig_wirdNichtGespeichert() { // P3-4 im Service
Kunde k = gueltigerKunde();
k.setNachname(null);
assertThrows(ValidierungsException.class, () -> service.anlegen(k));
assertTrue(service.suchen("Mustermann").isEmpty());
}
@Test
void bearbeiten_aendertGespeichertenKunden() { // BA-KV-02
service.anlegen(gueltigerKunde());
Kunde k = service.suchen("Mustermann").get(0);
k.setTelefon("0621 12345");
service.bearbeiten(k);
assertEquals("0621 12345", service.suchen("Mustermann").get(0).getTelefon());
}
@Test
void suchen_ueberKundennummer_findet() { // BA-KV-03 / MT-KV-08
long id = service.anlegen(gueltigerKunde());
assertEquals(1, service.suchen(String.valueOf(id)).size());
}
@Test
void suchen_ohneTreffer_istLeer() { // BA-KV-03 / MT-KV-09
service.anlegen(gueltigerKunde());
assertTrue(service.suchen("Gibtsnicht").isEmpty());
}
@Test
void loeschen_nichtReferenziert_entfernt() { // BA-KV-04 / MT-KV-10
long id = service.anlegen(gueltigerKunde());
kundeIstReferenziert = false;
service.loeschen(id);
assertTrue(service.suchen("Mustermann").isEmpty());
}
@Test
void loeschen_referenziert_wirdAbgewiesen() { // BA-KV-04 / GR-05 / MT-KV-11
long id = service.anlegen(gueltigerKunde());
kundeIstReferenziert = true;
assertThrows(LoeschsperreException.class, () -> service.loeschen(id));
assertEquals(1, service.suchen("Mustermann").size()); // noch vorhanden
}
@Test
void finde_liefertAngelegtenKunden() { // K3
long id = service.anlegen(gueltigerKunde());
assertTrue(service.finde(id).isPresent());
assertEquals(id, service.finde(id).get().getKundeId());
assertTrue(service.finde(999L).isEmpty()); // unbekannte ID -> leer
}
}

View File

@ -0,0 +1,54 @@
package de.hsmannheim.faktura.kunde.service;
import de.hsmannheim.faktura.kunde.model.Kunde;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertThrows;
class KundeValidatorTest {
private final KundeValidator validator = new KundeValidator();
private Kunde gueltigerKunde() {
Kunde k = new Kunde();
k.setNachname("Mustermann");
k.setStrasse("Hauptstr. 1");
k.setPlz("68159");
k.setOrt("Mannheim");
return k;
}
@Test
void gueltigerKunde_wirftNicht() {
assertDoesNotThrow(() -> validator.pruefe(gueltigerKunde()));
}
@Test
void ohneFirmennameUndNachname_wirdAbgelehnt() { // → MT-KV-02
Kunde k = gueltigerKunde();
k.setNachname(null);
assertThrows(ValidierungsException.class, () -> validator.pruefe(k));
}
@Test
void ohneStrasse_wirdAbgelehnt() { // → MT-KV-03
Kunde k = gueltigerKunde();
k.setStrasse(null);
assertThrows(ValidierungsException.class, () -> validator.pruefe(k));
}
@Test
void ungueltigeEmail_wirdAbgelehnt() { // → MT-KV-04
Kunde k = gueltigerKunde();
k.setEmail("kunde@");
assertThrows(ValidierungsException.class, () -> validator.pruefe(k));
}
@Test
void gueltigeEmail_wirftNicht() {
Kunde k = gueltigerKunde();
k.setEmail("kunde@example.com");
assertDoesNotThrow(() -> validator.pruefe(k));
}
}