AppConfigValidator and AppConfigValidatorTest added and integrated into ApplicationInitializer
This commit is contained in:
parent
9ae52a9785
commit
26a253abf6
@ -3,6 +3,7 @@ package vassistent.bootstrap;
|
||||
import com.formdev.flatlaf.FlatDarkLaf;
|
||||
import vassistent.model.AppState;
|
||||
import vassistent.service.*;
|
||||
import vassistent.util.AppConfigValidator;
|
||||
import vassistent.util.ConfigLoader;
|
||||
|
||||
import javax.swing.*;
|
||||
@ -25,6 +26,8 @@ public class ApplicationInitializer {
|
||||
*/
|
||||
public static ApplicationContext initialize() {
|
||||
|
||||
AppConfigValidator.validate(config);
|
||||
|
||||
ApplicationContext context = new ApplicationContext();
|
||||
|
||||
// ===== Model State =====
|
||||
|
||||
591
src/main/java/vassistent/util/AppConfigValidator.java
Normal file
591
src/main/java/vassistent/util/AppConfigValidator.java
Normal file
@ -0,0 +1,591 @@
|
||||
package vassistent.util;
|
||||
|
||||
import java.net.URI;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.InvalidPathException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.Properties;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Validates startup configuration and fails fast on invalid values.
|
||||
*/
|
||||
public final class AppConfigValidator {
|
||||
|
||||
private static final Set<String> SUPPORTED_MQTT_SCHEMES =
|
||||
Set.of("tcp", "ssl", "ws", "wss");
|
||||
|
||||
/**
|
||||
* Utility class constructor.
|
||||
*/
|
||||
private AppConfigValidator() {}
|
||||
|
||||
/**
|
||||
* Validates all required startup configuration values.
|
||||
*
|
||||
* @param config loaded application properties
|
||||
*/
|
||||
public static void validate(Properties config) {
|
||||
Objects.requireNonNull(config, "config must not be null");
|
||||
|
||||
List<String> errors = new ArrayList<>();
|
||||
|
||||
validateRequiredMqttSettings(config, errors);
|
||||
validateCoreUiAndOutputSettings(config, errors);
|
||||
|
||||
boolean mqttSimulatorEnabled =
|
||||
readBoolean(config, errors, "mqtt_sim.enabled", false);
|
||||
boolean unrealEnabled =
|
||||
readBoolean(config, errors, "unreal.enabled", false);
|
||||
|
||||
if (mqttSimulatorEnabled) {
|
||||
validateMqttSimulatorSettings(config, errors);
|
||||
}
|
||||
|
||||
if (unrealEnabled) {
|
||||
validateUnrealSettings(config, errors);
|
||||
}
|
||||
|
||||
if (!errors.isEmpty()) {
|
||||
throw new IllegalStateException(buildValidationMessage(errors));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates mandatory MQTT runtime keys.
|
||||
*
|
||||
* @param config loaded properties
|
||||
* @param errors collected validation errors
|
||||
*/
|
||||
private static void validateRequiredMqttSettings(
|
||||
Properties config,
|
||||
List<String> errors
|
||||
) {
|
||||
validateRequiredNonBlank(config, errors, "mqtt.topic");
|
||||
validateRequiredNonBlank(config, errors, "mqtt.client.id");
|
||||
|
||||
String brokerUrl =
|
||||
validateRequiredNonBlank(config, errors, "mqtt.broker.url");
|
||||
if (brokerUrl == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
URI uri = URI.create(brokerUrl);
|
||||
String scheme = uri.getScheme();
|
||||
if (scheme == null
|
||||
|| !SUPPORTED_MQTT_SCHEMES.contains(
|
||||
scheme.toLowerCase(Locale.ROOT))) {
|
||||
errors.add(
|
||||
"Property 'mqtt.broker.url' must use one of "
|
||||
+ SUPPORTED_MQTT_SCHEMES + ": "
|
||||
+ brokerUrl
|
||||
);
|
||||
}
|
||||
|
||||
if (uri.getHost() == null || uri.getHost().isBlank()) {
|
||||
errors.add(
|
||||
"Property 'mqtt.broker.url' must include a host: "
|
||||
+ brokerUrl
|
||||
);
|
||||
}
|
||||
|
||||
int port = uri.getPort();
|
||||
if (port != -1 && (port < 1 || port > 65535)) {
|
||||
errors.add(
|
||||
"Property 'mqtt.broker.url' contains invalid port "
|
||||
+ port + ": " + brokerUrl
|
||||
);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
errors.add(
|
||||
"Property 'mqtt.broker.url' is not a valid URI: " + brokerUrl
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates UI and animation output settings.
|
||||
*
|
||||
* @param config loaded properties
|
||||
* @param errors collected validation errors
|
||||
*/
|
||||
private static void validateCoreUiAndOutputSettings(
|
||||
Properties config,
|
||||
List<String> errors
|
||||
) {
|
||||
String streamingUrl =
|
||||
validateRequiredNonBlank(config, errors, "streaming.url");
|
||||
if (streamingUrl != null) {
|
||||
try {
|
||||
URI uri = URI.create(streamingUrl);
|
||||
String scheme = uri.getScheme();
|
||||
if (scheme == null
|
||||
|| (!"http".equalsIgnoreCase(scheme)
|
||||
&& !"https".equalsIgnoreCase(scheme))) {
|
||||
errors.add(
|
||||
"Property 'streaming.url' must start with http:// or https://: "
|
||||
+ streamingUrl
|
||||
);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
errors.add(
|
||||
"Property 'streaming.url' is not a valid URI: " + streamingUrl
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
validateRequiredNonBlank(config, errors, "animation.output.path");
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates simulator settings that are required only when simulator startup is enabled.
|
||||
*
|
||||
* @param config loaded properties
|
||||
* @param errors collected validation errors
|
||||
*/
|
||||
private static void validateMqttSimulatorSettings(
|
||||
Properties config,
|
||||
List<String> errors
|
||||
) {
|
||||
validateExistingFile(config, errors, "python.path");
|
||||
validateExistingFile(config, errors, "mqtt_sim.script");
|
||||
validateRequiredNonBlank(config, errors, "mqtt_sim.broker");
|
||||
validateRequiredIntegerInRange(config, errors, "mqtt_sim.port", 1, 65535);
|
||||
validateOptionalIntegerInRange(config, errors, "mqtt_sim.qos", 0, 2);
|
||||
validateOptionalPositiveDouble(config, errors, "mqtt_sim.interval_seconds");
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates Unreal-related settings that are required only when Unreal startup is enabled.
|
||||
*
|
||||
* @param config loaded properties
|
||||
* @param errors collected validation errors
|
||||
*/
|
||||
private static void validateUnrealSettings(
|
||||
Properties config,
|
||||
List<String> errors
|
||||
) {
|
||||
validateExistingFile(config, errors, "unreal.executable");
|
||||
validateExistingFile(config, errors, "unreal.signalling_server.script");
|
||||
validateExistingFile(config, errors, "unreal.target.executable");
|
||||
validateExistingDirectory(config, errors, "unreal.target.working_dir");
|
||||
validateOptionalExistingDirectory(config, errors, "unreal.pid.dir");
|
||||
validateOptionalNonNegativeInteger(config, errors, "unreal.startup.delay.seconds");
|
||||
validateOptionalPidFileParent(config, errors, "unreal.pid.file");
|
||||
validateOptionalPidFileParent(config, errors, "unreal.signalling.pid.file");
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that a required property exists and is not blank.
|
||||
*
|
||||
* @param config loaded properties
|
||||
* @param errors collected validation errors
|
||||
* @param key property key
|
||||
* @return normalized value or {@code null} when missing/blank
|
||||
*/
|
||||
private static String validateRequiredNonBlank(
|
||||
Properties config,
|
||||
List<String> errors,
|
||||
String key
|
||||
) {
|
||||
String value = normalizeConfigValue(config.getProperty(key));
|
||||
if (value == null || value.isBlank()) {
|
||||
errors.add("Missing required property '" + key + "'.");
|
||||
return null;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that a property points to an existing regular file.
|
||||
*
|
||||
* @param config loaded properties
|
||||
* @param errors collected validation errors
|
||||
* @param key property key
|
||||
*/
|
||||
private static void validateExistingFile(
|
||||
Properties config,
|
||||
List<String> errors,
|
||||
String key
|
||||
) {
|
||||
String rawValue = config.getProperty(key);
|
||||
String normalized = validateRequiredNonBlank(config, errors, key);
|
||||
if (normalized == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Path path = parsePath(rawValue, normalized, key, errors);
|
||||
if (path == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Files.isRegularFile(path)) {
|
||||
errors.add(
|
||||
"Property '" + key + "' must reference an existing file: " + normalized
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that a property points to an existing directory.
|
||||
*
|
||||
* @param config loaded properties
|
||||
* @param errors collected validation errors
|
||||
* @param key property key
|
||||
*/
|
||||
private static void validateExistingDirectory(
|
||||
Properties config,
|
||||
List<String> errors,
|
||||
String key
|
||||
) {
|
||||
String rawValue = config.getProperty(key);
|
||||
String normalized = validateRequiredNonBlank(config, errors, key);
|
||||
if (normalized == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Path path = parsePath(rawValue, normalized, key, errors);
|
||||
if (path == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Files.isDirectory(path)) {
|
||||
errors.add(
|
||||
"Property '" + key + "' must reference an existing directory: " + normalized
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates an optional directory property when a value is provided.
|
||||
*
|
||||
* @param config loaded properties
|
||||
* @param errors collected validation errors
|
||||
* @param key property key
|
||||
*/
|
||||
private static void validateOptionalExistingDirectory(
|
||||
Properties config,
|
||||
List<String> errors,
|
||||
String key
|
||||
) {
|
||||
String rawValue = config.getProperty(key);
|
||||
String normalized = normalizeConfigValue(rawValue);
|
||||
if (normalized == null || normalized.isBlank()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Path path = parsePath(rawValue, normalized, key, errors);
|
||||
if (path == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Files.isDirectory(path)) {
|
||||
errors.add(
|
||||
"Property '" + key + "' must reference an existing directory: " + normalized
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates an optional integer property with a minimum/maximum range.
|
||||
*
|
||||
* @param config loaded properties
|
||||
* @param errors collected validation errors
|
||||
* @param key property key
|
||||
* @param min minimum valid value
|
||||
* @param max maximum valid value
|
||||
*/
|
||||
private static void validateOptionalIntegerInRange(
|
||||
Properties config,
|
||||
List<String> errors,
|
||||
String key,
|
||||
int min,
|
||||
int max
|
||||
) {
|
||||
String value = normalizeConfigValue(config.getProperty(key));
|
||||
if (value == null || value.isBlank()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
int parsed = Integer.parseInt(value);
|
||||
if (parsed < min || parsed > max) {
|
||||
errors.add(
|
||||
"Property '" + key + "' must be between "
|
||||
+ min + " and " + max + ", but was " + parsed + "."
|
||||
);
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
errors.add(
|
||||
"Property '" + key + "' must be an integer, but was '" + value + "'."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates a required integer property with a minimum/maximum range.
|
||||
*
|
||||
* @param config loaded properties
|
||||
* @param errors collected validation errors
|
||||
* @param key property key
|
||||
* @param min minimum valid value
|
||||
* @param max maximum valid value
|
||||
*/
|
||||
private static void validateRequiredIntegerInRange(
|
||||
Properties config,
|
||||
List<String> errors,
|
||||
String key,
|
||||
int min,
|
||||
int max
|
||||
) {
|
||||
String value = validateRequiredNonBlank(config, errors, key);
|
||||
if (value == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
int parsed = Integer.parseInt(value);
|
||||
if (parsed < min || parsed > max) {
|
||||
errors.add(
|
||||
"Property '" + key + "' must be between "
|
||||
+ min + " and " + max + ", but was " + parsed + "."
|
||||
);
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
errors.add(
|
||||
"Property '" + key + "' must be an integer, but was '" + value + "'."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates an optional numeric property that must be greater than zero.
|
||||
*
|
||||
* @param config loaded properties
|
||||
* @param errors collected validation errors
|
||||
* @param key property key
|
||||
*/
|
||||
private static void validateOptionalPositiveDouble(
|
||||
Properties config,
|
||||
List<String> errors,
|
||||
String key
|
||||
) {
|
||||
String value = normalizeConfigValue(config.getProperty(key));
|
||||
if (value == null || value.isBlank()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
double parsed = Double.parseDouble(value);
|
||||
if (parsed <= 0.0) {
|
||||
errors.add(
|
||||
"Property '" + key + "' must be greater than 0, but was " + parsed + "."
|
||||
);
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
errors.add(
|
||||
"Property '" + key + "' must be numeric, but was '" + value + "'."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates an optional integer that must be non-negative.
|
||||
*
|
||||
* @param config loaded properties
|
||||
* @param errors collected validation errors
|
||||
* @param key property key
|
||||
*/
|
||||
private static void validateOptionalNonNegativeInteger(
|
||||
Properties config,
|
||||
List<String> errors,
|
||||
String key
|
||||
) {
|
||||
String value = normalizeConfigValue(config.getProperty(key));
|
||||
if (value == null || value.isBlank()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
int parsed = Integer.parseInt(value);
|
||||
if (parsed < 0) {
|
||||
errors.add(
|
||||
"Property '" + key + "' must be >= 0, but was " + parsed + "."
|
||||
);
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
errors.add(
|
||||
"Property '" + key + "' must be an integer, but was '" + value + "'."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that an optional PID file path has an existing parent directory.
|
||||
*
|
||||
* @param config loaded properties
|
||||
* @param errors collected validation errors
|
||||
* @param key property key
|
||||
*/
|
||||
private static void validateOptionalPidFileParent(
|
||||
Properties config,
|
||||
List<String> errors,
|
||||
String key
|
||||
) {
|
||||
String rawValue = config.getProperty(key);
|
||||
String normalized = normalizeConfigValue(rawValue);
|
||||
|
||||
if (normalized == null || normalized.isBlank()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Path path = parsePath(rawValue, normalized, key, errors);
|
||||
if (path == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Path parent = path.getParent();
|
||||
if (parent != null && !Files.isDirectory(parent)) {
|
||||
errors.add(
|
||||
"Parent directory for '" + key + "' does not exist: " + parent
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates a boolean property using strict true/false values.
|
||||
*
|
||||
* @param config loaded properties
|
||||
* @param errors collected validation errors
|
||||
* @param key property key
|
||||
* @param defaultValue fallback when value is absent/blank
|
||||
* @return parsed boolean value or default value
|
||||
*/
|
||||
private static boolean readBoolean(
|
||||
Properties config,
|
||||
List<String> errors,
|
||||
String key,
|
||||
boolean defaultValue
|
||||
) {
|
||||
String value = normalizeConfigValue(config.getProperty(key));
|
||||
if (value == null || value.isBlank()) {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
if ("true".equalsIgnoreCase(value)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ("false".equalsIgnoreCase(value)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
errors.add(
|
||||
"Property '" + key + "' must be 'true' or 'false', but was '" + value + "'."
|
||||
);
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a path value and reports invalid quoting/path syntax.
|
||||
*
|
||||
* @param rawValue raw property value
|
||||
* @param normalized normalized property value
|
||||
* @param key property key
|
||||
* @param errors collected validation errors
|
||||
* @return parsed path or {@code null} when invalid
|
||||
*/
|
||||
private static Path parsePath(
|
||||
String rawValue,
|
||||
String normalized,
|
||||
String key,
|
||||
List<String> errors
|
||||
) {
|
||||
if (hasMismatchedWrappingQuote(rawValue)) {
|
||||
errors.add(
|
||||
"Property '" + key + "' has mismatched wrapping quotes: " + rawValue
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
return Path.of(normalized);
|
||||
} catch (InvalidPathException e) {
|
||||
errors.add(
|
||||
"Property '" + key + "' contains an invalid path: " + normalized
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Detects mismatched leading/trailing single or double quote wrappers.
|
||||
*
|
||||
* @param rawValue raw property value
|
||||
* @return {@code true} when wrapper quotes are mismatched
|
||||
*/
|
||||
private static boolean hasMismatchedWrappingQuote(String rawValue) {
|
||||
if (rawValue == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String trimmed = rawValue.trim();
|
||||
if (trimmed.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
boolean startsDouble = trimmed.startsWith("\"");
|
||||
boolean endsDouble = trimmed.endsWith("\"");
|
||||
|
||||
boolean startsSingle = trimmed.startsWith("'");
|
||||
boolean endsSingle = trimmed.endsWith("'");
|
||||
|
||||
return startsDouble != endsDouble || startsSingle != endsSingle;
|
||||
}
|
||||
|
||||
/**
|
||||
* Trims config values and removes matching single/double quote wrappers.
|
||||
*
|
||||
* @param value raw config value
|
||||
* @return normalized value or {@code null}
|
||||
*/
|
||||
private static String normalizeConfigValue(String value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String trimmed = value.trim();
|
||||
|
||||
if ((trimmed.startsWith("\"") && trimmed.endsWith("\""))
|
||||
|| (trimmed.startsWith("'") && trimmed.endsWith("'"))) {
|
||||
return trimmed.substring(1, trimmed.length() - 1);
|
||||
}
|
||||
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a single human-readable validation exception message.
|
||||
*
|
||||
* @param errors collected validation errors
|
||||
* @return formatted message
|
||||
*/
|
||||
private static String buildValidationMessage(List<String> errors) {
|
||||
StringBuilder message = new StringBuilder(
|
||||
"Configuration validation failed (" + errors.size() + " issue(s)):"
|
||||
);
|
||||
|
||||
for (int i = 0; i < errors.size(); i++) {
|
||||
message.append(System.lineSeparator())
|
||||
.append(i + 1)
|
||||
.append(". ")
|
||||
.append(errors.get(i));
|
||||
}
|
||||
|
||||
return message.toString();
|
||||
}
|
||||
}
|
||||
155
src/test/java/vassistent/util/AppConfigValidatorTest.java
Normal file
155
src/test/java/vassistent/util/AppConfigValidatorTest.java
Normal file
@ -0,0 +1,155 @@
|
||||
package vassistent.util;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Properties;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link AppConfigValidator}.
|
||||
*/
|
||||
class AppConfigValidatorTest {
|
||||
|
||||
@TempDir
|
||||
Path tempDir;
|
||||
|
||||
/**
|
||||
* Verifies that a minimal core configuration passes validation.
|
||||
*/
|
||||
@Test
|
||||
void validate_withCoreSettingsOnly_doesNotThrow() {
|
||||
Properties config = baseConfig();
|
||||
|
||||
assertDoesNotThrow(() -> AppConfigValidator.validate(config));
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies strict boolean validation for feature toggles.
|
||||
*/
|
||||
@Test
|
||||
void validate_withInvalidBooleanToggle_throws() {
|
||||
Properties config = baseConfig();
|
||||
config.setProperty("mqtt_sim.enabled", "yes");
|
||||
|
||||
IllegalStateException exception =
|
||||
assertThrows(
|
||||
IllegalStateException.class,
|
||||
() -> AppConfigValidator.validate(config)
|
||||
);
|
||||
|
||||
assertTrue(exception.getMessage().contains("mqtt_sim.enabled"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies simulator-specific numeric validation when simulator is enabled.
|
||||
*
|
||||
* @throws Exception if test files cannot be created
|
||||
*/
|
||||
@Test
|
||||
void validate_withMqttSimulatorInvalidPort_throws() throws Exception {
|
||||
Properties config = baseConfig();
|
||||
Path pythonExe = Files.createFile(tempDir.resolve("python.exe"));
|
||||
Path simulatorScript = Files.createFile(tempDir.resolve("mqtt_simulator.py"));
|
||||
|
||||
config.setProperty("mqtt_sim.enabled", "true");
|
||||
config.setProperty("python.path", pythonExe.toString());
|
||||
config.setProperty("mqtt_sim.script", simulatorScript.toString());
|
||||
config.setProperty("mqtt_sim.broker", "localhost");
|
||||
config.setProperty("mqtt_sim.port", "99999");
|
||||
|
||||
IllegalStateException exception =
|
||||
assertThrows(
|
||||
IllegalStateException.class,
|
||||
() -> AppConfigValidator.validate(config)
|
||||
);
|
||||
|
||||
assertTrue(exception.getMessage().contains("mqtt_sim.port"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies Unreal validation detects paths with mismatched quote wrappers.
|
||||
*
|
||||
* @throws Exception if test files cannot be created
|
||||
*/
|
||||
@Test
|
||||
void validate_withUnrealSignallingPathTrailingQuote_throws() throws Exception {
|
||||
Properties config = baseConfig();
|
||||
Path launcherScript = Files.createFile(tempDir.resolve("start_avatar.ps1"));
|
||||
Path signallingScript = Files.createFile(tempDir.resolve("start_with_stun.bat"));
|
||||
Path unrealExe = Files.createFile(tempDir.resolve("Prototyp1.exe"));
|
||||
Path unrealWorkDir = Files.createDirectory(tempDir.resolve("Windows"));
|
||||
Path pidDir = Files.createDirectory(tempDir.resolve("pids"));
|
||||
|
||||
config.setProperty("unreal.enabled", "true");
|
||||
config.setProperty("unreal.executable", launcherScript.toString());
|
||||
config.setProperty(
|
||||
"unreal.signalling_server.script",
|
||||
signallingScript.toString() + "\""
|
||||
);
|
||||
config.setProperty("unreal.target.executable", unrealExe.toString());
|
||||
config.setProperty("unreal.target.working_dir", unrealWorkDir.toString());
|
||||
config.setProperty("unreal.pid.dir", pidDir.toString());
|
||||
|
||||
IllegalStateException exception =
|
||||
assertThrows(
|
||||
IllegalStateException.class,
|
||||
() -> AppConfigValidator.validate(config)
|
||||
);
|
||||
|
||||
assertTrue(exception.getMessage().contains("unreal.signalling_server.script"));
|
||||
assertTrue(exception.getMessage().toLowerCase().contains("quote"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies Unreal validation accepts a complete and valid local setup.
|
||||
*
|
||||
* @throws Exception if test files cannot be created
|
||||
*/
|
||||
@Test
|
||||
void validate_withUnrealEnabledAndValidPaths_doesNotThrow() throws Exception {
|
||||
Properties config = baseConfig();
|
||||
Path launcherScript = Files.createFile(tempDir.resolve("start_avatar.ps1"));
|
||||
Path signallingScript = Files.createFile(tempDir.resolve("start_with_stun.bat"));
|
||||
Path unrealExe = Files.createFile(tempDir.resolve("Prototyp1.exe"));
|
||||
Path unrealWorkDir = Files.createDirectory(tempDir.resolve("Windows"));
|
||||
Path pidDir = Files.createDirectory(tempDir.resolve("pids"));
|
||||
|
||||
config.setProperty("unreal.enabled", "true");
|
||||
config.setProperty("unreal.executable", launcherScript.toString());
|
||||
config.setProperty("unreal.signalling_server.script", signallingScript.toString());
|
||||
config.setProperty("unreal.target.executable", unrealExe.toString());
|
||||
config.setProperty("unreal.target.working_dir", unrealWorkDir.toString());
|
||||
config.setProperty("unreal.pid.dir", pidDir.toString());
|
||||
config.setProperty("unreal.startup.delay.seconds", "5");
|
||||
config.setProperty("unreal.pid.file", pidDir.resolve("unreal.pid").toString());
|
||||
config.setProperty(
|
||||
"unreal.signalling.pid.file",
|
||||
pidDir.resolve("signalling.pid").toString()
|
||||
);
|
||||
|
||||
assertDoesNotThrow(() -> AppConfigValidator.validate(config));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a baseline config with optional integrations disabled.
|
||||
*
|
||||
* @return baseline application configuration
|
||||
*/
|
||||
private Properties baseConfig() {
|
||||
Properties config = new Properties();
|
||||
config.setProperty("mqtt.topic", "PREDICTION");
|
||||
config.setProperty("mqtt.broker.url", "tcp://localhost:1883");
|
||||
config.setProperty("mqtt.client.id", "JavaClientPublisherSubscriber");
|
||||
config.setProperty("streaming.url", "http://localhost");
|
||||
config.setProperty("animation.output.path", "data/animation.json");
|
||||
config.setProperty("mqtt_sim.enabled", "false");
|
||||
config.setProperty("unreal.enabled", "false");
|
||||
return config;
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user