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 sealed class UnitTest1 : IClassFixture> { private readonly WebApplicationFactory m_BaseFactory; public UnitTest1(WebApplicationFactory factory) { m_BaseFactory = factory; } [Fact] public async Task RegisterLoginAndMeFlow_WorksWithDuplicateUsernameGuard() { using var factory = CreateFactory(4, 4, 4); using var client = factory.CreateClient(new WebApplicationFactoryClientOptions { AllowAutoRedirect = false }); var registerResult = await RegisterAsync(client, "alice", "Password123", "Alice"); Assert.Equal("alice", registerResult.Username); var duplicate = await client.PostAsJsonAsync("/api/auth/register", new RegisterRequest("alice", "Password123", "Alice 2")); Assert.Equal(HttpStatusCode.BadRequest, duplicate.StatusCode); var loginResult = await client.PostAsJsonAsync("/api/auth/login", new LoginRequest("alice", "Password123")); Assert.Equal(HttpStatusCode.OK, loginResult.StatusCode); var me = await GetAsync(client, "/api/me"); Assert.Equal(registerResult.Id, me.User.Id); Assert.Null(me.ActiveCharacterId); Assert.Null(me.CurrentCampaignId); var invalidLogin = await client.PostAsJsonAsync("/api/auth/login", new LoginRequest("alice", "wrong-password")); Assert.Equal(HttpStatusCode.BadRequest, invalidLogin.StatusCode); } [Fact] public async Task CampaignCharacterAndSkillFlow_EnforcesRulesetValidation() { using var factory = CreateFactory(6, 6, 6); using var gmClient = factory.CreateClient(new WebApplicationFactoryClientOptions { AllowAutoRedirect = false }); await RegisterAsync(gmClient, "gm", "Password123", "Game Master"); await LoginAsync(gmClient, "gm", "Password123"); var campaign = await PostAsync( gmClient, "/api/campaigns", new CreateCampaignRequest("Alpha Campaign", "dnd5e")); var gmCharacter = await PostAsync( gmClient, "/api/characters", new CreateCharacterRequest("Arin", campaign.Id)); var activateResponse = await gmClient.PostAsync($"/api/characters/{gmCharacter.Id}/activate", null); Assert.Equal(HttpStatusCode.OK, activateResponse.StatusCode); var createdSkill = await PostAsync( gmClient, $"/api/characters/{gmCharacter.Id}/skills", new CreateSkillRequest("Arcana", "2d12+2")); Assert.Equal("2d12+2", createdSkill.DiceRollDefinition); var invalidSkill = await gmClient.PostAsJsonAsync( $"/api/characters/{gmCharacter.Id}/skills", new CreateSkillRequest("Broken", "5D+4")); Assert.Equal(HttpStatusCode.BadRequest, invalidSkill.StatusCode); var details = await GetAsync(gmClient, $"/api/campaigns/{campaign.Id}"); Assert.Equal(campaign.Id, details.Id); Assert.Single(details.Characters); Assert.Single(details.Skills); var currentCampaignCharacters = await GetAsync>(gmClient, "/api/characters/current-campaign"); Assert.Single(currentCampaignCharacters); Assert.Equal(gmCharacter.Id, currentCampaignCharacters[0].Id); var otherCampaign = await PostAsync( gmClient, "/api/campaigns", new CreateCampaignRequest("Beta Campaign", "d6")); var updatedCharacter = await PutAsync( gmClient, $"/api/characters/{gmCharacter.Id}", new UpdateCharacterRequest("Arin Updated", otherCampaign.Id)); Assert.Equal("Arin Updated", updatedCharacter.Name); Assert.Equal(otherCampaign.Id, updatedCharacter.CampaignId); } [Fact] 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 }); await RegisterAsync(gmClient, "gm", "Password123", "GM"); await LoginAsync(gmClient, "gm", "Password123"); var campaign = await PostAsync( gmClient, "/api/campaigns", new CreateCampaignRequest("Main", "d6")); await RegisterAsync(playerClient, "player", "Password123", "Player"); await LoginAsync(playerClient, "player", "Password123"); var playerCharacter = await PostAsync( playerClient, "/api/characters", new CreateCharacterRequest("Rogue", campaign.Id)); var skill = await PostAsync( playerClient, $"/api/characters/{playerCharacter.Id}/skills", new CreateSkillRequest("Stealth", "2D+1")); await RegisterAsync(observerClient, "observer", "Password123", "Observer"); await LoginAsync(observerClient, "observer", "Password123"); await PostAsync( observerClient, "/api/characters", new CreateCharacterRequest("Watcher", campaign.Id)); var privateRoll = await PostAsync( playerClient, $"/api/skills/{skill.Id}/roll", new RollSkillRequest("private")); var publicRoll = await PostAsync( playerClient, $"/api/skills/{skill.Id}/roll", new RollSkillRequest("public")); Assert.Equal("private", privateRoll.Visibility); Assert.Equal("public", publicRoll.Visibility); var gmLog = await GetAsync>(gmClient, $"/api/campaigns/{campaign.Id}/log"); Assert.Equal(2, gmLog.Count); var playerLog = await GetAsync>(playerClient, $"/api/campaigns/{campaign.Id}/log"); Assert.Equal(2, playerLog.Count); var observerLog = await GetAsync>(observerClient, $"/api/campaigns/{campaign.Id}/log"); Assert.Single(observerLog); Assert.Equal("public", observerLog[0].Visibility); await RegisterAsync(outsiderClient, "outsider", "Password123", "Outsider"); await LoginAsync(outsiderClient, "outsider", "Password123"); 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")); 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")); Assert.Equal(HttpStatusCode.Unauthorized, unauthorizedCampaignCreate.StatusCode); } [Fact] public async Task RulesetAndSseEndpoints_ReturnExpectedResponses() { using var factory = CreateFactory(2, 2, 2); using var client = factory.CreateClient(new WebApplicationFactoryClientOptions { AllowAutoRedirect = false }); var rulesets = await GetAsync>(client, "/api/rulesets"); Assert.Equal(2, rulesets.Count); await RegisterAsync(client, "sse", "Password123", "Sse User"); await LoginAsync(client, "sse", "Password123"); var campaign = await PostAsync( client, "/api/campaigns", new CreateCampaignRequest("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); } private WebApplicationFactory CreateFactory(params int[] rollValues) { return m_BaseFactory.WithWebHostBuilder(builder => builder.ConfigureServices(services => { services.RemoveAll(); services.AddSingleton(new FixedDiceRoller(rollValues)); services.RemoveAll>(); services.RemoveAll(); var dbPath = Path.Combine(Path.GetTempPath(), $"rpgroller-tests-{Guid.NewGuid():N}.db"); services.AddDbContext(options => options.UseSqlite($"Data Source={dbPath}")); })); } private static async Task RegisterAsync(HttpClient client, string username, string password, string displayName) { return await PostAsync( client, "/api/auth/register", new RegisterRequest(username, password, displayName)); } private static async Task LoginAsync(HttpClient client, string username, string password) { var response = await client.PostAsJsonAsync("/api/auth/login", new LoginRequest(username, password)); Assert.Equal(HttpStatusCode.OK, response.StatusCode); } private static async Task PostAsync(HttpClient client, string uri, TRequest payload) { var response = await client.PostAsJsonAsync(uri, payload); Assert.Equal(HttpStatusCode.OK, response.StatusCode); var result = await response.Content.ReadFromJsonAsync(); Assert.NotNull(result); return result; } private static async Task PutAsync(HttpClient client, string uri, TRequest payload) { var response = await client.PutAsJsonAsync(uri, payload); Assert.Equal(HttpStatusCode.OK, response.StatusCode); var result = await response.Content.ReadFromJsonAsync(); Assert.NotNull(result); return result; } private static async Task GetAsync(HttpClient client, string uri) { var response = await client.GetAsync(uri); Assert.Equal(HttpStatusCode.OK, response.StatusCode); var result = await response.Content.ReadFromJsonAsync(); Assert.NotNull(result); return result; } private sealed class FixedDiceRoller : IDiceRoller { private readonly Queue m_Values; public FixedDiceRoller(IEnumerable values) { m_Values = new Queue(values); } public int Roll(int sides) { var next = m_Values.Count > 0 ? m_Values.Dequeue() : 1; return Math.Clamp(next, 1, sides); } } }