M3-AP-007: Startvalidierung für Quellordner testbar gemacht
This commit is contained in:
@@ -14,11 +14,57 @@ import java.util.List;
|
|||||||
* Performs mandatory field checks, numeric range validation, URI scheme validation,
|
* Performs mandatory field checks, numeric range validation, URI scheme validation,
|
||||||
* and basic path existence checks. Throws {@link InvalidStartConfigurationException}
|
* and basic path existence checks. Throws {@link InvalidStartConfigurationException}
|
||||||
* if any validation rule fails.
|
* if any validation rule fails.
|
||||||
|
* <p>
|
||||||
|
* M3/AP-007: Supports injected source folder validation for testability
|
||||||
|
* (allows mocking of platform-dependent filesystem checks).
|
||||||
*/
|
*/
|
||||||
public class StartConfigurationValidator {
|
public class StartConfigurationValidator {
|
||||||
|
|
||||||
private static final Logger LOG = LogManager.getLogger(StartConfigurationValidator.class);
|
private static final Logger LOG = LogManager.getLogger(StartConfigurationValidator.class);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abstraction for source folder existence, type, and readability checks.
|
||||||
|
* <p>
|
||||||
|
* Separates filesystem operations from validation logic to enable
|
||||||
|
* platform-independent unit testing (mocking) of readability edge cases.
|
||||||
|
* <p>
|
||||||
|
* Implementation note: The default implementation uses {@code java.nio.file.Files}
|
||||||
|
* static methods directly; tests can substitute alternative implementations.
|
||||||
|
*/
|
||||||
|
@FunctionalInterface
|
||||||
|
public interface SourceFolderChecker {
|
||||||
|
/**
|
||||||
|
* Checks source folder and returns validation error message, or null if valid.
|
||||||
|
* <p>
|
||||||
|
* Checks (in order):
|
||||||
|
* 1. Folder exists
|
||||||
|
* 2. Is a directory
|
||||||
|
* 3. Is readable
|
||||||
|
*
|
||||||
|
* @param path the source folder path
|
||||||
|
* @return error message string, or null if all checks pass
|
||||||
|
*/
|
||||||
|
String checkSourceFolder(Path path);
|
||||||
|
}
|
||||||
|
|
||||||
|
private final SourceFolderChecker sourceFolderChecker;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a validator with the default source folder checker (NIO-based).
|
||||||
|
*/
|
||||||
|
public StartConfigurationValidator() {
|
||||||
|
this(new DefaultSourceFolderChecker());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a validator with a custom source folder checker (primarily for testing).
|
||||||
|
*
|
||||||
|
* @param sourceFolderChecker the checker to use (must not be null)
|
||||||
|
*/
|
||||||
|
public StartConfigurationValidator(SourceFolderChecker sourceFolderChecker) {
|
||||||
|
this.sourceFolderChecker = sourceFolderChecker;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validates the given configuration.
|
* Validates the given configuration.
|
||||||
* <p>
|
* <p>
|
||||||
@@ -66,12 +112,9 @@ public class StartConfigurationValidator {
|
|||||||
errors.add("- source.folder: must not be null");
|
errors.add("- source.folder: must not be null");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!Files.exists(sourceFolder)) {
|
String checkError = sourceFolderChecker.checkSourceFolder(sourceFolder);
|
||||||
errors.add("- source.folder: path does not exist: " + sourceFolder);
|
if (checkError != null) {
|
||||||
} else if (!Files.isDirectory(sourceFolder)) {
|
errors.add(checkError);
|
||||||
errors.add("- source.folder: path is not a directory: " + sourceFolder);
|
|
||||||
} else if (!Files.isReadable(sourceFolder)) {
|
|
||||||
errors.add("- source.folder: directory is not readable: " + sourceFolder);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -197,4 +240,29 @@ public class StartConfigurationValidator {
|
|||||||
// If it doesn't exist yet, that's acceptable - we don't auto-create
|
// If it doesn't exist yet, that's acceptable - we don't auto-create
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default NIO-based implementation of {@link SourceFolderChecker}.
|
||||||
|
* <p>
|
||||||
|
* Uses {@code java.nio.file.Files} static methods to check existence, type, and readability.
|
||||||
|
* <p>
|
||||||
|
* M3/AP-007: This separation allows unit tests to inject alternative implementations
|
||||||
|
* that control the outcome of readability checks without relying on actual filesystem
|
||||||
|
* permissions (which are platform-dependent).
|
||||||
|
*/
|
||||||
|
private static class DefaultSourceFolderChecker implements SourceFolderChecker {
|
||||||
|
@Override
|
||||||
|
public String checkSourceFolder(Path path) {
|
||||||
|
if (!Files.exists(path)) {
|
||||||
|
return "- source.folder: path does not exist: " + path;
|
||||||
|
}
|
||||||
|
if (!Files.isDirectory(path)) {
|
||||||
|
return "- source.folder: path is not a directory: " + path;
|
||||||
|
}
|
||||||
|
if (!Files.isReadable(path)) {
|
||||||
|
return "- source.folder: directory is not readable: " + path;
|
||||||
|
}
|
||||||
|
return null; // All checks passed
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -683,4 +683,153 @@ class StartConfigurationValidatorTest {
|
|||||||
assertTrue(message.contains("max.pages: must be > 0"));
|
assertTrue(message.contains("max.pages: must be > 0"));
|
||||||
assertTrue(message.contains("max.text.characters: must be > 0"));
|
assertTrue(message.contains("max.text.characters: must be > 0"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* M3/AP-007: Focused tests for source folder validation using mocked filesystem checks.
|
||||||
|
* <p>
|
||||||
|
* These tests verify the four critical paths for source folder validation without
|
||||||
|
* relying on platform-dependent filesystem permissions or the actual FS state.
|
||||||
|
*/
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void validate_failsWhenSourceFolderDoesNotExist_mocked() 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"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Mock: always return "does not exist" error for any path
|
||||||
|
StartConfigurationValidator.SourceFolderChecker mockChecker = path ->
|
||||||
|
"- source.folder: path does not exist: " + path;
|
||||||
|
|
||||||
|
StartConfigurationValidator validatorWithMock = new StartConfigurationValidator(mockChecker);
|
||||||
|
|
||||||
|
InvalidStartConfigurationException exception = assertThrows(
|
||||||
|
InvalidStartConfigurationException.class,
|
||||||
|
() -> validatorWithMock.validate(config)
|
||||||
|
);
|
||||||
|
assertTrue(exception.getMessage().contains("source.folder: path does not exist"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void validate_failsWhenSourceFolderIsNotADirectory_mocked() 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("somepath"),
|
||||||
|
targetFolder,
|
||||||
|
sqliteFile,
|
||||||
|
URI.create("https://api.example.com"),
|
||||||
|
"gpt-4",
|
||||||
|
30,
|
||||||
|
3,
|
||||||
|
100,
|
||||||
|
50000,
|
||||||
|
promptTemplateFile,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
"INFO",
|
||||||
|
"test-api-key"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Mock: simulate path exists but is not a directory
|
||||||
|
StartConfigurationValidator.SourceFolderChecker mockChecker = path ->
|
||||||
|
"- source.folder: path is not a directory: " + path;
|
||||||
|
|
||||||
|
StartConfigurationValidator validatorWithMock = new StartConfigurationValidator(mockChecker);
|
||||||
|
|
||||||
|
InvalidStartConfigurationException exception = assertThrows(
|
||||||
|
InvalidStartConfigurationException.class,
|
||||||
|
() -> validatorWithMock.validate(config)
|
||||||
|
);
|
||||||
|
assertTrue(exception.getMessage().contains("source.folder: path is not a directory"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void validate_failsWhenSourceFolderIsNotReadable_mocked() 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("somepath"),
|
||||||
|
targetFolder,
|
||||||
|
sqliteFile,
|
||||||
|
URI.create("https://api.example.com"),
|
||||||
|
"gpt-4",
|
||||||
|
30,
|
||||||
|
3,
|
||||||
|
100,
|
||||||
|
50000,
|
||||||
|
promptTemplateFile,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
"INFO",
|
||||||
|
"test-api-key"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Mock: simulate path exists, is directory, but is not readable
|
||||||
|
// This is the critical M3/AP-007 case that is hard to test on actual FS
|
||||||
|
StartConfigurationValidator.SourceFolderChecker mockChecker = path ->
|
||||||
|
"- source.folder: directory is not readable: " + path;
|
||||||
|
|
||||||
|
StartConfigurationValidator validatorWithMock = new StartConfigurationValidator(mockChecker);
|
||||||
|
|
||||||
|
InvalidStartConfigurationException exception = assertThrows(
|
||||||
|
InvalidStartConfigurationException.class,
|
||||||
|
() -> validatorWithMock.validate(config)
|
||||||
|
);
|
||||||
|
assertTrue(exception.getMessage().contains("source.folder: directory is not readable"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void validate_succeedsWhenSourceFolderIsValid_mocked() 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,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
"INFO",
|
||||||
|
"test-api-key"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Mock: all checks pass (return null)
|
||||||
|
StartConfigurationValidator.SourceFolderChecker mockChecker = path -> null;
|
||||||
|
|
||||||
|
StartConfigurationValidator validatorWithMock = new StartConfigurationValidator(mockChecker);
|
||||||
|
|
||||||
|
assertDoesNotThrow(() -> validatorWithMock.validate(config),
|
||||||
|
"Validation should succeed when source folder checker returns null");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user