1
0

M1 Vollständiger Grundstand mit Build, Konfiguration, Tests und Smoke-Tests

This commit is contained in:
2026-03-31 14:04:47 +02:00
commit ea83f8fa8c
52 changed files with 2819 additions and 0 deletions

View File

@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>de.gecheckt</groupId>
<artifactId>pdf-umbenenner-parent</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<artifactId>pdf-umbenenner-application</artifactId>
<packaging>jar</packaging>
<dependencies>
<!-- Internal dependency -->
<dependency>
<groupId>de.gecheckt</groupId>
<artifactId>pdf-umbenenner-domain</artifactId>
<version>${project.version}</version>
</dependency>
<!-- Logging -->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
</dependency>
<!-- Test dependencies -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,21 @@
package de.gecheckt.pdf.umbenenner.application.config;
/**
* Exception thrown when startup configuration validation fails.
* <p>
* Contains an aggregated message describing all validation errors found.
* This is a controlled failure mode that prevents processing from starting.
*/
public class InvalidStartConfigurationException extends RuntimeException {
private static final long serialVersionUID = 1L;
/**
* Creates the exception with an aggregated error message.
*
* @param message the aggregated validation error message
*/
public InvalidStartConfigurationException(String message) {
super(message);
}
}

View File

@@ -0,0 +1,26 @@
package de.gecheckt.pdf.umbenenner.application.config;
import java.net.URI;
import java.nio.file.Path;
/**
* Typed immutable configuration model for PDF Umbenenner startup parameters.
* AP-005: Represents all M1-relevant configuration properties with strong typing.
*/
public record StartConfiguration(
Path sourceFolder,
Path targetFolder,
Path sqliteFile,
URI apiBaseUrl,
String apiModel,
int apiTimeoutSeconds,
int maxRetriesTransient,
int maxPages,
int maxTextCharacters,
Path promptTemplateFile,
Path runtimeLockFile,
Path logDirectory,
String logLevel,
String apiKey
)
{ }

View File

