Code cleanup

This commit is contained in:
2026-02-26 11:08:02 +01:00
parent 9036a3a157
commit e7114d8798
72 changed files with 1069 additions and 1604 deletions

View File

@@ -1,11 +1,8 @@
using Microsoft.AspNetCore.Mvc.Testing;
namespace RpgRoller.Tests;
public sealed class AuthApiTests : ApiTestBase
{
public AuthApiTests(WebApplicationFactory<Program> factory)
: base(factory)
public AuthApiTests(WebApplicationFactory<Program> factory) : base(factory)
{
}
@@ -13,7 +10,7 @@ public sealed class AuthApiTests : ApiTestBase
public async Task RegisterLoginAndMeFlow_WorksWithDuplicateUsernameGuard()
{
using var factory = CreateFactory(4, 4, 4);
using var client = factory.CreateClient(new WebApplicationFactoryClientOptions { AllowAutoRedirect = false });
using var client = factory.CreateClient(new() { AllowAutoRedirect = false });
var registerResult = await RegisterAsync(client, "alice", "Password123", "Alice");
Assert.Equal("alice", registerResult.Username);
@@ -32,4 +29,4 @@ public sealed class AuthApiTests : ApiTestBase
var invalidLogin = await client.PostAsJsonAsync("/api/auth/login", new LoginRequest("alice", "wrong-password"));
Assert.Equal(HttpStatusCode.BadRequest, invalidLogin.StatusCode);
}
}
}

View File

@@ -1,11 +1,8 @@
using Microsoft.AspNetCore.Mvc.Testing;
namespace RpgRoller.Tests;
public sealed class CampaignApiTests : ApiTestBase
{
public CampaignApiTests(WebApplicationFactory<Program> factory)
: base(factory)
public CampaignApiTests(WebApplicationFactory<Program> factory) : base(factory)
{
}
@@ -13,44 +10,30 @@ public sealed class CampaignApiTests : ApiTestBase
public async Task CampaignCharacterAndSkillFlow_EnforcesRulesetValidation()
{
using var factory = CreateFactory(6, 6, 6);
using var gmClient = factory.CreateClient(new WebApplicationFactoryClientOptions { AllowAutoRedirect = false });
using var gmClient = factory.CreateClient(new() { AllowAutoRedirect = false });
await RegisterAsync(gmClient, "gm", "Password123", "Game Master");
await LoginAsync(gmClient, "gm", "Password123");
var campaign = await PostAsync<CreateCampaignRequest, CampaignSummary>(
gmClient,
"/api/campaigns",
new CreateCampaignRequest("Alpha Campaign", "dnd5e"));
var campaign = await PostAsync<CreateCampaignRequest, CampaignSummary>(gmClient, "/api/campaigns", new("Alpha Campaign", "dnd5e"));
var gmCharacter = await PostAsync<CreateCharacterRequest, CharacterSummary>(
gmClient,
"/api/characters",
new CreateCharacterRequest("Arin", campaign.Id));
var gmCharacter = await PostAsync<CreateCharacterRequest, CharacterSummary>(gmClient, "/api/characters", new("Arin", campaign.Id));
var activateResponse = await gmClient.PostAsync($"/api/characters/{gmCharacter.Id}/activate", null);
Assert.Equal(HttpStatusCode.OK, activateResponse.StatusCode);
var createdSkill = await PostAsync<CreateSkillRequest, SkillSummary>(
gmClient,
$"/api/characters/{gmCharacter.Id}/skills",
new CreateSkillRequest("Arcana", "2d12+2", 0, false));
var createdSkill = await PostAsync<CreateSkillRequest, SkillSummary>(gmClient, $"/api/characters/{gmCharacter.Id}/skills", new("Arcana", "2d12+2", 0, false));
Assert.Equal("2d12+2", createdSkill.DiceRollDefinition);
Assert.Equal(0, createdSkill.WildDice);
Assert.False(createdSkill.AllowFumble);
var updatedSkill = await PutAsync<UpdateSkillRequest, SkillSummary>(
gmClient,
$"/api/skills/{createdSkill.Id}",
new UpdateSkillRequest("Arcana Mastery", "2d12+3", 0, false));
var updatedSkill = await PutAsync<UpdateSkillRequest, SkillSummary>(gmClient, $"/api/skills/{createdSkill.Id}", new("Arcana Mastery", "2d12+3", 0, false));
Assert.Equal("Arcana Mastery", updatedSkill.Name);
Assert.Equal("2d12+3", updatedSkill.DiceRollDefinition);
Assert.Equal(0, updatedSkill.WildDice);
Assert.False(updatedSkill.AllowFumble);
var invalidSkill = await gmClient.PostAsJsonAsync(
$"/api/characters/{gmCharacter.Id}/skills",
new CreateSkillRequest("Broken", "5D+4", 0, false));
var invalidSkill = await gmClient.PostAsJsonAsync($"/api/characters/{gmCharacter.Id}/skills", new CreateSkillRequest("Broken", "5D+4", 0, false));
Assert.Equal(HttpStatusCode.BadRequest, invalidSkill.StatusCode);
var details = await GetAsync<CampaignDetails>(gmClient, $"/api/campaigns/{campaign.Id}");
@@ -62,17 +45,11 @@ public sealed class CampaignApiTests : ApiTestBase
Assert.Single(currentCampaignCharacters);
Assert.Equal(gmCharacter.Id, currentCampaignCharacters[0].Id);
var otherCampaign = await PostAsync<CreateCampaignRequest, CampaignSummary>(
gmClient,
"/api/campaigns",
new CreateCampaignRequest("Beta Campaign", "d6"));
var otherCampaign = await PostAsync<CreateCampaignRequest, CampaignSummary>(gmClient, "/api/campaigns", new("Beta Campaign", "d6"));
var updatedCharacter = await PutAsync<UpdateCharacterRequest, CharacterSummary>(
gmClient,
$"/api/characters/{gmCharacter.Id}",
new UpdateCharacterRequest("Arin Updated", otherCampaign.Id));
var updatedCharacter = await PutAsync<UpdateCharacterRequest, CharacterSummary>(gmClient, $"/api/characters/{gmCharacter.Id}", new("Arin Updated", otherCampaign.Id));
Assert.Equal("Arin Updated", updatedCharacter.Name);
Assert.Equal(otherCampaign.Id, updatedCharacter.CampaignId);
}
}
}

