using FluentAssertions; using Microsoft.EntityFrameworkCore; using NovelSoftware.Application; using NovelSoftware.Application.Dtos; using NovelSoftware.Application.Services; using NovelSoftware.Domain; namespace NovelSoftware.Tests; public class ProjectDataTests : IDisposable { private readonly TestDatabase _db = new(); private readonly TagService _tags; private readonly ProjectService _projects; private readonly CharacterService _characters; private readonly ChapterService _chapters; private readonly SceneService _scenes; public ProjectDataTests() { _tags = new TagService(_db.Context); _projects = new ProjectService(_db.Context); _characters = new CharacterService(_db.Context, _tags); _chapters = new ChapterService(_db.Context, _tags); _scenes = new SceneService(_db.Context); } private async Task NewProjectAsync() => (await _projects.CreateAsync(new CreateProjectRequest("The Salt Road"))).Id; [Fact] public async Task Updating_leaves_omitted_fields_alone_and_clears_on_empty_string() { var id = (await _projects.CreateAsync( new CreateProjectRequest("Draft", Genre: "Fantasy", Logline: "A cartographer goes to sea."))).Id; var afterPartialUpdate = await _projects.UpdateAsync(id, new UpdateProjectRequest(Title: "The Salt Road")); afterPartialUpdate.Title.Should().Be("The Salt Road"); afterPartialUpdate.Genre.Should().Be("Fantasy"); afterPartialUpdate.Logline.Should().Be("A cartographer goes to sea."); var afterClear = await _projects.UpdateAsync(id, new UpdateProjectRequest(Genre: "")); afterClear.Genre.Should().BeNull(); afterClear.Logline.Should().Be("A cartographer goes to sea."); } [Fact] public async Task Chapters_are_numbered_in_sequence_when_no_number_is_given() { var projectId = await NewProjectAsync(); var first = await _chapters.CreateAsync(projectId, new CreateChapterRequest("Landfall")); var second = await _chapters.CreateAsync(projectId, new CreateChapterRequest("The Harbour")); first.Number.Should().Be(1); second.Number.Should().Be(2); } [Fact] public async Task Word_count_is_recomputed_whenever_prose_changes() { var projectId = await NewProjectAsync(); var chapter = await _chapters.CreateAsync(projectId, new CreateChapterRequest("Landfall")); var scene = await _scenes.CreateAsync(chapter.Id, new CreateSceneRequest( "The dock at dawn", Prose: "Five words go right here")); scene.WordCount.Should().Be(5); var rewritten = await _scenes.UpdateAsync(scene.Id, new UpdateSceneRequest( Prose: "Now\nthere are seven words in total")); rewritten.WordCount.Should().Be(7); var cleared = await _scenes.UpdateAsync(scene.Id, new UpdateSceneRequest(Prose: "")); cleared.Prose.Should().BeNull(); cleared.WordCount.Should().Be(0); } [Fact] public async Task Scene_updates_that_omit_prose_leave_the_draft_untouched() { var projectId = await NewProjectAsync(); var chapter = await _chapters.CreateAsync(projectId, new CreateChapterRequest("Landfall")); var scene = await _scenes.CreateAsync(chapter.Id, new CreateSceneRequest( "The dock at dawn", Prose: "The tide came in slow.")); var updated = await _scenes.UpdateAsync(scene.Id, new UpdateSceneRequest(Status: DraftStatus.Revised)); updated.Prose.Should().Be("The tide came in slow."); updated.WordCount.Should().Be(5); updated.Status.Should().Be(DraftStatus.Revised); } [Fact] public async Task Deleting_a_project_takes_its_characters_chapters_and_scenes() { var projectId = await NewProjectAsync(); await _characters.CreateAsync(projectId, new CreateCharacterRequest("Ines")); var chapter = await _chapters.CreateAsync(projectId, new CreateChapterRequest("Landfall")); await _scenes.CreateAsync(chapter.Id, new CreateSceneRequest("The dock at dawn")); await _projects.DeleteAsync(projectId); using var verification = _db.CreateContext(); (await verification.Projects.CountAsync()).Should().Be(0); (await verification.Characters.CountAsync()).Should().Be(0); (await verification.Chapters.CountAsync()).Should().Be(0); (await verification.Scenes.CountAsync()).Should().Be(0); } [Fact] public async Task Relating_characters_across_projects_is_refused() { var firstProject = await NewProjectAsync(); var secondProject = (await _projects.CreateAsync(new CreateProjectRequest("Other Book"))).Id; var ines = await _characters.CreateAsync(firstProject, new CreateCharacterRequest("Ines")); var stranger = await _characters.CreateAsync(secondProject, new CreateCharacterRequest("Stranger")); var relate = async () => await _characters.AddRelationshipAsync( ines.Id, new CreateRelationshipRequest(stranger.Id, "sister")); await relate.Should().ThrowAsync() .WithMessage("*same project*"); } [Fact] public async Task Relationships_resolve_the_other_character_by_name() { var projectId = await NewProjectAsync(); var ines = await _characters.CreateAsync(projectId, new CreateCharacterRequest("Ines")); var mara = await _characters.CreateAsync(projectId, new CreateCharacterRequest("Mara")); var updated = await _characters.AddRelationshipAsync( ines.Id, new CreateRelationshipRequest(mara.Id, "sister", "Estranged since the fire.")); updated.Relationships.Should().ContainSingle() .Which.RelatedCharacterName.Should().Be("Mara"); } [Fact] public async Task Reading_a_missing_project_reports_not_found() { var get = async () => await _projects.GetAsync(Guid.NewGuid()); await get.Should().ThrowAsync(); } public void Dispose() => _db.Dispose(); }