Refactor API/service boundaries and modularize frontend

This commit is contained in:
2026-02-24 23:33:12 +01:00
parent 1d512d321b
commit c6e95f16e1
39 changed files with 1628 additions and 1315 deletions

View File

@@ -1,6 +1,5 @@
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using RpgRoller.Contracts;
using RpgRoller.Data;
using RpgRoller.Domain;
using RpgRoller.Services;
@@ -15,11 +14,11 @@ public sealed class GameServiceTests
using var harness = CreateHarness();
var service = harness.Service;
var invalidUsername = service.Register(new RegisterRequest("", "Password123", "Display"));
var invalidDisplay = service.Register(new RegisterRequest("user", "Password123", ""));
var invalidPassword = service.Register(new RegisterRequest("user", "short", "Display"));
var valid = service.Register(new RegisterRequest("user", "Password123", "Display"));
var duplicate = service.Register(new RegisterRequest("user", "Password123", "Display 2"));
var invalidUsername = service.Register(new RegisterCommand("", "Password123", "Display"));
var invalidDisplay = service.Register(new RegisterCommand("user", "Password123", ""));
var invalidPassword = service.Register(new RegisterCommand("user", "short", "Display"));
var valid = service.Register(new RegisterCommand("user", "Password123", "Display"));
var duplicate = service.Register(new RegisterCommand("user", "Password123", "Display 2"));
Assert.False(invalidUsername.Succeeded);
Assert.False(invalidDisplay.Succeeded);
@@ -33,11 +32,11 @@ public sealed class GameServiceTests
{
using var harness = CreateHarness();
var service = harness.Service;
service.Register(new RegisterRequest("user", "Password123", "Display"));
service.Register(new RegisterCommand("user", "Password123", "Display"));
var invalidUser = service.Login(new LoginRequest("missing", "Password123"));
var invalidPassword = service.Login(new LoginRequest("user", "bad-password"));
var valid = service.Login(new LoginRequest("user", "Password123"));
var invalidUser = service.Login(new LoginCommand("missing", "Password123"));
var invalidPassword = service.Login(new LoginCommand("user", "bad-password"));
var valid = service.Login(new LoginCommand("user", "Password123"));
Assert.False(invalidUser.Succeeded);
Assert.False(invalidPassword.Succeeded);
@@ -57,8 +56,8 @@ public sealed class GameServiceTests
using var harness = CreateHarness(hasher);
var service = harness.Service;
service.Register(new RegisterRequest("user", "Password123", "Display"));
var login = service.Login(new LoginRequest("user", "Password123"));
service.Register(new RegisterCommand("user", "Password123", "Display"));
var login = service.Login(new LoginCommand("user", "Password123"));
Assert.True(login.Succeeded);
Assert.Equal(2, hasher.HashCalls);
@@ -70,20 +69,20 @@ public sealed class GameServiceTests
using var harness = CreateHarness();
var service = harness.Service;
var unauthorizedCampaign = service.CreateCampaign("missing", new CreateCampaignRequest("Name", "d6"));
var unauthorizedCampaign = service.CreateCampaign("missing", new CreateCampaignCommand("Name", "d6"));
Assert.False(unauthorizedCampaign.Succeeded);
service.Register(new RegisterRequest("gm", "Password123", "GM"));
var gmSession = GetValue(service.Login(new LoginRequest("gm", "Password123"))).SessionToken;
var campaign = GetValue(service.CreateCampaign(gmSession, new CreateCampaignRequest("Name", "d6")));
service.Register(new RegisterCommand("gm", "Password123", "GM"));
var gmSession = GetValue(service.Login(new LoginCommand("gm", "Password123"))).SessionToken;
var campaign = GetValue(service.CreateCampaign(gmSession, new CreateCampaignCommand("Name", "d6")));
var invalidRuleset = service.CreateCampaign(gmSession, new CreateCampaignRequest("Name 2", "unknown"));
var invalidRuleset = service.CreateCampaign(gmSession, new CreateCampaignCommand("Name 2", "unknown"));
Assert.False(invalidRuleset.Succeeded);
var noCampaignCharacter = service.CreateCharacter(gmSession, new CreateCharacterRequest("Hero", Guid.NewGuid()));
var noCampaignCharacter = service.CreateCharacter(gmSession, new CreateCharacterCommand("Hero", Guid.NewGuid()));
Assert.False(noCampaignCharacter.Succeeded);
var character = GetValue(service.CreateCharacter(gmSession, new CreateCharacterRequest("Hero", campaign.Id)));
var character = GetValue(service.CreateCharacter(gmSession, new CreateCharacterCommand("Hero", campaign.Id)));
var missingCharacterActivate = service.ActivateCharacter(gmSession, Guid.NewGuid());
Assert.False(missingCharacterActivate.Succeeded);
@@ -104,54 +103,54 @@ public sealed class GameServiceTests
{
using var harness = CreateHarness(3, 4, 5, 6);
var service = harness.Service;
service.Register(new RegisterRequest("gm", "Password123", "GM"));
service.Register(new RegisterRequest("owner", "Password123", "Owner"));
service.Register(new RegisterRequest("other", "Password123", "Other"));
service.Register(new RegisterCommand("gm", "Password123", "GM"));
service.Register(new RegisterCommand("owner", "Password123", "Owner"));
service.Register(new RegisterCommand("other", "Password123", "Other"));
var gmSession = GetValue(service.Login(new LoginRequest("gm", "Password123"))).SessionToken;
var ownerSession = GetValue(service.Login(new LoginRequest("owner", "Password123"))).SessionToken;
var otherSession = GetValue(service.Login(new LoginRequest("other", "Password123"))).SessionToken;
var gmSession = GetValue(service.Login(new LoginCommand("gm", "Password123"))).SessionToken;
var ownerSession = GetValue(service.Login(new LoginCommand("owner", "Password123"))).SessionToken;
var otherSession = GetValue(service.Login(new LoginCommand("other", "Password123"))).SessionToken;
var campaign = GetValue(service.CreateCampaign(gmSession, new CreateCampaignRequest("Main", "dnd5e")));
var character = GetValue(service.CreateCharacter(ownerSession, new CreateCharacterRequest("Owner Char", campaign.Id)));
var campaign = GetValue(service.CreateCampaign(gmSession, new CreateCampaignCommand("Main", "dnd5e")));
var character = GetValue(service.CreateCharacter(ownerSession, new CreateCharacterCommand("Owner Char", campaign.Id)));
var noPermissionUpdate = service.UpdateCharacter(otherSession, character.Id, new UpdateCharacterRequest("Renamed", campaign.Id));
var noPermissionUpdate = service.UpdateCharacter(otherSession, character.Id, new UpdateCharacterCommand("Renamed", campaign.Id));
Assert.False(noPermissionUpdate.Succeeded);
var invalidCharacterName = service.UpdateCharacter(ownerSession, character.Id, new UpdateCharacterRequest("", campaign.Id));
var invalidCharacterName = service.UpdateCharacter(ownerSession, character.Id, new UpdateCharacterCommand("", campaign.Id));
Assert.False(invalidCharacterName.Succeeded);
var missingTargetCampaign = service.UpdateCharacter(ownerSession, character.Id, new UpdateCharacterRequest("Renamed", Guid.NewGuid()));
var missingTargetCampaign = service.UpdateCharacter(ownerSession, character.Id, new UpdateCharacterCommand("Renamed", Guid.NewGuid()));
Assert.False(missingTargetCampaign.Succeeded);
var noSkillName = service.CreateSkill(ownerSession, character.Id, new CreateSkillRequest("", "1d20"));
var noSkillName = service.CreateSkill(ownerSession, character.Id, new CreateSkillCommand("", "1d20"));
Assert.False(noSkillName.Succeeded);
var invalidExpression = service.CreateSkill(ownerSession, character.Id, new CreateSkillRequest("Skill", "5D+4"));
var invalidExpression = service.CreateSkill(ownerSession, character.Id, new CreateSkillCommand("Skill", "5D+4"));
Assert.False(invalidExpression.Succeeded);
var skill = GetValue(service.CreateSkill(ownerSession, character.Id, new CreateSkillRequest("Skill", "1d20+2")));
var skill = GetValue(service.CreateSkill(ownerSession, character.Id, new CreateSkillCommand("Skill", "1d20+2")));
var missingSkillUpdate = service.UpdateSkill(ownerSession, Guid.NewGuid(), new UpdateSkillRequest("X", "1d20"));
var missingSkillUpdate = service.UpdateSkill(ownerSession, Guid.NewGuid(), new UpdateSkillCommand("X", "1d20"));
Assert.False(missingSkillUpdate.Succeeded);
var forbiddenSkillUpdate = service.UpdateSkill(otherSession, skill.Id, new UpdateSkillRequest("X", "1d20"));
var forbiddenSkillUpdate = service.UpdateSkill(otherSession, skill.Id, new UpdateSkillCommand("X", "1d20"));
Assert.False(forbiddenSkillUpdate.Succeeded);
var gmSkillUpdate = service.UpdateSkill(gmSession, skill.Id, new UpdateSkillRequest("GM Edit", "2d6+1"));
var gmSkillUpdate = service.UpdateSkill(gmSession, skill.Id, new UpdateSkillCommand("GM Edit", "2d6+1"));
Assert.True(gmSkillUpdate.Succeeded);
var missingRoll = service.RollSkill(ownerSession, Guid.NewGuid(), new RollSkillRequest("public"));
var missingRoll = service.RollSkill(ownerSession, Guid.NewGuid(), new RollSkillCommand("public"));
Assert.False(missingRoll.Succeeded);
var invalidVisibility = service.RollSkill(ownerSession, skill.Id, new RollSkillRequest("hidden"));
var invalidVisibility = service.RollSkill(ownerSession, skill.Id, new RollSkillCommand("hidden"));
Assert.False(invalidVisibility.Succeeded);
var forbiddenRoll = service.RollSkill(otherSession, skill.Id, new RollSkillRequest("public"));
var forbiddenRoll = service.RollSkill(otherSession, skill.Id, new RollSkillCommand("public"));
Assert.False(forbiddenRoll.Succeeded);
var privateRoll = service.RollSkill(ownerSession, skill.Id, new RollSkillRequest("private"));
var publicRoll = service.RollSkill(ownerSession, skill.Id, new RollSkillRequest("public"));
var privateRoll = service.RollSkill(ownerSession, skill.Id, new RollSkillCommand("private"));
var publicRoll = service.RollSkill(ownerSession, skill.Id, new RollSkillCommand("public"));
Assert.True(privateRoll.Succeeded);
Assert.True(publicRoll.Succeeded);
@@ -174,8 +173,8 @@ public sealed class GameServiceTests
{
using var harness = CreateHarness();
var service = harness.Service;
service.Register(new RegisterRequest("user", "Password123", "User"));
var sessionToken = GetValue(service.Login(new LoginRequest("user", "Password123"))).SessionToken;
service.Register(new RegisterCommand("user", "Password123", "User"));
var sessionToken = GetValue(service.Login(new LoginCommand("user", "Password123"))).SessionToken;
var result = service.GetCurrentCampaignCharacters(sessionToken);
Assert.False(result.Succeeded);
@@ -186,14 +185,14 @@ public sealed class GameServiceTests
{
using var harness = CreateHarness();
var service = harness.Service;
service.Register(new RegisterRequest("gm", "Password123", "GM"));
service.Register(new RegisterRequest("player", "Password123", "Player"));
var gmSession = GetValue(service.Login(new LoginRequest("gm", "Password123"))).SessionToken;
var playerSession = GetValue(service.Login(new LoginRequest("player", "Password123"))).SessionToken;
service.Register(new RegisterCommand("gm", "Password123", "GM"));
service.Register(new RegisterCommand("player", "Password123", "Player"));
var gmSession = GetValue(service.Login(new LoginCommand("gm", "Password123"))).SessionToken;
var playerSession = GetValue(service.Login(new LoginCommand("player", "Password123"))).SessionToken;
var gmCampaign = GetValue(service.CreateCampaign(gmSession, new CreateCampaignRequest("Owned", "d6")));
_ = GetValue(service.CreateCampaign(gmSession, new CreateCampaignRequest("Owned 2", "d6")));
_ = service.CreateCharacter(playerSession, new CreateCharacterRequest("Joiner", gmCampaign.Id));
var gmCampaign = GetValue(service.CreateCampaign(gmSession, new CreateCampaignCommand("Owned", "d6")));
_ = GetValue(service.CreateCampaign(gmSession, new CreateCampaignCommand("Owned 2", "d6")));
_ = service.CreateCharacter(playerSession, new CreateCharacterCommand("Joiner", gmCampaign.Id));
var playerCampaigns = service.GetCampaigns(playerSession);
Assert.True(playerCampaigns.Succeeded);
@@ -208,35 +207,35 @@ public sealed class GameServiceTests
using var harness = CreateHarness(2, 3, 4);
var service = harness.Service;
var invalidCredentials = service.Login(new LoginRequest("", ""));
var invalidCredentials = service.Login(new LoginCommand("", ""));
Assert.False(invalidCredentials.Succeeded);
service.Logout("missing-session");
service.Register(new RegisterRequest("gm", "Password123", "GM"));
service.Register(new RegisterRequest("owner", "Password123", "Owner"));
service.Register(new RegisterRequest("other", "Password123", "Other"));
service.Register(new RegisterCommand("gm", "Password123", "GM"));
service.Register(new RegisterCommand("owner", "Password123", "Owner"));
service.Register(new RegisterCommand("other", "Password123", "Other"));
var gmSession = GetValue(service.Login(new LoginRequest("gm", "Password123"))).SessionToken;
var ownerSession = GetValue(service.Login(new LoginRequest("owner", "Password123"))).SessionToken;
var otherSession = GetValue(service.Login(new LoginRequest("other", "Password123"))).SessionToken;
var gmSession = GetValue(service.Login(new LoginCommand("gm", "Password123"))).SessionToken;
var ownerSession = GetValue(service.Login(new LoginCommand("owner", "Password123"))).SessionToken;
var otherSession = GetValue(service.Login(new LoginCommand("other", "Password123"))).SessionToken;
var campaign = GetValue(service.CreateCampaign(gmSession, new CreateCampaignRequest("Main", "d6")));
var ownerCharacter = GetValue(service.CreateCharacter(ownerSession, new CreateCharacterRequest("Owner Character", campaign.Id)));
var campaign = GetValue(service.CreateCampaign(gmSession, new CreateCampaignCommand("Main", "d6")));
var ownerCharacter = GetValue(service.CreateCharacter(ownerSession, new CreateCharacterCommand("Owner Character", campaign.Id)));
Assert.False(service.GetMe(string.Empty).Succeeded);
Assert.False(service.CreateCampaign(gmSession, new CreateCampaignRequest("", "d6")).Succeeded);
Assert.False(service.CreateCampaign(gmSession, new CreateCampaignCommand("", "d6")).Succeeded);
Assert.False(service.GetCampaigns(string.Empty).Succeeded);
Assert.False(service.CreateCharacter(ownerSession, new CreateCharacterRequest("", campaign.Id)).Succeeded);
Assert.False(service.CreateCharacter(string.Empty, new CreateCharacterRequest("Name", campaign.Id)).Succeeded);
Assert.False(service.UpdateCharacter(string.Empty, ownerCharacter.Id, new UpdateCharacterRequest("Renamed", campaign.Id)).Succeeded);
Assert.False(service.UpdateCharacter(ownerSession, Guid.NewGuid(), new UpdateCharacterRequest("Renamed", campaign.Id)).Succeeded);
Assert.False(service.CreateCharacter(ownerSession, new CreateCharacterCommand("", campaign.Id)).Succeeded);
Assert.False(service.CreateCharacter(string.Empty, new CreateCharacterCommand("Name", campaign.Id)).Succeeded);
Assert.False(service.UpdateCharacter(string.Empty, ownerCharacter.Id, new UpdateCharacterCommand("Renamed", campaign.Id)).Succeeded);
Assert.False(service.UpdateCharacter(ownerSession, Guid.NewGuid(), new UpdateCharacterCommand("Renamed", campaign.Id)).Succeeded);
Assert.False(service.ActivateCharacter(string.Empty, ownerCharacter.Id).Succeeded);
Assert.False(service.ActivateCharacter(gmSession, ownerCharacter.Id).Succeeded);
Assert.False(service.GetCurrentCampaignCharacters(string.Empty).Succeeded);
Assert.False(service.CreateSkill(string.Empty, ownerCharacter.Id, new CreateSkillRequest("Stealth", "2D+1")).Succeeded);
Assert.False(service.CreateSkill(ownerSession, Guid.NewGuid(), new CreateSkillRequest("Stealth", "2D+1")).Succeeded);
Assert.False(service.CreateSkill(otherSession, ownerCharacter.Id, new CreateSkillRequest("Stealth", "2D+1")).Succeeded);
Assert.False(service.CreateSkill(string.Empty, ownerCharacter.Id, new CreateSkillCommand("Stealth", "2D+1")).Succeeded);
Assert.False(service.CreateSkill(ownerSession, Guid.NewGuid(), new CreateSkillCommand("Stealth", "2D+1")).Succeeded);
Assert.False(service.CreateSkill(otherSession, ownerCharacter.Id, new CreateSkillCommand("Stealth", "2D+1")).Succeeded);
using (var db = harness.CreateDbContext())
{
@@ -274,11 +273,11 @@ public sealed class GameServiceTests
Assert.Null(db.Users.Single(u => u.UsernameNormalized == "OWNER").ActiveCharacterId);
}
var skill = GetValue(service.CreateSkill(ownerSession, ownerCharacter.Id, new CreateSkillRequest("Stealth", "2D+1")));
Assert.False(service.UpdateSkill(ownerSession, skill.Id, new UpdateSkillRequest("", "2D+1")).Succeeded);
Assert.False(service.UpdateSkill(string.Empty, skill.Id, new UpdateSkillRequest("Stealth", "2D+1")).Succeeded);
Assert.False(service.UpdateSkill(ownerSession, skill.Id, new UpdateSkillRequest("Stealth", "bad")).Succeeded);
Assert.False(service.RollSkill(string.Empty, skill.Id, new RollSkillRequest("public")).Succeeded);
var skill = GetValue(service.CreateSkill(ownerSession, ownerCharacter.Id, new CreateSkillCommand("Stealth", "2D+1")));
Assert.False(service.UpdateSkill(ownerSession, skill.Id, new UpdateSkillCommand("", "2D+1")).Succeeded);
Assert.False(service.UpdateSkill(string.Empty, skill.Id, new UpdateSkillCommand("Stealth", "2D+1")).Succeeded);
Assert.False(service.UpdateSkill(ownerSession, skill.Id, new UpdateSkillCommand("Stealth", "bad")).Succeeded);
Assert.False(service.RollSkill(string.Empty, skill.Id, new RollSkillCommand("public")).Succeeded);
using (var db = harness.CreateDbContext())
{
@@ -288,7 +287,7 @@ public sealed class GameServiceTests
}
using var invalidExpressionHarness = CreateHarnessFromPath(harness.DbPath, 2, 3, 4);
Assert.False(invalidExpressionHarness.Service.RollSkill(ownerSession, skill.Id, new RollSkillRequest("public")).Succeeded);
Assert.False(invalidExpressionHarness.Service.RollSkill(ownerSession, skill.Id, new RollSkillCommand("public")).Succeeded);
Assert.False(service.GetCampaignLog(string.Empty, campaign.Id).Succeeded);
}
@@ -298,20 +297,20 @@ public sealed class GameServiceTests
using var harness = CreateHarness();
var service = harness.Service;
service.Register(new RegisterRequest("gm", "Password123", "GM"));
service.Register(new RegisterRequest("owner", "Password123", "Owner"));
service.Register(new RegisterRequest("other", "Password123", "Other"));
service.Register(new RegisterCommand("gm", "Password123", "GM"));
service.Register(new RegisterCommand("owner", "Password123", "Owner"));
service.Register(new RegisterCommand("other", "Password123", "Other"));
var gmSession = GetValue(service.Login(new LoginRequest("gm", "Password123"))).SessionToken;
var ownerSession = GetValue(service.Login(new LoginRequest("owner", "Password123"))).SessionToken;
var otherSession = GetValue(service.Login(new LoginRequest("other", "Password123"))).SessionToken;
var gmSession = GetValue(service.Login(new LoginCommand("gm", "Password123"))).SessionToken;
var ownerSession = GetValue(service.Login(new LoginCommand("owner", "Password123"))).SessionToken;
var otherSession = GetValue(service.Login(new LoginCommand("other", "Password123"))).SessionToken;
var campaign = GetValue(service.CreateCampaign(gmSession, new CreateCampaignRequest("Main", "d6")));
var ownerCharacter = GetValue(service.CreateCharacter(ownerSession, new CreateCharacterRequest("Owner Character", campaign.Id)));
var otherCharacter = GetValue(service.CreateCharacter(otherSession, new CreateCharacterRequest("Other Character", campaign.Id)));
var campaign = GetValue(service.CreateCampaign(gmSession, new CreateCampaignCommand("Main", "d6")));
var ownerCharacter = GetValue(service.CreateCharacter(ownerSession, new CreateCharacterCommand("Owner Character", campaign.Id)));
var otherCharacter = GetValue(service.CreateCharacter(otherSession, new CreateCharacterCommand("Other Character", campaign.Id)));
var ownerSkill = GetValue(service.CreateSkill(ownerSession, ownerCharacter.Id, new CreateSkillRequest("Stealth", "2D+1")));
_ = GetValue(service.CreateSkill(otherSession, otherCharacter.Id, new CreateSkillRequest("Perception", "1D+2")));
var ownerSkill = GetValue(service.CreateSkill(ownerSession, ownerCharacter.Id, new CreateSkillCommand("Stealth", "2D+1")));
_ = GetValue(service.CreateSkill(otherSession, otherCharacter.Id, new CreateSkillCommand("Perception", "1D+2")));
var ownerView = GetValue(service.GetCampaign(ownerSession, campaign.Id));
Assert.Single(ownerView.Characters);
@@ -329,17 +328,21 @@ public sealed class GameServiceTests
var d6 = DiceRules.ParseExpression(RulesetKind.D6, "5D+4");
var dnd = DiceRules.ParseExpression(RulesetKind.Dnd5e, "2d12+2");
var emptyExpression = DiceRules.ParseExpression(RulesetKind.Dnd5e, "");
var badFormat = DiceRules.ParseExpression(RulesetKind.Dnd5e, "abc");
var tooManyDice = DiceRules.ParseExpression(RulesetKind.D6, "51D+1");
var tooManySides = DiceRules.ParseExpression(RulesetKind.Dnd5e, "1d1001");
var tooLargeModifier = DiceRules.ParseExpression(RulesetKind.Dnd5e, "1d20+1001");
var unknownRulesetExpression = DiceRules.ParseExpression((RulesetKind)99, "1d20+1");
Assert.True(d6.Succeeded);
Assert.True(dnd.Succeeded);
Assert.False(emptyExpression.Succeeded);
Assert.False(badFormat.Succeeded);
Assert.False(tooManyDice.Succeeded);
Assert.False(tooManySides.Succeeded);
Assert.False(tooLargeModifier.Succeeded);
Assert.False(unknownRulesetExpression.Succeeded);
Assert.Equal("d6", DiceRules.ToRulesetId(RulesetKind.D6));
Assert.Equal("dnd5e", DiceRules.ToRulesetId(RulesetKind.Dnd5e));
@@ -476,3 +479,4 @@ public sealed class GameServiceTests
}
}
}