1 Commits

Author SHA1 Message Date
marcus 3da2bcc561 fix: Neue Datenbank anlegen ohne geladene Konfiguration ermoeglichen
Ohne Override und ohne geladene Konfiguration lieferte der aktive-DB-Supplier
faelschlich den Zielpfad selbst zurueck, wodurch die Selbst-Ueberschreib-Pruefung
immer ansprach und jede Anlage mit "Zielpfad ist die aktuell aktive
Datenbankdatei" abgelehnt wurde.

Der Supplier signalisiert "keine aktive Datenbank" nun mit null; der Use-Case
ueberspringt in diesem Fall die Pruefung und laesst die Anlage zu.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 14:26:24 +02:00
3 changed files with 96 additions and 28 deletions
@@ -41,7 +41,9 @@ import de.gecheckt.pdf.umbenenner.application.port.out.DatabaseCreationPort;
* Aktive DB und Zielpfad werden über {@link Path#toRealPath(java.nio.file.LinkOption...)} * Aktive DB und Zielpfad werden über {@link Path#toRealPath(java.nio.file.LinkOption...)}
* normalisiert verglichen. Für noch nicht existierende Dateien wird das Elternverzeichnis * normalisiert verglichen. Für noch nicht existierende Dateien wird das Elternverzeichnis
* real aufgelöst und der Dateiname normalisiert verglichen. Auf Windows erfolgt der * real aufgelöst und der Dateiname normalisiert verglichen. Auf Windows erfolgt der
* Vergleich case-insensitive. * Vergleich case-insensitive. Ist aktuell keine aktive Datenbank in Betrieb (der
* {@link ActiveDatabasePathSupplier} liefert {@code null}), entfällt die Prüfung und die
* Anlage fährt fort es gibt dann keine in Benutzung befindliche Datei zu schützen.
*/ */
public class DefaultCreateNewDatabaseUseCase implements CreateNewDatabaseUseCase { public class DefaultCreateNewDatabaseUseCase implements CreateNewDatabaseUseCase {
@@ -66,7 +68,9 @@ public class DefaultCreateNewDatabaseUseCase implements CreateNewDatabaseUseCase
/** /**
* Liefert den Pfad der aktuell aktiven SQLite-Datei. * Liefert den Pfad der aktuell aktiven SQLite-Datei.
* *
* @return den absoluten Pfad der aktiven Datei; nie {@code null} * @return den absoluten Pfad der aktiven Datei oder {@code null}, wenn aktuell keine
* aktive Datenbank in Betrieb ist. Bei {@code null} entfällt die
* Selbst-Überschreib-Prüfung und die Anlage darf fortfahren.
*/ */
Path get(); Path get();
} }
@@ -95,6 +99,9 @@ public class DefaultCreateNewDatabaseUseCase implements CreateNewDatabaseUseCase
/** /**
* Orchestriert den vollständigen Anlage- und Wechselvorgang. * Orchestriert den vollständigen Anlage- und Wechselvorgang.
* <p>
* Liefert der {@link ActiveDatabasePathSupplier} {@code null} (keine aktive Datenbank in
* Betrieb), entfällt die Selbst-Überschreib-Prüfung und die Anlage fährt fort.
* *
* @param targetFile der vom Benutzer ausgewählte Zielpfad; darf nicht {@code null} sein * @param targetFile der vom Benutzer ausgewählte Zielpfad; darf nicht {@code null} sein
* @return strukturiertes Ergebnis mit Erfolg oder klassifiziertem Fehler; nie {@code null} * @return strukturiertes Ergebnis mit Erfolg oder klassifiziertem Fehler; nie {@code null}
@@ -106,29 +113,29 @@ public class DefaultCreateNewDatabaseUseCase implements CreateNewDatabaseUseCase
LOG.info("Neue Datenbank anlegen: angeforderter Zielpfad = {}", absoluteTarget); LOG.info("Neue Datenbank anlegen: angeforderter Zielpfad = {}", absoluteTarget);
// Schritt 1: Pfad-Sicherheitsprüfung // Schritt 1: Pfad-Sicherheitsprüfung
// Ohne aktive Datenbank (Supplier liefert null) gibt es nichts zu schützen; die
// Selbst-Überschreib-Prüfung entfällt und die Anlage darf fortfahren.
Path activeDb = activeDatabasePathSupplier.get(); Path activeDb = activeDatabasePathSupplier.get();
if (activeDb == null) { if (activeDb == null) {
LOG.error("Aktiver Datenbankpfad ist nicht ermittelbar Anlage abgebrochen."); LOG.info("Keine aktive Datenbank in Betrieb Selbst-Überschreib-Prüfung entfällt, "
return new CreateNewDatabaseResult.CreationFailed( + "Anlage fährt fort.");
CreateNewDatabaseResult.Phase.PATH_RESOLUTION, } else {
"Aktiver Datenbankpfad konnte nicht ermittelt werden.", Path absoluteActive = activeDb.toAbsolutePath().normalize();
null); boolean sameFile;
} try {
Path absoluteActive = activeDb.toAbsolutePath().normalize(); sameFile = isSameFile(absoluteActive, absoluteTarget);
boolean sameFile; } catch (IOException e) {
try { LOG.error("Pfad-Sicherheitsprüfung fehlgeschlagen: {}", e.getMessage(), e);
sameFile = isSameFile(absoluteActive, absoluteTarget); return new CreateNewDatabaseResult.CreationFailed(
} catch (IOException e) { CreateNewDatabaseResult.Phase.PATH_RESOLUTION,
LOG.error("Pfad-Sicherheitsprüfung fehlgeschlagen: {}", e.getMessage(), e); "Pfad-Sicherheitsprüfung fehlgeschlagen: " + e.getMessage(),
return new CreateNewDatabaseResult.CreationFailed( e);
CreateNewDatabaseResult.Phase.PATH_RESOLUTION, }
"Pfad-Sicherheitsprüfung fehlgeschlagen: " + e.getMessage(), if (sameFile) {
e); LOG.warn("Anlage abgelehnt: Zielpfad entspricht der aktuell aktiven Datenbank: {}",
} absoluteTarget);
if (sameFile) { return new CreateNewDatabaseResult.SameAsActiveDatabase(absoluteTarget);
LOG.warn("Anlage abgelehnt: Zielpfad entspricht der aktuell aktiven Datenbank: {}", }
absoluteTarget);
return new CreateNewDatabaseResult.SameAsActiveDatabase(absoluteTarget);
} }
// Schritt 2: Temp-Datei im Zielverzeichnis vorbereiten // Schritt 2: Temp-Datei im Zielverzeichnis vorbereiten
@@ -168,6 +168,63 @@ class DefaultCreateNewDatabaseUseCaseTest {
assertThat(contextOverride.get()).isEqualTo(target.toAbsolutePath().normalize()); assertThat(contextOverride.get()).isEqualTo(target.toAbsolutePath().normalize());
} }
@Test
void createNewDatabase_shouldProceed_whenNoActiveDatabase(@TempDir Path tempDir) throws IOException {
// Szenario: GUI ohne geladene Konfiguration und ohne Override → Supplier liefert null.
// Es gibt keine aktive DB zu schützen; die Anlage muss durchlaufen.
Path target = tempDir.resolve("neu.sqlite");
AtomicReference<Path> capturedTemp = new AtomicReference<>();
DatabaseCreationPort creation = tempFile -> {
try {
Files.writeString(tempFile, "migrated-content");
} catch (IOException e) {
throw new IllegalStateException(e);
}
capturedTemp.set(tempFile);
return new DatabaseCreationPort.DatabaseCreationResult.Success(tempFile);
};
AtomicReference<Path> contextOverride = new AtomicReference<>();
DefaultCreateNewDatabaseUseCase useCase = new DefaultCreateNewDatabaseUseCase(
creation, trackingActiveContext(contextOverride), () -> null);
CreateNewDatabaseResult result = useCase.createNewDatabase(target);
assertThat(result).isInstanceOf(CreateNewDatabaseResult.Success.class);
assertThat(Files.exists(target)).isTrue();
assertThat(Files.readString(target)).isEqualTo("migrated-content");
assertThat(capturedTemp.get()).isNotNull();
assertThat(Files.exists(capturedTemp.get())).isFalse();
assertThat(contextOverride.get()).isEqualTo(target.toAbsolutePath().normalize());
}
@Test
void createNewDatabase_shouldOverwriteExistingTarget_whenNoActiveDatabase(@TempDir Path tempDir)
throws IOException {
// Kein aktiver DB-Kontext (Supplier liefert null), aber Zieldatei existiert bereits →
// Überschreiben ist erlaubt, weil keine in Benutzung befindliche DB betroffen ist.
Path target = tempDir.resolve("existing.sqlite");
Files.writeString(target, "alter-inhalt");
DatabaseCreationPort creation = tempFile -> {
try {
Files.writeString(tempFile, "neu-und-leer");
} catch (IOException e) {
throw new IllegalStateException(e);
}
return new DatabaseCreationPort.DatabaseCreationResult.Success(tempFile);
};
AtomicReference<Path> contextOverride = new AtomicReference<>();
DefaultCreateNewDatabaseUseCase useCase = new DefaultCreateNewDatabaseUseCase(
creation, trackingActiveContext(contextOverride), () -> null);
CreateNewDatabaseResult result = useCase.createNewDatabase(target);
assertThat(result).isInstanceOf(CreateNewDatabaseResult.Success.class);
assertThat(Files.readString(target)).isEqualTo("neu-und-leer");
assertThat(contextOverride.get()).isEqualTo(target.toAbsolutePath().normalize());
}
@Test @Test
void isSameFile_shouldReturnTrue_forIdenticalNonExistingFiles(@TempDir Path tempDir) throws IOException { void isSameFile_shouldReturnTrue_forIdenticalNonExistingFiles(@TempDir Path tempDir) throws IOException {
Path a = tempDir.resolve("data.sqlite"); Path a = tempDir.resolve("data.sqlite");
@@ -2042,6 +2042,9 @@ public class BootstrapRunner {
* {@link #activeDatabaseContext}. Die zugrundeliegende Liefer-Lambda für den aktiven * {@link #activeDatabaseContext}. Die zugrundeliegende Liefer-Lambda für den aktiven
* DB-Pfad bevorzugt das Override im Kontext-Port; ist keines gesetzt, fällt sie auf den * DB-Pfad bevorzugt das Override im Kontext-Port; ist keines gesetzt, fällt sie auf den
* {@code sqliteFile}-Wert der aus {@code configFilePath} geladenen Konfiguration zurück. * {@code sqliteFile}-Wert der aus {@code configFilePath} geladenen Konfiguration zurück.
* Existiert weder ein Override noch eine ladbare Konfiguration, gibt es keine aktive
* Datenbank: die Lambda liefert dann {@code null}, wodurch der Use-Case die
* Selbst-Überschreib-Prüfung überspringt und die Anlage zulässt.
* <p> * <p>
* Pfad-Validierungs- und Move-Schritte verbleiben im Use-Case; der Adapter führt * Pfad-Validierungs- und Move-Schritte verbleiben im Use-Case; der Adapter führt
* Anlage, Migration und Verbindungstest aus. * Anlage, Migration und Verbindungstest aus.
@@ -2061,9 +2064,10 @@ public class BootstrapRunner {
return override.get(); return override.get();
} }
if (configFilePath == null || !Files.exists(configFilePath)) { if (configFilePath == null || !Files.exists(configFilePath)) {
// Ohne Konfiguration und ohne Override: Zielpfad selbst als „aktiv" werten, // Ohne Override und ohne geladene Konfiguration existiert keine aktive
// damit die Pfadprüfung nur das Selbst-Überschreiben verhindert. // Datenbank. null signalisiert dem Use-Case: keine Selbst-Überschreib-Prüfung
return targetFile.toAbsolutePath().normalize(); // nötig, die Anlage darf fortfahren.
return null;
} }
try { try {
migrateConfigurationIfNeeded(configFilePath); migrateConfigurationIfNeeded(configFilePath);
@@ -2071,9 +2075,9 @@ public class BootstrapRunner {
return config.sqliteFile().toAbsolutePath().normalize(); return config.sqliteFile().toAbsolutePath().normalize();
} catch (Exception e) { } catch (Exception e) {
LOG.warn("GUI-Datenbankanlage: aktive DB konnte nicht aus {} ermittelt werden: {} — " LOG.warn("GUI-Datenbankanlage: aktive DB konnte nicht aus {} ermittelt werden: {} — "
+ "Zielpfad wird stattdessen als aktiv angenommen.", + "es wird keine aktive DB angenommen.",
configFilePath, e.getMessage()); configFilePath, e.getMessage());
return targetFile.toAbsolutePath().normalize(); return null;
} }
}; };
DefaultCreateNewDatabaseUseCase useCase = new DefaultCreateNewDatabaseUseCase( DefaultCreateNewDatabaseUseCase useCase = new DefaultCreateNewDatabaseUseCase(