Files
RpgRoller/RpgRoller.Tests/Api/CampaignApiTests.cs

269 lines
15 KiB
C#

using System.Text;
namespace RpgRoller.Tests;
public sealed class CampaignApiTests : ApiTestBase
{
public CampaignApiTests(WebApplicationFactory<Program> factory) : base(factory)
{
}
[Fact]
public async Task CampaignCharacterAndSkillFlow_EnforcesRulesetValidation()
{
using var factory = CreateFactory(6, 6, 6);
using var gmClient = factory.CreateClient(new() { AllowAutoRedirect = false });
await RegisterAsync(gmClient, "gm", "Password123", "Game Master");
await LoginAsync(gmClient, "gm", "Password123");
var campaign = await PostAsync<CreateCampaignRequest, CampaignDetails>(gmClient, "/api/campaigns", new("Alpha Campaign", "dnd5e"));
var gmCharacter = await PostAsync<CreateCharacterRequest, CharacterSummary>(gmClient, "/api/characters", new("Arin", campaign.Id));
Assert.Equal("Game Master", gmCharacter.OwnerDisplayName);
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("Arcana", "2d12+2", 0, false));
Assert.Equal("2d12+2", createdSkill.DiceRollDefinition);
Assert.Equal(0, createdSkill.WildDice);
Assert.False(createdSkill.AllowFumble);
var updatedSkill = await PutAsync<UpdateSkillRequest, SkillSummary>(gmClient, $"/api/skills/{createdSkill.Id}", new("Arcana Mastery", "2d12+3", 0, false));
Assert.Equal("Arcana Mastery", updatedSkill.Name);
Assert.Equal("2d12+3", updatedSkill.DiceRollDefinition);
Assert.Equal(0, updatedSkill.WildDice);
Assert.False(updatedSkill.AllowFumble);
var invalidSkill = await gmClient.PostAsJsonAsync($"/api/characters/{gmCharacter.Id}/skills", new CreateSkillRequest("Broken", "5D+4", 0, false));
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.Equal("Game Master", details.Characters[0].OwnerDisplayName);
var currentCampaignCharacters = await GetAsync<IReadOnlyList<CharacterSummary>>(gmClient, "/api/characters");
Assert.Single(currentCampaignCharacters);
Assert.Equal(gmCharacter.Id, currentCampaignCharacters[0].Id);
Assert.Equal("Game Master", currentCampaignCharacters[0].OwnerDisplayName);
var otherCampaign = await PostAsync<CreateCampaignRequest, CampaignDetails>(gmClient, "/api/campaigns", new("Beta Campaign", "d6"));
var updatedCharacter = await PutAsync<UpdateCharacterRequest, CharacterSummary>(gmClient, $"/api/characters/{gmCharacter.Id}", new("Arin Updated", otherCampaign.Id));
Assert.Equal("Arin Updated", updatedCharacter.Name);
Assert.Equal(otherCampaign.Id, updatedCharacter.CampaignId);
}
[Fact]
public async Task SkillGroupsAndOwnerTransfer_WorkThroughApi()
{
using var factory = CreateFactory(6, 4, 5, 3);
using var gmClient = factory.CreateClient(new() { AllowAutoRedirect = false });
using var ownerClient = factory.CreateClient(new() { AllowAutoRedirect = false });
using var receiverClient = factory.CreateClient(new() { AllowAutoRedirect = false });
await RegisterAsync(gmClient, "gm2", "Password123", "GM");
await LoginAsync(gmClient, "gm2", "Password123");
await RegisterAsync(ownerClient, "owner2", "Password123", "Owner");
await LoginAsync(ownerClient, "owner2", "Password123");
await RegisterAsync(receiverClient, "receiver2", "Password123", "Receiver");
await LoginAsync(receiverClient, "receiver2", "Password123");
var campaign = await PostAsync<CreateCampaignRequest, CampaignDetails>(gmClient, "/api/campaigns", new("Grouped Campaign", "d6"));
var character = await PostAsync<CreateCharacterRequest, CharacterSummary>(ownerClient, "/api/characters", new("Grouped Hero", campaign.Id));
var createdGroup = await PostAsync<CreateSkillGroupRequest, SkillGroupSummary>(ownerClient, $"/api/characters/{character.Id}/skill-groups", new("Combat", "2D+1", 1, true));
var renamedGroup = await PutAsync<UpdateSkillGroupRequest, SkillGroupSummary>(gmClient, $"/api/skill-groups/{createdGroup.Id}", new("Battle", "3D+2", 2, false));
Assert.Equal("Battle", renamedGroup.Name);
Assert.Equal("3D+2", renamedGroup.DiceRollDefinition);
Assert.Equal(2, renamedGroup.WildDice);
Assert.False(renamedGroup.AllowFumble);
var groupedSkill = await PostAsync<CreateSkillRequest, SkillSummary>(ownerClient, $"/api/characters/{character.Id}/skills", new("Strike", "2D+1", 1, true, renamedGroup.Id));
Assert.Equal(renamedGroup.Id, groupedSkill.SkillGroupId);
var ungroupedSkill = await PutAsync<UpdateSkillRequest, SkillSummary>(ownerClient, $"/api/skills/{groupedSkill.Id}", new("Strike", "2D+1", 1, true, null));
Assert.Null(ungroupedSkill.SkillGroupId);
var groupedAgainSkill = await PutAsync<UpdateSkillRequest, SkillSummary>(ownerClient, $"/api/skills/{groupedSkill.Id}", new("Strike", "2D+1", 1, true, renamedGroup.Id));
Assert.Equal(renamedGroup.Id, groupedAgainSkill.SkillGroupId);
var deleteSkill = await ownerClient.DeleteAsync($"/api/skills/{groupedAgainSkill.Id}");
Assert.Equal(HttpStatusCode.OK, deleteSkill.StatusCode);
var deleteGroup = await ownerClient.DeleteAsync($"/api/skill-groups/{renamedGroup.Id}");
Assert.Equal(HttpStatusCode.OK, deleteGroup.StatusCode);
var transferResult = await PutAsync<UpdateCharacterRequest, CharacterSummary>(gmClient, $"/api/characters/{character.Id}", new("Grouped Hero", campaign.Id, "receiver2"));
Assert.Equal("Grouped Hero", transferResult.Name);
Assert.Equal("Receiver", transferResult.OwnerDisplayName);
var gmCampaignView = await GetAsync<CampaignDetails>(gmClient, $"/api/campaigns/{campaign.Id}");
var gmViewedCharacter = Assert.Single(gmCampaignView.Characters, c => c.Id == character.Id);
Assert.Equal("Receiver", gmViewedCharacter.OwnerDisplayName);
var ownerActivate = await ownerClient.PostAsync($"/api/characters/{character.Id}/activate", null);
Assert.Equal(HttpStatusCode.BadRequest, ownerActivate.StatusCode);
var receiverActivate = await receiverClient.PostAsync($"/api/characters/{character.Id}/activate", null);
Assert.Equal(HttpStatusCode.OK, receiverActivate.StatusCode);
}
[Fact]
public async Task AdminUserManagementAndCampaignDeletion_WorkThroughApi()
{
using var factory = CreateFactory(6, 5, 4);
using var adminClient = factory.CreateClient(new() { AllowAutoRedirect = false });
using var gmClient = factory.CreateClient(new() { AllowAutoRedirect = false });
using var playerClient = factory.CreateClient(new() { AllowAutoRedirect = false });
var admin = await RegisterAsync(adminClient, "admin3", "Password123", "Admin");
var gm = await RegisterAsync(gmClient, "gm3", "Password123", "GM");
var player = await RegisterAsync(playerClient, "player3", "Password123", "Player");
await LoginAsync(adminClient, "admin3", "Password123");
await LoginAsync(gmClient, "gm3", "Password123");
await LoginAsync(playerClient, "player3", "Password123");
var adminUsers = await GetAsync<IReadOnlyList<AdminUserSummary>>(adminClient, "/api/admin/users");
var adminEntry = adminUsers.Single(user => user.Id == admin.Id);
var playerEntry = adminUsers.Single(user => user.Id == player.Id);
Assert.Contains(adminEntry.Roles, role => string.Equals(role, "admin", StringComparison.OrdinalIgnoreCase));
Assert.Empty(playerEntry.Roles);
var promotedPlayer = await PutAsync<UpdateUserRolesRequest, AdminUserSummary>(adminClient, $"/api/admin/users/{player.Id}/roles", new([ "admin" ]));
Assert.Contains(promotedPlayer.Roles, role => string.Equals(role, "admin", StringComparison.OrdinalIgnoreCase));
var campaign = await PostAsync<CreateCampaignRequest, CampaignDetails>(gmClient, "/api/campaigns", new("Disposable Campaign", "d6"));
var character = await PostAsync<CreateCharacterRequest, CharacterSummary>(playerClient, "/api/characters", new("Disposable Hero", campaign.Id));
var skill = await PostAsync<CreateSkillRequest, SkillSummary>(playerClient, $"/api/characters/{character.Id}/skills", new("Stealth", "2D+1", 1, true));
_ = await PostAsync<RollSkillRequest, RollResult>(playerClient, $"/api/skills/{skill.Id}/roll", new("public"));
var deleteCampaign = await adminClient.DeleteAsync($"/api/campaigns/{campaign.Id}");
Assert.Equal(HttpStatusCode.OK, deleteCampaign.StatusCode);
var getDeletedCampaign = await gmClient.GetAsync($"/api/campaigns/{campaign.Id}");
Assert.Equal(HttpStatusCode.BadRequest, getDeletedCampaign.StatusCode);
var playerCharacters = await GetAsync<IReadOnlyList<CharacterSummary>>(playerClient, "/api/characters");
Assert.Single(playerCharacters);
Assert.Null(playerCharacters[0].CampaignId);
var deleteUser = await adminClient.DeleteAsync($"/api/admin/users/{player.Id}");
Assert.Equal(HttpStatusCode.OK, deleteUser.StatusCode);
var usersAfterDelete = await GetAsync<IReadOnlyList<AdminUserSummary>>(adminClient, "/api/admin/users");
Assert.DoesNotContain(usersAfterDelete, user => user.Id == player.Id);
Assert.Contains(usersAfterDelete, user => user.Id == gm.Id);
}
[Fact]
public async Task AdminDatabaseDownload_RequiresAdminAndReturnsSqliteFile()
{
using var factory = CreateFactory();
using var anonymousClient = factory.CreateClient(new() { AllowAutoRedirect = false });
using var adminClient = factory.CreateClient(new() { AllowAutoRedirect = false });
using var memberClient = factory.CreateClient(new() { AllowAutoRedirect = false });
await RegisterAsync(adminClient, "admin-download", "Password123", "Admin Download");
await LoginAsync(adminClient, "admin-download", "Password123");
await RegisterAsync(memberClient, "member-download", "Password123", "Member Download");
await LoginAsync(memberClient, "member-download", "Password123");
var unauthorized = await anonymousClient.GetAsync("/api/admin/database");
Assert.Equal(HttpStatusCode.Unauthorized, unauthorized.StatusCode);
var forbidden = await memberClient.GetAsync("/api/admin/database");
Assert.Equal(HttpStatusCode.BadRequest, forbidden.StatusCode);
var response = await adminClient.GetAsync("/api/admin/database");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal("application/octet-stream", response.Content.Headers.ContentType?.MediaType);
var disposition = response.Content.Headers.ContentDisposition;
Assert.NotNull(disposition);
Assert.Equal("attachment", disposition.DispositionType);
Assert.EndsWith(".db", disposition.FileName?.Trim('"'), StringComparison.OrdinalIgnoreCase);
var bytes = await response.Content.ReadAsByteArrayAsync();
Assert.True(bytes.Length >= 16);
Assert.Equal("SQLite format 3\0", Encoding.ASCII.GetString(bytes, 0, 16));
}
[Fact]
public async Task CampaignOptionsEndpoint_ReturnsCampaignsBeyondVisibleCampaignList()
{
using var factory = CreateFactory(6, 5, 4);
using var gmClient = factory.CreateClient(new() { AllowAutoRedirect = false });
using var otherGmClient = factory.CreateClient(new() { AllowAutoRedirect = false });
using var playerClient = factory.CreateClient(new() { AllowAutoRedirect = false });
await RegisterAsync(gmClient, "gm-options-1", "Password123", "GM One");
await LoginAsync(gmClient, "gm-options-1", "Password123");
await RegisterAsync(otherGmClient, "gm-options-2", "Password123", "GM Two");
await LoginAsync(otherGmClient, "gm-options-2", "Password123");
await RegisterAsync(playerClient, "player-options", "Password123", "Player");
await LoginAsync(playerClient, "player-options", "Password123");
var firstCampaign = await PostAsync<CreateCampaignRequest, CampaignDetails>(gmClient, "/api/campaigns", new("Alpha Visible", "d6"));
var secondCampaign = await PostAsync<CreateCampaignRequest, CampaignDetails>(otherGmClient, "/api/campaigns", new("Beta Available", "d6"));
var playerVisibleCampaigns = await GetAsync<IReadOnlyList<CampaignDetails>>(playerClient, "/api/campaigns");
Assert.Empty(playerVisibleCampaigns);
var playerCampaignOptions = await GetAsync<IReadOnlyList<CampaignOption>>(playerClient, "/api/campaigns/options");
Assert.Equal(2, playerCampaignOptions.Count);
Assert.Contains(playerCampaignOptions, option => option.Id == firstCampaign.Id);
Assert.Contains(playerCampaignOptions, option => option.Id == secondCampaign.Id);
}
[Fact]
public async Task CharacterDelete_RequiresOwnerOrAdmin()
{
using var factory = CreateFactory(6, 5, 4);
using var adminClient = factory.CreateClient(new() { AllowAutoRedirect = false });
using var gmClient = factory.CreateClient(new() { AllowAutoRedirect = false });
using var ownerClient = factory.CreateClient(new() { AllowAutoRedirect = false });
using var otherClient = factory.CreateClient(new() { AllowAutoRedirect = false });
await RegisterAsync(adminClient, "admin-delete", "Password123", "Admin");
await LoginAsync(adminClient, "admin-delete", "Password123");
await RegisterAsync(gmClient, "gm-delete", "Password123", "GM");
await LoginAsync(gmClient, "gm-delete", "Password123");
await RegisterAsync(ownerClient, "owner-delete", "Password123", "Owner");
await LoginAsync(ownerClient, "owner-delete", "Password123");
await RegisterAsync(otherClient, "other-delete", "Password123", "Other");
await LoginAsync(otherClient, "other-delete", "Password123");
var campaign = await PostAsync<CreateCampaignRequest, CampaignDetails>(gmClient, "/api/campaigns", new("Deletion Campaign", "d6"));
var ownerCharacter = await PostAsync<CreateCharacterRequest, CharacterSummary>(ownerClient, "/api/characters", new("Owner Character", campaign.Id));
var otherCharacter = await PostAsync<CreateCharacterRequest, CharacterSummary>(otherClient, "/api/characters", new("Other Character", campaign.Id));
var gmDeleteAttempt = await gmClient.DeleteAsync($"/api/characters/{ownerCharacter.Id}");
Assert.Equal(HttpStatusCode.BadRequest, gmDeleteAttempt.StatusCode);
var otherDeleteAttempt = await otherClient.DeleteAsync($"/api/characters/{ownerCharacter.Id}");
Assert.Equal(HttpStatusCode.BadRequest, otherDeleteAttempt.StatusCode);
var ownerDelete = await ownerClient.DeleteAsync($"/api/characters/{ownerCharacter.Id}");
Assert.Equal(HttpStatusCode.OK, ownerDelete.StatusCode);
var adminDelete = await adminClient.DeleteAsync($"/api/characters/{otherCharacter.Id}");
Assert.Equal(HttpStatusCode.OK, adminDelete.StatusCode);
var campaignAfterDeletes = await GetAsync<CampaignDetails>(gmClient, $"/api/campaigns/{campaign.Id}");
Assert.Empty(campaignAfterDeletes.Characters);
}
}