mirror of
https://github.com/JogmansonReal/Radicade.git
synced 2026-10-27 07:59:03 -07:00
Radicade Public Code
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
using System.Reflection;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using RadicadeServer.API;
|
||||
using RadicadeServer.DB;
|
||||
using RadicadeServer.RecNet.Auth;
|
||||
using Xunit;
|
||||
|
||||
namespace RadicadeServer.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// The role lookup the client uses to decide whether to show developer-only content.
|
||||
///
|
||||
/// The 2023 client builds this URL from a literal of the form <c>role/{0}/{1}</c> and resolves
|
||||
/// it against its RecNet base, and the reference implementation serves it at the host root
|
||||
/// (<c>GET /role/developer/:id</c>). This server mounted it only under <c>auth/role/</c>, so on a
|
||||
/// client whose base does not end in <c>auth/</c> the lookup 404s - measured against the live
|
||||
/// server: <c>/role/developer/6</c> answered 404 while <c>/auth/role/developer/6</c> answered 200.
|
||||
/// A 404 here does not error visibly; it just silently reports "not a developer", which hides
|
||||
/// everything the role gates.
|
||||
///
|
||||
/// This is the same class of bug the equipment backpack hit - a correct handler behind a missing
|
||||
/// prefix - which is why the route assertions are the point of this file.
|
||||
/// </summary>
|
||||
[Collection(DbFactoryCollection.Name)]
|
||||
public class RoleLookupTests
|
||||
{
|
||||
private static RecDBContext NewDb() => new(
|
||||
TestDbOptions.Create()
|
||||
.UseInMemoryDatabase(Guid.NewGuid().ToString())
|
||||
.Options);
|
||||
|
||||
private static async Task<RecDBContext> WithProfile(long accountId, string? roles)
|
||||
{
|
||||
var db = NewDb();
|
||||
db.Profiles.Add(new Profile
|
||||
{
|
||||
accountId = accountId,
|
||||
role = roles,
|
||||
username = $"player{accountId}",
|
||||
displayName = $"Player {accountId}",
|
||||
profileImage = DBConfig.DefaultProfileImage
|
||||
});
|
||||
await db.SaveChangesAsync();
|
||||
return db;
|
||||
}
|
||||
|
||||
/// <summary>Every route the client can reach this lookup on: prefix + "{roleType}/{id}".</summary>
|
||||
private static string[] Routes()
|
||||
{
|
||||
var method = typeof(RolesAPI).GetMethod(nameof(RolesAPI.GetDeveloperRole))!;
|
||||
var templates = method.GetCustomAttributes<HttpGetAttribute>().Select(a => a.Template).ToArray();
|
||||
|
||||
return typeof(RolesAPI).GetCustomAttributes<RouteAttribute>()
|
||||
.SelectMany(prefix => templates.Select(suffix => $"{prefix.Template}/{suffix}"))
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TheRoleLookupAnswersAtTheRootPathTheClientBuilds()
|
||||
{
|
||||
var routes = Routes();
|
||||
|
||||
// What the client's literal resolves to when its base is the host root, and what the
|
||||
// reference implementation serves.
|
||||
Assert.Contains("role/{roleType}/{id}", routes);
|
||||
|
||||
// The path this server already had, kept so nothing that works today starts 404ing.
|
||||
Assert.Contains("auth/role/{roleType}/{id}", routes);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("developer")]
|
||||
[InlineData("moderator")]
|
||||
public void BothWalkedRolesReachTheLookup(string roleType)
|
||||
{
|
||||
var routes = Routes().Select(route => route.Replace("{roleType}", roleType)).ToArray();
|
||||
|
||||
Assert.Contains($"role/{roleType}/{{id}}", routes);
|
||||
Assert.Contains($"auth/role/{roleType}/{{id}}", routes);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TheLookupReportsTheProfilesRoles()
|
||||
{
|
||||
using var db = await WithProfile(6, "developer,moderator");
|
||||
var api = new RolesAPI(db);
|
||||
|
||||
Assert.True(await api.GetDeveloperRole("developer", 6));
|
||||
Assert.True(await api.GetDeveloperRole("moderator", 6));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ARoleTheProfileDoesNotHoldIsFalse()
|
||||
{
|
||||
using var db = await WithProfile(6, "developer");
|
||||
var api = new RolesAPI(db);
|
||||
|
||||
// The client reads a bare boolean, so a role that is not held has to answer false
|
||||
// rather than error - it is a gate, not a lookup that can fail.
|
||||
Assert.False(await api.GetDeveloperRole("moderator", 6));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AnUnknownOrRolelessAccountIsNotADeveloper()
|
||||
{
|
||||
using var roleless = await WithProfile(1, null);
|
||||
Assert.False(await new RolesAPI(roleless).GetDeveloperRole("developer", 1));
|
||||
|
||||
using var db = NewDb();
|
||||
Assert.False(await new RolesAPI(db).GetDeveloperRole("developer", 99999));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RolesAreMatchedCaseInsensitivelyAndTrimmed()
|
||||
{
|
||||
// The admin panel writes whatever the operator picked, joined with commas.
|
||||
using var db = await WithProfile(6, " Developer , Moderator ");
|
||||
var api = new RolesAPI(db);
|
||||
|
||||
Assert.True(await api.GetDeveloperRole("developer", 6));
|
||||
Assert.True(await api.GetDeveloperRole("moderator", 6));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GrantingTheDeveloperRoleFlipsTheLookup()
|
||||
{
|
||||
using var db = await WithProfile(6, null);
|
||||
var api = new RolesAPI(db);
|
||||
|
||||
Assert.False(await api.GetDeveloperRole("developer", 6));
|
||||
|
||||
var profile = await db.Profiles.FirstAsync(x => x.accountId == 6);
|
||||
profile.role = "developer";
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
Assert.True(await api.GetDeveloperRole("developer", 6));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user