Files
2026-10-16 23:33:13 -07:00

140 lines
5.0 KiB
C#

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));
}
}