2026-10-17 19:12:43 +02:00
using AIStudio.Tools ;
namespace AIStudio.Tests.Tools ;
public sealed class DirectoryFileTraversalTests
{
[Test]
public async Task HiddenFilesAndFoldersAreLeftOutByDefaultAndCanBeIncluded ()
{
var directoryPath = Path . Combine ( Path . GetTempPath (), $"ai-studio-hidden-folders-{Guid.NewGuid():N}" );
var hiddenDirectoryPath = Path . Combine ( directoryPath , ".hidden" );
var visibleFilePath = Path . Combine ( directoryPath , "notes.md" );
var hiddenFilePath = Path . Combine ( directoryPath , ".env" );
var fileInHiddenDirectoryPath = Path . Combine ( hiddenDirectoryPath , "notes.txt" );
Directory . CreateDirectory ( hiddenDirectoryPath );
try
{
await File . WriteAllTextAsync ( visibleFilePath , "Visible notes" );
await File . WriteAllTextAsync ( hiddenFilePath , "API_KEY=secret" );
await File . WriteAllTextAsync ( fileInHiddenDirectoryPath , "Hidden notes" );
var visibleOnly = DirectoryFileTraversal . EnumerateFiles ( directoryPath , true , false ). ToList ();
var includingHidden = DirectoryFileTraversal . EnumerateFiles ( directoryPath , true , true ). ToList ();
2026-11-01 20:32:16 +01:00
using ( Assert . EnterMultipleScope ())
2026-10-17 19:12:43 +02:00
{
Assert . That ( visibleOnly , Is . EqualTo ( new [] { visibleFilePath }), "Like * in a shell, nothing hidden is found unless asked for." );
Assert . That ( includingHidden , Is . EquivalentTo ( new [] { visibleFilePath , hiddenFilePath , fileInHiddenDirectoryPath }));
2026-11-01 20:32:16 +01:00
}
2026-10-17 19:12:43 +02:00
}
finally
{
Directory . Delete ( directoryPath , true );
}
}
[Test]
public async Task APatternWhichBeginsWithADotSelectsHiddenFilesButEntersNoHiddenFolder ()
{
var directoryPath = Path . Combine ( Path . GetTempPath (), $"ai-studio-hidden-pattern-{Guid.NewGuid():N}" );
var hiddenDirectoryPath = Path . Combine ( directoryPath , ".github" );
var gitignorePath = Path . Combine ( directoryPath , ".gitignore" );
var environmentPath = Path . Combine ( directoryPath , ".env" );
var fileInHiddenDirectoryPath = Path . Combine ( hiddenDirectoryPath , ".gitignore" );
Directory . CreateDirectory ( hiddenDirectoryPath );
try
{
await File . WriteAllTextAsync ( gitignorePath , "bin/" );
await File . WriteAllTextAsync ( environmentPath , "API_KEY=secret" );
await File . WriteAllTextAsync ( fileInHiddenDirectoryPath , "obj/" );
var patterns = FileNamePatterns . Parse ( "*;.gitignore" );
var files = DirectoryFileTraversal . EnumerateFiles (
directoryPath ,
true ,
false ,
includeHiddenFile : filePath => FileNamePatterns . NamesHiddenFile ( filePath , patterns )). ToList ();
Assert . That ( files , Is . EqualTo ( new [] { gitignorePath }), "The pattern .gitignore names that file; * names no hidden file, and no pattern opens a hidden folder." );
}
finally
{
Directory . Delete ( directoryPath , true );
}
}
[Test]
public async Task AFileHiddenByTheOperatingSystemIsLeftOutAsWell ()
{
if ( OperatingSystem . IsLinux ())
Assert . Ignore ( "Linux hides files by their names only." );
var directoryPath = Path . Combine ( Path . GetTempPath (), $"ai-studio-hidden-attribute-{Guid.NewGuid():N}" );
var hiddenFilePath = Path . Combine ( directoryPath , "notes.txt" );
Directory . CreateDirectory ( directoryPath );
try
{
await File . WriteAllTextAsync ( hiddenFilePath , "Hidden notes" );
File . SetAttributes ( hiddenFilePath , File . GetAttributes ( hiddenFilePath ) | FileAttributes . Hidden );
if (! File . GetAttributes ( hiddenFilePath ). HasFlag ( FileAttributes . Hidden ))
Assert . Ignore ( "The file system of the temporary folder cannot hide files." );
2026-11-01 20:32:16 +01:00
using ( Assert . EnterMultipleScope ())
2026-10-17 19:12:43 +02:00
{
Assert . That ( DirectoryFileTraversal . EnumerateFiles ( directoryPath , false , false ), Is . Empty );
Assert . That ( DirectoryFileTraversal . EnumerateFiles ( directoryPath , false , true ), Is . EqualTo ( new [] { hiddenFilePath }));
Assert . That ( DirectoryFileTraversal . IsHiddenPath ( hiddenFilePath ), Is . True );
2026-11-01 20:32:16 +01:00
}
2026-10-17 19:12:43 +02:00
}
finally
{
File . SetAttributes ( hiddenFilePath , FileAttributes . Normal );
Directory . Delete ( directoryPath , true );
}
}
[Test]
public async Task AnExplicitlySelectedHiddenRootIsStillRead ()
{
var parentPath = Path . Combine ( Path . GetTempPath (), $"ai-studio-hidden-root-{Guid.NewGuid():N}" );
var hiddenRootPath = Path . Combine ( parentPath , ".selected" );
var filePath = Path . Combine ( hiddenRootPath , "notes.txt" );
Directory . CreateDirectory ( hiddenRootPath );
try
{
await File . WriteAllTextAsync ( filePath , "Selected hidden root" );
Assert . That ( DirectoryFileTraversal . EnumerateFiles ( hiddenRootPath , true , false ), Does . Contain ( filePath ));
}
finally
{
Directory . Delete ( parentPath , true );
}
}
[Test]
public async Task DeepLongPathsAndUnusualFileNamesRemainDiscoverable ()
{
var rootPath = Path . Combine ( Path . GetTempPath (), $"ai-studio-long-path-{Guid.NewGuid():N}" );
var directoryPath = rootPath ;
var depth = 0 ;
while ( directoryPath . Length < 320 )
{
directoryPath = Path . Combine ( directoryPath , $"nested-{depth++}-{new string('x', 32)}" );
}
var filePath = Path . Combine ( directoryPath , "Dockerfile.開発 (final) [β] #1" );
try
{
Directory . CreateDirectory ( directoryPath );
await File . WriteAllTextAsync ( filePath , "FROM alpine:latest\nRUN echo 'Grüße'" );
var discoveredFiles = DirectoryFileTraversal . EnumerateFiles ( rootPath , true , false ). ToList ();
2026-11-01 20:32:16 +01:00
using ( Assert . EnterMultipleScope ())
2026-10-17 19:12:43 +02:00
{
Assert . That ( filePath . Length , Is . GreaterThan ( 300 ));
Assert . That ( discoveredFiles , Does . Contain ( filePath ));
Assert . That ( FileNamePatterns . Matches ( filePath , FileNamePatterns . Parse ( "Dockerfile*" )), Is . True );
Assert . That ( FileNamePatterns . Matches ( filePath , FileNamePatterns . Parse ( ":text:" )), Is . True );
2026-11-01 20:32:16 +01:00
}
2026-10-17 19:12:43 +02:00
}
finally
{
if ( Directory . Exists ( rootPath ))
Directory . Delete ( rootPath , true );
}
}
}