@@ -0,0 +1,198 @@
package de.gecheckt.pdf.umbenenner.application.config;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
/**
* Validates {@link StartConfiguration} before processing can begin.
* <p>
* Performs mandatory field checks, numeric range validation, URI scheme validation,
* and basic path existence checks. Throws {@link InvalidStartConfigurationException}
* if any validation rule fails.
*/
public class StartConfigurationValidator {
private static final Logger LOG = LogManager.getLogger(StartConfigurationValidator.class);
/**
* Validates the given configuration.
* <p>
* Checks all mandatory fields, numeric constraints, URI validity, and path existence.
* If validation fails, throws {@link InvalidStartConfigurationException} with an
* aggregated error message listing all problems.
*
* @param config the configuration to validate, must not be null
* @throws InvalidStartConfigurationException if any validation rule fails
*/
public void validate(StartConfiguration config) {
List<String> errors = new ArrayList<>();
// Mandatory string/path presence checks
validateSourceFolder(config.sourceFolder(), errors);
validateTargetFolder(config.targetFolder(), errors);
validateSqliteFile(config.sqliteFile(), errors);
validateApiBaseUrl(config.apiBaseUrl(), errors);
validateApiModel(config.apiModel(), errors);
validatePromptTemplateFile(config.promptTemplateFile(), errors);
// Numeric validation
validateApiTimeoutSeconds(config.apiTimeoutSeconds(), errors);
validateMaxRetriesTransient(config.maxRetriesTransient(), errors);
validateMaxPages(config.maxPages(), errors);
validateMaxTextCharacters(config.maxTextCharacters(), errors);
// Path relationship validation
validateSourceAndTargetNotSame(config.sourceFolder(), config.targetFolder(), errors);
// Optional path validations (only if present)
validateRuntimeLockFile(config.runtimeLockFile(), errors);
validateLogDirectory(config.logDirectory(), errors);
if (!errors.isEmpty()) {
String errorMessage = "Invalid startup configuration:\n" + String.join("\n", errors);
throw new InvalidStartConfigurationException(errorMessage);
}
LOG.info("Configuration validation successful.");
}
private void validateSourceFolder(Path sourceFolder, List<String> errors) {
if (sourceFolder == null) {
errors.add("- source.folder: must not be null");
return;
}
if (!Files.exists(sourceFolder)) {
errors.add("- source.folder: path does not exist: " + sourceFolder);
} else if (!Files.isDirectory(sourceFolder)) {
errors.add("- source.folder: path is not a directory: " + sourceFolder);
}
}
private void validateTargetFolder(Path targetFolder, List<String> errors) {
if (targetFolder == null) {
errors.add("- target.folder: must not be null");
return;
}
if (!Files.exists(targetFolder)) {
errors.add("- target.folder: path does not exist: " + targetFolder);
} else if (!Files.isDirectory(targetFolder)) {
errors.add("- target.folder: path is not a directory: " + targetFolder);
}
}
private void validateSqliteFile(Path sqliteFile, List<String> errors) {
if (sqliteFile == null) {
errors.add("- sqlite.file: must not be null");
return;
}
Path parent = sqliteFile.getParent();
if (parent == null) {
errors.add("- sqlite.file: has no parent directory: " + sqliteFile);
} else if (!Files.exists(parent)) {
errors.add("- sqlite.file: parent directory does not exist: " + parent);
} else if (!Files.isDirectory(parent)) {
errors.add("- sqlite.file: parent is not a directory: " + parent);
}
}
private void validateApiBaseUrl(java.net.URI apiBaseUrl, List<String> errors) {
if (apiBaseUrl == null) {
errors.add("- api.baseUrl: must not be null");
return;
}
if (!apiBaseUrl.isAbsolute()) {
errors.add("- api.baseUrl: must be an absolute URI: " + apiBaseUrl);
return;
}
String scheme = apiBaseUrl.getScheme();
if (scheme == null || (!"http".equalsIgnoreCase(scheme) && !"https".equalsIgnoreCase(scheme))) {
errors.add("- api.baseUrl: scheme must be http or https, got: " + scheme);
}
}
private void validateApiModel(String apiModel, List<String> errors) {
if (apiModel == null || apiModel.isBlank()) {
errors.add("- api.model: must not be null or blank");
}
}
private void validateApiTimeoutSeconds(int apiTimeoutSeconds, List<String> errors) {
if (apiTimeoutSeconds <= 0) {
errors.add("- api.timeoutSeconds: must be > 0, got: " + apiTimeoutSeconds);
}
}
private void validateMaxRetriesTransient(int maxRetriesTransient, List<String> errors) {
if (maxRetriesTransient < 0) {
errors.add("- max.retries.transient: must be >= 0, got: " + maxRetriesTransient);
}
}
private void validateMaxPages(int maxPages, List<String> errors) {
if (maxPages <= 0) {
errors.add("- max.pages: must be > 0, got: " + maxPages);
}
}
private void validateMaxTextCharacters(int maxTextCharacters, List<String> errors) {
if (maxTextCharacters <= 0) {
errors.add("- max.text.characters: must be > 0, got: " + maxTextCharacters);
}
}
private void validatePromptTemplateFile(Path promptTemplateFile, List<String> errors) {
if (promptTemplateFile == null) {
errors.add("- prompt.template.file: must not be null");
return;
}
if (!Files.exists(promptTemplateFile)) {
errors.add("- prompt.template.file: path does not exist: " + promptTemplateFile);
} else if (!Files.isRegularFile(promptTemplateFile)) {
errors.add("- prompt.template.file: path is not a regular file: " + promptTemplateFile);
}
}
private void validateSourceAndTargetNotSame(Path sourceFolder, Path targetFolder, List<String> errors) {
if (sourceFolder != null && targetFolder != null) {
try {
Path normalizedSource = sourceFolder.toRealPath();
Path normalizedTarget = targetFolder.toRealPath();
if (normalizedSource.equals(normalizedTarget)) {
errors.add("- source.folder and target.folder must not resolve to the same path: " + normalizedSource);
}
} catch (Exception e) {
// If toRealPath fails (e.g., path doesn't exist), skip this check
// The individual existence checks will catch missing paths
}
}
}
private void validateRuntimeLockFile(Path runtimeLockFile, List<String> errors) {
if (runtimeLockFile != null && !runtimeLockFile.toString().isBlank()) {
Path parent = runtimeLockFile.getParent();
if (parent != null) {
if (!Files.exists(parent)) {
errors.add("- runtime.lock.file: parent directory does not exist: " + parent);
} else if (!Files.isDirectory(parent)) {
errors.add("- runtime.lock.file: parent is not a directory: " + parent);
}
}
}
}
private void validateLogDirectory(Path logDirectory, List<String> errors) {
if (logDirectory != null && !logDirectory.toString().isBlank()) {
if (Files.exists(logDirectory)) {
if (!Files.isDirectory(logDirectory)) {
errors.add("- log.directory: exists but is not a directory: " + logDirectory);
}
}
// If it doesn't exist yet, that's acceptable - we don't auto-create
}
}
}