View File

@@ -1,11 +1,8 @@
using Microsoft.AspNetCore.Mvc.Testing;
namespace RpgRoller.Tests;
public sealed class FrontendHostTests : ApiTestBase
{
public FrontendHostTests(WebApplicationFactory<Program> factory)
: base(factory)
public FrontendHostTests(WebApplicationFactory<Program> factory) : base(factory)
{
}
@@ -13,7 +10,7 @@ public sealed class FrontendHostTests : ApiTestBase
public async Task RootPath_ServesBlazorFrontendShell()
{
using var factory = CreateFactory(1);
using var client = factory.CreateClient(new WebApplicationFactoryClientOptions { AllowAutoRedirect = false });
using var client = factory.CreateClient(new() { AllowAutoRedirect = false });
var response = await client.GetAsync("/");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
@@ -21,4 +18,4 @@ public sealed class FrontendHostTests : ApiTestBase
Assert.Contains("_framework/blazor.web.js", html);
Assert.Contains("Connecting...", html);
}
}
}

View File

@@ -1,11 +1,8 @@
using Microsoft.AspNetCore.Mvc.Testing;
namespace RpgRoller.Tests;
public sealed class RollVisibilityApiTests : ApiTestBase
{
public RollVisibilityApiTests(WebApplicationFactory<Program> factory)
: base(factory)
public RollVisibilityApiTests(WebApplicationFactory<Program> factory) : base(factory)
{
}
@@ -13,47 +10,29 @@ public sealed class RollVisibilityApiTests : ApiTestBase
public async Task RollVisibilityAndAuthorization_AreEnforced()
{
using var factory = CreateFactory(4, 3, 5, 2, 6);
using var gmClient = factory.CreateClient(new WebApplicationFactoryClientOptions { AllowAutoRedirect = false });
using var playerClient = factory.CreateClient(new WebApplicationFactoryClientOptions { AllowAutoRedirect = false });
using var observerClient = factory.CreateClient(new WebApplicationFactoryClientOptions { AllowAutoRedirect = false });
using var outsiderClient = factory.CreateClient(new WebApplicationFactoryClientOptions { AllowAutoRedirect = false });
using var gmClient = factory.CreateClient(new() { AllowAutoRedirect = false });
using var playerClient = factory.CreateClient(new() { AllowAutoRedirect = false });
using var observerClient = factory.CreateClient(new() { AllowAutoRedirect = false });
using var outsiderClient = factory.CreateClient(new() { AllowAutoRedirect = false });
await RegisterAsync(gmClient, "gm", "Password123", "GM");
await LoginAsync(gmClient, "gm", "Password123");
var campaign = await PostAsync<CreateCampaignRequest, CampaignSummary>(
gmClient,
"/api/campaigns",
new CreateCampaignRequest("Main", "d6"));
var campaign = await PostAsync<CreateCampaignRequest, CampaignSummary>(gmClient, "/api/campaigns", new("Main", "d6"));
await RegisterAsync(playerClient, "player", "Password123", "Player");
await LoginAsync(playerClient, "player", "Password123");
var playerCharacter = await PostAsync<CreateCharacterRequest, CharacterSummary>(
playerClient,
"/api/characters",
new CreateCharacterRequest("Rogue", campaign.Id));
var playerCharacter = await PostAsync<CreateCharacterRequest, CharacterSummary>(playerClient, "/api/characters", new("Rogue", campaign.Id));
var skill = await PostAsync<CreateSkillRequest, SkillSummary>(
playerClient,
$"/api/characters/{playerCharacter.Id}/skills",
new CreateSkillRequest("Stealth", "2D+1", 1, true));
var skill = await PostAsync<CreateSkillRequest, SkillSummary>(playerClient, $"/api/characters/{playerCharacter.Id}/skills", new("Stealth", "2D+1", 1, true));
Assert.Equal(1, skill.WildDice);
Assert.True(skill.AllowFumble);
await RegisterAsync(observerClient, "observer", "Password123", "Observer");
await LoginAsync(observerClient, "observer", "Password123");
await PostAsync<CreateCharacterRequest, CharacterSummary>(
observerClient,
"/api/characters",
new CreateCharacterRequest("Watcher", campaign.Id));
await PostAsync<CreateCharacterRequest, CharacterSummary>(observerClient, "/api/characters", new("Watcher", campaign.Id));
var privateRoll = await PostAsync<RollSkillRequest, RollResult>(
playerClient,
$"/api/skills/{skill.Id}/roll",
new RollSkillRequest("private"));
var publicRoll = await PostAsync<RollSkillRequest, RollResult>(
playerClient,
$"/api/skills/{skill.Id}/roll",
new RollSkillRequest("public"));
var privateRoll = await PostAsync<RollSkillRequest, RollResult>(playerClient, $"/api/skills/{skill.Id}/roll", new("private"));
var publicRoll = await PostAsync<RollSkillRequest, RollResult>(playerClient, $"/api/skills/{skill.Id}/roll", new("public"));
Assert.Equal("private", privateRoll.Visibility);
Assert.Equal("public", publicRoll.Visibility);
@@ -77,15 +56,11 @@ public sealed class RollVisibilityApiTests : ApiTestBase
var forbiddenCampaign = await outsiderClient.GetAsync($"/api/campaigns/{campaign.Id}");
Assert.Equal(HttpStatusCode.BadRequest, forbiddenCampaign.StatusCode);
var invalidVisibility = await playerClient.PostAsJsonAsync(
$"/api/skills/{skill.Id}/roll",
new RollSkillRequest("hidden"));
var invalidVisibility = await playerClient.PostAsJsonAsync($"/api/skills/{skill.Id}/roll", new RollSkillRequest("hidden"));
Assert.Equal(HttpStatusCode.BadRequest, invalidVisibility.StatusCode);
using var anonymousClient = factory.CreateClient(new WebApplicationFactoryClientOptions { AllowAutoRedirect = false });
var unauthorizedCampaignCreate = await anonymousClient.PostAsJsonAsync(
"/api/campaigns",
new CreateCampaignRequest("Nope", "d6"));
using var anonymousClient = factory.CreateClient(new() { AllowAutoRedirect = false });
var unauthorizedCampaignCreate = await anonymousClient.PostAsJsonAsync("/api/campaigns", new CreateCampaignRequest("Nope", "d6"));
Assert.Equal(HttpStatusCode.Unauthorized, unauthorizedCampaignCreate.StatusCode);
var invalidSessionRequest = new HttpRequestMessage(HttpMethod.Get, "/api/campaigns");
@@ -93,4 +68,4 @@ public sealed class RollVisibilityApiTests : ApiTestBase
var unauthorizedWithInvalidSession = await anonymousClient.SendAsync(invalidSessionRequest);
Assert.Equal(HttpStatusCode.Unauthorized, unauthorizedWithInvalidSession.StatusCode);
}
}
}

