Add owner role and admin management controls

This commit is contained in:
2026-02-08 19:01:58 +01:00
parent 97f1b30b75
commit 1c59d68a50
25 changed files with 540 additions and 9 deletions

View File

@@ -94,6 +94,82 @@ public class AdminTests
});
}
[Fact]
public async Task Admin_can_grant_and_revoke_admin_for_non_owner_accounts()
{
await using var factory = new TestWebApplicationFactory();
var owner = factory.CreateClientWithCookies();
await owner.RegisterAsync("owner", admin: true);
var player = factory.CreateClientWithCookies();
await player.RegisterAsync("player");
var playerId = await player.GetProfileIdAsync();
var grant = await owner.PostAsJsonAsync("/api/admin/player-admin", new
{
playerId = playerId,
isAdmin = true
});
grant.EnsureSuccessStatusCode();
await factory.WithDbContextAsync(async db =>
{
var promoted = await db.Players.AsNoTracking().SingleAsync(p => p.Id == playerId);
Assert.True(promoted.IsAdmin);
Assert.False(promoted.IsOwner);
});
var revoke = await owner.PostAsJsonAsync("/api/admin/player-admin", new
{
playerId = playerId,
isAdmin = false
});
revoke.EnsureSuccessStatusCode();
await factory.WithDbContextAsync(async db =>
{
var demoted = await db.Players.AsNoTracking().SingleAsync(p => p.Id == playerId);
Assert.False(demoted.IsAdmin);
});
}
[Fact]
public async Task Owner_admin_role_cannot_be_changed_or_deleted()
{
await using var factory = new TestWebApplicationFactory();
var owner = factory.CreateClientWithCookies();
await owner.RegisterAsync("owner", admin: true);
var ownerId = await owner.GetProfileIdAsync();
var toggleOwner = await owner.PostAsJsonAsync("/api/admin/player-admin", new
{
playerId = ownerId,
isAdmin = false
});
Assert.Equal(HttpStatusCode.BadRequest, toggleOwner.StatusCode);
var deleteOwner = await owner.SendAsync(new HttpRequestMessage(HttpMethod.Delete, $"/api/admin/players/{ownerId}")
{
Content = JsonContent.Create(new { password = AdminPassword })
});
Assert.Equal(HttpStatusCode.BadRequest, deleteOwner.StatusCode);
}
[Fact]
public async Task Set_player_admin_returns_not_found_for_unknown_player()
{
await using var factory = new TestWebApplicationFactory();
var owner = factory.CreateClientWithCookies();
await owner.RegisterAsync("owner", admin: true);
var response = await owner.PostAsJsonAsync("/api/admin/player-admin", new
{
playerId = Guid.NewGuid(),
isAdmin = true
});
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
[Fact]
public async Task Admin_player_phase_requires_vote_phase_and_suggest_target()
{

View File

@@ -117,6 +117,13 @@ public class AuthTests
response.EnsureSuccessStatusCode();
var json = await response.Content.ReadFromJsonAsync<JsonElement>();
Assert.True(json.GetProperty("isAdmin").GetBoolean());
await factory.WithDbContextAsync(async db =>
{
var owner = await db.Players.AsNoTracking().SingleAsync(p => p.Username == "adminuser");
Assert.True(owner.IsOwner);
Assert.True(owner.IsAdmin);
});
}
[Fact]
@@ -133,7 +140,23 @@ public class AuthTests
Assert.Equal(HttpStatusCode.BadRequest, secondAdmin.StatusCode);
var body = await secondAdmin.Content.ReadFromJsonAsync<JsonElement>();
Assert.Equal("Admin registration via admin key is disabled after the first admin account.", body.GetProperty("error").GetString());
Assert.Equal("Admin registration via admin key is disabled once an owner account exists.", body.GetProperty("error").GetString());
}
[Fact]
public async Task Auth_options_reports_owner_existence()
{
await using var factory = new TestWebApplicationFactory();
var client = factory.CreateClientWithCookies();
var before = await client.GetFromJsonAsync<JsonElement>("/api/auth/options");
Assert.False(before.GetProperty("ownerExists").GetBoolean());
var ownerRegister = await client.RegisterAsync("owner", admin: true);
ownerRegister.EnsureSuccessStatusCode();
var after = await client.GetFromJsonAsync<JsonElement>("/api/auth/options");
Assert.True(after.GetProperty("ownerExists").GetBoolean());
}
[Fact]