Scaffold full-stack solution and CI baseline

This commit is contained in:
2026-02-24 21:27:51 +01:00
parent f3e3178f2f
commit d9f0c7b7ac
27 changed files with 853 additions and 1 deletions

View File

@@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">
<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>
<Using Include="Xunit" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\RpgRoller\RpgRoller.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,89 @@
using System.Net;
using System.Net.Http.Json;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using RpgRoller.Contracts;
using RpgRoller.Services;
namespace RpgRoller.Tests;
public sealed class UnitTest1 : IClassFixture<WebApplicationFactory<Program>>
{
private readonly HttpClient m_Client;
public UnitTest1(WebApplicationFactory<Program> factory)
{
m_Client = factory.WithWebHostBuilder(builder =>
builder.ConfigureServices(services =>
{
services.RemoveAll<IDiceRoller>();
services.AddSingleton<IDiceRoller>(new FixedDiceRoller(7));
})).CreateClient();
}
[Fact]
public async Task GetHealth_ReturnsOkPayload()
{
var response = await m_Client.GetAsync("/api/health");
var payload = await response.Content.ReadFromJsonAsync<HealthResponse>();
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.NotNull(payload);
Assert.Equal("ok", payload.Status);
}
[Theory]
[InlineData(1, "Dice must have at least 2 sides.")]
[InlineData(1001, "Dice must have at most 1000 sides.")]
public async Task Roll_WithInvalidSides_ReturnsBadRequest(int sides, string expectedError)
{
var response = await m_Client.GetAsync($"/api/roll/{sides}");
var payload = await response.Content.ReadFromJsonAsync<ApiError>();
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
Assert.NotNull(payload);
Assert.Equal(expectedError, payload.Error);
}
[Theory]
[InlineData(2)]
[InlineData(1000)]
public async Task Roll_WithValidSides_ReturnsExpectedResult(int sides)
{
var response = await m_Client.GetAsync($"/api/roll/{sides}");
var payload = await response.Content.ReadFromJsonAsync<RollResponse>();
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.NotNull(payload);
Assert.Equal(sides, payload.Sides);
Assert.Equal(Math.Min(7, sides), payload.Value);
}
[Fact]
public void RandomDiceRoller_ProducesValueWithinRange()
{
var roller = new RandomDiceRoller();
for (var i = 0; i < 200; i += 1)
{
var value = roller.Roll(6);
Assert.InRange(value, 1, 6);
}
}
private sealed class FixedDiceRoller : IDiceRoller
{
private readonly int m_Result;
public FixedDiceRoller(int result)
{
m_Result = result;
}
public int Roll(int sides)
{
return Math.Min(m_Result, sides);
}
}
}

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<RunSettings>
<DataCollectionRunSettings>
<DataCollectors>
<DataCollector friendlyName="XPlat code coverage">
<Configuration>
<Format>cobertura</Format>
<ExcludeByAttribute>GeneratedCodeAttribute,CompilerGeneratedAttribute,ExcludeFromCodeCoverageAttribute</ExcludeByAttribute>
</Configuration>
</DataCollector>
</DataCollectors>
</DataCollectionRunSettings>
</RunSettings>