mirror of
https://github.com/JogmansonReal/Radicade.git
synced 2026-10-26 07:29:00 -07:00
217 lines
7.9 KiB
C#
217 lines
7.9 KiB
C#
using Microsoft.AspNetCore.Http;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using Microsoft.AspNetCore.Routing;
|
|
using Microsoft.AspNetCore.Routing.Template;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using RadicadeServer.Admin;
|
|
using RadicadeServer.DB;
|
|
using RadicadeServer.RecNet.Accounts.Local;
|
|
using RadicadeServer.RecNet.Auth;
|
|
using Xunit;
|
|
|
|
namespace RadicadeServer.Tests;
|
|
|
|
/// <summary>
|
|
/// The panel drives every player action through POST admin/panel/players/{id}/{kind}, and some of
|
|
/// those actions are named with a slash in them: the roles box's Add ("roles/add") and Clear
|
|
/// ("roles/clear"), plus settings/birthday, settings/delete, settings/reset and
|
|
/// settings/profilepicture. A slash-named action is two path segments, so routing hands the
|
|
/// controller kind="roles" and sub="add" - while the action the switch matches on is the rejoined
|
|
/// "roles/add". These tests hand the action its arguments exactly the way routing splits them, so
|
|
/// splitting the name again fails here instead of only in the deployed panel, where a miss shows
|
|
/// up as the toast "Gone" (the panel's name for 404).
|
|
/// </summary>
|
|
public class AdminPanelPlayerActionTests
|
|
{
|
|
private const string Secret = "test-bot-secret";
|
|
private const long Player = 2;
|
|
|
|
private static RecDBContext NewDb() => new(
|
|
TestDbOptions.Create()
|
|
.UseInMemoryDatabase(Guid.NewGuid().ToString())
|
|
.Options);
|
|
|
|
/// <summary>A database holding just the profile a player action needs.</summary>
|
|
private static async Task<RecDBContext> WithPlayer(string? roles)
|
|
{
|
|
var db = NewDb();
|
|
db.Profiles.Add(new Profile
|
|
{
|
|
accountId = Player,
|
|
username = "testplayer",
|
|
displayName = "Test Player",
|
|
profileImage = "",
|
|
role = roles
|
|
});
|
|
await db.SaveChangesAsync();
|
|
return db;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Runs one slash-named action the way the route delivers it: the request is
|
|
/// POST admin/panel/players/{id}/{kind}/{sub}, so the controller receives the name in halves.
|
|
/// </summary>
|
|
private static async Task<IActionResult> SendAction(RecDBContext db, string kind, string sub, string query)
|
|
{
|
|
var context = new DefaultHttpContext();
|
|
context.Request.QueryString = new QueryString("?" + query);
|
|
var api = new AdminPanelAPI(db)
|
|
{
|
|
ControllerContext = new ControllerContext { HttpContext = context }
|
|
};
|
|
|
|
var original = DBConfig.AdminApiKey;
|
|
DBConfig.AdminApiKey = Secret;
|
|
try
|
|
{
|
|
return await api.PlayerPanelSubAction(Player, kind, sub, Secret);
|
|
}
|
|
finally
|
|
{
|
|
DBConfig.AdminApiKey = original;
|
|
}
|
|
}
|
|
|
|
private static async Task<Profile> StoredProfile(RecDBContext db)
|
|
=> await db.Profiles.SingleAsync(x => x.accountId == Player);
|
|
|
|
/// <summary>
|
|
/// The link every test here rests on: the URL the panel builds for a slash-named action -
|
|
/// POST admin/panel/players/{id}/roles/add - is the route whose last two segments are
|
|
/// {kind}/{sub}, and the values it hands the action really are kind="roles", sub="add".
|
|
/// If the route is ever reshaped so the panel's own URL no longer lands there, the actions
|
|
/// below would start passing for the wrong reason; this fails first.
|
|
/// </summary>
|
|
[Fact]
|
|
public void ThePanelsUrlForASlashNamedActionBindsKindAndSub()
|
|
{
|
|
var matcher = new TemplateMatcher(
|
|
TemplateParser.Parse("admin/panel/players/{playerId:long}/{kind}/{sub}"),
|
|
new RouteValueDictionary());
|
|
var values = new RouteValueDictionary();
|
|
|
|
Assert.True(matcher.TryMatch(new PathString("/admin/panel/players/2/roles/add"), values));
|
|
Assert.Equal("2", values["playerId"]);
|
|
Assert.Equal("roles", values["kind"]);
|
|
Assert.Equal("add", values["sub"]);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task AddingRolesStoresThemOnThePlayer()
|
|
{
|
|
var db = await WithPlayer(null);
|
|
|
|
var result = await SendAction(db, "roles", "add", "roles=developer,moderator");
|
|
|
|
Assert.IsType<OkObjectResult>(result);
|
|
|
|
var profile = await StoredProfile(db);
|
|
Assert.Equal("developer,moderator", profile.role);
|
|
|
|
// The stored role is what the game reads, so the flags have to follow it.
|
|
Assert.True(profile.HasRole("moderator"));
|
|
Assert.False(profile.HasRole("influencer"));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task AddingARoleKeepsTheRolesThePlayerAlreadyHad()
|
|
{
|
|
var db = await WithPlayer("creatorforum");
|
|
|
|
// The panel's boxes are ticked independently, so it can send a role the player already
|
|
// holds alongside new ones; that must not duplicate it or drop what was already there.
|
|
var result = await SendAction(db, "roles", "add", "roles=Developer,creatorforum,keepsake");
|
|
|
|
Assert.IsType<OkObjectResult>(result);
|
|
Assert.Equal("creatorforum,Developer,keepsake", (await StoredProfile(db)).role);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task AddingWithNothingPickedIsRefusedRatherThanClearingTheRoles()
|
|
{
|
|
var db = await WithPlayer("developer");
|
|
|
|
// The panel keeps Add disabled until a box is ticked, but a lost pick must still be a
|
|
// refusal - "picked nothing" must never read as "clear what this player has".
|
|
var result = await SendAction(db, "roles", "add", "");
|
|
|
|
Assert.IsType<BadRequestObjectResult>(result);
|
|
Assert.Equal("developer", (await StoredProfile(db)).role);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ClearingRolesEmptiesThem()
|
|
{
|
|
var db = await WithPlayer("developer,moderator,keepsake");
|
|
|
|
var result = await SendAction(db, "roles", "clear", "");
|
|
|
|
Assert.IsType<OkObjectResult>(result);
|
|
|
|
var profile = await StoredProfile(db);
|
|
Assert.Equal("", profile.role);
|
|
Assert.False(profile.HasRole("developer"));
|
|
Assert.False(profile.HasRole("moderator"));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ClearingRolesOnAPlayerWithNoneLeftIsStillAccepted()
|
|
{
|
|
var db = await WithPlayer(null);
|
|
|
|
// Nothing to clear is not an error: the panel's confirm dialog already ran.
|
|
var result = await SendAction(db, "roles", "clear", "");
|
|
|
|
Assert.IsType<OkObjectResult>(result);
|
|
Assert.Equal("", (await StoredProfile(db)).role);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task TheOtherSlashNamedActionsReachTheirCasesToo()
|
|
{
|
|
var db = await WithPlayer("developer");
|
|
|
|
// Fixing the split must lift every slash-named action, not just the roles box - the
|
|
// settings panels build their URLs the same way.
|
|
var result = await SendAction(db, "settings", "birthday", "birthday=2004-02-29");
|
|
|
|
Assert.IsType<OkObjectResult>(result);
|
|
|
|
var settings = await db.PlayerSettings.SingleAsync(x => x.AccountId == Player);
|
|
Assert.Equal("2004-02-29", settings.DeserializeSettings().Single(x => x.Key == "birthday").Value);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task AnUnknownSlashNamedActionIsStillNotFound()
|
|
{
|
|
var db = await WithPlayer("developer");
|
|
|
|
// Rejoining the halves must not turn the route into a catch-all.
|
|
var result = await SendAction(db, "roles", "bogus", "");
|
|
|
|
var notFound = Assert.IsType<NotFoundObjectResult>(result);
|
|
Assert.Equal("Unknown player action 'roles/bogus'.", notFound.Value);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ASingleSegmentActionStillReachesItsCase()
|
|
{
|
|
var db = await WithPlayer("developer");
|
|
|
|
// The one-segment route and the two-segment one share the same switch; a plain action
|
|
// (no slash in its name) still arrives whole.
|
|
var api = new AdminPanelAPI(db);
|
|
var original = DBConfig.AdminApiKey;
|
|
DBConfig.AdminApiKey = Secret;
|
|
try
|
|
{
|
|
var result = await api.PlayerPanelAction(Player, "unban", Secret);
|
|
Assert.IsType<OkObjectResult>(result);
|
|
}
|
|
finally
|
|
{
|
|
DBConfig.AdminApiKey = original;
|
|
}
|
|
}
|
|
}
|