using System.Threading.Channels; using Novelly.Api.Imports; using Novelly.Api.Projects; namespace Novelly.Api.Tests; [TestFixture] public class ImportServiceTests : ServiceTestFixture { private string _root = null!; private Channel _queue = null!; private ImportService _imports = null!; protected override void OnSetUp() { _root = Directory.CreateTempSubdirectory("novelly-import-test-").FullName; _queue = Channel.CreateUnbounded(); _imports = new ImportService( Db.Context, Projects, _queue, new CapturingLogger(), 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.ProjectId, 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("""{"projectId": "11111111-1111-1111-1111-111111111111", "completedPasses": ["project"], "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(""" { "projectId": "11111111-1111-1111-1111-111111111111", "completedPasses": ["project", "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.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)); // Only one job was ever queued. 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_project() { var project = await Projects.CreateAsync(new CreateProjectRequest("The Blade Itself")); WriteLedger($$"""{"projectId": "{{project.Id}}", "completedPasses": ["project", "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(Projects.GetAsync(project.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); }