272 lines
12 KiB
C#
272 lines
12 KiB
C#
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<WebApplicationFactory<Program>>
|
|
{
|
|
private readonly WebApplicationFactory<Program> m_BaseFactory;
|
|
|
|
public UnitTest1(WebApplicationFactory<Program> 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<MeResponse>(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<CreateCampaignRequest, CampaignSummary>(
|
|
gmClient,
|
|
"/api/campaigns",
|
|
new CreateCampaignRequest("Alpha Campaign", "dnd5e"));
|
|
|
|
var gmCharacter = await PostAsync<CreateCharacterRequest, CharacterSummary>(
|
|
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<CreateSkillRequest, SkillSummary>(
|
|
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<CampaignDetails>(gmClient, $"/api/campaigns/{campaign.Id}");
|
|
Assert.Equal(campaign.Id, details.Id);
|
|
Assert.Single(details.Characters);
|
|
Assert.Single(details.Skills);
|
|
|
|
var currentCampaignCharacters = await GetAsync<IReadOnlyList<CharacterSummary>>(gmClient, "/api/characters/current-campaign");
|
|
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 updatedCharacter = await PutAsync<UpdateCharacterRequest, CharacterSummary>(
|
|
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<CreateCampaignRequest, CampaignSummary>(
|
|
gmClient,
|
|
"/api/campaigns",
|
|
new CreateCampaignRequest("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 skill = await PostAsync<CreateSkillRequest, SkillSummary>(
|
|
playerClient,
|
|
$"/api/characters/{playerCharacter.Id}/skills",
|
|
new CreateSkillRequest("Stealth", "2D+1"));
|
|
|
|
await RegisterAsync(observerClient, "observer", "Password123", "Observer");
|
|
await LoginAsync(observerClient, "observer", "Password123");
|
|
await PostAsync<CreateCharacterRequest, CharacterSummary>(
|
|
observerClient,
|
|
"/api/characters",
|
|
new CreateCharacterRequest("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"));
|
|
|
|
Assert.Equal("private", privateRoll.Visibility);
|
|
Assert.Equal("public", publicRoll.Visibility);
|
|
|
|
var gmLog = await GetAsync<IReadOnlyList<CampaignLogEntry>>(gmClient, $"/api/campaigns/{campaign.Id}/log");
|
|
Assert.Equal(2, gmLog.Count);
|
|
|
|
var playerLog = await GetAsync<IReadOnlyList<CampaignLogEntry>>(playerClient, $"/api/campaigns/{campaign.Id}/log");
|
|
Assert.Equal(2, playerLog.Count);
|
|
|
|
var observerLog = await GetAsync<IReadOnlyList<CampaignLogEntry>>(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<IReadOnlyList<RulesetDefinition>>(client, "/api/rulesets");
|
|
Assert.Equal(2, rulesets.Count);
|
|
|
|
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 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<Program> CreateFactory(params int[] rollValues)
|
|
{
|
|
return m_BaseFactory.WithWebHostBuilder(builder =>
|
|
builder.ConfigureServices(services =>
|
|
{
|
|
services.RemoveAll<IDiceRoller>();
|
|
services.AddSingleton<IDiceRoller>(new FixedDiceRoller(rollValues));
|
|
|
|
services.RemoveAll<DbContextOptions<RpgRollerDbContext>>();
|
|
services.RemoveAll<RpgRollerDbContext>();
|
|
|
|
var dbPath = Path.Combine(Path.GetTempPath(), $"rpgroller-tests-{Guid.NewGuid():N}.db");
|
|
services.AddDbContext<RpgRollerDbContext>(options => options.UseSqlite($"Data Source={dbPath}"));
|
|
}));
|
|
}
|
|
|
|
private 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));
|
|
}
|
|
|
|
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<TResponse> PostAsync<TRequest, TResponse>(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<TResponse>();
|
|
Assert.NotNull(result);
|
|
return result;
|
|
}
|
|
|
|
private static async Task<TResponse> PutAsync<TRequest, TResponse>(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<TResponse>();
|
|
Assert.NotNull(result);
|
|
return result;
|
|
}
|
|
|
|
private static async Task<TResponse> GetAsync<TResponse>(HttpClient client, string uri)
|
|
{
|
|
var response = await client.GetAsync(uri);
|
|
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
|
var result = await response.Content.ReadFromJsonAsync<TResponse>();
|
|
Assert.NotNull(result);
|
|
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);
|
|
}
|
|
}
|
|
}
|