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().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().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.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()); }