using System.IO.Compression; using Novelly.Api.Imports; namespace Novelly.Api.Tests; [TestFixture] public class ImportZipExtractorTests { private string _stagingParent = null!; private ImportZipExtractor _extractor = null!; [SetUp] public void SetUp() { _stagingParent = Directory.CreateTempSubdirectory("novelly-zip-test-").FullName; _extractor = new ImportZipExtractor(new CapturingLogger()); } [TearDown] public void TearDown() { if (Directory.Exists(_stagingParent)) { Directory.Delete(_stagingParent, recursive: true); } } [Test] public void A_zip_of_the_expected_folder_layout_extracts_intact() { using var zip = BuildZip(("outline.md", "# My Novel"), ("outlines/01-chapter.md", "# Chapter 1")); var staging = StagingDir(); _extractor.Extract(zip, staging); Assert.Multiple(() => { Assert.That(File.Exists(Path.Combine(staging, "outline.md")), Is.True); Assert.That(File.Exists(Path.Combine(staging, "outlines", "01-chapter.md")), Is.True); }); } [Test] public void A_single_wrapper_directory_is_flattened_away() { using var zip = BuildZip(("my-novel/outline.md", "# My Novel"), ("my-novel/outlines/01-chapter.md", "# Chapter 1")); var staging = StagingDir(); _extractor.Extract(zip, staging); Assert.Multiple(() => { Assert.That(File.Exists(Path.Combine(staging, "outline.md")), Is.True); Assert.That(Directory.Exists(Path.Combine(staging, "my-novel")), Is.False); }); } [Test] public void A_zip_slip_entry_is_rejected_and_staging_is_cleaned_up() { using var zip = BuildZip(("outline.md", "# ok"), ("../evil.md", "pwned")); var staging = StagingDir(); Assert.That(() => _extractor.Extract(zip, staging), Throws.TypeOf()); Assert.That(Directory.Exists(staging), Is.False); } [Test] public void A_disallowed_file_extension_is_rejected() { using var zip = BuildZip(("outline.md", "# ok"), ("script.sh", "echo hi")); var staging = StagingDir(); Assert.That(() => _extractor.Extract(zip, staging), Throws.TypeOf()); Assert.That(Directory.Exists(staging), Is.False); } [Test] public void The_ledger_file_is_allowed_through() { using var zip = BuildZip(("outline.md", "# ok"), (".novelly-import.json", "{}")); var staging = StagingDir(); _extractor.Extract(zip, staging); Assert.That(File.Exists(Path.Combine(staging, ".novelly-import.json")), Is.True); } [Test] public void An_empty_zip_is_rejected() { using var zip = BuildZip(); var staging = StagingDir(); Assert.That(() => _extractor.Extract(zip, staging), Throws.TypeOf()); } private string StagingDir() => Path.Combine(_stagingParent, $"staging-{Guid.NewGuid():N}"); private static MemoryStream BuildZip(params (string Path, string Content)[] entries) { var stream = new MemoryStream(); using (var archive = new ZipArchive(stream, ZipArchiveMode.Create, leaveOpen: true)) { foreach (var (path, content) in entries) { var entry = archive.CreateEntry(path); using var writer = new StreamWriter(entry.Open()); writer.Write(content); } } stream.Position = 0; return stream; } }