using Novelly.Api.Beats; using Novelly.Api.Chapters; using Novelly.Api.Characters; using Novelly.Api.Common; using Novelly.Api.Projects; namespace Novelly.Api.Tests; [TestFixture] public class CharacterArcTests : ServiceTestFixture { private Guid _projectId; private Guid _characterId; protected override void OnSetUp() { _projectId = Projects.CreateAsync(new CreateProjectRequest("The Salt Road")).Result.Id; _characterId = Characters.CreateAsync( _projectId, new CreateCharacterRequest("Ines", CharacterRole.Protagonist, CharacterImportance.Main)) .Result.Id; } [Test] public async Task A_character_is_supporting_until_promoted() { var mara = await Characters.CreateAsync(_projectId, new CreateCharacterRequest("Mara")); Assert.That(mara.Importance, Is.EqualTo(CharacterImportance.Supporting)); var promoted = (await Characters.UpdateAsync( mara.Id, new UpdateCharacterRequest(Importance: CharacterImportance.Main)))!; Assert.That(promoted.Importance, Is.EqualTo(CharacterImportance.Main)); } [Test] public async Task Importance_is_separate_from_the_part_a_character_plays() { var mentor = await Characters.CreateAsync(_projectId, new CreateCharacterRequest( "Anders", CharacterRole.Mentor, CharacterImportance.Main)); Assert.Multiple(() => { Assert.That(mentor.Role, Is.EqualTo(CharacterRole.Mentor)); Assert.That(mentor.Importance, Is.EqualTo(CharacterImportance.Main)); }); } [Test] public async Task Main_characters_are_listed_before_supporting_ones() { await Characters.CreateAsync(_projectId, new CreateCharacterRequest("Zeno")); await Characters.CreateAsync( _projectId, new CreateCharacterRequest("Mara", Importance: CharacterImportance.Main)); var listed = await Characters.ListAsync(_projectId); Assert.That( listed.Select(c => c.Name), Is.EqualTo(new[] { "Ines", "Mara", "Zeno" })); } [Test] public async Task Within_a_group_the_lead_comes_before_the_second_lead() { await Characters.CreateAsync(_projectId, new CreateCharacterRequest( "Mara", CharacterRole.Deuteragonist, CharacterImportance.Main)); await Characters.CreateAsync(_projectId, new CreateCharacterRequest( "Anders", CharacterRole.Antagonist, CharacterImportance.Main)); var listed = await Characters.ListAsync(_projectId); Assert.That( listed.Select(c => c.Name), Is.EqualTo(new[] { "Ines", "Anders", "Mara" })); } [Test] public async Task Arc_stages_are_appended_in_order_and_read_back_that_way() { await Arcs.CreateAsync(_characterId, new CreateArcStageRequest("She trusts the map")); await Arcs.CreateAsync(_characterId, new CreateArcStageRequest("The map is wrong")); await Arcs.CreateAsync(_characterId, new CreateArcStageRequest("She navigates by memory")); var arc = await Arcs.ListAsync(_characterId); Assert.Multiple(() => { Assert.That( arc.Select(s => s.Title), Is.EqualTo(new[] { "She trusts the map", "The map is wrong", "She navigates by memory" })); Assert.That(arc.Select(s => s.SortOrder), Is.EqualTo(new[] { 1, 2, 3 })); }); } [Test] public async Task A_character_dossier_carries_its_arc() { await Arcs.CreateAsync(_characterId, new CreateArcStageRequest( "She trusts the map", Description: "Because her mother drew it.")); var character = (await Characters.GetAsync(_characterId))!; Assert.Multiple(() => { Assert.That(character.ArcStages, Has.Count.EqualTo(1)); Assert.That(character.ArcStages[0].Description, Does.Contain("her mother drew it")); }); } [Test] public async Task Stages_left_out_of_a_reorder_keep_their_relative_position_at_the_end() { var first = await Arcs.CreateAsync(_characterId, new CreateArcStageRequest("First")); await Arcs.CreateAsync(_characterId, new CreateArcStageRequest("Second")); var third = await Arcs.CreateAsync(_characterId, new CreateArcStageRequest("Third")); var reordered = await Arcs.ReorderAsync( _characterId, new ReorderArcStagesRequest([third.Id, first.Id])); Assert.That( reordered.Select(s => s.Title), Is.EqualTo(new[] { "Third", "First", "Second" })); } [Test] public async Task A_stage_pinned_to_a_chapter_resolves_that_chapter() { var chapter = await Chapters.CreateAsync(_projectId, new CreateChapterRequest("Landfall")); var stage = await Arcs.CreateAsync( _characterId, new CreateArcStageRequest("The map is wrong", ChapterId: chapter.Id)); Assert.Multiple(() => { Assert.That(stage.Chapter!.Title, Is.EqualTo("Landfall")); Assert.That(stage.Chapter!.Number, Is.EqualTo(1)); }); } [Test] public async Task A_stage_cannot_be_pinned_to_a_chapter_from_another_project() { var other = await Projects.CreateAsync(new CreateProjectRequest("Other Book")); var elsewhere = await Chapters.CreateAsync(other.Id, new CreateChapterRequest("Elsewhere")); Assert.That( async () => await Arcs.CreateAsync( _characterId, new CreateArcStageRequest("A stage", ChapterId: elsewhere.Id)), Throws.TypeOf().With.Message.Contains("same project")); } [Test] public async Task Deleting_a_chapter_unpins_an_arc_stage_rather_than_deleting_it() { var chapter = await Chapters.CreateAsync(_projectId, new CreateChapterRequest("Landfall")); var stage = await Arcs.CreateAsync( _characterId, new CreateArcStageRequest("The map is wrong", ChapterId: chapter.Id)); await Chapters.DeleteAsync(chapter.Id); var survivor = (await Arcs.GetAsync(stage.Id))!; Assert.Multiple(() => { Assert.That(survivor.ChapterId, Is.Null); Assert.That(survivor.Title, Is.EqualTo("The map is wrong")); }); } [Test] public async Task Deleting_a_character_takes_their_arc() { await Arcs.CreateAsync(_characterId, new CreateArcStageRequest("She trusts the map")); await Characters.DeleteAsync(_characterId); Assert.That(await Arcs.ListAsync(_characterId), Is.Empty); } [Test] public async Task Reordering_with_an_unknown_stage_returns_null_rather_than_throwing() => Assert.That( await Arcs.ReorderAsync(_characterId, new ReorderArcStagesRequest([Guid.NewGuid()])), Is.Null); [Test] public async Task The_character_page_sees_every_beat_they_appear_in_across_the_book() { var second = await Chapters.CreateAsync(_projectId, new CreateChapterRequest("Second", Number: 2)); var first = await Chapters.CreateAsync(_projectId, new CreateChapterRequest("First", Number: 1)); var mara = await Characters.CreateAsync(_projectId, new CreateCharacterRequest("Mara")); await Beats.CreateAsync(second.Id, new CreateBeatRequest("She boards anyway", CharacterIds: [_characterId])); await Beats.CreateAsync(first.Id, new CreateBeatRequest("She finds the map", CharacterIds: [_characterId])); await Beats.CreateAsync(first.Id, new CreateBeatRequest("Mara lies", CharacterIds: [mara.Id])); await Beats.CreateAsync(first.Id, new CreateBeatRequest("Nobody's beat")); var beats = (await Beats.ListForCharacterAsync(_characterId))!; Assert.Multiple(() => { Assert.That(beats.Select(b => b.Title), Is.EqualTo(new[] { "She finds the map", "She boards anyway" })); Assert.That(beats[0].Chapter!.Number, Is.EqualTo(1)); Assert.That(beats[0].Chapter!.Title, Is.EqualTo("First")); Assert.That(beats[1].ChapterId, Is.EqualTo(second.Id)); }); } [Test] public async Task Asking_for_the_beats_of_a_character_who_does_not_exist_returns_null_rather_than_throwing() => Assert.That(await Beats.ListForCharacterAsync(Guid.NewGuid()), Is.Null); }