From 6e6fec99a471621a6b473907578f17e42b8a3aa7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Breu=C3=9F=20Valentin?= Date: Fri, 12 Sep 2025 07:45:27 +0200 Subject: [PATCH 1/3] fix: throw an `UnauthorizedAccessException` immediately when enumerating a directory without access --- .../Storage/InMemoryStorage.cs | 152 +++++++++--------- .../FileSystem/DirectoryMockTests.cs | 63 ++++++++ 2 files changed, 138 insertions(+), 77 deletions(-) create mode 100644 Tests/Testably.Abstractions.Testing.Tests/FileSystem/DirectoryMockTests.cs diff --git a/Source/Testably.Abstractions.Testing/Storage/InMemoryStorage.cs b/Source/Testably.Abstractions.Testing/Storage/InMemoryStorage.cs index d2e4aa601..f458a0e41 100644 --- a/Source/Testably.Abstractions.Testing/Storage/InMemoryStorage.cs +++ b/Source/Testably.Abstractions.Testing/Storage/InMemoryStorage.cs @@ -199,8 +199,17 @@ public IEnumerable EnumerateLocations( throw ExceptionFactory.DirectoryNotFound(location.FullPath); } - return EnumerateLocationsImpl(location, type, requestParentAccess, searchPattern, - enumerationOptions, parentContainer); + IDisposable parentAccess = new NoOpDisposable(); + if (requestParentAccess) + { + parentAccess = parentContainer.RequestAccess(FileAccess.Read, FileShare.ReadWrite); + } + + using (parentAccess) + { + return EnumerateLocationsImpl(location, type, searchPattern, + enumerationOptions); + } } /// @@ -210,99 +219,88 @@ public IEnumerable EnumerateLocations( private IEnumerable EnumerateLocationsImpl( IStorageLocation location, FileSystemTypes type, - bool requestParentAccess, string searchPattern, - EnumerationOptions? enumerationOptions, - IStorageContainer parentContainer) + EnumerationOptions? enumerationOptions) { - IDisposable parentAccess = new NoOpDisposable(); - if (requestParentAccess) + enumerationOptions ??= EnumerationOptionsHelper.Compatible; + + string fullPath = location.FullPath; + + if (enumerationOptions.MatchType == MatchType.Win32) { - parentAccess = parentContainer.RequestAccess(FileAccess.Read, FileShare.ReadWrite); + EnumerationOptionsHelper.NormalizeInputs(_fileSystem.Execute, + ref fullPath, + ref searchPattern); } - using (parentAccess) + string fullPathWithoutTrailingSlash = fullPath; + if (!fullPath.EndsWith(_fileSystem.Execute.Path.DirectorySeparatorChar)) { - enumerationOptions ??= EnumerationOptionsHelper.Compatible; - - string fullPath = location.FullPath; + fullPath += _fileSystem.Execute.Path.DirectorySeparatorChar; + } + else if (!string.Equals(_fileSystem.Execute.Path.GetPathRoot(fullPath), fullPath, + _fileSystem.Execute.StringComparisonMode)) + { + fullPathWithoutTrailingSlash = + fullPathWithoutTrailingSlash.TrimEnd( + _fileSystem.Execute.Path.DirectorySeparatorChar); + } - if (enumerationOptions.MatchType == MatchType.Win32) + if (enumerationOptions.ReturnSpecialDirectories && + type == FileSystemTypes.Directory) + { + IStorageDrive? drive = _fileSystem.Storage.GetDrive(fullPath); + if (drive == null && + !fullPath.IsUncPath(_fileSystem)) { - EnumerationOptionsHelper.NormalizeInputs(_fileSystem.Execute, - ref fullPath, - ref searchPattern); + drive = _fileSystem.Storage.MainDrive; } - string fullPathWithoutTrailingSlash = fullPath; - if (!fullPath.EndsWith(_fileSystem.Execute.Path.DirectorySeparatorChar)) - { - fullPath += _fileSystem.Execute.Path.DirectorySeparatorChar; - } - else if (!string.Equals(_fileSystem.Execute.Path.GetPathRoot(fullPath), fullPath, - _fileSystem.Execute.StringComparisonMode)) + string prefix = + location.FriendlyName.EndsWith(_fileSystem.Execute.Path.DirectorySeparatorChar) + ? location.FriendlyName + : location.FriendlyName + _fileSystem.Execute.Path.DirectorySeparatorChar; + + yield return InMemoryLocation.New(_fileSystem, drive, fullPath, + $"{prefix}."); + string? parentPath = _fileSystem.Execute.Path.GetDirectoryName( + fullPath.TrimEnd(_fileSystem.Execute.Path + .DirectorySeparatorChar)); + if (parentPath != null || !_fileSystem.Execute.IsWindows) { - fullPathWithoutTrailingSlash = - fullPathWithoutTrailingSlash.TrimEnd( - _fileSystem.Execute.Path.DirectorySeparatorChar); + yield return InMemoryLocation.New(_fileSystem, drive, parentPath ?? "/", + $"{prefix}.."); } + } - if (enumerationOptions.ReturnSpecialDirectories && - type == FileSystemTypes.Directory) + foreach (KeyValuePair item in _containers + .Where(x => x.Key.FullPath.StartsWith(fullPath, + _fileSystem.Execute.StringComparisonMode) && + !x.Key.Equals(location)) + .OrderBy(x => x.Key.FullPath)) + { + if (type.HasFlag(item.Value.Type) && + IncludeItemInEnumeration(item, fullPathWithoutTrailingSlash, + enumerationOptions)) { - IStorageDrive? drive = _fileSystem.Storage.GetDrive(fullPath); - if (drive == null && - !fullPath.IsUncPath(_fileSystem)) + string itemPath = item.Key.FullPath; + if (itemPath.EndsWith(_fileSystem.Path.DirectorySeparatorChar)) { - drive = _fileSystem.Storage.MainDrive; + itemPath = itemPath.TrimEnd(_fileSystem.Path.DirectorySeparatorChar); } - string prefix = - location.FriendlyName.EndsWith(_fileSystem.Execute.Path.DirectorySeparatorChar) - ? location.FriendlyName - : location.FriendlyName + _fileSystem.Execute.Path.DirectorySeparatorChar; - - yield return InMemoryLocation.New(_fileSystem, drive, fullPath, - $"{prefix}."); - string? parentPath = _fileSystem.Execute.Path.GetDirectoryName( - fullPath.TrimEnd(_fileSystem.Execute.Path - .DirectorySeparatorChar)); - if (parentPath != null || !_fileSystem.Execute.IsWindows) + string name = _fileSystem.Execute.Path.GetFileName(itemPath); + if (EnumerationOptionsHelper.MatchesPattern( + _fileSystem.Execute, + enumerationOptions, + name, + searchPattern) || + (_fileSystem.Execute.IsNetFramework && + SearchPatternMatchesFileExtensionOnNetFramework( + searchPattern, + _fileSystem.Execute.Path.GetExtension(name)))) { - yield return InMemoryLocation.New(_fileSystem, drive, parentPath ?? "/", - $"{prefix}.."); - } - } - - foreach (KeyValuePair item in _containers - .Where(x => x.Key.FullPath.StartsWith(fullPath, - _fileSystem.Execute.StringComparisonMode) && - !x.Key.Equals(location)) - .OrderBy(x => x.Key.FullPath)) - { - if (type.HasFlag(item.Value.Type) && - IncludeItemInEnumeration(item, fullPathWithoutTrailingSlash, - enumerationOptions)) - { - string itemPath = item.Key.FullPath; - if (itemPath.EndsWith(_fileSystem.Path.DirectorySeparatorChar)) - { - itemPath = itemPath.TrimEnd(_fileSystem.Path.DirectorySeparatorChar); - } - - string name = _fileSystem.Execute.Path.GetFileName(itemPath); - if (EnumerationOptionsHelper.MatchesPattern( - _fileSystem.Execute, - enumerationOptions, - name, - searchPattern) || - (_fileSystem.Execute.IsNetFramework && - SearchPatternMatchesFileExtensionOnNetFramework( - searchPattern, - _fileSystem.Execute.Path.GetExtension(name)))) - { - yield return item.Key; - } + yield return item.Key; } } } diff --git a/Tests/Testably.Abstractions.Testing.Tests/FileSystem/DirectoryMockTests.cs b/Tests/Testably.Abstractions.Testing.Tests/FileSystem/DirectoryMockTests.cs new file mode 100644 index 000000000..c9e60bbf5 --- /dev/null +++ b/Tests/Testably.Abstractions.Testing.Tests/FileSystem/DirectoryMockTests.cs @@ -0,0 +1,63 @@ +using Testably.Abstractions.Testing.FileSystem; + +namespace Testably.Abstractions.Testing.Tests.FileSystem; + +public class DirectoryMockTests +{ + [Fact] + public async Task + EnumerateDirectories_UnauthorizedParentAccess_ShouldThrowUnauthorizedAccessExceptionImmediately() + { + string path = "foo"; + MockFileSystem fileSystem = new(); + IDirectoryInfo sut = fileSystem.Directory.CreateDirectory(path); + fileSystem.WithAccessControlStrategy( + new DefaultAccessControlStrategy((p, _) + => !p.EndsWith(path, StringComparison.Ordinal))); + + void Act() => + _ = fileSystem.Directory.EnumerateDirectories(path); + + await That(Act).Throws() + .WithMessageContaining($"'{sut.FullName}'").And + .WithHResult(-2147024891); + } + + [Fact] + public async Task + EnumerateFiles_UnauthorizedParentAccess_ShouldThrowUnauthorizedAccessExceptionImmediately() + { + string path = "foo"; + MockFileSystem fileSystem = new(); + IDirectoryInfo sut = fileSystem.Directory.CreateDirectory(path); + fileSystem.WithAccessControlStrategy( + new DefaultAccessControlStrategy((p, _) + => !p.EndsWith(path, StringComparison.Ordinal))); + + void Act() => + _ = fileSystem.Directory.EnumerateFiles(path); + + await That(Act).Throws() + .WithMessageContaining($"'{sut.FullName}'").And + .WithHResult(-2147024891); + } + + [Fact] + public async Task + EnumerateFileSystemEntries_UnauthorizedParentAccess_ShouldThrowUnauthorizedAccessExceptionImmediately() + { + string path = "foo"; + MockFileSystem fileSystem = new(); + IDirectoryInfo sut = fileSystem.Directory.CreateDirectory(path); + fileSystem.WithAccessControlStrategy( + new DefaultAccessControlStrategy((p, _) + => !p.EndsWith(path, StringComparison.Ordinal))); + + void Act() => + _ = fileSystem.Directory.EnumerateFileSystemEntries(path); + + await That(Act).Throws() + .WithMessageContaining($"'{sut.FullName}'").And + .WithHResult(-2147024891); + } +} From a5e501ea842dd55a2ce41a8268eb2018c78b8c94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Breu=C3=9F=20Valentin?= Date: Fri, 12 Sep 2025 07:51:57 +0200 Subject: [PATCH 2/3] Move using to the enumeration --- .../Storage/InMemoryStorage.cs | 143 +++++++++--------- 1 file changed, 72 insertions(+), 71 deletions(-) diff --git a/Source/Testably.Abstractions.Testing/Storage/InMemoryStorage.cs b/Source/Testably.Abstractions.Testing/Storage/InMemoryStorage.cs index f458a0e41..b51377d15 100644 --- a/Source/Testably.Abstractions.Testing/Storage/InMemoryStorage.cs +++ b/Source/Testably.Abstractions.Testing/Storage/InMemoryStorage.cs @@ -205,11 +205,8 @@ public IEnumerable EnumerateLocations( parentAccess = parentContainer.RequestAccess(FileAccess.Read, FileShare.ReadWrite); } - using (parentAccess) - { - return EnumerateLocationsImpl(location, type, searchPattern, - enumerationOptions); - } + return EnumerateLocationsImpl(location, type, searchPattern, + enumerationOptions, parentAccess); } /// @@ -220,87 +217,91 @@ private IEnumerable EnumerateLocationsImpl( IStorageLocation location, FileSystemTypes type, string searchPattern, - EnumerationOptions? enumerationOptions) + EnumerationOptions? enumerationOptions, + IDisposable parentAccess) { - enumerationOptions ??= EnumerationOptionsHelper.Compatible; - - string fullPath = location.FullPath; - - if (enumerationOptions.MatchType == MatchType.Win32) + using (parentAccess) { - EnumerationOptionsHelper.NormalizeInputs(_fileSystem.Execute, - ref fullPath, - ref searchPattern); - } + enumerationOptions ??= EnumerationOptionsHelper.Compatible; - string fullPathWithoutTrailingSlash = fullPath; - if (!fullPath.EndsWith(_fileSystem.Execute.Path.DirectorySeparatorChar)) - { - fullPath += _fileSystem.Execute.Path.DirectorySeparatorChar; - } - else if (!string.Equals(_fileSystem.Execute.Path.GetPathRoot(fullPath), fullPath, - _fileSystem.Execute.StringComparisonMode)) - { - fullPathWithoutTrailingSlash = - fullPathWithoutTrailingSlash.TrimEnd( - _fileSystem.Execute.Path.DirectorySeparatorChar); - } + string fullPath = location.FullPath; - if (enumerationOptions.ReturnSpecialDirectories && - type == FileSystemTypes.Directory) - { - IStorageDrive? drive = _fileSystem.Storage.GetDrive(fullPath); - if (drive == null && - !fullPath.IsUncPath(_fileSystem)) + if (enumerationOptions.MatchType == MatchType.Win32) { - drive = _fileSystem.Storage.MainDrive; + EnumerationOptionsHelper.NormalizeInputs(_fileSystem.Execute, + ref fullPath, + ref searchPattern); } - string prefix = - location.FriendlyName.EndsWith(_fileSystem.Execute.Path.DirectorySeparatorChar) - ? location.FriendlyName - : location.FriendlyName + _fileSystem.Execute.Path.DirectorySeparatorChar; - - yield return InMemoryLocation.New(_fileSystem, drive, fullPath, - $"{prefix}."); - string? parentPath = _fileSystem.Execute.Path.GetDirectoryName( - fullPath.TrimEnd(_fileSystem.Execute.Path - .DirectorySeparatorChar)); - if (parentPath != null || !_fileSystem.Execute.IsWindows) + string fullPathWithoutTrailingSlash = fullPath; + if (!fullPath.EndsWith(_fileSystem.Execute.Path.DirectorySeparatorChar)) { - yield return InMemoryLocation.New(_fileSystem, drive, parentPath ?? "/", - $"{prefix}.."); + fullPath += _fileSystem.Execute.Path.DirectorySeparatorChar; + } + else if (!string.Equals(_fileSystem.Execute.Path.GetPathRoot(fullPath), fullPath, + _fileSystem.Execute.StringComparisonMode)) + { + fullPathWithoutTrailingSlash = + fullPathWithoutTrailingSlash.TrimEnd( + _fileSystem.Execute.Path.DirectorySeparatorChar); } - } - foreach (KeyValuePair item in _containers - .Where(x => x.Key.FullPath.StartsWith(fullPath, - _fileSystem.Execute.StringComparisonMode) && - !x.Key.Equals(location)) - .OrderBy(x => x.Key.FullPath)) - { - if (type.HasFlag(item.Value.Type) && - IncludeItemInEnumeration(item, fullPathWithoutTrailingSlash, - enumerationOptions)) + if (enumerationOptions.ReturnSpecialDirectories && + type == FileSystemTypes.Directory) { - string itemPath = item.Key.FullPath; - if (itemPath.EndsWith(_fileSystem.Path.DirectorySeparatorChar)) + IStorageDrive? drive = _fileSystem.Storage.GetDrive(fullPath); + if (drive == null && + !fullPath.IsUncPath(_fileSystem)) + { + drive = _fileSystem.Storage.MainDrive; + } + + string prefix = + location.FriendlyName.EndsWith(_fileSystem.Execute.Path.DirectorySeparatorChar) + ? location.FriendlyName + : location.FriendlyName + _fileSystem.Execute.Path.DirectorySeparatorChar; + + yield return InMemoryLocation.New(_fileSystem, drive, fullPath, + $"{prefix}."); + string? parentPath = _fileSystem.Execute.Path.GetDirectoryName( + fullPath.TrimEnd(_fileSystem.Execute.Path + .DirectorySeparatorChar)); + if (parentPath != null || !_fileSystem.Execute.IsWindows) { - itemPath = itemPath.TrimEnd(_fileSystem.Path.DirectorySeparatorChar); + yield return InMemoryLocation.New(_fileSystem, drive, parentPath ?? "/", + $"{prefix}.."); } + } - string name = _fileSystem.Execute.Path.GetFileName(itemPath); - if (EnumerationOptionsHelper.MatchesPattern( - _fileSystem.Execute, - enumerationOptions, - name, - searchPattern) || - (_fileSystem.Execute.IsNetFramework && - SearchPatternMatchesFileExtensionOnNetFramework( - searchPattern, - _fileSystem.Execute.Path.GetExtension(name)))) + foreach (KeyValuePair item in _containers + .Where(x => x.Key.FullPath.StartsWith(fullPath, + _fileSystem.Execute.StringComparisonMode) && + !x.Key.Equals(location)) + .OrderBy(x => x.Key.FullPath)) + { + if (type.HasFlag(item.Value.Type) && + IncludeItemInEnumeration(item, fullPathWithoutTrailingSlash, + enumerationOptions)) { - yield return item.Key; + string itemPath = item.Key.FullPath; + if (itemPath.EndsWith(_fileSystem.Path.DirectorySeparatorChar)) + { + itemPath = itemPath.TrimEnd(_fileSystem.Path.DirectorySeparatorChar); + } + + string name = _fileSystem.Execute.Path.GetFileName(itemPath); + if (EnumerationOptionsHelper.MatchesPattern( + _fileSystem.Execute, + enumerationOptions, + name, + searchPattern) || + (_fileSystem.Execute.IsNetFramework && + SearchPatternMatchesFileExtensionOnNetFramework( + searchPattern, + _fileSystem.Execute.Path.GetExtension(name)))) + { + yield return item.Key; + } } } } From 04684af441d9b567eef5acd6aa9ec308ea3e8d28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Breu=C3=9F=20Valentin?= Date: Fri, 12 Sep 2025 08:25:16 +0200 Subject: [PATCH 3/3] Run authorization tests only on windows --- .../FileSystem/DirectoryMockTests.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Tests/Testably.Abstractions.Testing.Tests/FileSystem/DirectoryMockTests.cs b/Tests/Testably.Abstractions.Testing.Tests/FileSystem/DirectoryMockTests.cs index c9e60bbf5..9e8fcf62f 100644 --- a/Tests/Testably.Abstractions.Testing.Tests/FileSystem/DirectoryMockTests.cs +++ b/Tests/Testably.Abstractions.Testing.Tests/FileSystem/DirectoryMockTests.cs @@ -8,6 +8,8 @@ public class DirectoryMockTests public async Task EnumerateDirectories_UnauthorizedParentAccess_ShouldThrowUnauthorizedAccessExceptionImmediately() { + Skip.IfNot(Test.RunsOnWindows); + string path = "foo"; MockFileSystem fileSystem = new(); IDirectoryInfo sut = fileSystem.Directory.CreateDirectory(path); @@ -27,6 +29,8 @@ await That(Act).Throws() public async Task EnumerateFiles_UnauthorizedParentAccess_ShouldThrowUnauthorizedAccessExceptionImmediately() { + Skip.IfNot(Test.RunsOnWindows); + string path = "foo"; MockFileSystem fileSystem = new(); IDirectoryInfo sut = fileSystem.Directory.CreateDirectory(path); @@ -46,6 +50,8 @@ await That(Act).Throws() public async Task EnumerateFileSystemEntries_UnauthorizedParentAccess_ShouldThrowUnauthorizedAccessExceptionImmediately() { + Skip.IfNot(Test.RunsOnWindows); + string path = "foo"; MockFileSystem fileSystem = new(); IDirectoryInfo sut = fileSystem.Directory.CreateDirectory(path);