Add admin roles, user management, and campaign deletion
This commit is contained in:
@@ -14,6 +14,7 @@ public sealed class AuthApiTests : ApiTestBase
|
||||
|
||||
var registerResult = await RegisterAsync(client, "alice", "Password123", "Alice");
|
||||
Assert.Equal("alice", registerResult.Username);
|
||||
Assert.Contains(registerResult.Roles, role => string.Equals(role, "admin", StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
var duplicate = await client.PostAsJsonAsync("/api/auth/register", new RegisterRequest("alice", "Password123", "Alice 2"));
|
||||
Assert.Equal(HttpStatusCode.BadRequest, duplicate.StatusCode);
|
||||
|
||||
@@ -103,4 +103,52 @@ public sealed class CampaignApiTests : ApiTestBase
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,6 +82,9 @@ public sealed class HostingCoverageTests
|
||||
"Breakdown" TEXT NOT NULL,
|
||||
"TimestampUtc" TEXT NOT NULL
|
||||
);
|
||||
|
||||
INSERT INTO "Users" ("Id", "Username", "UsernameNormalized", "PasswordHash", "DisplayName", "ActiveCharacterId")
|
||||
VALUES ('00000000-0000-0000-0000-000000000001', 'legacy-admin', 'LEGACY-ADMIN', 'hash', 'Legacy Admin', NULL);
|
||||
""";
|
||||
_ = command.ExecuteNonQuery();
|
||||
}
|
||||
@@ -115,6 +118,35 @@ public sealed class HostingCoverageTests
|
||||
|
||||
Assert.Contains("Dice", rollColumns);
|
||||
|
||||
using var usersTableInfoCommand = verifyConnection.CreateCommand();
|
||||
usersTableInfoCommand.CommandText = "PRAGMA table_info('Users');";
|
||||
using var usersTableInfoReader = usersTableInfoCommand.ExecuteReader();
|
||||
var usersColumns = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
while (usersTableInfoReader.Read())
|
||||
usersColumns.Add(usersTableInfoReader.GetString(1));
|
||||
|
||||
Assert.Contains("Roles", usersColumns);
|
||||
|
||||
using var usersRoleCommand = verifyConnection.CreateCommand();
|
||||
usersRoleCommand.CommandText = "SELECT Roles FROM Users WHERE UsernameNormalized = 'LEGACY-ADMIN';";
|
||||
var roles = Convert.ToString(usersRoleCommand.ExecuteScalar());
|
||||
Assert.Equal("admin", roles);
|
||||
|
||||
using var charactersTableInfoCommand = verifyConnection.CreateCommand();
|
||||
charactersTableInfoCommand.CommandText = "PRAGMA table_info('Characters');";
|
||||
using var charactersTableInfoReader = charactersTableInfoCommand.ExecuteReader();
|
||||
var campaignIdNotNull = true;
|
||||
while (charactersTableInfoReader.Read())
|
||||
{
|
||||
if (!string.Equals(charactersTableInfoReader.GetString(1), "CampaignId", StringComparison.OrdinalIgnoreCase))
|
||||
continue;
|
||||
|
||||
campaignIdNotNull = charactersTableInfoReader.GetInt32(3) == 1;
|
||||
break;
|
||||
}
|
||||
|
||||
Assert.False(campaignIdNotNull);
|
||||
|
||||
using var historyCommand = verifyConnection.CreateCommand();
|
||||
historyCommand.CommandText = "SELECT COUNT(*) FROM \"__EFMigrationsHistory\" WHERE \"MigrationId\" = '20260226084000_InitialSchema';";
|
||||
var historyCount = Convert.ToInt32(historyCommand.ExecuteScalar());
|
||||
@@ -129,5 +161,10 @@ public sealed class HostingCoverageTests
|
||||
rollDiceHistoryCommand.CommandText = "SELECT COUNT(*) FROM \"__EFMigrationsHistory\" WHERE \"MigrationId\" = '20260226100000_AddRollLogDice';";
|
||||
var rollDiceHistoryCount = Convert.ToInt32(rollDiceHistoryCommand.ExecuteScalar());
|
||||
Assert.Equal(1, rollDiceHistoryCount);
|
||||
|
||||
using var rolesHistoryCommand = verifyConnection.CreateCommand();
|
||||
rolesHistoryCommand.CommandText = "SELECT COUNT(*) FROM \"__EFMigrationsHistory\" WHERE \"MigrationId\" = '20260226160859_AddAuthorizationRolesAndCampaignDeletion';";
|
||||
var rolesHistoryCount = Convert.ToInt32(rolesHistoryCommand.ExecuteScalar());
|
||||
Assert.Equal(1, rolesHistoryCount);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
using RpgRoller.Domain;
|
||||
|
||||
namespace RpgRoller.Tests;
|
||||
|
||||
public sealed class ServiceAdminAndCampaignDeletionTests
|
||||
{
|
||||
[Fact]
|
||||
public void AdminRoleManagement_RequiresAdminAndProtectsSelf()
|
||||
{
|
||||
using var harness = ServiceTestSupport.CreateHarness();
|
||||
var service = harness.Service;
|
||||
|
||||
var bootstrapAdmin = ServiceTestSupport.GetValue(service.Register("admin", "Password123", "Admin"));
|
||||
_ = ServiceTestSupport.GetValue(service.Register("member", "Password123", "Member"));
|
||||
|
||||
Assert.Contains(bootstrapAdmin.Roles, role => string.Equals(role, UserRoles.Admin, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
var adminSession = ServiceTestSupport.GetValue(service.Login("admin", "Password123")).SessionToken;
|
||||
var memberSession = ServiceTestSupport.GetValue(service.Login("member", "Password123")).SessionToken;
|
||||
|
||||
var forbiddenList = service.GetUsers(memberSession);
|
||||
Assert.False(forbiddenList.Succeeded);
|
||||
|
||||
var users = ServiceTestSupport.GetValue(service.GetUsers(adminSession));
|
||||
var memberUser = users.Single(user => string.Equals(user.Username, "member", StringComparison.OrdinalIgnoreCase));
|
||||
Assert.Empty(memberUser.Roles);
|
||||
|
||||
var promoted = ServiceTestSupport.GetValue(service.UpdateUserRoles(adminSession, memberUser.Id, [UserRoles.Admin]));
|
||||
Assert.Contains(promoted.Roles, role => string.Equals(role, UserRoles.Admin, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
var selfDemote = service.UpdateUserRoles(adminSession, bootstrapAdmin.Id, Array.Empty<string>());
|
||||
Assert.False(selfDemote.Succeeded);
|
||||
|
||||
var selfDelete = service.DeleteUser(adminSession, bootstrapAdmin.Id);
|
||||
Assert.False(selfDelete.Succeeded);
|
||||
|
||||
var deletedMember = ServiceTestSupport.GetValue(service.DeleteUser(adminSession, memberUser.Id));
|
||||
Assert.True(deletedMember);
|
||||
|
||||
var remainingUsers = ServiceTestSupport.GetValue(service.GetUsers(adminSession));
|
||||
Assert.Single(remainingUsers);
|
||||
Assert.Equal(bootstrapAdmin.Id, remainingUsers[0].Id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CampaignDeletion_ByOwnerOrAdmin_UnlinksCharactersAndClearsLog()
|
||||
{
|
||||
using var harness = ServiceTestSupport.CreateHarness(4, 5, 6);
|
||||
var service = harness.Service;
|
||||
|
||||
_ = ServiceTestSupport.GetValue(service.Register("admin", "Password123", "Admin"));
|
||||
_ = ServiceTestSupport.GetValue(service.Register("gm", "Password123", "GM"));
|
||||
_ = ServiceTestSupport.GetValue(service.Register("player", "Password123", "Player"));
|
||||
|
||||
var adminSession = ServiceTestSupport.GetValue(service.Login("admin", "Password123")).SessionToken;
|
||||
var gmSession = ServiceTestSupport.GetValue(service.Login("gm", "Password123")).SessionToken;
|
||||
var playerSession = ServiceTestSupport.GetValue(service.Login("player", "Password123")).SessionToken;
|
||||
|
||||
var ownerDeletedCampaign = ServiceTestSupport.GetValue(service.CreateCampaign(gmSession, "Owner Delete", "d6"));
|
||||
var ownerDeleteResult = ServiceTestSupport.GetValue(service.DeleteCampaign(gmSession, ownerDeletedCampaign.Id));
|
||||
Assert.True(ownerDeleteResult);
|
||||
Assert.False(service.GetCampaign(gmSession, ownerDeletedCampaign.Id).Succeeded);
|
||||
|
||||
var adminDeletedCampaign = ServiceTestSupport.GetValue(service.CreateCampaign(gmSession, "Admin Delete", "d6"));
|
||||
var playerCharacter = ServiceTestSupport.GetValue(service.CreateCharacter(playerSession, "Scout", adminDeletedCampaign.Id));
|
||||
var playerSkill = ServiceTestSupport.GetValue(service.CreateSkill(playerSession, playerCharacter.Id, "Stealth", "2D+1", 1, true));
|
||||
_ = ServiceTestSupport.GetValue(service.RollSkill(playerSession, playerSkill.Id, "public"));
|
||||
|
||||
var forbiddenDelete = service.DeleteCampaign(playerSession, adminDeletedCampaign.Id);
|
||||
Assert.False(forbiddenDelete.Succeeded);
|
||||
|
||||
var adminDelete = ServiceTestSupport.GetValue(service.DeleteCampaign(adminSession, adminDeletedCampaign.Id));
|
||||
Assert.True(adminDelete);
|
||||
Assert.False(service.GetCampaign(gmSession, adminDeletedCampaign.Id).Succeeded);
|
||||
|
||||
var playerCharacters = ServiceTestSupport.GetValue(service.GetOwnCharacters(playerSession));
|
||||
Assert.Single(playerCharacters);
|
||||
Assert.Null(playerCharacters[0].CampaignId);
|
||||
|
||||
using var db = harness.CreateDbContext();
|
||||
Assert.Empty(db.RollLogEntries.Where(entry => entry.CampaignId == adminDeletedCampaign.Id));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user