diff --git a/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/usecase/DefaultCreateNewDatabaseUseCase.java b/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/usecase/DefaultCreateNewDatabaseUseCase.java index 2ed78de..c574dc9 100644 --- a/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/usecase/DefaultCreateNewDatabaseUseCase.java +++ b/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/usecase/DefaultCreateNewDatabaseUseCase.java @@ -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...)} * normalisiert verglichen. Für noch nicht existierende Dateien wird das Elternverzeichnis * 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 { @@ -66,7 +68,9 @@ public class DefaultCreateNewDatabaseUseCase implements CreateNewDatabaseUseCase /** * 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(); } @@ -95,6 +99,9 @@ public class DefaultCreateNewDatabaseUseCase implements CreateNewDatabaseUseCase /** * Orchestriert den vollständigen Anlage- und Wechselvorgang. + *

+ * 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 * @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); // 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(); if (activeDb == null) { - LOG.error("Aktiver Datenbankpfad ist nicht ermittelbar – Anlage abgebrochen."); - return new CreateNewDatabaseResult.CreationFailed( - CreateNewDatabaseResult.Phase.PATH_RESOLUTION, - "Aktiver Datenbankpfad konnte nicht ermittelt werden.", - null); - } - Path absoluteActive = activeDb.toAbsolutePath().normalize(); - boolean sameFile; - try { - sameFile = isSameFile(absoluteActive, absoluteTarget); - } catch (IOException e) { - LOG.error("Pfad-Sicherheitsprüfung fehlgeschlagen: {}", e.getMessage(), e); - return new CreateNewDatabaseResult.CreationFailed( - CreateNewDatabaseResult.Phase.PATH_RESOLUTION, - "Pfad-Sicherheitsprüfung fehlgeschlagen: " + e.getMessage(), - e); - } - if (sameFile) { - LOG.warn("Anlage abgelehnt: Zielpfad entspricht der aktuell aktiven Datenbank: {}", - absoluteTarget); - return new CreateNewDatabaseResult.SameAsActiveDatabase(absoluteTarget); + LOG.info("Keine aktive Datenbank in Betrieb – Selbst-Überschreib-Prüfung entfällt, " + + "Anlage fährt fort."); + } else { + Path absoluteActive = activeDb.toAbsolutePath().normalize(); + boolean sameFile; + try { + sameFile = isSameFile(absoluteActive, absoluteTarget); + } catch (IOException e) { + LOG.error("Pfad-Sicherheitsprüfung fehlgeschlagen: {}", e.getMessage(), e); + return new CreateNewDatabaseResult.CreationFailed( + CreateNewDatabaseResult.Phase.PATH_RESOLUTION, + "Pfad-Sicherheitsprüfung fehlgeschlagen: " + e.getMessage(), + e); + } + if (sameFile) { + LOG.warn("Anlage abgelehnt: Zielpfad entspricht der aktuell aktiven Datenbank: {}", + absoluteTarget); + return new CreateNewDatabaseResult.SameAsActiveDatabase(absoluteTarget); + } } // Schritt 2: Temp-Datei im Zielverzeichnis vorbereiten diff --git a/pdf-umbenenner-application/src/test/java/de/gecheckt/pdf/umbenenner/application/usecase/DefaultCreateNewDatabaseUseCaseTest.java b/pdf-umbenenner-application/src/test/java/de/gecheckt/pdf/umbenenner/application/usecase/DefaultCreateNewDatabaseUseCaseTest.java index bbdd1d0..1b80dc6 100644 --- a/pdf-umbenenner-application/src/test/java/de/gecheckt/pdf/umbenenner/application/usecase/DefaultCreateNewDatabaseUseCaseTest.java +++ b/pdf-umbenenner-application/src/test/java/de/gecheckt/pdf/umbenenner/application/usecase/DefaultCreateNewDatabaseUseCaseTest.java @@ -168,6 +168,63 @@ class DefaultCreateNewDatabaseUseCaseTest { 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 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 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 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 void isSameFile_shouldReturnTrue_forIdenticalNonExistingFiles(@TempDir Path tempDir) throws IOException { Path a = tempDir.resolve("data.sqlite"); diff --git a/pdf-umbenenner-bootstrap/src/main/java/de/gecheckt/pdf/umbenenner/bootstrap/BootstrapRunner.java b/pdf-umbenenner-bootstrap/src/main/java/de/gecheckt/pdf/umbenenner/bootstrap/BootstrapRunner.java index 6cc50d3..90e98af 100644 --- a/pdf-umbenenner-bootstrap/src/main/java/de/gecheckt/pdf/umbenenner/bootstrap/BootstrapRunner.java +++ b/pdf-umbenenner-bootstrap/src/main/java/de/gecheckt/pdf/umbenenner/bootstrap/BootstrapRunner.java @@ -2042,6 +2042,9 @@ public class BootstrapRunner { * {@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 * {@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. *

* Pfad-Validierungs- und Move-Schritte verbleiben im Use-Case; der Adapter führt * Anlage, Migration und Verbindungstest aus. @@ -2061,9 +2064,10 @@ public class BootstrapRunner { return override.get(); } if (configFilePath == null || !Files.exists(configFilePath)) { - // Ohne Konfiguration und ohne Override: Zielpfad selbst als „aktiv" werten, - // damit die Pfadprüfung nur das Selbst-Überschreiben verhindert. - return targetFile.toAbsolutePath().normalize(); + // Ohne Override und ohne geladene Konfiguration existiert keine aktive + // Datenbank. null signalisiert dem Use-Case: keine Selbst-Überschreib-Prüfung + // nötig, die Anlage darf fortfahren. + return null; } try { migrateConfigurationIfNeeded(configFilePath); @@ -2071,9 +2075,9 @@ public class BootstrapRunner { return config.sqliteFile().toAbsolutePath().normalize(); } catch (Exception e) { 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()); - return targetFile.toAbsolutePath().normalize(); + return null; } }; DefaultCreateNewDatabaseUseCase useCase = new DefaultCreateNewDatabaseUseCase(