Skip to content
Merged
24 changes: 24 additions & 0 deletions Source/Testably.Abstractions.Testing/Helpers/ExceptionFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,22 @@ internal static FileNotFoundException FileNotFound(string path)
#endif
};

internal static IOException FileSharingViolation()
=> new("The process cannot access the file because it is being used by another process.")
{
#if FEATURE_EXCEPTION_HRESULT
HResult = -2147024864,
#endif
};

internal static IOException FileSharingViolation(string path)
=> new($"The process cannot access the file '{path}' because it is being used by another process.")
{
#if FEATURE_EXCEPTION_HRESULT
HResult = -2147024864,
#endif
};

internal static ArgumentException HandleIsInvalid(string? paramName = "handle")
=> new("Invalid handle.", paramName);

Expand Down Expand Up @@ -297,6 +313,14 @@ internal static UnauthorizedAccessException AccessDenied(string path)
#endif
};

internal static IOException IOAccessDenied(string path)
=> new($"Access to the path '{path}' is denied.")
{
#if FEATURE_EXCEPTION_HRESULT
HResult = -2147024891,
#endif
};

internal static PlatformNotSupportedException UnixFileModeNotSupportedOnThisPlatform()
=> new("Unix file modes are not supported on this platform.")
{
Expand Down
53 changes: 52 additions & 1 deletion Source/Testably.Abstractions.Testing/Storage/InMemoryStorage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,9 @@ public bool DeleteContainer(
}

ValidateContainerType(container.Type, expectedType, _fileSystem.Execute, location);


ValidateDeleteHandle(container, location.FullPath);

if (container.Type == FileSystemTypes.Directory)
{
IEnumerable<IStorageLocation> children =
Expand Down Expand Up @@ -992,6 +994,8 @@ private bool IncludeItemInEnumeration(
return null;
}

ValidateMoveHandle(container, source.FullPath);

if (container.Type == FileSystemTypes.Directory &&
source.FullPath.Equals(destination.FullPath, _fileSystem.Execute.IsNetFramework
? StringComparison.OrdinalIgnoreCase
Expand Down Expand Up @@ -1197,6 +1201,53 @@ private static void ValidateContainerType(
}
}

private void ValidateDeleteHandle(IStorageContainer container, string fullPath)
{
if (!_fileSystem.Execute.IsWindows || container.Type != FileSystemTypes.Directory)
{
return;
}

string? currentDirectory
= _fileSystem.Directory.GetCurrentDirectory().NormalizePath(_fileSystem);

string currentDirectoryWithSeparator
= currentDirectory + _fileSystem.Path.DirectorySeparatorChar;

string fullPathWithSeparator = fullPath + _fileSystem.Path.DirectorySeparatorChar;

if (currentDirectoryWithSeparator.StartsWith(
fullPathWithSeparator, _fileSystem.Execute.StringComparisonMode
))
{
throw ExceptionFactory.FileSharingViolation(currentDirectory);
}
}

private void ValidateMoveHandle(IStorageContainer container, string fullPath)
{
if (!_fileSystem.Execute.IsWindows || container.Type != FileSystemTypes.Directory)
{
return;
}

string? currentDirectory
= _fileSystem.Directory.GetCurrentDirectory().NormalizePath(_fileSystem)
+ _fileSystem.Path.DirectorySeparatorChar;

string fullPathWithSeparator = fullPath + _fileSystem.Path.DirectorySeparatorChar;

if (currentDirectory.Equals(fullPathWithSeparator, _fileSystem.Execute.StringComparisonMode))
{
throw ExceptionFactory.FileSharingViolation();
}

if (currentDirectory.StartsWith(fullPathWithSeparator, _fileSystem.Execute.StringComparisonMode))
{
throw ExceptionFactory.IOAccessDenied(fullPath);
}
}

private static void ValidateExpression(string expression)
{
if (expression.Contains('\0', StringComparison.Ordinal))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -306,4 +306,71 @@ await That(Act).Throws<IOException>()
: $"'{FileSystem.Path.Combine(BasePath, path)}'");
await That(FileSystem.Directory.Exists(path)).IsTrue();
}

[Theory]
[AutoData]
[InlineData(null)]
public async Task Delete_CurrentDirectory_ShouldThrowIOException_OnWindows(string? nested)
{
// Arrange
string directory = FileSystem.Directory.GetCurrentDirectory();
string expectedExceptionDirectory = directory;

if (nested != null)
{
string nestedDirectory = FileSystem.Path.Combine(directory, nested);
FileSystem.Directory.CreateDirectory(nestedDirectory);
FileSystem.Directory.SetCurrentDirectory(nestedDirectory);
expectedExceptionDirectory = nestedDirectory;
}

// Act
void Act()
{
FileSystem.Directory.Delete(directory, true);
}

try
{
// Assert
await That(Act).ThrowsExactly<IOException>().OnlyIf(Test.RunsOnWindows).Which
.HasMessage(
$"The process cannot access the file '*{expectedExceptionDirectory}' because it is being used by another process."
).AsWildcard();
}
finally
{
if (Test.RunsOnWindows)
{
// Cleanup
FileSystem.Directory.SetCurrentDirectory(BasePath);
}
}
}

[Theory]
[InlineData("next")]
[InlineData("next", "sub")]
public async Task Delete_DirNextToCurrentDirectory_ShouldNotThrow(params string[] paths)
{
// Arrange
string directory = FileSystem.Directory.GetCurrentDirectory();
// Intended missing separator, we want to test that the handle does not affect similar paths
string nextTo = directory + FileSystem.Path.Combine(paths);

FileSystem.Directory.CreateDirectory(nextTo);
FileSystem.Directory.SetCurrentDirectory(nextTo);

// Act
void Act()
{
FileSystem.Directory.Delete(directory, true);
}

// Assert
await That(Act).DoesNotThrow();

await That(FileSystem.Directory.Exists(directory)).IsFalse();
await That(FileSystem.Directory.Exists(nextTo)).IsTrue();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -268,4 +268,84 @@ await That(
SearchOption.AllDirectories))
.HasSingle();
}

[Fact]
public async Task Move_CurrentDirectory_ShouldThrowIOException_OnWindows()
{
// Arrange
string directory = FileSystem.Directory.GetCurrentDirectory();
string newPath = FileSystem.Path.GetFullPath("../new");

// Act
void Act()
{
FileSystem.Directory.Move(directory, newPath);
}

// Assert
await That(Act).ThrowsExactly<IOException>().OnlyIf(Test.RunsOnWindows).Which.HasMessage(
"The process cannot access the file because it is being used by another process."
);
}

[Theory]
[AutoData]
public async Task Move_NestedCurrentDirectory_ShouldThrowIOException_OnWindows(string nested)
{
// Arrange
string directory = FileSystem.Directory.GetCurrentDirectory();
string newPath = FileSystem.Path.GetFullPath("../new");

string nestedDirectory = FileSystem.Path.Combine(directory, nested);
FileSystem.Directory.CreateDirectory(nestedDirectory);
FileSystem.Directory.SetCurrentDirectory(nestedDirectory);

// Act
void Act()
{
FileSystem.Directory.Move(directory, newPath);
}

// Assert
try
{
await That(Act).ThrowsExactly<IOException>().OnlyIf(Test.RunsOnWindows).Which
.HasMessage($"Access to the path '{directory}' is denied.");
}
finally
{
if (Test.RunsOnWindows)
{
// Cleanup
FileSystem.Directory.SetCurrentDirectory(BasePath);
}
}
}

[Theory]
[InlineData("next")]
[InlineData("next", "sub")]
public async Task Move_DirNextToCurrentDirectory_ShouldNotThrow(params string[] paths)
{
// Arrange
string directory = FileSystem.Directory.GetCurrentDirectory();
// Intended missing separator, we want to test that the handle does not affect similar paths
string nextTo = directory + FileSystem.Path.Combine(paths);
string moveTarget = FileSystem.Path.Combine(nextTo, "move-target");

FileSystem.Directory.CreateDirectory(nextTo);
FileSystem.Directory.SetCurrentDirectory(nextTo);

// Act
void Act()
{
FileSystem.Directory.Move(directory, moveTarget);
}

// Assert
await That(Act).DoesNotThrow();

await That(FileSystem.Directory.Exists(directory)).IsFalse();
await That(FileSystem.Directory.Exists(nextTo)).IsTrue();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -143,4 +143,71 @@ await That(Act).Throws<IOException>()
await That(sut.Exists).IsTrue();
await That(FileSystem.Directory.Exists(sut.FullName)).IsTrue();
}

[Theory]
[AutoData]
[InlineData(null)]
public async Task Delete_CurrentDirectory_ShouldThrowIOException_OnWindows(string? nested)
{
// Arrange
string directory = FileSystem.Directory.GetCurrentDirectory();
string expectedExceptionDirectory = directory;

if (nested != null)
{
string nestedDirectory = FileSystem.Path.Combine(directory, nested);
FileSystem.Directory.CreateDirectory(nestedDirectory);
FileSystem.Directory.SetCurrentDirectory(nestedDirectory);
expectedExceptionDirectory = nestedDirectory;
}

// Act
void Act()
{
FileSystem.DirectoryInfo.New(directory).Delete(true);
}

try
{
// Assert
await That(Act).ThrowsExactly<IOException>().OnlyIf(Test.RunsOnWindows).Which
.HasMessage(
$"The process cannot access the file '*{expectedExceptionDirectory}' because it is being used by another process."
).AsWildcard();
}
finally
{
if (Test.RunsOnWindows)
{
// Cleanup
FileSystem.Directory.SetCurrentDirectory(BasePath);
}
}
}

[Theory]
[InlineData("next")]
[InlineData("next", "sub")]
public async Task Delete_DirNextToCurrentDirectory_ShouldNotThrow(params string[] paths)
{
// Arrange
string directory = FileSystem.Directory.GetCurrentDirectory();
// Intended missing separator, we want to test that the handle does not affect similar paths
string nextTo = directory + FileSystem.Path.Combine(paths);

FileSystem.Directory.CreateDirectory(nextTo);
FileSystem.Directory.SetCurrentDirectory(nextTo);

// Act
void Act()
{
FileSystem.DirectoryInfo.New(directory).Delete(true);
}

// Assert
await That(Act).DoesNotThrow();

await That(FileSystem.Directory.Exists(directory)).IsFalse();
await That(FileSystem.Directory.Exists(nextTo)).IsTrue();
}
}
Loading
Loading