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,358 @@
|
||||
using System.Security.Claims;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Storage;
|
||||
using RadicadeServer;
|
||||
using RadicadeServer.DB;
|
||||
using RadicadeServer.RecNet.Auth;
|
||||
using RadicadeServer.RecNet.Enums;
|
||||
using RadicadeServer.RecNet.Enums.Rooms;
|
||||
using RadicadeServer.RecNet.Rooms;
|
||||
using RadicadeServer.RoomServer;
|
||||
using RadicadeServer.Utils;
|
||||
using Xunit;
|
||||
|
||||
namespace RadicadeServer.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// A player's room role is what decides everything they may do in that room, and the client
|
||||
/// reads it out of the room payload. These pin the ways that went wrong: the role list being
|
||||
/// served empty (which silently removed every co-owner's and moderator's permissions), the
|
||||
/// owner missing from it, an invite matching the target's role in a DIFFERENT room, and the
|
||||
/// Creator tier being grantable through the role route.
|
||||
/// </summary>
|
||||
[Collection(DbFactoryCollection.Name)]
|
||||
public class RoomRolesTests
|
||||
{
|
||||
private const long CreatorId = 11;
|
||||
private const long RoomId = 5;
|
||||
private const long MemberId = 22;
|
||||
private const long StrangerId = 999;
|
||||
|
||||
private static readonly JsonSerializerOptions ServerJson = new()
|
||||
{
|
||||
PropertyNamingPolicy = null,
|
||||
WriteIndented = true,
|
||||
};
|
||||
|
||||
private static string Serialize(object? value) => JsonSerializer.Serialize(value, ServerJson);
|
||||
|
||||
private sealed class TestDbContextFactory : IDbContextFactory<RecDBContext>
|
||||
{
|
||||
private readonly DbContextOptions<RecDBContext> options;
|
||||
|
||||
public TestDbContextFactory()
|
||||
{
|
||||
// A unique database name isolates each test; the store is NOT given its own
|
||||
// InMemoryDatabaseRoot, because a distinct root makes Entity Framework build a
|
||||
// distinct internal service provider every time and it throws once the process
|
||||
// has more than twenty of them.
|
||||
options = TestDbOptions.Create()
|
||||
.UseInMemoryDatabase(Guid.NewGuid().ToString())
|
||||
.Options;
|
||||
}
|
||||
|
||||
public RecDBContext CreateDbContext() => new(options);
|
||||
}
|
||||
|
||||
private static (TestDbContextFactory Factory, RecDBContext Db) CreateContext()
|
||||
{
|
||||
var factory = new TestDbContextFactory();
|
||||
RoomUtils.DbFactory = factory;
|
||||
MatchmakingUtils.DbFactory = factory;
|
||||
return (factory, factory.CreateDbContext());
|
||||
}
|
||||
|
||||
private static void SetUser(Controller controller, long accountId)
|
||||
{
|
||||
var identity = new ClaimsIdentity("TestAuth");
|
||||
identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, accountId.ToString()));
|
||||
controller.ControllerContext = new ControllerContext
|
||||
{
|
||||
HttpContext = new DefaultHttpContext { User = new ClaimsPrincipal(identity) }
|
||||
};
|
||||
}
|
||||
|
||||
private static void SetFormBody(Controller controller, string body)
|
||||
{
|
||||
var request = controller.ControllerContext.HttpContext.Request;
|
||||
request.ContentType = "application/x-www-form-urlencoded";
|
||||
request.Body = new MemoryStream(Encoding.UTF8.GetBytes(body));
|
||||
}
|
||||
|
||||
private static void SetQuery(Controller controller, string query)
|
||||
=> controller.ControllerContext.HttpContext.Request.QueryString = new QueryString(query);
|
||||
|
||||
private static async Task SeedRoomAsync(RecDBContext db, long roomId = RoomId, long creatorId = CreatorId)
|
||||
{
|
||||
var creator = await db.Players.FirstOrDefaultAsync(x => x.AccountId == creatorId);
|
||||
if (creator == null)
|
||||
db.Players.Add(new PlayerData { AccountId = creatorId, Hashword = "x" });
|
||||
|
||||
var member = await db.Players.FirstOrDefaultAsync(x => x.AccountId == MemberId);
|
||||
if (member == null)
|
||||
db.Players.Add(new PlayerData { AccountId = MemberId, Hashword = "x" });
|
||||
|
||||
db.Rooms.Add(new RoomData
|
||||
{
|
||||
RoomId = roomId,
|
||||
Name = $"Room {roomId}",
|
||||
CreatorAccountId = creatorId,
|
||||
Accessibility = Accessibility.Public,
|
||||
MaxPlayers = 8,
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
});
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
private static async Task<RoomRole?> ReadPersistedRoleAsync(TestDbContextFactory factory, long roomId, long accountId)
|
||||
{
|
||||
await using var db = factory.CreateDbContext();
|
||||
return await db.RoomRoles.AsNoTracking().FirstOrDefaultAsync(x => x.RoomId == roomId && x.AccountId == accountId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RolesAreServedEvenWhenTheHighRanksConfigIsMissing()
|
||||
{
|
||||
var (_, db) = CreateContext();
|
||||
await using var _db = db;
|
||||
|
||||
await SeedRoomAsync(db);
|
||||
db.RoomRoles.Add(new RoomRole { RoomId = RoomId, AccountId = MemberId, Role = RoleType.CoOwner, InvitedRole = RoleType.None });
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
// No ServerConfig "HighRanks" row: the role list used to come back empty here, which
|
||||
// took every co-owner's and moderator's permissions with it.
|
||||
var roles = await RoomUtils.GetRoles(RoomId);
|
||||
|
||||
Assert.Contains(roles, x => x.AccountId == MemberId && x.Role == RoleType.CoOwner);
|
||||
Assert.True(await MatchmakingUtils.IsAtLeastRole(MemberId, RoomId, RoleType.CoOwner));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TheRoomsCreatorAlwaysAppearsAsCreator()
|
||||
{
|
||||
var (_, db) = CreateContext();
|
||||
await using var _db = db;
|
||||
|
||||
await SeedRoomAsync(db);
|
||||
|
||||
var roles = await RoomUtils.GetRoles(RoomId);
|
||||
var row = Assert.Single(roles);
|
||||
Assert.Equal(CreatorId, row.AccountId);
|
||||
Assert.Equal(RoleType.Creator, row.Role);
|
||||
|
||||
// ...and the room the client loads carries the same entry, so the owner's own watch
|
||||
// shows them as the owner.
|
||||
var room = await RoomUtils.GetRoom(RoomId);
|
||||
Assert.Contains(room!.Roles, x => x.AccountId == CreatorId && x.Role == RoleType.Creator);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RoleLookupUsesTheHighestRoleAnAccountHolds()
|
||||
{
|
||||
var (_, db) = CreateContext();
|
||||
await using var _db = db;
|
||||
|
||||
await SeedRoomAsync(db);
|
||||
db.RoomRoles.AddRange(
|
||||
new RoomRole { RoomId = RoomId, AccountId = MemberId, Role = RoleType.Moderator, InvitedRole = RoleType.None },
|
||||
new RoomRole { RoomId = RoomId, AccountId = MemberId, Role = RoleType.Creator, InvitedRole = RoleType.None });
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
var role = await RoomUtils.GetRoleForPlayer(MemberId, RoomId);
|
||||
|
||||
Assert.NotNull(role);
|
||||
Assert.Equal(RoleType.Creator, role!.Role);
|
||||
Assert.True(await MatchmakingUtils.IsAtLeastRole(MemberId, RoomId, RoleType.CoOwner));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InvitingAPlayerOnlyTouchesThisRoomsRole()
|
||||
{
|
||||
var (factory, db) = CreateContext();
|
||||
await using var _db = db;
|
||||
|
||||
await SeedRoomAsync(db, RoomId, CreatorId);
|
||||
await SeedRoomAsync(db, 6, CreatorId);
|
||||
db.RoomRoles.Add(new RoomRole { RoomId = 6, AccountId = MemberId, Role = RoleType.CoOwner, InvitedRole = RoleType.None });
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
var api = new RoomServerAPI(db);
|
||||
SetUser(api, CreatorId);
|
||||
SetFormBody(api, "role=30");
|
||||
|
||||
var result = await api.InvitePlayerToRole(RoomId, MemberId);
|
||||
using var doc = JsonDocument.Parse(Serialize(result));
|
||||
Assert.True(doc.RootElement.GetProperty("success").GetBoolean());
|
||||
|
||||
// The other room's role is untouched...
|
||||
var other = await ReadPersistedRoleAsync(factory, 6, MemberId);
|
||||
Assert.NotNull(other);
|
||||
Assert.Equal(RoleType.CoOwner, other!.Role);
|
||||
Assert.Null(other.LastChangedByAccountId);
|
||||
|
||||
// ...and this room has its own pending invite.
|
||||
var invited = await ReadPersistedRoleAsync(factory, RoomId, MemberId);
|
||||
Assert.NotNull(invited);
|
||||
Assert.Equal(RoleType.None, invited!.Role);
|
||||
Assert.Equal(RoleType.CoOwner, invited.InvitedRole);
|
||||
Assert.Equal(CreatorId, invited.LastChangedByAccountId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SettingARoleRecordsWhoChangedItAndReturnsTheRoom()
|
||||
{
|
||||
var (factory, db) = CreateContext();
|
||||
await using var _db = db;
|
||||
|
||||
await SeedRoomAsync(db);
|
||||
|
||||
var api = new RoomServerAPI(db);
|
||||
SetUser(api, CreatorId);
|
||||
SetQuery(api, "?role=20");
|
||||
|
||||
var result = await api.SetPlayerRole(RoomId, MemberId);
|
||||
using var doc = JsonDocument.Parse(Serialize(result));
|
||||
|
||||
Assert.True(doc.RootElement.GetProperty("success").GetBoolean());
|
||||
Assert.Equal(RoomId, doc.RootElement.GetProperty("value").GetProperty("RoomId").GetInt64());
|
||||
|
||||
var granted = await ReadPersistedRoleAsync(factory, RoomId, MemberId);
|
||||
Assert.Equal(RoleType.Moderator, granted!.Role);
|
||||
Assert.Equal(RoleType.None, granted.InvitedRole);
|
||||
Assert.Equal(CreatorId, granted.LastChangedByAccountId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RoleEntriesCarryLastChangedByAccountIdOnTheWire()
|
||||
{
|
||||
var (_, db) = CreateContext();
|
||||
await using var _db = db;
|
||||
|
||||
await SeedRoomAsync(db);
|
||||
db.RoomRoles.Add(new RoomRole
|
||||
{
|
||||
RoomId = RoomId,
|
||||
AccountId = MemberId,
|
||||
Role = RoleType.CoOwner,
|
||||
InvitedRole = RoleType.None,
|
||||
LastChangedByAccountId = CreatorId,
|
||||
});
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
var room = await RoomUtils.GetRoom(RoomId);
|
||||
using var doc = JsonDocument.Parse(Serialize(room!.Roles));
|
||||
var entry = doc.RootElement.EnumerateArray()
|
||||
.Single(x => x.GetProperty("AccountId").GetInt64() == MemberId);
|
||||
|
||||
Assert.Equal(CreatorId, entry.GetProperty("LastChangedByAccountId").GetInt64());
|
||||
Assert.True(entry.TryGetProperty("InvitedRole", out _));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TheCreatorTierCannotBeGrantedThroughTheRoleRoute()
|
||||
{
|
||||
var (factory, db) = CreateContext();
|
||||
await using var _db = db;
|
||||
|
||||
await SeedRoomAsync(db);
|
||||
|
||||
var api = new RoomServerAPI(db);
|
||||
SetUser(api, CreatorId);
|
||||
SetFormBody(api, "role=255");
|
||||
|
||||
var result = await api.SetPlayerRole(RoomId, MemberId);
|
||||
using var doc = JsonDocument.Parse(Serialize(result));
|
||||
|
||||
Assert.False(doc.RootElement.GetProperty("success").GetBoolean());
|
||||
Assert.Null(await ReadPersistedRoleAsync(factory, RoomId, MemberId));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ARequestWithNoRoleIsRejectedInsteadOfDemotingThePlayer()
|
||||
{
|
||||
var (factory, db) = CreateContext();
|
||||
await using var _db = db;
|
||||
|
||||
await SeedRoomAsync(db);
|
||||
db.RoomRoles.Add(new RoomRole { RoomId = RoomId, AccountId = MemberId, Role = RoleType.CoOwner, InvitedRole = RoleType.None });
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
var api = new RoomServerAPI(db);
|
||||
SetUser(api, CreatorId);
|
||||
SetFormBody(api, ""); // no role field at all
|
||||
|
||||
var result = await api.SetPlayerRole(RoomId, MemberId);
|
||||
using var doc = JsonDocument.Parse(Serialize(result));
|
||||
|
||||
Assert.False(doc.RootElement.GetProperty("success").GetBoolean());
|
||||
Assert.Contains("valid role", doc.RootElement.GetProperty("error").GetString()!);
|
||||
|
||||
// An unbound role must never read as None, which would strip the co-owner.
|
||||
var unchanged = await ReadPersistedRoleAsync(factory, RoomId, MemberId);
|
||||
Assert.Equal(RoleType.CoOwner, unchanged!.Role);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DemotingAPlayerRemovesTheirRoleEntry()
|
||||
{
|
||||
var (factory, db) = CreateContext();
|
||||
await using var _db = db;
|
||||
|
||||
await SeedRoomAsync(db);
|
||||
db.RoomRoles.Add(new RoomRole { RoomId = RoomId, AccountId = MemberId, Role = RoleType.CoOwner, InvitedRole = RoleType.None });
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
var api = new RoomServerAPI(db);
|
||||
SetUser(api, CreatorId);
|
||||
SetFormBody(api, "role=0");
|
||||
|
||||
var result = await api.SetPlayerRole(RoomId, MemberId);
|
||||
using var doc = JsonDocument.Parse(Serialize(result));
|
||||
|
||||
Assert.True(doc.RootElement.GetProperty("success").GetBoolean());
|
||||
Assert.Null(await ReadPersistedRoleAsync(factory, RoomId, MemberId));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SettingARoleRequiresCoOwnerRights()
|
||||
{
|
||||
var (factory, db) = CreateContext();
|
||||
await using var _db = db;
|
||||
|
||||
await SeedRoomAsync(db, RoomId, StrangerId);
|
||||
|
||||
var api = new RoomServerAPI(db);
|
||||
SetUser(api, CreatorId);
|
||||
SetFormBody(api, "role=30");
|
||||
|
||||
var result = await api.SetPlayerRole(RoomId, MemberId);
|
||||
using var doc = JsonDocument.Parse(Serialize(result));
|
||||
|
||||
Assert.False(doc.RootElement.GetProperty("success").GetBoolean());
|
||||
Assert.Null(await ReadPersistedRoleAsync(factory, RoomId, MemberId));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TheRoomsCreatorCannotBeGivenAnotherRole()
|
||||
{
|
||||
var (_, db) = CreateContext();
|
||||
await using var _db = db;
|
||||
|
||||
await SeedRoomAsync(db);
|
||||
|
||||
var api = new RoomServerAPI(db);
|
||||
SetUser(api, CreatorId);
|
||||
SetFormBody(api, "role=20");
|
||||
|
||||
var result = await api.SetPlayerRole(RoomId, CreatorId);
|
||||
using var doc = JsonDocument.Parse(Serialize(result));
|
||||
|
||||
Assert.False(doc.RootElement.GetProperty("success").GetBoolean());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user