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:
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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"));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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" }));
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
+34
-34
@@ -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))!;
|
||||
|
||||
|
||||
+42
-42
@@ -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);
|
||||
}
|
||||
@@ -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(),
|
||||
|
||||
@@ -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>());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user