View File

@@ -0,0 +1,5 @@
/**
* Configuration model types for the PDF Umbenenner application.
* Contains typed configuration objects representing startup parameters.
*/
package de.gecheckt.pdf.umbenenner.application.config;

View File

@@ -0,0 +1,5 @@
/**
* Application layer containing use cases and inbound ports.
* This package defines the business logic boundary without infrastructure details.
*/
package de.gecheckt.pdf.umbenenner.application;

View File

@@ -0,0 +1,18 @@
package de.gecheckt.pdf.umbenenner.application.port.in;
/**
* Inbound port for batch processing execution.
* This interface defines the contract for triggering batch operations.
* <p>
* AP-003 Implementation: Currently a no-op placeholder to establish the technical startup path.
*/
public interface RunBatchProcessingUseCase {
/**
* Executes the batch processing workflow.
* <p>
* AP-003: This method performs no actual work, only validates the call chain.
*
* @return true if the workflow completed successfully, false otherwise
*/
boolean execute();
}

View File

@@ -0,0 +1,5 @@
/**
* Inbound ports (application service interfaces) for the hexagonal architecture.
* Use cases in this package are invoked by adapters from the outside world.
*/
package de.gecheckt.pdf.umbenenner.application.port.in;

View File

@@ -0,0 +1,17 @@
package de.gecheckt.pdf.umbenenner.application.port.out;
import de.gecheckt.pdf.umbenenner.application.config.StartConfiguration;
/**
* Outbound port for configuration access.
* AP-005: Minimal interface for loading typed startup configuration.
*/
public interface ConfigurationPort {
/**
* Loads and returns the startup configuration.
*
* @return the loaded StartConfiguration
*/
StartConfiguration loadConfiguration();
}

View File

@@ -0,0 +1,5 @@
/**
* Outbound ports for the application layer.
* Defines interfaces for infrastructure access from the application layer.
*/
package de.gecheckt.pdf.umbenenner.application.port.out;

View File

@@ -0,0 +1,41 @@
package de.gecheckt.pdf.umbenenner.application.usecase;
import de.gecheckt.pdf.umbenenner.application.config.StartConfiguration;
import de.gecheckt.pdf.umbenenner.application.port.in.RunBatchProcessingUseCase;
import de.gecheckt.pdf.umbenenner.application.port.out.ConfigurationPort;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/**
* Minimal no-op implementation of {@link RunBatchProcessingUseCase}.
* <p>
* AP-003 Implementation: Provides a controlled, non-functional startup path
* without any business logic, PDF processing, or infrastructure access.
* <p>
* AP-005: Accepts {@link ConfigurationPort} to load typed startup configuration.
*/
public class NoOpRunBatchProcessingUseCase implements RunBatchProcessingUseCase {
private static final Logger LOG = LogManager.getLogger(NoOpRunBatchProcessingUseCase.class);
private final ConfigurationPort configurationPort;
/**
* Creates the no-op use case with a configuration port.
*
* @param configurationPort the configuration port for loading startup configuration
*/
public NoOpRunBatchProcessingUseCase(ConfigurationPort configurationPort) {
this.configurationPort = configurationPort;
}
@Override
public boolean execute() {
// AP-005: Load configuration through the port (technical loading only)
StartConfiguration config = configurationPort.loadConfiguration();
LOG.info("Configuration loaded successfully. Source: {}, Target: {}", config.sourceFolder(), config.targetFolder());
// AP-003: Intentional no-op - validates the technical call chain only
return true;
}
}

