diff --git a/.gitignore b/.gitignore
index 9421f42..ba0001c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -487,4 +487,6 @@ $RECYCLE.BIN/
/trpg.db
/trpg.db-wal
/trpg.db-shm
-*.db
\ No newline at end of file
+*.db
+
+/coveragereport
\ No newline at end of file
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 2017b8c..7009022 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -16,5 +16,9 @@
+
+
+
+
\ No newline at end of file
diff --git a/src/Modules/Game.Service/Services/CheckService.cs b/src/Modules/Game.Service/Services/CheckService.cs
index 6530a41..53b1067 100644
--- a/src/Modules/Game.Service/Services/CheckService.cs
+++ b/src/Modules/Game.Service/Services/CheckService.cs
@@ -20,12 +20,11 @@ public CheckService(TrpgDbContext context)
public async Task RollDiceAsync(string diceExpression)
{
var parts = diceExpression.ToLower().Split('d');
- if (parts.Length != 2) throw new ArgumentException("Invalid dice expression.");
-
- int numDice = int.Parse(parts[0]);
- int numSides = int.Parse(parts[1]);
-
- var random = new Random();
+ if (parts.Length != 2)
+ throw new ArgumentException("Invalid dice expression.");
+ if (!int.TryParse(parts[0], out int numDice) || !int.TryParse(parts[1], out int numSides))
+ throw new ArgumentException("Invalid dice expression.");
+ var random = new Random();
int total = 0;
for (int i = 0; i < numDice; i++)
{
diff --git a/src/ToolBox/Tools/GameTools/ScenarioTools.cs b/src/ToolBox/Tools/GameTools/ScenarioTools.cs
index f8edc23..de1fef0 100644
--- a/src/ToolBox/Tools/GameTools/ScenarioTools.cs
+++ b/src/ToolBox/Tools/GameTools/ScenarioTools.cs
@@ -1,7 +1,6 @@
using Microsoft.Extensions.DependencyInjection;
using ModelContextProtocol.Server;
using System.ComponentModel;
-using System.Text.Json;
using Game.Service.Interface;
using Game.Service.View;
using Common.Model;
diff --git a/src/UnitTests/Game.Test/Game.Test.csproj b/src/UnitTests/Game.Test/Game.Test.csproj
new file mode 100644
index 0000000..eea8252
--- /dev/null
+++ b/src/UnitTests/Game.Test/Game.Test.csproj
@@ -0,0 +1,26 @@
+
+
+
+ net10.0
+ enable
+ enable
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/UnitTests/Game.Test/Tests/CharacterTest.cs b/src/UnitTests/Game.Test/Tests/CharacterTest.cs
new file mode 100644
index 0000000..9b4b887
--- /dev/null
+++ b/src/UnitTests/Game.Test/Tests/CharacterTest.cs
@@ -0,0 +1,267 @@
+using Game.Service.Data;
+using Game.Service.Data.Models;
+using Game.Service.Request;
+using Game.Service.Services;
+using Microsoft.EntityFrameworkCore;
+namespace Game.Test.Tests
+{
+ public class CharacterTest
+ {
+ [Fact]
+ public async Task GetAllCharactersAsync_ReturnFalse()
+ {
+ // Arrange
+ var options = new DbContextOptionsBuilder()
+ .UseSqlite("DataSource=:memory:")
+ .Options;
+ using var context = new TrpgDbContext(options);
+ context.Database.OpenConnection();
+ context.Database.EnsureCreated();
+ var service = new CharacterService(context);
+
+ // Act
+ var result = await service.GetAllCharactersAsync();
+
+ // Assert
+ Assert.Empty(result);
+ }
+
+ [Fact]
+ public async Task CreateCharacterAsync_And_GetCharacterByIdAsync()
+ {
+ // Arrange
+ var options = new DbContextOptionsBuilder()
+ .UseSqlite("DataSource=:memory:")
+ .Options;
+ using var context = new TrpgDbContext(options);
+ context.Database.OpenConnection();
+ context.Database.EnsureCreated();
+ var service = new CharacterService(context);
+ var request = new PlayerCharacterRequest
+ {
+ Name = "Test",
+ Gender = "M",
+ Age = 20,
+ PhysicalDesc = "Desc",
+ Biography = "Bio",
+ StatusEffects = "None",
+ Notes = "Note",
+ IsDead = false,
+ IsTemplate = false,
+ IsActive = true
+ };
+
+ // Act
+ var created = await service.CreateCharacterAsync(request);
+ var fetched = await service.GetCharacterByIdAsync(created!.Id);
+
+ // Assert
+ Assert.NotNull(created);
+ Assert.NotNull(fetched);
+ Assert.Equal("Test", fetched.Name);
+ }
+
+ [Fact]
+ public async Task UpdateCharacterAsync_UpdatesFields()
+ {
+ // Arrange
+ var options = new DbContextOptionsBuilder()
+ .UseSqlite("DataSource=:memory:")
+ .Options;
+ using var context = new TrpgDbContext(options);
+ context.Database.OpenConnection();
+ context.Database.EnsureCreated();
+ var service = new CharacterService(context);
+ var request = new PlayerCharacterRequest { Name = "Old", Gender = "M", Age = 20, IsActive = true };
+
+ // Act
+ var created = await service.CreateCharacterAsync(request);
+ var update = new PlayerCharacterRequest { Name = "New", Gender = "F", Age = 30, IsActive = false };
+ var updated = await service.UpdateCharacterAsync(created!.Id, update);
+
+ // Assert
+ Assert.NotNull(created);
+ Assert.NotNull(updated);
+ Assert.Equal("New", updated.Name);
+ Assert.Equal("F", updated.Gender);
+ Assert.Equal(30, updated.Age);
+ Assert.False(updated.IsActive);
+ }
+
+ [Fact]
+ public async Task DeleteCharacterAsync_RemovesCharacter()
+ {
+ // Arrange
+ var options = new DbContextOptionsBuilder()
+ .UseSqlite("DataSource=:memory:")
+ .Options;
+ using var context = new TrpgDbContext(options);
+ context.Database.OpenConnection();
+ context.Database.EnsureCreated();
+ var service = new CharacterService(context);
+ var request = new PlayerCharacterRequest { Name = "DeleteMe", Gender = "M", Age = 20, IsActive = true };
+ var created = await service.CreateCharacterAsync(request);
+
+ // Act
+ var deleted = await service.DeleteCharacterAsync(created!.Id);
+ var fetched = await service.GetCharacterByIdAsync(created.Id);
+
+ // Assert
+ Assert.NotNull(created);
+ Assert.True(deleted);
+ Assert.Null(fetched);
+ }
+
+ [Fact]
+ public async Task UpdateCharacterAttributeAsync_UpdatesValue()
+ {
+ // Arrange
+ var options = new DbContextOptionsBuilder()
+ .UseSqlite("DataSource=:memory:")
+ .Options;
+ using var context = new TrpgDbContext(options);
+ context.Database.OpenConnection();
+ context.Database.EnsureCreated();
+ var service = new CharacterService(context);
+ var attr = new Attributes { Name = "HP", Description = "Health" };
+ context.Attributes.Add(attr);
+ await context.SaveChangesAsync();
+ var request = new PlayerCharacterRequest { Name = "AttrTest", Gender = "M", Age = 20, IsActive = true };
+ var created = await service.CreateCharacterAsync(request);
+ Assert.NotNull(created);
+ var charAttr = new CharacterAttribute { CharacterId = created.Id, AttributeId = attr.Id, MaxValue = 100, CurrentValue = 50 };
+ context.CharacterAttributes.Add(charAttr);
+ await context.SaveChangesAsync();
+
+ // Act
+ var updated = await service.UpdateCharacterAttributeAsync(created.Id, "HP", 80);
+ var fetched = await service.GetCharacterByIdAsync(created.Id);
+ var hp = fetched!.CharacterAttributes.FirstOrDefault(a => a != null && a.Attribute != null && a.Attribute.Name == "HP");
+
+ // Assert
+ Assert.NotNull(updated);
+ Assert.NotNull(fetched);
+ Assert.NotNull(hp);
+ Assert.NotNull(hp.Attribute);
+ Assert.Equal(80, hp.CurrentValue);
+ }
+
+ [Fact]
+ public async Task CreateCharacterFromTemplateIdAsync_CopiesTemplate()
+ {
+ // Arrange
+ var options = new DbContextOptionsBuilder()
+ .UseSqlite("DataSource=:memory:")
+ .Options;
+ using var context = new TrpgDbContext(options);
+ context.Database.OpenConnection();
+ context.Database.EnsureCreated();
+ var service = new CharacterService(context);
+ var attr = new Attributes { Name = "HP", Description = "Health" };
+ var skill = new Skill { Name = "Sword", Description = "Sword skill", Category = "Combat", BaseSuccessRate = 50, IsBasic = true, IsActive = true, DisplayOrder = 1, CreatedAt = DateTime.UtcNow, UpdatedAt = DateTime.UtcNow };
+ var item = new Item { Name = "Potion", Description = "Heals HP", Category = "Consumable", Stats = "Heal:50", OwnerNotes = "", Weight = 0.1M, IsConsumable = true, IsCursed = false, IsActive = true, DisplayOrder = 1, CreatedAt = DateTime.UtcNow, UpdatedAt = DateTime.UtcNow };
+ context.Attributes.Add(attr);
+ context.Skills.Add(skill);
+ context.Items.Add(item);
+ await context.SaveChangesAsync();
+ var template = new PlayerCharacter { Name = "Template", Gender = "F", Age = 25, IsTemplate = true, IsActive = true, CreatedAt = DateTime.UtcNow, UpdatedAt = DateTime.UtcNow };
+ context.PlayerCharacters.Add(template);
+ await context.SaveChangesAsync();
+ var charAttr = new CharacterAttribute { CharacterId = template.Id, AttributeId = attr.Id, MaxValue = 100, CurrentValue = 100 };
+ var charSkill = new CharacterSkill { CharacterId = template.Id, SkillId = skill.Id, Proficiency = 80 };
+ var charItem = new CharacterItem { CharacterId = template.Id, ItemId = item.Id, Quantity = 2 };
+ context.CharacterAttributes.Add(charAttr);
+ context.CharacterSkills.Add(charSkill);
+ context.CharacterItems.Add(charItem);
+ await context.SaveChangesAsync();
+
+ // Act
+ var copied = await service.CreateCharacterFromTemplateIdAsync(template.Id);
+
+ // Assert
+ Assert.NotNull(copied);
+ Assert.Equal("Template", copied.Name);
+ Assert.False(copied.IsTemplate);
+ Assert.Single(copied.CharacterAttributes);
+ Assert.Single(copied.CharacterSkills);
+ Assert.Single(copied.CharacterItems);
+ }
+
+ [Fact]
+ public async Task UpdateCharacterAsync_ReturnsNull_WhenNotFound()
+ {
+ // Arrange
+ var options = new DbContextOptionsBuilder()
+ .UseSqlite("DataSource=:memory:")
+ .Options;
+ using var context = new TrpgDbContext(options);
+ context.Database.OpenConnection();
+ context.Database.EnsureCreated();
+ var service = new CharacterService(context);
+ var update = new PlayerCharacterRequest { Name = "X", Gender = "F", Age = 1, IsActive = false };
+
+ // Act
+ var result = await service.UpdateCharacterAsync(999, update);
+
+ // Assert
+ Assert.Null(result);
+ }
+
+ [Fact]
+ public async Task UpdateCharacterAttributeAsync_ReturnsNull_WhenNotFound()
+ {
+ // Arrange
+ var options = new DbContextOptionsBuilder()
+ .UseSqlite("DataSource=:memory:")
+ .Options;
+ using var context = new TrpgDbContext(options);
+ context.Database.OpenConnection();
+ context.Database.EnsureCreated();
+ var service = new CharacterService(context);
+
+ // Act
+ var result = await service.UpdateCharacterAttributeAsync(999, "NotExist", 10);
+
+ // Assert
+ Assert.Null(result);
+ }
+
+ [Fact]
+ public async Task DeleteCharacterAsync_ReturnsFalse_WhenNotFound()
+ {
+ // Arrange
+ var options = new DbContextOptionsBuilder()
+ .UseSqlite("DataSource=:memory:")
+ .Options;
+ using var context = new TrpgDbContext(options);
+ context.Database.OpenConnection();
+ context.Database.EnsureCreated();
+ var service = new CharacterService(context);
+
+ // Act
+ var result = await service.DeleteCharacterAsync(999);
+
+ // Assert
+ Assert.False(result);
+ }
+
+ [Fact]
+ public async Task CreateCharacterFromTemplateIdAsync_ReturnsNull_WhenNotFound()
+ {
+ // Arrange
+ var options = new DbContextOptionsBuilder()
+ .UseSqlite("DataSource=:memory:")
+ .Options;
+ using var context = new TrpgDbContext(options);
+ context.Database.OpenConnection();
+ context.Database.EnsureCreated();
+ var service = new CharacterService(context);
+
+ // Act
+ var result = await service.CreateCharacterFromTemplateIdAsync(999);
+
+ // Assert
+ Assert.Null(result);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/UnitTests/Game.Test/Tests/CheckTest.cs b/src/UnitTests/Game.Test/Tests/CheckTest.cs
new file mode 100644
index 0000000..f4e6c9c
--- /dev/null
+++ b/src/UnitTests/Game.Test/Tests/CheckTest.cs
@@ -0,0 +1,270 @@
+using Game.Service.Data;
+using Game.Service.Data.Models;
+using Game.Service.Services;
+using Microsoft.EntityFrameworkCore;
+namespace Game.Test.Tests
+{
+ public class CheckTest
+ {
+ public CheckTest()
+ {
+
+ }
+ [Fact]
+ public async Task RollDiceAsync_ReturnsValue()
+ {
+ var options = new DbContextOptionsBuilder()
+ .UseSqlite("DataSource=:memory:")
+ .Options;
+ using var context = new TrpgDbContext(options);
+ context.Database.OpenConnection();
+ context.Database.EnsureCreated();
+ var service = new CheckService(context);
+
+ var result = await service.RollDiceAsync("2d6");
+ Assert.InRange(result, 2, 12);
+ }
+
+ [Fact]
+ public async Task RollDiceAsync_Throws_OnInvalidFormat()
+ {
+ var options = new DbContextOptionsBuilder()
+ .UseSqlite("DataSource=:memory:")
+ .Options;
+ using var context = new TrpgDbContext(options);
+ context.Database.OpenConnection();
+ context.Database.EnsureCreated();
+ var service = new CheckService(context);
+
+ await Assert.ThrowsAsync(() => service.RollDiceAsync("badformat"));
+ }
+
+ [Fact]
+ public async Task SanityCheckAsync_NoAttribute_ReturnsMessage()
+ {
+ var options = new DbContextOptionsBuilder()
+ .UseSqlite("DataSource=:memory:")
+ .Options;
+ using var context = new TrpgDbContext(options);
+ context.Database.OpenConnection();
+ context.Database.EnsureCreated();
+ var service = new CheckService(context);
+
+ var msg = await service.SanityCheckAsync(1, "1d6");
+ Assert.Contains("No sanity attribute", msg);
+ }
+
+ [Fact]
+ public async Task SanityCheckAsync_WithAttribute_ReturnsResult()
+ {
+ var options = new DbContextOptionsBuilder()
+ .UseSqlite("DataSource=:memory:")
+ .Options;
+ using var context = new TrpgDbContext(options);
+ context.Database.OpenConnection();
+ context.Database.EnsureCreated();
+ var service = new CheckService(context);
+
+
+ // Arrange
+ var player = new PlayerCharacter { Id = 1, Name = "Test" };
+ context.PlayerCharacters.Add(player);
+ var attr = new Attributes { Name = "Sanity", Description = "San" };
+ context.Attributes.Add(attr);
+ await context.SaveChangesAsync();
+ var charAttr = new CharacterAttribute { CharacterId = 1, AttributeId = attr.Id, MaxValue = 100, CurrentValue = 50 };
+ context.CharacterAttributes.Add(charAttr);
+ await context.SaveChangesAsync();
+
+ var msg = await service.SanityCheckAsync(1, "1d6");
+ Assert.Contains("Roll:", msg);
+ Assert.Contains("Threshold: 50", msg);
+ }
+
+ [Fact]
+ public async Task AttributeCheckAsync_ByName_And_ById()
+ {
+ var options = new DbContextOptionsBuilder()
+ .UseSqlite("DataSource=:memory:")
+ .Options;
+ using var context = new TrpgDbContext(options);
+ context.Database.OpenConnection();
+ context.Database.EnsureCreated();
+ var service = new CheckService(context);
+
+
+ // Arrange
+ var player = new PlayerCharacter { Id = 1, Name = "Test" };
+ context.PlayerCharacters.Add(player);
+ var attr = new Attributes { Name = "HP", Description = "Health" };
+ context.Attributes.Add(attr);
+ await context.SaveChangesAsync();
+ var charAttr = new CharacterAttribute { CharacterId = 1, AttributeId = attr.Id, MaxValue = 100, CurrentValue = 60 };
+ context.CharacterAttributes.Add(charAttr);
+ await context.SaveChangesAsync();
+
+ var msgByName = await service.AttributeCheckAsync(1, "HP", "1d6");
+ Assert.Contains("Roll:", msgByName);
+ Assert.Contains("Threshold: 60", msgByName);
+
+ var msgById = await service.AttributeCheckAsync(1, attr.Id.ToString(), "1d6");
+ Assert.Contains("Roll:", msgById);
+ Assert.Contains("Threshold: 60", msgById);
+ }
+
+ [Fact]
+ public async Task AttributeCheckAsync_NotFound()
+ {
+ var options = new DbContextOptionsBuilder()
+ .UseSqlite("DataSource=:memory:")
+ .Options;
+ using var context = new TrpgDbContext(options);
+ context.Database.OpenConnection();
+ context.Database.EnsureCreated();
+ var service = new CheckService(context);
+
+ var msg = await service.AttributeCheckAsync(1, "STR", "1d6");
+ Assert.Contains("not found", msg);
+ }
+
+ [Fact]
+ public async Task SkillCheckAsync_ByName_And_ById()
+ {
+ var options = new DbContextOptionsBuilder()
+ .UseSqlite("DataSource=:memory:")
+ .Options;
+ using var context = new TrpgDbContext(options);
+ context.Database.OpenConnection();
+ context.Database.EnsureCreated();
+ var service = new CheckService(context);
+
+
+ // Arrange
+ var player = new PlayerCharacter { Id = 1, Name = "Test" };
+ context.PlayerCharacters.Add(player);
+ var skill = new Skill { Name = "Sword", Description = "Sword skill", BaseSuccessRate = 30, IsBasic = true, IsActive = true, DisplayOrder = 1, CreatedAt = DateTime.UtcNow, UpdatedAt = DateTime.UtcNow };
+ context.Skills.Add(skill);
+ await context.SaveChangesAsync();
+ var charSkill = new CharacterSkill { CharacterId = 1, SkillId = skill.Id, Proficiency = 20 };
+ context.CharacterSkills.Add(charSkill);
+ await context.SaveChangesAsync();
+
+ var msgByName = await service.SkillCheckAsync(1, "Sword", "1d6");
+ Assert.Contains("Roll:", msgByName);
+ Assert.Contains("Target: 50", msgByName);
+
+ var msgById = await service.SkillCheckAsync(1, skill.Id.ToString(), "1d6");
+ Assert.Contains("Roll:", msgById);
+ Assert.Contains("Target: 50", msgById);
+ }
+
+ [Fact]
+ public async Task SkillCheckAsync_NotFound()
+ {
+ var options = new DbContextOptionsBuilder()
+ .UseSqlite("DataSource=:memory:")
+ .Options;
+ using var context = new TrpgDbContext(options);
+ context.Database.OpenConnection();
+ context.Database.EnsureCreated();
+ var service = new CheckService(context);
+
+ var msg = await service.SkillCheckAsync(1, "Magic", "1d6");
+ Assert.Contains("not found", msg);
+ }
+
+ [Fact]
+ public async Task SavingThrowAsync_DelegatesToAttributeCheck()
+ {
+ var options = new DbContextOptionsBuilder()
+ .UseSqlite("DataSource=:memory:")
+ .Options;
+ using var context = new TrpgDbContext(options);
+ context.Database.OpenConnection();
+ context.Database.EnsureCreated();
+ var service = new CheckService(context);
+
+
+ // Arrange
+ var player = new PlayerCharacter { Id = 1, Name = "Test" };
+ context.PlayerCharacters.Add(player);
+ var attr = new Attributes { Name = "Luck", Description = "Luck" };
+ context.Attributes.Add(attr);
+ await context.SaveChangesAsync();
+ var charAttr = new CharacterAttribute { CharacterId = 1, AttributeId = attr.Id, MaxValue = 100, CurrentValue = 70 };
+ context.CharacterAttributes.Add(charAttr);
+ await context.SaveChangesAsync();
+
+ var msg = await service.SavingThrowAsync(1, "Luck", "1d6");
+ Assert.Contains("Roll:", msg);
+ Assert.Contains("Threshold: 70", msg);
+ }
+
+ [Fact]
+ public async Task CalculateDamageAsync_UsesItemStats()
+ {
+ var options = new DbContextOptionsBuilder()
+ .UseSqlite("DataSource=:memory:")
+ .Options;
+ using var context = new TrpgDbContext(options);
+ context.Database.OpenConnection();
+ context.Database.EnsureCreated();
+ var service = new CheckService(context);
+
+ var item = new Item { Name = "Sword", Description = "Sword", Stats = "2d6", Category = "Weapon", OwnerNotes = "", Weight = 1, IsConsumable = false, IsCursed = false, IsActive = true, DisplayOrder = 1, CreatedAt = DateTime.UtcNow, UpdatedAt = DateTime.UtcNow };
+ context.Items.Add(item);
+ await context.SaveChangesAsync();
+
+ var msg = await service.CalculateDamageAsync(1, "Sword", "1d4");
+ Assert.Contains("Damage:", msg);
+ Assert.Contains("dice: 2d6", msg);
+ }
+
+ [Fact]
+ public async Task CalculateDamageAsync_UsesRollExpression()
+ {
+ var options = new DbContextOptionsBuilder()
+ .UseSqlite("DataSource=:memory:")
+ .Options;
+ using var context = new TrpgDbContext(options);
+ context.Database.OpenConnection();
+ context.Database.EnsureCreated();
+ var service = new CheckService(context);
+
+ var msg = await service.CalculateDamageAsync(1, "Unknown", "1d4");
+ Assert.Contains("Damage:", msg);
+ Assert.Contains("dice: 1d4", msg);
+ }
+
+ [Fact]
+ public async Task AutoRollPlayerAttributeAsync_AssignsAttributes()
+ {
+ var options = new DbContextOptionsBuilder()
+ .UseSqlite("DataSource=:memory:")
+ .Options;
+ using var context = new TrpgDbContext(options);
+ context.Database.OpenConnection();
+ context.Database.EnsureCreated();
+ var service = new CheckService(context);
+
+ // Arrange
+ var player = new PlayerCharacter { Id = 1, Name = "Test" };
+ context.PlayerCharacters.Add(player);
+ context.Attributes.Add(new Attributes { Name = "POW", Description = "Power" });
+ context.Attributes.Add(new Attributes { Name = "SAN", Description = "Sanity" });
+ context.Attributes.Add(new Attributes { Name = "SIZ", Description = "Size" });
+ context.Attributes.Add(new Attributes { Name = "INT", Description = "Intelligence" });
+ context.Attributes.Add(new Attributes { Name = "EDU", Description = "Education" });
+ context.Attributes.Add(new Attributes { Name = "STR", Description = "Strength" });
+ await context.SaveChangesAsync();
+
+ // Act
+ var msg = await service.AutoRollPlayerAttributeAsync(1);
+
+ // Assert
+ Assert.Equal("Attributes rolled and assigned successfully.", msg);
+ var attrs = context.CharacterAttributes.Where(a => a.CharacterId == 1).ToList();
+ Assert.Equal(6, attrs.Count);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/UnitTests/Game.Test/Tests/KPTest.cs b/src/UnitTests/Game.Test/Tests/KPTest.cs
new file mode 100644
index 0000000..bebf229
--- /dev/null
+++ b/src/UnitTests/Game.Test/Tests/KPTest.cs
@@ -0,0 +1,226 @@
+using Game.Service.Data;
+using Game.Service.Data.Models;
+using Game.Service.Services;
+using Microsoft.EntityFrameworkCore;
+
+namespace Game.Test.Tests
+{
+ public class KPTest
+ {
+ [Fact]
+ public async Task GenerateSceneDescriptionAsync_ReturnsDescription_WhenSceneExists()
+ {
+ // Arrange
+ var options = new DbContextOptionsBuilder()
+ .UseSqlite("DataSource=:memory:")
+ .Options;
+ using var context = new TrpgDbContext(options);
+ context.Database.OpenConnection();
+ context.Database.EnsureCreated();
+ context.Scenarios.Add(new Scenario { Id = 1, Name = "Scenario1", Description = "S1" });
+ context.SaveChanges();
+ context.Scenes.Add(new Scene { Id = 1, Name = "TestScene", Description = "Desc", ScenarioId = 1 });
+ await context.SaveChangesAsync();
+ var service = new KPService(context);
+
+ // Act
+ var result = await service.GenerateSceneDescriptionAsync(1);
+
+ // Assert
+ Assert.Contains("Desc", result);
+ }
+
+ [Fact]
+ public async Task GenerateNpcDialogueAsync_ReturnsNotFound_WhenNoNpc()
+ {
+ // Arrange
+ var options = new DbContextOptionsBuilder()
+ .UseSqlite("DataSource=:memory:")
+ .Options;
+ using var context = new TrpgDbContext(options);
+ context.Database.OpenConnection();
+ context.Database.EnsureCreated();
+ var service = new KPService(context);
+
+ // Act
+ var result = await service.GenerateNpcDialogueAsync(999);
+
+ // Assert
+ Assert.Contains("No NPCs found", result, StringComparison.OrdinalIgnoreCase);
+ }
+
+ [Fact]
+ public async Task GenerateNpcDialogueAsync_ReturnsDialogue_WhenNpcExists()
+ {
+ // Arrange
+ var options = new DbContextOptionsBuilder()
+ .UseSqlite("DataSource=:memory:")
+ .Options;
+ using var context = new TrpgDbContext(options);
+ context.Database.OpenConnection();
+ context.Database.EnsureCreated();
+ context.Scenarios.Add(new Scenario { Id = 1, Name = "Scenario1", Description = "S1" });
+ context.SaveChanges();
+ context.Scenes.Add(new Scene { Id = 1, Name = "TestScene", Description = "Desc", ScenarioId = 1 });
+ context.SaveChanges();
+ context.NonPlayerCharacters.Add(new NonPlayerCharacter { Id = 1, Name = "NPC", Role = "Villager", LastKnownSceneId = 1, IsActive = true });
+ await context.SaveChangesAsync();
+ var service = new KPService(context);
+
+ // Act
+ var result = await service.GenerateNpcDialogueAsync(1);
+
+ // Assert
+ Assert.Contains("NPC", result);
+ }
+
+ [Fact]
+ public async Task GenerateRandomEventAsync_ReturnsEvent_WhenExists()
+ {
+ // Arrange
+ var options = new DbContextOptionsBuilder()
+ .UseSqlite("DataSource=:memory:")
+ .Options;
+ using var context = new TrpgDbContext(options);
+ context.Database.OpenConnection();
+ context.Database.EnsureCreated();
+ context.Scenarios.Add(new Scenario { Id = 1, Name = "Scenario1", Description = "S1" });
+ context.SaveChanges();
+ context.Scenes.Add(new Scene { Id = 1, Name = "TestScene", Description = "Desc", ScenarioId = 1 });
+ context.SaveChanges();
+ context.EventIntensities.Add(new EventIntensity { Id = 1, Name = "Normal", Description = "Normal intensity" });
+ context.SaveChanges();
+ context.RandomEvents.Add(new RandomEvent { Id = 1, SceneId = 1, Description = "Random event!", EventIntensityId = 1 });
+ await context.SaveChangesAsync();
+ var service = new KPService(context);
+
+ // Act
+ var result = await service.GenerateRandomEventAsync(1);
+
+ // Assert
+ Assert.Contains("No available random events", result, StringComparison.OrdinalIgnoreCase);
+ }
+
+ [Fact]
+ public async Task SuggestChecksAndDifficultiesAsync_ReturnsNotFound_WhenNoScene()
+ {
+ // Arrange
+ var options = new DbContextOptionsBuilder()
+ .UseSqlite("DataSource=:memory:")
+ .Options;
+ using var context = new TrpgDbContext(options);
+ context.Database.OpenConnection();
+ context.Database.EnsureCreated();
+ var service = new KPService(context);
+
+ // Act
+ var result = await service.SuggestChecksAndDifficultiesAsync(999);
+
+ // Assert
+ Assert.Contains("No roll suggestions", result, StringComparison.OrdinalIgnoreCase);
+ }
+
+ [Fact]
+ public async Task SuggestChecksAndDifficultiesAsync_ReturnsSuggestion_WhenSceneExists()
+ {
+ // Arrange
+ var options = new DbContextOptionsBuilder()
+ .UseSqlite("DataSource=:memory:")
+ .Options;
+ using var context = new TrpgDbContext(options);
+ context.Database.OpenConnection();
+ context.Database.EnsureCreated();
+ context.Scenarios.Add(new Scenario { Id = 1, Name = "Scenario1", Description = "S1" });
+ context.SaveChanges();
+ context.Scenes.Add(new Scene { Id = 2, Name = "Scene2", Description = "Desc2", ScenarioId = 1 });
+ await context.SaveChangesAsync();
+ var service = new KPService(context);
+
+ // Act
+ var result = await service.SuggestChecksAndDifficultiesAsync(2);
+
+ // Assert
+ Assert.Contains("No roll suggestions", result, StringComparison.OrdinalIgnoreCase);
+ }
+
+ [Fact]
+ public async Task GetGameProgressSuggestionsAsync_ReturnsSuggestion_WhenRecordsExist()
+ {
+ // Arrange
+ var options = new DbContextOptionsBuilder()
+ .UseSqlite("DataSource=:memory:")
+ .Options;
+ using var context = new TrpgDbContext(options);
+ context.Database.OpenConnection();
+ context.Database.EnsureCreated();
+ context.Scenarios.Add(new Scenario { Id = 1, Name = "Scenario1", Description = "S1" });
+ context.SaveChanges();
+ context.GameRecords.Add(new GameRecords { Id = 1, ScenarioId = 1, Description = "Progress!", CreatedAt = DateTime.UtcNow });
+ await context.SaveChangesAsync();
+ var service = new KPService(context);
+
+ // Act
+ var result = await service.GetGameProgressSuggestionsAsync(1);
+
+ // Assert
+ Assert.Contains("Progress", result);
+ }
+
+ [Fact]
+ public async Task GenerateSceneDescriptionAsync_ReturnsNotFound_WhenNoScene()
+ {
+ // Arrange
+ var options = new DbContextOptionsBuilder()
+ .UseSqlite("DataSource=:memory:")
+ .Options;
+ using var context = new TrpgDbContext(options);
+ context.Database.OpenConnection();
+ context.Database.EnsureCreated();
+ var service = new KPService(context);
+
+ // Act
+ var result = await service.GenerateSceneDescriptionAsync(999);
+
+ // Assert
+ Assert.Contains("not found", result, StringComparison.OrdinalIgnoreCase);
+ }
+
+ [Fact]
+ public async Task GenerateRandomEventAsync_ReturnsNotFound_WhenNoScene()
+ {
+ // Arrange
+ var options = new DbContextOptionsBuilder()
+ .UseSqlite("DataSource=:memory:")
+ .Options;
+ using var context = new TrpgDbContext(options);
+ context.Database.OpenConnection();
+ context.Database.EnsureCreated();
+ var service = new KPService(context);
+
+ // Act
+ var result = await service.GenerateRandomEventAsync(999);
+
+ // Assert
+ Assert.Contains("not found", result, StringComparison.OrdinalIgnoreCase);
+ }
+
+ [Fact]
+ public async Task GetGameProgressSuggestionsAsync_ReturnsNoRecords()
+ {
+ // Arrange
+ var options = new DbContextOptionsBuilder()
+ .UseSqlite("DataSource=:memory:")
+ .Options;
+ using var context = new TrpgDbContext(options);
+ context.Database.OpenConnection();
+ context.Database.EnsureCreated();
+ var service = new KPService(context);
+
+ // Act
+ var result = await service.GetGameProgressSuggestionsAsync(999);
+
+ // Assert
+ Assert.Contains("no recent game records", result, StringComparison.OrdinalIgnoreCase);
+ }
+ }
+}
diff --git a/src/UnitTests/Game.Test/Tests/SenarioTest.cs b/src/UnitTests/Game.Test/Tests/SenarioTest.cs
new file mode 100644
index 0000000..95f97f7
--- /dev/null
+++ b/src/UnitTests/Game.Test/Tests/SenarioTest.cs
@@ -0,0 +1,74 @@
+using Game.Service.Data;
+using Game.Service.Data.Models;
+using Game.Service.Services;
+using Microsoft.EntityFrameworkCore;
+
+namespace Game.Test.Tests
+{
+ public class SenarioTest
+ {
+ [Fact]
+ public async Task GetScenarioByIdAsync_ReturnsScenario_WhenExists()
+ {
+ // Arrange
+ var options = new DbContextOptionsBuilder()
+ .UseSqlite("DataSource=:memory:")
+ .Options;
+ using var context = new TrpgDbContext(options);
+ context.Database.OpenConnection();
+ context.Database.EnsureCreated();
+ context.Scenarios.Add(new Scenario { Id = 2, Name = "S2", Description = "D2", CreatedAt = DateTime.UtcNow, UpdatedAt = DateTime.UtcNow });
+ await context.SaveChangesAsync();
+ var service = new ScenarioService(context);
+
+ // Act
+ var result = await service.GetScenarioByIdAsync(2);
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.Equal("S2", result.Name);
+ }
+
+ [Fact]
+ public async Task GetAllScenariosAsync_ReturnsScenarios()
+ {
+ // Arrange
+ var options = new DbContextOptionsBuilder()
+ .UseSqlite("DataSource=:memory:")
+ .Options;
+ using var context = new TrpgDbContext(options);
+ context.Database.OpenConnection();
+ context.Database.EnsureCreated();
+ context.Scenarios.Add(new Scenario { Name = "S1", Description = "D1", CreatedAt = DateTime.UtcNow, UpdatedAt = DateTime.UtcNow });
+ await context.SaveChangesAsync();
+ var service = new ScenarioService(context);
+
+ // Act
+ var result = await service.GetAllScenariosAsync();
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.Single(result);
+ Assert.Equal("S1", result[0]?.Name);
+ }
+
+ [Fact]
+ public async Task GetScenarioByIdAsync_ReturnsNull_WhenNotFound()
+ {
+ // Arrange
+ var options = new DbContextOptionsBuilder()
+ .UseSqlite("DataSource=:memory:")
+ .Options;
+ using var context = new TrpgDbContext(options);
+ context.Database.OpenConnection();
+ context.Database.EnsureCreated();
+ var service = new ScenarioService(context);
+
+ // Act
+ var result = await service.GetScenarioByIdAsync(999);
+
+ // Assert
+ Assert.Null(result);
+ }
+ }
+}
diff --git a/trpg-mcp.sln b/trpg-mcp.sln
index 9690d54..7a535b7 100644
--- a/trpg-mcp.sln
+++ b/trpg-mcp.sln
@@ -13,6 +13,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Game.Service", "src\Modules
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Common", "src\Common\Common.csproj", "{D4CDE262-F9A1-44C9-9681-E6AD04AE139C}"
EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "UnitTests", "UnitTests", "{424A6336-F132-E78C-9D5B-EA9A8E793A9C}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Game.Test", "src\UnitTests\Game.Test\Game.Test.csproj", "{8B5E5565-561D-4371-84FC-51FA037599A2}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -59,6 +63,18 @@ Global
{D4CDE262-F9A1-44C9-9681-E6AD04AE139C}.Release|x64.Build.0 = Release|Any CPU
{D4CDE262-F9A1-44C9-9681-E6AD04AE139C}.Release|x86.ActiveCfg = Release|Any CPU
{D4CDE262-F9A1-44C9-9681-E6AD04AE139C}.Release|x86.Build.0 = Release|Any CPU
+ {8B5E5565-561D-4371-84FC-51FA037599A2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {8B5E5565-561D-4371-84FC-51FA037599A2}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {8B5E5565-561D-4371-84FC-51FA037599A2}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {8B5E5565-561D-4371-84FC-51FA037599A2}.Debug|x64.Build.0 = Debug|Any CPU
+ {8B5E5565-561D-4371-84FC-51FA037599A2}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {8B5E5565-561D-4371-84FC-51FA037599A2}.Debug|x86.Build.0 = Debug|Any CPU
+ {8B5E5565-561D-4371-84FC-51FA037599A2}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {8B5E5565-561D-4371-84FC-51FA037599A2}.Release|Any CPU.Build.0 = Release|Any CPU
+ {8B5E5565-561D-4371-84FC-51FA037599A2}.Release|x64.ActiveCfg = Release|Any CPU
+ {8B5E5565-561D-4371-84FC-51FA037599A2}.Release|x64.Build.0 = Release|Any CPU
+ {8B5E5565-561D-4371-84FC-51FA037599A2}.Release|x86.ActiveCfg = Release|Any CPU
+ {8B5E5565-561D-4371-84FC-51FA037599A2}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -68,5 +84,7 @@ Global
{EC447DCF-ABFA-6E24-52A5-D7FD48A5C558} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
{50E1E429-B4CD-4014-AA53-943F60188088} = {EC447DCF-ABFA-6E24-52A5-D7FD48A5C558}
{D4CDE262-F9A1-44C9-9681-E6AD04AE139C} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
+ {424A6336-F132-E78C-9D5B-EA9A8E793A9C} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
+ {8B5E5565-561D-4371-84FC-51FA037599A2} = {424A6336-F132-E78C-9D5B-EA9A8E793A9C}
EndGlobalSection
EndGlobal