420 lines
19 KiB
C#
420 lines
19 KiB
C#
using Microsoft.AspNetCore.Identity;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using RpgRoller.Contracts;
|
|
using RpgRoller.Data;
|
|
using RpgRoller.Domain;
|
|
using RpgRoller.Services;
|
|
|
|
namespace RpgRoller.Tests;
|
|
|
|
public sealed class GameServiceTests
|
|
{
|
|
[Fact]
|
|
public void Register_ValidatesRequiredFieldsAndDuplicates()
|
|
{
|
|
using var harness = CreateHarness();
|
|
var service = harness.Service;
|
|
|
|
var invalidUsername = service.Register(new RegisterRequest("", "Password123", "Display"));
|
|
var invalidDisplay = service.Register(new RegisterRequest("user", "Password123", ""));
|
|
var invalidPassword = service.Register(new RegisterRequest("user", "short", "Display"));
|
|
var valid = service.Register(new RegisterRequest("user", "Password123", "Display"));
|
|
var duplicate = service.Register(new RegisterRequest("user", "Password123", "Display 2"));
|
|
|
|
Assert.False(invalidUsername.Succeeded);
|
|
Assert.False(invalidDisplay.Succeeded);
|
|
Assert.False(invalidPassword.Succeeded);
|
|
Assert.True(valid.Succeeded);
|
|
Assert.False(duplicate.Succeeded);
|
|
}
|
|
|
|
[Fact]
|
|
public void Login_ValidatesCredentialsAndSessionLookup()
|
|
{
|
|
using var harness = CreateHarness();
|
|
var service = harness.Service;
|
|
service.Register(new RegisterRequest("user", "Password123", "Display"));
|
|
|
|
var invalidUser = service.Login(new LoginRequest("missing", "Password123"));
|
|
var invalidPassword = service.Login(new LoginRequest("user", "bad-password"));
|
|
var valid = service.Login(new LoginRequest("user", "Password123"));
|
|
|
|
Assert.False(invalidUser.Succeeded);
|
|
Assert.False(invalidPassword.Succeeded);
|
|
Assert.True(valid.Succeeded);
|
|
|
|
var sessionUser = service.GetUserBySession(valid.Value.SessionToken);
|
|
Assert.NotNull(sessionUser);
|
|
|
|
service.Logout(valid.Value.SessionToken);
|
|
Assert.Null(service.GetUserBySession(valid.Value.SessionToken));
|
|
}
|
|
|
|
[Fact]
|
|
public void Login_RehashesPasswordWhenHasherRequestsIt()
|
|
{
|
|
var hasher = new RehashingPasswordHasher();
|
|
using var harness = CreateHarness(hasher);
|
|
var service = harness.Service;
|
|
|
|
service.Register(new RegisterRequest("user", "Password123", "Display"));
|
|
var login = service.Login(new LoginRequest("user", "Password123"));
|
|
|
|
Assert.True(login.Succeeded);
|
|
Assert.Equal(2, hasher.HashCalls);
|
|
}
|
|
|
|
[Fact]
|
|
public void CampaignAndCharacterOperations_CheckUnauthorizedAndNotFoundCases()
|
|
{
|
|
using var harness = CreateHarness();
|
|
var service = harness.Service;
|
|
|
|
var unauthorizedCampaign = service.CreateCampaign("missing", new CreateCampaignRequest("Name", "d6"));
|
|
Assert.False(unauthorizedCampaign.Succeeded);
|
|
|
|
service.Register(new RegisterRequest("gm", "Password123", "GM"));
|
|
var gmSession = GetValue(service.Login(new LoginRequest("gm", "Password123"))).SessionToken;
|
|
var campaign = GetValue(service.CreateCampaign(gmSession, new CreateCampaignRequest("Name", "d6")));
|
|
|
|
var invalidRuleset = service.CreateCampaign(gmSession, new CreateCampaignRequest("Name 2", "unknown"));
|
|
Assert.False(invalidRuleset.Succeeded);
|
|
|
|
var noCampaignCharacter = service.CreateCharacter(gmSession, new CreateCharacterRequest("Hero", Guid.NewGuid()));
|
|
Assert.False(noCampaignCharacter.Succeeded);
|
|
|
|
var character = GetValue(service.CreateCharacter(gmSession, new CreateCharacterRequest("Hero", campaign.Id)));
|
|
|
|
var missingCharacterActivate = service.ActivateCharacter(gmSession, Guid.NewGuid());
|
|
Assert.False(missingCharacterActivate.Succeeded);
|
|
|
|
var activateSuccess = service.ActivateCharacter(gmSession, character.Id);
|
|
Assert.True(activateSuccess.Succeeded);
|
|
|
|
var currentCharacters = service.GetCurrentCampaignCharacters(gmSession);
|
|
Assert.True(currentCharacters.Succeeded);
|
|
Assert.Single(GetValue(currentCharacters));
|
|
|
|
var missingCampaignGet = service.GetCampaign(gmSession, Guid.NewGuid());
|
|
Assert.False(missingCampaignGet.Succeeded);
|
|
}
|
|
|
|
[Fact]
|
|
public void CharacterSkillAndRollOperations_CheckAuthorizationAndValidationBranches()
|
|
{
|
|
using var harness = CreateHarness(3, 4, 5, 6);
|
|
var service = harness.Service;
|
|
service.Register(new RegisterRequest("gm", "Password123", "GM"));
|
|
service.Register(new RegisterRequest("owner", "Password123", "Owner"));
|
|
service.Register(new RegisterRequest("other", "Password123", "Other"));
|
|
|
|
var gmSession = GetValue(service.Login(new LoginRequest("gm", "Password123"))).SessionToken;
|
|
var ownerSession = GetValue(service.Login(new LoginRequest("owner", "Password123"))).SessionToken;
|
|
var otherSession = GetValue(service.Login(new LoginRequest("other", "Password123"))).SessionToken;
|
|
|
|
var campaign = GetValue(service.CreateCampaign(gmSession, new CreateCampaignRequest("Main", "dnd5e")));
|
|
var character = GetValue(service.CreateCharacter(ownerSession, new CreateCharacterRequest("Owner Char", campaign.Id)));
|
|
|
|
var noPermissionUpdate = service.UpdateCharacter(otherSession, character.Id, new UpdateCharacterRequest("Renamed", campaign.Id));
|
|
Assert.False(noPermissionUpdate.Succeeded);
|
|
|
|
var invalidCharacterName = service.UpdateCharacter(ownerSession, character.Id, new UpdateCharacterRequest("", campaign.Id));
|
|
Assert.False(invalidCharacterName.Succeeded);
|
|
|
|
var missingTargetCampaign = service.UpdateCharacter(ownerSession, character.Id, new UpdateCharacterRequest("Renamed", Guid.NewGuid()));
|
|
Assert.False(missingTargetCampaign.Succeeded);
|
|
|
|
var noSkillName = service.CreateSkill(ownerSession, character.Id, new CreateSkillRequest("", "1d20"));
|
|
Assert.False(noSkillName.Succeeded);
|
|
|
|
var invalidExpression = service.CreateSkill(ownerSession, character.Id, new CreateSkillRequest("Skill", "5D+4"));
|
|
Assert.False(invalidExpression.Succeeded);
|
|
|
|
var skill = GetValue(service.CreateSkill(ownerSession, character.Id, new CreateSkillRequest("Skill", "1d20+2")));
|
|
|
|
var missingSkillUpdate = service.UpdateSkill(ownerSession, Guid.NewGuid(), new UpdateSkillRequest("X", "1d20"));
|
|
Assert.False(missingSkillUpdate.Succeeded);
|
|
|
|
var forbiddenSkillUpdate = service.UpdateSkill(otherSession, skill.Id, new UpdateSkillRequest("X", "1d20"));
|
|
Assert.False(forbiddenSkillUpdate.Succeeded);
|
|
|
|
var gmSkillUpdate = service.UpdateSkill(gmSession, skill.Id, new UpdateSkillRequest("GM Edit", "2d6+1"));
|
|
Assert.True(gmSkillUpdate.Succeeded);
|
|
|
|
var missingRoll = service.RollSkill(ownerSession, Guid.NewGuid(), new RollSkillRequest("public"));
|
|
Assert.False(missingRoll.Succeeded);
|
|
|
|
var invalidVisibility = service.RollSkill(ownerSession, skill.Id, new RollSkillRequest("hidden"));
|
|
Assert.False(invalidVisibility.Succeeded);
|
|
|
|
var forbiddenRoll = service.RollSkill(otherSession, skill.Id, new RollSkillRequest("public"));
|
|
Assert.False(forbiddenRoll.Succeeded);
|
|
|
|
var privateRoll = service.RollSkill(ownerSession, skill.Id, new RollSkillRequest("private"));
|
|
var publicRoll = service.RollSkill(ownerSession, skill.Id, new RollSkillRequest("public"));
|
|
Assert.True(privateRoll.Succeeded);
|
|
Assert.True(publicRoll.Succeeded);
|
|
|
|
var ownerLog = service.GetCampaignLog(ownerSession, campaign.Id);
|
|
var gmLog = service.GetCampaignLog(gmSession, campaign.Id);
|
|
var outsiderLog = service.GetCampaignLog(otherSession, campaign.Id);
|
|
|
|
Assert.Equal(2, GetValue(ownerLog).Count);
|
|
Assert.Equal(2, GetValue(gmLog).Count);
|
|
Assert.False(outsiderLog.Succeeded);
|
|
|
|
var version = service.GetCampaignVersion(ownerSession, campaign.Id);
|
|
var missingVersion = service.GetCampaignVersion(ownerSession, Guid.NewGuid());
|
|
Assert.True(version.Succeeded);
|
|
Assert.False(missingVersion.Succeeded);
|
|
}
|
|
|
|
[Fact]
|
|
public void CurrentCampaignCharacters_ReturnsNoActiveCharacterWhenUnset()
|
|
{
|
|
using var harness = CreateHarness();
|
|
var service = harness.Service;
|
|
service.Register(new RegisterRequest("user", "Password123", "User"));
|
|
var sessionToken = GetValue(service.Login(new LoginRequest("user", "Password123"))).SessionToken;
|
|
|
|
var result = service.GetCurrentCampaignCharacters(sessionToken);
|
|
Assert.False(result.Succeeded);
|
|
}
|
|
|
|
[Fact]
|
|
public void GetCampaigns_ReturnsOwnedAndParticipatingCampaigns()
|
|
{
|
|
using var harness = CreateHarness();
|
|
var service = harness.Service;
|
|
service.Register(new RegisterRequest("gm", "Password123", "GM"));
|
|
service.Register(new RegisterRequest("player", "Password123", "Player"));
|
|
var gmSession = GetValue(service.Login(new LoginRequest("gm", "Password123"))).SessionToken;
|
|
var playerSession = GetValue(service.Login(new LoginRequest("player", "Password123"))).SessionToken;
|
|
|
|
var gmCampaign = GetValue(service.CreateCampaign(gmSession, new CreateCampaignRequest("Owned", "d6")));
|
|
_ = GetValue(service.CreateCampaign(gmSession, new CreateCampaignRequest("Owned 2", "d6")));
|
|
_ = service.CreateCharacter(playerSession, new CreateCharacterRequest("Joiner", gmCampaign.Id));
|
|
|
|
var playerCampaigns = service.GetCampaigns(playerSession);
|
|
Assert.True(playerCampaigns.Succeeded);
|
|
var campaigns = GetValue(playerCampaigns);
|
|
Assert.Single(campaigns);
|
|
Assert.Equal(gmCampaign.Id, campaigns[0].Id);
|
|
}
|
|
|
|
[Fact]
|
|
public void ServiceGuardAndPersistenceBranches_AreHandled()
|
|
{
|
|
using var harness = CreateHarness(2, 3, 4);
|
|
var service = harness.Service;
|
|
|
|
var invalidCredentials = service.Login(new LoginRequest("", ""));
|
|
Assert.False(invalidCredentials.Succeeded);
|
|
|
|
service.Logout("missing-session");
|
|
|
|
service.Register(new RegisterRequest("gm", "Password123", "GM"));
|
|
service.Register(new RegisterRequest("owner", "Password123", "Owner"));
|
|
service.Register(new RegisterRequest("other", "Password123", "Other"));
|
|
|
|
var gmSession = GetValue(service.Login(new LoginRequest("gm", "Password123"))).SessionToken;
|
|
var ownerSession = GetValue(service.Login(new LoginRequest("owner", "Password123"))).SessionToken;
|
|
var otherSession = GetValue(service.Login(new LoginRequest("other", "Password123"))).SessionToken;
|
|
|
|
var campaign = GetValue(service.CreateCampaign(gmSession, new CreateCampaignRequest("Main", "d6")));
|
|
var ownerCharacter = GetValue(service.CreateCharacter(ownerSession, new CreateCharacterRequest("Owner Character", campaign.Id)));
|
|
|
|
Assert.False(service.GetMe(string.Empty).Succeeded);
|
|
Assert.False(service.CreateCampaign(gmSession, new CreateCampaignRequest("", "d6")).Succeeded);
|
|
Assert.False(service.GetCampaigns(string.Empty).Succeeded);
|
|
Assert.False(service.CreateCharacter(ownerSession, new CreateCharacterRequest("", campaign.Id)).Succeeded);
|
|
Assert.False(service.CreateCharacter(string.Empty, new CreateCharacterRequest("Name", campaign.Id)).Succeeded);
|
|
Assert.False(service.UpdateCharacter(string.Empty, ownerCharacter.Id, new UpdateCharacterRequest("Renamed", campaign.Id)).Succeeded);
|
|
Assert.False(service.UpdateCharacter(ownerSession, Guid.NewGuid(), new UpdateCharacterRequest("Renamed", campaign.Id)).Succeeded);
|
|
Assert.False(service.ActivateCharacter(string.Empty, ownerCharacter.Id).Succeeded);
|
|
Assert.False(service.ActivateCharacter(gmSession, ownerCharacter.Id).Succeeded);
|
|
Assert.False(service.GetCurrentCampaignCharacters(string.Empty).Succeeded);
|
|
Assert.False(service.CreateSkill(string.Empty, ownerCharacter.Id, new CreateSkillRequest("Stealth", "2D+1")).Succeeded);
|
|
Assert.False(service.CreateSkill(ownerSession, Guid.NewGuid(), new CreateSkillRequest("Stealth", "2D+1")).Succeeded);
|
|
Assert.False(service.CreateSkill(otherSession, ownerCharacter.Id, new CreateSkillRequest("Stealth", "2D+1")).Succeeded);
|
|
|
|
var ownerUser = harness.Db.Users.Single(u => u.UsernameNormalized == "OWNER");
|
|
ownerUser.ActiveCharacterId = Guid.NewGuid();
|
|
harness.Db.SaveChanges();
|
|
|
|
var staleMe = GetValue(service.GetMe(ownerSession));
|
|
Assert.Null(staleMe.ActiveCharacterId);
|
|
Assert.Null(staleMe.CurrentCampaignId);
|
|
|
|
Assert.True(service.ActivateCharacter(ownerSession, ownerCharacter.Id).Succeeded);
|
|
var activeMe = GetValue(service.GetMe(ownerSession));
|
|
Assert.Equal(ownerCharacter.Id, activeMe.ActiveCharacterId);
|
|
Assert.Equal(campaign.Id, activeMe.CurrentCampaignId);
|
|
|
|
var staleOwner = harness.Db.Users.Single(u => u.Id == ownerUser.Id);
|
|
staleOwner.ActiveCharacterId = Guid.NewGuid();
|
|
harness.Db.SaveChanges();
|
|
|
|
var staleCurrentCampaign = service.GetCurrentCampaignCharacters(ownerSession);
|
|
Assert.False(staleCurrentCampaign.Succeeded);
|
|
Assert.Null(harness.Db.Users.Single(u => u.Id == ownerUser.Id).ActiveCharacterId);
|
|
|
|
var skill = GetValue(service.CreateSkill(ownerSession, ownerCharacter.Id, new CreateSkillRequest("Stealth", "2D+1")));
|
|
Assert.False(service.UpdateSkill(ownerSession, skill.Id, new UpdateSkillRequest("", "2D+1")).Succeeded);
|
|
Assert.False(service.UpdateSkill(string.Empty, skill.Id, new UpdateSkillRequest("Stealth", "2D+1")).Succeeded);
|
|
Assert.False(service.UpdateSkill(ownerSession, skill.Id, new UpdateSkillRequest("Stealth", "bad")).Succeeded);
|
|
Assert.False(service.RollSkill(string.Empty, skill.Id, new RollSkillRequest("public")).Succeeded);
|
|
|
|
var mutableSkill = harness.Db.Skills.Single(s => s.Id == skill.Id);
|
|
mutableSkill.DiceRollDefinition = "bad";
|
|
harness.Db.SaveChanges();
|
|
|
|
Assert.False(service.RollSkill(ownerSession, skill.Id, new RollSkillRequest("public")).Succeeded);
|
|
Assert.False(service.GetCampaignLog(string.Empty, campaign.Id).Succeeded);
|
|
}
|
|
|
|
[Fact]
|
|
public void GetCampaign_ForNonGm_ReturnsOnlyOwnedCharactersAndSkills()
|
|
{
|
|
using var harness = CreateHarness();
|
|
var service = harness.Service;
|
|
|
|
service.Register(new RegisterRequest("gm", "Password123", "GM"));
|
|
service.Register(new RegisterRequest("owner", "Password123", "Owner"));
|
|
service.Register(new RegisterRequest("other", "Password123", "Other"));
|
|
|
|
var gmSession = GetValue(service.Login(new LoginRequest("gm", "Password123"))).SessionToken;
|
|
var ownerSession = GetValue(service.Login(new LoginRequest("owner", "Password123"))).SessionToken;
|
|
var otherSession = GetValue(service.Login(new LoginRequest("other", "Password123"))).SessionToken;
|
|
|
|
var campaign = GetValue(service.CreateCampaign(gmSession, new CreateCampaignRequest("Main", "d6")));
|
|
var ownerCharacter = GetValue(service.CreateCharacter(ownerSession, new CreateCharacterRequest("Owner Character", campaign.Id)));
|
|
var otherCharacter = GetValue(service.CreateCharacter(otherSession, new CreateCharacterRequest("Other Character", campaign.Id)));
|
|
|
|
var ownerSkill = GetValue(service.CreateSkill(ownerSession, ownerCharacter.Id, new CreateSkillRequest("Stealth", "2D+1")));
|
|
_ = GetValue(service.CreateSkill(otherSession, otherCharacter.Id, new CreateSkillRequest("Perception", "1D+2")));
|
|
|
|
var ownerView = GetValue(service.GetCampaign(ownerSession, campaign.Id));
|
|
Assert.Single(ownerView.Characters);
|
|
Assert.Equal(ownerCharacter.Id, ownerView.Characters[0].Id);
|
|
Assert.Single(ownerView.Skills);
|
|
Assert.Equal(ownerSkill.Id, ownerView.Skills[0].Id);
|
|
}
|
|
|
|
[Fact]
|
|
public void DiceRules_CoversParsingAndMappingBranches()
|
|
{
|
|
Assert.Equal(RulesetKind.D6, DiceRules.TryParseRulesetId("d6"));
|
|
Assert.Equal(RulesetKind.Dnd5e, DiceRules.TryParseRulesetId("dnd5e"));
|
|
Assert.Null(DiceRules.TryParseRulesetId("unknown"));
|
|
|
|
var d6 = DiceRules.ParseExpression(RulesetKind.D6, "5D+4");
|
|
var dnd = DiceRules.ParseExpression(RulesetKind.Dnd5e, "2d12+2");
|
|
var badFormat = DiceRules.ParseExpression(RulesetKind.Dnd5e, "abc");
|
|
var tooManyDice = DiceRules.ParseExpression(RulesetKind.D6, "51D+1");
|
|
var tooManySides = DiceRules.ParseExpression(RulesetKind.Dnd5e, "1d1001");
|
|
var tooLargeModifier = DiceRules.ParseExpression(RulesetKind.Dnd5e, "1d20+1001");
|
|
|
|
Assert.True(d6.Succeeded);
|
|
Assert.True(dnd.Succeeded);
|
|
Assert.False(badFormat.Succeeded);
|
|
Assert.False(tooManyDice.Succeeded);
|
|
Assert.False(tooManySides.Succeeded);
|
|
Assert.False(tooLargeModifier.Succeeded);
|
|
|
|
Assert.Equal("d6", DiceRules.ToRulesetId(RulesetKind.D6));
|
|
Assert.Equal("dnd5e", DiceRules.ToRulesetId(RulesetKind.Dnd5e));
|
|
Assert.Throws<ArgumentOutOfRangeException>(() => DiceRules.ToRulesetId((RulesetKind)99));
|
|
}
|
|
|
|
[Fact]
|
|
public void RandomDiceRoller_ProducesValueWithinRange()
|
|
{
|
|
var roller = new RandomDiceRoller();
|
|
for (var i = 0; i < 64; i += 1)
|
|
{
|
|
var value = roller.Roll(12);
|
|
Assert.InRange(value, 1, 12);
|
|
}
|
|
}
|
|
|
|
private static ServiceHarness CreateHarness(params int[] rollValues)
|
|
{
|
|
return CreateHarness(new PasswordHasher<UserAccount>(), rollValues);
|
|
}
|
|
|
|
private static ServiceHarness CreateHarness(IPasswordHasher<UserAccount> passwordHasher, params int[] rollValues)
|
|
{
|
|
var dbPath = Path.Combine(Path.GetTempPath(), $"rpgroller-servicetests-{Guid.NewGuid():N}.db");
|
|
var options = new DbContextOptionsBuilder<RpgRollerDbContext>()
|
|
.UseSqlite($"Data Source={dbPath}")
|
|
.Options;
|
|
|
|
var db = new RpgRollerDbContext(options);
|
|
db.Database.EnsureCreated();
|
|
|
|
var service = new GameService(db, passwordHasher, new FixedDiceRoller(rollValues));
|
|
return new ServiceHarness(service, db);
|
|
}
|
|
|
|
private static T GetValue<T>(ServiceResult<T> result)
|
|
{
|
|
Assert.True(result.Succeeded);
|
|
Assert.NotNull(result.Value);
|
|
return result.Value!;
|
|
}
|
|
|
|
private sealed class FixedDiceRoller : IDiceRoller
|
|
{
|
|
private readonly Queue<int> m_Values;
|
|
|
|
public FixedDiceRoller(IEnumerable<int> values)
|
|
{
|
|
m_Values = new Queue<int>(values);
|
|
}
|
|
|
|
public int Roll(int sides)
|
|
{
|
|
var next = m_Values.Count > 0 ? m_Values.Dequeue() : 1;
|
|
return Math.Clamp(next, 1, sides);
|
|
}
|
|
}
|
|
|
|
private sealed class ServiceHarness : IDisposable
|
|
{
|
|
private readonly RpgRollerDbContext m_Db;
|
|
|
|
public ServiceHarness(GameService service, RpgRollerDbContext db)
|
|
{
|
|
Service = service;
|
|
m_Db = db;
|
|
}
|
|
|
|
public GameService Service { get; }
|
|
public RpgRollerDbContext Db => m_Db;
|
|
|
|
public void Dispose()
|
|
{
|
|
m_Db.Dispose();
|
|
}
|
|
}
|
|
|
|
private sealed class RehashingPasswordHasher : IPasswordHasher<UserAccount>
|
|
{
|
|
public int HashCalls { get; private set; }
|
|
|
|
public string HashPassword(UserAccount user, string password)
|
|
{
|
|
HashCalls += 1;
|
|
return $"hash:{HashCalls}:{password}";
|
|
}
|
|
|
|
public PasswordVerificationResult VerifyHashedPassword(UserAccount user, string hashedPassword, string providedPassword)
|
|
{
|
|
return providedPassword == "Password123"
|
|
? PasswordVerificationResult.SuccessRehashNeeded
|
|
: PasswordVerificationResult.Failed;
|
|
}
|
|
}
|
|
}
|