View File

@@ -1,11 +1,8 @@
using Microsoft.AspNetCore.Mvc.Testing;
namespace RpgRoller.Tests;
public sealed class SystemApiTests : ApiTestBase
{
public SystemApiTests(WebApplicationFactory<Program> factory)
: base(factory)
public SystemApiTests(WebApplicationFactory<Program> factory) : base(factory)
{
}
@@ -13,7 +10,7 @@ public sealed class SystemApiTests : ApiTestBase
public async Task RulesetAndSseEndpoints_ReturnExpectedResponses()
{
using var factory = CreateFactory(2, 2, 2);
using var client = factory.CreateClient(new WebApplicationFactoryClientOptions { AllowAutoRedirect = false });
using var client = factory.CreateClient(new() { AllowAutoRedirect = false });
var rulesets = await GetAsync<IReadOnlyList<RulesetDefinition>>(client, "/api/rulesets");
Assert.Equal(2, rulesets.Count);
@@ -21,13 +18,10 @@ public sealed class SystemApiTests : ApiTestBase
await RegisterAsync(client, "sse", "Password123", "Sse User");
await LoginAsync(client, "sse", "Password123");
var campaign = await PostAsync<CreateCampaignRequest, CampaignSummary>(
client,
"/api/campaigns",
new CreateCampaignRequest("SSE", "d6"));
var campaign = await PostAsync<CreateCampaignRequest, CampaignSummary>(client, "/api/campaigns", new("SSE", "d6"));
var sseResponse = await client.GetAsync($"/api/events/state?campaignId={campaign.Id}", HttpCompletionOption.ResponseHeadersRead);
Assert.Equal(HttpStatusCode.OK, sseResponse.StatusCode);
Assert.Equal("text/event-stream", sseResponse.Content.Headers.ContentType?.MediaType);
}
}
}

