Files
novelly/tests/Novelly.Api.Tests/ImportZipExtractorTests.cs
James Wampler 661f2917ea Add zip upload and mapped-path picker for outline import
Sandbox source paths under a configured Imports:RootPath, browse it
from the web dialog, upload a zip that extracts into staging, and
import a single markdown file (agent infers chapter vs character).
2026-08-20 14:21:44 -07:00

117 lines
3.5 KiB
C#

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<ImportZipExtractor>());
}
[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<ArgumentException>());
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<ArgumentException>());
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<ArgumentException>());
}
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;
}
}