Persist backend state with EF Core SQLite
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using RpgRoller.Contracts;
|
||||
using RpgRoller.Data;
|
||||
using RpgRoller.Domain;
|
||||
using RpgRoller.Services;
|
||||
|
||||
@@ -10,7 +12,8 @@ public sealed class GameServiceTests
|
||||
[Fact]
|
||||
public void Register_ValidatesRequiredFieldsAndDuplicates()
|
||||
{
|
||||
var service = CreateService();
|
||||
using var harness = CreateHarness();
|
||||
var service = harness.Service;
|
||||
|
||||
var invalidUsername = service.Register(new RegisterRequest("", "Password123", "Display"));
|
||||
var invalidDisplay = service.Register(new RegisterRequest("user", "Password123", ""));
|
||||
@@ -28,7 +31,8 @@ public sealed class GameServiceTests
|
||||
[Fact]
|
||||
public void Login_ValidatesCredentialsAndSessionLookup()
|
||||
{
|
||||
var service = CreateService();
|
||||
using var harness = CreateHarness();
|
||||
var service = harness.Service;
|
||||
service.Register(new RegisterRequest("user", "Password123", "Display"));
|
||||
|
||||
var invalidUser = service.Login(new LoginRequest("missing", "Password123"));
|
||||
@@ -46,10 +50,25 @@ public sealed class GameServiceTests
|
||||
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()
|
||||
{
|
||||
var service = CreateService();
|
||||
using var harness = CreateHarness();
|
||||
var service = harness.Service;
|
||||
|
||||
var unauthorizedCampaign = service.CreateCampaign("missing", new CreateCampaignRequest("Name", "d6"));
|
||||
Assert.False(unauthorizedCampaign.Succeeded);
|
||||
@@ -83,7 +102,8 @@ public sealed class GameServiceTests
|
||||
[Fact]
|
||||
public void CharacterSkillAndRollOperations_CheckAuthorizationAndValidationBranches()
|
||||
{
|
||||
var service = CreateService(3, 4, 5, 6);
|
||||
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"));
|
||||
@@ -152,7 +172,8 @@ public sealed class GameServiceTests
|
||||
[Fact]
|
||||
public void CurrentCampaignCharacters_ReturnsNoActiveCharacterWhenUnset()
|
||||
{
|
||||
var service = CreateService();
|
||||
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;
|
||||
|
||||
@@ -163,7 +184,8 @@ public sealed class GameServiceTests
|
||||
[Fact]
|
||||
public void GetCampaigns_ReturnsOwnedAndParticipatingCampaigns()
|
||||
{
|
||||
var service = CreateService();
|
||||
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;
|
||||
@@ -180,6 +202,105 @@ public sealed class GameServiceTests
|
||||
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()
|
||||
{
|
||||
@@ -217,9 +338,23 @@ public sealed class GameServiceTests
|
||||
}
|
||||
}
|
||||
|
||||
private static GameService CreateService(params int[] rollValues)
|
||||
private static ServiceHarness CreateHarness(params int[] rollValues)
|
||||
{
|
||||
return new GameService(new PasswordHasher<UserAccount>(), new FixedDiceRoller(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)
|
||||
@@ -244,4 +379,41 @@ public sealed class GameServiceTests
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
using System.Net;
|
||||
using System.Net.Http.Json;
|
||||
using Microsoft.AspNetCore.Mvc.Testing;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using RpgRoller.Contracts;
|
||||
using RpgRoller.Data;
|
||||
using RpgRoller.Services;
|
||||
|
||||
namespace RpgRoller.Tests;
|
||||
@@ -201,6 +203,12 @@ public sealed class UnitTest1 : IClassFixture<WebApplicationFactory<Program>>
|
||||
{
|
||||
services.RemoveAll<IDiceRoller>();
|
||||
services.AddSingleton<IDiceRoller>(new FixedDiceRoller(rollValues));
|
||||
|
||||
services.RemoveAll<DbContextOptions<RpgRollerDbContext>>();
|
||||
services.RemoveAll<RpgRollerDbContext>();
|
||||
|
||||
var dbPath = Path.Combine(Path.GetTempPath(), $"rpgroller-tests-{Guid.NewGuid():N}.db");
|
||||
services.AddDbContext<RpgRollerDbContext>(options => options.UseSqlite($"Data Source={dbPath}"));
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user