View File

@@ -11,13 +11,11 @@ public sealed class BackendCoverageTests
var extensionsType = typeof(Program).Assembly.GetType("RpgRoller.Api.SessionTokenHttpContextExtensions");
Assert.NotNull(extensionsType);
var method = extensionsType!.GetMethod(
"GetRequiredSessionToken",
BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);
var method = extensionsType!.GetMethod("GetRequiredSessionToken", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);
Assert.NotNull(method);
var context = new DefaultHttpContext();
var exception = Assert.Throws<TargetInvocationException>(() => method!.Invoke(null, [context]));
Assert.IsType<InvalidOperationException>(exception.InnerException);
}
}
}

View File

@@ -1,3 +1,3 @@
namespace RpgRoller.Tests;
// Service-level tests were split by concern under RpgRoller.Tests/Services.
// Service-level tests were split by concern under RpgRoller.Tests/Services.

View File

@@ -4,4 +4,4 @@ global using System.Net.Http.Json;
global using Microsoft.AspNetCore.Mvc.Testing;
global using RpgRoller.Contracts;
global using RpgRoller.Domain;
global using RpgRoller.Services;
global using RpgRoller.Services;

View File

@@ -1,9 +1,9 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using RpgRoller.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using RpgRoller.Data;
using RpgRoller.Hosting;
namespace RpgRoller.Tests;
@@ -13,22 +13,14 @@ public sealed class HostingCoverageTests
public void AddRpgRollerCore_WithInMemoryConnectionString_RegistersCoreServices()
{
var services = new ServiceCollection();
var configuration = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string?>
{
["ConnectionStrings:RpgRoller"] = "Data Source=:memory:"
})
.Build();
var configuration = new ConfigurationBuilder().AddInMemoryCollection(new Dictionary<string, string?> { ["ConnectionStrings:RpgRoller"] = "Data Source=:memory:" }).Build();
var environment = new TestWebHostEnvironment
{
ContentRootPath = Path.GetTempPath()
};
var environment = new TestWebHostEnvironment { ContentRootPath = Path.GetTempPath() };
services.AddRpgRollerCore(configuration, environment);
Assert.Contains(services, d => d.ServiceType == typeof(RpgRoller.Services.IGameService));
Assert.Contains(services, d => d.ServiceType == typeof(RpgRoller.Services.IDiceRoller));
Assert.Contains(services, d => d.ServiceType == typeof(IGameService));
Assert.Contains(services, d => d.ServiceType == typeof(IDiceRoller));
}
[Fact]
@@ -41,63 +33,60 @@ public sealed class HostingCoverageTests
{
connection.Open();
using var command = connection.CreateCommand();
command.CommandText =
"""
CREATE TABLE "Users" (
"Id" TEXT NOT NULL CONSTRAINT "PK_Users" PRIMARY KEY,
"Username" TEXT NOT NULL,
"UsernameNormalized" TEXT NOT NULL,
"PasswordHash" TEXT NOT NULL,
"DisplayName" TEXT NOT NULL,
"ActiveCharacterId" TEXT NULL
);
command.CommandText = """
CREATE TABLE "Users" (
"Id" TEXT NOT NULL CONSTRAINT "PK_Users" PRIMARY KEY,
"Username" TEXT NOT NULL,
"UsernameNormalized" TEXT NOT NULL,
"PasswordHash" TEXT NOT NULL,
"DisplayName" TEXT NOT NULL,
"ActiveCharacterId" TEXT NULL
);
CREATE TABLE "Sessions" (
"Token" TEXT NOT NULL CONSTRAINT "PK_Sessions" PRIMARY KEY,
"UserId" TEXT NOT NULL,
"CreatedAtUtc" TEXT NOT NULL
);
CREATE TABLE "Sessions" (
"Token" TEXT NOT NULL CONSTRAINT "PK_Sessions" PRIMARY KEY,
"UserId" TEXT NOT NULL,
"CreatedAtUtc" TEXT NOT NULL
);
CREATE TABLE "Campaigns" (
"Id" TEXT NOT NULL CONSTRAINT "PK_Campaigns" PRIMARY KEY,
"GmUserId" TEXT NOT NULL,
"Name" TEXT NOT NULL,
"Ruleset" TEXT NOT NULL,
"Version" INTEGER NOT NULL
);
CREATE TABLE "Campaigns" (
"Id" TEXT NOT NULL CONSTRAINT "PK_Campaigns" PRIMARY KEY,
"GmUserId" TEXT NOT NULL,
"Name" TEXT NOT NULL,
"Ruleset" TEXT NOT NULL,
"Version" INTEGER NOT NULL
);
CREATE TABLE "Characters" (
"Id" TEXT NOT NULL CONSTRAINT "PK_Characters" PRIMARY KEY,
"OwnerUserId" TEXT NOT NULL,
"CampaignId" TEXT NOT NULL,
"Name" TEXT NOT NULL
);
CREATE TABLE "Characters" (
"Id" TEXT NOT NULL CONSTRAINT "PK_Characters" PRIMARY KEY,
"OwnerUserId" TEXT NOT NULL,
"CampaignId" TEXT NOT NULL,
"Name" TEXT NOT NULL
);
CREATE TABLE "Skills" (
"Id" TEXT NOT NULL CONSTRAINT "PK_Skills" PRIMARY KEY,
"CharacterId" TEXT NOT NULL,
"Name" TEXT NOT NULL,
"DiceRollDefinition" TEXT NOT NULL
);
CREATE TABLE "Skills" (
"Id" TEXT NOT NULL CONSTRAINT "PK_Skills" PRIMARY KEY,
"CharacterId" TEXT NOT NULL,
"Name" TEXT NOT NULL,
"DiceRollDefinition" TEXT NOT NULL
);
CREATE TABLE "RollLogEntries" (
"Id" TEXT NOT NULL CONSTRAINT "PK_RollLogEntries" PRIMARY KEY,
"CampaignId" TEXT NOT NULL,
"CharacterId" TEXT NOT NULL,
"SkillId" TEXT NOT NULL,
"RollerUserId" TEXT NOT NULL,
"Visibility" TEXT NOT NULL,
"Result" INTEGER NOT NULL,
"Breakdown" TEXT NOT NULL,
"TimestampUtc" TEXT NOT NULL
);
""";
CREATE TABLE "RollLogEntries" (
"Id" TEXT NOT NULL CONSTRAINT "PK_RollLogEntries" PRIMARY KEY,
"CampaignId" TEXT NOT NULL,
"CharacterId" TEXT NOT NULL,
"SkillId" TEXT NOT NULL,
"RollerUserId" TEXT NOT NULL,
"Visibility" TEXT NOT NULL,
"Result" INTEGER NOT NULL,
"Breakdown" TEXT NOT NULL,
"TimestampUtc" TEXT NOT NULL
);
""";
_ = command.ExecuteNonQuery();
}
var options = new DbContextOptionsBuilder<RpgRollerDbContext>()
.UseSqlite(connectionString)
.Options;
var options = new DbContextOptionsBuilder<RpgRollerDbContext>().UseSqlite(connectionString).Options;
using (var db = new RpgRollerDbContext(options))
{
@@ -112,9 +101,7 @@ public sealed class HostingCoverageTests
using var tableInfoReader = tableInfoCommand.ExecuteReader();
var columns = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
while (tableInfoReader.Read())
{
columns.Add(tableInfoReader.GetString(1));
}
Assert.Contains("WildDice", columns);
Assert.Contains("AllowFumble", columns);
@@ -124,9 +111,7 @@ public sealed class HostingCoverageTests
using var rollTableInfoReader = rollTableInfoCommand.ExecuteReader();
var rollColumns = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
while (rollTableInfoReader.Read())
{
rollColumns.Add(rollTableInfoReader.GetString(1));
}
Assert.Contains("Dice", rollColumns);
@@ -145,4 +130,4 @@ public sealed class HostingCoverageTests
var rollDiceHistoryCount = Convert.ToInt32(rollDiceHistoryCommand.ExecuteScalar());
Assert.Equal(1, rollDiceHistoryCount);
}
}
}

