Chapters can now be marked FrontMatter/Body/BackMatter. Number stays the manuscript sort key for every chapter; the author-facing display number is now computed per-request as the chapter's ordinal among Body chapters only, so a foreword or afterword no longer shifts the numbering of the rest of the book. Surfaced through the API, agent toolset, and import toolset.
140 lines
5.3 KiB
C#
140 lines
5.3 KiB
C#
using Microsoft.EntityFrameworkCore;
|
|
using Novelly.Api.Beats;
|
|
using Novelly.Api.Chapters;
|
|
using Novelly.Api.Common;
|
|
using Novelly.Api.Novels;
|
|
|
|
namespace Novelly.Api.Tests;
|
|
|
|
[TestFixture]
|
|
public class ChapterServiceTests : ServiceTestFixture
|
|
{
|
|
private Guid _novelId;
|
|
|
|
protected override void OnSetUp()
|
|
{
|
|
_novelId = Novels.CreateAsync(new CreateNovelRequest("The Salt Road")).Result.Id;
|
|
}
|
|
|
|
[Test]
|
|
public async Task Setting_a_number_that_already_exists_is_still_stored_as_given()
|
|
{
|
|
var first = await Chapters.CreateAsync(_novelId, new CreateChapterRequest("Landfall", Number: 5));
|
|
var second = await Chapters.CreateAsync(_novelId, new CreateChapterRequest("The Harbour", Number: 5));
|
|
|
|
Assert.Multiple(() =>
|
|
{
|
|
Assert.That(first.Number, Is.EqualTo(5));
|
|
Assert.That(second.Number, Is.EqualTo(5));
|
|
});
|
|
}
|
|
|
|
[Test]
|
|
public async Task Updating_leaves_omitted_fields_alone_and_clears_notes_on_empty_string()
|
|
{
|
|
var chapter = await Chapters.CreateAsync(_novelId, new CreateChapterRequest(
|
|
"Landfall", Summary: "The ship makes shore.", Notes: "Check the tide tables."));
|
|
|
|
var renamed = (await Chapters.UpdateAsync(chapter.Id, new UpdateChapterRequest(Title: "First Landfall")))!;
|
|
|
|
Assert.Multiple(() =>
|
|
{
|
|
Assert.That(renamed.Title, Is.EqualTo("First Landfall"));
|
|
Assert.That(renamed.Summary, Is.EqualTo("The ship makes shore."));
|
|
Assert.That(renamed.Notes, Is.EqualTo("Check the tide tables."));
|
|
});
|
|
|
|
var cleared = (await Chapters.UpdateAsync(chapter.Id, new UpdateChapterRequest(Notes: "")))!;
|
|
|
|
Assert.Multiple(() =>
|
|
{
|
|
Assert.That(cleared.Notes, Is.Null);
|
|
Assert.That(cleared.Summary, Is.EqualTo("The ship makes shore."));
|
|
});
|
|
}
|
|
|
|
[Test]
|
|
public async Task Deleting_a_chapter_takes_its_beats_with_it()
|
|
{
|
|
var chapter = await Chapters.CreateAsync(_novelId, new CreateChapterRequest("Landfall"));
|
|
await Beats.CreateAsync(chapter.Id, new CreateBeatRequest("She finds the map"));
|
|
|
|
await Chapters.DeleteAsync(chapter.Id);
|
|
|
|
using var verification = Db.CreateContext();
|
|
Assert.That(await verification.Beats.CountAsync(), Is.EqualTo(0));
|
|
}
|
|
|
|
[Test]
|
|
public async Task Creating_a_chapter_under_a_missing_novel_returns_null_rather_than_throwing() =>
|
|
Assert.That(
|
|
await Chapters.CreateAsync(Guid.NewGuid(), new CreateChapterRequest("Landfall")),
|
|
Is.Null);
|
|
|
|
[Test]
|
|
public async Task Reading_a_missing_chapter_returns_null_rather_than_throwing() =>
|
|
Assert.That(await Chapters.GetAsync(Guid.NewGuid()), Is.Null);
|
|
|
|
[Test]
|
|
public async Task Deleting_a_missing_chapter_returns_false_rather_than_throwing() =>
|
|
Assert.That(await Chapters.DeleteAsync(Guid.NewGuid()), Is.False);
|
|
|
|
[Test]
|
|
public async Task A_chapter_defaults_to_a_body_chapter()
|
|
{
|
|
var chapter = await Chapters.CreateAsync(_novelId, new CreateChapterRequest("Landfall"));
|
|
|
|
Assert.That(chapter!.Kind, Is.EqualTo(ChapterKind.Body));
|
|
}
|
|
|
|
[Test]
|
|
public async Task Front_matter_does_not_consume_a_chapter_number()
|
|
{
|
|
var foreword = await Chapters.CreateAsync(_novelId, new CreateChapterRequest("Foreword", Number: 1, Kind: ChapterKind.FrontMatter));
|
|
var first = await Chapters.CreateAsync(_novelId, new CreateChapterRequest("Landfall", Number: 2));
|
|
var second = await Chapters.CreateAsync(_novelId, new CreateChapterRequest("The Harbour", Number: 3));
|
|
|
|
var displayNumbers = ChapterNumbering.DisplayNumbers(await Chapters.ListAsync(_novelId));
|
|
|
|
Assert.Multiple(() =>
|
|
{
|
|
Assert.That(displayNumbers.ContainsKey(foreword!.Id), Is.False);
|
|
Assert.That(displayNumbers[first!.Id], Is.EqualTo(1));
|
|
Assert.That(displayNumbers[second!.Id], Is.EqualTo(2));
|
|
});
|
|
}
|
|
|
|
[Test]
|
|
public async Task Back_matter_is_listed_last_but_carries_no_chapter_number()
|
|
{
|
|
var body = await Chapters.CreateAsync(_novelId, new CreateChapterRequest("Landfall", Number: 1));
|
|
var afterword = await Chapters.CreateAsync(_novelId, new CreateChapterRequest("Afterword", Number: 2, Kind: ChapterKind.BackMatter));
|
|
|
|
var afterwordDisplayNumber = await Chapters.DisplayNumberAsync(afterword!);
|
|
var bodyDisplayNumber = await Chapters.DisplayNumberAsync(body!);
|
|
|
|
Assert.Multiple(() =>
|
|
{
|
|
Assert.That(afterwordDisplayNumber, Is.Null);
|
|
Assert.That(bodyDisplayNumber, Is.EqualTo(1));
|
|
});
|
|
}
|
|
|
|
[Test]
|
|
public async Task Changing_a_chapter_to_front_matter_drops_it_from_the_chapter_count()
|
|
{
|
|
var chapter = await Chapters.CreateAsync(_novelId, new CreateChapterRequest("Prologue", Number: 1));
|
|
var other = await Chapters.CreateAsync(_novelId, new CreateChapterRequest("Landfall", Number: 2));
|
|
|
|
var updated = await Chapters.UpdateAsync(chapter!.Id, new UpdateChapterRequest(Kind: ChapterKind.FrontMatter));
|
|
var updatedDisplayNumber = await Chapters.DisplayNumberAsync(updated!);
|
|
var otherDisplayNumber = await Chapters.DisplayNumberAsync(other!);
|
|
|
|
Assert.Multiple(() =>
|
|
{
|
|
Assert.That(updatedDisplayNumber, Is.Null);
|
|
Assert.That(otherDisplayNumber, Is.EqualTo(1));
|
|
});
|
|
}
|
|
}
|