|
|
|
|
@@ -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;
|
|
|
|
|
}
|
|
|
|
|
}
|