View File

@@ -1,26 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
</PropertyGroup>
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.4" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.4" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.4"/>
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.0"/>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1"/>
<PackageReference Include="xunit" Version="2.9.3"/>
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.4"/>
</ItemGroup>
<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>
<ItemGroup>
<Using Include="Xunit"/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\RpgRoller\RpgRoller.csproj" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\RpgRoller\RpgRoller.csproj"/>
</ItemGroup>
</Project>

View File

@@ -1,5 +1,3 @@
using RpgRoller.Services;
namespace RpgRoller.Tests;
public sealed class DiceRulesTests
@@ -33,4 +31,4 @@ public sealed class DiceRulesTests
Assert.Equal("dnd5e", DiceRules.ToRulesetId(RulesetKind.Dnd5e));
Assert.Throws<ArgumentOutOfRangeException>(() => DiceRules.ToRulesetId((RulesetKind)99));
}
}
}

View File

@@ -1,5 +1,3 @@
using RpgRoller.Services;
namespace RpgRoller.Tests;
public sealed class RandomDiceRollerTests
@@ -14,4 +12,4 @@ public sealed class RandomDiceRollerTests
Assert.InRange(value, 1, 12);
}
}
}
}

View File

@@ -56,4 +56,4 @@ public sealed class ServiceAuthTests
Assert.True(login.Succeeded);
Assert.Equal(2, hasher.HashCalls);
}
}
}

