Expand coverage to full backend assembly
This commit is contained in:
4
FAQ.md
4
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.
|
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?
|
## 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.
|
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.
|
||||||
|
|||||||
@@ -69,6 +69,8 @@ To use a custom SQLite database path, set `ConnectionStrings__RpgRoller`.
|
|||||||
```powershell
|
```powershell
|
||||||
pwsh ./scripts/check-coverage.ps1 -MinLineRate 0.90 -MinBranchRate 0.70
|
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
|
## Implemented Backend Scope
|
||||||
|
|
||||||
|
|||||||
60
RpgRoller.Tests/BackendCoverageTests.cs
Normal file
60
RpgRoller.Tests/BackendCoverageTests.cs
Normal file
@@ -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<string, string?>
|
||||||
|
{
|
||||||
|
["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<TargetInvocationException>(() => method!.Invoke(null, [context]));
|
||||||
|
Assert.IsType<InvalidOperationException>(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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -71,6 +71,13 @@ public sealed class UnitTest1 : IClassFixture<WebApplicationFactory<Program>>
|
|||||||
new CreateSkillRequest("Arcana", "2d12+2"));
|
new CreateSkillRequest("Arcana", "2d12+2"));
|
||||||
Assert.Equal("2d12+2", createdSkill.DiceRollDefinition);
|
Assert.Equal("2d12+2", createdSkill.DiceRollDefinition);
|
||||||
|
|
||||||
|
var updatedSkill = await PutAsync<UpdateSkillRequest, SkillSummary>(
|
||||||
|
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(
|
var invalidSkill = await gmClient.PostAsJsonAsync(
|
||||||
$"/api/characters/{gmCharacter.Id}/skills",
|
$"/api/characters/{gmCharacter.Id}/skills",
|
||||||
new CreateSkillRequest("Broken", "5D+4"));
|
new CreateSkillRequest("Broken", "5D+4"));
|
||||||
@@ -172,6 +179,11 @@ public sealed class UnitTest1 : IClassFixture<WebApplicationFactory<Program>>
|
|||||||
"/api/campaigns",
|
"/api/campaigns",
|
||||||
new CreateCampaignRequest("Nope", "d6"));
|
new CreateCampaignRequest("Nope", "d6"));
|
||||||
Assert.Equal(HttpStatusCode.Unauthorized, unauthorizedCampaignCreate.StatusCode);
|
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]
|
[Fact]
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
<DataCollector friendlyName="XPlat code coverage">
|
<DataCollector friendlyName="XPlat code coverage">
|
||||||
<Configuration>
|
<Configuration>
|
||||||
<Format>cobertura</Format>
|
<Format>cobertura</Format>
|
||||||
<Include>[RpgRoller]RpgRoller.Services.*</Include>
|
|
||||||
<ExcludeByAttribute>GeneratedCodeAttribute,CompilerGeneratedAttribute,ExcludeFromCodeCoverageAttribute</ExcludeByAttribute>
|
<ExcludeByAttribute>GeneratedCodeAttribute,CompilerGeneratedAttribute,ExcludeFromCodeCoverageAttribute</ExcludeByAttribute>
|
||||||
</Configuration>
|
</Configuration>
|
||||||
</DataCollector>
|
</DataCollector>
|
||||||
|
|||||||
Reference in New Issue
Block a user