Refactor backend to remove service command models
This commit is contained in:
4
FAQ.md
4
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.*`.
|
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.
|
||||||
|
|||||||
@@ -19,8 +19,8 @@ Backend:
|
|||||||
|
|
||||||
- `RpgRoller/Program.cs`: thin app bootstrap only
|
- `RpgRoller/Program.cs`: thin app bootstrap only
|
||||||
- `RpgRoller/Hosting/`: service registration + startup initialization
|
- `RpgRoller/Hosting/`: service registration + startup initialization
|
||||||
- `RpgRoller/Api/`: endpoint mapping modules, API-to-service request mapping, auth/session filter helpers
|
- `RpgRoller/Api/`: endpoint mapping modules and auth/session filter helpers
|
||||||
- `RpgRoller/Services/`: game workflows and service-layer command models (`*Command` records)
|
- `RpgRoller/Services/`: game workflows with explicit method parameters (no API DTO dependencies)
|
||||||
|
|
||||||
Frontend:
|
Frontend:
|
||||||
|
|
||||||
|
|||||||
@@ -8,11 +8,11 @@ public sealed class ServiceAuthTests
|
|||||||
using var harness = ServiceTestSupport.CreateHarness();
|
using var harness = ServiceTestSupport.CreateHarness();
|
||||||
var service = harness.Service;
|
var service = harness.Service;
|
||||||
|
|
||||||
var invalidUsername = service.Register(new RegisterCommand("", "Password123", "Display"));
|
var invalidUsername = service.Register("", "Password123", "Display");
|
||||||
var invalidDisplay = service.Register(new RegisterCommand("user", "Password123", ""));
|
var invalidDisplay = service.Register("user", "Password123", "");
|
||||||
var invalidPassword = service.Register(new RegisterCommand("user", "short", "Display"));
|
var invalidPassword = service.Register("user", "short", "Display");
|
||||||
var valid = service.Register(new RegisterCommand("user", "Password123", "Display"));
|
var valid = service.Register("user", "Password123", "Display");
|
||||||
var duplicate = service.Register(new RegisterCommand("user", "Password123", "Display 2"));
|
var duplicate = service.Register("user", "Password123", "Display 2");
|
||||||
|
|
||||||
Assert.False(invalidUsername.Succeeded);
|
Assert.False(invalidUsername.Succeeded);
|
||||||
Assert.False(invalidDisplay.Succeeded);
|
Assert.False(invalidDisplay.Succeeded);
|
||||||
@@ -26,11 +26,11 @@ public sealed class ServiceAuthTests
|
|||||||
{
|
{
|
||||||
using var harness = ServiceTestSupport.CreateHarness();
|
using var harness = ServiceTestSupport.CreateHarness();
|
||||||
var service = harness.Service;
|
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 invalidUser = service.Login("missing", "Password123");
|
||||||
var invalidPassword = service.Login(new LoginCommand("user", "bad-password"));
|
var invalidPassword = service.Login("user", "bad-password");
|
||||||
var valid = service.Login(new LoginCommand("user", "Password123"));
|
var valid = service.Login("user", "Password123");
|
||||||
|
|
||||||
Assert.False(invalidUser.Succeeded);
|
Assert.False(invalidUser.Succeeded);
|
||||||
Assert.False(invalidPassword.Succeeded);
|
Assert.False(invalidPassword.Succeeded);
|
||||||
@@ -50,8 +50,8 @@ public sealed class ServiceAuthTests
|
|||||||
using var harness = ServiceTestSupport.CreateHarness(hasher);
|
using var harness = ServiceTestSupport.CreateHarness(hasher);
|
||||||
var service = harness.Service;
|
var service = harness.Service;
|
||||||
|
|
||||||
service.Register(new RegisterCommand("user", "Password123", "Display"));
|
service.Register("user", "Password123", "Display");
|
||||||
var login = service.Login(new LoginCommand("user", "Password123"));
|
var login = service.Login("user", "Password123");
|
||||||
|
|
||||||
Assert.True(login.Succeeded);
|
Assert.True(login.Succeeded);
|
||||||
Assert.Equal(2, hasher.HashCalls);
|
Assert.Equal(2, hasher.HashCalls);
|
||||||
|
|||||||
@@ -8,20 +8,20 @@ public sealed class ServiceCampaignTests
|
|||||||
using var harness = ServiceTestSupport.CreateHarness();
|
using var harness = ServiceTestSupport.CreateHarness();
|
||||||
var service = harness.Service;
|
var service = harness.Service;
|
||||||
|
|
||||||
var unauthorizedCampaign = service.CreateCampaign("missing", new CreateCampaignCommand("Name", "d6"));
|
var unauthorizedCampaign = service.CreateCampaign("missing", "Name", "d6");
|
||||||
Assert.False(unauthorizedCampaign.Succeeded);
|
Assert.False(unauthorizedCampaign.Succeeded);
|
||||||
|
|
||||||
service.Register(new RegisterCommand("gm", "Password123", "GM"));
|
service.Register("gm", "Password123", "GM");
|
||||||
var gmSession = ServiceTestSupport.GetValue(service.Login(new LoginCommand("gm", "Password123"))).SessionToken;
|
var gmSession = ServiceTestSupport.GetValue(service.Login("gm", "Password123")).SessionToken;
|
||||||
var campaign = ServiceTestSupport.GetValue(service.CreateCampaign(gmSession, new CreateCampaignCommand("Name", "d6")));
|
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);
|
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);
|
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());
|
var missingCharacterActivate = service.ActivateCharacter(gmSession, Guid.NewGuid());
|
||||||
Assert.False(missingCharacterActivate.Succeeded);
|
Assert.False(missingCharacterActivate.Succeeded);
|
||||||
@@ -42,8 +42,8 @@ public sealed class ServiceCampaignTests
|
|||||||
{
|
{
|
||||||
using var harness = ServiceTestSupport.CreateHarness();
|
using var harness = ServiceTestSupport.CreateHarness();
|
||||||
var service = harness.Service;
|
var service = harness.Service;
|
||||||
service.Register(new RegisterCommand("user", "Password123", "User"));
|
service.Register("user", "Password123", "User");
|
||||||
var sessionToken = ServiceTestSupport.GetValue(service.Login(new LoginCommand("user", "Password123"))).SessionToken;
|
var sessionToken = ServiceTestSupport.GetValue(service.Login("user", "Password123")).SessionToken;
|
||||||
|
|
||||||
var result = service.GetCurrentCampaignCharacters(sessionToken);
|
var result = service.GetCurrentCampaignCharacters(sessionToken);
|
||||||
Assert.False(result.Succeeded);
|
Assert.False(result.Succeeded);
|
||||||
@@ -54,14 +54,14 @@ public sealed class ServiceCampaignTests
|
|||||||
{
|
{
|
||||||
using var harness = ServiceTestSupport.CreateHarness();
|
using var harness = ServiceTestSupport.CreateHarness();
|
||||||
var service = harness.Service;
|
var service = harness.Service;
|
||||||
service.Register(new RegisterCommand("gm", "Password123", "GM"));
|
service.Register("gm", "Password123", "GM");
|
||||||
service.Register(new RegisterCommand("player", "Password123", "Player"));
|
service.Register("player", "Password123", "Player");
|
||||||
var gmSession = ServiceTestSupport.GetValue(service.Login(new LoginCommand("gm", "Password123"))).SessionToken;
|
var gmSession = ServiceTestSupport.GetValue(service.Login("gm", "Password123")).SessionToken;
|
||||||
var playerSession = ServiceTestSupport.GetValue(service.Login(new LoginCommand("player", "Password123"))).SessionToken;
|
var playerSession = ServiceTestSupport.GetValue(service.Login("player", "Password123")).SessionToken;
|
||||||
|
|
||||||
var gmCampaign = ServiceTestSupport.GetValue(service.CreateCampaign(gmSession, new CreateCampaignCommand("Owned", "d6")));
|
var gmCampaign = ServiceTestSupport.GetValue(service.CreateCampaign(gmSession, "Owned", "d6"));
|
||||||
_ = ServiceTestSupport.GetValue(service.CreateCampaign(gmSession, new CreateCampaignCommand("Owned 2", "d6")));
|
_ = ServiceTestSupport.GetValue(service.CreateCampaign(gmSession, "Owned 2", "d6"));
|
||||||
_ = service.CreateCharacter(playerSession, new CreateCharacterCommand("Joiner", gmCampaign.Id));
|
_ = service.CreateCharacter(playerSession, "Joiner", gmCampaign.Id);
|
||||||
|
|
||||||
var playerCampaigns = service.GetCampaigns(playerSession);
|
var playerCampaigns = service.GetCampaigns(playerSession);
|
||||||
Assert.True(playerCampaigns.Succeeded);
|
Assert.True(playerCampaigns.Succeeded);
|
||||||
@@ -76,20 +76,20 @@ public sealed class ServiceCampaignTests
|
|||||||
using var harness = ServiceTestSupport.CreateHarness();
|
using var harness = ServiceTestSupport.CreateHarness();
|
||||||
var service = harness.Service;
|
var service = harness.Service;
|
||||||
|
|
||||||
service.Register(new RegisterCommand("gm", "Password123", "GM"));
|
service.Register("gm", "Password123", "GM");
|
||||||
service.Register(new RegisterCommand("owner", "Password123", "Owner"));
|
service.Register("owner", "Password123", "Owner");
|
||||||
service.Register(new RegisterCommand("other", "Password123", "Other"));
|
service.Register("other", "Password123", "Other");
|
||||||
|
|
||||||
var gmSession = ServiceTestSupport.GetValue(service.Login(new LoginCommand("gm", "Password123"))).SessionToken;
|
var gmSession = ServiceTestSupport.GetValue(service.Login("gm", "Password123")).SessionToken;
|
||||||
var ownerSession = ServiceTestSupport.GetValue(service.Login(new LoginCommand("owner", "Password123"))).SessionToken;
|
var ownerSession = ServiceTestSupport.GetValue(service.Login("owner", "Password123")).SessionToken;
|
||||||
var otherSession = ServiceTestSupport.GetValue(service.Login(new LoginCommand("other", "Password123"))).SessionToken;
|
var otherSession = ServiceTestSupport.GetValue(service.Login("other", "Password123")).SessionToken;
|
||||||
|
|
||||||
var campaign = ServiceTestSupport.GetValue(service.CreateCampaign(gmSession, new CreateCampaignCommand("Main", "d6")));
|
var campaign = ServiceTestSupport.GetValue(service.CreateCampaign(gmSession, "Main", "d6"));
|
||||||
var ownerCharacter = ServiceTestSupport.GetValue(service.CreateCharacter(ownerSession, new CreateCharacterCommand("Owner Character", campaign.Id)));
|
var ownerCharacter = ServiceTestSupport.GetValue(service.CreateCharacter(ownerSession, "Owner Character", campaign.Id));
|
||||||
var otherCharacter = ServiceTestSupport.GetValue(service.CreateCharacter(otherSession, new CreateCharacterCommand("Other 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")));
|
var ownerSkill = ServiceTestSupport.GetValue(service.CreateSkill(ownerSession, ownerCharacter.Id, "Stealth", "2D+1"));
|
||||||
_ = ServiceTestSupport.GetValue(service.CreateSkill(otherSession, otherCharacter.Id, new CreateSkillCommand("Perception", "1D+2")));
|
_ = ServiceTestSupport.GetValue(service.CreateSkill(otherSession, otherCharacter.Id, "Perception", "1D+2"));
|
||||||
|
|
||||||
var ownerView = ServiceTestSupport.GetValue(service.GetCampaign(ownerSession, campaign.Id));
|
var ownerView = ServiceTestSupport.GetValue(service.GetCampaign(ownerSession, campaign.Id));
|
||||||
Assert.Single(ownerView.Characters);
|
Assert.Single(ownerView.Characters);
|
||||||
|
|||||||
@@ -8,35 +8,35 @@ public sealed class ServicePersistenceTests
|
|||||||
using var harness = ServiceTestSupport.CreateHarness(2, 3, 4);
|
using var harness = ServiceTestSupport.CreateHarness(2, 3, 4);
|
||||||
var service = harness.Service;
|
var service = harness.Service;
|
||||||
|
|
||||||
var invalidCredentials = service.Login(new LoginCommand("", ""));
|
var invalidCredentials = service.Login("", "");
|
||||||
Assert.False(invalidCredentials.Succeeded);
|
Assert.False(invalidCredentials.Succeeded);
|
||||||
|
|
||||||
service.Logout("missing-session");
|
service.Logout("missing-session");
|
||||||
|
|
||||||
service.Register(new RegisterCommand("gm", "Password123", "GM"));
|
service.Register("gm", "Password123", "GM");
|
||||||
service.Register(new RegisterCommand("owner", "Password123", "Owner"));
|
service.Register("owner", "Password123", "Owner");
|
||||||
service.Register(new RegisterCommand("other", "Password123", "Other"));
|
service.Register("other", "Password123", "Other");
|
||||||
|
|
||||||
var gmSession = ServiceTestSupport.GetValue(service.Login(new LoginCommand("gm", "Password123"))).SessionToken;
|
var gmSession = ServiceTestSupport.GetValue(service.Login("gm", "Password123")).SessionToken;
|
||||||
var ownerSession = ServiceTestSupport.GetValue(service.Login(new LoginCommand("owner", "Password123"))).SessionToken;
|
var ownerSession = ServiceTestSupport.GetValue(service.Login("owner", "Password123")).SessionToken;
|
||||||
var otherSession = ServiceTestSupport.GetValue(service.Login(new LoginCommand("other", "Password123"))).SessionToken;
|
var otherSession = ServiceTestSupport.GetValue(service.Login("other", "Password123")).SessionToken;
|
||||||
|
|
||||||
var campaign = ServiceTestSupport.GetValue(service.CreateCampaign(gmSession, new CreateCampaignCommand("Main", "d6")));
|
var campaign = ServiceTestSupport.GetValue(service.CreateCampaign(gmSession, "Main", "d6"));
|
||||||
var ownerCharacter = ServiceTestSupport.GetValue(service.CreateCharacter(ownerSession, new CreateCharacterCommand("Owner Character", campaign.Id)));
|
var ownerCharacter = ServiceTestSupport.GetValue(service.CreateCharacter(ownerSession, "Owner Character", campaign.Id));
|
||||||
|
|
||||||
Assert.False(service.GetMe(string.Empty).Succeeded);
|
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.GetCampaigns(string.Empty).Succeeded);
|
||||||
Assert.False(service.CreateCharacter(ownerSession, new CreateCharacterCommand("", campaign.Id)).Succeeded);
|
Assert.False(service.CreateCharacter(ownerSession, "", campaign.Id).Succeeded);
|
||||||
Assert.False(service.CreateCharacter(string.Empty, new CreateCharacterCommand("Name", campaign.Id)).Succeeded);
|
Assert.False(service.CreateCharacter(string.Empty, "Name", campaign.Id).Succeeded);
|
||||||
Assert.False(service.UpdateCharacter(string.Empty, ownerCharacter.Id, new UpdateCharacterCommand("Renamed", campaign.Id)).Succeeded);
|
Assert.False(service.UpdateCharacter(string.Empty, ownerCharacter.Id, "Renamed", campaign.Id).Succeeded);
|
||||||
Assert.False(service.UpdateCharacter(ownerSession, Guid.NewGuid(), new UpdateCharacterCommand("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(string.Empty, ownerCharacter.Id).Succeeded);
|
||||||
Assert.False(service.ActivateCharacter(gmSession, ownerCharacter.Id).Succeeded);
|
Assert.False(service.ActivateCharacter(gmSession, ownerCharacter.Id).Succeeded);
|
||||||
Assert.False(service.GetCurrentCampaignCharacters(string.Empty).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(string.Empty, ownerCharacter.Id, "Stealth", "2D+1").Succeeded);
|
||||||
Assert.False(service.CreateSkill(ownerSession, Guid.NewGuid(), new CreateSkillCommand("Stealth", "2D+1")).Succeeded);
|
Assert.False(service.CreateSkill(ownerSession, Guid.NewGuid(), "Stealth", "2D+1").Succeeded);
|
||||||
Assert.False(service.CreateSkill(otherSession, ownerCharacter.Id, new CreateSkillCommand("Stealth", "2D+1")).Succeeded);
|
Assert.False(service.CreateSkill(otherSession, ownerCharacter.Id, "Stealth", "2D+1").Succeeded);
|
||||||
|
|
||||||
using (var db = harness.CreateDbContext())
|
using (var db = harness.CreateDbContext())
|
||||||
{
|
{
|
||||||
@@ -74,11 +74,11 @@ public sealed class ServicePersistenceTests
|
|||||||
Assert.Null(db.Users.Single(u => u.UsernameNormalized == "OWNER").ActiveCharacterId);
|
Assert.Null(db.Users.Single(u => u.UsernameNormalized == "OWNER").ActiveCharacterId);
|
||||||
}
|
}
|
||||||
|
|
||||||
var skill = ServiceTestSupport.GetValue(service.CreateSkill(ownerSession, ownerCharacter.Id, new CreateSkillCommand("Stealth", "2D+1")));
|
var skill = ServiceTestSupport.GetValue(service.CreateSkill(ownerSession, ownerCharacter.Id, "Stealth", "2D+1"));
|
||||||
Assert.False(service.UpdateSkill(ownerSession, skill.Id, new UpdateSkillCommand("", "2D+1")).Succeeded);
|
Assert.False(service.UpdateSkill(ownerSession, skill.Id, "", "2D+1").Succeeded);
|
||||||
Assert.False(service.UpdateSkill(string.Empty, skill.Id, new UpdateSkillCommand("Stealth", "2D+1")).Succeeded);
|
Assert.False(service.UpdateSkill(string.Empty, skill.Id, "Stealth", "2D+1").Succeeded);
|
||||||
Assert.False(service.UpdateSkill(ownerSession, skill.Id, new UpdateSkillCommand("Stealth", "bad")).Succeeded);
|
Assert.False(service.UpdateSkill(ownerSession, skill.Id, "Stealth", "bad").Succeeded);
|
||||||
Assert.False(service.RollSkill(string.Empty, skill.Id, new RollSkillCommand("public")).Succeeded);
|
Assert.False(service.RollSkill(string.Empty, skill.Id, "public").Succeeded);
|
||||||
|
|
||||||
using (var db = harness.CreateDbContext())
|
using (var db = harness.CreateDbContext())
|
||||||
{
|
{
|
||||||
@@ -88,7 +88,7 @@ public sealed class ServicePersistenceTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
using var invalidExpressionHarness = ServiceTestSupport.CreateHarnessFromPath(harness.DbPath, 2, 3, 4);
|
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);
|
Assert.False(service.GetCampaignLog(string.Empty, campaign.Id).Succeeded);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,54 +7,54 @@ public sealed class ServiceSkillRollTests
|
|||||||
{
|
{
|
||||||
using var harness = ServiceTestSupport.CreateHarness(3, 4, 5, 6);
|
using var harness = ServiceTestSupport.CreateHarness(3, 4, 5, 6);
|
||||||
var service = harness.Service;
|
var service = harness.Service;
|
||||||
service.Register(new RegisterCommand("gm", "Password123", "GM"));
|
service.Register("gm", "Password123", "GM");
|
||||||
service.Register(new RegisterCommand("owner", "Password123", "Owner"));
|
service.Register("owner", "Password123", "Owner");
|
||||||
service.Register(new RegisterCommand("other", "Password123", "Other"));
|
service.Register("other", "Password123", "Other");
|
||||||
|
|
||||||
var gmSession = ServiceTestSupport.GetValue(service.Login(new LoginCommand("gm", "Password123"))).SessionToken;
|
var gmSession = ServiceTestSupport.GetValue(service.Login("gm", "Password123")).SessionToken;
|
||||||
var ownerSession = ServiceTestSupport.GetValue(service.Login(new LoginCommand("owner", "Password123"))).SessionToken;
|
var ownerSession = ServiceTestSupport.GetValue(service.Login("owner", "Password123")).SessionToken;
|
||||||
var otherSession = ServiceTestSupport.GetValue(service.Login(new LoginCommand("other", "Password123"))).SessionToken;
|
var otherSession = ServiceTestSupport.GetValue(service.Login("other", "Password123")).SessionToken;
|
||||||
|
|
||||||
var campaign = ServiceTestSupport.GetValue(service.CreateCampaign(gmSession, new CreateCampaignCommand("Main", "dnd5e")));
|
var campaign = ServiceTestSupport.GetValue(service.CreateCampaign(gmSession, "Main", "dnd5e"));
|
||||||
var character = ServiceTestSupport.GetValue(service.CreateCharacter(ownerSession, new CreateCharacterCommand("Owner Char", campaign.Id)));
|
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);
|
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);
|
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);
|
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);
|
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);
|
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);
|
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);
|
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);
|
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);
|
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);
|
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);
|
Assert.False(forbiddenRoll.Succeeded);
|
||||||
|
|
||||||
var privateRoll = service.RollSkill(ownerSession, skill.Id, new RollSkillCommand("private"));
|
var privateRoll = service.RollSkill(ownerSession, skill.Id, "private");
|
||||||
var publicRoll = service.RollSkill(ownerSession, skill.Id, new RollSkillCommand("public"));
|
var publicRoll = service.RollSkill(ownerSession, skill.Id, "public");
|
||||||
Assert.True(privateRoll.Succeeded);
|
Assert.True(privateRoll.Succeeded);
|
||||||
Assert.True(publicRoll.Succeeded);
|
Assert.True(publicRoll.Succeeded);
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
namespace RpgRoller.Tests;
|
|
||||||
|
|
||||||
// Integration API tests were split by concern under RpgRoller.Tests/Api.
|
|
||||||
@@ -10,7 +10,7 @@ internal static class AuthEndpoints
|
|||||||
{
|
{
|
||||||
group.MapPost("/auth/register", Results<Ok<UserSummary>, BadRequest<ApiError>> (RegisterRequest request, IGameService game) =>
|
group.MapPost("/auth/register", Results<Ok<UserSummary>, BadRequest<ApiError>> (RegisterRequest request, IGameService game) =>
|
||||||
{
|
{
|
||||||
var result = game.Register(request.ToCommand());
|
var result = game.Register(request.Username, request.Password, request.DisplayName);
|
||||||
if (!result.Succeeded)
|
if (!result.Succeeded)
|
||||||
{
|
{
|
||||||
return ApiResultMapper.ToBadRequest(result.Error!);
|
return ApiResultMapper.ToBadRequest(result.Error!);
|
||||||
@@ -21,7 +21,7 @@ internal static class AuthEndpoints
|
|||||||
|
|
||||||
group.MapPost("/auth/login", Results<Ok<UserSummary>, BadRequest<ApiError>> (LoginRequest request, HttpContext context, IGameService game) =>
|
group.MapPost("/auth/login", Results<Ok<UserSummary>, BadRequest<ApiError>> (LoginRequest request, HttpContext context, IGameService game) =>
|
||||||
{
|
{
|
||||||
var result = game.Login(request.ToCommand());
|
var result = game.Login(request.Username, request.Password);
|
||||||
if (!result.Succeeded)
|
if (!result.Succeeded)
|
||||||
{
|
{
|
||||||
return ApiResultMapper.ToBadRequest(result.Error!);
|
return ApiResultMapper.ToBadRequest(result.Error!);
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ internal static class CampaignEndpoints
|
|||||||
{
|
{
|
||||||
group.MapPost("/campaigns", (CreateCampaignRequest request, HttpContext context, IGameService game) =>
|
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);
|
return ApiResultMapper.ToApiResult(result);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -9,13 +9,13 @@ internal static class CharacterEndpoints
|
|||||||
{
|
{
|
||||||
group.MapPost("/characters", (CreateCharacterRequest request, HttpContext context, IGameService game) =>
|
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);
|
return ApiResultMapper.ToApiResult(result);
|
||||||
});
|
});
|
||||||
|
|
||||||
group.MapPut("/characters/{characterId:guid}", (Guid characterId, UpdateCharacterRequest request, HttpContext context, IGameService game) =>
|
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);
|
return ApiResultMapper.ToApiResult(result);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,47 +1,5 @@
|
|||||||
using RpgRoller.Contracts;
|
|
||||||
using RpgRoller.Services;
|
|
||||||
|
|
||||||
namespace RpgRoller.Api;
|
namespace RpgRoller.Api;
|
||||||
|
|
||||||
internal static class RequestMappings
|
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,19 +9,19 @@ internal static class SkillEndpoints
|
|||||||
{
|
{
|
||||||
group.MapPost("/characters/{characterId:guid}/skills", (Guid characterId, CreateSkillRequest request, HttpContext context, IGameService game) =>
|
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);
|
return ApiResultMapper.ToApiResult(result);
|
||||||
});
|
});
|
||||||
|
|
||||||
group.MapPut("/skills/{skillId:guid}", (Guid skillId, UpdateSkillRequest request, HttpContext context, IGameService game) =>
|
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);
|
return ApiResultMapper.ToApiResult(result);
|
||||||
});
|
});
|
||||||
|
|
||||||
group.MapPost("/skills/{skillId:guid}/roll", (Guid skillId, RollSkillRequest request, HttpContext context, IGameService game) =>
|
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);
|
return ApiResultMapper.ToApiResult(result);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -35,27 +35,27 @@ public sealed class GameService : IGameService
|
|||||||
.ToArray();
|
.ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
public ServiceResult<UserSummary> Register(RegisterCommand request)
|
public ServiceResult<UserSummary> Register(string username, string password, string displayName)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(request.Username))
|
if (string.IsNullOrWhiteSpace(username))
|
||||||
{
|
{
|
||||||
return ServiceResult<UserSummary>.Failure("invalid_username", "Username is required.");
|
return ServiceResult<UserSummary>.Failure("invalid_username", "Username is required.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(request.DisplayName))
|
if (string.IsNullOrWhiteSpace(displayName))
|
||||||
{
|
{
|
||||||
return ServiceResult<UserSummary>.Failure("invalid_display_name", "Display name is required.");
|
return ServiceResult<UserSummary>.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<UserSummary>.Failure("invalid_password", "Password must be at least 8 characters.");
|
return ServiceResult<UserSummary>.Failure("invalid_password", "Password must be at least 8 characters.");
|
||||||
}
|
}
|
||||||
|
|
||||||
lock (m_Gate)
|
lock (m_Gate)
|
||||||
{
|
{
|
||||||
var username = request.Username.Trim();
|
var trimmedUsername = username.Trim();
|
||||||
var normalizedUsername = NormalizeUsername(username);
|
var normalizedUsername = NormalizeUsername(trimmedUsername);
|
||||||
if (m_UserIdsByUsername.ContainsKey(normalizedUsername))
|
if (m_UserIdsByUsername.ContainsKey(normalizedUsername))
|
||||||
{
|
{
|
||||||
return ServiceResult<UserSummary>.Failure("duplicate_username", "Username is already taken.");
|
return ServiceResult<UserSummary>.Failure("duplicate_username", "Username is already taken.");
|
||||||
@@ -64,14 +64,14 @@ public sealed class GameService : IGameService
|
|||||||
var user = new UserAccount
|
var user = new UserAccount
|
||||||
{
|
{
|
||||||
Id = Guid.NewGuid(),
|
Id = Guid.NewGuid(),
|
||||||
Username = username,
|
Username = trimmedUsername,
|
||||||
UsernameNormalized = normalizedUsername,
|
UsernameNormalized = normalizedUsername,
|
||||||
DisplayName = request.DisplayName.Trim(),
|
DisplayName = displayName.Trim(),
|
||||||
PasswordHash = string.Empty,
|
PasswordHash = string.Empty,
|
||||||
ActiveCharacterId = null
|
ActiveCharacterId = null
|
||||||
};
|
};
|
||||||
|
|
||||||
user.PasswordHash = m_PasswordHasher.HashPassword(user, request.Password);
|
user.PasswordHash = m_PasswordHasher.HashPassword(user, password);
|
||||||
|
|
||||||
m_UsersById[user.Id] = user;
|
m_UsersById[user.Id] = user;
|
||||||
m_UserIdsByUsername[user.UsernameNormalized] = user.Id;
|
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.");
|
return ServiceResult<(UserSummary User, string SessionToken)>.Failure("invalid_credentials", "Invalid username or password.");
|
||||||
}
|
}
|
||||||
|
|
||||||
lock (m_Gate)
|
lock (m_Gate)
|
||||||
{
|
{
|
||||||
var normalizedUsername = NormalizeUsername(request.Username.Trim());
|
var normalizedUsername = NormalizeUsername(username.Trim());
|
||||||
if (!m_UserIdsByUsername.TryGetValue(normalizedUsername, out var userId))
|
if (!m_UserIdsByUsername.TryGetValue(normalizedUsername, out var userId))
|
||||||
{
|
{
|
||||||
return ServiceResult<(UserSummary User, string SessionToken)>.Failure("invalid_credentials", "Invalid username or password.");
|
return ServiceResult<(UserSummary User, string SessionToken)>.Failure("invalid_credentials", "Invalid username or password.");
|
||||||
}
|
}
|
||||||
|
|
||||||
var user = m_UsersById[userId];
|
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)
|
if (verification == PasswordVerificationResult.Failed)
|
||||||
{
|
{
|
||||||
return ServiceResult<(UserSummary User, string SessionToken)>.Failure("invalid_credentials", "Invalid username or password.");
|
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)
|
if (verification == PasswordVerificationResult.SuccessRehashNeeded)
|
||||||
{
|
{
|
||||||
user.PasswordHash = m_PasswordHasher.HashPassword(user, request.Password);
|
user.PasswordHash = m_PasswordHasher.HashPassword(user, password);
|
||||||
}
|
}
|
||||||
|
|
||||||
var session = CreateSession(userId);
|
var session = CreateSession(userId);
|
||||||
@@ -162,14 +162,14 @@ public sealed class GameService : IGameService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public ServiceResult<CampaignSummary> CreateCampaign(string sessionToken, CreateCampaignCommand request)
|
public ServiceResult<CampaignSummary> CreateCampaign(string sessionToken, string name, string rulesetId)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(request.Name))
|
if (string.IsNullOrWhiteSpace(name))
|
||||||
{
|
{
|
||||||
return ServiceResult<CampaignSummary>.Failure("invalid_campaign_name", "Campaign name is required.");
|
return ServiceResult<CampaignSummary>.Failure("invalid_campaign_name", "Campaign name is required.");
|
||||||
}
|
}
|
||||||
|
|
||||||
var ruleset = DiceRules.TryParseRulesetId(request.RulesetId);
|
var ruleset = DiceRules.TryParseRulesetId(rulesetId);
|
||||||
if (ruleset is null)
|
if (ruleset is null)
|
||||||
{
|
{
|
||||||
return ServiceResult<CampaignSummary>.Failure("invalid_ruleset", "Unknown ruleset.");
|
return ServiceResult<CampaignSummary>.Failure("invalid_ruleset", "Unknown ruleset.");
|
||||||
@@ -187,7 +187,7 @@ public sealed class GameService : IGameService
|
|||||||
{
|
{
|
||||||
Id = Guid.NewGuid(),
|
Id = Guid.NewGuid(),
|
||||||
GmUserId = user.Id,
|
GmUserId = user.Id,
|
||||||
Name = request.Name.Trim(),
|
Name = name.Trim(),
|
||||||
Ruleset = ruleset.Value,
|
Ruleset = ruleset.Value,
|
||||||
Version = 1
|
Version = 1
|
||||||
};
|
};
|
||||||
@@ -263,9 +263,9 @@ public sealed class GameService : IGameService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public ServiceResult<CharacterSummary> CreateCharacter(string sessionToken, CreateCharacterCommand request)
|
public ServiceResult<CharacterSummary> CreateCharacter(string sessionToken, string name, Guid campaignId)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(request.Name))
|
if (string.IsNullOrWhiteSpace(name))
|
||||||
{
|
{
|
||||||
return ServiceResult<CharacterSummary>.Failure("invalid_character_name", "Character name is required.");
|
return ServiceResult<CharacterSummary>.Failure("invalid_character_name", "Character name is required.");
|
||||||
}
|
}
|
||||||
@@ -278,7 +278,7 @@ public sealed class GameService : IGameService
|
|||||||
return ServiceResult<CharacterSummary>.Failure("unauthorized", "You must be logged in.");
|
return ServiceResult<CharacterSummary>.Failure("unauthorized", "You must be logged in.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!m_CampaignsById.ContainsKey(request.CampaignId))
|
if (!m_CampaignsById.ContainsKey(campaignId))
|
||||||
{
|
{
|
||||||
return ServiceResult<CharacterSummary>.Failure("campaign_not_found", "Campaign was not found.");
|
return ServiceResult<CharacterSummary>.Failure("campaign_not_found", "Campaign was not found.");
|
||||||
}
|
}
|
||||||
@@ -287,8 +287,8 @@ public sealed class GameService : IGameService
|
|||||||
{
|
{
|
||||||
Id = Guid.NewGuid(),
|
Id = Guid.NewGuid(),
|
||||||
OwnerUserId = user.Id,
|
OwnerUserId = user.Id,
|
||||||
CampaignId = request.CampaignId,
|
CampaignId = campaignId,
|
||||||
Name = request.Name.Trim()
|
Name = name.Trim()
|
||||||
};
|
};
|
||||||
|
|
||||||
m_CharactersById[character.Id] = character;
|
m_CharactersById[character.Id] = character;
|
||||||
@@ -299,9 +299,9 @@ public sealed class GameService : IGameService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public ServiceResult<CharacterSummary> UpdateCharacter(string sessionToken, Guid characterId, UpdateCharacterCommand request)
|
public ServiceResult<CharacterSummary> UpdateCharacter(string sessionToken, Guid characterId, string name, Guid campaignId)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(request.Name))
|
if (string.IsNullOrWhiteSpace(name))
|
||||||
{
|
{
|
||||||
return ServiceResult<CharacterSummary>.Failure("invalid_character_name", "Character name is required.");
|
return ServiceResult<CharacterSummary>.Failure("invalid_character_name", "Character name is required.");
|
||||||
}
|
}
|
||||||
@@ -319,7 +319,7 @@ public sealed class GameService : IGameService
|
|||||||
return ServiceResult<CharacterSummary>.Failure("character_not_found", "Character was not found.");
|
return ServiceResult<CharacterSummary>.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<CharacterSummary>.Failure("campaign_not_found", "Campaign was not found.");
|
return ServiceResult<CharacterSummary>.Failure("campaign_not_found", "Campaign was not found.");
|
||||||
}
|
}
|
||||||
@@ -334,8 +334,8 @@ public sealed class GameService : IGameService
|
|||||||
}
|
}
|
||||||
|
|
||||||
var sourceCampaignId = character.CampaignId;
|
var sourceCampaignId = character.CampaignId;
|
||||||
character.Name = request.Name.Trim();
|
character.Name = name.Trim();
|
||||||
character.CampaignId = request.CampaignId;
|
character.CampaignId = campaignId;
|
||||||
|
|
||||||
TouchCampaignLocked(sourceCampaignId);
|
TouchCampaignLocked(sourceCampaignId);
|
||||||
if (sourceCampaignId != character.CampaignId)
|
if (sourceCampaignId != character.CampaignId)
|
||||||
@@ -399,9 +399,9 @@ public sealed class GameService : IGameService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public ServiceResult<SkillSummary> CreateSkill(string sessionToken, Guid characterId, CreateSkillCommand request)
|
public ServiceResult<SkillSummary> CreateSkill(string sessionToken, Guid characterId, string name, string diceRollDefinition)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(request.Name))
|
if (string.IsNullOrWhiteSpace(name))
|
||||||
{
|
{
|
||||||
return ServiceResult<SkillSummary>.Failure("invalid_skill_name", "Skill name is required.");
|
return ServiceResult<SkillSummary>.Failure("invalid_skill_name", "Skill name is required.");
|
||||||
}
|
}
|
||||||
@@ -425,7 +425,7 @@ public sealed class GameService : IGameService
|
|||||||
return ServiceResult<SkillSummary>.Failure("forbidden", "Only the owner or GM can edit skills.");
|
return ServiceResult<SkillSummary>.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)
|
if (!expressionValidation.Succeeded)
|
||||||
{
|
{
|
||||||
return ServiceResult<SkillSummary>.Failure(expressionValidation.Error!.Code, expressionValidation.Error.Message);
|
return ServiceResult<SkillSummary>.Failure(expressionValidation.Error!.Code, expressionValidation.Error.Message);
|
||||||
@@ -435,7 +435,7 @@ public sealed class GameService : IGameService
|
|||||||
{
|
{
|
||||||
Id = Guid.NewGuid(),
|
Id = Guid.NewGuid(),
|
||||||
CharacterId = character.Id,
|
CharacterId = character.Id,
|
||||||
Name = request.Name.Trim(),
|
Name = name.Trim(),
|
||||||
DiceRollDefinition = expressionValidation.Value!.Canonical
|
DiceRollDefinition = expressionValidation.Value!.Canonical
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -447,9 +447,9 @@ public sealed class GameService : IGameService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public ServiceResult<SkillSummary> UpdateSkill(string sessionToken, Guid skillId, UpdateSkillCommand request)
|
public ServiceResult<SkillSummary> UpdateSkill(string sessionToken, Guid skillId, string name, string diceRollDefinition)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(request.Name))
|
if (string.IsNullOrWhiteSpace(name))
|
||||||
{
|
{
|
||||||
return ServiceResult<SkillSummary>.Failure("invalid_skill_name", "Skill name is required.");
|
return ServiceResult<SkillSummary>.Failure("invalid_skill_name", "Skill name is required.");
|
||||||
}
|
}
|
||||||
@@ -474,13 +474,13 @@ public sealed class GameService : IGameService
|
|||||||
return ServiceResult<SkillSummary>.Failure("forbidden", "Only the owner or GM can edit skills.");
|
return ServiceResult<SkillSummary>.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)
|
if (!expressionValidation.Succeeded)
|
||||||
{
|
{
|
||||||
return ServiceResult<SkillSummary>.Failure(expressionValidation.Error!.Code, expressionValidation.Error.Message);
|
return ServiceResult<SkillSummary>.Failure(expressionValidation.Error!.Code, expressionValidation.Error.Message);
|
||||||
}
|
}
|
||||||
|
|
||||||
skill.Name = request.Name.Trim();
|
skill.Name = name.Trim();
|
||||||
skill.DiceRollDefinition = expressionValidation.Value!.Canonical;
|
skill.DiceRollDefinition = expressionValidation.Value!.Canonical;
|
||||||
TouchCampaignLocked(campaign.Id);
|
TouchCampaignLocked(campaign.Id);
|
||||||
|
|
||||||
@@ -489,7 +489,7 @@ public sealed class GameService : IGameService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public ServiceResult<RollResult> RollSkill(string sessionToken, Guid skillId, RollSkillCommand request)
|
public ServiceResult<RollResult> RollSkill(string sessionToken, Guid skillId, string visibility)
|
||||||
{
|
{
|
||||||
lock (m_Gate)
|
lock (m_Gate)
|
||||||
{
|
{
|
||||||
@@ -517,10 +517,10 @@ public sealed class GameService : IGameService
|
|||||||
return ServiceResult<RollResult>.Failure(parsedExpression.Error!.Code, parsedExpression.Error.Message);
|
return ServiceResult<RollResult>.Failure(parsedExpression.Error!.Code, parsedExpression.Error.Message);
|
||||||
}
|
}
|
||||||
|
|
||||||
var visibility = ParseVisibility(request.Visibility);
|
var parsedVisibility = ParseVisibility(visibility);
|
||||||
if (!visibility.Succeeded)
|
if (!parsedVisibility.Succeeded)
|
||||||
{
|
{
|
||||||
return ServiceResult<RollResult>.Failure(visibility.Error!.Code, visibility.Error.Message);
|
return ServiceResult<RollResult>.Failure(parsedVisibility.Error!.Code, parsedVisibility.Error.Message);
|
||||||
}
|
}
|
||||||
|
|
||||||
var roll = ComputeRoll(parsedExpression.Value!);
|
var roll = ComputeRoll(parsedExpression.Value!);
|
||||||
@@ -531,7 +531,7 @@ public sealed class GameService : IGameService
|
|||||||
CharacterId = character.Id,
|
CharacterId = character.Id,
|
||||||
SkillId = skill.Id,
|
SkillId = skill.Id,
|
||||||
RollerUserId = user.Id,
|
RollerUserId = user.Id,
|
||||||
Visibility = visibility.Value,
|
Visibility = parsedVisibility.Value,
|
||||||
Result = roll.Total,
|
Result = roll.Total,
|
||||||
Breakdown = roll.Breakdown,
|
Breakdown = roll.Breakdown,
|
||||||
TimestampUtc = DateTimeOffset.UtcNow
|
TimestampUtc = DateTimeOffset.UtcNow
|
||||||
|
|||||||
@@ -7,25 +7,25 @@ public interface IGameService
|
|||||||
{
|
{
|
||||||
IReadOnlyList<RulesetDefinition> GetRulesets();
|
IReadOnlyList<RulesetDefinition> GetRulesets();
|
||||||
|
|
||||||
ServiceResult<UserSummary> Register(RegisterCommand request);
|
ServiceResult<UserSummary> Register(string username, string password, string displayName);
|
||||||
ServiceResult<(UserSummary User, string SessionToken)> Login(LoginCommand request);
|
ServiceResult<(UserSummary User, string SessionToken)> Login(string username, string password);
|
||||||
void Logout(string sessionToken);
|
void Logout(string sessionToken);
|
||||||
UserSummary? GetUserBySession(string sessionToken);
|
UserSummary? GetUserBySession(string sessionToken);
|
||||||
ServiceResult<MeResponse> GetMe(string sessionToken);
|
ServiceResult<MeResponse> GetMe(string sessionToken);
|
||||||
|
|
||||||
ServiceResult<CampaignSummary> CreateCampaign(string sessionToken, CreateCampaignCommand request);
|
ServiceResult<CampaignSummary> CreateCampaign(string sessionToken, string name, string rulesetId);
|
||||||
ServiceResult<IReadOnlyList<CampaignSummary>> GetCampaigns(string sessionToken);
|
ServiceResult<IReadOnlyList<CampaignSummary>> GetCampaigns(string sessionToken);
|
||||||
ServiceResult<CampaignDetails> GetCampaign(string sessionToken, Guid campaignId);
|
ServiceResult<CampaignDetails> GetCampaign(string sessionToken, Guid campaignId);
|
||||||
|
|
||||||
ServiceResult<CharacterSummary> CreateCharacter(string sessionToken, CreateCharacterCommand request);
|
ServiceResult<CharacterSummary> CreateCharacter(string sessionToken, string name, Guid campaignId);
|
||||||
ServiceResult<CharacterSummary> UpdateCharacter(string sessionToken, Guid characterId, UpdateCharacterCommand request);
|
ServiceResult<CharacterSummary> UpdateCharacter(string sessionToken, Guid characterId, string name, Guid campaignId);
|
||||||
ServiceResult<bool> ActivateCharacter(string sessionToken, Guid characterId);
|
ServiceResult<bool> ActivateCharacter(string sessionToken, Guid characterId);
|
||||||
ServiceResult<IReadOnlyList<CharacterSummary>> GetCurrentCampaignCharacters(string sessionToken);
|
ServiceResult<IReadOnlyList<CharacterSummary>> GetCurrentCampaignCharacters(string sessionToken);
|
||||||
|
|
||||||
ServiceResult<SkillSummary> CreateSkill(string sessionToken, Guid characterId, CreateSkillCommand request);
|
ServiceResult<SkillSummary> CreateSkill(string sessionToken, Guid characterId, string name, string diceRollDefinition);
|
||||||
ServiceResult<SkillSummary> UpdateSkill(string sessionToken, Guid skillId, UpdateSkillCommand request);
|
ServiceResult<SkillSummary> UpdateSkill(string sessionToken, Guid skillId, string name, string diceRollDefinition);
|
||||||
|
|
||||||
ServiceResult<RollResult> RollSkill(string sessionToken, Guid skillId, RollSkillCommand request);
|
ServiceResult<RollResult> RollSkill(string sessionToken, Guid skillId, string visibility);
|
||||||
ServiceResult<IReadOnlyList<CampaignLogEntry>> GetCampaignLog(string sessionToken, Guid campaignId);
|
ServiceResult<IReadOnlyList<CampaignLogEntry>> GetCampaignLog(string sessionToken, Guid campaignId);
|
||||||
|
|
||||||
ServiceResult<long> GetCampaignVersion(string sessionToken, Guid campaignId);
|
ServiceResult<long> GetCampaignVersion(string sessionToken, Guid campaignId);
|
||||||
|
|||||||
@@ -1,13 +1 @@
|
|||||||
namespace RpgRoller.Services;
|
// Intentionally left blank after removing service command records.
|
||||||
|
|
||||||
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);
|
|
||||||
|
|||||||
2
TECH.md
2
TECH.md
@@ -14,7 +14,7 @@
|
|||||||
- Generated client output: `RpgRoller/wwwroot/generated/api-client.js`
|
- Generated client output: `RpgRoller/wwwroot/generated/api-client.js`
|
||||||
- Local CI parity entrypoint: `scripts/ci-local.ps1`
|
- Local CI parity entrypoint: `scripts/ci-local.ps1`
|
||||||
- API endpoint modules: `RpgRoller/Api/*Endpoints.cs` + shared session/auth helpers
|
- 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 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.
|
- Current frontend features: authenticated campaign workspace with live log updates and full roll workflow controls.
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user