V2.9: Integrierte PDF-Vorschau und editierbarer Dateiname im Verarbeitungslauf
Neu im Tab "Verarbeitungslauf": - Integrierte PDF-Vorschau der Quelldatei mit Lazy Rendering (Seite 1 sofort, weitere Seiten on-demand), Cache pro Selektion, "latest preview request wins" - Editierbarer KI-Dateinamenvorschlag mit Live-Validierung, Dirty-State-Dialog bei Zeilen-/Tabwechsel, Schließen und Laufstart, atomare FS+DB-Transaktion inkl. Rollback und Fingerprint-basierter Konfliktauflösung Architektur: - Neuer Application-Use-Case ManualFileRenameUseCase und Outbound-Port TargetFileRenamePort mit Filesystem-Adapter - Neuer GuiManualFileRenamePort, verdrahtet im Bootstrap - GuiBatchRunResultRow um correctedFileName erweitert - GuiBatchRunTab auf SplitPane-Layout (60/40) umgebaut, Detail-Panel mit KI-Begründung, FileNameEditorPane und PdfPreviewPane - Spike-Code (PdfViewerSpike) entfernt, produktive Implementierung ersetzt Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+25
@@ -0,0 +1,25 @@
|
||||
package de.gecheckt.pdf.umbenenner.application.port.in;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Ergebnis, wenn das zu umbennende Dokument in der Persistenz nicht gefunden wurde.
|
||||
* <p>
|
||||
* Gibt an, dass kein Dokument-Stammsatz mit dem angegebenen Fingerprint existiert.
|
||||
* Dies kann eintreten, wenn der Fingerprint ungültig ist oder der Datensatz
|
||||
* zwischenzeitlich gelöscht wurde.
|
||||
*
|
||||
* @param reason menschenlesbare Begründung, warum das Dokument nicht gefunden wurde;
|
||||
* nie null
|
||||
*/
|
||||
public record ManualFileRenameDocumentNotFound(String reason) implements ManualFileRenameResult {
|
||||
|
||||
/**
|
||||
* Kompakter Konstruktor zur Validierung des Pflichtfelds.
|
||||
*
|
||||
* @throws NullPointerException wenn {@code reason} null ist
|
||||
*/
|
||||
public ManualFileRenameDocumentNotFound {
|
||||
Objects.requireNonNull(reason, "reason must not be null");
|
||||
}
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
package de.gecheckt.pdf.umbenenner.application.port.in;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Ergebnis, wenn die Umbenennung der Zieldatei im Dateisystem fehlgeschlagen ist.
|
||||
* <p>
|
||||
* Gibt an, dass ein technischer Fehler beim Dateisystemzugriff aufgetreten ist,
|
||||
* z. B. fehlende Schreibrechte, gesperrte Datei durch einen anderen Prozess oder
|
||||
* ein nicht erreichbares Netzlaufwerk. Ebenfalls verwendet, wenn der Zielordner-Port
|
||||
* einen technischen Fehler meldet.
|
||||
* <p>
|
||||
* Gemäß dem Alles-oder-Nichts-Prinzip wird in diesem Fall die Persistenz nicht
|
||||
* aktualisiert.
|
||||
*
|
||||
* @param message menschenlesbare Beschreibung des aufgetretenen Fehlers; nie null
|
||||
*/
|
||||
public record ManualFileRenameFileSystemFailure(String message) implements ManualFileRenameResult {
|
||||
|
||||
/**
|
||||
* Kompakter Konstruktor zur Validierung des Pflichtfelds.
|
||||
*
|
||||
* @throws NullPointerException wenn {@code message} null ist
|
||||
*/
|
||||
public ManualFileRenameFileSystemFailure {
|
||||
Objects.requireNonNull(message, "message must not be null");
|
||||
}
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
package de.gecheckt.pdf.umbenenner.application.port.in;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Ergebnis, wenn das Dokument sich in einem ungültigen Zustand für eine manuelle
|
||||
* Umbenennung befindet.
|
||||
* <p>
|
||||
* Eine Umbenennung ist nur möglich, wenn das Dokument den Status
|
||||
* {@link de.gecheckt.pdf.umbenenner.domain.model.ProcessingStatus#SUCCESS} hat und
|
||||
* ein gültiger {@code lastTargetFileName} sowie {@code lastTargetPath} vorhanden sind.
|
||||
* Dieses Ergebnis wird zurückgegeben, wenn eine dieser Voraussetzungen nicht erfüllt ist,
|
||||
* z. B.:
|
||||
* <ul>
|
||||
* <li>der Status ist nicht {@code SUCCESS} (z. B. {@code FAILED_FINAL}), oder</li>
|
||||
* <li>{@code lastTargetFileName} oder {@code lastTargetPath} ist {@code null}.</li>
|
||||
* </ul>
|
||||
*
|
||||
* @param reason menschenlesbare Begründung für den ungültigen Zustand; nie null
|
||||
*/
|
||||
public record ManualFileRenameInvalidState(String reason) implements ManualFileRenameResult {
|
||||
|
||||
/**
|
||||
* Kompakter Konstruktor zur Validierung des Pflichtfelds.
|
||||
*
|
||||
* @throws NullPointerException wenn {@code reason} null ist
|
||||
*/
|
||||
public ManualFileRenameInvalidState {
|
||||
Objects.requireNonNull(reason, "reason must not be null");
|
||||
}
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
package de.gecheckt.pdf.umbenenner.application.port.in;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Ergebnis, wenn keine Umbenennung notwendig ist, weil die Zieldatei mit dem
|
||||
* gewünschten Namen bereits vorhanden ist und denselben Inhalt hat (gleicher Fingerprint).
|
||||
* <p>
|
||||
* Dieses Ergebnis tritt auf, wenn:
|
||||
* <ul>
|
||||
* <li>der gewünschte neue Dateiname bereits dem aktuellen {@code lastTargetFileName}
|
||||
* entspricht, oder</li>
|
||||
* <li>{@link de.gecheckt.pdf.umbenenner.application.port.out.TargetFolderPort#resolveUniqueFilename}
|
||||
* einen {@link de.gecheckt.pdf.umbenenner.application.port.out.ExistingIdenticalTargetFile}
|
||||
* zurückliefert (identischer Fingerprint im Zielordner).</li>
|
||||
* </ul>
|
||||
* Weder Dateisystem noch Persistenz werden in diesem Fall verändert.
|
||||
*
|
||||
* @param existingFileName der Dateiname (ohne Pfad) der bereits vorhandenen identischen
|
||||
* Datei; nie null
|
||||
*/
|
||||
public record ManualFileRenameNoOpIdenticalTarget(String existingFileName) implements ManualFileRenameResult {
|
||||
|
||||
/**
|
||||
* Kompakter Konstruktor zur Validierung des Pflichtfelds.
|
||||
*
|
||||
* @throws NullPointerException wenn {@code existingFileName} null ist
|
||||
*/
|
||||
public ManualFileRenameNoOpIdenticalTarget {
|
||||
Objects.requireNonNull(existingFileName, "existingFileName must not be null");
|
||||
}
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
package de.gecheckt.pdf.umbenenner.application.port.in;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Ergebnis, wenn die Persistenzaktualisierung nach erfolgreicher Dateisystem-Umbenennung
|
||||
* fehlgeschlagen ist.
|
||||
* <p>
|
||||
* Gibt an, dass die Zieldatei im Dateisystem erfolgreich umbenannt werden konnte, jedoch
|
||||
* die anschließende Aktualisierung des Dokument-Stammsatzes in der Persistenz fehlgeschlagen
|
||||
* ist. Der Use-Case versucht in diesem Fall, die Dateisystem-Umbenennung rückgängig zu
|
||||
* machen (Best-Effort-Rollback).
|
||||
* <p>
|
||||
* Schlägt auch der Rollback fehl, wird dies auf ERROR-Ebene protokolliert. In jedem Fall
|
||||
* bleibt dieses Ergebnis die Rückgabe, sodass der Aufrufer den Benutzer informieren kann.
|
||||
*
|
||||
* @param message menschenlesbare Beschreibung des Persistenzfehlers; nie null
|
||||
*/
|
||||
public record ManualFileRenamePersistenceFailure(String message) implements ManualFileRenameResult {
|
||||
|
||||
/**
|
||||
* Kompakter Konstruktor zur Validierung des Pflichtfelds.
|
||||
*
|
||||
* @throws NullPointerException wenn {@code message} null ist
|
||||
*/
|
||||
public ManualFileRenamePersistenceFailure {
|
||||
Objects.requireNonNull(message, "message must not be null");
|
||||
}
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
package de.gecheckt.pdf.umbenenner.application.port.in;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import de.gecheckt.pdf.umbenenner.domain.model.DocumentFingerprint;
|
||||
|
||||
/**
|
||||
* Anfrage an den {@link ManualFileRenameUseCase} zum manuellen Umbenennen einer Zieldatei.
|
||||
* <p>
|
||||
* Der Benutzer gibt im GUI ausschließlich den Basistitel ohne {@code .pdf}-Endung an.
|
||||
* Der Use-Case hängt die Erweiterung selbst an.
|
||||
*
|
||||
* @param fingerprint Inhalts-Fingerabdruck des Dokuments, das umbenannt werden soll;
|
||||
* nie null
|
||||
* @param desiredBaseFileName gewünschter Basisdateiname ohne {@code .pdf}-Endung;
|
||||
* nie null; darf nicht leer oder nur aus Leerzeichen bestehen
|
||||
*/
|
||||
public record ManualFileRenameRequest(
|
||||
DocumentFingerprint fingerprint,
|
||||
String desiredBaseFileName) {
|
||||
|
||||
/**
|
||||
* Kompakter Konstruktor zur Validierung der Pflichtfelder.
|
||||
*
|
||||
* @throws NullPointerException wenn {@code fingerprint} oder
|
||||
* {@code desiredBaseFileName} null sind
|
||||
* @throws IllegalArgumentException wenn {@code desiredBaseFileName} leer ist
|
||||
*/
|
||||
public ManualFileRenameRequest {
|
||||
Objects.requireNonNull(fingerprint, "fingerprint must not be null");
|
||||
Objects.requireNonNull(desiredBaseFileName, "desiredBaseFileName must not be null");
|
||||
if (desiredBaseFileName.isBlank()) {
|
||||
throw new IllegalArgumentException("desiredBaseFileName must not be blank");
|
||||
}
|
||||
}
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
package de.gecheckt.pdf.umbenenner.application.port.in;
|
||||
|
||||
/**
|
||||
* Versiegeltes Ergebnis-Interface für eine manuelle Dateiumbenennung via
|
||||
* {@link ManualFileRenameUseCase}.
|
||||
* <p>
|
||||
* Mögliche Ergebnisse:
|
||||
* <ul>
|
||||
* <li>{@link ManualFileRenameSuccess} – Umbenennung war erfolgreich (ggf. mit Suffix).</li>
|
||||
* <li>{@link ManualFileRenameNoOpIdenticalTarget} – keine Aktion erforderlich, da die
|
||||
* Zieldatei bereits denselben Inhalt hat.</li>
|
||||
* <li>{@link ManualFileRenameDocumentNotFound} – das Dokument wurde in der Persistenz
|
||||
* nicht gefunden.</li>
|
||||
* <li>{@link ManualFileRenameInvalidState} – das Dokument befindet sich in einem
|
||||
* ungültigen Zustand für eine Umbenennung.</li>
|
||||
* <li>{@link ManualFileRenameSourceFileMissing} – die bisherige Zieldatei existiert
|
||||
* im Zielordner nicht mehr.</li>
|
||||
* <li>{@link ManualFileRenameFileSystemFailure} – ein technischer Dateisystemfehler
|
||||
* ist aufgetreten.</li>
|
||||
* <li>{@link ManualFileRenamePersistenceFailure} – die Persistenzaktualisierung ist
|
||||
* fehlgeschlagen (Dateisystem ggf. zurückgerollt).</li>
|
||||
* </ul>
|
||||
*/
|
||||
public sealed interface ManualFileRenameResult
|
||||
permits ManualFileRenameSuccess,
|
||||
ManualFileRenameNoOpIdenticalTarget,
|
||||
ManualFileRenameDocumentNotFound,
|
||||
ManualFileRenameInvalidState,
|
||||
ManualFileRenameSourceFileMissing,
|
||||
ManualFileRenameFileSystemFailure,
|
||||
ManualFileRenamePersistenceFailure {
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
package de.gecheckt.pdf.umbenenner.application.port.in;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Ergebnis, wenn die bisherige Zieldatei im Zielordner nicht mehr vorhanden ist.
|
||||
* <p>
|
||||
* Gibt an, dass die in der Persistenz gespeicherte Zieldatei ({@code lastTargetFileName})
|
||||
* zum Zeitpunkt des Umbenennungsversuchs nicht mehr im Zielordner existiert. Dies kann
|
||||
* eintreten, wenn die Datei zwischenzeitlich von einem externen Prozess gelöscht oder
|
||||
* verschoben wurde.
|
||||
* <p>
|
||||
* Gemäß dem Alles-oder-Nichts-Prinzip wird in diesem Fall die Persistenz nicht
|
||||
* aktualisiert.
|
||||
*
|
||||
* @param expectedFileName der Dateiname (ohne Pfad), der im Zielordner erwartet wurde,
|
||||
* aber nicht gefunden wurde; nie null
|
||||
*/
|
||||
public record ManualFileRenameSourceFileMissing(String expectedFileName) implements ManualFileRenameResult {
|
||||
|
||||
/**
|
||||
* Kompakter Konstruktor zur Validierung des Pflichtfelds.
|
||||
*
|
||||
* @throws NullPointerException wenn {@code expectedFileName} null ist
|
||||
*/
|
||||
public ManualFileRenameSourceFileMissing {
|
||||
Objects.requireNonNull(expectedFileName, "expectedFileName must not be null");
|
||||
}
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
package de.gecheckt.pdf.umbenenner.application.port.in;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Ergebnis einer erfolgreich abgeschlossenen manuellen Dateiumbenennung.
|
||||
* <p>
|
||||
* Gibt an, dass die Zieldatei im Dateisystem erfolgreich umbenannt und der
|
||||
* Dokument-Stammsatz in der Persistenz aktualisiert wurde.
|
||||
*
|
||||
* @param previousFileName der Dateiname (ohne Pfad) vor der Umbenennung; nie null
|
||||
* @param appliedFileName der tatsächlich angewendete Dateiname (ohne Pfad) nach der
|
||||
* Umbenennung; kann bei Konflikten ein Suffix wie {@code (1)}
|
||||
* enthalten; nie null
|
||||
* @param conflictSuffixApplied {@code true} wenn dem gewünschten Basisdateinamen ein
|
||||
* Konflikt-Suffix angehängt wurde, weil der Wunschname bereits
|
||||
* durch eine andere Datei belegt war
|
||||
*/
|
||||
public record ManualFileRenameSuccess(
|
||||
String previousFileName,
|
||||
String appliedFileName,
|
||||
boolean conflictSuffixApplied) implements ManualFileRenameResult {
|
||||
|
||||
/**
|
||||
* Kompakter Konstruktor zur Validierung der Pflichtfelder.
|
||||
*
|
||||
* @throws NullPointerException wenn {@code previousFileName} oder
|
||||
* {@code appliedFileName} null sind
|
||||
*/
|
||||
public ManualFileRenameSuccess {
|
||||
Objects.requireNonNull(previousFileName, "previousFileName must not be null");
|
||||
Objects.requireNonNull(appliedFileName, "appliedFileName must not be null");
|
||||
}
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
package de.gecheckt.pdf.umbenenner.application.port.in;
|
||||
|
||||
/**
|
||||
* Inbound-Port für die manuelle Umbenennung einer bereits erfolgreich verarbeiteten
|
||||
* Zieldatei.
|
||||
* <p>
|
||||
* Ermöglicht dem Benutzer, den von der KI vorgeschlagenen Dateinamen nachträglich
|
||||
* zu korrigieren. Der Use-Case führt die Umbenennung als atomare Operation durch:
|
||||
* Dateisystem und Persistenz werden entweder beide aktualisiert oder beide bleiben
|
||||
* im vorherigen Zustand.
|
||||
* <p>
|
||||
* Eine Umbenennung ist nur für Dokumente mit Status
|
||||
* {@link de.gecheckt.pdf.umbenenner.domain.model.ProcessingStatus#SUCCESS} zulässig,
|
||||
* die einen bekannten letzten Zieldateinamen haben.
|
||||
* <p>
|
||||
* <strong>Konfliktsemantik:</strong> Existiert im Zielordner bereits eine Datei mit dem
|
||||
* gewünschten Namen, wird anhand des Inhalts-Fingerprints entschieden:
|
||||
* <ul>
|
||||
* <li>Gleicher Fingerprint → keine Aktion ({@link ManualFileRenameNoOpIdenticalTarget})</li>
|
||||
* <li>Verschiedener Fingerprint → automatische Suffix-Vergabe ({@code (1)}, {@code (2)}, …)</li>
|
||||
* </ul>
|
||||
*/
|
||||
public interface ManualFileRenameUseCase {
|
||||
|
||||
/**
|
||||
* Benennt die Zieldatei eines erfolgreich verarbeiteten Dokuments manuell um.
|
||||
* <p>
|
||||
* Der Aufruf ist atomar: Entweder werden Dateisystem und Persistenz beide
|
||||
* aktualisiert, oder beide bleiben unverändert. Bei einem Persistenzfehler
|
||||
* nach erfolgreicher Dateisystem-Umbenennung wird die Umbenennung im Dateisystem
|
||||
* im Rahmen eines Best-Effort-Rollbacks rückgängig gemacht.
|
||||
*
|
||||
* @param request die Umbenennungsanfrage mit Fingerprint und gewünschtem Basisdateinamen;
|
||||
* darf nicht null sein
|
||||
* @return das Ergebnis der Umbenennung; nie null
|
||||
* @throws NullPointerException wenn {@code request} null ist
|
||||
*/
|
||||
ManualFileRenameResult rename(ManualFileRenameRequest request);
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
package de.gecheckt.pdf.umbenenner.application.port.out;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Ergebnis einer fehlgeschlagenen Umbenennung, weil die Quelldatei im Zielordner
|
||||
* nicht mehr vorhanden ist.
|
||||
* <p>
|
||||
* Gibt an, dass {@link TargetFileRenamePort#rename(String, String)} die Datei mit dem
|
||||
* angegebenen {@code oldFileName} nicht gefunden hat. Dies kann eintreten, wenn die
|
||||
* Datei zwischenzeitlich von einem anderen Prozess gelöscht oder verschoben wurde.
|
||||
*
|
||||
* @param oldFileName der Dateiname (ohne Pfad), der im Zielordner erwartet wurde, aber
|
||||
* nicht gefunden wurde; nie null
|
||||
*/
|
||||
public record TargetFileRenameFailureFileNotFound(String oldFileName) implements TargetFileRenameResult {
|
||||
|
||||
/**
|
||||
* Kompakter Konstruktor zur Validierung des Pflichtfelds.
|
||||
*
|
||||
* @throws NullPointerException wenn {@code oldFileName} null ist
|
||||
*/
|
||||
public TargetFileRenameFailureFileNotFound {
|
||||
Objects.requireNonNull(oldFileName, "oldFileName must not be null");
|
||||
}
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
package de.gecheckt.pdf.umbenenner.application.port.out;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Ergebnis einer fehlgeschlagenen Umbenennung, weil der gewünschte neue Dateiname im
|
||||
* Zielordner bereits existiert und nicht die gleiche Datei ist.
|
||||
* <p>
|
||||
* Dieser Zustand sollte durch eine vorherige Auflösung via
|
||||
* {@link TargetFolderPort#resolveUniqueFilename(String, de.gecheckt.pdf.umbenenner.domain.model.DocumentFingerprint)}
|
||||
* normalerweise verhindert werden. Das Ergebnis dient der defensiven Fehlerbehandlung
|
||||
* für Race-Conditions oder unvorhergesehene Konkurrenz durch andere Prozesse.
|
||||
*
|
||||
* @param newFileName der Dateiname (ohne Pfad), der bereits im Zielordner existiert;
|
||||
* nie null
|
||||
*/
|
||||
public record TargetFileRenameFailureTargetExists(String newFileName) implements TargetFileRenameResult {
|
||||
|
||||
/**
|
||||
* Kompakter Konstruktor zur Validierung des Pflichtfelds.
|
||||
*
|
||||
* @throws NullPointerException wenn {@code newFileName} null ist
|
||||
*/
|
||||
public TargetFileRenameFailureTargetExists {
|
||||
Objects.requireNonNull(newFileName, "newFileName must not be null");
|
||||
}
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
package de.gecheckt.pdf.umbenenner.application.port.out;
|
||||
|
||||
/**
|
||||
* Outbound-Port für das Umbenennen einer bereits existierenden Datei im Zielordner.
|
||||
* <p>
|
||||
* Dieser Port kapselt die reine Dateisystem-Operation des Umbenennens. Er ist
|
||||
* provider-neutral und kennt ausschließlich opake Dateinamen-Strings – keine
|
||||
* {@code Path}-, {@code File}- oder NIO-Typen. Die Übersetzung in tatsächliche
|
||||
* Dateisystemoperationen obliegt ausschließlich der Adapter-Implementierung.
|
||||
* <p>
|
||||
* <strong>Zuständigkeit:</strong> Dieser Port ist nicht für die Suffix-Logik bei
|
||||
* Namenskollisionen zuständig. Die Auflösung eines eindeutigen Zieldateinamens
|
||||
* (inkl. Suffix-Vergabe) erfolgt über
|
||||
* {@link TargetFolderPort#resolveUniqueFilename(String, de.gecheckt.pdf.umbenenner.domain.model.DocumentFingerprint)}.
|
||||
* <p>
|
||||
* <strong>Architekturgrenze:</strong> Keine {@code Path}-, {@code File}-, NIO- oder
|
||||
* JDBC-Typen erscheinen in diesem Interface oder in Typen, die es referenziert.
|
||||
*/
|
||||
public interface TargetFileRenamePort {
|
||||
|
||||
/**
|
||||
* Benennt eine existierende Datei im Zielordner von {@code oldFileName} zu
|
||||
* {@code newFileName} um.
|
||||
* <p>
|
||||
* Die Methode erwartet, dass {@code oldFileName} im Zielordner vorhanden ist.
|
||||
* Ist {@code newFileName} bereits vorhanden und nicht identisch mit {@code oldFileName},
|
||||
* wird {@link TargetFileRenameFailureTargetExists} zurückgegeben. Die eigentliche
|
||||
* Konfliktvermeidung (Suffix-Vergabe) liegt im Verantwortungsbereich des Aufrufers.
|
||||
*
|
||||
* @param oldFileName der aktuell im Zielordner vorhandene Dateiname (ohne Pfad);
|
||||
* darf nicht null oder leer sein
|
||||
* @param newFileName der gewünschte neue Dateiname (ohne Pfad);
|
||||
* darf nicht null oder leer sein
|
||||
* @return {@link TargetFileRenameSuccess} bei Erfolg,
|
||||
* {@link TargetFileRenameFailureFileNotFound} wenn {@code oldFileName} nicht existiert,
|
||||
* {@link TargetFileRenameFailureTargetExists} wenn {@code newFileName} bereits durch
|
||||
* eine andere Datei belegt ist,
|
||||
* {@link TargetFileRenameTechnicalFailure} bei einem sonstigen technischen Fehler
|
||||
*/
|
||||
TargetFileRenameResult rename(String oldFileName, String newFileName);
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
package de.gecheckt.pdf.umbenenner.application.port.out;
|
||||
|
||||
/**
|
||||
* Versiegeltes Ergebnis-Interface für eine Umbenennung einer Zieldatei via
|
||||
* {@link TargetFileRenamePort}.
|
||||
* <p>
|
||||
* Mögliche Ergebnisse:
|
||||
* <ul>
|
||||
* <li>{@link TargetFileRenameSuccess} – die Umbenennung war erfolgreich.</li>
|
||||
* <li>{@link TargetFileRenameFailureFileNotFound} – die ursprüngliche Datei wurde
|
||||
* im Zielordner nicht gefunden.</li>
|
||||
* <li>{@link TargetFileRenameFailureTargetExists} – der gewünschte neue Dateiname
|
||||
* existiert bereits und gehört zu einer anderen Datei.</li>
|
||||
* <li>{@link TargetFileRenameTechnicalFailure} – ein technischer Fehler beim
|
||||
* Dateisystemzugriff ist aufgetreten.</li>
|
||||
* </ul>
|
||||
*/
|
||||
public sealed interface TargetFileRenameResult
|
||||
permits TargetFileRenameSuccess,
|
||||
TargetFileRenameFailureFileNotFound,
|
||||
TargetFileRenameFailureTargetExists,
|
||||
TargetFileRenameTechnicalFailure {
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
package de.gecheckt.pdf.umbenenner.application.port.out;
|
||||
|
||||
/**
|
||||
* Ergebnis einer erfolgreichen Umbenennung einer Zieldatei via {@link TargetFileRenamePort}.
|
||||
* <p>
|
||||
* Gibt an, dass die Datei im Zielordner erfolgreich von ihrem alten auf den neuen
|
||||
* Dateinamen umbenannt wurde.
|
||||
*/
|
||||
public record TargetFileRenameSuccess() implements TargetFileRenameResult {
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
package de.gecheckt.pdf.umbenenner.application.port.out;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Ergebnis einer technisch fehlgeschlagenen Umbenennung einer Zieldatei.
|
||||
* <p>
|
||||
* Gibt an, dass beim Umbenennen ein nicht klassifizierbarer technischer Fehler
|
||||
* aufgetreten ist, z. B. fehlende Schreibrechte, gesperrte Datei durch einen anderen
|
||||
* Prozess oder ein nicht erreichbares Netzlaufwerk.
|
||||
*
|
||||
* @param message menschenlesbare Beschreibung des aufgetretenen Fehlers; nie null
|
||||
*/
|
||||
public record TargetFileRenameTechnicalFailure(String message) implements TargetFileRenameResult {
|
||||
|
||||
/**
|
||||
* Kompakter Konstruktor zur Validierung des Pflichtfelds.
|
||||
*
|
||||
* @throws NullPointerException wenn {@code message} null ist
|
||||
*/
|
||||
public TargetFileRenameTechnicalFailure {
|
||||
Objects.requireNonNull(message, "message must not be null");
|
||||
}
|
||||
}
|
||||
+237
@@ -0,0 +1,237 @@
|
||||
package de.gecheckt.pdf.umbenenner.application.usecase;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import de.gecheckt.pdf.umbenenner.application.port.in.ManualFileRenameDocumentNotFound;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.in.ManualFileRenameFileSystemFailure;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.in.ManualFileRenameInvalidState;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.in.ManualFileRenameNoOpIdenticalTarget;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.in.ManualFileRenamePersistenceFailure;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.in.ManualFileRenameRequest;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.in.ManualFileRenameResult;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.in.ManualFileRenameSourceFileMissing;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.in.ManualFileRenameSuccess;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.in.ManualFileRenameUseCase;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.out.ClockPort;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.out.DocumentRecord;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.out.DocumentRecordRepository;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.out.DocumentTerminalFinalFailure;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.out.DocumentTerminalSuccess;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.out.ExistingIdenticalTargetFile;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.out.ProcessingLogger;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.out.ResolvedTargetFilename;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.out.TargetFileRenameFailureFileNotFound;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.out.TargetFileRenameFailureTargetExists;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.out.TargetFileRenamePort;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.out.TargetFileRenameSuccess;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.out.TargetFileRenameTechnicalFailure;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.out.TargetFolderPort;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.out.TargetFolderTechnicalFailure;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.out.UnitOfWorkPort;
|
||||
import de.gecheckt.pdf.umbenenner.domain.model.DocumentFingerprint;
|
||||
|
||||
/**
|
||||
* Standardimplementierung von {@link ManualFileRenameUseCase}.
|
||||
* <p>
|
||||
* Führt die manuelle Umbenennung einer Zieldatei als atomare Operation durch:
|
||||
* Entweder werden Dateisystem und Persistenz beide aktualisiert, oder beide
|
||||
* bleiben im vorherigen Zustand.
|
||||
* <p>
|
||||
* <strong>Ablauf:</strong>
|
||||
* <ol>
|
||||
* <li>Dokument-Stammsatz aus dem Repository laden und Zustand prüfen.</li>
|
||||
* <li>Prüfen, ob der gewünschte Name bereits dem aktuellen entspricht (No-Op).</li>
|
||||
* <li>Eindeutigen Zieldateinamen über {@link TargetFolderPort} auflösen.</li>
|
||||
* <li>Zieldatei im Dateisystem umbenennen via {@link TargetFileRenamePort}.</li>
|
||||
* <li>Dokument-Stammsatz in der Persistenz aktualisieren.</li>
|
||||
* <li>Bei Persistenzfehler: Best-Effort-Rollback der Dateisystem-Umbenennung.</li>
|
||||
* </ol>
|
||||
* <p>
|
||||
* Eine Umbenennung ist ausschließlich für Dokumente mit Status
|
||||
* {@link de.gecheckt.pdf.umbenenner.domain.model.ProcessingStatus#SUCCESS} zulässig,
|
||||
* die einen bekannten letzten Zieldateinamen und Zielpfad haben.
|
||||
*/
|
||||
public class DefaultManualFileRenameUseCase implements ManualFileRenameUseCase {
|
||||
|
||||
private final DocumentRecordRepository repository;
|
||||
private final TargetFolderPort targetFolderPort;
|
||||
private final TargetFileRenamePort targetFileRenamePort;
|
||||
private final UnitOfWorkPort unitOfWorkPort;
|
||||
private final ClockPort clock;
|
||||
private final ProcessingLogger logger;
|
||||
|
||||
/**
|
||||
* Erstellt den Use-Case mit allen erforderlichen Ports.
|
||||
*
|
||||
* @param repository Repository zum Lesen und Schreiben des Dokument-Stammsatzes;
|
||||
* darf nicht null sein
|
||||
* @param targetFolderPort Port zur Auflösung eindeutiger Zieldateinamen;
|
||||
* darf nicht null sein
|
||||
* @param targetFileRenamePort Port zum physischen Umbenennen einer Zieldatei;
|
||||
* darf nicht null sein
|
||||
* @param unitOfWorkPort Port zur atomaren Persistenzaktualisierung;
|
||||
* darf nicht null sein
|
||||
* @param clock Port zur Abfrage des aktuellen Zeitstempels;
|
||||
* darf nicht null sein
|
||||
* @param logger für die Protokollierung von Betriebsereignissen;
|
||||
* darf nicht null sein
|
||||
* @throws NullPointerException wenn einer der Parameter null ist
|
||||
*/
|
||||
public DefaultManualFileRenameUseCase(
|
||||
DocumentRecordRepository repository,
|
||||
TargetFolderPort targetFolderPort,
|
||||
TargetFileRenamePort targetFileRenamePort,
|
||||
UnitOfWorkPort unitOfWorkPort,
|
||||
ClockPort clock,
|
||||
ProcessingLogger logger) {
|
||||
this.repository = Objects.requireNonNull(repository, "repository must not be null");
|
||||
this.targetFolderPort = Objects.requireNonNull(targetFolderPort, "targetFolderPort must not be null");
|
||||
this.targetFileRenamePort = Objects.requireNonNull(targetFileRenamePort, "targetFileRenamePort must not be null");
|
||||
this.unitOfWorkPort = Objects.requireNonNull(unitOfWorkPort, "unitOfWorkPort must not be null");
|
||||
this.clock = Objects.requireNonNull(clock, "clock must not be null");
|
||||
this.logger = Objects.requireNonNull(logger, "logger must not be null");
|
||||
}
|
||||
|
||||
/**
|
||||
* Benennt die Zieldatei eines erfolgreich verarbeiteten Dokuments manuell um.
|
||||
* <p>
|
||||
* Der Aufruf ist atomar: Entweder werden Dateisystem und Persistenz beide
|
||||
* aktualisiert, oder beide bleiben unverändert. Bei einem Persistenzfehler nach
|
||||
* erfolgreicher Dateisystem-Umbenennung wird die Umbenennung im Dateisystem im
|
||||
* Rahmen eines Best-Effort-Rollbacks rückgängig gemacht.
|
||||
*
|
||||
* @param request die Umbenennungsanfrage mit Fingerprint und gewünschtem Basisdateinamen;
|
||||
* darf nicht null sein
|
||||
* @return das Ergebnis der Umbenennung; nie null
|
||||
* @throws NullPointerException wenn {@code request} null ist
|
||||
*/
|
||||
@Override
|
||||
public ManualFileRenameResult rename(ManualFileRenameRequest request) {
|
||||
Objects.requireNonNull(request, "request must not be null");
|
||||
|
||||
DocumentFingerprint fingerprint = request.fingerprint();
|
||||
String desiredFullName = request.desiredBaseFileName() + ".pdf";
|
||||
|
||||
logger.info("Manuelle Umbenennung angefordert: Fingerprint={}, Zielname={}",
|
||||
fingerprint.sha256Hex(), desiredFullName);
|
||||
|
||||
// Schritt 1: Dokument-Stammsatz laden und Zustand prüfen
|
||||
var lookupResult = repository.findByFingerprint(fingerprint);
|
||||
|
||||
if (lookupResult instanceof DocumentTerminalFinalFailure) {
|
||||
logger.warn("Manuelle Umbenennung verweigert: Dokument hat terminalen Fehlerstatus. Fingerprint={}",
|
||||
fingerprint.sha256Hex());
|
||||
return new ManualFileRenameInvalidState(
|
||||
"Dokument ist final fehlgeschlagen und kann nicht umbenannt werden.");
|
||||
}
|
||||
|
||||
if (!(lookupResult instanceof DocumentTerminalSuccess terminalSuccess)) {
|
||||
logger.warn("Manuelle Umbenennung verweigert: Dokument nicht gefunden oder nicht im Erfolgsstatus. Fingerprint={}",
|
||||
fingerprint.sha256Hex());
|
||||
return new ManualFileRenameDocumentNotFound(
|
||||
"Kein erfolgreich verarbeitetes Dokument mit dem angegebenen Fingerprint gefunden.");
|
||||
}
|
||||
|
||||
DocumentRecord record = terminalSuccess.record();
|
||||
|
||||
if (record.lastTargetFileName() == null || record.lastTargetPath() == null) {
|
||||
logger.warn("Manuelle Umbenennung verweigert: Kein Zieldateiname im Stammsatz vorhanden. Fingerprint={}",
|
||||
fingerprint.sha256Hex());
|
||||
return new ManualFileRenameInvalidState(
|
||||
"Dokument hat keinen gespeicherten Zieldateinamen und kann nicht umbenannt werden.");
|
||||
}
|
||||
|
||||
String currentFileName = record.lastTargetFileName();
|
||||
|
||||
// Schritt 2: Prüfen, ob der gewünschte Name bereits dem aktuellen entspricht
|
||||
if (desiredFullName.equals(currentFileName)) {
|
||||
logger.info("Manuelle Umbenennung: Kein Handlungsbedarf, Name ist bereits identisch. Fingerprint={}",
|
||||
fingerprint.sha256Hex());
|
||||
return new ManualFileRenameNoOpIdenticalTarget(currentFileName);
|
||||
}
|
||||
|
||||
// Schritt 3: Eindeutigen Zieldateinamen über TargetFolderPort auflösen
|
||||
var resolutionResult = targetFolderPort.resolveUniqueFilename(desiredFullName, fingerprint);
|
||||
|
||||
if (resolutionResult instanceof ExistingIdenticalTargetFile identical) {
|
||||
logger.info("Manuelle Umbenennung: Identische Datei bereits im Zielordner vorhanden. Fingerprint={}",
|
||||
fingerprint.sha256Hex());
|
||||
return new ManualFileRenameNoOpIdenticalTarget(identical.existingFilename());
|
||||
}
|
||||
|
||||
if (resolutionResult instanceof TargetFolderTechnicalFailure folderFailure) {
|
||||
logger.warn("Manuelle Umbenennung fehlgeschlagen: Technischer Fehler beim Zielordner-Zugriff. Fingerprint={}, Ursache={}",
|
||||
fingerprint.sha256Hex(), folderFailure.errorMessage());
|
||||
return new ManualFileRenameFileSystemFailure(
|
||||
"Zielordner nicht zugänglich: " + folderFailure.errorMessage());
|
||||
}
|
||||
|
||||
// resolutionResult ist jetzt ResolvedTargetFilename
|
||||
String appliedFileName = ((ResolvedTargetFilename) resolutionResult).resolvedFilename();
|
||||
|
||||
// Schritt 4: Zieldatei im Dateisystem umbenennen
|
||||
var renameResult = targetFileRenamePort.rename(currentFileName, appliedFileName);
|
||||
|
||||
if (renameResult instanceof TargetFileRenameFailureFileNotFound notFound) {
|
||||
logger.warn("Manuelle Umbenennung fehlgeschlagen: Bisherige Zieldatei nicht gefunden. Fingerprint={}, Datei={}",
|
||||
fingerprint.sha256Hex(), notFound.oldFileName());
|
||||
return new ManualFileRenameSourceFileMissing(notFound.oldFileName());
|
||||
}
|
||||
|
||||
if (renameResult instanceof TargetFileRenameFailureTargetExists targetExists) {
|
||||
logger.warn("Manuelle Umbenennung fehlgeschlagen: Zieldatei bereits vorhanden (defensiv). Fingerprint={}, Datei={}",
|
||||
fingerprint.sha256Hex(), targetExists.newFileName());
|
||||
return new ManualFileRenameFileSystemFailure(
|
||||
"Zieldatei bereits vorhanden: " + targetExists.newFileName());
|
||||
}
|
||||
|
||||
if (renameResult instanceof TargetFileRenameTechnicalFailure technical) {
|
||||
logger.warn("Manuelle Umbenennung fehlgeschlagen: Technischer Dateisystemfehler. Fingerprint={}, Ursache={}",
|
||||
fingerprint.sha256Hex(), technical.message());
|
||||
return new ManualFileRenameFileSystemFailure(technical.message());
|
||||
}
|
||||
|
||||
// Schritt 5: Persistenz aktualisieren (renameResult ist jetzt TargetFileRenameSuccess)
|
||||
DocumentRecord updatedRecord = new DocumentRecord(
|
||||
record.fingerprint(),
|
||||
record.lastKnownSourceLocator(),
|
||||
record.lastKnownSourceFileName(),
|
||||
record.overallStatus(),
|
||||
record.failureCounters(),
|
||||
record.lastFailureInstant(),
|
||||
record.lastSuccessInstant(),
|
||||
record.createdAt(),
|
||||
clock.now(),
|
||||
record.lastTargetPath(),
|
||||
appliedFileName);
|
||||
|
||||
try {
|
||||
unitOfWorkPort.executeInTransaction(tx -> tx.updateDocumentRecord(updatedRecord));
|
||||
} catch (RuntimeException persistenceException) {
|
||||
// Best-Effort-Rollback: Dateisystem-Umbenennung rückgängig machen
|
||||
String errorMessage = persistenceException.getMessage() != null
|
||||
? persistenceException.getMessage()
|
||||
: persistenceException.getClass().getSimpleName();
|
||||
|
||||
logger.warn("Manuelle Umbenennung: Persistenzfehler nach erfolgreicher Dateisystem-Umbenennung. " +
|
||||
"Versuche Rollback. Fingerprint={}, Ursache={}", fingerprint.sha256Hex(), errorMessage);
|
||||
|
||||
var rollbackResult = targetFileRenamePort.rename(appliedFileName, currentFileName);
|
||||
if (!(rollbackResult instanceof TargetFileRenameSuccess)) {
|
||||
logger.error("Rollback der Dateisystem-Umbenennung fehlgeschlagen: {} → {}. " +
|
||||
"Dateisystem und Persistenz sind möglicherweise inkonsistent. Fingerprint={}",
|
||||
appliedFileName, currentFileName, fingerprint.sha256Hex());
|
||||
}
|
||||
|
||||
return new ManualFileRenamePersistenceFailure(
|
||||
"Persistenzfehler nach Umbenennung: " + errorMessage);
|
||||
}
|
||||
|
||||
boolean conflictSuffixApplied = !appliedFileName.equals(desiredFullName);
|
||||
|
||||
logger.info("Manuelle Umbenennung erfolgreich: {} → {} (Suffix angewendet: {})",
|
||||
currentFileName, appliedFileName, conflictSuffixApplied);
|
||||
|
||||
return new ManualFileRenameSuccess(currentFileName, appliedFileName, conflictSuffixApplied);
|
||||
}
|
||||
}
|
||||
+640
@@ -0,0 +1,640 @@
|
||||
package de.gecheckt.pdf.umbenenner.application.usecase;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatNullPointerException;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import de.gecheckt.pdf.umbenenner.application.port.in.ManualFileRenameDocumentNotFound;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.in.ManualFileRenameFileSystemFailure;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.in.ManualFileRenameInvalidState;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.in.ManualFileRenameNoOpIdenticalTarget;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.in.ManualFileRenamePersistenceFailure;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.in.ManualFileRenameRequest;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.in.ManualFileRenameResult;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.in.ManualFileRenameSourceFileMissing;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.in.ManualFileRenameSuccess;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.out.ClockPort;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.out.DocumentKnownProcessable;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.out.DocumentPersistenceException;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.out.DocumentRecord;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.out.DocumentRecordLookupResult;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.out.DocumentRecordRepository;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.out.DocumentTerminalFinalFailure;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.out.DocumentTerminalSuccess;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.out.DocumentUnknown;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.out.ExistingIdenticalTargetFile;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.out.FailureCounters;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.out.ProcessingAttempt;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.out.ProcessingLogger;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.out.ResolvedTargetFilename;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.out.TargetFileRenameFailureFileNotFound;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.out.TargetFileRenameFailureTargetExists;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.out.TargetFileRenamePort;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.out.TargetFileRenameResult;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.out.TargetFileRenameSuccess;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.out.TargetFileRenameTechnicalFailure;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.out.TargetFilenameResolutionResult;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.out.TargetFolderPort;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.out.TargetFolderTechnicalFailure;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.out.UnitOfWorkPort;
|
||||
import de.gecheckt.pdf.umbenenner.domain.model.DocumentFingerprint;
|
||||
import de.gecheckt.pdf.umbenenner.domain.model.ProcessingStatus;
|
||||
import de.gecheckt.pdf.umbenenner.domain.model.SourceDocumentLocator;
|
||||
|
||||
/**
|
||||
* Tests für {@link DefaultManualFileRenameUseCase}.
|
||||
* <p>
|
||||
* Alle Mocks sind handgeschrieben (kein Mockito). Jeder Test prüft ausschließlich
|
||||
* das zurückgegebene Ergebnis sowie die an die Mock-Ports weitergegebenen Parameter.
|
||||
* Protokollaufrufe werden nicht verifiziert.
|
||||
*/
|
||||
class DefaultManualFileRenameUseCaseTest {
|
||||
|
||||
private static final DocumentFingerprint FINGERPRINT =
|
||||
new DocumentFingerprint("a".repeat(64));
|
||||
|
||||
private static final String CURRENT_FILE = "2024-01-01 - Rechnung.pdf";
|
||||
private static final String DESIRED_BASE = "2024-01-01 - Korrigierte Rechnung";
|
||||
private static final String DESIRED_FULL = DESIRED_BASE + ".pdf";
|
||||
|
||||
private static final Instant FIXED_NOW = Instant.parse("2024-06-01T10:00:00Z");
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Hilfsmethoden zum Erstellen von Testdaten
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private static DocumentRecord successRecord(String lastTargetFileName) {
|
||||
return new DocumentRecord(
|
||||
FINGERPRINT,
|
||||
new SourceDocumentLocator("/quelldatei.pdf"),
|
||||
"quelldatei.pdf",
|
||||
ProcessingStatus.SUCCESS,
|
||||
FailureCounters.zero(),
|
||||
null,
|
||||
FIXED_NOW.minusSeconds(60),
|
||||
FIXED_NOW.minusSeconds(120),
|
||||
FIXED_NOW.minusSeconds(60),
|
||||
"/zielordner",
|
||||
lastTargetFileName);
|
||||
}
|
||||
|
||||
private static DocumentRecord successRecordWithoutTargetFile() {
|
||||
return new DocumentRecord(
|
||||
FINGERPRINT,
|
||||
new SourceDocumentLocator("/quelldatei.pdf"),
|
||||
"quelldatei.pdf",
|
||||
ProcessingStatus.SUCCESS,
|
||||
FailureCounters.zero(),
|
||||
null,
|
||||
FIXED_NOW.minusSeconds(60),
|
||||
FIXED_NOW.minusSeconds(120),
|
||||
FIXED_NOW.minusSeconds(60),
|
||||
null,
|
||||
null);
|
||||
}
|
||||
|
||||
private static DocumentRecord failedRecord() {
|
||||
return new DocumentRecord(
|
||||
FINGERPRINT,
|
||||
new SourceDocumentLocator("/quelldatei.pdf"),
|
||||
"quelldatei.pdf",
|
||||
ProcessingStatus.FAILED_FINAL,
|
||||
FailureCounters.zero(),
|
||||
FIXED_NOW.minusSeconds(60),
|
||||
null,
|
||||
FIXED_NOW.minusSeconds(120),
|
||||
FIXED_NOW.minusSeconds(60),
|
||||
null,
|
||||
null);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Hilfsmethoden zum Erstellen von Stubs
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private static ProcessingLogger noOpLogger() {
|
||||
return new ProcessingLogger() {
|
||||
@Override public void info(String msg, Object... args) { }
|
||||
@Override public void debug(String msg, Object... args) { }
|
||||
@Override public void debugSensitiveAiContent(String msg, Object... args) { }
|
||||
@Override public void warn(String msg, Object... args) { }
|
||||
@Override public void error(String msg, Object... args) { }
|
||||
};
|
||||
}
|
||||
|
||||
private static ClockPort fixedClock() {
|
||||
return () -> FIXED_NOW;
|
||||
}
|
||||
|
||||
private static DocumentRecordRepository repositoryReturning(DocumentRecordLookupResult result) {
|
||||
return new DocumentRecordRepository() {
|
||||
@Override public DocumentRecordLookupResult findByFingerprint(DocumentFingerprint fp) { return result; }
|
||||
@Override public void create(DocumentRecord r) { }
|
||||
@Override public void update(DocumentRecord r) { }
|
||||
@Override public void deleteByFingerprint(DocumentFingerprint fp) { }
|
||||
};
|
||||
}
|
||||
|
||||
private static TargetFolderPort folderPortReturning(TargetFilenameResolutionResult result) {
|
||||
return new TargetFolderPort() {
|
||||
@Override public String getTargetFolderLocator() { return "/zielordner"; }
|
||||
@Override public TargetFilenameResolutionResult resolveUniqueFilename(String baseName, DocumentFingerprint fp) { return result; }
|
||||
@Override public void tryDeleteTargetFile(String name) { }
|
||||
};
|
||||
}
|
||||
|
||||
private static TargetFileRenamePort renamePortReturning(TargetFileRenameResult result) {
|
||||
return (oldName, newName) -> result;
|
||||
}
|
||||
|
||||
private static UnitOfWorkPort alwaysSucceedingUnitOfWork() {
|
||||
return ops -> ops.accept(new NoOpTransactionOperations());
|
||||
}
|
||||
|
||||
private static UnitOfWorkPort throwingUnitOfWork(RuntimeException ex) {
|
||||
return ops -> { throw ex; };
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Testfall 1: Erfolgreicher Pfad ohne Konflikt
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@Test
|
||||
void rename_delegatesToAllPortsAndReturnsSuccess_whenNoConflict() {
|
||||
List<String[]> renameArgs = new ArrayList<>();
|
||||
List<DocumentRecord> updatedRecords = new ArrayList<>();
|
||||
|
||||
TargetFileRenamePort renamePort = (oldName, newName) -> {
|
||||
renameArgs.add(new String[]{oldName, newName});
|
||||
return new TargetFileRenameSuccess();
|
||||
};
|
||||
|
||||
UnitOfWorkPort uow = ops -> ops.accept(new RecordCapturingTransactionOperations(updatedRecords));
|
||||
|
||||
DefaultManualFileRenameUseCase useCase = new DefaultManualFileRenameUseCase(
|
||||
repositoryReturning(new DocumentTerminalSuccess(successRecord(CURRENT_FILE))),
|
||||
folderPortReturning(new ResolvedTargetFilename(DESIRED_FULL)),
|
||||
renamePort,
|
||||
uow,
|
||||
fixedClock(),
|
||||
noOpLogger());
|
||||
|
||||
ManualFileRenameResult result = useCase.rename(new ManualFileRenameRequest(FINGERPRINT, DESIRED_BASE));
|
||||
|
||||
assertThat(result).isInstanceOf(ManualFileRenameSuccess.class);
|
||||
ManualFileRenameSuccess success = (ManualFileRenameSuccess) result;
|
||||
assertThat(success.previousFileName()).isEqualTo(CURRENT_FILE);
|
||||
assertThat(success.appliedFileName()).isEqualTo(DESIRED_FULL);
|
||||
assertThat(success.conflictSuffixApplied()).isFalse();
|
||||
|
||||
assertThat(renameArgs).hasSize(1);
|
||||
assertThat(renameArgs.get(0)[0]).isEqualTo(CURRENT_FILE);
|
||||
assertThat(renameArgs.get(0)[1]).isEqualTo(DESIRED_FULL);
|
||||
|
||||
assertThat(updatedRecords).hasSize(1);
|
||||
assertThat(updatedRecords.get(0).lastTargetFileName()).isEqualTo(DESIRED_FULL);
|
||||
assertThat(updatedRecords.get(0).updatedAt()).isEqualTo(FIXED_NOW);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Testfall 2: Konflikt mit anderer Datei → Suffix angewendet
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@Test
|
||||
void rename_appliesSuffix_whenConflictWithDifferentFingerprint() {
|
||||
String suffixedName = DESIRED_BASE + "(1).pdf";
|
||||
|
||||
List<DocumentRecord> updatedRecords = new ArrayList<>();
|
||||
UnitOfWorkPort uow = ops -> ops.accept(new RecordCapturingTransactionOperations(updatedRecords));
|
||||
|
||||
DefaultManualFileRenameUseCase useCase = new DefaultManualFileRenameUseCase(
|
||||
repositoryReturning(new DocumentTerminalSuccess(successRecord(CURRENT_FILE))),
|
||||
folderPortReturning(new ResolvedTargetFilename(suffixedName)),
|
||||
renamePortReturning(new TargetFileRenameSuccess()),
|
||||
uow,
|
||||
fixedClock(),
|
||||
noOpLogger());
|
||||
|
||||
ManualFileRenameResult result = useCase.rename(new ManualFileRenameRequest(FINGERPRINT, DESIRED_BASE));
|
||||
|
||||
assertThat(result).isInstanceOf(ManualFileRenameSuccess.class);
|
||||
ManualFileRenameSuccess success = (ManualFileRenameSuccess) result;
|
||||
assertThat(success.appliedFileName()).isEqualTo(suffixedName);
|
||||
assertThat(success.conflictSuffixApplied()).isTrue();
|
||||
|
||||
assertThat(updatedRecords).hasSize(1);
|
||||
assertThat(updatedRecords.get(0).lastTargetFileName()).isEqualTo(suffixedName);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Testfall 3: No-Op – gewünschter Name ist identisch mit aktuellem
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@Test
|
||||
void rename_returnsNoOp_whenNewNameEqualsCurrent() {
|
||||
String currentName = DESIRED_FULL; // Gleicher Name wie gewünscht
|
||||
|
||||
DefaultManualFileRenameUseCase useCase = new DefaultManualFileRenameUseCase(
|
||||
repositoryReturning(new DocumentTerminalSuccess(successRecord(currentName))),
|
||||
folderPortReturning(new ResolvedTargetFilename("wird nicht aufgerufen.pdf")),
|
||||
renamePortReturning(new TargetFileRenameSuccess()),
|
||||
alwaysSucceedingUnitOfWork(),
|
||||
fixedClock(),
|
||||
noOpLogger());
|
||||
|
||||
ManualFileRenameResult result = useCase.rename(new ManualFileRenameRequest(FINGERPRINT, DESIRED_BASE));
|
||||
|
||||
assertThat(result).isInstanceOf(ManualFileRenameNoOpIdenticalTarget.class);
|
||||
ManualFileRenameNoOpIdenticalTarget noOp = (ManualFileRenameNoOpIdenticalTarget) result;
|
||||
assertThat(noOp.existingFileName()).isEqualTo(currentName);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Testfall 4: No-Op – TargetFolderPort meldet identischen Inhalt
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@Test
|
||||
void rename_returnsNoOp_whenTargetFolderReportsIdenticalContent() {
|
||||
DefaultManualFileRenameUseCase useCase = new DefaultManualFileRenameUseCase(
|
||||
repositoryReturning(new DocumentTerminalSuccess(successRecord(CURRENT_FILE))),
|
||||
folderPortReturning(new ExistingIdenticalTargetFile(DESIRED_FULL)),
|
||||
renamePortReturning(new TargetFileRenameSuccess()),
|
||||
alwaysSucceedingUnitOfWork(),
|
||||
fixedClock(),
|
||||
noOpLogger());
|
||||
|
||||
ManualFileRenameResult result = useCase.rename(new ManualFileRenameRequest(FINGERPRINT, DESIRED_BASE));
|
||||
|
||||
assertThat(result).isInstanceOf(ManualFileRenameNoOpIdenticalTarget.class);
|
||||
assertThat(((ManualFileRenameNoOpIdenticalTarget) result).existingFileName()).isEqualTo(DESIRED_FULL);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Testfall 5: Dokument nicht gefunden
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@Test
|
||||
void rename_returnsDocumentNotFound_whenRepositoryReturnsDocumentUnknown() {
|
||||
DefaultManualFileRenameUseCase useCase = new DefaultManualFileRenameUseCase(
|
||||
repositoryReturning(new DocumentUnknown()),
|
||||
folderPortReturning(new ResolvedTargetFilename(DESIRED_FULL)),
|
||||
renamePortReturning(new TargetFileRenameSuccess()),
|
||||
alwaysSucceedingUnitOfWork(),
|
||||
fixedClock(),
|
||||
noOpLogger());
|
||||
|
||||
ManualFileRenameResult result = useCase.rename(new ManualFileRenameRequest(FINGERPRINT, DESIRED_BASE));
|
||||
|
||||
assertThat(result).isInstanceOf(ManualFileRenameDocumentNotFound.class);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Testfall 6: Ungültiger Zustand – kein Zieldateiname vorhanden
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@Test
|
||||
void rename_returnsInvalidState_whenDocumentHasNoTargetFilename() {
|
||||
DefaultManualFileRenameUseCase useCase = new DefaultManualFileRenameUseCase(
|
||||
repositoryReturning(new DocumentTerminalSuccess(successRecordWithoutTargetFile())),
|
||||
folderPortReturning(new ResolvedTargetFilename(DESIRED_FULL)),
|
||||
renamePortReturning(new TargetFileRenameSuccess()),
|
||||
alwaysSucceedingUnitOfWork(),
|
||||
fixedClock(),
|
||||
noOpLogger());
|
||||
|
||||
ManualFileRenameResult result = useCase.rename(new ManualFileRenameRequest(FINGERPRINT, DESIRED_BASE));
|
||||
|
||||
assertThat(result).isInstanceOf(ManualFileRenameInvalidState.class);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Testfall 7: Ungültiger Zustand – Status ist nicht SUCCESS
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@Test
|
||||
void rename_returnsInvalidState_whenStatusIsNotSuccess() {
|
||||
DefaultManualFileRenameUseCase useCase = new DefaultManualFileRenameUseCase(
|
||||
repositoryReturning(new DocumentTerminalFinalFailure(failedRecord())),
|
||||
folderPortReturning(new ResolvedTargetFilename(DESIRED_FULL)),
|
||||
renamePortReturning(new TargetFileRenameSuccess()),
|
||||
alwaysSucceedingUnitOfWork(),
|
||||
fixedClock(),
|
||||
noOpLogger());
|
||||
|
||||
ManualFileRenameResult result = useCase.rename(new ManualFileRenameRequest(FINGERPRINT, DESIRED_BASE));
|
||||
|
||||
assertThat(result).isInstanceOf(ManualFileRenameInvalidState.class);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Testfall 8: Bisherige Zieldatei nicht mehr vorhanden
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@Test
|
||||
void rename_returnsSourceFileMissing_whenOldFileGone() {
|
||||
DefaultManualFileRenameUseCase useCase = new DefaultManualFileRenameUseCase(
|
||||
repositoryReturning(new DocumentTerminalSuccess(successRecord(CURRENT_FILE))),
|
||||
folderPortReturning(new ResolvedTargetFilename(DESIRED_FULL)),
|
||||
renamePortReturning(new TargetFileRenameFailureFileNotFound(CURRENT_FILE)),
|
||||
alwaysSucceedingUnitOfWork(),
|
||||
fixedClock(),
|
||||
noOpLogger());
|
||||
|
||||
ManualFileRenameResult result = useCase.rename(new ManualFileRenameRequest(FINGERPRINT, DESIRED_BASE));
|
||||
|
||||
assertThat(result).isInstanceOf(ManualFileRenameSourceFileMissing.class);
|
||||
assertThat(((ManualFileRenameSourceFileMissing) result).expectedFileName()).isEqualTo(CURRENT_FILE);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Testfall 9: Technischer Dateisystemfehler beim Umbenennen
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@Test
|
||||
void rename_returnsFileSystemFailure_whenRenameHasTechnicalError() {
|
||||
DefaultManualFileRenameUseCase useCase = new DefaultManualFileRenameUseCase(
|
||||
repositoryReturning(new DocumentTerminalSuccess(successRecord(CURRENT_FILE))),
|
||||
folderPortReturning(new ResolvedTargetFilename(DESIRED_FULL)),
|
||||
renamePortReturning(new TargetFileRenameTechnicalFailure("Datei gesperrt")),
|
||||
alwaysSucceedingUnitOfWork(),
|
||||
fixedClock(),
|
||||
noOpLogger());
|
||||
|
||||
ManualFileRenameResult result = useCase.rename(new ManualFileRenameRequest(FINGERPRINT, DESIRED_BASE));
|
||||
|
||||
assertThat(result).isInstanceOf(ManualFileRenameFileSystemFailure.class);
|
||||
assertThat(((ManualFileRenameFileSystemFailure) result).message()).contains("Datei gesperrt");
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Testfall 10: Persistenzfehler → Rollback der Umbenennung
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@Test
|
||||
void rename_rollsBackRename_whenPersistenceFails() {
|
||||
List<String[]> renameArgs = new ArrayList<>();
|
||||
|
||||
TargetFileRenamePort renamePort = (oldName, newName) -> {
|
||||
renameArgs.add(new String[]{oldName, newName});
|
||||
return new TargetFileRenameSuccess();
|
||||
};
|
||||
|
||||
DefaultManualFileRenameUseCase useCase = new DefaultManualFileRenameUseCase(
|
||||
repositoryReturning(new DocumentTerminalSuccess(successRecord(CURRENT_FILE))),
|
||||
folderPortReturning(new ResolvedTargetFilename(DESIRED_FULL)),
|
||||
renamePort,
|
||||
throwingUnitOfWork(new DocumentPersistenceException("DB nicht erreichbar")),
|
||||
fixedClock(),
|
||||
noOpLogger());
|
||||
|
||||
ManualFileRenameResult result = useCase.rename(new ManualFileRenameRequest(FINGERPRINT, DESIRED_BASE));
|
||||
|
||||
// Ergebnis ist PersistenceFailure
|
||||
assertThat(result).isInstanceOf(ManualFileRenamePersistenceFailure.class);
|
||||
|
||||
// Erster Aufruf: Eigentliche Umbenennung
|
||||
assertThat(renameArgs.get(0)[0]).isEqualTo(CURRENT_FILE);
|
||||
assertThat(renameArgs.get(0)[1]).isEqualTo(DESIRED_FULL);
|
||||
|
||||
// Zweiter Aufruf: Rollback
|
||||
assertThat(renameArgs).hasSize(2);
|
||||
assertThat(renameArgs.get(1)[0]).isEqualTo(DESIRED_FULL);
|
||||
assertThat(renameArgs.get(1)[1]).isEqualTo(CURRENT_FILE);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Testfall 11: Persistenzfehler + Rollback schlägt fehl → PersistenceFailure
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@Test
|
||||
void rename_logsErrorButStillReturnsPersistenceFailure_whenRollbackRenameAlsoFails() {
|
||||
List<String[]> renameArgs = new ArrayList<>();
|
||||
|
||||
TargetFileRenamePort renamePort = (oldName, newName) -> {
|
||||
renameArgs.add(new String[]{oldName, newName});
|
||||
// Erster Aufruf: Erfolg; Zweiter Aufruf (Rollback): technischer Fehler
|
||||
if (renameArgs.size() == 1) {
|
||||
return new TargetFileRenameSuccess();
|
||||
} else {
|
||||
return new TargetFileRenameTechnicalFailure("Rollback fehlgeschlagen");
|
||||
}
|
||||
};
|
||||
|
||||
DefaultManualFileRenameUseCase useCase = new DefaultManualFileRenameUseCase(
|
||||
repositoryReturning(new DocumentTerminalSuccess(successRecord(CURRENT_FILE))),
|
||||
folderPortReturning(new ResolvedTargetFilename(DESIRED_FULL)),
|
||||
renamePort,
|
||||
throwingUnitOfWork(new DocumentPersistenceException("DB-Fehler")),
|
||||
fixedClock(),
|
||||
noOpLogger());
|
||||
|
||||
ManualFileRenameResult result = useCase.rename(new ManualFileRenameRequest(FINGERPRINT, DESIRED_BASE));
|
||||
|
||||
// Trotz Rollback-Fehler: Ergebnis bleibt PersistenceFailure
|
||||
assertThat(result).isInstanceOf(ManualFileRenamePersistenceFailure.class);
|
||||
// Rollback wurde versucht (2 Aufrufe insgesamt)
|
||||
assertThat(renameArgs).hasSize(2);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Testfall 12: Technischer Fehler beim Zielordner-Zugriff
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@Test
|
||||
void rename_returnsFileSystemFailure_whenTargetFolderTechnicalFailure() {
|
||||
DefaultManualFileRenameUseCase useCase = new DefaultManualFileRenameUseCase(
|
||||
repositoryReturning(new DocumentTerminalSuccess(successRecord(CURRENT_FILE))),
|
||||
folderPortReturning(new TargetFolderTechnicalFailure("Laufwerk nicht erreichbar")),
|
||||
renamePortReturning(new TargetFileRenameSuccess()),
|
||||
alwaysSucceedingUnitOfWork(),
|
||||
fixedClock(),
|
||||
noOpLogger());
|
||||
|
||||
ManualFileRenameResult result = useCase.rename(new ManualFileRenameRequest(FINGERPRINT, DESIRED_BASE));
|
||||
|
||||
assertThat(result).isInstanceOf(ManualFileRenameFileSystemFailure.class);
|
||||
assertThat(((ManualFileRenameFileSystemFailure) result).message()).contains("Laufwerk nicht erreichbar");
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Testfall 13: .pdf-Erweiterung wird automatisch angehängt
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@Test
|
||||
void rename_appendsPdfExtensionAutomatically() {
|
||||
List<String[]> folderArgs = new ArrayList<>();
|
||||
|
||||
TargetFolderPort folderPort = new TargetFolderPort() {
|
||||
@Override public String getTargetFolderLocator() { return "/zielordner"; }
|
||||
@Override
|
||||
public TargetFilenameResolutionResult resolveUniqueFilename(String baseName, DocumentFingerprint fp) {
|
||||
folderArgs.add(new String[]{baseName});
|
||||
return new ResolvedTargetFilename(baseName);
|
||||
}
|
||||
@Override public void tryDeleteTargetFile(String name) { }
|
||||
};
|
||||
|
||||
DefaultManualFileRenameUseCase useCase = new DefaultManualFileRenameUseCase(
|
||||
repositoryReturning(new DocumentTerminalSuccess(successRecord(CURRENT_FILE))),
|
||||
folderPort,
|
||||
renamePortReturning(new TargetFileRenameSuccess()),
|
||||
alwaysSucceedingUnitOfWork(),
|
||||
fixedClock(),
|
||||
noOpLogger());
|
||||
|
||||
// Eingabe ohne .pdf-Erweiterung
|
||||
useCase.rename(new ManualFileRenameRequest(FINGERPRINT, "2024-01-01 - Ohne Erweiterung"));
|
||||
|
||||
// Genau einmal aufgerufen, mit .pdf-Erweiterung
|
||||
assertThat(folderArgs).hasSize(1);
|
||||
assertThat(folderArgs.get(0)[0]).endsWith(".pdf");
|
||||
assertThat(folderArgs.get(0)[0]).isEqualTo("2024-01-01 - Ohne Erweiterung.pdf");
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Testfall: Nicht-processable Dokumentstatus → DocumentNotFound
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@Test
|
||||
void rename_returnsDocumentNotFound_whenDocumentIsKnownProcessable() {
|
||||
DocumentRecord knownRecord = new DocumentRecord(
|
||||
FINGERPRINT,
|
||||
new SourceDocumentLocator("/quelldatei.pdf"),
|
||||
"quelldatei.pdf",
|
||||
ProcessingStatus.FAILED_RETRYABLE,
|
||||
FailureCounters.zero(),
|
||||
FIXED_NOW,
|
||||
null,
|
||||
FIXED_NOW,
|
||||
FIXED_NOW,
|
||||
null,
|
||||
null);
|
||||
|
||||
DefaultManualFileRenameUseCase useCase = new DefaultManualFileRenameUseCase(
|
||||
repositoryReturning(new DocumentKnownProcessable(knownRecord)),
|
||||
folderPortReturning(new ResolvedTargetFilename(DESIRED_FULL)),
|
||||
renamePortReturning(new TargetFileRenameSuccess()),
|
||||
alwaysSucceedingUnitOfWork(),
|
||||
fixedClock(),
|
||||
noOpLogger());
|
||||
|
||||
ManualFileRenameResult result = useCase.rename(new ManualFileRenameRequest(FINGERPRINT, DESIRED_BASE));
|
||||
|
||||
assertThat(result).isInstanceOf(ManualFileRenameDocumentNotFound.class);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Testfall: Konstruktor-Null-Guards
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@Test
|
||||
void constructor_rejectsNullRepository() {
|
||||
assertThatNullPointerException().isThrownBy(() -> new DefaultManualFileRenameUseCase(
|
||||
null,
|
||||
folderPortReturning(new ResolvedTargetFilename(DESIRED_FULL)),
|
||||
renamePortReturning(new TargetFileRenameSuccess()),
|
||||
alwaysSucceedingUnitOfWork(),
|
||||
fixedClock(),
|
||||
noOpLogger()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void constructor_rejectsNullTargetFolderPort() {
|
||||
assertThatNullPointerException().isThrownBy(() -> new DefaultManualFileRenameUseCase(
|
||||
repositoryReturning(new DocumentUnknown()),
|
||||
null,
|
||||
renamePortReturning(new TargetFileRenameSuccess()),
|
||||
alwaysSucceedingUnitOfWork(),
|
||||
fixedClock(),
|
||||
noOpLogger()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void constructor_rejectsNullTargetFileRenamePort() {
|
||||
assertThatNullPointerException().isThrownBy(() -> new DefaultManualFileRenameUseCase(
|
||||
repositoryReturning(new DocumentUnknown()),
|
||||
folderPortReturning(new ResolvedTargetFilename(DESIRED_FULL)),
|
||||
null,
|
||||
alwaysSucceedingUnitOfWork(),
|
||||
fixedClock(),
|
||||
noOpLogger()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void constructor_rejectsNullUnitOfWorkPort() {
|
||||
assertThatNullPointerException().isThrownBy(() -> new DefaultManualFileRenameUseCase(
|
||||
repositoryReturning(new DocumentUnknown()),
|
||||
folderPortReturning(new ResolvedTargetFilename(DESIRED_FULL)),
|
||||
renamePortReturning(new TargetFileRenameSuccess()),
|
||||
null,
|
||||
fixedClock(),
|
||||
noOpLogger()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void constructor_rejectsNullClock() {
|
||||
assertThatNullPointerException().isThrownBy(() -> new DefaultManualFileRenameUseCase(
|
||||
repositoryReturning(new DocumentUnknown()),
|
||||
folderPortReturning(new ResolvedTargetFilename(DESIRED_FULL)),
|
||||
renamePortReturning(new TargetFileRenameSuccess()),
|
||||
alwaysSucceedingUnitOfWork(),
|
||||
null,
|
||||
noOpLogger()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void constructor_rejectsNullLogger() {
|
||||
assertThatNullPointerException().isThrownBy(() -> new DefaultManualFileRenameUseCase(
|
||||
repositoryReturning(new DocumentUnknown()),
|
||||
folderPortReturning(new ResolvedTargetFilename(DESIRED_FULL)),
|
||||
renamePortReturning(new TargetFileRenameSuccess()),
|
||||
alwaysSucceedingUnitOfWork(),
|
||||
fixedClock(),
|
||||
null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void rename_rejectsNullRequest() {
|
||||
DefaultManualFileRenameUseCase useCase = new DefaultManualFileRenameUseCase(
|
||||
repositoryReturning(new DocumentUnknown()),
|
||||
folderPortReturning(new ResolvedTargetFilename(DESIRED_FULL)),
|
||||
renamePortReturning(new TargetFileRenameSuccess()),
|
||||
alwaysSucceedingUnitOfWork(),
|
||||
fixedClock(),
|
||||
noOpLogger());
|
||||
|
||||
assertThatNullPointerException().isThrownBy(() -> useCase.rename(null));
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Hilfsklassen
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/** Führt keine Persistenzoperationen durch. */
|
||||
private static class NoOpTransactionOperations implements UnitOfWorkPort.TransactionOperations {
|
||||
@Override public void saveProcessingAttempt(ProcessingAttempt attempt) { }
|
||||
@Override public void createDocumentRecord(DocumentRecord record) { }
|
||||
@Override public void updateDocumentRecord(DocumentRecord record) { }
|
||||
@Override public void resetDocumentByFingerprint(DocumentFingerprint fingerprint) { }
|
||||
}
|
||||
|
||||
/** Zeichnet updateDocumentRecord-Aufrufe auf. */
|
||||
private static class RecordCapturingTransactionOperations implements UnitOfWorkPort.TransactionOperations {
|
||||
private final List<DocumentRecord> captured;
|
||||
|
||||
RecordCapturingTransactionOperations(List<DocumentRecord> captured) {
|
||||
this.captured = captured;
|
||||
}
|
||||
|
||||
@Override public void saveProcessingAttempt(ProcessingAttempt attempt) { }
|
||||
@Override public void createDocumentRecord(DocumentRecord record) { }
|
||||
@Override public void updateDocumentRecord(DocumentRecord record) { captured.add(record); }
|
||||
@Override public void resetDocumentByFingerprint(DocumentFingerprint fingerprint) { }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user