View File

@@ -97,4 +97,4 @@ public sealed class ServiceCampaignTests
Assert.Single(ownerView.Skills);
Assert.Equal(ownerSkill.Id, ownerView.Skills[0].Id);
}
}
}

View File

@@ -77,4 +77,4 @@ public sealed class ServiceD6RollTests
Assert.Equal(0, dndSkill.WildDice);
Assert.False(dndSkill.AllowFumble);
}
}
}

View File

@@ -91,4 +91,4 @@ public sealed class ServicePersistenceTests
Assert.False(invalidExpressionHarness.Service.RollSkill(ownerSession, skill.Id, "public").Succeeded);
Assert.False(service.GetCampaignLog(string.Empty, campaign.Id).Succeeded);
}
}
}

View File

@@ -73,4 +73,4 @@ public sealed class ServiceSkillRollTests
Assert.True(version.Succeeded);
Assert.False(missingVersion.Succeeded);
}
}
}

View File

@@ -1,18 +1,27 @@
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;
public abstract class ApiTestBase : IClassFixture<WebApplicationFactory<Program>>
{
private readonly WebApplicationFactory<Program> m_BaseFactory;
private sealed class FixedDiceRoller : IDiceRoller
{
public FixedDiceRoller(IEnumerable<int> values)
{
m_Values = new(values);
}
public int Roll(int sides)
{
var next = m_Values.Count > 0 ? m_Values.Dequeue() : 1;
return Math.Clamp(next, 1, sides);
}
private readonly Queue<int> m_Values;
}
protected ApiTestBase(WebApplicationFactory<Program> factory)
{
@@ -21,28 +30,23 @@ public abstract class ApiTestBase : IClassFixture<WebApplicationFactory<Program>
protected WebApplicationFactory<Program> CreateFactory(params int[] rollValues)
{
return m_BaseFactory.WithWebHostBuilder(builder =>
builder.ConfigureServices(services =>
{
services.RemoveAll<IDiceRoller>();
services.AddSingleton<IDiceRoller>(new FixedDiceRoller(rollValues));
return m_BaseFactory.WithWebHostBuilder(builder => builder.ConfigureServices(services =>
{
services.RemoveAll<IDiceRoller>();
services.AddSingleton<IDiceRoller>(new FixedDiceRoller(rollValues));
services.RemoveAll<DbContextOptions<RpgRollerDbContext>>();
services.RemoveAll<IDbContextFactory<RpgRollerDbContext>>();
services.RemoveAll<RpgRollerDbContext>();
services.RemoveAll<DbContextOptions<RpgRollerDbContext>>();
services.RemoveAll<IDbContextFactory<RpgRollerDbContext>>();
services.RemoveAll<RpgRollerDbContext>();
var dbPath = Path.Combine(Path.GetTempPath(), $"rpgroller-tests-{Guid.NewGuid():N}.db");
services.AddDbContextFactory<RpgRollerDbContext>(options =>
options.UseSqlite($"Data Source={dbPath}"));
}));
var dbPath = Path.Combine(Path.GetTempPath(), $"rpgroller-tests-{Guid.NewGuid():N}.db");
services.AddDbContextFactory<RpgRollerDbContext>(options => options.UseSqlite($"Data Source={dbPath}"));
}));
}
protected static async Task<UserSummary> RegisterAsync(HttpClient client, string username, string password, string displayName)
{
return await PostAsync<RegisterRequest, UserSummary>(
client,
"/api/auth/register",
new RegisterRequest(username, password, displayName));
return await PostAsync<RegisterRequest, UserSummary>(client, "/api/auth/register", new(username, password, displayName));
}
protected static async Task LoginAsync(HttpClient client, string username, string password)
@@ -78,19 +82,5 @@ public abstract class ApiTestBase : IClassFixture<WebApplicationFactory<Program>
return result;
}
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 readonly WebApplicationFactory<Program> m_BaseFactory;
}