View File

@@ -0,0 +1,5 @@
/**
* Use case implementations that realize the inbound port contracts.
* Currently contains minimal no-op implementations for AP-003 technical validation.
*/
package de.gecheckt.pdf.umbenenner.application.usecase;

View File

@@ -0,0 +1 @@
# Keep directory

View File

@@ -0,0 +1,686 @@
package de.gecheckt.pdf.umbenenner.application.config;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.junit.jupiter.api.Assertions.*;
/**
* Unit tests for {@link StartConfigurationValidator}.
* <p>
* Tests cover valid configuration, missing mandatory fields, invalid values,
* and path relationship validations.
*/
class StartConfigurationValidatorTest {
private final StartConfigurationValidator validator = new StartConfigurationValidator();
@TempDir
Path tempDir;
@Test
void validate_successWithValidConfiguration() throws Exception {
Path sourceFolder = Files.createDirectory(tempDir.resolve("source"));
Path targetFolder = Files.createDirectory(tempDir.resolve("target"));
Path sqliteFile = Files.createFile(tempDir.resolve("db.sqlite"));
Path promptTemplateFile = Files.createFile(tempDir.resolve("prompt.txt"));
StartConfiguration config = new StartConfiguration(
sourceFolder,
targetFolder,
sqliteFile,
URI.create("https://api.example.com"),
"gpt-4",
30,
3,
100,
50000,
promptTemplateFile,
tempDir.resolve("lock.lock"),
tempDir.resolve("logs"),
"INFO",
"test-api-key"
);
assertDoesNotThrow(() -> validator.validate(config));
}
@Test
void validate_failsWhenSourceFolderIsNull() {
StartConfiguration config = new StartConfiguration(
null,
tempDir.resolve("target"),
tempDir.resolve("db.sqlite"),
URI.create("https://api.example.com"),
"gpt-4",
30,
3,
100,
50000,
tempDir.resolve("prompt.txt"),
null,
null,
"INFO",
"test-api-key"
);
InvalidStartConfigurationException exception = assertThrows(
InvalidStartConfigurationException.class,
() -> validator.validate(config)
);
assertTrue(exception.getMessage().contains("source.folder: must not be null"));
}
@Test
void validate_failsWhenTargetFolderIsNull() {
StartConfiguration config = new StartConfiguration(
tempDir.resolve("source"),
null,
tempDir.resolve("db.sqlite"),
URI.create("https://api.example.com"),
"gpt-4",
30,
3,
100,
50000,
tempDir.resolve("prompt.txt"),
null,
null,
"INFO",
"test-api-key"
);
InvalidStartConfigurationException exception = assertThrows(
InvalidStartConfigurationException.class,
() -> validator.validate(config)
);
assertTrue(exception.getMessage().contains("target.folder: must not be null"));
}
@Test
void validate_failsWhenSqliteFileIsNull() {
StartConfiguration config = new StartConfiguration(
tempDir.resolve("source"),
tempDir.resolve("target"),
null,
URI.create("https://api.example.com"),
"gpt-4",
30,
3,
100,
50000,
tempDir.resolve("prompt.txt"),
null,
null,
"INFO",
"test-api-key"
);
InvalidStartConfigurationException exception = assertThrows(
InvalidStartConfigurationException.class,
() -> validator.validate(config)
);
assertTrue(exception.getMessage().contains("sqlite.file: must not be null"));
}
@Test
void validate_failsWhenApiBaseUrlIsNull() throws Exception {
Path sourceFolder = Files.createDirectory(tempDir.resolve("source"));
Path targetFolder = Files.createDirectory(tempDir.resolve("target"));
Path sqliteFile = Files.createFile(tempDir.resolve("db.sqlite"));
Path promptTemplateFile = Files.createFile(tempDir.resolve("prompt.txt"));
StartConfiguration config = new StartConfiguration(
sourceFolder,
targetFolder,
sqliteFile,
null,
"gpt-4",
30,
3,
100,
50000,
promptTemplateFile,
null,
null,
"INFO",
"test-api-key"
);
InvalidStartConfigurationException exception = assertThrows(
InvalidStartConfigurationException.class,
() -> validator.validate(config)
);
assertTrue(exception.getMessage().contains("api.baseUrl: must not be null"));
}
@Test
void validate_failsWhenApiModelIsNull() throws Exception {
Path sourceFolder = Files.createDirectory(tempDir.resolve("source"));
Path targetFolder = Files.createDirectory(tempDir.resolve("target"));
Path sqliteFile = Files.createFile(tempDir.resolve("db.sqlite"));
Path promptTemplateFile = Files.createFile(tempDir.resolve("prompt.txt"));
StartConfiguration config = new StartConfiguration(
sourceFolder,
targetFolder,
sqliteFile,
URI.create("https://api.example.com"),
null,
30,
3,
100,
50000,
promptTemplateFile,
null,
null,
"INFO",
"test-api-key"
);
InvalidStartConfigurationException exception = assertThrows(
InvalidStartConfigurationException.class,
() -> validator.validate(config)
);
assertTrue(exception.getMessage().contains("api.model: must not be null or blank"));
}
@Test
void validate_failsWhenPromptTemplateFileIsNull() throws Exception {
Path sourceFolder = Files.createDirectory(tempDir.resolve("source"));
Path targetFolder = Files.createDirectory(tempDir.resolve("target"));
Path sqliteFile = Files.createFile(tempDir.resolve("db.sqlite"));
StartConfiguration config = new StartConfiguration(
sourceFolder,
targetFolder,
sqliteFile,
URI.create("https://api.example.com"),
"gpt-4",
30,
3,
100,
50000,
null,
null,
null,
"INFO",
"test-api-key"
);
InvalidStartConfigurationException exception = assertThrows(
InvalidStartConfigurationException.class,
() -> validator.validate(config)
);
assertTrue(exception.getMessage().contains("prompt.template.file: must not be null"));
}
@Test
void validate_failsWhenApiTimeoutSecondsIsZeroOrNegative() throws Exception {
Path sourceFolder = Files.createDirectory(tempDir.resolve("source"));
Path targetFolder = Files.createDirectory(tempDir.resolve("target"));
Path sqliteFile = Files.createFile(tempDir.resolve("db.sqlite"));
Path promptTemplateFile = Files.createFile(tempDir.resolve("prompt.txt"));
StartConfiguration config = new StartConfiguration(
sourceFolder,
targetFolder,
sqliteFile,
URI.create("https://api.example.com"),
"gpt-4",
0,
3,
100,
50000,
promptTemplateFile,
null,
null,
"INFO",
"test-api-key"
);
InvalidStartConfigurationException exception = assertThrows(
InvalidStartConfigurationException.class,
() -> validator.validate(config)
);
assertTrue(exception.getMessage().contains("api.timeoutSeconds: must be > 0"));
}
@Test
void validate_failsWhenMaxRetriesTransientIsNegative() throws Exception {
Path sourceFolder = Files.createDirectory(tempDir.resolve("source"));
Path targetFolder = Files.createDirectory(tempDir.resolve("target"));
Path sqliteFile = Files.createFile(tempDir.resolve("db.sqlite"));
Path promptTemplateFile = Files.createFile(tempDir.resolve("prompt.txt"));
StartConfiguration config = new StartConfiguration(
sourceFolder,
targetFolder,
sqliteFile,
URI.create("https://api.example.com"),
"gpt-4",
30,
-1,
100,
50000,
promptTemplateFile,
null,
null,
"INFO",
"test-api-key"
);
InvalidStartConfigurationException exception = assertThrows(
InvalidStartConfigurationException.class,
() -> validator.validate(config)
);
assertTrue(exception.getMessage().contains("max.retries.transient: must be >= 0"));
}
@Test
void validate_failsWhenMaxPagesIsZeroOrNegative() throws Exception {
Path sourceFolder = Files.createDirectory(tempDir.resolve("source"));
Path targetFolder = Files.createDirectory(tempDir.resolve("target"));
Path sqliteFile = Files.createFile(tempDir.resolve("db.sqlite"));
Path promptTemplateFile = Files.createFile(tempDir.resolve("prompt.txt"));
StartConfiguration config = new StartConfiguration(
sourceFolder,
targetFolder,
sqliteFile,
URI.create("https://api.example.com"),
"gpt-4",
30,
3,
0,
50000,
promptTemplateFile,
null,
null,
"INFO",
"test-api-key"
);
InvalidStartConfigurationException exception = assertThrows(
InvalidStartConfigurationException.class,
() -> validator.validate(config)
);
assertTrue(exception.getMessage().contains("max.pages: must be > 0"));
}
@Test
void validate_failsWhenMaxTextCharactersIsZeroOrNegative() throws Exception {
Path sourceFolder = Files.createDirectory(tempDir.resolve("source"));
Path targetFolder = Files.createDirectory(tempDir.resolve("target"));
Path sqliteFile = Files.createFile(tempDir.resolve("db.sqlite"));
Path promptTemplateFile = Files.createFile(tempDir.resolve("prompt.txt"));
StartConfiguration config = new StartConfiguration(
sourceFolder,
targetFolder,
sqliteFile,
URI.create("https://api.example.com"),
"gpt-4",
30,
3,
100,
-1,
promptTemplateFile,
null,
null,
"INFO",
"test-api-key"
);
InvalidStartConfigurationException exception = assertThrows(
InvalidStartConfigurationException.class,
() -> validator.validate(config)
);
assertTrue(exception.getMessage().contains("max.text.characters: must be > 0"));
}
@Test
void validate_failsWhenSourceFolderDoesNotExist() throws Exception {
Path targetFolder = Files.createDirectory(tempDir.resolve("target"));
Path sqliteFile = Files.createFile(tempDir.resolve("db.sqlite"));
Path promptTemplateFile = Files.createFile(tempDir.resolve("prompt.txt"));
StartConfiguration config = new StartConfiguration(
tempDir.resolve("nonexistent"),
targetFolder,
sqliteFile,
URI.create("https://api.example.com"),
"gpt-4",
30,
3,
100,
50000,
promptTemplateFile,
null,
null,
"INFO",
"test-api-key"
);
InvalidStartConfigurationException exception = assertThrows(
InvalidStartConfigurationException.class,
() -> validator.validate(config)
);
assertTrue(exception.getMessage().contains("source.folder: path does not exist"));
}
@Test
void validate_failsWhenSourceFolderIsNotADirectory() throws Exception {
Path sourceFile = Files.createFile(tempDir.resolve("sourcefile.txt"));
Path targetFolder = Files.createDirectory(tempDir.resolve("target"));
Path sqliteFile = Files.createFile(tempDir.resolve("db.sqlite"));
Path promptTemplateFile = Files.createFile(tempDir.resolve("prompt.txt"));
StartConfiguration config = new StartConfiguration(
sourceFile,
targetFolder,
sqliteFile,
URI.create("https://api.example.com"),
"gpt-4",
30,
3,
100,
50000,
promptTemplateFile,
null,
null,
"INFO",
"test-api-key"
);
InvalidStartConfigurationException exception = assertThrows(
InvalidStartConfigurationException.class,
() -> validator.validate(config)
);
assertTrue(exception.getMessage().contains("source.folder: path is not a directory"));
}
@Test
void validate_failsWhenTargetFolderDoesNotExist() throws Exception {
Path sourceFolder = Files.createDirectory(tempDir.resolve("source"));
Path sqliteFile = Files.createFile(tempDir.resolve("db.sqlite"));
Path promptTemplateFile = Files.createFile(tempDir.resolve("prompt.txt"));
StartConfiguration config = new StartConfiguration(
sourceFolder,
tempDir.resolve("nonexistent"),
sqliteFile,
URI.create("https://api.example.com"),
"gpt-4",
30,
3,
100,
50000,
promptTemplateFile,
null,
null,
"INFO",
"test-api-key"
);
InvalidStartConfigurationException exception = assertThrows(
InvalidStartConfigurationException.class,
() -> validator.validate(config)
);
assertTrue(exception.getMessage().contains("target.folder: path does not exist"));
}
@Test
void validate_failsWhenTargetFolderIsNotADirectory() throws Exception {
Path sourceFolder = Files.createDirectory(tempDir.resolve("source"));
Path targetFile = Files.createFile(tempDir.resolve("targetfile.txt"));
Path sqliteFile = Files.createFile(tempDir.resolve("db.sqlite"));
Path promptTemplateFile = Files.createFile(tempDir.resolve("prompt.txt"));
StartConfiguration config = new StartConfiguration(
sourceFolder,
targetFile,
sqliteFile,
URI.create("https://api.example.com"),
"gpt-4",
30,
3,
100,
50000,
promptTemplateFile,
null,
null,
"INFO",
"test-api-key"
);
InvalidStartConfigurationException exception = assertThrows(
InvalidStartConfigurationException.class,
() -> validator.validate(config)
);
assertTrue(exception.getMessage().contains("target.folder: path is not a directory"));
}
@Test
void validate_failsWhenSqliteFileParentDoesNotExist() throws Exception {
Path sourceFolder = Files.createDirectory(tempDir.resolve("source"));
Path targetFolder = Files.createDirectory(tempDir.resolve("target"));
Path promptTemplateFile = Files.createFile(tempDir.resolve("prompt.txt"));
StartConfiguration config = new StartConfiguration(
sourceFolder,
targetFolder,
tempDir.resolve("nonexistent/db.sqlite"),
URI.create("https://api.example.com"),
"gpt-4",
30,
3,
100,
50000,
promptTemplateFile,
null,
null,
"INFO",
"test-api-key"
);
InvalidStartConfigurationException exception = assertThrows(
InvalidStartConfigurationException.class,
() -> validator.validate(config)
);
assertTrue(exception.getMessage().contains("sqlite.file: parent directory does not exist"));
}
@Test
void validate_failsWhenApiBaseUrlIsNotAbsolute() throws Exception {
Path sourceFolder = Files.createDirectory(tempDir.resolve("source"));
Path targetFolder = Files.createDirectory(tempDir.resolve("target"));
Path sqliteFile = Files.createFile(tempDir.resolve("db.sqlite"));
Path promptTemplateFile = Files.createFile(tempDir.resolve("prompt.txt"));
StartConfiguration config = new StartConfiguration(
sourceFolder,
targetFolder,
sqliteFile,
URI.create("/api/v1"),
"gpt-4",
30,
3,
100,
50000,
promptTemplateFile,
null,
null,
"INFO",
"test-api-key"
);
InvalidStartConfigurationException exception = assertThrows(
InvalidStartConfigurationException.class,
() -> validator.validate(config)
);
assertTrue(exception.getMessage().contains("api.baseUrl: must be an absolute URI"));
}
@Test
void validate_failsWhenApiBaseUrlHasUnsupportedScheme() throws Exception {
Path sourceFolder = Files.createDirectory(tempDir.resolve("source"));
Path targetFolder = Files.createDirectory(tempDir.resolve("target"));
Path sqliteFile = Files.createFile(tempDir.resolve("db.sqlite"));
Path promptTemplateFile = Files.createFile(tempDir.resolve("prompt.txt"));
StartConfiguration config = new StartConfiguration(
sourceFolder,
targetFolder,
sqliteFile,
URI.create("ftp://api.example.com"),
"gpt-4",
30,
3,
100,
50000,
promptTemplateFile,
null,
null,
"INFO",
"test-api-key"
);
InvalidStartConfigurationException exception = assertThrows(
InvalidStartConfigurationException.class,
() -> validator.validate(config)
);
assertTrue(exception.getMessage().contains("api.baseUrl: scheme must be http or https"));
}
@Test
void validate_failsWhenPromptTemplateFileDoesNotExist() throws Exception {
Path sourceFolder = Files.createDirectory(tempDir.resolve("source"));
Path targetFolder = Files.createDirectory(tempDir.resolve("target"));
Path sqliteFile = Files.createFile(tempDir.resolve("db.sqlite"));
StartConfiguration config = new StartConfiguration(
sourceFolder,
targetFolder,
sqliteFile,
URI.create("https://api.example.com"),
"gpt-4",
30,
3,
100,
50000,
tempDir.resolve("nonexistent.txt"),
null,
null,
"INFO",
"test-api-key"
);
InvalidStartConfigurationException exception = assertThrows(
InvalidStartConfigurationException.class,
() -> validator.validate(config)
);
assertTrue(exception.getMessage().contains("prompt.template.file: path does not exist"));
}
@Test
void validate_failsWhenPromptTemplateFileIsNotARegularFile() throws Exception {
Path sourceFolder = Files.createDirectory(tempDir.resolve("source"));
Path targetFolder = Files.createDirectory(tempDir.resolve("target"));
Path sqliteFile = Files.createFile(tempDir.resolve("db.sqlite"));
Path dirForPrompt = Files.createDirectory(tempDir.resolve("promptdir"));
StartConfiguration config = new StartConfiguration(
sourceFolder,
targetFolder,
sqliteFile,
URI.create("https://api.example.com"),
"gpt-4",
30,
3,
100,
50000,
dirForPrompt,
null,
null,
"INFO",
"test-api-key"
);
InvalidStartConfigurationException exception = assertThrows(
InvalidStartConfigurationException.class,
() -> validator.validate(config)
);
assertTrue(exception.getMessage().contains("prompt.template.file: path is not a regular file"));
}
@Test
void validate_failsWhenSourceAndTargetAreSamePath() throws Exception {
Path sameFolder = Files.createDirectory(tempDir.resolve("samefolder"));
Path sqliteFile = Files.createFile(tempDir.resolve("db.sqlite"));
Path promptTemplateFile = Files.createFile(tempDir.resolve("prompt.txt"));
StartConfiguration config = new StartConfiguration(
sameFolder,
sameFolder,
sqliteFile,
URI.create("https://api.example.com"),
"gpt-4",
30,
3,
100,
50000,
promptTemplateFile,
null,
null,
"INFO",
"test-api-key"
);
InvalidStartConfigurationException exception = assertThrows(
InvalidStartConfigurationException.class,
() -> validator.validate(config)
);
assertTrue(exception.getMessage().contains("source.folder and target.folder must not resolve to the same path"));
}
@Test
void validate_failsWhenMultipleErrorsOccur() {
StartConfiguration config = new StartConfiguration(
null,
null,
null,
null,
null,
0,
-1,
0,
-1,
null,
null,
null,
"INFO",
"test-api-key"
);
InvalidStartConfigurationException exception = assertThrows(
InvalidStartConfigurationException.class,
() -> validator.validate(config)
);
String message = exception.getMessage();
assertTrue(message.contains("source.folder: must not be null"));
assertTrue(message.contains("target.folder: must not be null"));
assertTrue(message.contains("sqlite.file: must not be null"));
assertTrue(message.contains("api.baseUrl: must not be null"));
assertTrue(message.contains("api.model: must not be null or blank"));
assertTrue(message.contains("prompt.template.file: must not be null"));
assertTrue(message.contains("api.timeoutSeconds: must be > 0"));
assertTrue(message.contains("max.retries.transient: must be >= 0"));
assertTrue(message.contains("max.pages: must be > 0"));
assertTrue(message.contains("max.text.characters: must be > 0"));
}
}

View File

@@ -0,0 +1 @@
# Keep directory