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 @@
# Keep directory

View File

@@ -0,0 +1,295 @@
package de.gecheckt.pdf.umbenenner.adapter.outbound.configuration;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.io.FileWriter;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.function.Function;
import static org.junit.jupiter.api.Assertions.*;
/**
* Unit tests for {@link PropertiesConfigurationPortAdapter}.
* <p>
* Tests cover valid configuration loading, missing mandatory properties,
* invalid property values, and API-key environment variable precedence.
*/
class PropertiesConfigurationPortAdapterTest {
private Function<String, String> emptyEnvLookup;
@TempDir
Path tempDir;
@BeforeEach
void setUp() {
emptyEnvLookup = key -> null;
}
@Test
void loadConfiguration_successWithValidProperties() throws Exception {
Path configFile = createConfigFile("valid-config.properties");
PropertiesConfigurationPortAdapter adapter = new PropertiesConfigurationPortAdapter(emptyEnvLookup, configFile);
var config = adapter.loadConfiguration();
assertNotNull(config);
// Use endsWith to handle platform-specific path separators
assertTrue(config.sourceFolder().toString().endsWith("source"));
assertTrue(config.targetFolder().toString().endsWith("target"));
assertTrue(config.sqliteFile().toString().endsWith("db.sqlite"));
assertEquals("https://api.example.com", config.apiBaseUrl().toString());
assertEquals("gpt-4", config.apiModel());
assertEquals(30, config.apiTimeoutSeconds());
assertEquals(3, config.maxRetriesTransient());
assertEquals(100, config.maxPages());
assertEquals(50000, config.maxTextCharacters());
assertTrue(config.promptTemplateFile().toString().endsWith("prompt.txt"));
assertTrue(config.runtimeLockFile().toString().endsWith("lock.lock"));
assertTrue(config.logDirectory().toString().endsWith("logs"));
assertEquals("DEBUG", config.logLevel());
assertEquals("test-api-key-from-properties", config.apiKey());
}
@Test
void loadConfiguration_usesPropertiesApiKeyWhenEnvVarIsAbsent() throws Exception {
Path configFile = createConfigFile("no-api-key.properties");
PropertiesConfigurationPortAdapter adapter = new PropertiesConfigurationPortAdapter(emptyEnvLookup, configFile);
var config = adapter.loadConfiguration();
assertEquals("", config.apiKey(), "API key should be empty when not in properties and no env var");
}
@Test
void loadConfiguration_usesPropertiesApiKeyWhenEnvVarIsNull() throws Exception {
Path configFile = createConfigFile("no-api-key.properties");
Function<String, String> envLookup = key -> null;
PropertiesConfigurationPortAdapter adapter = new PropertiesConfigurationPortAdapter(envLookup, configFile);
var config = adapter.loadConfiguration();
assertEquals("", config.apiKey());
}
@Test
void loadConfiguration_usesPropertiesApiKeyWhenEnvVarIsEmpty() throws Exception {
Path configFile = createConfigFile("no-api-key.properties");
Function<String, String> envLookup = key -> "";
PropertiesConfigurationPortAdapter adapter = new PropertiesConfigurationPortAdapter(envLookup, configFile);
var config = adapter.loadConfiguration();
assertEquals("", config.apiKey(), "Empty env var should fall back to empty string");
}
@Test
void loadConfiguration_usesPropertiesApiKeyWhenEnvVarIsBlank() throws Exception {
Path configFile = createConfigFile("no-api-key.properties");
Function<String, String> envLookup = key -> " ";
PropertiesConfigurationPortAdapter adapter = new PropertiesConfigurationPortAdapter(envLookup, configFile);
var config = adapter.loadConfiguration();
assertEquals("", config.apiKey(), "Blank env var should fall back to empty string");
}
@Test
void loadConfiguration_envVarOverridesPropertiesApiKey() throws Exception {
Path configFile = createConfigFile("valid-config.properties");
Function<String, String> envLookup = key -> {
if ("PDF_UMBENENNER_API_KEY".equals(key)) {
return "env-api-key-override";
}
return null;
};
PropertiesConfigurationPortAdapter adapter = new PropertiesConfigurationPortAdapter(envLookup, configFile);
var config = adapter.loadConfiguration();
assertEquals("env-api-key-override", config.apiKey(), "Environment variable should override properties");
}
@Test
void loadConfiguration_throwsIllegalStateExceptionWhenRequiredPropertyMissing() throws Exception {
Path configFile = createConfigFile("missing-required.properties");
PropertiesConfigurationPortAdapter adapter = new PropertiesConfigurationPortAdapter(emptyEnvLookup, configFile);
IllegalStateException exception = assertThrows(
IllegalStateException.class,
() -> adapter.loadConfiguration()
);
assertTrue(exception.getMessage().contains("Required property missing"));
assertTrue(exception.getMessage().contains("sqlite.file"));
}
@Test
void loadConfiguration_throwsRuntimeExceptionWhenConfigFileNotFound() {
Path nonExistentFile = tempDir.resolve("nonexistent.properties");
PropertiesConfigurationPortAdapter adapter = new PropertiesConfigurationPortAdapter(emptyEnvLookup, nonExistentFile);
RuntimeException exception = assertThrows(
RuntimeException.class,
() -> adapter.loadConfiguration()
);
assertTrue(exception.getMessage().contains("Failed to load configuration"));
assertTrue(exception.getCause() instanceof java.io.FileNotFoundException);
}
@Test
void loadConfiguration_parsesIntegerValuesCorrectly() throws Exception {
Path configFile = createInlineConfig(
"source.folder=/tmp/source\n" +
"target.folder=/tmp/target\n" +
"sqlite.file=/tmp/db.sqlite\n" +
"api.baseUrl=https://api.example.com\n" +
"api.model=gpt-4\n" +
"api.timeoutSeconds=60\n" +
"max.retries.transient=5\n" +
"max.pages=200\n" +
"max.text.characters=100000\n" +
"prompt.template.file=/tmp/prompt.txt\n" +
"api.key=test-key\n"
);
PropertiesConfigurationPortAdapter adapter = new PropertiesConfigurationPortAdapter(emptyEnvLookup, configFile);
var config = adapter.loadConfiguration();
assertEquals(60, config.apiTimeoutSeconds());
assertEquals(5, config.maxRetriesTransient());
assertEquals(200, config.maxPages());
assertEquals(100000, config.maxTextCharacters());
}
@Test
void loadConfiguration_handlesWhitespaceInIntegerValues() throws Exception {
Path configFile = createInlineConfig(
"source.folder=/tmp/source\n" +
"target.folder=/tmp/target\n" +
"sqlite.file=/tmp/db.sqlite\n" +
"api.baseUrl=https://api.example.com\n" +
"api.model=gpt-4\n" +
"api.timeoutSeconds= 45 \n" +
"max.retries.transient=2\n" +
"max.pages=150\n" +
"max.text.characters=75000\n" +
"prompt.template.file=/tmp/prompt.txt\n" +
"api.key=test-key\n"
);
PropertiesConfigurationPortAdapter adapter = new PropertiesConfigurationPortAdapter(emptyEnvLookup, configFile);
var config = adapter.loadConfiguration();
assertEquals(45, config.apiTimeoutSeconds(), "Whitespace should be trimmed from integer values");
}
@Test
void loadConfiguration_throwsIllegalStateExceptionForInvalidIntegerValue() throws Exception {
Path configFile = createInlineConfig(
"source.folder=/tmp/source\n" +
"target.folder=/tmp/target\n" +
"sqlite.file=/tmp/db.sqlite\n" +
"api.baseUrl=https://api.example.com\n" +
"api.model=gpt-4\n" +
"api.timeoutSeconds=not-a-number\n" +
"max.retries.transient=2\n" +
"max.pages=150\n" +
"max.text.characters=75000\n" +
"prompt.template.file=/tmp/prompt.txt\n" +
"api.key=test-key\n"
);
PropertiesConfigurationPortAdapter adapter = new PropertiesConfigurationPortAdapter(emptyEnvLookup, configFile);
IllegalStateException exception = assertThrows(
IllegalStateException.class,
() -> adapter.loadConfiguration()
);
assertTrue(exception.getMessage().contains("Invalid integer value"));
}
@Test
void loadConfiguration_parsesUriCorrectly() throws Exception {
Path configFile = createInlineConfig(
"source.folder=/tmp/source\n" +
"target.folder=/tmp/target\n" +
"sqlite.file=/tmp/db.sqlite\n" +
"api.baseUrl=https://api.example.com:8080/v1\n" +
"api.model=gpt-4\n" +
"api.timeoutSeconds=30\n" +
"max.retries.transient=3\n" +
"max.pages=100\n" +
"max.text.characters=50000\n" +
"prompt.template.file=/tmp/prompt.txt\n" +
"api.key=test-key\n"
);
PropertiesConfigurationPortAdapter adapter = new PropertiesConfigurationPortAdapter(emptyEnvLookup, configFile);
var config = adapter.loadConfiguration();
assertEquals("https://api.example.com:8080/v1", config.apiBaseUrl().toString());
}
@Test
void loadConfiguration_defaultsOptionalValuesWhenNotPresent() throws Exception {
Path configFile = createInlineConfig(
"source.folder=/tmp/source\n" +
"target.folder=/tmp/target\n" +
"sqlite.file=/tmp/db.sqlite\n" +
"api.baseUrl=https://api.example.com\n" +
"api.model=gpt-4\n" +
"api.timeoutSeconds=30\n" +
"max.retries.transient=3\n" +
"max.pages=100\n" +
"max.text.characters=50000\n" +
"prompt.template.file=/tmp/prompt.txt\n" +
"api.key=test-key\n"
);
PropertiesConfigurationPortAdapter adapter = new PropertiesConfigurationPortAdapter(emptyEnvLookup, configFile);
var config = adapter.loadConfiguration();
assertEquals("", config.runtimeLockFile().toString(), "runtime.lock.file should default to empty");
assertEquals("", config.logDirectory().toString(), "log.directory should default to empty");
assertEquals("INFO", config.logLevel(), "log.level should default to INFO");
}
private Path createConfigFile(String resourceName) throws Exception {
Path sourceResource = Path.of("src/test/resources", resourceName);
Path targetConfigFile = tempDir.resolve("application.properties");
// Copy content from resource file
Files.copy(sourceResource, targetConfigFile);
return targetConfigFile;
}
private Path createInlineConfig(String content) throws Exception {
Path configFile = tempDir.resolve("config.properties");
try (FileWriter writer = new FileWriter(configFile.toFile())) {
writer.write(content);
}
return configFile;
}
}

View File

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

View File

@@ -0,0 +1,11 @@
source.folder=/tmp/source
target.folder=/tmp/target
# sqlite.file is missing
api.baseUrl=https://api.example.com
api.model=gpt-4
api.timeoutSeconds=30
max.retries.transient=3
max.pages=100
max.text.characters=50000
prompt.template.file=/tmp/prompt.txt
api.key=test-api-key

View File

@@ -0,0 +1,10 @@
source.folder=/tmp/source
target.folder=/tmp/target
sqlite.file=/tmp/db.sqlite
api.baseUrl=https://api.example.com
api.model=gpt-4
api.timeoutSeconds=30
max.retries.transient=3
max.pages=100
max.text.characters=50000
prompt.template.file=/tmp/prompt.txt

View File

@@ -0,0 +1,14 @@
source.folder=/tmp/source
target.folder=/tmp/target
sqlite.file=/tmp/db.sqlite
api.baseUrl=https://api.example.com
api.model=gpt-4
api.timeoutSeconds=30
max.retries.transient=3
max.pages=100
max.text.characters=50000
prompt.template.file=/tmp/prompt.txt
runtime.lock.file=/tmp/lock.lock
log.directory=/tmp/logs
log.level=DEBUG
api.key=test-api-key-from-properties