diff --git a/DataAccess/Extensions/ZipExtensionWrapper.cs b/DataAccess/Extensions/ZipExtensionWrapper.cs
new file mode 100644
index 00000000..081a7b7e
--- /dev/null
+++ b/DataAccess/Extensions/ZipExtensionWrapper.cs
@@ -0,0 +1,36 @@
+using System.IO.Abstractions;
+using System.IO.Compression;
+
+namespace DataAccess.Extensions;
+///
+public class ZipExtensionWrapper
+{
+ ///
+ /// Gets the file system used by this wrapper.
+ ///
+ public IFileSystem FileSystem { get; private set; }
+
+
+ public ZipExtensionWrapper(IFileSystem fileSystem)
+ {
+ FileSystem = fileSystem;
+ }
+
+ ///
+ public ZipArchive GetZipArchive(string archivePath)
+ {
+ return ZipExtensions.GetZipArchive(FileSystem, archivePath);
+ }
+
+ ///
+ public void ExtractToDirectory(ZipArchive archive, string destination)
+ {
+ ZipExtensions.ExtractToDirectory(archive, FileSystem, destination);
+ }
+
+ ///
+ public async Task CreateFromDirectoryAsync(string sourcePath, string destinationPath)
+ {
+ await ZipExtensions.CreateFromDirectoryAsync(FileSystem, sourcePath, destinationPath);
+ }
+}
\ No newline at end of file
diff --git a/DataAccess/Extensions/ZipExtensions.cs b/DataAccess/Extensions/ZipExtensions.cs
index 38576df8..38d9ec40 100644
--- a/DataAccess/Extensions/ZipExtensions.cs
+++ b/DataAccess/Extensions/ZipExtensions.cs
@@ -6,74 +6,126 @@ namespace DataAccess.Extensions;
public static class ZipExtensions
{
///
- /// Creates a object for a given path on the filesystem .
+ /// Opens a zip archive in read mode.
///
- /// The filesystem on which the file resides.
- /// The path to the zip archive on the filesystem.
- ///
- public static ZipArchive GetZipArchive(IFileSystem fs, string archivePath)
+ /// The file system abstraction used to access the file.
+ /// The path to the zip archive file.
+ /// A instance representing the archive.
+ /// Thrown if or is null.
+ /// Thrown if the specified file does not exist.
+ /// Thrown if the caller does not have the required permissions.
+ /// Thrown if the file is not a valid zip archive.
+ public static ZipArchive GetZipArchive(IFileSystem fileSystemReference, string archivePath)
{
- var zipStream = fs.File.OpenRead(archivePath);
+ var zipStream = fileSystemReference.File.OpenRead(archivePath);
return new ZipArchive(zipStream, ZipArchiveMode.Read);
}
-
- private static ZipArchive GetWritableZipArchive(IFileSystem fs, string archivePath)
+
+ private static ZipArchive GetWritableZipArchive(IFileSystem fileSystemReference, string archivePath)
{
- var zipStream = fs.File.OpenWrite(archivePath);
+ var zipStream = fileSystemReference.File.OpenWrite(archivePath);
return new ZipArchive(zipStream, ZipArchiveMode.Create);
}
-
///
- /// Extracts the contents of a to a given directory on the filesystem .
+ /// Extracts all entries from a zip archive to the specified directory.
///
/// The zip archive to extract.
- /// The filesystem to extract the archive onto.
- /// The destination path on the filesystem.
- public static void ExtractToDirectory(this ZipArchive archive, IFileSystem fs, string destination)
+ /// The file system abstraction used to access files and directories.
+ /// The directory to extract the archive's contents to.
+ /// Thrown if is null.
+ /// Thrown if the caller does not have write access to the destination directory.
+ /// Thrown if an I/O error occurs during extraction.
+ public static void ExtractToDirectory(this ZipArchive archive, IFileSystem fileSystemReference, string destination)
{
- if (!fs.Directory.Exists(destination))
- {
- fs.Directory.CreateDirectory(destination);
- }
+ EnsureDestinationDirectoryExists(fileSystemReference, destination);
foreach (var entry in archive.Entries)
{
- var entryFullName = Path.DirectorySeparatorChar switch
- {
- //adjust paths for unpacking on unix when packed on windows
- '/' => entry.FullName.Replace("\\", "/"),
- //adjust paths for unpacking on windows when packed on unix
- '\\' => entry.FullName.Replace("/", "\\"),
- _ => entry.FullName
- };
- var path = Path.Combine(destination, entryFullName);
- var directoryName = Path.GetDirectoryName(path);
- if (directoryName != null) fs.Directory.CreateDirectory(directoryName);
- if (fs.File.Exists(path)) fs.File.Delete(path);
+ var extractedEntryPath = NormalizeExtractedEntryPathForWindowsAndUnix(entry);
+ var fullPath = Path.Combine(destination, extractedEntryPath);
+ CreateEntryDirectory(fileSystemReference, fullPath);
+
+ if (fileSystemReference.File.Exists(fullPath))
+ fileSystemReference.File.Delete(fullPath);
- using var destStream = fs.File.Create(path);
+ using var destinationStream = fileSystemReference.File.Create(fullPath);
using var sourceStream = entry.Open();
- sourceStream.CopyTo(destStream);
+ sourceStream.CopyTo(destinationStream);
}
}
-
///
- /// Creates a zip archive from a given directory on the filesystem .
+ /// Creates a zip archive from the contents of a directory asynchronously.
///
- /// The filesystem to operate on.
- /// The folder that should be packed into the zip archive.
- /// The file path the zip archive should be written to.
- public static async Task CreateFromDirectoryAsync(IFileSystem fs, string source, string destination)
+ /// The file system abstraction used to access files and directories.
+ /// The source directory to compress.
+ /// The path of the resulting zip archive.
+ /// A task that represents the asynchronous operation.
+ /// Thrown if or is null.
+ /// Thrown if the source directory does not exist.
+ /// Thrown if the caller does not have the required permissions.
+ /// Thrown if an I/O error occurs during compression.
+ public static async Task CreateFromDirectoryAsync(IFileSystem fileSystemReference, string source, string destination)
{
- using var archive = GetWritableZipArchive(fs, destination);
- var files = fs.Directory.GetFiles(source, "*", SearchOption.AllDirectories);
+ ValidatePath(source);
+ ValidatePath(destination);
+ using var archive = GetWritableZipArchive(fileSystemReference, destination);
+
+ var files = GetAllFilesExceptSymbolicLinksToPreventLoops(fileSystemReference, source);
foreach (var file in files)
{
var relativePath = Path.GetRelativePath(source, file);
- var entry = archive.CreateEntry(relativePath);
- await using var entryStream = entry.Open();
- await using var fileStream = fs.File.OpenRead(file);
- await fileStream.CopyToAsync(entryStream);
+ await AddFileToArchiveAsync(fileSystemReference, archive, relativePath, file);
+ }
+ }
+ private static void ValidatePath(string path)
+ {
+ if (path == null)
+ throw new ArgumentNullException(nameof(path), "Path cannot be null.");
+ if (path.IndexOfAny(Path.GetInvalidPathChars()) >= 0)
+ {
+ throw new ArgumentException($"The path is invalid: {path}");
+ }
+ }
+ private static async Task AddFileToArchiveAsync(IFileSystem fileSystemReference, ZipArchive archive,
+ string relativePath, string file)
+ {
+ var entry = archive.CreateEntry(relativePath);
+ await using var entryStream = entry.Open();
+ await using var fileStream = fileSystemReference.File.OpenRead(file);
+ await fileStream.CopyToAsync(entryStream);
+ }
+
+ private static string[] GetAllFilesExceptSymbolicLinksToPreventLoops(IFileSystem fileSystemReference, string source)
+ {
+ return fileSystemReference.Directory.GetFiles(source, "*", SearchOption.AllDirectories)
+ .Where(file => !fileSystemReference.File.GetAttributes(file).HasFlag(FileAttributes.ReparsePoint))
+ .ToArray();
+ }
+
+
+ private static void CreateEntryDirectory(IFileSystem fileSystemReference, string fullPath)
+ {
+ var directoryName = Path.GetDirectoryName(fullPath);
+ if (directoryName != null) fileSystemReference.Directory.CreateDirectory(directoryName);
+ }
+
+ private static string NormalizeExtractedEntryPathForWindowsAndUnix(ZipArchiveEntry entry)
+ {
+ return Path.DirectorySeparatorChar switch
+ {
+ //adjust paths for unpacking on unix when packed on windows
+ '/' => entry.FullName.Replace("\\", "/"),
+ //adjust paths for unpacking on windows when packed on unix
+ '\\' => entry.FullName.Replace("/", "\\"),
+ _ => entry.FullName
+ };
+ }
+
+ private static void EnsureDestinationDirectoryExists(IFileSystem fileSystemReference, string destination)
+ {
+ if (!fileSystemReference.Directory.Exists(destination))
+ {
+ fileSystemReference.Directory.CreateDirectory(destination);
}
}
}
\ No newline at end of file
diff --git a/DataAccessTest/Extensions/ZipExtensionWrapperUt.cs b/DataAccessTest/Extensions/ZipExtensionWrapperUt.cs
new file mode 100644
index 00000000..e8965002
--- /dev/null
+++ b/DataAccessTest/Extensions/ZipExtensionWrapperUt.cs
@@ -0,0 +1,644 @@
+using DataAccess.Extensions;
+using NUnit.Framework;
+
+using System.IO.Abstractions;
+using System.IO.Abstractions.TestingHelpers;
+using System.IO.Compression;
+using NSubstitute;
+using NSubstitute.ExceptionExtensions;
+
+namespace DataAccessTest.Extensions;
+[TestFixture]
+public class ZipExtensionUnitTest
+{
+ private MockFileSystem _mockFileSystem;
+
+ [SetUp]
+ public void SetUp()
+ {
+ _mockFileSystem = new MockFileSystem();
+ }
+ public ZipExtensionWrapper CreateZipExtensionWrapper(IFileSystem? fileSystem = null)
+ {
+ fileSystem ??= new MockFileSystem();
+ return new ZipExtensionWrapper(fileSystem);
+ }
+
+ [Test]
+ public void GetZipArchive_ValidArchive_ReturnsZipArchive()
+ {
+ var archivePath = "myZipFile.zip";
+ var memoryStream = CreateMemmoryStreamWithZipArchive();
+ _mockFileSystem.AddFile(archivePath, new MockFileData(memoryStream.ToArray()));
+
+ var systemUnderTest = CreateZipExtensionWrapper(_mockFileSystem);
+ var result = systemUnderTest.GetZipArchive(archivePath);
+
+ Assert.That(result, Is.TypeOf());
+ }
+ [Test]
+ public void GetZipArchive_NonExistentFile_ThrowsFileNotFoundException()
+ {
+ var nonExistentFilePath = @"C:\test\nonexistent.zip";
+
+ var systemUnderTest = CreateZipExtensionWrapper(_mockFileSystem);
+
+ Assert.Throws(() => systemUnderTest.GetZipArchive(nonExistentFilePath));
+ }
+ [Test]
+ public void GetZipArchive_NotAZipFile_ThrowsInvalidDataException()
+ {
+ var invalidZipFilePath = @"C:\test\invalid.zip";
+ _mockFileSystem.AddFile(invalidZipFilePath, new MockFileData("Not a zip file"));
+
+ var systemUnderTest = CreateZipExtensionWrapper(_mockFileSystem);
+
+ Assert.Throws(() => systemUnderTest.GetZipArchive( invalidZipFilePath));
+ }
+ [Test]
+ public void GetZipArchive_NullArgument_ThrowsArgumentNullException()
+ {
+ var systemUnderTest = CreateZipExtensionWrapper(_mockFileSystem);
+
+ Assert.Throws(() => systemUnderTest.GetZipArchive( null));
+ }
+
+ [Test]
+ public void GetZipArchive_InvalidCharactersInPath_ThrowsArgumentException([Range(0, 32)] int number)
+ {
+ var badChars = Path.GetInvalidPathChars();// 33 elements
+ const string validPath = @"C:\Temp\Invalid";
+ var invalidPath = validPath + badChars[number];
+
+ var systemUnderTest = CreateZipExtensionWrapper(_mockFileSystem);
+ Assert.Throws(() => systemUnderTest.GetZipArchive( invalidPath));
+ }
+ [Test]
+ public void GetZipArchive_InvalidPathFormat_ThrowsNotSupportedException()
+ {
+ var invalidFormatPath = "://InvalidPath/file.zip";
+ var systemUnderTest = CreateZipExtensionWrapper(_mockFileSystem);
+
+ Assert.Throws(() => systemUnderTest.GetZipArchive( invalidFormatPath));
+ }
+ ///
+ /// Implementing tested with OpenRead. It would be better if a mock file would be constructed using the TestingHelpers that we are not allowed to access.
+ ///
+ [Test]
+ public void GetZipArchiveAccessIsDenied_ThrowsUnauthorizedAccessException()
+ {
+ var fileSystemMock = NSubstitute.Substitute.For();
+ string archivePath = "test.zip";
+ fileSystemMock.File
+ .OpenRead(Arg.Any())
+ .Returns( _=> throw new UnauthorizedAccessException("Simulated Unauthorized Access"));
+
+ var systemUnderTest = CreateZipExtensionWrapper(fileSystemMock);
+
+ Assert.Throws(() =>
+ {
+ systemUnderTest.GetZipArchive(archivePath);
+ });
+ }
+ ///
+ /// Implementing tested with OpenRead. It would be better if a mock file would be constructed using the TestingHelpers that we are not allowed to access.
+ ///
+ [Test]
+ public void GetZipArchiveFileCannotBeRead_ThrowsIOException()
+ {
+ var fileSystemMock = NSubstitute.Substitute.For();
+ string archivePath = "test.zip";
+ fileSystemMock.File
+ .OpenRead(Arg.Any())
+ .Returns( _=> throw new IOException("Simulated IO Exception"));
+
+ var systemUnderTest = CreateZipExtensionWrapper(fileSystemMock);
+
+ Assert.Throws(() =>
+ {
+ systemUnderTest.GetZipArchive(archivePath);
+ });
+ }
+ [Test]
+ public void GetZipArchive_LargeArchive_Success()
+ {
+ var archivePath = "largeArchive.zip";
+ var memoryStream = CreateLargeArchive();
+ _mockFileSystem.AddFile(archivePath, new MockFileData(memoryStream.ToArray()));
+
+ var systemUnderTest = CreateZipExtensionWrapper(_mockFileSystem);
+ var result = systemUnderTest.GetZipArchive(archivePath);
+
+ Assert.That(result, Is.TypeOf());
+ }
+ [Test]
+ public void GetZipArchive_WhitespacePath_ThrowsArgumentException()
+ {
+ var systemUnderTest = CreateZipExtensionWrapper(_mockFileSystem);
+
+ Assert.Throws(() => systemUnderTest.GetZipArchive(" "));
+ }
+ ///
+ /// Implementing tested with OpenRead. TestingHelper doesn't implement PathTooLong exception.
+ ///
+ [Test]
+ public void GetZipArchive_PathTooLong_ThrowsPathTooLongException()
+ {
+ var longPath = new string('a', 260) + ".zip";
+ var mockFileSystem = Substitute.For();
+ mockFileSystem.File.OpenRead(Arg.Is(path => path.Length > 259))
+ .Throws();
+
+ var systemUnderTest = CreateZipExtensionWrapper(mockFileSystem);
+
+ Assert.Throws(() => systemUnderTest.GetZipArchive(longPath));
+ }
+ private static MemoryStream CreateLargeArchive()
+ {
+ var memoryStream = new MemoryStream();
+ using (var archive = new ZipArchive(memoryStream, ZipArchiveMode.Create, true))
+ {
+ for (int i = 0; i < 1000; i++)
+ {
+ var entry = archive.CreateEntry($"file{i}.txt");
+ using var entryStream = entry.Open();
+ using var writer = new StreamWriter(entryStream);
+ writer.Write($"This is file {i}");
+ }
+ }
+
+ memoryStream.Seek(0, SeekOrigin.Begin);
+ return memoryStream;
+ }
+
+ private static MemoryStream CreateMemmoryStreamWithZipArchive()
+ {
+ MemoryStream? memoryStream = null;
+ try
+ {
+ memoryStream = new MemoryStream();
+ using (var archive = new ZipArchive(memoryStream, ZipArchiveMode.Create, true))
+ {
+ var demoFile = archive.CreateEntry("test.txt");
+ using (var entryStream = demoFile.Open())
+ using (var streamWriter = new StreamWriter(entryStream))
+ {
+ streamWriter.Write("Hello, world!");
+ }
+ }
+
+ return memoryStream;
+ }
+ catch
+ {
+ memoryStream?.Dispose();
+ throw;
+ }
+ }
+ [Test]
+ public void ExtractToDirectory_ValidArchiveAndDestination_ExtractsFilesSuccessfully()
+ {
+ var mockFile = new MockFileData("Mock zip content");
+ _mockFileSystem.AddFile("/mockArchive.zip", mockFile);
+ var mockArchive = GetMockZipArchive(new[] { "file1.txt", "folder/file2.txt" });
+
+ var systemUnderTest = CreateZipExtensionWrapper(_mockFileSystem);
+ systemUnderTest.ExtractToDirectory(mockArchive, "/extracted");
+
+ Assert.That(_mockFileSystem.Directory.Exists("/extracted"), Is.True);
+ Assert.That(_mockFileSystem.File.Exists("/extracted/file1.txt"), Is.True);
+ Assert.That(_mockFileSystem.File.Exists("/extracted/folder/file2.txt"), Is.True);
+ }
+
+ [Test]
+ public void ExtractToDirectory_NullDestination_ThrowsArgumentNullException()
+ {
+ var mockArchive = GetMockZipArchive(new[] { "file1.txt" });
+
+ var systemUnderTest = CreateZipExtensionWrapper(_mockFileSystem);
+
+ Assert.That(() => systemUnderTest.ExtractToDirectory(mockArchive, null), Throws.TypeOf());
+ }
+ [Test]
+ public void ExtractToDirectory_WhitespaceDestination_ThrowsArgumentException()
+ {
+ var mockArchive = GetMockZipArchive(new[] { "file1.txt" });
+
+ var systemUnderTest = CreateZipExtensionWrapper(_mockFileSystem);
+
+ Assert.Throws(() => systemUnderTest.ExtractToDirectory(mockArchive, " "));
+ }
+ [Test]
+ public void ExtractToDirectory_EmptyArchive_CreatesEmptyDirectory()
+ {
+ var mockArchive = GetMockZipArchive(Array.Empty());
+
+ var systemUnderTest = CreateZipExtensionWrapper(_mockFileSystem);
+ systemUnderTest.ExtractToDirectory(mockArchive, "/emptyExtract");
+
+ Assert.That(_mockFileSystem.Directory.Exists("/emptyExtract"), Is.True);
+ Assert.That(_mockFileSystem.Directory.GetFiles("/emptyExtract"), Is.Empty);
+ }
+ ///
+ /// Implementing tested with substituted mockFileSystem. It would be better if a mock file would be constructed using the TestingHelpers that we are not allowed to access.
+ ///
+ [Test]
+ public void ExtractToDirectory_NonWritableDirectory_ThrowsUnauthorizedAccessException()
+ {
+ var mockArchive = GetMockZipArchive(new[] { "file1.txt" });
+ var mockFileSystem = Substitute.For();
+ mockFileSystem.Directory.Exists("/nonWritable").Returns(true);
+ mockFileSystem.Directory.CreateDirectory(Arg.Any()).Returns(_ => throw new UnauthorizedAccessException("Simulated Unauthorized Access"));
+
+ var systemUnderTest = new ZipExtensionWrapper(mockFileSystem);
+
+ Assert.Throws(() => systemUnderTest.ExtractToDirectory(mockArchive, "/nonWritable"));
+ }
+
+
+ private ZipArchive GetMockZipArchive(string[] fileNames)
+ {
+ var memoryStream = new MemoryStream();
+ using (var archive = new ZipArchive(memoryStream, ZipArchiveMode.Create, true))
+ {
+ foreach (var fileName in fileNames)
+ {
+ var entry = archive.CreateEntry(fileName);
+ using var entryStream = entry.Open();
+ using var writer = new StreamWriter(entryStream);
+ writer.Write("Mock content");
+ }
+ }
+
+ memoryStream.Seek(0, SeekOrigin.Begin);
+ return new ZipArchive(memoryStream, ZipArchiveMode.Read);
+ }
+
+ [Test]
+ public async Task CreateFromDirectoryAsync_NullSourcePath_ThrowsArgumentNullException()
+ {
+ string sourcePath = null;
+ string destinationPath = "/output.zip";
+
+ var systemUnderTest = CreateZipExtensionWrapper();
+
+ Assert.That(
+ async () => await systemUnderTest.CreateFromDirectoryAsync(sourcePath, destinationPath),
+ Throws.ArgumentNullException);
+ }
+
+ [Test]
+ public async Task CreateFromDirectoryAsync_NullDestinationPath_ThrowsArgumentNullException()
+ {
+ string sourcePath = "/source";
+ string destinationPath = null;
+
+ var systemUnderTest = CreateZipExtensionWrapper();
+ Assert.That(
+ async () => await systemUnderTest.CreateFromDirectoryAsync(sourcePath, destinationPath),
+ Throws.ArgumentNullException);
+ }
+
+ [Test]
+ public async Task CreateFromDirectoryAsync_SourcePathDoesNotExist_ThrowsDirectoryNotFoundException()
+ {
+ string sourcePath = "/nonexistent";
+ string destinationPath = "/output.zip";
+
+ var systemUnderTest = CreateZipExtensionWrapper();
+
+ Assert.That(
+ async () => await systemUnderTest.CreateFromDirectoryAsync(sourcePath, destinationPath),
+ Throws.InstanceOf());
+ }
+
+ ///
+ /// Should be discussed further on 29.01.
+ ///
+ [Test]
+ public void CreateFromDirectoryAsync_NoWritePermission_ThrowsUnauthorizedAccessException()
+ {
+ string sourcePath = @"C:\source";
+ string destinationPath = @"C:\output.zip";
+
+ _mockFileSystem.AddDirectory(sourcePath);
+ _mockFileSystem.AddFile(Path.Combine(sourcePath, "file1.txt"), new MockFileData("File1 content"));
+ _mockFileSystem.AddFile(destinationPath, new MockFileData("Existing content")
+ {
+ Attributes = FileAttributes.ReadOnly
+ });
+
+ var systemUnderTest = CreateZipExtensionWrapper(_mockFileSystem);
+ var exception = Assert.ThrowsAsync(async () =>
+ await systemUnderTest.CreateFromDirectoryAsync(sourcePath, destinationPath));
+
+ Assert.That(exception, Is.Not.Null);
+ Assert.That(exception.Message, Contains.Substring("Access to the path"));
+ }
+
+
+
+ [Test]
+ public async Task CreateFromDirectoryAsync_SubdirectoriesAreIncluded_CreatesArchiveWithHierarchy()
+{
+ string sourcePath = @"C:\source";
+ string destinationPath = @"C:\output.zip";
+
+ string subdirectoryPath = Path.Combine(sourcePath, "subdir");
+ _mockFileSystem.AddDirectory(sourcePath);
+ _mockFileSystem.AddDirectory(subdirectoryPath);
+ _mockFileSystem.AddFile(Path.Combine(sourcePath, "file1.txt"), new MockFileData("File1 content"));
+ _mockFileSystem.AddFile(Path.Combine(subdirectoryPath, "file2.txt"), new MockFileData("File2 content"));
+
+ var systemUnderTest = CreateZipExtensionWrapper(_mockFileSystem);
+ await systemUnderTest.CreateFromDirectoryAsync(sourcePath, destinationPath);
+ var archive = OpenZipArchive(destinationPath);
+
+ Assert.That(_mockFileSystem.File.Exists(destinationPath), Is.True);
+ Assert.That(archive.Entries.Count, Is.EqualTo(2));
+ Assert.That(archive.Entries.Any(e => e.FullName == "file1.txt"), Is.True);
+ Assert.That(archive.Entries.Any(e => e.FullName == "subdir/file2.txt" || e.FullName == @"subdir\file2.txt"), Is.True);
+}
+
+ [Test]
+ public async Task CreateFromDirectoryAsync_EmptySourceDirectory_CreatesEmptyArchive()
+ {
+ string sourcePath = @"C:\source";
+ string destinationPath = @"C:\output.zip";
+ _mockFileSystem.AddDirectory(sourcePath);
+
+ var systemUnderTest = CreateZipExtensionWrapper(_mockFileSystem);
+
+ await systemUnderTest.CreateFromDirectoryAsync(sourcePath, destinationPath);
+ var archive = OpenZipArchive(destinationPath);
+
+ Assert.That(_mockFileSystem.File.Exists(destinationPath), Is.True);
+ Assert.That(archive.Entries.Count, Is.EqualTo(0));
+ }
+ [Test]
+ public async Task CreateFromDirectoryAsync_EmptySourcePath_ThrowsArgumentException()
+ {
+ var systemUnderTest = CreateZipExtensionWrapper();
+
+ var exception = Assert.ThrowsAsync(async () =>
+ await systemUnderTest.CreateFromDirectoryAsync(string.Empty, "/destination.zip"));
+
+ Assert.That(exception!.ParamName, Is.EqualTo("path"));
+ }
+
+ [Test]
+ public async Task CreateFromDirectoryAsync_EmptyDestinationPath_ThrowsArgumentException()
+ {
+ var systemUnderTest = CreateZipExtensionWrapper();
+
+ var exception = Assert.ThrowsAsync(async () =>
+ await systemUnderTest.CreateFromDirectoryAsync("/source", string.Empty));
+
+ Assert.That(exception!.ParamName, Is.EqualTo("path"));
+ }
+ [Test]
+ public async Task CreateFromDirectoryAsync_WhitespaceSourcePath_ThrowsArgumentException()
+ {
+ var systemUnderTest = CreateZipExtensionWrapper();
+ var exception = Assert.ThrowsAsync(async () =>
+ await systemUnderTest.CreateFromDirectoryAsync(" ", "/destination.zip"));
+
+ Assert.That(exception!.ParamName, Is.EqualTo("path"));
+ }
+ [Test]
+ public async Task CreateFromDirectoryAsync_WhitespaceDestinationPath_ThrowsArgumentException()
+ {
+ var systemUnderTest = CreateZipExtensionWrapper();
+ var exception = Assert.ThrowsAsync(async () =>
+ await systemUnderTest.CreateFromDirectoryAsync("/source", " "));
+
+ Assert.That(exception!.ParamName, Is.EqualTo("path"));
+ }
+
+ [Test]
+ public async Task CreateFromDirectoryAsync_SourceContainsFiles_CreateArchiveWithFiles()
+{
+ string sourcePath = @"C:\source";
+ string destinationPath = @"C:\output.zip";
+ AddFilesToMockFileSystem(sourcePath, "File1 content", "File2 content");
+
+ var systemUnderTest = CreateZipExtensionWrapper(_mockFileSystem);
+ await systemUnderTest.CreateFromDirectoryAsync(sourcePath, destinationPath);
+ var archive = OpenZipArchive(destinationPath);
+
+ Assert.That(_mockFileSystem.File.Exists(destinationPath), Is.True);
+ Assert.That(archive.Entries.Count, Is.EqualTo(2));
+ Assert.That(archive.Entries.Any(e => e.FullName == "file1.txt"), Is.True);
+ Assert.That(archive.Entries.Any(e => e.FullName == "file2.txt"), Is.True);
+}
+
+
+
+ [Test]
+ public async Task CreateFromDirectoryAsync_DestinationAlreadyExists_OverwritesFile()
+ {
+ string sourcePath = @"C:\source";
+ string destinationPath = @"C:\output.zip";
+ AddFilesToMockFileSystem(sourcePath, "file1content","file2content");
+ _mockFileSystem.AddFile(destinationPath, new MockFileData("Existing content"));
+
+ var systemUnderTest = CreateZipExtensionWrapper(_mockFileSystem);
+ await systemUnderTest.CreateFromDirectoryAsync(sourcePath, destinationPath);
+ var archive = OpenZipArchive(destinationPath);
+
+ Assert.That(_mockFileSystem.File.Exists(destinationPath), Is.True);
+ Assert.That(archive.Entries.Count, Is.EqualTo(2));
+ Assert.That(archive.Entries.Any(e => e.FullName == "file1.txt"), Is.True);
+ Assert.That(archive.Entries.Any(e => e.FullName == "file2.txt"), Is.True);
+ }
+ [Test]
+ public async Task CreateFromDirectoryAsync_HiddenFilesInSource_IncludesHiddenFilesInArchive()
+ {
+ string sourcePath = @"C:\source";
+ string destinationPath = @"C:\output.zip";
+ _mockFileSystem.AddDirectory(sourcePath);
+ string hiddenFilePath = Path.Combine(sourcePath, "hidden.txt");
+ _mockFileSystem.AddFile(hiddenFilePath, new MockFileData("Hidden file content")
+ {
+ Attributes = FileAttributes.Hidden
+ });
+
+ var systemUnderTest = CreateZipExtensionWrapper(_mockFileSystem);
+ await systemUnderTest.CreateFromDirectoryAsync(sourcePath, destinationPath);
+ var archive = OpenZipArchive(destinationPath);
+
+ Assert.That(_mockFileSystem.File.Exists(destinationPath), Is.True);
+ Assert.That(archive.Entries.Count, Is.EqualTo(1));
+ Assert.That(archive.Entries.Any(e => e.FullName == "hidden.txt"), Is.True);
+ }
+ [Test]
+ public async Task CreateFromDirectoryAsync_SourcePathIsFile_ThrowsDirectoryNotFoundException()
+ {
+ string sourcePath = @"C:\source\file.txt";
+ string destinationPath = @"C:\output.zip";
+ _mockFileSystem.AddFile(sourcePath, new MockFileData("This is a file"));
+
+ var systemUnderTest = CreateZipExtensionWrapper(_mockFileSystem);
+
+ Assert.That(
+ async () => await systemUnderTest.CreateFromDirectoryAsync(sourcePath, destinationPath),
+ Throws.InstanceOf().With.Message.Contains("Could not find a part of the path"));
+ }
+
+ [Test]
+ public async Task CreateFromDirectoryAsync_SymlinksInSource_IgnoresSymlinks()
+ {
+ string sourcePath = "/source";
+ string destinationPath = "/output.zip";
+ _mockFileSystem.AddDirectory(sourcePath);
+ _mockFileSystem.AddFile(Path.Combine(sourcePath, "file1.txt"), new MockFileData("File1 content"));
+ _mockFileSystem.AddFile(Path.Combine(sourcePath, "symlink"), new MockFileData(string.Empty)
+ {
+ Attributes = FileAttributes.ReparsePoint
+ });
+
+
+ var systemUnderTest = CreateZipExtensionWrapper(_mockFileSystem);
+ await systemUnderTest.CreateFromDirectoryAsync(sourcePath, destinationPath);
+ var archive = OpenZipArchive(destinationPath);
+
+ Assert.That(_mockFileSystem.File.Exists(destinationPath), Is.True);
+ Assert.That(archive.Entries.Count, Is.EqualTo(1));
+ Assert.That(archive.Entries.Any(e => e.FullName == "file1.txt"), Is.True);
+ }
+ [Test]
+ public async Task CreateFromDirectoryAsync_SourceContainsReadOnlyFiles_Success()
+ {
+ var sourcePath = "/source";
+ var destinationPath = "/destination.zip";
+ _mockFileSystem.AddDirectory(sourcePath);
+ _mockFileSystem.AddFile(Path.Combine(sourcePath, "readonly.txt"), new MockFileData("Read-only content")
+ {
+ Attributes = FileAttributes.ReadOnly
+ });
+
+ var systemUnderTest = CreateZipExtensionWrapper(_mockFileSystem);
+ await systemUnderTest.CreateFromDirectoryAsync(sourcePath, destinationPath);
+ var archive = OpenZipArchive(destinationPath);
+
+ Assert.That(_mockFileSystem.File.Exists(destinationPath), Is.True);
+ Assert.That(archive.Entries.Any(e => e.FullName == "readonly.txt"), Is.True);
+ }
+ [Test]
+ public async Task CreateFromDirectoryAsync_SourceContainsFilesWithLongPaths_Success()
+ {
+ var sourcePath = "/source";
+ var destinationPath = "/destination.zip";
+ _mockFileSystem.AddDirectory(sourcePath);
+ var longFileName = new string('a', 255) + ".txt";
+ _mockFileSystem.AddFile(Path.Combine(sourcePath, longFileName), new MockFileData("Long path content"));
+
+ var systemUnderTest = CreateZipExtensionWrapper(_mockFileSystem);
+ await systemUnderTest.CreateFromDirectoryAsync(sourcePath, destinationPath);
+
+ Assert.That(_mockFileSystem.File.Exists(destinationPath), Is.True);
+ }
+ [Test]
+ public async Task CreateFromDirectoryAsync_SourceContainsSpecialCharacterFiles_Success()
+ {
+ var sourcePath = "/source";
+ var destinationPath = "/destination.zip";
+ _mockFileSystem.AddDirectory(sourcePath);
+ _mockFileSystem.AddFile(Path.Combine(sourcePath, "file@#$!.txt"), new MockFileData("Special characters"));
+
+ var systemUnderTest = CreateZipExtensionWrapper(_mockFileSystem);
+ await systemUnderTest.CreateFromDirectoryAsync(sourcePath, destinationPath);
+ var archive = OpenZipArchive(destinationPath);
+
+ Assert.That(_mockFileSystem.File.Exists(destinationPath), Is.True);
+ Assert.That(archive.Entries.Any(e => e.FullName == "file@#$!.txt"), Is.True);
+ }
+ [Test]
+ public async Task CreateFromDirectoryAsync_FilesAddedDuringOperation_IgnoresNewFiles()
+ {
+ var sourcePath = "/source";
+ var destinationPath = "/destination.zip";
+ _mockFileSystem.AddDirectory(sourcePath);
+ _mockFileSystem.AddFile(Path.Combine(sourcePath, "file1.txt"), new MockFileData("Initial content"));
+
+ var systemUnderTest = CreateZipExtensionWrapper(_mockFileSystem);
+ var task = systemUnderTest.CreateFromDirectoryAsync(sourcePath, destinationPath);
+ await Task.Delay(10);
+ _mockFileSystem.AddFile(Path.Combine(sourcePath, "file2.txt"), new MockFileData("New content"));
+ await task;
+ var archive = OpenZipArchive(destinationPath);
+
+ Assert.That(_mockFileSystem.File.Exists(destinationPath), Is.True);
+ Assert.That(archive.Entries.Count, Is.EqualTo(1)); // Only file1.txt should be included
+ Assert.That(archive.Entries.Any(e => e.FullName == "file1.txt"), Is.True);
+ }
+ [Test]
+ public async Task CreateFromDirectoryAsync_FilesRemovedDuringOperation_DoesNotFail()
+ {
+ var sourcePath = "/source";
+ var destinationPath = "/destination.zip";
+ _mockFileSystem.AddDirectory(sourcePath);
+ _mockFileSystem.AddFile(Path.Combine(sourcePath, "file1.txt"), new MockFileData("Initial content"));
+
+ var systemUnderTest = CreateZipExtensionWrapper(_mockFileSystem);
+ var task = systemUnderTest.CreateFromDirectoryAsync(sourcePath, destinationPath);
+ await Task.Delay(10);
+ _mockFileSystem.RemoveFile(Path.Combine(sourcePath, "file1.txt"));
+ await task;
+ var archive = OpenZipArchive(destinationPath);
+
+ Assert.That(_mockFileSystem.File.Exists(destinationPath), Is.True);
+ Assert.That(archive.Entries.Count, Is.EqualTo(1)); // Expect file1.txt to be included
+ Assert.That(archive.Entries.Any(e => e.FullName == "file1.txt"), Is.True);
+ }
+ [Test]
+ public async Task CreateFromDirectoryAsync_DestinationDriveOutOfSpace_ThrowsIOException()
+ {
+ var sourcePath = "/source";
+ var destinationPath = "/destination.zip";
+ _mockFileSystem.AddDirectory(sourcePath);
+ _mockFileSystem.AddFile(Path.Combine(sourcePath, "file.txt"), new MockFileData("Some content"));
+ var limitedFileSystem = Substitute.For();
+ limitedFileSystem.FileSystemWatcher.Returns(_mockFileSystem.FileSystemWatcher);
+ limitedFileSystem.Directory.Returns(_mockFileSystem.Directory);
+ limitedFileSystem.File.OpenWrite(Arg.Any()).Returns(ci => throw new IOException("No space left on device"));
+
+ var systemUnderTest = CreateZipExtensionWrapper(limitedFileSystem);
+
+ Assert.ThrowsAsync(async () =>
+ await systemUnderTest.CreateFromDirectoryAsync(sourcePath, destinationPath));
+ }
+
+ [Test]
+ public async Task CreateFromDirectoryAsync_SourceContainsVeryLargeFiles_Success()
+ {
+ var sourcePath = "/source";
+ var destinationPath = "/destination.zip";
+ _mockFileSystem.AddDirectory(sourcePath);
+ _mockFileSystem.AddFile(Path.Combine(sourcePath, "largefile.bin"), new MockFileData(new byte[1024 * 1024 * 500])); // 500MB
+
+ var systemUnderTest = CreateZipExtensionWrapper(_mockFileSystem);
+ await systemUnderTest.CreateFromDirectoryAsync(sourcePath, destinationPath);
+ var archive = OpenZipArchive(destinationPath);
+
+ Assert.That(_mockFileSystem.File.Exists(destinationPath), Is.True);
+ Assert.That(archive.Entries.Count, Is.EqualTo(1));
+ Assert.That(archive.Entries.Any(e => e.FullName == "largefile.bin"), Is.True);
+ }
+
+
+ private void AddFilesToMockFileSystem(string path, string content, string content2)
+ {
+ _mockFileSystem.AddDirectory(path);
+ _mockFileSystem.AddFile(Path.Combine(path, "file1.txt"), new MockFileData(content));
+ _mockFileSystem.AddFile(Path.Combine(path, "file2.txt"), new MockFileData(content2));
+ }
+
+ private ZipArchive OpenZipArchive(string path)
+ {
+ var stream = _mockFileSystem.File.OpenRead(path);
+ return new ZipArchive(stream, ZipArchiveMode.Read);
+ }
+
+
+}
\ No newline at end of file