Rename Project concept to Novel across the stack

Renames the domain concept from Project to Novel throughout the backend
(entities, DTOs, services, endpoints, ProjectAccessService/Permission,
ProjectId foreign keys), MCP server (tool names and routes), and the
React/Vite frontend (types, hooks, routes, components). Adds a new EF
Core migration (RenameProjectToNovel) using RenameTable/RenameColumn to
preserve existing data instead of dropping/recreating tables. Updates
CLAUDE.md's structure section to reference Novels/ instead of Projects/.
This commit is contained in:
James Wampler
2026-08-17 23:03:09 -07:00
parent 0ab4f568b5
commit 4313c8f206
95 changed files with 3192 additions and 1660 deletions
+20 -20
View File
@@ -1,20 +1,20 @@
using Novelly.Api.Beats;
using Novelly.Api.Chapters;
using Novelly.Api.Characters;
using Novelly.Api.Projects;
using Novelly.Api.Novels;
namespace Novelly.Api.Tests;
[TestFixture]
public class BeatServiceTests : ServiceTestFixture
{
private Guid _projectId;
private Guid _novelId;
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;
_novelId = Novels.CreateAsync(new CreateNovelRequest("The Salt Road")).Result.Id;
_chapterId = Chapters.CreateAsync(_novelId, new CreateChapterRequest("Landfall")).Result.Id;
}
[Test]
@@ -100,8 +100,8 @@ public class BeatServiceTests : ServiceTestFixture
[Test]
public async Task A_beat_can_carry_several_characters()
{
var ines = await Characters.CreateAsync(_projectId, new CreateCharacterRequest("Ines"));
var mara = await Characters.CreateAsync(_projectId, new CreateCharacterRequest("Mara"));
var ines = await Characters.CreateAsync(_novelId, new CreateCharacterRequest("Ines"));
var mara = await Characters.CreateAsync(_novelId, new CreateCharacterRequest("Mara"));
var beat = await Beats.CreateAsync(_chapterId, new CreateBeatRequest(
"She burns the atlas",
@@ -117,22 +117,22 @@ public class BeatServiceTests : ServiceTestFixture
}
[Test]
public async Task A_beat_cannot_borrow_a_character_from_another_project()
public async Task A_beat_cannot_borrow_a_character_from_another_novel()
{
var other = await Projects.CreateAsync(new CreateProjectRequest("Other Book"));
var other = await Novels.CreateAsync(new CreateNovelRequest("Other Book"));
var stranger = await Characters.CreateAsync(other.Id, new CreateCharacterRequest("Stranger"));
Assert.That(
async () => await Beats.CreateAsync(
_chapterId, new CreateBeatRequest("A beat", CharacterIds: [stranger.Id])),
Throws.TypeOf<InvalidOperationException>().With.Message.Contains("same project"));
Throws.TypeOf<InvalidOperationException>().With.Message.Contains("same novel"));
}
[Test]
public async Task Assigning_a_character_to_several_beats_leaves_their_other_characters_alone()
{
var ines = await Characters.CreateAsync(_projectId, new CreateCharacterRequest("Ines"));
var mara = await Characters.CreateAsync(_projectId, new CreateCharacterRequest("Mara"));
var ines = await Characters.CreateAsync(_novelId, new CreateCharacterRequest("Ines"));
var mara = await Characters.CreateAsync(_novelId, new CreateCharacterRequest("Mara"));
var first = await Beats.CreateAsync(_chapterId, new CreateBeatRequest("First", CharacterIds: [ines.Id]));
var second = await Beats.CreateAsync(_chapterId, new CreateBeatRequest("Second"));
@@ -153,7 +153,7 @@ public class BeatServiceTests : ServiceTestFixture
[Test]
public async Task Assigning_a_character_already_on_a_beat_does_not_duplicate_it()
{
var ines = await Characters.CreateAsync(_projectId, new CreateCharacterRequest("Ines"));
var ines = await Characters.CreateAsync(_novelId, new CreateCharacterRequest("Ines"));
var beat = await Beats.CreateAsync(_chapterId, new CreateBeatRequest("First", CharacterIds: [ines.Id]));
var assigned = await Beats.AssignCharacterAsync(
@@ -163,9 +163,9 @@ public class BeatServiceTests : ServiceTestFixture
}
[Test]
public async Task Assigning_a_character_from_another_project_returns_null()
public async Task Assigning_a_character_from_another_novel_returns_null()
{
var other = await Projects.CreateAsync(new CreateProjectRequest("Other Book"));
var other = await Novels.CreateAsync(new CreateNovelRequest("Other Book"));
var stranger = await Characters.CreateAsync(other.Id, new CreateCharacterRequest("Stranger"));
var beat = await Beats.CreateAsync(_chapterId, new CreateBeatRequest("First"));
@@ -177,7 +177,7 @@ public class BeatServiceTests : ServiceTestFixture
[Test]
public async Task Assigning_to_an_unknown_beat_returns_null_rather_than_partially_applying()
{
var ines = await Characters.CreateAsync(_projectId, new CreateCharacterRequest("Ines"));
var ines = await Characters.CreateAsync(_novelId, new CreateCharacterRequest("Ines"));
var beat = await Beats.CreateAsync(_chapterId, new CreateBeatRequest("First"));
var result = await Beats.AssignCharacterAsync(
@@ -190,7 +190,7 @@ public class BeatServiceTests : ServiceTestFixture
[Test]
public async Task Moving_beats_appends_them_to_the_end_of_the_target_chapter()
{
var other = await Chapters.CreateAsync(_projectId, new CreateChapterRequest("Second landfall"));
var other = await Chapters.CreateAsync(_novelId, new CreateChapterRequest("Second landfall"));
await Beats.CreateAsync(other.Id, new CreateBeatRequest("Already there"));
var first = await Beats.CreateAsync(_chapterId, new CreateBeatRequest("First"));
var second = await Beats.CreateAsync(_chapterId, new CreateBeatRequest("Second"));
@@ -216,9 +216,9 @@ public class BeatServiceTests : ServiceTestFixture
}
[Test]
public async Task Moving_to_a_chapter_in_another_project_returns_null()
public async Task Moving_to_a_chapter_in_another_novel_returns_null()
{
var other = await Projects.CreateAsync(new CreateProjectRequest("Other Book"));
var other = await Novels.CreateAsync(new CreateNovelRequest("Other Book"));
var otherChapter = await Chapters.CreateAsync(other.Id, new CreateChapterRequest("Elsewhere"));
var beat = await Beats.CreateAsync(_chapterId, new CreateBeatRequest("First"));
@@ -228,7 +228,7 @@ public class BeatServiceTests : ServiceTestFixture
[Test]
public async Task Moving_an_unknown_beat_returns_null_rather_than_partially_applying()
{
var other = await Chapters.CreateAsync(_projectId, new CreateChapterRequest("Second landfall"));
var other = await Chapters.CreateAsync(_novelId, new CreateChapterRequest("Second landfall"));
var beat = await Beats.CreateAsync(_chapterId, new CreateBeatRequest("First"));
var result = await Beats.MoveAsync(_chapterId, new MoveBeatsRequest(other.Id, [beat.Id, Guid.NewGuid()]));
@@ -265,7 +265,7 @@ public class BeatServiceTests : ServiceTestFixture
[Test]
public async Task An_empty_CharacterIds_list_clears_a_beats_characters_since_null_means_leave_it_alone()
{
var ines = await Characters.CreateAsync(_projectId, new CreateCharacterRequest("Ines"));
var ines = await Characters.CreateAsync(_novelId, new CreateCharacterRequest("Ines"));
var beat = await Beats.CreateAsync(_chapterId, new CreateBeatRequest(
"She burns the atlas", CharacterIds: [ines.Id]));
@@ -2,25 +2,25 @@ using Microsoft.EntityFrameworkCore;
using Novelly.Api.Beats;
using Novelly.Api.Chapters;
using Novelly.Api.Common;
using Novelly.Api.Projects;
using Novelly.Api.Novels;
namespace Novelly.Api.Tests;
[TestFixture]
public class ChapterServiceTests : ServiceTestFixture
{
private Guid _projectId;
private Guid _novelId;
protected override void OnSetUp()
{
_projectId = Projects.CreateAsync(new CreateProjectRequest("The Salt Road")).Result.Id;
_novelId = Novels.CreateAsync(new CreateNovelRequest("The Salt Road")).Result.Id;
}
[Test]
public async Task Setting_a_number_that_already_exists_is_still_stored_as_given()
{
var first = await Chapters.CreateAsync(_projectId, new CreateChapterRequest("Landfall", Number: 5));
var second = await Chapters.CreateAsync(_projectId, new CreateChapterRequest("The Harbour", Number: 5));
var first = await Chapters.CreateAsync(_novelId, new CreateChapterRequest("Landfall", Number: 5));
var second = await Chapters.CreateAsync(_novelId, new CreateChapterRequest("The Harbour", Number: 5));
Assert.Multiple(() =>
{
@@ -32,7 +32,7 @@ public class ChapterServiceTests : ServiceTestFixture
[Test]
public async Task Updating_leaves_omitted_fields_alone_and_clears_notes_on_empty_string()
{
var chapter = await Chapters.CreateAsync(_projectId, new CreateChapterRequest(
var chapter = await Chapters.CreateAsync(_novelId, new CreateChapterRequest(
"Landfall", Summary: "The ship makes shore.", Notes: "Check the tide tables."));
var renamed = (await Chapters.UpdateAsync(chapter.Id, new UpdateChapterRequest(Title: "First Landfall")))!;
@@ -56,7 +56,7 @@ public class ChapterServiceTests : ServiceTestFixture
[Test]
public async Task Deleting_a_chapter_takes_its_beats_with_it()
{
var chapter = await Chapters.CreateAsync(_projectId, new CreateChapterRequest("Landfall"));
var chapter = await Chapters.CreateAsync(_novelId, new CreateChapterRequest("Landfall"));
await Beats.CreateAsync(chapter.Id, new CreateBeatRequest("She finds the map"));
await Chapters.DeleteAsync(chapter.Id);
@@ -66,7 +66,7 @@ public class ChapterServiceTests : ServiceTestFixture
}
[Test]
public async Task Creating_a_chapter_under_a_missing_project_returns_null_rather_than_throwing() =>
public async Task Creating_a_chapter_under_a_missing_novel_returns_null_rather_than_throwing() =>
Assert.That(
await Chapters.CreateAsync(Guid.NewGuid(), new CreateChapterRequest("Landfall")),
Is.Null);
+25 -25
View File
@@ -2,21 +2,21 @@ using Novelly.Api.Beats;
using Novelly.Api.Chapters;
using Novelly.Api.Characters;
using Novelly.Api.Common;
using Novelly.Api.Projects;
using Novelly.Api.Novels;
namespace Novelly.Api.Tests;
[TestFixture]
public class CharacterArcTests : ServiceTestFixture
{
private Guid _projectId;
private Guid _novelId;
private Guid _characterId;
protected override void OnSetUp()
{
_projectId = Projects.CreateAsync(new CreateProjectRequest("The Salt Road")).Result.Id;
_novelId = Novels.CreateAsync(new CreateNovelRequest("The Salt Road")).Result.Id;
_characterId = Characters.CreateAsync(
_projectId,
_novelId,
new CreateCharacterRequest("Ines", CharacterRole.Protagonist, CharacterImportance.Main))
.Result.Id;
}
@@ -24,7 +24,7 @@ public class CharacterArcTests : ServiceTestFixture
[Test]
public async Task A_character_is_supporting_until_promoted()
{
var mara = await Characters.CreateAsync(_projectId, new CreateCharacterRequest("Mara"));
var mara = await Characters.CreateAsync(_novelId, new CreateCharacterRequest("Mara"));
Assert.That(mara.Importance, Is.EqualTo(CharacterImportance.Supporting));
@@ -37,7 +37,7 @@ public class CharacterArcTests : ServiceTestFixture
[Test]
public async Task Importance_is_separate_from_the_part_a_character_plays()
{
var mentor = await Characters.CreateAsync(_projectId, new CreateCharacterRequest(
var mentor = await Characters.CreateAsync(_novelId, new CreateCharacterRequest(
"Anders", CharacterRole.Mentor, CharacterImportance.Main));
Assert.Multiple(() =>
@@ -50,11 +50,11 @@ public class CharacterArcTests : ServiceTestFixture
[Test]
public async Task Main_characters_are_listed_before_supporting_ones()
{
await Characters.CreateAsync(_projectId, new CreateCharacterRequest("Zeno"));
await Characters.CreateAsync(_novelId, new CreateCharacterRequest("Zeno"));
await Characters.CreateAsync(
_projectId, new CreateCharacterRequest("Mara", Importance: CharacterImportance.Main));
_novelId, new CreateCharacterRequest("Mara", Importance: CharacterImportance.Main));
var listed = await Characters.ListAsync(_projectId);
var listed = await Characters.ListAsync(_novelId);
Assert.That(
listed.Select(c => c.Name),
@@ -64,12 +64,12 @@ public class CharacterArcTests : ServiceTestFixture
[Test]
public async Task Within_a_group_the_lead_comes_before_the_second_lead()
{
await Characters.CreateAsync(_projectId, new CreateCharacterRequest(
await Characters.CreateAsync(_novelId, new CreateCharacterRequest(
"Mara", CharacterRole.Deuteragonist, CharacterImportance.Main));
await Characters.CreateAsync(_projectId, new CreateCharacterRequest(
await Characters.CreateAsync(_novelId, new CreateCharacterRequest(
"Anders", CharacterRole.Antagonist, CharacterImportance.Main));
var listed = await Characters.ListAsync(_projectId);
var listed = await Characters.ListAsync(_novelId);
Assert.That(
listed.Select(c => c.Name),
@@ -127,7 +127,7 @@ public class CharacterArcTests : ServiceTestFixture
[Test]
public async Task A_stage_pinned_to_a_chapter_resolves_that_chapter()
{
var chapter = await Chapters.CreateAsync(_projectId, new CreateChapterRequest("Landfall"));
var chapter = await Chapters.CreateAsync(_novelId, new CreateChapterRequest("Landfall"));
var stage = await Arcs.CreateAsync(
_characterId, new CreateArcStageRequest("The map is wrong", ChapterId: chapter.Id));
@@ -140,21 +140,21 @@ public class CharacterArcTests : ServiceTestFixture
}
[Test]
public async Task A_stage_cannot_be_pinned_to_a_chapter_from_another_project()
public async Task A_stage_cannot_be_pinned_to_a_chapter_from_another_novel()
{
var other = await Projects.CreateAsync(new CreateProjectRequest("Other Book"));
var other = await Novels.CreateAsync(new CreateNovelRequest("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<InvalidOperationException>().With.Message.Contains("same project"));
Throws.TypeOf<InvalidOperationException>().With.Message.Contains("same novel"));
}
[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 chapter = await Chapters.CreateAsync(_novelId, new CreateChapterRequest("Landfall"));
var stage = await Arcs.CreateAsync(
_characterId, new CreateArcStageRequest("The map is wrong", ChapterId: chapter.Id));
@@ -188,9 +188,9 @@ public class CharacterArcTests : ServiceTestFixture
[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"));
var second = await Chapters.CreateAsync(_novelId, new CreateChapterRequest("Second", Number: 2));
var first = await Chapters.CreateAsync(_novelId, new CreateChapterRequest("First", Number: 1));
var mara = await Characters.CreateAsync(_novelId, 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]));
@@ -215,7 +215,7 @@ public class CharacterArcTests : ServiceTestFixture
[Test]
public async Task An_arc_stage_groups_the_beats_assigned_to_it()
{
var chapter = await Chapters.CreateAsync(_projectId, new CreateChapterRequest("Landfall"));
var chapter = await Chapters.CreateAsync(_novelId, new CreateChapterRequest("Landfall"));
var spoiled = await Beats.CreateAsync(chapter.Id, new CreateBeatRequest("Snaps at the crew", CharacterIds: [_characterId]));
var humbled = await Beats.CreateAsync(chapter.Id, new CreateBeatRequest("Learns to swab a deck", CharacterIds: [_characterId]));
var stage = await Arcs.CreateAsync(_characterId, new CreateArcStageRequest("Spoiled noble"));
@@ -229,7 +229,7 @@ public class CharacterArcTests : ServiceTestFixture
[Test]
public async Task Assigning_a_beat_to_a_stage_moves_it_out_of_the_characters_other_stage()
{
var chapter = await Chapters.CreateAsync(_projectId, new CreateChapterRequest("Landfall"));
var chapter = await Chapters.CreateAsync(_novelId, new CreateChapterRequest("Landfall"));
var beat = await Beats.CreateAsync(chapter.Id, new CreateBeatRequest("Gets hurt", CharacterIds: [_characterId]));
var early = await Arcs.CreateAsync(_characterId, new CreateArcStageRequest("Spoiled noble"));
var later = await Arcs.CreateAsync(_characterId, new CreateArcStageRequest("Humbled"));
@@ -250,8 +250,8 @@ public class CharacterArcTests : ServiceTestFixture
[Test]
public async Task A_beat_can_only_be_grouped_into_a_stage_for_a_character_who_appears_in_it()
{
var chapter = await Chapters.CreateAsync(_projectId, new CreateChapterRequest("Landfall"));
var mara = await Characters.CreateAsync(_projectId, new CreateCharacterRequest("Mara"));
var chapter = await Chapters.CreateAsync(_novelId, new CreateChapterRequest("Landfall"));
var mara = await Characters.CreateAsync(_novelId, new CreateCharacterRequest("Mara"));
var beat = await Beats.CreateAsync(chapter.Id, new CreateBeatRequest("Mara alone", CharacterIds: [mara.Id]));
var stage = await Arcs.CreateAsync(_characterId, new CreateArcStageRequest("Spoiled noble"));
@@ -269,7 +269,7 @@ public class CharacterArcTests : ServiceTestFixture
[Test]
public async Task Clearing_a_stages_beats_with_an_empty_list_ungroups_them()
{
var chapter = await Chapters.CreateAsync(_projectId, new CreateChapterRequest("Landfall"));
var chapter = await Chapters.CreateAsync(_novelId, new CreateChapterRequest("Landfall"));
var beat = await Beats.CreateAsync(chapter.Id, new CreateBeatRequest("Snaps at the crew", CharacterIds: [_characterId]));
var stage = await Arcs.CreateAsync(_characterId, new CreateArcStageRequest("Spoiled noble"));
await Arcs.SetBeatsAsync(stage.Id, new SetArcStageBeatsRequest([beat!.Id]));
@@ -2,24 +2,24 @@ using Microsoft.EntityFrameworkCore;
using Novelly.Api.Beats;
using Novelly.Api.Chapters;
using Novelly.Api.Characters;
using Novelly.Api.Projects;
using Novelly.Api.Novels;
namespace Novelly.Api.Tests;
[TestFixture]
public class CharacterServiceTests : ServiceTestFixture
{
private Guid _projectId;
private Guid _novelId;
protected override void OnSetUp()
{
_projectId = Projects.CreateAsync(new CreateProjectRequest("The Salt Road")).Result.Id;
_novelId = Novels.CreateAsync(new CreateNovelRequest("The Salt Road")).Result.Id;
}
[Test]
public async Task New_characters_default_to_supporting_role_and_importance()
{
var character = await Characters.CreateAsync(_projectId, new CreateCharacterRequest("Ines"));
var character = await Characters.CreateAsync(_novelId, new CreateCharacterRequest("Ines"));
Assert.Multiple(() =>
{
@@ -31,7 +31,7 @@ public class CharacterServiceTests : ServiceTestFixture
[Test]
public async Task Promoting_a_character_to_main_sticks_until_changed_again()
{
var character = await Characters.CreateAsync(_projectId, new CreateCharacterRequest("Ines"));
var character = await Characters.CreateAsync(_novelId, new CreateCharacterRequest("Ines"));
var promoted = (await Characters.UpdateAsync(
character.Id, new UpdateCharacterRequest(Importance: CharacterImportance.Main)))!;
@@ -47,7 +47,7 @@ public class CharacterServiceTests : ServiceTestFixture
[Test]
public async Task Updating_leaves_omitted_fields_alone_and_clears_on_empty_string()
{
var character = await Characters.CreateAsync(_projectId, new CreateCharacterRequest(
var character = await Characters.CreateAsync(_novelId, new CreateCharacterRequest(
"Ines", Want: "To find her sister.", Need: "To let go of the guilt."));
var renamed = (await Characters.UpdateAsync(character.Id, new UpdateCharacterRequest(Name: "Ines Vell")))!;
@@ -69,23 +69,23 @@ public class CharacterServiceTests : ServiceTestFixture
}
[Test]
public async Task Relating_characters_across_projects_is_refused()
public async Task Relating_characters_across_novels_is_refused()
{
var other = await Projects.CreateAsync(new CreateProjectRequest("Other Book"));
var ines = await Characters.CreateAsync(_projectId, new CreateCharacterRequest("Ines"));
var other = await Novels.CreateAsync(new CreateNovelRequest("Other Book"));
var ines = await Characters.CreateAsync(_novelId, new CreateCharacterRequest("Ines"));
var stranger = await Characters.CreateAsync(other.Id, new CreateCharacterRequest("Stranger"));
Assert.That(
async () => await Characters.AddRelationshipAsync(
ines.Id, new CreateRelationshipRequest(stranger.Id, "sister")),
Throws.TypeOf<InvalidOperationException>().With.Message.Contains("same project"));
Throws.TypeOf<InvalidOperationException>().With.Message.Contains("same novel"));
}
[Test]
public async Task Removing_a_relationship_leaves_both_characters_in_place()
{
var ines = await Characters.CreateAsync(_projectId, new CreateCharacterRequest("Ines"));
var mara = await Characters.CreateAsync(_projectId, new CreateCharacterRequest("Mara"));
var ines = await Characters.CreateAsync(_novelId, new CreateCharacterRequest("Ines"));
var mara = await Characters.CreateAsync(_novelId, new CreateCharacterRequest("Mara"));
var withRelationship = (await Characters.AddRelationshipAsync(
ines.Id, new CreateRelationshipRequest(mara.Id, "sister")))!;
@@ -104,8 +104,8 @@ public class CharacterServiceTests : ServiceTestFixture
[Test]
public async Task Adding_a_relationship_records_it_on_both_characters()
{
var ines = await Characters.CreateAsync(_projectId, new CreateCharacterRequest("Ines"));
var mara = await Characters.CreateAsync(_projectId, new CreateCharacterRequest("Mara"));
var ines = await Characters.CreateAsync(_novelId, new CreateCharacterRequest("Ines"));
var mara = await Characters.CreateAsync(_novelId, new CreateCharacterRequest("Mara"));
await Characters.AddRelationshipAsync(
ines.Id, new CreateRelationshipRequest(mara.Id, "sister", ReciprocalRelationshipType: "brother"));
@@ -127,8 +127,8 @@ public class CharacterServiceTests : ServiceTestFixture
[Test]
public async Task A_relationship_with_no_reciprocal_type_mirrors_the_same_type_both_ways()
{
var ines = await Characters.CreateAsync(_projectId, new CreateCharacterRequest("Ines"));
var mara = await Characters.CreateAsync(_projectId, new CreateCharacterRequest("Mara"));
var ines = await Characters.CreateAsync(_novelId, new CreateCharacterRequest("Ines"));
var mara = await Characters.CreateAsync(_novelId, new CreateCharacterRequest("Mara"));
await Characters.AddRelationshipAsync(ines.Id, new CreateRelationshipRequest(mara.Id, "rival"));
@@ -140,8 +140,8 @@ public class CharacterServiceTests : ServiceTestFixture
[Test]
public async Task Removing_a_relationship_removes_the_reciprocal_side_too()
{
var ines = await Characters.CreateAsync(_projectId, new CreateCharacterRequest("Ines"));
var mara = await Characters.CreateAsync(_projectId, new CreateCharacterRequest("Mara"));
var ines = await Characters.CreateAsync(_novelId, new CreateCharacterRequest("Ines"));
var mara = await Characters.CreateAsync(_novelId, new CreateCharacterRequest("Mara"));
var withRelationship = (await Characters.AddRelationshipAsync(
ines.Id, new CreateRelationshipRequest(mara.Id, "sister", ReciprocalRelationshipType: "brother")))!;
@@ -156,8 +156,8 @@ public class CharacterServiceTests : ServiceTestFixture
[Test]
public async Task Deleting_a_character_detaches_it_from_beats_rather_than_deleting_them()
{
var chapter = await Chapters.CreateAsync(_projectId, new CreateChapterRequest("Landfall"));
var ines = await Characters.CreateAsync(_projectId, new CreateCharacterRequest("Ines"));
var chapter = await Chapters.CreateAsync(_novelId, new CreateChapterRequest("Landfall"));
var ines = await Characters.CreateAsync(_novelId, new CreateCharacterRequest("Ines"));
var beat = await Beats.CreateAsync(chapter.Id, new CreateBeatRequest("She finds the map", CharacterIds: [ines.Id]));
await Characters.DeleteAsync(ines.Id);
@@ -168,7 +168,7 @@ public class CharacterServiceTests : ServiceTestFixture
}
[Test]
public async Task Creating_a_character_under_a_missing_project_returns_null_rather_than_throwing() =>
public async Task Creating_a_character_under_a_missing_novel_returns_null_rather_than_throwing() =>
Assert.That(
await Characters.CreateAsync(Guid.NewGuid(), new CreateCharacterRequest("Ines")),
Is.Null);
@@ -181,7 +181,7 @@ public class CharacterServiceTests : ServiceTestFixture
public async Task Aliases_round_trip_on_create_and_update()
{
var created = await Characters.CreateAsync(
_projectId, new CreateCharacterRequest("Ines", Aliases: ["The Grey Man", "Kael"]));
_novelId, new CreateCharacterRequest("Ines", Aliases: ["The Grey Man", "Kael"]));
Assert.That(created.Aliases, Is.EqualTo(new[] { "The Grey Man", "Kael" }));
@@ -195,7 +195,7 @@ public class CharacterServiceTests : ServiceTestFixture
public async Task Clearing_aliases_with_an_empty_list_empties_them()
{
var created = await Characters.CreateAsync(
_projectId, new CreateCharacterRequest("Ines", Aliases: ["The Grey Man"]));
_novelId, new CreateCharacterRequest("Ines", Aliases: ["The Grey Man"]));
var cleared = (await Characters.UpdateAsync(created.Id, new UpdateCharacterRequest(Aliases: [])))!;
@@ -205,8 +205,8 @@ public class CharacterServiceTests : ServiceTestFixture
[Test]
public async Task Linking_a_character_to_its_true_identity_records_it_on_both_sides()
{
var kael = await Characters.CreateAsync(_projectId, new CreateCharacterRequest("Kael"));
var stranger = await Characters.CreateAsync(_projectId, new CreateCharacterRequest("The Stranger"));
var kael = await Characters.CreateAsync(_novelId, new CreateCharacterRequest("Kael"));
var stranger = await Characters.CreateAsync(_novelId, new CreateCharacterRequest("The Stranger"));
var linked = (await Characters.LinkIdentityAsync(
stranger.Id, new LinkCharacterIdentityRequest(kael.Id, Note: "Same man, after the exile.")))!;
@@ -225,9 +225,9 @@ public class CharacterServiceTests : ServiceTestFixture
[Test]
public async Task Linking_to_a_character_that_is_itself_an_alias_flattens_to_the_canonical()
{
var kael = await Characters.CreateAsync(_projectId, new CreateCharacterRequest("Kael"));
var stranger = await Characters.CreateAsync(_projectId, new CreateCharacterRequest("The Stranger"));
var exile = await Characters.CreateAsync(_projectId, new CreateCharacterRequest("The Exile"));
var kael = await Characters.CreateAsync(_novelId, new CreateCharacterRequest("Kael"));
var stranger = await Characters.CreateAsync(_novelId, new CreateCharacterRequest("The Stranger"));
var exile = await Characters.CreateAsync(_novelId, new CreateCharacterRequest("The Exile"));
await Characters.LinkIdentityAsync(stranger.Id, new LinkCharacterIdentityRequest(kael.Id));
var linked = (await Characters.LinkIdentityAsync(exile.Id, new LinkCharacterIdentityRequest(stranger.Id)))!;
@@ -238,7 +238,7 @@ public class CharacterServiceTests : ServiceTestFixture
[Test]
public async Task Linking_a_character_to_itself_is_rejected()
{
var kael = await Characters.CreateAsync(_projectId, new CreateCharacterRequest("Kael"));
var kael = await Characters.CreateAsync(_novelId, new CreateCharacterRequest("Kael"));
Assert.That(
async () => await Characters.LinkIdentityAsync(kael.Id, new LinkCharacterIdentityRequest(kael.Id)),
@@ -246,22 +246,22 @@ public class CharacterServiceTests : ServiceTestFixture
}
[Test]
public async Task Linking_identities_across_projects_is_refused()
public async Task Linking_identities_across_novels_is_refused()
{
var other = await Projects.CreateAsync(new CreateProjectRequest("Other Book"));
var kael = await Characters.CreateAsync(_projectId, new CreateCharacterRequest("Kael"));
var other = await Novels.CreateAsync(new CreateNovelRequest("Other Book"));
var kael = await Characters.CreateAsync(_novelId, new CreateCharacterRequest("Kael"));
var stranger = await Characters.CreateAsync(other.Id, new CreateCharacterRequest("Stranger"));
Assert.That(
async () => await Characters.LinkIdentityAsync(stranger.Id, new LinkCharacterIdentityRequest(kael.Id)),
Throws.TypeOf<InvalidOperationException>().With.Message.Contains("same project"));
Throws.TypeOf<InvalidOperationException>().With.Message.Contains("same novel"));
}
[Test]
public async Task Deleting_the_canonical_character_leaves_its_other_identities_alive()
{
var kael = await Characters.CreateAsync(_projectId, new CreateCharacterRequest("Kael"));
var stranger = await Characters.CreateAsync(_projectId, new CreateCharacterRequest("The Stranger"));
var kael = await Characters.CreateAsync(_novelId, new CreateCharacterRequest("Kael"));
var stranger = await Characters.CreateAsync(_novelId, new CreateCharacterRequest("The Stranger"));
await Characters.LinkIdentityAsync(stranger.Id, new LinkCharacterIdentityRequest(kael.Id));
await Characters.DeleteAsync(kael.Id);
@@ -274,9 +274,9 @@ public class CharacterServiceTests : ServiceTestFixture
[Test]
public async Task Unlinking_an_identity_clears_the_reveal_chapter_and_note()
{
var chapter = await Chapters.CreateAsync(_projectId, new CreateChapterRequest("The Reveal"));
var kael = await Characters.CreateAsync(_projectId, new CreateCharacterRequest("Kael"));
var stranger = await Characters.CreateAsync(_projectId, new CreateCharacterRequest("The Stranger"));
var chapter = await Chapters.CreateAsync(_novelId, new CreateChapterRequest("The Reveal"));
var kael = await Characters.CreateAsync(_novelId, new CreateCharacterRequest("Kael"));
var stranger = await Characters.CreateAsync(_novelId, new CreateCharacterRequest("The Stranger"));
await Characters.LinkIdentityAsync(
stranger.Id, new LinkCharacterIdentityRequest(kael.Id, chapter.Id, "Same man."));
@@ -1,7 +1,7 @@
using Novelly.Api.Chapters;
using Novelly.Api.Common;
using Novelly.Api.Common.Validation;
using Novelly.Api.Projects;
using Novelly.Api.Novels;
namespace Novelly.Api.Tests;
@@ -10,22 +10,22 @@ public class ExceptionHandlingTests : ServiceTestFixture
{
[Test]
public void Guard_rejects_an_empty_guid_passed_as_a_required_id() =>
Assert.That(() => Projects.GetAsync(Guid.Empty), Throws.TypeOf<ArgumentException>());
Assert.That(() => Novels.GetAsync(Guid.Empty), Throws.TypeOf<ArgumentException>());
[Test]
public void Guard_rejects_a_null_request_object() =>
Assert.That(
() => Projects.CreateAsync(null!),
() => Novels.CreateAsync(null!),
Throws.TypeOf<ArgumentNullException>());
[Test]
public async Task Deleting_a_missing_project_returns_false_rather_than_throwing() =>
Assert.That(await Projects.DeleteAsync(Guid.NewGuid()), Is.False);
public async Task Deleting_a_missing_novel_returns_false_rather_than_throwing() =>
Assert.That(await Novels.DeleteAsync(Guid.NewGuid()), Is.False);
[Test]
public void A_blank_title_fails_the_create_project_validator()
public void A_blank_title_fails_the_create_novel_validator()
{
var result = new CreateProjectRequestValidator().Validate(new CreateProjectRequest(""));
var result = new CreateNovelRequestValidator().Validate(new CreateNovelRequest(""));
Assert.Multiple(() =>
{
@@ -37,10 +37,10 @@ public class ExceptionHandlingTests : ServiceTestFixture
[Test]
public void Calling_a_service_directly_with_an_invalid_request_throws_rather_than_silently_accepting_it() =>
Assert.That(
() => Projects.CreateAsync(new CreateProjectRequest("")),
() => Novels.CreateAsync(new CreateNovelRequest("")),
Throws.TypeOf<ArgumentException>());
[Test]
public async Task Creating_a_chapter_under_a_missing_project_returns_null_rather_than_throwing() =>
public async Task Creating_a_chapter_under_a_missing_novel_returns_null_rather_than_throwing() =>
Assert.That(await Chapters.CreateAsync(Guid.NewGuid(), new CreateChapterRequest("Landfall")), Is.Null);
}
+8 -8
View File
@@ -1,4 +1,4 @@
using Novelly.Api.Projects;
using Novelly.Api.Novels;
namespace Novelly.Api.Tests;
@@ -20,24 +20,24 @@ public class GenreServiceTests : ServiceTestFixture
}
[Test]
public async Task A_project_can_be_filed_under_a_genre_off_the_list()
public async Task A_novel_can_be_filed_under_a_genre_off_the_list()
{
var fantasy = (await Genres.ListAsync()).First(g => g.Name == "Fantasy");
var project = await Projects.CreateAsync(new CreateProjectRequest("The Salt Road", Genre: fantasy.Name));
var novel = await Novels.CreateAsync(new CreateNovelRequest("The Salt Road", Genre: fantasy.Name));
Assert.That(project.Genre, Is.EqualTo("Fantasy"));
Assert.That(novel.Genre, Is.EqualTo("Fantasy"));
}
[Test]
public async Task A_project_can_still_carry_a_genre_that_is_not_on_the_list()
public async Task A_novel_can_still_carry_a_genre_that_is_not_on_the_list()
{
var project = await Projects.CreateAsync(
new CreateProjectRequest("The Salt Road", Genre: "Nautical Gothic"));
var novel = await Novels.CreateAsync(
new CreateNovelRequest("The Salt Road", Genre: "Nautical Gothic"));
Assert.Multiple(async () =>
{
Assert.That(project.Genre, Is.EqualTo("Nautical Gothic"));
Assert.That(novel.Genre, Is.EqualTo("Nautical Gothic"));
Assert.That((await Genres.ListAsync()).Select(g => g.Name), Has.No.Member("Nautical Gothic"));
});
}
@@ -13,8 +13,8 @@ public class ImportAgentToolsetTests : ServiceTestFixture
{
_root = Directory.CreateTempSubdirectory("novelly-import-toolset-test-").FullName;
_toolset = new ImportAgentToolset(
Projects, Characters, Arcs, Chapters, Beats, new CapturingLogger<ImportAgentToolset>());
_toolset.Initialize(_root, existingProjectId: null);
Novels, Characters, Arcs, Chapters, Beats, new CapturingLogger<ImportAgentToolset>());
_toolset.Initialize(_root, existingNovelId: null);
}
[TearDown]
@@ -64,7 +64,7 @@ public class ImportAgentToolsetTests : ServiceTestFixture
[Test]
public async Task Write_ledger_can_only_ever_touch_the_ledger_file_no_matter_what_path_is_asked_for()
{
await _toolset.ExecuteAsync("write_ledger", Input(new { json = """{"completedPasses": ["project"]}""" }));
await _toolset.ExecuteAsync("write_ledger", Input(new { json = """{"completedPasses": ["novel"]}""" }));
Assert.Multiple(() =>
{
@@ -86,25 +86,25 @@ public class ImportAgentToolsetTests : ServiceTestFixture
}
[Test]
public async Task Create_project_binds_the_toolsets_project_id_for_later_calls()
public async Task Create_novel_binds_the_toolsets_novel_id_for_later_calls()
{
await _toolset.ExecuteAsync("create_project", Input(new { title = "The Blade Itself", author = "Joe Abercrombie" }));
await _toolset.ExecuteAsync("create_novel", Input(new { title = "The Blade Itself", author = "Joe Abercrombie" }));
Assert.That(_toolset.ProjectId, Is.Not.Null);
Assert.That(_toolset.NovelId, Is.Not.Null);
var project = await Projects.GetAsync(_toolset.ProjectId!.Value);
Assert.That(project!.Title, Is.EqualTo("The Blade Itself"));
var novel = await Novels.GetAsync(_toolset.NovelId!.Value);
Assert.That(novel!.Title, Is.EqualTo("The Blade Itself"));
}
[Test]
public async Task Domain_tools_refuse_to_run_before_a_project_exists()
public async Task Domain_tools_refuse_to_run_before_a_novel_exists()
{
var result = await _toolset.ExecuteAsync("create_character", Input(new { name = "Logen" }));
Assert.Multiple(() =>
{
Assert.That(result.IsError, Is.True);
Assert.That(result.Content, Does.Contain("create_project first"));
Assert.That(result.Content, Does.Contain("create_novel first"));
});
}
+10 -10
View File
@@ -1,6 +1,6 @@
using System.Threading.Channels;
using Novelly.Api.Imports;
using Novelly.Api.Projects;
using Novelly.Api.Novels;
namespace Novelly.Api.Tests;
@@ -17,7 +17,7 @@ public class ImportServiceTests : ServiceTestFixture
_queue = Channel.CreateUnbounded<Guid>();
_imports = new ImportService(
Db.Context,
Projects,
Novels,
_queue,
UserContext,
new CapturingLogger<ImportService>(),
@@ -44,7 +44,7 @@ public class ImportServiceTests : ServiceTestFixture
Assert.Multiple(() =>
{
Assert.That(inspection.Readiness, Is.EqualTo(ImportReadiness.Fresh));
Assert.That(inspection.ProjectId, Is.Null);
Assert.That(inspection.NovelId, Is.Null);
Assert.That(inspection.ChaptersTotal, Is.EqualTo(3));
Assert.That(inspection.ChaptersCompleted, Is.EqualTo(0));
});
@@ -54,7 +54,7 @@ public class ImportServiceTests : ServiceTestFixture
public async Task Inspecting_a_folder_with_an_incomplete_ledger_reports_resumable()
{
WriteChapterFiles(3);
WriteLedger("""{"projectId": "11111111-1111-1111-1111-111111111111", "completedPasses": ["project"], "completedChapters": [1]}""");
WriteLedger("""{"novelId": "11111111-1111-1111-1111-111111111111", "completedPasses": ["novel"], "completedChapters": [1]}""");
var inspection = await _imports.InspectAsync(new InspectImportRequest(_root));
@@ -72,8 +72,8 @@ public class ImportServiceTests : ServiceTestFixture
WriteChapterFiles(2);
WriteLedger("""
{
"projectId": "11111111-1111-1111-1111-111111111111",
"completedPasses": ["project", "characters", "chapters", "arcs"],
"novelId": "11111111-1111-1111-1111-111111111111",
"completedPasses": ["novel", "characters", "chapters", "arcs"],
"completedChapters": [1, 2]
}
""");
@@ -117,17 +117,17 @@ public class ImportServiceTests : ServiceTestFixture
}
[Test]
public async Task Force_restarting_a_completed_import_deletes_the_ledger_and_its_project()
public async Task Force_restarting_a_completed_import_deletes_the_ledger_and_its_novel()
{
var project = await Projects.CreateAsync(new CreateProjectRequest("The Blade Itself"));
WriteLedger($$"""{"projectId": "{{project.Id}}", "completedPasses": ["project", "characters", "chapters", "arcs"], "completedChapters": [1]}""");
var novel = await Novels.CreateAsync(new CreateNovelRequest("The Blade Itself"));
WriteLedger($$"""{"novelId": "{{novel.Id}}", "completedPasses": ["novel", "characters", "chapters", "arcs"], "completedChapters": [1]}""");
await _imports.StartOrResumeAsync(new StartImportRequest(_root, ForceRestart: true));
Assert.Multiple(() =>
{
Assert.That(File.Exists(Path.Combine(_root, ".novelly-import.json")), Is.False);
Assert.That(Projects.GetAsync(project.Id).Result, Is.Null);
Assert.That(Novels.GetAsync(novel.Id).Result, Is.Null);
});
}
+30 -30
View File
@@ -3,7 +3,7 @@ using Microsoft.Extensions.Options;
using Novelly.Api.Agent;
using Novelly.Api.Chapters;
using Novelly.Api.Characters;
using Novelly.Api.Projects;
using Novelly.Api.Novels;
namespace Novelly.Api.Tests;
@@ -11,14 +11,14 @@ namespace Novelly.Api.Tests;
public class ListingTests : ServiceTestFixture
{
[Test]
public async Task Projects_are_listed_most_recently_updated_first()
public async Task Novels_are_listed_most_recently_updated_first()
{
var older = await Projects.CreateAsync(new CreateProjectRequest("Older Book"));
var newer = await Projects.CreateAsync(new CreateProjectRequest("Newer Book"));
var older = await Novels.CreateAsync(new CreateNovelRequest("Older Book"));
var newer = await Novels.CreateAsync(new CreateNovelRequest("Newer Book"));
await Projects.UpdateAsync(older.Id, new UpdateProjectRequest(Logline: "Revised."));
await Novels.UpdateAsync(older.Id, new UpdateNovelRequest(Logline: "Revised."));
var listed = await Projects.ListAsync();
var listed = await Novels.ListAsync();
Assert.Multiple(() =>
{
@@ -28,16 +28,16 @@ public class ListingTests : ServiceTestFixture
}
[Test]
public async Task Project_summaries_aggregate_counts_and_words_across_chapters()
public async Task Novel_summaries_aggregate_counts_and_words_across_chapters()
{
var project = await Projects.CreateAsync(new CreateProjectRequest("The Salt Road"));
await Characters.CreateAsync(project.Id, new CreateCharacterRequest("Ines"));
await Characters.CreateAsync(project.Id, new CreateCharacterRequest("Mara"));
var novel = await Novels.CreateAsync(new CreateNovelRequest("The Salt Road"));
await Characters.CreateAsync(novel.Id, new CreateCharacterRequest("Ines"));
await Characters.CreateAsync(novel.Id, new CreateCharacterRequest("Mara"));
await Chapters.CreateAsync(project.Id, new CreateChapterRequest("Landfall", Prose: "One two three"));
await Chapters.CreateAsync(project.Id, new CreateChapterRequest("The Harbour", Prose: "Four five"));
await Chapters.CreateAsync(novel.Id, new CreateChapterRequest("Landfall", Prose: "One two three"));
await Chapters.CreateAsync(novel.Id, new CreateChapterRequest("The Harbour", Prose: "Four five"));
var summary = (await Projects.ListAsync()).Single();
var summary = (await Novels.ListAsync()).Single();
Assert.Multiple(() =>
{
@@ -48,11 +48,11 @@ public class ListingTests : ServiceTestFixture
}
[Test]
public async Task A_project_with_no_chapters_reports_zero_words_rather_than_failing()
public async Task A_novel_with_no_chapters_reports_zero_words_rather_than_failing()
{
await Projects.CreateAsync(new CreateProjectRequest("Empty"));
await Novels.CreateAsync(new CreateNovelRequest("Empty"));
var summary = (await Projects.ListAsync()).Single();
var summary = (await Novels.ListAsync()).Single();
Assert.Multiple(() =>
{
@@ -64,11 +64,11 @@ public class ListingTests : ServiceTestFixture
[Test]
public async Task Chapters_are_listed_in_manuscript_order_with_word_counts()
{
var project = await Projects.CreateAsync(new CreateProjectRequest("The Salt Road"));
var second = await Chapters.CreateAsync(project.Id, new CreateChapterRequest("Second", Number: 2, Prose: "One two three"));
var first = await Chapters.CreateAsync(project.Id, new CreateChapterRequest("First", Number: 1));
var novel = await Novels.CreateAsync(new CreateNovelRequest("The Salt Road"));
var second = await Chapters.CreateAsync(novel.Id, new CreateChapterRequest("Second", Number: 2, Prose: "One two three"));
var first = await Chapters.CreateAsync(novel.Id, new CreateChapterRequest("First", Number: 1));
var listed = await Chapters.ListAsync(project.Id);
var listed = await Chapters.ListAsync(novel.Id);
Assert.Multiple(() =>
{
@@ -81,19 +81,19 @@ public class ListingTests : ServiceTestFixture
[Test]
public async Task Conversations_are_listed_most_recently_updated_first()
{
var project = await Projects.CreateAsync(new CreateProjectRequest("The Salt Road"));
var novel = await Novels.CreateAsync(new CreateNovelRequest("The Salt Road"));
var agent = new NovelAgentService(
Db.Context,
new ScriptedModelClient([[new AgentTextBlock("Reply.")]]),
new NovelAgentToolset(Projects, Characters, Arcs, Chapters, Beats, Tags, Questions, NullLogger<NovelAgentToolset>.Instance),
new NovelAgentToolset(Novels, Characters, Arcs, Chapters, Beats, Tags, Questions, NullLogger<NovelAgentToolset>.Instance),
Options.Create(new AgentOptions()),
NullLogger<NovelAgentService>.Instance,
new SendAgentMessageRequestValidator());
await agent.SendMessageAsync(project.Id, new SendAgentMessageRequest("First question."));
await agent.SendMessageAsync(project.Id, new SendAgentMessageRequest("Second question."));
await agent.SendMessageAsync(novel.Id, new SendAgentMessageRequest("First question."));
await agent.SendMessageAsync(novel.Id, new SendAgentMessageRequest("Second question."));
var listed = await agent.ListConversationsAsync(project.Id);
var listed = await agent.ListConversationsAsync(novel.Id);
Assert.Multiple(() =>
{
@@ -106,12 +106,12 @@ public class ListingTests : ServiceTestFixture
[Test]
public async Task Characters_are_listed_by_role_then_name()
{
var project = await Projects.CreateAsync(new CreateProjectRequest("The Salt Road"));
await Characters.CreateAsync(project.Id, new CreateCharacterRequest("Zeno", CharacterRole.Supporting));
await Characters.CreateAsync(project.Id, new CreateCharacterRequest("Ines", CharacterRole.Protagonist));
await Characters.CreateAsync(project.Id, new CreateCharacterRequest("Anders", CharacterRole.Supporting));
var novel = await Novels.CreateAsync(new CreateNovelRequest("The Salt Road"));
await Characters.CreateAsync(novel.Id, new CreateCharacterRequest("Zeno", CharacterRole.Supporting));
await Characters.CreateAsync(novel.Id, new CreateCharacterRequest("Ines", CharacterRole.Protagonist));
await Characters.CreateAsync(novel.Id, new CreateCharacterRequest("Anders", CharacterRole.Supporting));
var listed = await Characters.ListAsync(project.Id);
var listed = await Characters.ListAsync(novel.Id);
Assert.That(listed.Select(c => c.Name), Is.EqualTo(new[] { "Ines", "Anders", "Zeno" }));
}
+17 -17
View File
@@ -3,7 +3,7 @@ using Novelly.Api.Beats;
using Novelly.Api.Chapters;
using Novelly.Api.Characters;
using Novelly.Api.Common;
using Novelly.Api.Projects;
using Novelly.Api.Novels;
namespace Novelly.Api.Tests;
@@ -27,53 +27,53 @@ public class LoggingTests : ServiceTestFixture
}
[Test]
public async Task Creating_a_chapter_logs_the_project_and_title_at_information()
public async Task Creating_a_chapter_logs_the_novel_and_title_at_information()
{
var project = await Projects.CreateAsync(new CreateProjectRequest("The Salt Road"));
var novel = await Novels.CreateAsync(new CreateNovelRequest("The Salt Road"));
ChapterLogs.Entries.Clear();
await Chapters.CreateAsync(project.Id, new CreateChapterRequest("Landfall"));
await Chapters.CreateAsync(novel.Id, new CreateChapterRequest("Landfall"));
var info = ChapterLogs.Entries.Single(e => e.Level == LogLevel.Information);
Assert.Multiple(() =>
{
Assert.That(info.Message, Does.Contain("Landfall"));
Assert.That(info.Message, Does.Contain(project.Id.ToString()));
Assert.That(info.Message, Does.Contain(novel.Id.ToString()));
});
}
[Test]
public async Task Logged_values_never_include_a_chapter_summary_body()
{
var project = await Projects.CreateAsync(new CreateProjectRequest("The Salt Road"));
var novel = await Novels.CreateAsync(new CreateNovelRequest("The Salt Road"));
const string secretSummary = "A very specific plot twist nobody should see in a log line.";
ChapterLogs.Entries.Clear();
await Chapters.CreateAsync(project.Id, new CreateChapterRequest("Landfall", Summary: secretSummary));
await Chapters.CreateAsync(novel.Id, new CreateChapterRequest("Landfall", Summary: secretSummary));
Assert.That(ChapterLogs.Entries.Select(e => e.Message), Has.None.Contain(secretSummary));
}
[Test]
public async Task Deleting_a_project_logs_information_before_the_lookup()
public async Task Deleting_a_novel_logs_information_before_the_lookup()
{
var project = await Projects.CreateAsync(new CreateProjectRequest("The Salt Road"));
ProjectLogs.Entries.Clear();
var novel = await Novels.CreateAsync(new CreateNovelRequest("The Salt Road"));
NovelLogs.Entries.Clear();
await Projects.DeleteAsync(project.Id);
await Novels.DeleteAsync(novel.Id);
Assert.That(
ProjectLogs.Entries,
Has.Some.Matches<CapturedLogEntry>(e => e.Level == LogLevel.Information && e.Message.Contains(project.Id.ToString())));
NovelLogs.Entries,
Has.Some.Matches<CapturedLogEntry>(e => e.Level == LogLevel.Information && e.Message.Contains(novel.Id.ToString())));
}
[Test]
public async Task Rejecting_a_beat_with_a_foreign_character_logs_a_warning_not_an_error()
{
var projectA = await Projects.CreateAsync(new CreateProjectRequest("Project A"));
var projectB = await Projects.CreateAsync(new CreateProjectRequest("Project B"));
var chapter = await Chapters.CreateAsync(projectA.Id, new CreateChapterRequest("Landfall"));
var foreignCharacter = await Characters.CreateAsync(projectB.Id, new CreateCharacterRequest("Ines"));
var novelA = await Novels.CreateAsync(new CreateNovelRequest("Novel A"));
var novelB = await Novels.CreateAsync(new CreateNovelRequest("Novel B"));
var chapter = await Chapters.CreateAsync(novelA.Id, new CreateChapterRequest("Landfall"));
var foreignCharacter = await Characters.CreateAsync(novelB.Id, new CreateCharacterRequest("Ines"));
BeatLogs.Entries.Clear();
Assert.That(
@@ -1,13 +1,13 @@
using Microsoft.EntityFrameworkCore;
using Novelly.Api.Chapters;
using Novelly.Api.Common;
using Novelly.Api.Projects;
using Novelly.Api.Novels;
using Novelly.Api.Users;
namespace Novelly.Api.Tests;
[TestFixture]
public class ProjectAccessTests : ServiceTestFixture
public class NovelAccessTests : ServiceTestFixture
{
private Guid AsNewUser(GlobalRole globalRole)
{
@@ -27,9 +27,9 @@ public class ProjectAccessTests : ServiceTestFixture
return user.Id;
}
private void GrantProjectRole(Guid projectId, Guid userId, ProjectRole role)
private void GrantNovelRole(Guid novelId, Guid userId, NovelRole role)
{
Db.Context.ProjectMembers.Add(new ProjectMember { ProjectId = projectId, UserId = userId, ProjectRole = role, GrantedByUserId = userId });
Db.Context.NovelMembers.Add(new NovelMember { NovelId = novelId, UserId = userId, NovelRole = role, GrantedByUserId = userId });
Db.Context.SaveChanges();
}
@@ -43,31 +43,31 @@ public class ProjectAccessTests : ServiceTestFixture
public async Task A_writer_sees_only_novels_they_own_or_have_been_granted()
{
var writerId = AsNewUser(GlobalRole.Writer);
var ownedProject = await Projects.CreateAsync(new CreateProjectRequest("Owned by writer"));
var ownedNovel = await Novels.CreateAsync(new CreateNovelRequest("Owned by writer"));
AsAdmin();
var otherProject = await Projects.CreateAsync(new CreateProjectRequest("Owned by someone else"));
var otherNovel = await Novels.CreateAsync(new CreateNovelRequest("Owned by someone else"));
UserContext.UserId = writerId;
UserContext.GlobalRole = GlobalRole.Writer;
var visibleBeforeGrant = await Projects.ListAsync();
Assert.That(visibleBeforeGrant.Select(p => p.Id), Is.EquivalentTo(new[] { ownedProject.Id }));
var visibleBeforeGrant = await Novels.ListAsync();
Assert.That(visibleBeforeGrant.Select(p => p.Id), Is.EquivalentTo(new[] { ownedNovel.Id }));
GrantProjectRole(otherProject.Id, writerId, ProjectRole.Reviewer);
GrantNovelRole(otherNovel.Id, writerId, NovelRole.Reviewer);
var visibleAfterGrant = await Projects.ListAsync();
Assert.That(visibleAfterGrant.Select(p => p.Id), Is.EquivalentTo(new[] { ownedProject.Id, otherProject.Id }));
var visibleAfterGrant = await Novels.ListAsync();
Assert.That(visibleAfterGrant.Select(p => p.Id), Is.EquivalentTo(new[] { ownedNovel.Id, otherNovel.Id }));
}
[Test]
public async Task An_editor_can_rewrite_a_chapter_but_cannot_delete_it()
{
var project = await Projects.CreateAsync(new CreateProjectRequest("Editable Novel"));
var chapter = await Chapters.CreateAsync(project.Id, new CreateChapterRequest("Chapter One"));
var novel = await Novels.CreateAsync(new CreateNovelRequest("Editable Novel"));
var chapter = await Chapters.CreateAsync(novel.Id, new CreateChapterRequest("Chapter One"));
var editorId = AsNewUser(GlobalRole.Reviewer);
GrantProjectRole(project.Id, editorId, ProjectRole.Editor);
GrantNovelRole(novel.Id, editorId, NovelRole.Editor);
var updated = await Chapters.UpdateAsync(chapter!.Id, new UpdateChapterRequest(Title: "Renamed"));
Assert.That(updated!.Title, Is.EqualTo("Renamed"));
@@ -80,17 +80,17 @@ public class ProjectAccessTests : ServiceTestFixture
{
AsNewUser(GlobalRole.Editor);
Assert.That(() => Projects.CreateAsync(new CreateProjectRequest("Should not exist")), Throws.TypeOf<NotAuthorizedException>());
Assert.That(() => Novels.CreateAsync(new CreateNovelRequest("Should not exist")), Throws.TypeOf<NotAuthorizedException>());
}
[Test]
public async Task A_reviewer_can_read_a_chapter_but_not_change_it()
{
var project = await Projects.CreateAsync(new CreateProjectRequest("Reviewed Novel"));
var chapter = await Chapters.CreateAsync(project.Id, new CreateChapterRequest("Chapter One"));
var novel = await Novels.CreateAsync(new CreateNovelRequest("Reviewed Novel"));
var chapter = await Chapters.CreateAsync(novel.Id, new CreateChapterRequest("Chapter One"));
var reviewerId = AsNewUser(GlobalRole.Reviewer);
GrantProjectRole(project.Id, reviewerId, ProjectRole.Reviewer);
GrantNovelRole(novel.Id, reviewerId, NovelRole.Reviewer);
var read = await Chapters.GetAsync(chapter!.Id);
Assert.That(read, Is.Not.Null);
@@ -103,13 +103,13 @@ public class ProjectAccessTests : ServiceTestFixture
[Test]
public async Task A_writer_granted_access_to_someone_elses_novel_still_cannot_grant_access_to_others()
{
var project = await Projects.CreateAsync(new CreateProjectRequest("Someone Else's Novel"));
var novel = await Novels.CreateAsync(new CreateNovelRequest("Someone Else's Novel"));
var grantedWriterId = AsNewUser(GlobalRole.Writer);
GrantProjectRole(project.Id, grantedWriterId, ProjectRole.Writer);
GrantNovelRole(novel.Id, grantedWriterId, NovelRole.Writer);
Assert.That(
() => Access.RequireAsync(project.Id, ProjectPermission.ManageAccess),
() => Access.RequireAsync(novel.Id, NovelPermission.ManageAccess),
Throws.TypeOf<NotAuthorizedException>());
}
@@ -117,19 +117,19 @@ public class ProjectAccessTests : ServiceTestFixture
public async Task An_admin_reaches_every_novel()
{
AsNewUser(GlobalRole.Writer);
await Projects.CreateAsync(new CreateProjectRequest("Writer's Novel"));
await Novels.CreateAsync(new CreateNovelRequest("Writer's Novel"));
AsAdmin();
await Projects.CreateAsync(new CreateProjectRequest("Admin's Novel"));
await Novels.CreateAsync(new CreateNovelRequest("Admin's Novel"));
var visible = await Projects.ListAsync();
var visible = await Novels.ListAsync();
Assert.That(visible, Has.Count.EqualTo(2));
}
[Test]
public void Deleting_a_user_does_not_cascade_to_their_novels()
{
var ownerNavigation = Db.Context.Model.FindEntityType(typeof(Project))!.FindNavigation(nameof(Project.Owner))!;
var ownerNavigation = Db.Context.Model.FindEntityType(typeof(Novel))!.FindNavigation(nameof(Novel.Owner))!;
Assert.That(ownerNavigation.ForeignKey.DeleteBehavior, Is.EqualTo(DeleteBehavior.Restrict));
}
@@ -137,10 +137,10 @@ public class ProjectAccessTests : ServiceTestFixture
public async Task The_creator_of_a_novel_sees_their_role_as_owner()
{
var writerId = AsNewUser(GlobalRole.Writer);
var project = await Projects.CreateAsync(new CreateProjectRequest("Owned by writer"));
var novel = await Novels.CreateAsync(new CreateNovelRequest("Owned by writer"));
UserContext.UserId = writerId;
var role = await Access.GetMyRoleAsync(project);
var role = await Access.GetMyRoleAsync(novel);
Assert.That(role, Is.EqualTo("Owner"));
}
@@ -149,10 +149,10 @@ public class ProjectAccessTests : ServiceTestFixture
public async Task An_admin_sees_their_role_as_admin_even_on_a_novel_they_do_not_own()
{
AsNewUser(GlobalRole.Writer);
var project = await Projects.CreateAsync(new CreateProjectRequest("Owned by someone else"));
var novel = await Novels.CreateAsync(new CreateNovelRequest("Owned by someone else"));
AsAdmin();
var role = await Access.GetMyRoleAsync(project);
var role = await Access.GetMyRoleAsync(novel);
Assert.That(role, Is.EqualTo("Admin"));
}
@@ -160,11 +160,11 @@ public class ProjectAccessTests : ServiceTestFixture
[Test]
public async Task A_user_granted_editor_sees_their_role_as_editor()
{
var project = await Projects.CreateAsync(new CreateProjectRequest("Granted Novel"));
var novel = await Novels.CreateAsync(new CreateNovelRequest("Granted Novel"));
var editorId = AsNewUser(GlobalRole.Reviewer);
GrantProjectRole(project.Id, editorId, ProjectRole.Editor);
GrantNovelRole(novel.Id, editorId, NovelRole.Editor);
var role = await Access.GetMyRoleAsync(project);
var role = await Access.GetMyRoleAsync(novel);
Assert.That(role, Is.EqualTo("Editor"));
}
@@ -172,10 +172,10 @@ public class ProjectAccessTests : ServiceTestFixture
[Test]
public async Task A_user_with_no_access_sees_a_null_role()
{
var project = await Projects.CreateAsync(new CreateProjectRequest("Someone Else's Novel"));
var novel = await Novels.CreateAsync(new CreateNovelRequest("Someone Else's Novel"));
AsNewUser(GlobalRole.Writer);
var role = await Access.GetMyRoleAsync(project);
var role = await Access.GetMyRoleAsync(novel);
Assert.That(role, Is.Null);
}
@@ -2,7 +2,7 @@ using System.Text.Json;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Novelly.Api.Agent;
using Novelly.Api.Projects;
using Novelly.Api.Novels;
namespace Novelly.Api.Tests;
@@ -12,7 +12,7 @@ public class NovelAgentServiceTests : ServiceTestFixture
private NovelAgentToolset _toolset = null!;
protected override void OnSetUp() =>
_toolset = new NovelAgentToolset(Projects, Characters, Arcs, Chapters, Beats, Tags, Questions, NullLogger<NovelAgentToolset>.Instance);
_toolset = new NovelAgentToolset(Novels, Characters, Arcs, Chapters, Beats, Tags, Questions, NullLogger<NovelAgentToolset>.Instance);
private NovelAgentService BuildAgent(ScriptedModelClient model) => new(
Db.Context,
@@ -25,11 +25,11 @@ public class NovelAgentServiceTests : ServiceTestFixture
[Test]
public async Task A_plain_reply_is_persisted_as_a_conversation()
{
var projectId = (await Projects.CreateAsync(new CreateProjectRequest("The Salt Road"))).Id;
var novelId = (await Novels.CreateAsync(new CreateNovelRequest("The Salt Road"))).Id;
var model = new ScriptedModelClient([[new AgentTextBlock("Tell me about the ending.")]]);
var agent = BuildAgent(model);
var turn = await agent.SendMessageAsync(projectId, new SendAgentMessageRequest("Where do I start?"));
var turn = await agent.SendMessageAsync(novelId, new SendAgentMessageRequest("Where do I start?"));
Assert.That(turn.Content, Is.EqualTo("Tell me about the ending."));
@@ -44,9 +44,9 @@ public class NovelAgentServiceTests : ServiceTestFixture
}
[Test]
public async Task Tool_calls_are_executed_against_real_project_data()
public async Task Tool_calls_are_executed_against_real_novel_data()
{
var projectId = (await Projects.CreateAsync(new CreateProjectRequest("The Salt Road"))).Id;
var novelId = (await Novels.CreateAsync(new CreateNovelRequest("The Salt Road"))).Id;
var model = new ScriptedModelClient([
[ToolUse("t1", "create_character", new { name = "Ines", role = "Protagonist" })],
@@ -54,9 +54,9 @@ public class NovelAgentServiceTests : ServiceTestFixture
]);
var turn = await BuildAgent(model).SendMessageAsync(
projectId, new SendAgentMessageRequest("Add a protagonist called Ines."));
novelId, new SendAgentMessageRequest("Add a protagonist called Ines."));
var characters = await Characters.ListAsync(projectId);
var characters = await Characters.ListAsync(novelId);
Assert.Multiple(() =>
{
@@ -71,7 +71,7 @@ public class NovelAgentServiceTests : ServiceTestFixture
[Test]
public async Task Every_tool_result_comes_back_in_a_single_user_turn()
{
var projectId = (await Projects.CreateAsync(new CreateProjectRequest("The Salt Road"))).Id;
var novelId = (await Novels.CreateAsync(new CreateNovelRequest("The Salt Road"))).Id;
var model = new ScriptedModelClient([
[
@@ -81,10 +81,10 @@ public class NovelAgentServiceTests : ServiceTestFixture
[new AgentTextBlock("Both added.")]
]);
await BuildAgent(model).SendMessageAsync(projectId, new SendAgentMessageRequest("Add two characters."));
await BuildAgent(model).SendMessageAsync(novelId, new SendAgentMessageRequest("Add two characters."));
var resultTurn = model.Transcripts[1][^1];
var listed = await Characters.ListAsync(projectId);
var listed = await Characters.ListAsync(novelId);
Assert.Multiple(() =>
{
@@ -97,7 +97,7 @@ public class NovelAgentServiceTests : ServiceTestFixture
[Test]
public async Task A_failing_tool_is_reported_back_rather_than_thrown()
{
var projectId = (await Projects.CreateAsync(new CreateProjectRequest("The Salt Road"))).Id;
var novelId = (await Novels.CreateAsync(new CreateNovelRequest("The Salt Road"))).Id;
var model = new ScriptedModelClient([
[ToolUse("t1", "update_character", new { character_id = Guid.NewGuid().ToString(), name = "Ines" })],
@@ -105,7 +105,7 @@ public class NovelAgentServiceTests : ServiceTestFixture
]);
var turn = await BuildAgent(model).SendMessageAsync(
projectId, new SendAgentMessageRequest("Rename her."));
novelId, new SendAgentMessageRequest("Rename her."));
var errorResult = model.Transcripts[1][^1].Content.OfType<AgentToolResultBlock>().Single();
@@ -120,14 +120,14 @@ public class NovelAgentServiceTests : ServiceTestFixture
[Test]
public async Task Unknown_tools_are_reported_without_breaking_the_loop()
{
var projectId = (await Projects.CreateAsync(new CreateProjectRequest("The Salt Road"))).Id;
var novelId = (await Novels.CreateAsync(new CreateNovelRequest("The Salt Road"))).Id;
var model = new ScriptedModelClient([
[ToolUse("t1", "summon_muse", new { })],
[new AgentTextBlock("Sorry — I do not have that tool.")]
]);
await BuildAgent(model).SendMessageAsync(projectId, new SendAgentMessageRequest("Summon the muse."));
await BuildAgent(model).SendMessageAsync(novelId, new SendAgentMessageRequest("Summon the muse."));
var result = model.Transcripts[1][^1].Content.OfType<AgentToolResultBlock>().Single();
@@ -141,14 +141,14 @@ public class NovelAgentServiceTests : ServiceTestFixture
[Test]
public async Task The_loop_stops_at_the_iteration_ceiling()
{
var projectId = (await Projects.CreateAsync(new CreateProjectRequest("The Salt Road"))).Id;
var novelId = (await Novels.CreateAsync(new CreateNovelRequest("The Salt Road"))).Id;
var model = new ScriptedModelClient(
Enumerable.Repeat<IReadOnlyList<AgentContentBlock>>(
[ToolUse("t", "list_characters", new { })], 20).ToList());
var turn = await BuildAgent(model).SendMessageAsync(
projectId, new SendAgentMessageRequest("Keep going forever."));
novelId, new SendAgentMessageRequest("Keep going forever."));
Assert.Multiple(() =>
{
@@ -160,16 +160,16 @@ public class NovelAgentServiceTests : ServiceTestFixture
[Test]
public async Task Follow_up_messages_continue_the_same_conversation()
{
var projectId = (await Projects.CreateAsync(new CreateProjectRequest("The Salt Road"))).Id;
var novelId = (await Novels.CreateAsync(new CreateNovelRequest("The Salt Road"))).Id;
var model = new ScriptedModelClient([
[new AgentTextBlock("First answer.")],
[new AgentTextBlock("Second answer.")]
]);
var agent = BuildAgent(model);
var first = await agent.SendMessageAsync(projectId, new SendAgentMessageRequest("Question one."));
var first = await agent.SendMessageAsync(novelId, new SendAgentMessageRequest("Question one."));
var second = await agent.SendMessageAsync(
projectId, new SendAgentMessageRequest("Question two.", first.ConversationId));
novelId, new SendAgentMessageRequest("Question two.", first.ConversationId));
var conversation = (await agent.GetConversationAsync(first.ConversationId))!;
@@ -2,23 +2,23 @@ using Microsoft.EntityFrameworkCore;
using Novelly.Api.Chapters;
using Novelly.Api.Characters;
using Novelly.Api.Common;
using Novelly.Api.Projects;
using Novelly.Api.Novels;
namespace Novelly.Api.Tests;
[TestFixture]
public class ProjectDataTests : ServiceTestFixture
public class NovelDataTests : ServiceTestFixture
{
private async Task<Guid> NewProjectAsync() =>
(await Projects.CreateAsync(new CreateProjectRequest("The Salt Road"))).Id;
private async Task<Guid> NewNovelAsync() =>
(await Novels.CreateAsync(new CreateNovelRequest("The Salt Road"))).Id;
[Test]
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 id = (await Novels.CreateAsync(
new CreateNovelRequest("Draft", Genre: "Fantasy", Logline: "A cartographer goes to sea."))).Id;
var afterPartialUpdate = (await Projects.UpdateAsync(id, new UpdateProjectRequest(Title: "The Salt Road")))!;
var afterPartialUpdate = (await Novels.UpdateAsync(id, new UpdateNovelRequest(Title: "The Salt Road")))!;
Assert.Multiple(() =>
{
@@ -27,7 +27,7 @@ public class ProjectDataTests : ServiceTestFixture
Assert.That(afterPartialUpdate.Logline, Is.EqualTo("A cartographer goes to sea."));
});
var afterClear = (await Projects.UpdateAsync(id, new UpdateProjectRequest(Genre: "")))!;
var afterClear = (await Novels.UpdateAsync(id, new UpdateNovelRequest(Genre: "")))!;
Assert.Multiple(() =>
{
@@ -37,23 +37,23 @@ public class ProjectDataTests : ServiceTestFixture
}
[Test]
public async Task New_projects_start_in_the_brainstorming_phase_and_can_be_advanced()
public async Task New_novels_start_in_the_brainstorming_phase_and_can_be_advanced()
{
var project = await Projects.CreateAsync(new CreateProjectRequest("The Salt Road"));
Assert.That(project.Phase, Is.EqualTo(ProjectPhase.Brainstorming));
var novel = await Novels.CreateAsync(new CreateNovelRequest("The Salt Road"));
Assert.That(novel.Phase, Is.EqualTo(NovelPhase.Brainstorming));
var afterAdvance = (await Projects.UpdateAsync(project.Id, new UpdateProjectRequest(Phase: ProjectPhase.Outlining)))!;
Assert.That(afterAdvance.Phase, Is.EqualTo(ProjectPhase.Outlining));
var afterAdvance = (await Novels.UpdateAsync(novel.Id, new UpdateNovelRequest(Phase: NovelPhase.Outlining)))!;
Assert.That(afterAdvance.Phase, Is.EqualTo(NovelPhase.Outlining));
var afterUnrelatedUpdate = (await Projects.UpdateAsync(project.Id, new UpdateProjectRequest(Genre: "Fantasy")))!;
Assert.That(afterUnrelatedUpdate.Phase, Is.EqualTo(ProjectPhase.Outlining));
var afterUnrelatedUpdate = (await Novels.UpdateAsync(novel.Id, new UpdateNovelRequest(Genre: "Fantasy")))!;
Assert.That(afterUnrelatedUpdate.Phase, Is.EqualTo(NovelPhase.Outlining));
}
[Test]
public async Task A_project_response_carries_the_owner_id_and_the_caller_s_role()
public async Task A_novel_response_carries_the_owner_id_and_the_caller_s_role()
{
var project = await Projects.CreateAsync(new CreateProjectRequest("The Salt Road"));
var response = project.ToResponse(await Access.GetMyRoleAsync(project));
var novel = await Novels.CreateAsync(new CreateNovelRequest("The Salt Road"));
var response = novel.ToResponse(await Access.GetMyRoleAsync(novel));
Assert.Multiple(() =>
{
@@ -65,10 +65,10 @@ public class ProjectDataTests : ServiceTestFixture
[Test]
public async Task Chapters_are_numbered_in_sequence_when_no_number_is_given()
{
var projectId = await NewProjectAsync();
var novelId = await NewNovelAsync();
var first = await Chapters.CreateAsync(projectId, new CreateChapterRequest("Landfall"));
var second = await Chapters.CreateAsync(projectId, new CreateChapterRequest("The Harbour"));
var first = await Chapters.CreateAsync(novelId, new CreateChapterRequest("Landfall"));
var second = await Chapters.CreateAsync(novelId, new CreateChapterRequest("The Harbour"));
Assert.Multiple(() =>
{
@@ -80,9 +80,9 @@ public class ProjectDataTests : ServiceTestFixture
[Test]
public async Task Word_count_is_recomputed_whenever_prose_changes()
{
var projectId = await NewProjectAsync();
var novelId = await NewNovelAsync();
var chapter = await Chapters.CreateAsync(projectId, new CreateChapterRequest(
var chapter = await Chapters.CreateAsync(novelId, new CreateChapterRequest(
"Landfall", Prose: "Five words go right here"));
Assert.That(chapter.WordCount, Is.EqualTo(5));
@@ -104,8 +104,8 @@ public class ProjectDataTests : ServiceTestFixture
[Test]
public async Task Chapter_updates_that_omit_prose_leave_the_draft_untouched()
{
var projectId = await NewProjectAsync();
var chapter = await Chapters.CreateAsync(projectId, new CreateChapterRequest(
var novelId = await NewNovelAsync();
var chapter = await Chapters.CreateAsync(novelId, new CreateChapterRequest(
"Landfall", Prose: "The tide came in slow."));
var updated = (await Chapters.UpdateAsync(chapter.Id, new UpdateChapterRequest(Status: DraftStatus.Revised)))!;
@@ -119,45 +119,45 @@ public class ProjectDataTests : ServiceTestFixture
}
[Test]
public async Task Deleting_a_project_takes_its_characters_and_chapters()
public async Task Deleting_a_novel_takes_its_characters_and_chapters()
{
var projectId = await NewProjectAsync();
await Characters.CreateAsync(projectId, new CreateCharacterRequest("Ines"));
await Chapters.CreateAsync(projectId, new CreateChapterRequest("Landfall"));
var novelId = await NewNovelAsync();
await Characters.CreateAsync(novelId, new CreateCharacterRequest("Ines"));
await Chapters.CreateAsync(novelId, new CreateChapterRequest("Landfall"));
await Projects.DeleteAsync(projectId);
await Novels.DeleteAsync(novelId);
using var verification = Db.CreateContext();
Assert.Multiple(async () =>
{
Assert.That(await verification.Projects.CountAsync(), Is.EqualTo(0));
Assert.That(await verification.Novels.CountAsync(), Is.EqualTo(0));
Assert.That(await verification.Characters.CountAsync(), Is.EqualTo(0));
Assert.That(await verification.Chapters.CountAsync(), Is.EqualTo(0));
});
}
[Test]
public async Task Relating_characters_across_projects_is_refused()
public async Task Relating_characters_across_novels_is_refused()
{
var firstProject = await NewProjectAsync();
var secondProject = (await Projects.CreateAsync(new CreateProjectRequest("Other Book"))).Id;
var firstNovel = await NewNovelAsync();
var secondNovel = (await Novels.CreateAsync(new CreateNovelRequest("Other Book"))).Id;
var ines = await Characters.CreateAsync(firstProject, new CreateCharacterRequest("Ines"));
var stranger = await Characters.CreateAsync(secondProject, new CreateCharacterRequest("Stranger"));
var ines = await Characters.CreateAsync(firstNovel, new CreateCharacterRequest("Ines"));
var stranger = await Characters.CreateAsync(secondNovel, new CreateCharacterRequest("Stranger"));
Assert.That(
async () => await Characters.AddRelationshipAsync(
ines.Id, new CreateRelationshipRequest(stranger.Id, "sister")),
Throws.TypeOf<InvalidOperationException>().With.Message.Contains("same project"));
Throws.TypeOf<InvalidOperationException>().With.Message.Contains("same novel"));
}
[Test]
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 novelId = await NewNovelAsync();
var ines = await Characters.CreateAsync(novelId, new CreateCharacterRequest("Ines"));
var mara = await Characters.CreateAsync(novelId, new CreateCharacterRequest("Mara"));
var updated = (await Characters.AddRelationshipAsync(
ines.Id, new CreateRelationshipRequest(mara.Id, "sister", "Estranged since the fire.")))!;
@@ -170,6 +170,6 @@ public class ProjectDataTests : ServiceTestFixture
}
[Test]
public async Task Reading_a_missing_project_returns_null_rather_than_throwing() =>
Assert.That(await Projects.GetAsync(Guid.NewGuid()), Is.Null);
public async Task Reading_a_missing_novel_returns_null_rather_than_throwing() =>
Assert.That(await Novels.GetAsync(Guid.NewGuid()), Is.Null);
}
+35 -35
View File
@@ -1,7 +1,7 @@
using Novelly.Api.Chapters;
using Novelly.Api.Characters;
using Novelly.Api.Common;
using Novelly.Api.Projects;
using Novelly.Api.Novels;
using Novelly.Api.Questions;
namespace Novelly.Api.Tests;
@@ -9,21 +9,21 @@ namespace Novelly.Api.Tests;
[TestFixture]
public class OpenQuestionTests : ServiceTestFixture
{
private Guid _projectId;
private Guid _novelId;
private Guid _chapterId;
private Guid _characterId;
protected override void OnSetUp()
{
_projectId = Projects.CreateAsync(new CreateProjectRequest("The Salt Road")).Result.Id;
_chapterId = Chapters.CreateAsync(_projectId, new CreateChapterRequest("Landfall")).Result.Id;
_characterId = Characters.CreateAsync(_projectId, new CreateCharacterRequest("Ines")).Result.Id;
_novelId = Novels.CreateAsync(new CreateNovelRequest("The Salt Road")).Result.Id;
_chapterId = Chapters.CreateAsync(_novelId, new CreateChapterRequest("Landfall")).Result.Id;
_characterId = Characters.CreateAsync(_novelId, new CreateCharacterRequest("Ines")).Result.Id;
}
[Test]
public async Task A_question_can_hang_off_a_chapter_and_a_character_at_once()
{
var question = await Questions.CreateAsync(_projectId, new CreateOpenQuestionRequest(
var question = await Questions.CreateAsync(_novelId, new CreateOpenQuestionRequest(
"Does she know about the letter before the harbour?",
ChapterId: _chapterId,
CharacterId: _characterId));
@@ -41,7 +41,7 @@ public class OpenQuestionTests : ServiceTestFixture
public async Task A_question_about_the_book_as_a_whole_needs_no_association()
{
var question = await Questions.CreateAsync(
_projectId, new CreateOpenQuestionRequest("Is this one book or two?"));
_novelId, new CreateOpenQuestionRequest("Is this one book or two?"));
Assert.Multiple(() =>
{
@@ -53,21 +53,21 @@ public class OpenQuestionTests : ServiceTestFixture
[Test]
public async Task The_outline_and_the_character_page_each_see_only_their_own_questions()
{
await Questions.CreateAsync(_projectId, new CreateOpenQuestionRequest(
await Questions.CreateAsync(_novelId, new CreateOpenQuestionRequest(
"Where does the chapter break?", ChapterId: _chapterId));
await Questions.CreateAsync(_projectId, new CreateOpenQuestionRequest(
await Questions.CreateAsync(_novelId, new CreateOpenQuestionRequest(
"What does she actually want?", CharacterId: _characterId));
await Questions.CreateAsync(_projectId, new CreateOpenQuestionRequest("Is this one book or two?"));
await Questions.CreateAsync(_novelId, new CreateOpenQuestionRequest("Is this one book or two?"));
var forChapter = await Questions.ListAsync(_projectId, chapterId: _chapterId);
var forCharacter = await Questions.ListAsync(_projectId, characterId: _characterId);
var forProject = await Questions.ListAsync(_projectId);
var forChapter = await Questions.ListAsync(_novelId, chapterId: _chapterId);
var forCharacter = await Questions.ListAsync(_novelId, characterId: _characterId);
var forNovel = await Questions.ListAsync(_novelId);
Assert.Multiple(() =>
{
Assert.That(forChapter.Select(q => q.Question), Is.EqualTo(new[] { "Where does the chapter break?" }));
Assert.That(forCharacter.Select(q => q.Question), Is.EqualTo(new[] { "What does she actually want?" }));
Assert.That(forProject, Has.Count.EqualTo(3));
Assert.That(forNovel, Has.Count.EqualTo(3));
});
}
@@ -75,13 +75,13 @@ public class OpenQuestionTests : ServiceTestFixture
public async Task Resolved_questions_drop_off_the_list_unless_asked_for()
{
var settled = await Questions.CreateAsync(
_projectId, new CreateOpenQuestionRequest("Where does the chapter break?"));
await Questions.CreateAsync(_projectId, new CreateOpenQuestionRequest("Is this one book or two?"));
_novelId, new CreateOpenQuestionRequest("Where does the chapter break?"));
await Questions.CreateAsync(_novelId, new CreateOpenQuestionRequest("Is this one book or two?"));
await Questions.ResolveAsync(settled.Id, new ResolveOpenQuestionRequest("After the harbour."));
var open = await Questions.ListAsync(_projectId);
var everything = await Questions.ListAsync(_projectId, includeResolved: true);
var open = await Questions.ListAsync(_novelId);
var everything = await Questions.ListAsync(_novelId, includeResolved: true);
Assert.Multiple(() =>
{
@@ -97,7 +97,7 @@ public class OpenQuestionTests : ServiceTestFixture
public async Task Resolving_records_what_was_decided()
{
var question = await Questions.CreateAsync(
_projectId, new CreateOpenQuestionRequest("Where does the chapter break?"));
_novelId, new CreateOpenQuestionRequest("Where does the chapter break?"));
var resolved = (await Questions.ResolveAsync(
question.Id, new ResolveOpenQuestionRequest("After the harbour burns.")))!;
@@ -115,7 +115,7 @@ public class OpenQuestionTests : ServiceTestFixture
{
await Chapters.UpdateAsync(_chapterId, new UpdateChapterRequest(Notes: "Runs long."));
var question = await Questions.CreateAsync(_projectId, new CreateOpenQuestionRequest(
var question = await Questions.CreateAsync(_novelId, new CreateOpenQuestionRequest(
"Where does the chapter break?", ChapterId: _chapterId, CharacterId: _characterId));
await Questions.ResolveAsync(
@@ -136,7 +136,7 @@ public class OpenQuestionTests : ServiceTestFixture
[Test]
public async Task A_resolution_stays_off_the_notes_unless_asked_for()
{
var question = await Questions.CreateAsync(_projectId, new CreateOpenQuestionRequest(
var question = await Questions.CreateAsync(_novelId, new CreateOpenQuestionRequest(
"Where does the chapter break?", ChapterId: _chapterId));
await Questions.ResolveAsync(question.Id, new ResolveOpenQuestionRequest("After the harbour."));
@@ -147,7 +147,7 @@ public class OpenQuestionTests : ServiceTestFixture
[Test]
public async Task Reopening_clears_the_resolution_but_leaves_the_note_behind()
{
var question = await Questions.CreateAsync(_projectId, new CreateOpenQuestionRequest(
var question = await Questions.CreateAsync(_novelId, new CreateOpenQuestionRequest(
"Where does the chapter break?", ChapterId: _chapterId));
await Questions.ResolveAsync(
@@ -167,7 +167,7 @@ public class OpenQuestionTests : ServiceTestFixture
[Test]
public async Task A_question_can_be_detached_from_what_it_was_about()
{
var question = await Questions.CreateAsync(_projectId, new CreateOpenQuestionRequest(
var question = await Questions.CreateAsync(_novelId, new CreateOpenQuestionRequest(
"Where does the chapter break?", ChapterId: _chapterId, CharacterId: _characterId));
var detached = (await Questions.UpdateAsync(
@@ -184,7 +184,7 @@ public class OpenQuestionTests : ServiceTestFixture
[Test]
public async Task Deleting_a_chapter_leaves_its_questions_open_rather_than_taking_them()
{
var question = await Questions.CreateAsync(_projectId, new CreateOpenQuestionRequest(
var question = await Questions.CreateAsync(_novelId, new CreateOpenQuestionRequest(
"Does she know about the letter?", ChapterId: _chapterId));
await Chapters.DeleteAsync(_chapterId);
@@ -202,52 +202,52 @@ public class OpenQuestionTests : ServiceTestFixture
public async Task A_question_can_be_deleted_outright()
{
var question = await Questions.CreateAsync(
_projectId, new CreateOpenQuestionRequest("Where does the chapter break?"));
_novelId, new CreateOpenQuestionRequest("Where does the chapter break?"));
await Questions.DeleteAsync(question.Id);
Assert.Multiple(async () =>
{
Assert.That(await Questions.ListAsync(_projectId, includeResolved: true), Is.Empty);
Assert.That(await Questions.ListAsync(_novelId, includeResolved: true), Is.Empty);
Assert.That(await Questions.GetAsync(question.Id), Is.Null);
});
}
[Test]
public async Task Deleting_a_project_takes_its_questions()
public async Task Deleting_a_novel_takes_its_questions()
{
await Questions.CreateAsync(_projectId, new CreateOpenQuestionRequest("Is this one book or two?"));
await Questions.CreateAsync(_novelId, new CreateOpenQuestionRequest("Is this one book or two?"));
await Projects.DeleteAsync(_projectId);
await Novels.DeleteAsync(_novelId);
using var verification = Db.CreateContext();
Assert.That(verification.OpenQuestions.Count(), Is.EqualTo(0));
}
[Test]
public void A_question_cannot_be_attached_to_another_project_s_chapter()
public void A_question_cannot_be_attached_to_another_novel_s_chapter()
{
var elsewhere = Chapters.CreateAsync(
Projects.CreateAsync(new CreateProjectRequest("Other Book")).Result.Id,
Novels.CreateAsync(new CreateNovelRequest("Other Book")).Result.Id,
new CreateChapterRequest("Elsewhere")).Result;
Assert.That(
async () => await Questions.CreateAsync(
_projectId, new CreateOpenQuestionRequest("A question", ChapterId: elsewhere.Id)),
Throws.TypeOf<InvalidOperationException>().With.Message.Contains("same project"));
_novelId, new CreateOpenQuestionRequest("A question", ChapterId: elsewhere.Id)),
Throws.TypeOf<InvalidOperationException>().With.Message.Contains("same novel"));
}
[Test]
public void A_blank_question_is_refused() =>
Assert.That(
async () => await Questions.CreateAsync(_projectId, new CreateOpenQuestionRequest(" ")),
async () => await Questions.CreateAsync(_novelId, new CreateOpenQuestionRequest(" ")),
Throws.TypeOf<ArgumentException>());
[Test]
public async Task Resolving_with_nothing_decided_is_refused()
{
var question = await Questions.CreateAsync(
_projectId, new CreateOpenQuestionRequest("Where does the chapter break?"));
_novelId, new CreateOpenQuestionRequest("Where does the chapter break?"));
Assert.That(
async () => await Questions.ResolveAsync(question.Id, new ResolveOpenQuestionRequest(" ")),
@@ -2,7 +2,7 @@ using Novelly.Api.Beats;
using Novelly.Api.Chapters;
using Novelly.Api.Characters;
using Novelly.Api.Genres;
using Novelly.Api.Projects;
using Novelly.Api.Novels;
using Novelly.Api.Questions;
using Novelly.Api.Tags;
using Novelly.Api.Users;
@@ -13,9 +13,9 @@ public abstract class ServiceTestFixture
{
protected TestDatabase Db { get; private set; } = null!;
protected TestUserContext UserContext { get; private set; } = null!;
protected ProjectAccessService Access { get; private set; } = null!;
protected NovelAccessService Access { get; private set; } = null!;
protected TagService Tags { get; private set; } = null!;
protected ProjectService Projects { get; private set; } = null!;
protected NovelService Novels { get; private set; } = null!;
protected CharacterService Characters { get; private set; } = null!;
protected ChapterService Chapters { get; private set; } = null!;
protected BeatService Beats { get; private set; } = null!;
@@ -23,7 +23,7 @@ public abstract class ServiceTestFixture
protected OpenQuestionService Questions { get; private set; } = null!;
protected GenreService Genres { get; private set; } = null!;
protected CapturingLogger<ProjectService> ProjectLogs { get; private set; } = null!;
protected CapturingLogger<NovelService> NovelLogs { get; private set; } = null!;
protected CapturingLogger<CharacterService> CharacterLogs { get; private set; } = null!;
protected CapturingLogger<ChapterService> ChapterLogs { get; private set; } = null!;
protected CapturingLogger<BeatService> BeatLogs { get; private set; } = null!;
@@ -37,7 +37,7 @@ public abstract class ServiceTestFixture
{
Db = new TestDatabase();
UserContext = new TestUserContext();
Access = new ProjectAccessService(Db.Context, UserContext, new CapturingLogger<ProjectAccessService>());
Access = new NovelAccessService(Db.Context, UserContext, new CapturingLogger<NovelAccessService>());
Db.Context.Users.Add(new NovellyUser
{
@@ -50,7 +50,7 @@ public abstract class ServiceTestFixture
Db.Context.SaveChanges();
TagLogs = new CapturingLogger<TagService>();
ProjectLogs = new CapturingLogger<ProjectService>();
NovelLogs = new CapturingLogger<NovelService>();
CharacterLogs = new CapturingLogger<CharacterService>();
ChapterLogs = new CapturingLogger<ChapterService>();
BeatLogs = new CapturingLogger<BeatService>();
@@ -59,8 +59,8 @@ public abstract class ServiceTestFixture
GenreLogs = new CapturingLogger<GenreService>();
Tags = new TagService(Db.Context, Access, TagLogs, new CreateTagRequestValidator(), new UpdateTagRequestValidator());
Projects = new ProjectService(
Db.Context, Access, UserContext, ProjectLogs, new CreateProjectRequestValidator(), new UpdateProjectRequestValidator());
Novels = new NovelService(
Db.Context, Access, UserContext, NovelLogs, new CreateNovelRequestValidator(), new UpdateNovelRequestValidator());
Characters = new CharacterService(
Db.Context, Access, Tags, CharacterLogs,
new CreateCharacterRequestValidator(), new UpdateCharacterRequestValidator(), new CreateRelationshipRequestValidator(),
+32 -32
View File
@@ -2,7 +2,7 @@ using Microsoft.EntityFrameworkCore;
using Novelly.Api.Beats;
using Novelly.Api.Chapters;
using Novelly.Api.Characters;
using Novelly.Api.Projects;
using Novelly.Api.Novels;
using Novelly.Api.Tags;
namespace Novelly.Api.Tests;
@@ -10,34 +10,34 @@ namespace Novelly.Api.Tests;
[TestFixture]
public class TagServiceTests : ServiceTestFixture
{
private Guid _projectId;
private Guid _novelId;
protected override void OnSetUp() =>
_projectId = Projects.CreateAsync(new CreateProjectRequest("The Salt Road")).Result.Id;
_novelId = Novels.CreateAsync(new CreateNovelRequest("The Salt Road")).Result.Id;
[Test]
public async Task Applying_an_unknown_tag_by_name_creates_it()
{
var character = await Characters.CreateAsync(
_projectId, new CreateCharacterRequest("Ines", Tags: ["betrayal", "the sea"]));
_novelId, new CreateCharacterRequest("Ines", Tags: ["betrayal", "the sea"]));
Assert.Multiple(async () =>
{
Assert.That(
character.Tags.Select(t => t.Name),
Is.EquivalentTo(new[] { "betrayal", "the sea" }));
Assert.That(await Tags.ListAsync(_projectId), Has.Count.EqualTo(2));
Assert.That(await Tags.ListAsync(_novelId), Has.Count.EqualTo(2));
});
}
[Test]
public async Task The_same_name_resolves_to_one_tag_regardless_of_casing()
{
await Characters.CreateAsync(_projectId, new CreateCharacterRequest("Ines", Tags: ["Betrayal"]));
await Characters.CreateAsync(_novelId, new CreateCharacterRequest("Ines", Tags: ["Betrayal"]));
var chapter = await Chapters.CreateAsync(
_projectId, new CreateChapterRequest("Landfall", Tags: ["betrayal"]));
_novelId, new CreateChapterRequest("Landfall", Tags: ["betrayal"]));
var listed = await Tags.ListAsync(_projectId);
var listed = await Tags.ListAsync(_novelId);
Assert.Multiple(() =>
{
@@ -52,7 +52,7 @@ public class TagServiceTests : ServiceTestFixture
public async Task Supplying_a_tag_list_replaces_the_existing_tags()
{
var character = await Characters.CreateAsync(
_projectId, new CreateCharacterRequest("Ines", Tags: ["betrayal", "the sea"]));
_novelId, new CreateCharacterRequest("Ines", Tags: ["betrayal", "the sea"]));
var updated = (await Characters.UpdateAsync(
character.Id, new UpdateCharacterRequest(Tags: ["the sea", "maps"])))!;
@@ -64,7 +64,7 @@ public class TagServiceTests : ServiceTestFixture
public async Task Omitting_the_tag_list_leaves_tags_alone()
{
var character = await Characters.CreateAsync(
_projectId, new CreateCharacterRequest("Ines", Tags: ["betrayal"]));
_novelId, new CreateCharacterRequest("Ines", Tags: ["betrayal"]));
var updated = (await Characters.UpdateAsync(
character.Id, new UpdateCharacterRequest(Occupation: "Cartographer")))!;
@@ -80,14 +80,14 @@ public class TagServiceTests : ServiceTestFixture
[Test]
public async Task Cross_reference_gathers_everything_carrying_a_tag()
{
await Characters.CreateAsync(_projectId, new CreateCharacterRequest("Ines", Tags: ["betrayal"]));
await Characters.CreateAsync(_novelId, new CreateCharacterRequest("Ines", Tags: ["betrayal"]));
var chapter = await Chapters.CreateAsync(
_projectId, new CreateChapterRequest("Landfall", Tags: ["betrayal"]));
_novelId, new CreateChapterRequest("Landfall", Tags: ["betrayal"]));
await Beats.CreateAsync(chapter.Id, new CreateBeatRequest(
"She burns the atlas", WhatHappened: "In the galley stove.", Tags: ["betrayal"]));
await Beats.CreateAsync(chapter.Id, new CreateBeatRequest("Unrelated beat"));
var tagId = (await Tags.ListAsync(_projectId)).Single().Id;
var tagId = (await Tags.ListAsync(_novelId)).Single().Id;
var references = (await Tags.GetReferencesAsync(tagId))!;
Assert.Multiple(() =>
@@ -106,12 +106,12 @@ public class TagServiceTests : ServiceTestFixture
[Test]
public async Task Usage_counts_are_reported_per_kind()
{
await Characters.CreateAsync(_projectId, new CreateCharacterRequest("Ines", Tags: ["sea"]));
await Characters.CreateAsync(_projectId, new CreateCharacterRequest("Mara", Tags: ["sea"]));
var chapter = await Chapters.CreateAsync(_projectId, new CreateChapterRequest("Landfall"));
await Characters.CreateAsync(_novelId, new CreateCharacterRequest("Ines", Tags: ["sea"]));
await Characters.CreateAsync(_novelId, new CreateCharacterRequest("Mara", Tags: ["sea"]));
var chapter = await Chapters.CreateAsync(_novelId, new CreateChapterRequest("Landfall"));
await Beats.CreateAsync(chapter.Id, new CreateBeatRequest("A beat", Tags: ["sea"]));
var summary = (await Tags.ListAsync(_projectId)).Single();
var summary = (await Tags.ListAsync(_novelId)).Single();
Assert.Multiple(() =>
{
@@ -125,13 +125,13 @@ public class TagServiceTests : ServiceTestFixture
[Test]
public async Task Duplicate_tag_names_are_refused_on_create_and_rename()
{
await Tags.CreateAsync(_projectId, new CreateTagRequest("betrayal"));
await Tags.CreateAsync(_novelId, new CreateTagRequest("betrayal"));
Assert.That(
async () => await Tags.CreateAsync(_projectId, new CreateTagRequest("Betrayal")),
async () => await Tags.CreateAsync(_novelId, new CreateTagRequest("Betrayal")),
Throws.TypeOf<InvalidOperationException>().With.Message.Contains("already has a tag"));
var other = await Tags.CreateAsync(_projectId, new CreateTagRequest("the sea"));
var other = await Tags.CreateAsync(_novelId, new CreateTagRequest("the sea"));
Assert.That(
async () => await Tags.UpdateAsync(other.Id, new UpdateTagRequest(Name: "betrayal")),
@@ -139,19 +139,19 @@ public class TagServiceTests : ServiceTestFixture
}
[Test]
public async Task Tags_are_scoped_to_their_project()
public async Task Tags_are_scoped_to_their_novel()
{
var otherProject = await Projects.CreateAsync(new CreateProjectRequest("Other Book"));
var otherNovel = await Novels.CreateAsync(new CreateNovelRequest("Other Book"));
await Characters.CreateAsync(_projectId, new CreateCharacterRequest("Ines", Tags: ["sea"]));
await Characters.CreateAsync(otherProject.Id, new CreateCharacterRequest("Someone", Tags: ["sea"]));
await Characters.CreateAsync(_novelId, new CreateCharacterRequest("Ines", Tags: ["sea"]));
await Characters.CreateAsync(otherNovel.Id, new CreateCharacterRequest("Someone", Tags: ["sea"]));
using var verification = Db.CreateContext();
Assert.Multiple(async () =>
{
Assert.That(await Tags.ListAsync(_projectId), Has.Count.EqualTo(1));
Assert.That(await Tags.ListAsync(otherProject.Id), Has.Count.EqualTo(1));
Assert.That(await Tags.ListAsync(_novelId), Has.Count.EqualTo(1));
Assert.That(await Tags.ListAsync(otherNovel.Id), Has.Count.EqualTo(1));
Assert.That(await verification.Tags.CountAsync(), Is.EqualTo(2));
});
}
@@ -160,8 +160,8 @@ public class TagServiceTests : ServiceTestFixture
public async Task Deleting_a_tag_leaves_what_carried_it_intact()
{
var character = await Characters.CreateAsync(
_projectId, new CreateCharacterRequest("Ines", Tags: ["betrayal"]));
var tagId = (await Tags.ListAsync(_projectId)).Single().Id;
_novelId, new CreateCharacterRequest("Ines", Tags: ["betrayal"]));
var tagId = (await Tags.ListAsync(_novelId)).Single().Id;
await Tags.DeleteAsync(tagId);
@@ -175,11 +175,11 @@ public class TagServiceTests : ServiceTestFixture
}
[Test]
public async Task Deleting_a_project_takes_its_tags()
public async Task Deleting_a_novel_takes_its_tags()
{
await Characters.CreateAsync(_projectId, new CreateCharacterRequest("Ines", Tags: ["betrayal"]));
await Characters.CreateAsync(_novelId, new CreateCharacterRequest("Ines", Tags: ["betrayal"]));
await Projects.DeleteAsync(_projectId);
await Novels.DeleteAsync(_novelId);
using var verification = Db.CreateContext();
Assert.That(await verification.Tags.CountAsync(), Is.EqualTo(0));
@@ -188,6 +188,6 @@ public class TagServiceTests : ServiceTestFixture
[Test]
public void A_blank_tag_name_is_refused() =>
Assert.That(
async () => await Tags.CreateAsync(_projectId, new CreateTagRequest(" ")),
async () => await Tags.CreateAsync(_novelId, new CreateTagRequest(" ")),
Throws.TypeOf<ArgumentException>());
}