From 885121723d994ef01dcc02662f37af471af89539 Mon Sep 17 00:00:00 2001 From: Frank Tovar Date: Tue, 24 Feb 2026 23:37:53 +0100 Subject: [PATCH] Expand coverage to full backend assembly --- FAQ.md | 4 ++ README.md | 2 + RpgRoller.Tests/BackendCoverageTests.cs | 60 +++++++++++++++++++++++++ RpgRoller.Tests/UnitTest1.cs | 12 +++++ RpgRoller.Tests/coverlet.runsettings | 1 - 5 files changed, 78 insertions(+), 1 deletion(-) create mode 100644 RpgRoller.Tests/BackendCoverageTests.cs diff --git a/FAQ.md b/FAQ.md index 79f1f7e..f8e480d 100644 --- a/FAQ.md +++ b/FAQ.md @@ -28,6 +28,10 @@ To start with a clean backend state, stop the app and remove the corresponding S No. The backend loads state from SQLite once during startup into in-memory state and serves requests from memory. Successful state mutations are then written back to SQLite. +## What does test coverage include? + +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? 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. diff --git a/README.md b/README.md index 6119360..2589dee 100644 --- a/README.md +++ b/README.md @@ -69,6 +69,8 @@ To use a custom SQLite database path, set `ConnectionStrings__RpgRoller`. ```powershell pwsh ./scripts/check-coverage.ps1 -MinLineRate 0.90 -MinBranchRate 0.70 ``` +- Coverage collector scope: + - `RpgRoller.Tests/coverlet.runsettings` now measures the full backend assembly (`RpgRoller`), not only service namespace files. ## Implemented Backend Scope diff --git a/RpgRoller.Tests/BackendCoverageTests.cs b/RpgRoller.Tests/BackendCoverageTests.cs new file mode 100644 index 0000000..8fcd04f --- /dev/null +++ b/RpgRoller.Tests/BackendCoverageTests.cs @@ -0,0 +1,60 @@ +using System.Reflection; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.FileProviders; +using RpgRoller.Hosting; + +namespace RpgRoller.Tests; + +public sealed class BackendCoverageTests +{ + [Fact] + public void AddRpgRollerCore_WithInMemoryConnectionString_RegistersCoreServices() + { + var services = new ServiceCollection(); + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + ["ConnectionStrings:RpgRoller"] = "Data Source=:memory:" + }) + .Build(); + + 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)); + } + + [Fact] + public void GetRequiredSessionToken_ThrowsWhenTokenWasNotStored() + { + var extensionsType = typeof(Program).Assembly.GetType("RpgRoller.Api.SessionTokenHttpContextExtensions"); + Assert.NotNull(extensionsType); + + var method = extensionsType!.GetMethod( + "GetRequiredSessionToken", + BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); + Assert.NotNull(method); + + var context = new DefaultHttpContext(); + var exception = Assert.Throws(() => method!.Invoke(null, [context])); + Assert.IsType(exception.InnerException); + } + + private sealed class TestWebHostEnvironment : IWebHostEnvironment + { + public string ApplicationName { get; set; } = "RpgRoller.Tests"; + public IFileProvider WebRootFileProvider { get; set; } = new NullFileProvider(); + public string WebRootPath { get; set; } = string.Empty; + public string EnvironmentName { get; set; } = "Development"; + public string ContentRootPath { get; set; } = string.Empty; + public IFileProvider ContentRootFileProvider { get; set; } = new NullFileProvider(); + } +} diff --git a/RpgRoller.Tests/UnitTest1.cs b/RpgRoller.Tests/UnitTest1.cs index a958a00..33a8bc0 100644 --- a/RpgRoller.Tests/UnitTest1.cs +++ b/RpgRoller.Tests/UnitTest1.cs @@ -71,6 +71,13 @@ public sealed class UnitTest1 : IClassFixture> new CreateSkillRequest("Arcana", "2d12+2")); Assert.Equal("2d12+2", createdSkill.DiceRollDefinition); + var updatedSkill = await PutAsync( + gmClient, + $"/api/skills/{createdSkill.Id}", + new UpdateSkillRequest("Arcana Mastery", "2d12+3")); + Assert.Equal("Arcana Mastery", updatedSkill.Name); + Assert.Equal("2d12+3", updatedSkill.DiceRollDefinition); + var invalidSkill = await gmClient.PostAsJsonAsync( $"/api/characters/{gmCharacter.Id}/skills", new CreateSkillRequest("Broken", "5D+4")); @@ -172,6 +179,11 @@ public sealed class UnitTest1 : IClassFixture> "/api/campaigns", new CreateCampaignRequest("Nope", "d6")); Assert.Equal(HttpStatusCode.Unauthorized, unauthorizedCampaignCreate.StatusCode); + + var invalidSessionRequest = new HttpRequestMessage(HttpMethod.Get, "/api/campaigns"); + invalidSessionRequest.Headers.Add("Cookie", "rpgroller_session=invalid-token"); + var unauthorizedWithInvalidSession = await anonymousClient.SendAsync(invalidSessionRequest); + Assert.Equal(HttpStatusCode.Unauthorized, unauthorizedWithInvalidSession.StatusCode); } [Fact] diff --git a/RpgRoller.Tests/coverlet.runsettings b/RpgRoller.Tests/coverlet.runsettings index cbdb961..21564f5 100644 --- a/RpgRoller.Tests/coverlet.runsettings +++ b/RpgRoller.Tests/coverlet.runsettings @@ -5,7 +5,6 @@ cobertura - [RpgRoller]RpgRoller.Services.* GeneratedCodeAttribute,CompilerGeneratedAttribute,ExcludeFromCodeCoverageAttribute