mateclaw/mateclaw-server/src/test/java/vip/mate/wiki/service/WikiGlobSymlinkBaseE2ETest.java
倪程伟 37e9afa9de fix(wiki): match glob against symlink-resolved scan root
The base directory is canonicalized via toRealPath before walking, so the
walked files carry the symlink-resolved prefix. The PathMatcher was built
from the literal pattern, so a symlinked base never matched and files were
silently dropped. Rebuild the glob against the resolved scan root, escaping
glob metacharacters in the base so a real directory name containing */?/{}/[]
is treated literally.
2026-06-08 20:51:03 +08:00

80 lines
3.2 KiB
Java

package vip.mate.wiki.service;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.junit.jupiter.api.Assertions.assertEquals;
/**
* Verifies glob matching works when the pattern's base directory is reached
* through a symbolic link. {@code WikiSourcePathValidator} canonicalizes the
* base with {@code toRealPath()}, so the walked files carry the symlink-resolved
* prefix; the matcher must be built against that resolved scan root rather than
* the literal pattern prefix, otherwise nothing matches and files are silently
* dropped.
*/
@SpringBootTest(
webEnvironment = SpringBootTest.WebEnvironment.NONE,
properties = {
"spring.flyway.enabled=true",
"spring.flyway.locations=classpath:db/migration/h2",
"mateclaw.feature-flag.refresh-ms=999999",
"mate.wiki.auto-process-on-upload=false"
}
)
class WikiGlobSymlinkBaseE2ETest {
@Autowired
private WikiDirectoryScanService scanService;
private static final java.util.concurrent.atomic.AtomicLong SEQ =
new java.util.concurrent.atomic.AtomicLong(System.nanoTime());
@Test
void globWithSymlinkBase_singleLevel_matches(@TempDir Path base) throws IOException {
Path realDir = Files.createDirectories(base.resolve("real"));
Files.writeString(realDir.resolve("a.txt"), "alpha");
Files.write(realDir.resolve("b.pdf"), "PDF-bytes".getBytes());
Path linkDir = base.resolve("link");
try {
Files.createSymbolicLink(linkDir, realDir);
} catch (UnsupportedOperationException | IOException e) {
return; // filesystem without symlink support — skip
}
WikiDirectoryScanService.ScanResult result =
scanService.scanDirectory(SEQ.incrementAndGet(), linkDir + "/*.txt");
// Only a.txt matches; the .pdf is excluded by the explicit *.txt pattern.
// Before the fix the symlink-resolved file prefix never matched the literal
// pattern prefix, so added would be 0.
assertEquals(1, result.added(), "glob through a symlinked base must match the .txt file");
}
@Test
void globWithSymlinkBase_recursive_matchesSubdir(@TempDir Path base) throws IOException {
Path realDir = Files.createDirectories(base.resolve("real"));
Files.createDirectories(realDir.resolve("sub"));
Files.writeString(realDir.resolve("sub/c.txt"), "charlie");
Path linkDir = base.resolve("link");
try {
Files.createSymbolicLink(linkDir, realDir);
} catch (UnsupportedOperationException | IOException e) {
return; // filesystem without symlink support — skip
}
WikiDirectoryScanService.ScanResult result =
scanService.scanDirectory(SEQ.incrementAndGet(), linkDir + "/**/*.txt");
assertEquals(1, result.added(), "recursive glob through a symlinked base must match the nested .txt file");
}
}