View File

@@ -1,13 +1,86 @@
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using RpgRoller.Data;
using RpgRoller.Domain;
using RpgRoller.Services;
namespace RpgRoller.Tests;
internal static class ServiceTestSupport
{
internal sealed class ServiceHarness : IDisposable
{
internal ServiceHarness(GameService service, SqliteDbContextFactory factory, string dbPath)
{
Service = service;
m_Factory = factory;
DbPath = dbPath;
}
public void Dispose()
{
m_Factory.Dispose();
}
public RpgRollerDbContext CreateDbContext()
{
return m_Factory.CreateDbContext();
}
public GameService Service { get; }
public string DbPath { get; }
private readonly SqliteDbContextFactory m_Factory;
}
internal sealed class RehashingPasswordHasher : IPasswordHasher<UserAccount>
{
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;
}
public int HashCalls { get; private set; }
}
private sealed class FixedDiceRoller : IDiceRoller
{
public FixedDiceRoller(IEnumerable<int> values)
{
m_Values = new(values);
}
public int Roll(int sides)
{
var next = m_Values.Count > 0 ? m_Values.Dequeue() : 1;
return Math.Clamp(next, 1, sides);
}
private readonly Queue<int> m_Values;
}
internal sealed class SqliteDbContextFactory : IDbContextFactory<RpgRollerDbContext>, IDisposable
{
public SqliteDbContextFactory(string dbPath)
{
m_Options = new DbContextOptionsBuilder<RpgRollerDbContext>().UseSqlite($"Data Source={dbPath}").Options;
}
public RpgRollerDbContext CreateDbContext()
{
return new(m_Options);
}
public void Dispose()
{
}
private readonly DbContextOptions<RpgRollerDbContext> m_Options;
}
internal static ServiceHarness CreateHarness(params int[] rollValues)
{
return CreateHarness(new PasswordHasher<UserAccount>(), rollValues);
@@ -26,9 +99,7 @@ internal static class ServiceTestSupport
internal static ServiceHarness CreateHarnessFromPath(string dbPath, IPasswordHasher<UserAccount> passwordHasher, params int[] rollValues)
{
var options = new DbContextOptionsBuilder<RpgRollerDbContext>()
.UseSqlite($"Data Source={dbPath}")
.Options;
var options = new DbContextOptionsBuilder<RpgRollerDbContext>().UseSqlite($"Data Source={dbPath}").Options;
using (var db = new RpgRollerDbContext(options))
{
@@ -37,7 +108,7 @@ internal static class ServiceTestSupport
var factory = new SqliteDbContextFactory(dbPath);
var service = new GameService(factory, passwordHasher, new FixedDiceRoller(rollValues));
return new ServiceHarness(service, factory, dbPath);
return new(service, factory, dbPath);
}
internal static T GetValue<T>(ServiceResult<T> result)
@@ -46,84 +117,4 @@ internal static class ServiceTestSupport
Assert.NotNull(result.Value);
return result.Value!;
}
internal sealed class ServiceHarness : IDisposable
{
private readonly SqliteDbContextFactory m_Factory;
internal ServiceHarness(GameService service, SqliteDbContextFactory factory, string dbPath)
{
Service = service;
m_Factory = factory;
DbPath = dbPath;
}
public GameService Service { get; }
public string DbPath { get; }
public void Dispose()
{
m_Factory.Dispose();
}
public RpgRollerDbContext CreateDbContext()
{
return m_Factory.CreateDbContext();
}
}
internal 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;
}
}
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);
}
}
internal sealed class SqliteDbContextFactory : IDbContextFactory<RpgRollerDbContext>, IDisposable
{
private readonly DbContextOptions<RpgRollerDbContext> m_Options;
public SqliteDbContextFactory(string dbPath)
{
m_Options = new DbContextOptionsBuilder<RpgRollerDbContext>()
.UseSqlite($"Data Source={dbPath}")
.Options;
}
public RpgRollerDbContext CreateDbContext()
{
return new RpgRollerDbContext(m_Options);
}
public void Dispose()
{
}
}
}
}

View File

@@ -11,4 +11,4 @@ internal sealed class TestWebHostEnvironment : IWebHostEnvironment
public string EnvironmentName { get; set; } = "Development";
public string ContentRootPath { get; set; } = string.Empty;
public IFileProvider ContentRootFileProvider { get; set; } = new NullFileProvider();
}
}