M1 Vollständiger Grundstand mit Build, Konfiguration, Tests und Smoke-Tests
This commit is contained in:
@@ -0,0 +1 @@
|
||||
# Keep directory
|
||||
@@ -0,0 +1,165 @@
|
||||
package de.gecheckt.pdf.umbenenner.adapter.outbound.configuration;
|
||||
|
||||
import de.gecheckt.pdf.umbenenner.application.config.StartConfiguration;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.out.ConfigurationPort;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.StringReader;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.Properties;
|
||||
import java.util.function.Function;
|
||||
|
||||
/**
|
||||
* Properties-based implementation of {@link ConfigurationPort}.
|
||||
* AP-005: Loads configuration from config/application.properties with environment variable precedence.
|
||||
*/
|
||||
public class PropertiesConfigurationPortAdapter implements ConfigurationPort {
|
||||
|
||||
private static final Logger LOG = LogManager.getLogger(PropertiesConfigurationPortAdapter.class);
|
||||
private static final String DEFAULT_CONFIG_FILE_PATH = "config/application.properties";
|
||||
private static final String API_KEY_ENV_VAR = "PDF_UMBENENNER_API_KEY";
|
||||
|
||||
private final Function<String, String> environmentLookup;
|
||||
private final Path configFilePath;
|
||||
|
||||
/**
|
||||
* Creates the adapter with default system environment lookup and default config file path.
|
||||
*/
|
||||
public PropertiesConfigurationPortAdapter() {
|
||||
this(System::getenv, Paths.get(DEFAULT_CONFIG_FILE_PATH));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the adapter with a custom environment lookup function.
|
||||
* <p>
|
||||
* This constructor is primarily intended for testing purposes to allow deterministic
|
||||
* control over environment variable values without modifying the real process environment.
|
||||
*
|
||||
* @param environmentLookup a function that looks up environment variables by name
|
||||
*/
|
||||
public PropertiesConfigurationPortAdapter(Function<String, String> environmentLookup) {
|
||||
this(environmentLookup, Paths.get(DEFAULT_CONFIG_FILE_PATH));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the adapter with a custom config file path.
|
||||
* <p>
|
||||
* This constructor is primarily intended for testing purposes to allow loading
|
||||
* configuration from arbitrary temporary files.
|
||||
*
|
||||
* @param configFilePath the path to the configuration properties file
|
||||
*/
|
||||
public PropertiesConfigurationPortAdapter(Path configFilePath) {
|
||||
this(System::getenv, configFilePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the adapter with custom environment lookup and config file path.
|
||||
* <p>
|
||||
* This constructor is primarily intended for testing purposes.
|
||||
*
|
||||
* @param environmentLookup a function that looks up environment variables by name
|
||||
* @param configFilePath the path to the configuration properties file
|
||||
*/
|
||||
public PropertiesConfigurationPortAdapter(Function<String, String> environmentLookup, Path configFilePath) {
|
||||
this.environmentLookup = environmentLookup;
|
||||
this.configFilePath = configFilePath;
|
||||
}
|
||||
|
||||
@Override
|
||||
public StartConfiguration loadConfiguration() {
|
||||
Properties props = new Properties();
|
||||
try {
|
||||
// Check if file exists first to preserve FileNotFoundException behavior for tests
|
||||
if (!Files.exists(configFilePath)) {
|
||||
throw new java.io.FileNotFoundException("Config file not found: " + configFilePath);
|
||||
}
|
||||
// Read file content as string to avoid escape sequence interpretation issues
|
||||
String content = Files.readString(configFilePath, StandardCharsets.UTF_8);
|
||||
// Escape backslashes to prevent Java Properties from interpreting them as escape sequences
|
||||
// This is needed because Windows paths use backslashes (e.g., C:\temp\...)
|
||||
// and Java Properties interprets \t as tab, \n as newline, etc.
|
||||
String escapedContent = content.replace("\\", "\\\\");
|
||||
props.load(new StringReader(escapedContent));
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("Failed to load configuration from " + configFilePath, e);
|
||||
}
|
||||
|
||||
// Apply environment variable precedence for api.key
|
||||
String apiKey = getApiKey(props);
|
||||
|
||||
return new StartConfiguration(
|
||||
Paths.get(getRequiredProperty(props, "source.folder")),
|
||||
Paths.get(getRequiredProperty(props, "target.folder")),
|
||||
Paths.get(getRequiredProperty(props, "sqlite.file")),
|
||||
parseUri(getRequiredProperty(props, "api.baseUrl")),
|
||||
getRequiredProperty(props, "api.model"),
|
||||
parseInt(getRequiredProperty(props, "api.timeoutSeconds")),
|
||||
parseInt(getRequiredProperty(props, "max.retries.transient")),
|
||||
parseInt(getRequiredProperty(props, "max.pages")),
|
||||
parseInt(getRequiredProperty(props, "max.text.characters")),
|
||||
Paths.get(getRequiredProperty(props, "prompt.template.file")),
|
||||
Paths.get(getOptionalProperty(props, "runtime.lock.file", "")),
|
||||
Paths.get(getOptionalProperty(props, "log.directory", "")),
|
||||
getOptionalProperty(props, "log.level", "INFO"),
|
||||
apiKey
|
||||
);
|
||||
}
|
||||
|
||||
private String getApiKey(Properties props) {
|
||||
String envApiKey = environmentLookup.apply(API_KEY_ENV_VAR);
|
||||
if (envApiKey != null && !envApiKey.isBlank()) {
|
||||
LOG.info("Using API key from environment variable {}", API_KEY_ENV_VAR);
|
||||
return envApiKey;
|
||||
}
|
||||
String propsApiKey = props.getProperty("api.key");
|
||||
return propsApiKey != null ? propsApiKey : "";
|
||||
}
|
||||
|
||||
private String getRequiredProperty(Properties props, String key) {
|
||||
String value = props.getProperty(key);
|
||||
if (value == null || value.isBlank()) {
|
||||
throw new IllegalStateException("Required property missing: " + key);
|
||||
}
|
||||
return normalizePath(value.trim());
|
||||
}
|
||||
|
||||
private String getOptionalProperty(Properties props, String key, String defaultValue) {
|
||||
String value = props.getProperty(key);
|
||||
return (value == null || value.isBlank()) ? defaultValue : normalizePath(value.trim());
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes a property value that represents a path.
|
||||
* Converts backslashes to forward slashes to avoid issues with Java Properties
|
||||
* escape sequence interpretation (e.g., \t becoming tab).
|
||||
* Also trims whitespace from the value.
|
||||
*/
|
||||
private String normalizePath(String value) {
|
||||
return value.replace('\\', '/');
|
||||
}
|
||||
|
||||
private int parseInt(String value) {
|
||||
try {
|
||||
return Integer.parseInt(value.trim());
|
||||
} catch (NumberFormatException e) {
|
||||
throw new IllegalStateException("Invalid integer value for property: " + value, e);
|
||||
}
|
||||
}
|
||||
|
||||
private URI parseUri(String value) {
|
||||
try {
|
||||
return new URI(value.trim());
|
||||
} catch (URISyntaxException e) {
|
||||
throw new IllegalStateException("Invalid URI value for property: " + value, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
/**
|
||||
* Configuration adapters for outbound infrastructure access.
|
||||
* Contains implementations of configuration loading from external sources.
|
||||
*/
|
||||
package de.gecheckt.pdf.umbenenner.adapter.outbound.configuration;
|
||||
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* Outbound adapters for infrastructure interactions.
|
||||
* <p>
|
||||
* This package contains implementations of outbound ports for external systems
|
||||
* (PDF processing, database access, HTTP clients, etc.).
|
||||
* AP-003: Currently empty as no outbound implementations have been created yet.
|
||||
*/
|
||||
package de.gecheckt.pdf.umbenenner.adapter.outbound;
|
||||
1
pdf-umbenenner-adapter-out/src/test/java/.gitkeep
Normal file
1
pdf-umbenenner-adapter-out/src/test/java/.gitkeep
Normal file
@@ -0,0 +1 @@
|
||||
# Keep directory
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
1
pdf-umbenenner-adapter-out/src/test/resources/.gitkeep
Normal file
1
pdf-umbenenner-adapter-out/src/test/resources/.gitkeep
Normal file
@@ -0,0 +1 @@
|
||||
# Keep directory
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user