Implement admin back-pass flow and guarded admin actions
This commit is contained in:
@@ -77,7 +77,13 @@ public class AdminTests
|
||||
Score = 8
|
||||
});
|
||||
|
||||
var resp = await admin.DeleteAsync($"/api/admin/players/{await player.GetProfileIdAsync()}");
|
||||
var resp = await admin.SendAsync(new HttpRequestMessage(HttpMethod.Delete, $"/api/admin/players/{await player.GetProfileIdAsync()}")
|
||||
{
|
||||
Content = JsonContent.Create(new
|
||||
{
|
||||
AdminPassword = "Pass123!"
|
||||
})
|
||||
});
|
||||
resp.EnsureSuccessStatusCode();
|
||||
|
||||
await factory.WithDbContextAsync(db =>
|
||||
@@ -189,7 +195,10 @@ public class AdminTests
|
||||
await player.RegisterAsync("player");
|
||||
await player.CreateSuggestionAsync("Keep");
|
||||
|
||||
var reset = await admin.PostAsJsonAsync("/api/admin/reset", new { });
|
||||
var reset = await admin.PostAsJsonAsync("/api/admin/reset", new
|
||||
{
|
||||
AdminPassword = "Pass123!"
|
||||
});
|
||||
reset.EnsureSuccessStatusCode();
|
||||
|
||||
await factory.WithDbContextAsync(db =>
|
||||
@@ -209,7 +218,10 @@ public class AdminTests
|
||||
}
|
||||
});
|
||||
|
||||
var factoryReset = await admin.PostAsJsonAsync("/api/admin/factory-reset", new { });
|
||||
var factoryReset = await admin.PostAsJsonAsync("/api/admin/factory-reset", new
|
||||
{
|
||||
AdminPassword = "Pass123!"
|
||||
});
|
||||
factoryReset.EnsureSuccessStatusCode();
|
||||
|
||||
await factory.WithDbContextAsync(db =>
|
||||
@@ -229,21 +241,26 @@ public class AdminTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Admin_results_closing_moves_back_to_vote_and_clears_finalize()
|
||||
public async Task Admin_results_closing_moves_only_players_with_suggestions_back_to_vote()
|
||||
{
|
||||
await using var factory = new TestWebApplicationFactory();
|
||||
var admin = factory.CreateClientWithCookies();
|
||||
await admin.RegisterAsync("admin", admin: true);
|
||||
var player = factory.CreateClientWithCookies();
|
||||
await player.RegisterAsync("player");
|
||||
var fresh = factory.CreateClientWithCookies();
|
||||
await fresh.RegisterAsync("fresh");
|
||||
await player.CreateSuggestionAsync("Player game");
|
||||
|
||||
var open = await admin.PostAsJsonAsync("/api/admin/results", new { resultsOpen = true });
|
||||
open.EnsureSuccessStatusCode();
|
||||
|
||||
await factory.WithDbContextAsync(async db =>
|
||||
{
|
||||
var p = await db.Players.FirstAsync(x => !x.IsAdmin);
|
||||
var p = await db.Players.SingleAsync(x => x.Username == "player");
|
||||
var freshPlayer = await db.Players.SingleAsync(x => x.Username == "fresh");
|
||||
p.VotesFinal = true;
|
||||
freshPlayer.VotesFinal = true;
|
||||
var state = await db.AppState.SingleAsync();
|
||||
state.UpdatedAt = DateTimeOffset.UnixEpoch;
|
||||
await db.SaveChangesAsync();
|
||||
@@ -254,9 +271,12 @@ public class AdminTests
|
||||
|
||||
await factory.WithDbContextAsync(async db =>
|
||||
{
|
||||
var p = await db.Players.FirstAsync(x => !x.IsAdmin);
|
||||
var p = await db.Players.SingleAsync(x => x.Username == "player");
|
||||
var freshPlayer = await db.Players.SingleAsync(x => x.Username == "fresh");
|
||||
Assert.Equal(Phase.Vote, p.CurrentPhase);
|
||||
Assert.False(p.VotesFinal);
|
||||
Assert.Equal(Phase.Suggest, freshPlayer.CurrentPhase);
|
||||
Assert.False(freshPlayer.VotesFinal);
|
||||
var state = await db.AppState.AsNoTracking().SingleAsync();
|
||||
Assert.False(state.ResultsOpen);
|
||||
Assert.True(state.UpdatedAt > DateTimeOffset.UnixEpoch);
|
||||
@@ -425,7 +445,10 @@ public class AdminTests
|
||||
await db.SaveChangesAsync();
|
||||
});
|
||||
|
||||
var reset = await admin.PostAsJsonAsync("/api/admin/reset", new { });
|
||||
var reset = await admin.PostAsJsonAsync("/api/admin/reset", new
|
||||
{
|
||||
AdminPassword = "Pass123!"
|
||||
});
|
||||
reset.EnsureSuccessStatusCode();
|
||||
|
||||
await factory.WithDbContextAsync(async db =>
|
||||
@@ -437,7 +460,10 @@ public class AdminTests
|
||||
Assert.False(state.ResultsOpen);
|
||||
});
|
||||
|
||||
var factoryReset = await admin.PostAsJsonAsync("/api/admin/factory-reset", new { });
|
||||
var factoryReset = await admin.PostAsJsonAsync("/api/admin/factory-reset", new
|
||||
{
|
||||
AdminPassword = "Pass123!"
|
||||
});
|
||||
factoryReset.EnsureSuccessStatusCode();
|
||||
await factory.WithDbContextAsync(async db =>
|
||||
{
|
||||
@@ -445,4 +471,56 @@ public class AdminTests
|
||||
Assert.False(state.ResultsOpen);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Admin_destructive_actions_require_valid_admin_password()
|
||||
{
|
||||
await using var factory = new TestWebApplicationFactory();
|
||||
var admin = factory.CreateClientWithCookies();
|
||||
await admin.RegisterAsync("admin", admin: true);
|
||||
var player = factory.CreateClientWithCookies();
|
||||
await player.RegisterAsync("victim");
|
||||
|
||||
var delete = await admin.SendAsync(new HttpRequestMessage(HttpMethod.Delete, $"/api/admin/players/{await player.GetProfileIdAsync()}")
|
||||
{
|
||||
Content = JsonContent.Create(new
|
||||
{
|
||||
AdminPassword = "wrong"
|
||||
})
|
||||
});
|
||||
Assert.Equal(HttpStatusCode.Unauthorized, delete.StatusCode);
|
||||
|
||||
var reset = await admin.PostAsJsonAsync("/api/admin/reset", new
|
||||
{
|
||||
AdminPassword = "wrong"
|
||||
});
|
||||
Assert.Equal(HttpStatusCode.Unauthorized, reset.StatusCode);
|
||||
|
||||
var factoryReset = await admin.PostAsJsonAsync("/api/admin/factory-reset", new
|
||||
{
|
||||
AdminPassword = "wrong"
|
||||
});
|
||||
Assert.Equal(HttpStatusCode.Unauthorized, factoryReset.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Admin_can_move_voter_back_to_suggest_via_phase_endpoint()
|
||||
{
|
||||
await using var factory = new TestWebApplicationFactory();
|
||||
var admin = factory.CreateClientWithCookies();
|
||||
await admin.RegisterAsync("admin", admin: true);
|
||||
|
||||
var player = factory.CreateClientWithCookies();
|
||||
await player.RegisterAsync("moveme");
|
||||
await player.AdvanceToVoteAsync("Move seed");
|
||||
|
||||
var move = await admin.PostAsJsonAsync($"/api/admin/players/{await player.GetProfileIdAsync()}/phase", new
|
||||
{
|
||||
Phase = "Suggest"
|
||||
});
|
||||
move.EnsureSuccessStatusCode();
|
||||
|
||||
var me = await player.GetFromJsonAsync<JsonElement>("/api/me");
|
||||
Assert.Equal(nameof(Phase.Suggest), me.GetProperty("currentPhase").GetString());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -224,6 +224,32 @@ public class StateTests
|
||||
Assert.Equal(nameof(Phase.Suggest), me.GetProperty("currentPhase").GetString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Phase_prev_with_granted_joker_moves_player_back_once_and_consumes_it()
|
||||
{
|
||||
await using var factory = new TestWebApplicationFactory();
|
||||
var admin = factory.CreateClientWithCookies();
|
||||
await admin.RegisterAsync("admin", admin: true);
|
||||
|
||||
var player = factory.CreateClientWithCookies();
|
||||
await player.RegisterAsync("jokerback");
|
||||
await player.AdvanceToVoteAsync("Joker back seed");
|
||||
|
||||
var grant = await admin.PostAsJsonAsync("/api/admin/joker", new { playerId = await player.GetProfileIdAsync() });
|
||||
grant.EnsureSuccessStatusCode();
|
||||
|
||||
var back = await player.PostAsJsonAsync("/api/me/phase/prev", new { });
|
||||
back.EnsureSuccessStatusCode();
|
||||
|
||||
var meAfterBack = await player.GetFromJsonAsync<JsonElement>("/api/me");
|
||||
Assert.Equal(nameof(Phase.Suggest), meAfterBack.GetProperty("currentPhase").GetString());
|
||||
Assert.False(meAfterBack.GetProperty("hasJoker").GetBoolean());
|
||||
|
||||
await player.PostAsJsonAsync("/api/me/phase/next", new { });
|
||||
var denied = await player.PostAsJsonAsync("/api/me/phase/prev", new { });
|
||||
Assert.Equal(HttpStatusCode.BadRequest, denied.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task State_endpoint_requires_auth_and_counts()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user