Files
novelly/tests/Novelly.Api.Tests/ImportServiceTests.cs
T
James Wampler 4313c8f206 Rename Project concept to Novel across the stack
Renames the domain concept from Project to Novel throughout the backend
(entities, DTOs, services, endpoints, ProjectAccessService/Permission,
ProjectId foreign keys), MCP server (tool names and routes), and the
React/Vite frontend (types, hooks, routes, components). Adds a new EF
Core migration (RenameProjectToNovel) using RenameTable/RenameColumn to
preserve existing data instead of dropping/recreating tables. Updates
CLAUDE.md's structure section to reference Novels/ instead of Projects/.
2026-08-17 23:03:09 -07:00

147 lines
4.8 KiB
C#

using System.Threading.Channels;
using Novelly.Api.Imports;
using Novelly.Api.Novels;
namespace Novelly.Api.Tests;
[TestFixture]
public class ImportServiceTests : ServiceTestFixture
{
private string _root = null!;
private Channel<Guid> _queue = null!;
private ImportService _imports = null!;
protected override void OnSetUp()
{
_root = Directory.CreateTempSubdirectory("novelly-import-test-").FullName;
_queue = Channel.CreateUnbounded<Guid>();
_imports = new ImportService(
Db.Context,
Novels,
_queue,
UserContext,
new CapturingLogger<ImportService>(),
new InspectImportRequestValidator(),
new StartImportRequestValidator());
}
[TearDown]
public void CleanUpTempFolder()
{
if (Directory.Exists(_root))
{
Directory.Delete(_root, recursive: true);
}
}
[Test]
public async Task Inspecting_a_folder_with_no_ledger_reports_fresh()
{
WriteChapterFiles(3);
var inspection = await _imports.InspectAsync(new InspectImportRequest(_root));
Assert.Multiple(() =>
{
Assert.That(inspection.Readiness, Is.EqualTo(ImportReadiness.Fresh));
Assert.That(inspection.NovelId, Is.Null);
Assert.That(inspection.ChaptersTotal, Is.EqualTo(3));
Assert.That(inspection.ChaptersCompleted, Is.EqualTo(0));
});
}
[Test]
public async Task Inspecting_a_folder_with_an_incomplete_ledger_reports_resumable()
{
WriteChapterFiles(3);
WriteLedger("""{"novelId": "11111111-1111-1111-1111-111111111111", "completedPasses": ["novel"], "completedChapters": [1]}""");
var inspection = await _imports.InspectAsync(new InspectImportRequest(_root));
Assert.Multiple(() =>
{
Assert.That(inspection.Readiness, Is.EqualTo(ImportReadiness.Resumable));
Assert.That(inspection.ChaptersCompleted, Is.EqualTo(1));
Assert.That(inspection.ChaptersTotal, Is.EqualTo(3));
});
}
[Test]
public async Task Inspecting_a_folder_whose_ledger_covers_every_pass_and_chapter_reports_complete()
{
WriteChapterFiles(2);
WriteLedger("""
{
"novelId": "11111111-1111-1111-1111-111111111111",
"completedPasses": ["novel", "characters", "chapters", "arcs"],
"completedChapters": [1, 2]
}
""");
var inspection = await _imports.InspectAsync(new InspectImportRequest(_root));
Assert.That(inspection.Readiness, Is.EqualTo(ImportReadiness.Complete));
}
[Test]
public async Task Starting_an_import_enqueues_a_pending_job()
{
var job = await _imports.StartOrResumeAsync(new StartImportRequest(_root));
Assert.Multiple(() =>
{
Assert.That(job.Status, Is.EqualTo(ImportJobStatus.Pending));
Assert.That(job.SourceRoot, Is.EqualTo(_root));
});
Assert.Multiple(() =>
{
Assert.That(_queue.Reader.TryRead(out var queued), Is.True);
Assert.That(queued, Is.EqualTo(job.Id));
});
}
[Test]
public async Task Starting_an_import_a_second_time_reuses_the_pending_job_instead_of_duplicating_it()
{
var first = await _imports.StartOrResumeAsync(new StartImportRequest(_root));
var second = await _imports.StartOrResumeAsync(new StartImportRequest(_root));
Assert.That(second.Id, Is.EqualTo(first.Id));
Assert.Multiple(() =>
{
Assert.That(_queue.Reader.TryRead(out _), Is.True);
Assert.That(_queue.Reader.TryRead(out _), Is.False);
});
}
[Test]
public async Task Force_restarting_a_completed_import_deletes_the_ledger_and_its_novel()
{
var novel = await Novels.CreateAsync(new CreateNovelRequest("The Blade Itself"));
WriteLedger($$"""{"novelId": "{{novel.Id}}", "completedPasses": ["novel", "characters", "chapters", "arcs"], "completedChapters": [1]}""");
await _imports.StartOrResumeAsync(new StartImportRequest(_root, ForceRestart: true));
Assert.Multiple(() =>
{
Assert.That(File.Exists(Path.Combine(_root, ".novelly-import.json")), Is.False);
Assert.That(Novels.GetAsync(novel.Id).Result, Is.Null);
});
}
private void WriteChapterFiles(int count)
{
var outlines = Path.Combine(_root, "outlines");
Directory.CreateDirectory(outlines);
for (var i = 1; i <= count; i++)
{
File.WriteAllText(Path.Combine(outlines, $"{i:D2}-chapter.md"), $"# Chapter {i}");
}
}
private void WriteLedger(string json) =>
File.WriteAllText(Path.Combine(_root, ".novelly-import.json"), json);
}