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; /// /// 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 role/{0}/{1} and resolves /// it against its RecNet base, and the reference implementation serves it at the host root /// (GET /role/developer/:id). This server mounted it only under auth/role/, so on a /// client whose base does not end in auth/ the lookup 404s - measured against the live /// server: /role/developer/6 answered 404 while /auth/role/developer/6 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. /// [Collection(DbFactoryCollection.Name)] public class RoleLookupTests { private static RecDBContext NewDb() => new( TestDbOptions.Create() .UseInMemoryDatabase(Guid.NewGuid().ToString()) .Options); private static async Task 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; } /// Every route the client can reach this lookup on: prefix + "{roleType}/{id}". private static string[] Routes() { var method = typeof(RolesAPI).GetMethod(nameof(RolesAPI.GetDeveloperRole))!; var templates = method.GetCustomAttributes().Select(a => a.Template).ToArray(); return typeof(RolesAPI).GetCustomAttributes() .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)); } }