diff --git a/FAQ.md b/FAQ.md index f8e480d..7507391 100644 --- a/FAQ.md +++ b/FAQ.md @@ -32,6 +32,6 @@ No. The backend loads state from SQLite once during startup into in-memory state Coverage now includes the entire backend project (`RpgRoller`), including API/hosting/bootstrap code and services. It is no longer restricted to `RpgRoller.Services.*`. -## Why do backend services use `*Command` types instead of API request DTOs? +## Why do backend services avoid API request DTO dependencies? -Service workflows now consume service-layer command models (for example, `CreateCampaignCommand`) so endpoint transport contracts stay isolated in the API layer. This reduces coupling and keeps service code reusable when input shapes evolve at the HTTP boundary. +Service workflows accept explicit parameters (for example, `CreateCampaign(sessionToken, name, rulesetId)`) instead of API request DTOs. This keeps the service layer independent from HTTP transport contracts while avoiding extra service-only wrapper command types. diff --git a/README.md b/README.md index a64a403..cbd4895 100644 --- a/README.md +++ b/README.md @@ -19,8 +19,8 @@ Backend: - `RpgRoller/Program.cs`: thin app bootstrap only - `RpgRoller/Hosting/`: service registration + startup initialization -- `RpgRoller/Api/`: endpoint mapping modules, API-to-service request mapping, auth/session filter helpers -- `RpgRoller/Services/`: game workflows and service-layer command models (`*Command` records) +- `RpgRoller/Api/`: endpoint mapping modules and auth/session filter helpers +- `RpgRoller/Services/`: game workflows with explicit method parameters (no API DTO dependencies) Frontend: diff --git a/RpgRoller.Tests/Services/ServiceAuthTests.cs b/RpgRoller.Tests/Services/ServiceAuthTests.cs index 39a9b7e..04285f2 100644 --- a/RpgRoller.Tests/Services/ServiceAuthTests.cs +++ b/RpgRoller.Tests/Services/ServiceAuthTests.cs @@ -8,11 +8,11 @@ public sealed class ServiceAuthTests using var harness = ServiceTestSupport.CreateHarness(); var service = harness.Service; - 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")); + var invalidUsername = service.Register("", "Password123", "Display"); + var invalidDisplay = service.Register("user", "Password123", ""); + var invalidPassword = service.Register("user", "short", "Display"); + var valid = service.Register("user", "Password123", "Display"); + var duplicate = service.Register("user", "Password123", "Display 2"); Assert.False(invalidUsername.Succeeded); Assert.False(invalidDisplay.Succeeded); @@ -26,11 +26,11 @@ public sealed class ServiceAuthTests { using var harness = ServiceTestSupport.CreateHarness(); var service = harness.Service; - service.Register(new RegisterCommand("user", "Password123", "Display")); + service.Register("user", "Password123", "Display"); - 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")); + var invalidUser = service.Login("missing", "Password123"); + var invalidPassword = service.Login("user", "bad-password"); + var valid = service.Login("user", "Password123"); Assert.False(invalidUser.Succeeded); Assert.False(invalidPassword.Succeeded); @@ -50,8 +50,8 @@ public sealed class ServiceAuthTests using var harness = ServiceTestSupport.CreateHarness(hasher); var service = harness.Service; - service.Register(new RegisterCommand("user", "Password123", "Display")); - var login = service.Login(new LoginCommand("user", "Password123")); + service.Register("user", "Password123", "Display"); + var login = service.Login("user", "Password123"); Assert.True(login.Succeeded); Assert.Equal(2, hasher.HashCalls); diff --git a/RpgRoller.Tests/Services/ServiceCampaignTests.cs b/RpgRoller.Tests/Services/ServiceCampaignTests.cs index 20096de..c3bca70 100644 --- a/RpgRoller.Tests/Services/ServiceCampaignTests.cs +++ b/RpgRoller.Tests/Services/ServiceCampaignTests.cs @@ -8,20 +8,20 @@ public sealed class ServiceCampaignTests using var harness = ServiceTestSupport.CreateHarness(); var service = harness.Service; - var unauthorizedCampaign = service.CreateCampaign("missing", new CreateCampaignCommand("Name", "d6")); + var unauthorizedCampaign = service.CreateCampaign("missing", "Name", "d6"); Assert.False(unauthorizedCampaign.Succeeded); - service.Register(new RegisterCommand("gm", "Password123", "GM")); - var gmSession = ServiceTestSupport.GetValue(service.Login(new LoginCommand("gm", "Password123"))).SessionToken; - var campaign = ServiceTestSupport.GetValue(service.CreateCampaign(gmSession, new CreateCampaignCommand("Name", "d6"))); + service.Register("gm", "Password123", "GM"); + var gmSession = ServiceTestSupport.GetValue(service.Login("gm", "Password123")).SessionToken; + var campaign = ServiceTestSupport.GetValue(service.CreateCampaign(gmSession, "Name", "d6")); - var invalidRuleset = service.CreateCampaign(gmSession, new CreateCampaignCommand("Name 2", "unknown")); + var invalidRuleset = service.CreateCampaign(gmSession, "Name 2", "unknown"); Assert.False(invalidRuleset.Succeeded); - var noCampaignCharacter = service.CreateCharacter(gmSession, new CreateCharacterCommand("Hero", Guid.NewGuid())); + var noCampaignCharacter = service.CreateCharacter(gmSession, "Hero", Guid.NewGuid()); Assert.False(noCampaignCharacter.Succeeded); - var character = ServiceTestSupport.GetValue(service.CreateCharacter(gmSession, new CreateCharacterCommand("Hero", campaign.Id))); + var character = ServiceTestSupport.GetValue(service.CreateCharacter(gmSession, "Hero", campaign.Id)); var missingCharacterActivate = service.ActivateCharacter(gmSession, Guid.NewGuid()); Assert.False(missingCharacterActivate.Succeeded); @@ -42,8 +42,8 @@ public sealed class ServiceCampaignTests { using var harness = ServiceTestSupport.CreateHarness(); var service = harness.Service; - service.Register(new RegisterCommand("user", "Password123", "User")); - var sessionToken = ServiceTestSupport.GetValue(service.Login(new LoginCommand("user", "Password123"))).SessionToken; + service.Register("user", "Password123", "User"); + var sessionToken = ServiceTestSupport.GetValue(service.Login("user", "Password123")).SessionToken; var result = service.GetCurrentCampaignCharacters(sessionToken); Assert.False(result.Succeeded); @@ -54,14 +54,14 @@ public sealed class ServiceCampaignTests { using var harness = ServiceTestSupport.CreateHarness(); var service = harness.Service; - service.Register(new RegisterCommand("gm", "Password123", "GM")); - service.Register(new RegisterCommand("player", "Password123", "Player")); - var gmSession = ServiceTestSupport.GetValue(service.Login(new LoginCommand("gm", "Password123"))).SessionToken; - var playerSession = ServiceTestSupport.GetValue(service.Login(new LoginCommand("player", "Password123"))).SessionToken; + service.Register("gm", "Password123", "GM"); + service.Register("player", "Password123", "Player"); + var gmSession = ServiceTestSupport.GetValue(service.Login("gm", "Password123")).SessionToken; + var playerSession = ServiceTestSupport.GetValue(service.Login("player", "Password123")).SessionToken; - var gmCampaign = ServiceTestSupport.GetValue(service.CreateCampaign(gmSession, new CreateCampaignCommand("Owned", "d6"))); - _ = ServiceTestSupport.GetValue(service.CreateCampaign(gmSession, new CreateCampaignCommand("Owned 2", "d6"))); - _ = service.CreateCharacter(playerSession, new CreateCharacterCommand("Joiner", gmCampaign.Id)); + var gmCampaign = ServiceTestSupport.GetValue(service.CreateCampaign(gmSession, "Owned", "d6")); + _ = ServiceTestSupport.GetValue(service.CreateCampaign(gmSession, "Owned 2", "d6")); + _ = service.CreateCharacter(playerSession, "Joiner", gmCampaign.Id); var playerCampaigns = service.GetCampaigns(playerSession); Assert.True(playerCampaigns.Succeeded); @@ -76,20 +76,20 @@ public sealed class ServiceCampaignTests using var harness = ServiceTestSupport.CreateHarness(); var service = harness.Service; - service.Register(new RegisterCommand("gm", "Password123", "GM")); - service.Register(new RegisterCommand("owner", "Password123", "Owner")); - service.Register(new RegisterCommand("other", "Password123", "Other")); + service.Register("gm", "Password123", "GM"); + service.Register("owner", "Password123", "Owner"); + service.Register("other", "Password123", "Other"); - var gmSession = ServiceTestSupport.GetValue(service.Login(new LoginCommand("gm", "Password123"))).SessionToken; - var ownerSession = ServiceTestSupport.GetValue(service.Login(new LoginCommand("owner", "Password123"))).SessionToken; - var otherSession = ServiceTestSupport.GetValue(service.Login(new LoginCommand("other", "Password123"))).SessionToken; + var gmSession = ServiceTestSupport.GetValue(service.Login("gm", "Password123")).SessionToken; + var ownerSession = ServiceTestSupport.GetValue(service.Login("owner", "Password123")).SessionToken; + var otherSession = ServiceTestSupport.GetValue(service.Login("other", "Password123")).SessionToken; - var campaign = ServiceTestSupport.GetValue(service.CreateCampaign(gmSession, new CreateCampaignCommand("Main", "d6"))); - var ownerCharacter = ServiceTestSupport.GetValue(service.CreateCharacter(ownerSession, new CreateCharacterCommand("Owner Character", campaign.Id))); - var otherCharacter = ServiceTestSupport.GetValue(service.CreateCharacter(otherSession, new CreateCharacterCommand("Other Character", campaign.Id))); + var campaign = ServiceTestSupport.GetValue(service.CreateCampaign(gmSession, "Main", "d6")); + var ownerCharacter = ServiceTestSupport.GetValue(service.CreateCharacter(ownerSession, "Owner Character", campaign.Id)); + var otherCharacter = ServiceTestSupport.GetValue(service.CreateCharacter(otherSession, "Other Character", campaign.Id)); - var ownerSkill = ServiceTestSupport.GetValue(service.CreateSkill(ownerSession, ownerCharacter.Id, new CreateSkillCommand("Stealth", "2D+1"))); - _ = ServiceTestSupport.GetValue(service.CreateSkill(otherSession, otherCharacter.Id, new CreateSkillCommand("Perception", "1D+2"))); + var ownerSkill = ServiceTestSupport.GetValue(service.CreateSkill(ownerSession, ownerCharacter.Id, "Stealth", "2D+1")); + _ = ServiceTestSupport.GetValue(service.CreateSkill(otherSession, otherCharacter.Id, "Perception", "1D+2")); var ownerView = ServiceTestSupport.GetValue(service.GetCampaign(ownerSession, campaign.Id)); Assert.Single(ownerView.Characters); diff --git a/RpgRoller.Tests/Services/ServicePersistenceTests.cs b/RpgRoller.Tests/Services/ServicePersistenceTests.cs index a223888..1bc4355 100644 --- a/RpgRoller.Tests/Services/ServicePersistenceTests.cs +++ b/RpgRoller.Tests/Services/ServicePersistenceTests.cs @@ -8,35 +8,35 @@ public sealed class ServicePersistenceTests using var harness = ServiceTestSupport.CreateHarness(2, 3, 4); var service = harness.Service; - var invalidCredentials = service.Login(new LoginCommand("", "")); + var invalidCredentials = service.Login("", ""); Assert.False(invalidCredentials.Succeeded); service.Logout("missing-session"); - service.Register(new RegisterCommand("gm", "Password123", "GM")); - service.Register(new RegisterCommand("owner", "Password123", "Owner")); - service.Register(new RegisterCommand("other", "Password123", "Other")); + service.Register("gm", "Password123", "GM"); + service.Register("owner", "Password123", "Owner"); + service.Register("other", "Password123", "Other"); - var gmSession = ServiceTestSupport.GetValue(service.Login(new LoginCommand("gm", "Password123"))).SessionToken; - var ownerSession = ServiceTestSupport.GetValue(service.Login(new LoginCommand("owner", "Password123"))).SessionToken; - var otherSession = ServiceTestSupport.GetValue(service.Login(new LoginCommand("other", "Password123"))).SessionToken; + var gmSession = ServiceTestSupport.GetValue(service.Login("gm", "Password123")).SessionToken; + var ownerSession = ServiceTestSupport.GetValue(service.Login("owner", "Password123")).SessionToken; + var otherSession = ServiceTestSupport.GetValue(service.Login("other", "Password123")).SessionToken; - var campaign = ServiceTestSupport.GetValue(service.CreateCampaign(gmSession, new CreateCampaignCommand("Main", "d6"))); - var ownerCharacter = ServiceTestSupport.GetValue(service.CreateCharacter(ownerSession, new CreateCharacterCommand("Owner Character", campaign.Id))); + var campaign = ServiceTestSupport.GetValue(service.CreateCampaign(gmSession, "Main", "d6")); + var ownerCharacter = ServiceTestSupport.GetValue(service.CreateCharacter(ownerSession, "Owner Character", campaign.Id)); Assert.False(service.GetMe(string.Empty).Succeeded); - Assert.False(service.CreateCampaign(gmSession, new CreateCampaignCommand("", "d6")).Succeeded); + Assert.False(service.CreateCampaign(gmSession, "", "d6").Succeeded); Assert.False(service.GetCampaigns(string.Empty).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.CreateCharacter(ownerSession, "", campaign.Id).Succeeded); + Assert.False(service.CreateCharacter(string.Empty, "Name", campaign.Id).Succeeded); + Assert.False(service.UpdateCharacter(string.Empty, ownerCharacter.Id, "Renamed", campaign.Id).Succeeded); + Assert.False(service.UpdateCharacter(ownerSession, Guid.NewGuid(), "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 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); + Assert.False(service.CreateSkill(string.Empty, ownerCharacter.Id, "Stealth", "2D+1").Succeeded); + Assert.False(service.CreateSkill(ownerSession, Guid.NewGuid(), "Stealth", "2D+1").Succeeded); + Assert.False(service.CreateSkill(otherSession, ownerCharacter.Id, "Stealth", "2D+1").Succeeded); using (var db = harness.CreateDbContext()) { @@ -74,11 +74,11 @@ public sealed class ServicePersistenceTests Assert.Null(db.Users.Single(u => u.UsernameNormalized == "OWNER").ActiveCharacterId); } - var skill = ServiceTestSupport.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); + var skill = ServiceTestSupport.GetValue(service.CreateSkill(ownerSession, ownerCharacter.Id, "Stealth", "2D+1")); + Assert.False(service.UpdateSkill(ownerSession, skill.Id, "", "2D+1").Succeeded); + Assert.False(service.UpdateSkill(string.Empty, skill.Id, "Stealth", "2D+1").Succeeded); + Assert.False(service.UpdateSkill(ownerSession, skill.Id, "Stealth", "bad").Succeeded); + Assert.False(service.RollSkill(string.Empty, skill.Id, "public").Succeeded); using (var db = harness.CreateDbContext()) { @@ -88,7 +88,7 @@ public sealed class ServicePersistenceTests } using var invalidExpressionHarness = ServiceTestSupport.CreateHarnessFromPath(harness.DbPath, 2, 3, 4); - Assert.False(invalidExpressionHarness.Service.RollSkill(ownerSession, skill.Id, new RollSkillCommand("public")).Succeeded); + Assert.False(invalidExpressionHarness.Service.RollSkill(ownerSession, skill.Id, "public").Succeeded); Assert.False(service.GetCampaignLog(string.Empty, campaign.Id).Succeeded); } } diff --git a/RpgRoller.Tests/Services/ServiceSkillRollTests.cs b/RpgRoller.Tests/Services/ServiceSkillRollTests.cs index 2e2f2f7..ec8296a 100644 --- a/RpgRoller.Tests/Services/ServiceSkillRollTests.cs +++ b/RpgRoller.Tests/Services/ServiceSkillRollTests.cs @@ -7,54 +7,54 @@ public sealed class ServiceSkillRollTests { using var harness = ServiceTestSupport.CreateHarness(3, 4, 5, 6); var service = harness.Service; - service.Register(new RegisterCommand("gm", "Password123", "GM")); - service.Register(new RegisterCommand("owner", "Password123", "Owner")); - service.Register(new RegisterCommand("other", "Password123", "Other")); + service.Register("gm", "Password123", "GM"); + service.Register("owner", "Password123", "Owner"); + service.Register("other", "Password123", "Other"); - var gmSession = ServiceTestSupport.GetValue(service.Login(new LoginCommand("gm", "Password123"))).SessionToken; - var ownerSession = ServiceTestSupport.GetValue(service.Login(new LoginCommand("owner", "Password123"))).SessionToken; - var otherSession = ServiceTestSupport.GetValue(service.Login(new LoginCommand("other", "Password123"))).SessionToken; + var gmSession = ServiceTestSupport.GetValue(service.Login("gm", "Password123")).SessionToken; + var ownerSession = ServiceTestSupport.GetValue(service.Login("owner", "Password123")).SessionToken; + var otherSession = ServiceTestSupport.GetValue(service.Login("other", "Password123")).SessionToken; - var campaign = ServiceTestSupport.GetValue(service.CreateCampaign(gmSession, new CreateCampaignCommand("Main", "dnd5e"))); - var character = ServiceTestSupport.GetValue(service.CreateCharacter(ownerSession, new CreateCharacterCommand("Owner Char", campaign.Id))); + var campaign = ServiceTestSupport.GetValue(service.CreateCampaign(gmSession, "Main", "dnd5e")); + var character = ServiceTestSupport.GetValue(service.CreateCharacter(ownerSession, "Owner Char", campaign.Id)); - var noPermissionUpdate = service.UpdateCharacter(otherSession, character.Id, new UpdateCharacterCommand("Renamed", campaign.Id)); + var noPermissionUpdate = service.UpdateCharacter(otherSession, character.Id, "Renamed", campaign.Id); Assert.False(noPermissionUpdate.Succeeded); - var invalidCharacterName = service.UpdateCharacter(ownerSession, character.Id, new UpdateCharacterCommand("", campaign.Id)); + var invalidCharacterName = service.UpdateCharacter(ownerSession, character.Id, "", campaign.Id); Assert.False(invalidCharacterName.Succeeded); - var missingTargetCampaign = service.UpdateCharacter(ownerSession, character.Id, new UpdateCharacterCommand("Renamed", Guid.NewGuid())); + var missingTargetCampaign = service.UpdateCharacter(ownerSession, character.Id, "Renamed", Guid.NewGuid()); Assert.False(missingTargetCampaign.Succeeded); - var noSkillName = service.CreateSkill(ownerSession, character.Id, new CreateSkillCommand("", "1d20")); + var noSkillName = service.CreateSkill(ownerSession, character.Id, "", "1d20"); Assert.False(noSkillName.Succeeded); - var invalidExpression = service.CreateSkill(ownerSession, character.Id, new CreateSkillCommand("Skill", "5D+4")); + var invalidExpression = service.CreateSkill(ownerSession, character.Id, "Skill", "5D+4"); Assert.False(invalidExpression.Succeeded); - var skill = ServiceTestSupport.GetValue(service.CreateSkill(ownerSession, character.Id, new CreateSkillCommand("Skill", "1d20+2"))); + var skill = ServiceTestSupport.GetValue(service.CreateSkill(ownerSession, character.Id, "Skill", "1d20+2")); - var missingSkillUpdate = service.UpdateSkill(ownerSession, Guid.NewGuid(), new UpdateSkillCommand("X", "1d20")); + var missingSkillUpdate = service.UpdateSkill(ownerSession, Guid.NewGuid(), "X", "1d20"); Assert.False(missingSkillUpdate.Succeeded); - var forbiddenSkillUpdate = service.UpdateSkill(otherSession, skill.Id, new UpdateSkillCommand("X", "1d20")); + var forbiddenSkillUpdate = service.UpdateSkill(otherSession, skill.Id, "X", "1d20"); Assert.False(forbiddenSkillUpdate.Succeeded); - var gmSkillUpdate = service.UpdateSkill(gmSession, skill.Id, new UpdateSkillCommand("GM Edit", "2d6+1")); + var gmSkillUpdate = service.UpdateSkill(gmSession, skill.Id, "GM Edit", "2d6+1"); Assert.True(gmSkillUpdate.Succeeded); - var missingRoll = service.RollSkill(ownerSession, Guid.NewGuid(), new RollSkillCommand("public")); + var missingRoll = service.RollSkill(ownerSession, Guid.NewGuid(), "public"); Assert.False(missingRoll.Succeeded); - var invalidVisibility = service.RollSkill(ownerSession, skill.Id, new RollSkillCommand("hidden")); + var invalidVisibility = service.RollSkill(ownerSession, skill.Id, "hidden"); Assert.False(invalidVisibility.Succeeded); - var forbiddenRoll = service.RollSkill(otherSession, skill.Id, new RollSkillCommand("public")); + var forbiddenRoll = service.RollSkill(otherSession, skill.Id, "public"); Assert.False(forbiddenRoll.Succeeded); - var privateRoll = service.RollSkill(ownerSession, skill.Id, new RollSkillCommand("private")); - var publicRoll = service.RollSkill(ownerSession, skill.Id, new RollSkillCommand("public")); + var privateRoll = service.RollSkill(ownerSession, skill.Id, "private"); + var publicRoll = service.RollSkill(ownerSession, skill.Id, "public"); Assert.True(privateRoll.Succeeded); Assert.True(publicRoll.Succeeded); diff --git a/RpgRoller.Tests/UnitTest1.cs b/RpgRoller.Tests/UnitTest1.cs deleted file mode 100644 index 093d131..0000000 --- a/RpgRoller.Tests/UnitTest1.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace RpgRoller.Tests; - -// Integration API tests were split by concern under RpgRoller.Tests/Api. diff --git a/RpgRoller/Api/AuthEndpoints.cs b/RpgRoller/Api/AuthEndpoints.cs index 5f02b47..620cd4b 100644 --- a/RpgRoller/Api/AuthEndpoints.cs +++ b/RpgRoller/Api/AuthEndpoints.cs @@ -10,7 +10,7 @@ internal static class AuthEndpoints { group.MapPost("/auth/register", Results, BadRequest> (RegisterRequest request, IGameService game) => { - var result = game.Register(request.ToCommand()); + var result = game.Register(request.Username, request.Password, request.DisplayName); if (!result.Succeeded) { return ApiResultMapper.ToBadRequest(result.Error!); @@ -21,7 +21,7 @@ internal static class AuthEndpoints group.MapPost("/auth/login", Results, BadRequest> (LoginRequest request, HttpContext context, IGameService game) => { - var result = game.Login(request.ToCommand()); + var result = game.Login(request.Username, request.Password); if (!result.Succeeded) { return ApiResultMapper.ToBadRequest(result.Error!); diff --git a/RpgRoller/Api/CampaignEndpoints.cs b/RpgRoller/Api/CampaignEndpoints.cs index 3835376..6169efe 100644 --- a/RpgRoller/Api/CampaignEndpoints.cs +++ b/RpgRoller/Api/CampaignEndpoints.cs @@ -9,7 +9,7 @@ internal static class CampaignEndpoints { group.MapPost("/campaigns", (CreateCampaignRequest request, HttpContext context, IGameService game) => { - var result = game.CreateCampaign(context.GetRequiredSessionToken(), request.ToCommand()); + var result = game.CreateCampaign(context.GetRequiredSessionToken(), request.Name, request.RulesetId); return ApiResultMapper.ToApiResult(result); }); diff --git a/RpgRoller/Api/CharacterEndpoints.cs b/RpgRoller/Api/CharacterEndpoints.cs index c605579..13832fb 100644 --- a/RpgRoller/Api/CharacterEndpoints.cs +++ b/RpgRoller/Api/CharacterEndpoints.cs @@ -9,13 +9,13 @@ internal static class CharacterEndpoints { group.MapPost("/characters", (CreateCharacterRequest request, HttpContext context, IGameService game) => { - var result = game.CreateCharacter(context.GetRequiredSessionToken(), request.ToCommand()); + var result = game.CreateCharacter(context.GetRequiredSessionToken(), request.Name, request.CampaignId); return ApiResultMapper.ToApiResult(result); }); group.MapPut("/characters/{characterId:guid}", (Guid characterId, UpdateCharacterRequest request, HttpContext context, IGameService game) => { - var result = game.UpdateCharacter(context.GetRequiredSessionToken(), characterId, request.ToCommand()); + var result = game.UpdateCharacter(context.GetRequiredSessionToken(), characterId, request.Name, request.CampaignId); return ApiResultMapper.ToApiResult(result); }); diff --git a/RpgRoller/Api/RequestMappings.cs b/RpgRoller/Api/RequestMappings.cs index 7fa96f5..44f2b0f 100644 --- a/RpgRoller/Api/RequestMappings.cs +++ b/RpgRoller/Api/RequestMappings.cs @@ -1,47 +1,5 @@ -using RpgRoller.Contracts; -using RpgRoller.Services; - namespace RpgRoller.Api; internal static class RequestMappings { - public static RegisterCommand ToCommand(this RegisterRequest request) - { - return new RegisterCommand(request.Username, request.Password, request.DisplayName); - } - - public static LoginCommand ToCommand(this LoginRequest request) - { - return new LoginCommand(request.Username, request.Password); - } - - public static CreateCampaignCommand ToCommand(this CreateCampaignRequest request) - { - return new CreateCampaignCommand(request.Name, request.RulesetId); - } - - public static CreateCharacterCommand ToCommand(this CreateCharacterRequest request) - { - return new CreateCharacterCommand(request.Name, request.CampaignId); - } - - public static UpdateCharacterCommand ToCommand(this UpdateCharacterRequest request) - { - return new UpdateCharacterCommand(request.Name, request.CampaignId); - } - - public static CreateSkillCommand ToCommand(this CreateSkillRequest request) - { - return new CreateSkillCommand(request.Name, request.DiceRollDefinition); - } - - public static UpdateSkillCommand ToCommand(this UpdateSkillRequest request) - { - return new UpdateSkillCommand(request.Name, request.DiceRollDefinition); - } - - public static RollSkillCommand ToCommand(this RollSkillRequest request) - { - return new RollSkillCommand(request.Visibility); - } } diff --git a/RpgRoller/Api/SkillEndpoints.cs b/RpgRoller/Api/SkillEndpoints.cs index 9c3ed97..f568f12 100644 --- a/RpgRoller/Api/SkillEndpoints.cs +++ b/RpgRoller/Api/SkillEndpoints.cs @@ -9,19 +9,19 @@ internal static class SkillEndpoints { group.MapPost("/characters/{characterId:guid}/skills", (Guid characterId, CreateSkillRequest request, HttpContext context, IGameService game) => { - var result = game.CreateSkill(context.GetRequiredSessionToken(), characterId, request.ToCommand()); + var result = game.CreateSkill(context.GetRequiredSessionToken(), characterId, request.Name, request.DiceRollDefinition); return ApiResultMapper.ToApiResult(result); }); group.MapPut("/skills/{skillId:guid}", (Guid skillId, UpdateSkillRequest request, HttpContext context, IGameService game) => { - var result = game.UpdateSkill(context.GetRequiredSessionToken(), skillId, request.ToCommand()); + var result = game.UpdateSkill(context.GetRequiredSessionToken(), skillId, request.Name, request.DiceRollDefinition); return ApiResultMapper.ToApiResult(result); }); group.MapPost("/skills/{skillId:guid}/roll", (Guid skillId, RollSkillRequest request, HttpContext context, IGameService game) => { - var result = game.RollSkill(context.GetRequiredSessionToken(), skillId, request.ToCommand()); + var result = game.RollSkill(context.GetRequiredSessionToken(), skillId, request.Visibility); return ApiResultMapper.ToApiResult(result); }); diff --git a/RpgRoller/Services/GameService.cs b/RpgRoller/Services/GameService.cs index 184ea71..4ab6f7e 100644 --- a/RpgRoller/Services/GameService.cs +++ b/RpgRoller/Services/GameService.cs @@ -35,27 +35,27 @@ public sealed class GameService : IGameService .ToArray(); } - public ServiceResult Register(RegisterCommand request) + public ServiceResult Register(string username, string password, string displayName) { - if (string.IsNullOrWhiteSpace(request.Username)) + if (string.IsNullOrWhiteSpace(username)) { return ServiceResult.Failure("invalid_username", "Username is required."); } - if (string.IsNullOrWhiteSpace(request.DisplayName)) + if (string.IsNullOrWhiteSpace(displayName)) { return ServiceResult.Failure("invalid_display_name", "Display name is required."); } - if (string.IsNullOrWhiteSpace(request.Password) || request.Password.Length < 8) + if (string.IsNullOrWhiteSpace(password) || password.Length < 8) { return ServiceResult.Failure("invalid_password", "Password must be at least 8 characters."); } lock (m_Gate) { - var username = request.Username.Trim(); - var normalizedUsername = NormalizeUsername(username); + var trimmedUsername = username.Trim(); + var normalizedUsername = NormalizeUsername(trimmedUsername); if (m_UserIdsByUsername.ContainsKey(normalizedUsername)) { return ServiceResult.Failure("duplicate_username", "Username is already taken."); @@ -64,14 +64,14 @@ public sealed class GameService : IGameService var user = new UserAccount { Id = Guid.NewGuid(), - Username = username, + Username = trimmedUsername, UsernameNormalized = normalizedUsername, - DisplayName = request.DisplayName.Trim(), + DisplayName = displayName.Trim(), PasswordHash = string.Empty, ActiveCharacterId = null }; - user.PasswordHash = m_PasswordHasher.HashPassword(user, request.Password); + user.PasswordHash = m_PasswordHasher.HashPassword(user, password); m_UsersById[user.Id] = user; m_UserIdsByUsername[user.UsernameNormalized] = user.Id; @@ -81,23 +81,23 @@ public sealed class GameService : IGameService } } - public ServiceResult<(UserSummary User, string SessionToken)> Login(LoginCommand request) + public ServiceResult<(UserSummary User, string SessionToken)> Login(string username, string password) { - if (string.IsNullOrWhiteSpace(request.Username) || string.IsNullOrWhiteSpace(request.Password)) + if (string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(password)) { return ServiceResult<(UserSummary User, string SessionToken)>.Failure("invalid_credentials", "Invalid username or password."); } lock (m_Gate) { - var normalizedUsername = NormalizeUsername(request.Username.Trim()); + var normalizedUsername = NormalizeUsername(username.Trim()); if (!m_UserIdsByUsername.TryGetValue(normalizedUsername, out var userId)) { return ServiceResult<(UserSummary User, string SessionToken)>.Failure("invalid_credentials", "Invalid username or password."); } var user = m_UsersById[userId]; - var verification = m_PasswordHasher.VerifyHashedPassword(user, user.PasswordHash, request.Password); + var verification = m_PasswordHasher.VerifyHashedPassword(user, user.PasswordHash, password); if (verification == PasswordVerificationResult.Failed) { return ServiceResult<(UserSummary User, string SessionToken)>.Failure("invalid_credentials", "Invalid username or password."); @@ -105,7 +105,7 @@ public sealed class GameService : IGameService if (verification == PasswordVerificationResult.SuccessRehashNeeded) { - user.PasswordHash = m_PasswordHasher.HashPassword(user, request.Password); + user.PasswordHash = m_PasswordHasher.HashPassword(user, password); } var session = CreateSession(userId); @@ -162,14 +162,14 @@ public sealed class GameService : IGameService } } - public ServiceResult CreateCampaign(string sessionToken, CreateCampaignCommand request) + public ServiceResult CreateCampaign(string sessionToken, string name, string rulesetId) { - if (string.IsNullOrWhiteSpace(request.Name)) + if (string.IsNullOrWhiteSpace(name)) { return ServiceResult.Failure("invalid_campaign_name", "Campaign name is required."); } - var ruleset = DiceRules.TryParseRulesetId(request.RulesetId); + var ruleset = DiceRules.TryParseRulesetId(rulesetId); if (ruleset is null) { return ServiceResult.Failure("invalid_ruleset", "Unknown ruleset."); @@ -187,7 +187,7 @@ public sealed class GameService : IGameService { Id = Guid.NewGuid(), GmUserId = user.Id, - Name = request.Name.Trim(), + Name = name.Trim(), Ruleset = ruleset.Value, Version = 1 }; @@ -263,9 +263,9 @@ public sealed class GameService : IGameService } } - public ServiceResult CreateCharacter(string sessionToken, CreateCharacterCommand request) + public ServiceResult CreateCharacter(string sessionToken, string name, Guid campaignId) { - if (string.IsNullOrWhiteSpace(request.Name)) + if (string.IsNullOrWhiteSpace(name)) { return ServiceResult.Failure("invalid_character_name", "Character name is required."); } @@ -278,7 +278,7 @@ public sealed class GameService : IGameService return ServiceResult.Failure("unauthorized", "You must be logged in."); } - if (!m_CampaignsById.ContainsKey(request.CampaignId)) + if (!m_CampaignsById.ContainsKey(campaignId)) { return ServiceResult.Failure("campaign_not_found", "Campaign was not found."); } @@ -287,8 +287,8 @@ public sealed class GameService : IGameService { Id = Guid.NewGuid(), OwnerUserId = user.Id, - CampaignId = request.CampaignId, - Name = request.Name.Trim() + CampaignId = campaignId, + Name = name.Trim() }; m_CharactersById[character.Id] = character; @@ -299,9 +299,9 @@ public sealed class GameService : IGameService } } - public ServiceResult UpdateCharacter(string sessionToken, Guid characterId, UpdateCharacterCommand request) + public ServiceResult UpdateCharacter(string sessionToken, Guid characterId, string name, Guid campaignId) { - if (string.IsNullOrWhiteSpace(request.Name)) + if (string.IsNullOrWhiteSpace(name)) { return ServiceResult.Failure("invalid_character_name", "Character name is required."); } @@ -319,7 +319,7 @@ public sealed class GameService : IGameService return ServiceResult.Failure("character_not_found", "Character was not found."); } - if (!m_CampaignsById.TryGetValue(request.CampaignId, out var targetCampaign)) + if (!m_CampaignsById.TryGetValue(campaignId, out var targetCampaign)) { return ServiceResult.Failure("campaign_not_found", "Campaign was not found."); } @@ -334,8 +334,8 @@ public sealed class GameService : IGameService } var sourceCampaignId = character.CampaignId; - character.Name = request.Name.Trim(); - character.CampaignId = request.CampaignId; + character.Name = name.Trim(); + character.CampaignId = campaignId; TouchCampaignLocked(sourceCampaignId); if (sourceCampaignId != character.CampaignId) @@ -399,9 +399,9 @@ public sealed class GameService : IGameService } } - public ServiceResult CreateSkill(string sessionToken, Guid characterId, CreateSkillCommand request) + public ServiceResult CreateSkill(string sessionToken, Guid characterId, string name, string diceRollDefinition) { - if (string.IsNullOrWhiteSpace(request.Name)) + if (string.IsNullOrWhiteSpace(name)) { return ServiceResult.Failure("invalid_skill_name", "Skill name is required."); } @@ -425,7 +425,7 @@ public sealed class GameService : IGameService return ServiceResult.Failure("forbidden", "Only the owner or GM can edit skills."); } - var expressionValidation = DiceRules.ParseExpression(campaign.Ruleset, request.DiceRollDefinition); + var expressionValidation = DiceRules.ParseExpression(campaign.Ruleset, diceRollDefinition); if (!expressionValidation.Succeeded) { return ServiceResult.Failure(expressionValidation.Error!.Code, expressionValidation.Error.Message); @@ -435,7 +435,7 @@ public sealed class GameService : IGameService { Id = Guid.NewGuid(), CharacterId = character.Id, - Name = request.Name.Trim(), + Name = name.Trim(), DiceRollDefinition = expressionValidation.Value!.Canonical }; @@ -447,9 +447,9 @@ public sealed class GameService : IGameService } } - public ServiceResult UpdateSkill(string sessionToken, Guid skillId, UpdateSkillCommand request) + public ServiceResult UpdateSkill(string sessionToken, Guid skillId, string name, string diceRollDefinition) { - if (string.IsNullOrWhiteSpace(request.Name)) + if (string.IsNullOrWhiteSpace(name)) { return ServiceResult.Failure("invalid_skill_name", "Skill name is required."); } @@ -474,13 +474,13 @@ public sealed class GameService : IGameService return ServiceResult.Failure("forbidden", "Only the owner or GM can edit skills."); } - var expressionValidation = DiceRules.ParseExpression(campaign.Ruleset, request.DiceRollDefinition); + var expressionValidation = DiceRules.ParseExpression(campaign.Ruleset, diceRollDefinition); if (!expressionValidation.Succeeded) { return ServiceResult.Failure(expressionValidation.Error!.Code, expressionValidation.Error.Message); } - skill.Name = request.Name.Trim(); + skill.Name = name.Trim(); skill.DiceRollDefinition = expressionValidation.Value!.Canonical; TouchCampaignLocked(campaign.Id); @@ -489,7 +489,7 @@ public sealed class GameService : IGameService } } - public ServiceResult RollSkill(string sessionToken, Guid skillId, RollSkillCommand request) + public ServiceResult RollSkill(string sessionToken, Guid skillId, string visibility) { lock (m_Gate) { @@ -517,10 +517,10 @@ public sealed class GameService : IGameService return ServiceResult.Failure(parsedExpression.Error!.Code, parsedExpression.Error.Message); } - var visibility = ParseVisibility(request.Visibility); - if (!visibility.Succeeded) + var parsedVisibility = ParseVisibility(visibility); + if (!parsedVisibility.Succeeded) { - return ServiceResult.Failure(visibility.Error!.Code, visibility.Error.Message); + return ServiceResult.Failure(parsedVisibility.Error!.Code, parsedVisibility.Error.Message); } var roll = ComputeRoll(parsedExpression.Value!); @@ -531,7 +531,7 @@ public sealed class GameService : IGameService CharacterId = character.Id, SkillId = skill.Id, RollerUserId = user.Id, - Visibility = visibility.Value, + Visibility = parsedVisibility.Value, Result = roll.Total, Breakdown = roll.Breakdown, TimestampUtc = DateTimeOffset.UtcNow diff --git a/RpgRoller/Services/IGameService.cs b/RpgRoller/Services/IGameService.cs index 23fc29c..f4ff386 100644 --- a/RpgRoller/Services/IGameService.cs +++ b/RpgRoller/Services/IGameService.cs @@ -7,25 +7,25 @@ public interface IGameService { IReadOnlyList GetRulesets(); - ServiceResult Register(RegisterCommand request); - ServiceResult<(UserSummary User, string SessionToken)> Login(LoginCommand request); + ServiceResult Register(string username, string password, string displayName); + ServiceResult<(UserSummary User, string SessionToken)> Login(string username, string password); void Logout(string sessionToken); UserSummary? GetUserBySession(string sessionToken); ServiceResult GetMe(string sessionToken); - ServiceResult CreateCampaign(string sessionToken, CreateCampaignCommand request); + ServiceResult CreateCampaign(string sessionToken, string name, string rulesetId); ServiceResult> GetCampaigns(string sessionToken); ServiceResult GetCampaign(string sessionToken, Guid campaignId); - ServiceResult CreateCharacter(string sessionToken, CreateCharacterCommand request); - ServiceResult UpdateCharacter(string sessionToken, Guid characterId, UpdateCharacterCommand request); + ServiceResult CreateCharacter(string sessionToken, string name, Guid campaignId); + ServiceResult UpdateCharacter(string sessionToken, Guid characterId, string name, Guid campaignId); ServiceResult ActivateCharacter(string sessionToken, Guid characterId); ServiceResult> GetCurrentCampaignCharacters(string sessionToken); - ServiceResult CreateSkill(string sessionToken, Guid characterId, CreateSkillCommand request); - ServiceResult UpdateSkill(string sessionToken, Guid skillId, UpdateSkillCommand request); + ServiceResult CreateSkill(string sessionToken, Guid characterId, string name, string diceRollDefinition); + ServiceResult UpdateSkill(string sessionToken, Guid skillId, string name, string diceRollDefinition); - ServiceResult RollSkill(string sessionToken, Guid skillId, RollSkillCommand request); + ServiceResult RollSkill(string sessionToken, Guid skillId, string visibility); ServiceResult> GetCampaignLog(string sessionToken, Guid campaignId); ServiceResult GetCampaignVersion(string sessionToken, Guid campaignId); diff --git a/RpgRoller/Services/ServiceCommands.cs b/RpgRoller/Services/ServiceCommands.cs index e9d5f9c..2e31e8d 100644 --- a/RpgRoller/Services/ServiceCommands.cs +++ b/RpgRoller/Services/ServiceCommands.cs @@ -1,13 +1 @@ -namespace RpgRoller.Services; - -public sealed record RegisterCommand(string Username, string Password, string DisplayName); -public sealed record LoginCommand(string Username, string Password); - -public sealed record CreateCampaignCommand(string Name, string RulesetId); -public sealed record CreateCharacterCommand(string Name, Guid CampaignId); -public sealed record UpdateCharacterCommand(string Name, Guid CampaignId); - -public sealed record CreateSkillCommand(string Name, string DiceRollDefinition); -public sealed record UpdateSkillCommand(string Name, string DiceRollDefinition); - -public sealed record RollSkillCommand(string Visibility); +// Intentionally left blank after removing service command records. diff --git a/TECH.md b/TECH.md index 91bbf5d..de8c45d 100644 --- a/TECH.md +++ b/TECH.md @@ -14,7 +14,7 @@ - Generated client output: `RpgRoller/wwwroot/generated/api-client.js` - Local CI parity entrypoint: `scripts/ci-local.ps1` - API endpoint modules: `RpgRoller/Api/*Endpoints.cs` + shared session/auth helpers -- Service boundary model: API request DTOs are mapped to `RpgRoller.Services/*Command` records before workflow execution +- Service boundary model: API request DTOs are mapped to explicit service method parameters before workflow execution - Current backend features: auth/session, campaign/character/skill management, ruleset-aware rolls, filtered campaign logs, and SSE state updates. - Current frontend features: authenticated campaign workspace with live log updates and full roll workflow controls.