using NovelSoftware.Application; using NovelSoftware.Application.Dtos; namespace NovelSoftware.Tests; [TestFixture] public class BeatServiceTests : ServiceTestFixture { private Guid _projectId; private Guid _chapterId; protected override void OnSetUp() { _projectId = Projects.CreateAsync(new CreateProjectRequest("The Salt Road")).Result.Id; _chapterId = Chapters.CreateAsync(_projectId, new CreateChapterRequest("Landfall")).Result.Id; } [Test] public async Task Beats_are_appended_in_order_and_listed_that_way() { await Beats.CreateAsync(_chapterId, new CreateBeatRequest("She finds the map")); await Beats.CreateAsync(_chapterId, new CreateBeatRequest("The harbour burns")); await Beats.CreateAsync(_chapterId, new CreateBeatRequest("She boards anyway")); var listed = await Beats.ListAsync(_chapterId); Assert.Multiple(() => { Assert.That( listed.Select(b => b.Title), Is.EqualTo(new[] { "She finds the map", "The harbour burns", "She boards anyway" })); Assert.That(listed.Select(b => b.SortOrder), Is.EqualTo(new[] { 1, 2, 3 })); }); } [Test] public async Task Reordering_renumbers_to_match_the_order_given() { var first = await Beats.CreateAsync(_chapterId, new CreateBeatRequest("First")); var second = await Beats.CreateAsync(_chapterId, new CreateBeatRequest("Second")); var third = await Beats.CreateAsync(_chapterId, new CreateBeatRequest("Third")); var reordered = await Beats.ReorderAsync( _chapterId, new ReorderBeatsRequest([third.Id, first.Id, second.Id])); Assert.That( reordered.Select(b => b.Title), Is.EqualTo(new[] { "Third", "First", "Second" })); } [Test] public async Task Beats_left_out_of_a_reorder_keep_their_relative_position_at_the_end() { var first = await Beats.CreateAsync(_chapterId, new CreateBeatRequest("First")); await Beats.CreateAsync(_chapterId, new CreateBeatRequest("Second")); var third = await Beats.CreateAsync(_chapterId, new CreateBeatRequest("Third")); var reordered = await Beats.ReorderAsync(_chapterId, new ReorderBeatsRequest([third.Id, first.Id])); Assert.That( reordered.Select(b => b.Title), Is.EqualTo(new[] { "Third", "First", "Second" })); } [Test] public void Reordering_with_an_unknown_beat_is_refused() { Beats.CreateAsync(_chapterId, new CreateBeatRequest("First")).Wait(); Assert.That( async () => await Beats.ReorderAsync(_chapterId, new ReorderBeatsRequest([Guid.NewGuid()])), Throws.TypeOf()); } [Test] public async Task A_beat_resolves_its_character_and_scene_names() { var ines = await Characters.CreateAsync(_projectId, new CreateCharacterRequest("Ines")); var scene = await Scenes.CreateAsync(_chapterId, new CreateSceneRequest("The dock at dawn")); var beat = await Beats.CreateAsync(_chapterId, new CreateBeatRequest( "She burns the atlas", CharacterId: ines.Id, WhatHappened: "The pages go up faster than she expected.", WhatsNext: "Nothing to navigate by but memory.", SceneId: scene.Id)); Assert.Multiple(() => { Assert.That(beat.CharacterName, Is.EqualTo("Ines")); Assert.That(beat.SceneTitle, Is.EqualTo("The dock at dawn")); Assert.That(beat.WhatHappened, Does.Contain("faster than she expected")); }); } [Test] public async Task A_beat_cannot_borrow_a_character_from_another_project() { var other = await Projects.CreateAsync(new CreateProjectRequest("Other Book")); var stranger = await Characters.CreateAsync(other.Id, new CreateCharacterRequest("Stranger")); Assert.That( async () => await Beats.CreateAsync( _chapterId, new CreateBeatRequest("A beat", CharacterId: stranger.Id)), Throws.TypeOf().With.Message.Contains("same project")); } [Test] public async Task A_beat_cannot_be_grouped_under_a_scene_from_another_chapter() { var elsewhere = await Chapters.CreateAsync(_projectId, new CreateChapterRequest("Elsewhere")); var scene = await Scenes.CreateAsync(elsewhere.Id, new CreateSceneRequest("Another scene")); Assert.That( async () => await Beats.CreateAsync( _chapterId, new CreateBeatRequest("A beat", SceneId: scene.Id)), Throws.TypeOf().With.Message.Contains("same chapter")); } [Test] public async Task Deleting_a_scene_leaves_its_beats_alone() { var scene = await Scenes.CreateAsync(_chapterId, new CreateSceneRequest("The dock at dawn")); var beat = await Beats.CreateAsync( _chapterId, new CreateBeatRequest("She burns the atlas", SceneId: scene.Id)); await Scenes.DeleteAsync(scene.Id); // The plan outlives a decision about prose — the beat is simply ungrouped. var survivor = await Beats.GetAsync(beat.Id); Assert.Multiple(() => { Assert.That(survivor.SceneId, Is.Null); Assert.That(survivor.Title, Is.EqualTo("She burns the atlas")); }); } [Test] public async Task Updating_leaves_omitted_fields_alone_and_clears_on_empty_string() { var beat = await Beats.CreateAsync(_chapterId, new CreateBeatRequest( "She finds the map", WhatHappened: "Behind the lining of the case.", WhatsNext: "She books passage.")); var renamed = await Beats.UpdateAsync(beat.Id, new UpdateBeatRequest(Title: "She finds it")); Assert.Multiple(() => { Assert.That(renamed.WhatHappened, Is.EqualTo("Behind the lining of the case.")); Assert.That(renamed.WhatsNext, Is.EqualTo("She books passage.")); }); var cleared = await Beats.UpdateAsync(beat.Id, new UpdateBeatRequest(WhatsNext: "")); Assert.Multiple(() => { Assert.That(cleared.WhatsNext, Is.Null); Assert.That(cleared.WhatHappened, Is.EqualTo("Behind the lining of the case.")); }); } }