Files
novelly/tests/Novelly.Api.Tests/LocationServiceTests.cs
T
James Wampler aca26588f9
CI / build-and-push (push) Failing after 31s
CI / deploy (push) Has been skipped
Add soft delete + trash, keyboard-first web overhaul, move chapter tags to bottom
Adds SoftDelete/Trash across characters, chapters, locations, beats with a
purge schedule and Trash page. Reworks the web client for keyboard-driven
navigation (focus helpers, help overlay, keyboard.md doc). Moves the
ChapterPage tag editor to the bottom of the page to match CharacterDetailPage.
2026-08-20 16:39:09 -07:00

175 lines
6.5 KiB
C#

using Microsoft.EntityFrameworkCore;
using Novelly.Api.Chapters;
using Novelly.Api.Locations;
using Novelly.Api.Novels;
namespace Novelly.Api.Tests;
[TestFixture]
public class LocationServiceTests : ServiceTestFixture
{
private Guid _novelId;
protected override void OnSetUp() =>
_novelId = Novels.CreateAsync(new CreateNovelRequest("The Salt Road")).Result.Id;
[Test]
public async Task Applying_an_unknown_location_by_name_creates_it()
{
var chapter = await Chapters.CreateAsync(
_novelId, new CreateChapterRequest("Landfall", Locations: ["the harbour", "the wreck"]));
Assert.Multiple(async () =>
{
Assert.That(
chapter!.Locations.Select(l => l.Name),
Is.EquivalentTo(new[] { "the harbour", "the wreck" }));
Assert.That(await Locations.ListAsync(_novelId), Has.Count.EqualTo(2));
});
}
[Test]
public async Task The_same_name_resolves_to_one_location_regardless_of_casing()
{
var first = await Chapters.CreateAsync(
_novelId, new CreateChapterRequest("Landfall", Locations: ["The Harbour"]));
var second = await Chapters.CreateAsync(
_novelId, new CreateChapterRequest("Departure", Locations: ["the harbour"]));
var listed = await Locations.ListAsync(_novelId);
Assert.Multiple(() =>
{
Assert.That(listed, Has.Count.EqualTo(1));
Assert.That(listed[0].Name, Is.EqualTo("The Harbour"));
Assert.That(first!.Locations, Has.Count.EqualTo(1));
Assert.That(second!.Locations[0].Id, Is.EqualTo(first.Locations[0].Id));
});
}
[Test]
public async Task Supplying_a_location_list_replaces_the_existing_locations()
{
var chapter = await Chapters.CreateAsync(
_novelId, new CreateChapterRequest("Landfall", Locations: ["the harbour", "the wreck"]));
var updated = (await Chapters.UpdateAsync(
chapter!.Id, new UpdateChapterRequest(Locations: ["the wreck", "the cliffs"])))!;
Assert.That(updated.Locations.Select(l => l.Name), Is.EquivalentTo(new[] { "the wreck", "the cliffs" }));
}
[Test]
public async Task Omitting_the_location_list_leaves_locations_alone()
{
var chapter = await Chapters.CreateAsync(
_novelId, new CreateChapterRequest("Landfall", Locations: ["the harbour"]));
var updated = (await Chapters.UpdateAsync(
chapter!.Id, new UpdateChapterRequest(Summary: "Ships come in.")))!;
Assert.Multiple(() =>
{
Assert.That(updated.Locations, Has.Count.EqualTo(1));
Assert.That(updated.Locations[0].Name, Is.EqualTo("the harbour"));
Assert.That(updated.Summary, Is.EqualTo("Ships come in."));
});
}
[Test]
public async Task Cross_reference_gathers_every_chapter_at_a_location()
{
await Chapters.CreateAsync(_novelId, new CreateChapterRequest("Landfall", Locations: ["the harbour"]));
await Chapters.CreateAsync(_novelId, new CreateChapterRequest("Unrelated chapter"));
var locationId = (await Locations.ListAsync(_novelId)).Single().Id;
var references = (await Locations.GetReferencesAsync(locationId))!;
Assert.Multiple(() =>
{
Assert.That(references.Chapters, Has.Count.EqualTo(1));
Assert.That(references.Chapters[0].Title, Is.EqualTo("Landfall"));
});
}
[Test]
public async Task Usage_counts_are_reported()
{
await Chapters.CreateAsync(_novelId, new CreateChapterRequest("Landfall", Locations: ["the harbour"]));
await Chapters.CreateAsync(_novelId, new CreateChapterRequest("Departure", Locations: ["the harbour"]));
var summary = (await Locations.ListAsync(_novelId)).Single();
Assert.That(summary.ChapterCount, Is.EqualTo(2));
}
[Test]
public async Task Duplicate_location_names_are_refused_on_create_and_rename()
{
await Locations.CreateAsync(_novelId, new CreateLocationRequest("the harbour"));
Assert.That(
async () => await Locations.CreateAsync(_novelId, new CreateLocationRequest("The Harbour")),
Throws.TypeOf<InvalidOperationException>().With.Message.Contains("already has a location"));
var other = await Locations.CreateAsync(_novelId, new CreateLocationRequest("the wreck"));
Assert.That(
async () => await Locations.UpdateAsync(other!.Id, new UpdateLocationRequest(Name: "the harbour")),
Throws.TypeOf<InvalidOperationException>().With.Message.Contains("already has a location"));
}
[Test]
public async Task Locations_are_scoped_to_their_novel()
{
var otherNovel = await Novels.CreateAsync(new CreateNovelRequest("Other Book"));
await Chapters.CreateAsync(_novelId, new CreateChapterRequest("Landfall", Locations: ["the harbour"]));
await Chapters.CreateAsync(otherNovel.Id, new CreateChapterRequest("Somewhere", Locations: ["the harbour"]));
using var verification = Db.CreateContext();
Assert.Multiple(async () =>
{
Assert.That(await Locations.ListAsync(_novelId), Has.Count.EqualTo(1));
Assert.That(await Locations.ListAsync(otherNovel.Id), Has.Count.EqualTo(1));
Assert.That(await verification.Locations.CountAsync(), Is.EqualTo(2));
});
}
[Test]
public async Task Deleting_a_location_leaves_the_chapter_intact()
{
var chapter = await Chapters.CreateAsync(
_novelId, new CreateChapterRequest("Landfall", Locations: ["the harbour"]));
var locationId = (await Locations.ListAsync(_novelId)).Single().Id;
await Locations.DeleteAsync(locationId);
var survivor = (await Chapters.GetAsync(chapter!.Id))!;
Assert.Multiple(() =>
{
Assert.That(survivor.Title, Is.EqualTo("Landfall"));
Assert.That(survivor.ToResponse().Locations, Is.Empty);
});
}
[Test]
public async Task Deleting_a_novel_takes_its_locations()
{
await Chapters.CreateAsync(_novelId, new CreateChapterRequest("Landfall", Locations: ["the harbour"]));
await Novels.DeleteAsync(_novelId);
using var verification = Db.CreateContext();
Assert.That(await verification.Locations.CountAsync(), Is.EqualTo(0));
}
[Test]
public void A_blank_location_name_is_refused() =>
Assert.That(
async () => await Locations.CreateAsync(_novelId, new CreateLocationRequest(" ")),
Throws.TypeOf<ArgumentException>());
}