mirror of
https://github.com/JogmansonReal/Radicade.git
synced 2026-10-26 23:49:02 -07:00
Radicade Public Code
This commit is contained in:
@@ -0,0 +1,445 @@
|
||||
using System.Security.Claims;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using RadicadeServer.DB;
|
||||
using RadicadeServer.RecNet.Accounts;
|
||||
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>
|
||||
/// The Moderator Permission Settings screen writes a subroom's overrides, and
|
||||
/// <c>rooms/photon_access_token</c> is the only thing that ever reads them back to the client.
|
||||
/// Both halves failed before: the PUT was a stub that stored nothing, and the token answered an
|
||||
/// empty table, so every box in that screen came up unchecked no matter what a creator had
|
||||
/// saved. These pin the pair - that a stored override survives the round trip, that unchecking
|
||||
/// the box removes it, that (Permission, Role) is the identity so re-sending overwrites, and
|
||||
/// that the token table the client actually applies carries the override over the default.
|
||||
/// </summary>
|
||||
[Collection(DbFactoryCollection.Name)]
|
||||
public class SubRoomPermissionTests
|
||||
{
|
||||
private const long CreatorId = 7;
|
||||
private const long StrangerId = 99;
|
||||
private const long RoomId = 1;
|
||||
private const long SubRoomId = 40;
|
||||
|
||||
private static readonly JsonSerializerOptions ServerJson = new()
|
||||
{
|
||||
PropertyNamingPolicy = null,
|
||||
WriteIndented = true,
|
||||
};
|
||||
|
||||
private sealed class TestDbContextFactory : IDbContextFactory<RecDBContext>
|
||||
{
|
||||
private readonly DbContextOptions<RecDBContext> options;
|
||||
|
||||
public TestDbContextFactory()
|
||||
{
|
||||
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 SetJsonBody(Controller controller, string json)
|
||||
{
|
||||
var request = controller.ControllerContext.HttpContext.Request;
|
||||
request.ContentType = "application/json";
|
||||
request.Body = new MemoryStream(Encoding.UTF8.GetBytes(json));
|
||||
}
|
||||
|
||||
private static RoomServerAPI Api(RecDBContext db, long accountId)
|
||||
{
|
||||
var api = new RoomServerAPI(db);
|
||||
SetUser(api, accountId);
|
||||
return api;
|
||||
}
|
||||
|
||||
private static async Task SeedRoomAsync(RecDBContext db, long creatorId = CreatorId)
|
||||
{
|
||||
db.Rooms.Add(new RoomData
|
||||
{
|
||||
RoomId = RoomId,
|
||||
Name = "Rec Center",
|
||||
CreatorAccountId = creatorId,
|
||||
Accessibility = Accessibility.Public,
|
||||
MaxPlayers = 16,
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
});
|
||||
|
||||
db.SubRooms.Add(new SubRoom
|
||||
{
|
||||
SubRoomId = SubRoomId,
|
||||
RoomId = RoomId,
|
||||
Name = "Rec Center",
|
||||
UnitySceneId = "RecCenterScene",
|
||||
MaxPlayers = 16,
|
||||
Accessibility = Accessibility.Public,
|
||||
DataBlob = "",
|
||||
});
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The permission entries a route answered with. The GET and the token table both hand back
|
||||
/// objects whose keys are the client's own field names, so they are read as JSON.
|
||||
/// </summary>
|
||||
private static List<JsonElement> PermissionList(object? value)
|
||||
{
|
||||
using var doc = JsonDocument.Parse(JsonSerializer.Serialize(value, ServerJson));
|
||||
return doc.RootElement.EnumerateArray().Select(x => x.Clone()).ToList();
|
||||
}
|
||||
|
||||
private static List<JsonElement> RootPropertyList(object? value, string property)
|
||||
{
|
||||
using var doc = JsonDocument.Parse(JsonSerializer.Serialize(value, ServerJson));
|
||||
return doc.RootElement.GetProperty(property).EnumerateArray().Select(x => x.Clone()).ToList();
|
||||
}
|
||||
|
||||
private static (string Permission, long Role) Pair(JsonElement entry) =>
|
||||
(entry.GetProperty("Permission").GetString() ?? "", entry.GetProperty("Role").GetInt64());
|
||||
|
||||
private static async Task<List<SubRoomPermission>> StoredRowsAsync(TestDbContextFactory factory)
|
||||
{
|
||||
await using var db = factory.CreateDbContext();
|
||||
return await db.SubRoomPermissions.AsNoTracking().OrderBy(x => x.Id).ToListAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A stored row is the override, so reads always answer <c>Override: true</c>; a pair with no
|
||||
/// row is simply absent. The GET route is what the settings screen loads its checkboxes from.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task StoredOverridesComeBackAsTheyWereSaved()
|
||||
{
|
||||
var (factory, db) = CreateContext();
|
||||
await using var _db = db;
|
||||
await SeedRoomAsync(db);
|
||||
|
||||
var api = Api(db, CreatorId);
|
||||
SetJsonBody(api, """
|
||||
[
|
||||
{ "Permission": "CAN_SPAWN_INVENTIONS", "Role": 0, "Override": true, "Type": 0, "Value": "False" },
|
||||
{ "Permission": "CAN_INVITE", "Role": 0, "Override": true, "Type": 0, "Value": "True" }
|
||||
]
|
||||
""");
|
||||
|
||||
Assert.IsType<OkObjectResult>(await api.SetSubRoomPermissions(RoomId, SubRoomId));
|
||||
|
||||
var read = Api(db, CreatorId);
|
||||
var result = Assert.IsType<OkObjectResult>(await read.GetSubRoomPermissions(RoomId, SubRoomId));
|
||||
var entries = PermissionList(result.Value);
|
||||
|
||||
Assert.Equal(2, entries.Count);
|
||||
Assert.Equal(("CAN_SPAWN_INVENTIONS", 0L), Pair(entries[0]));
|
||||
Assert.Equal("False", entries[0].GetProperty("Value").GetString());
|
||||
Assert.Equal(("CAN_INVITE", 0L), Pair(entries[1]));
|
||||
Assert.Equal("True", entries[1].GetProperty("Value").GetString());
|
||||
|
||||
// A stored row IS an override, so the box beside it reads as checked.
|
||||
Assert.All(entries, x => Assert.True(x.GetProperty("Override").GetBoolean()));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unchecking a box is the ABSENCE of a row, not a row that says false - otherwise the
|
||||
/// default table would be overridden by a value the creator just asked to stop overriding.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task UncheckingTheOverrideBoxFallsBackToTheDefault()
|
||||
{
|
||||
var (factory, db) = CreateContext();
|
||||
await using var _db = db;
|
||||
await SeedRoomAsync(db);
|
||||
|
||||
var set = Api(db, CreatorId);
|
||||
SetJsonBody(set, """
|
||||
[ { "Permission": "CAN_SAVE_INVENTIONS", "Role": 30, "Override": true, "Type": 0, "Value": "False" } ]
|
||||
""");
|
||||
Assert.IsType<OkObjectResult>(await set.SetSubRoomPermissions(RoomId, SubRoomId));
|
||||
Assert.Single(await StoredRowsAsync(factory));
|
||||
|
||||
var unset = Api(db, CreatorId);
|
||||
SetJsonBody(unset, """
|
||||
[ { "Permission": "CAN_SAVE_INVENTIONS", "Role": 30, "Override": false, "Type": 0, "Value": "False" } ]
|
||||
""");
|
||||
Assert.IsType<OkObjectResult>(await unset.SetSubRoomPermissions(RoomId, SubRoomId));
|
||||
|
||||
Assert.Empty(await StoredRowsAsync(factory));
|
||||
|
||||
var read = Api(db, CreatorId);
|
||||
var result = Assert.IsType<OkObjectResult>(await read.GetSubRoomPermissions(RoomId, SubRoomId));
|
||||
Assert.Empty(PermissionList(result.Value));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The client re-sends the whole table when one checkbox changes, so a pair that arrives
|
||||
/// twice must update its row. Appending instead would leave two rows for one pair, and the
|
||||
/// token table would carry whichever the query happened to return last.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task ResendingAPairReplacesItsRowInsteadOfAddingASecond()
|
||||
{
|
||||
var (factory, db) = CreateContext();
|
||||
await using var _db = db;
|
||||
await SeedRoomAsync(db);
|
||||
|
||||
var first = Api(db, CreatorId);
|
||||
SetJsonBody(first, """
|
||||
[ { "Permission": "CAN_USE_DELETE_ALL_BUTTON", "Role": 0, "Override": true, "Type": 0, "Value": "False" } ]
|
||||
""");
|
||||
await first.SetSubRoomPermissions(RoomId, SubRoomId);
|
||||
|
||||
var second = Api(db, CreatorId);
|
||||
SetJsonBody(second, """
|
||||
[ { "Permission": "CAN_USE_DELETE_ALL_BUTTON", "Role": 0, "Override": true, "Type": 3, "Value": "True" } ]
|
||||
""");
|
||||
await second.SetSubRoomPermissions(RoomId, SubRoomId);
|
||||
|
||||
var rows = await StoredRowsAsync(factory);
|
||||
var row = Assert.Single(rows);
|
||||
Assert.Equal("CAN_USE_DELETE_ALL_BUTTON", row.permission);
|
||||
Assert.Equal(3, row.type);
|
||||
Assert.Equal("True", row.value);
|
||||
}
|
||||
|
||||
/// <summary>Deciding what a role may do is a co-owner decision.</summary>
|
||||
[Fact]
|
||||
public async Task OnlyACoOwnerCanChangeThePermissions()
|
||||
{
|
||||
var (factory, db) = CreateContext();
|
||||
await using var _db = db;
|
||||
await SeedRoomAsync(db);
|
||||
|
||||
var stranger = Api(db, StrangerId);
|
||||
SetJsonBody(stranger, """
|
||||
[ { "Permission": "CAN_USE_MAKER_PEN", "Role": 0, "Override": true, "Type": 0, "Value": "True" } ]
|
||||
""");
|
||||
|
||||
Assert.IsType<ForbidResult>(await stranger.SetSubRoomPermissions(RoomId, SubRoomId));
|
||||
Assert.Empty(await StoredRowsAsync(factory));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AnUnknownSubroomIsNotFound()
|
||||
{
|
||||
var (factory, db) = CreateContext();
|
||||
await using var _db = db;
|
||||
await SeedRoomAsync(db);
|
||||
|
||||
var api = Api(db, CreatorId);
|
||||
SetJsonBody(api, "[]");
|
||||
|
||||
Assert.IsType<NotFoundResult>(await api.SetSubRoomPermissions(RoomId, SubRoomId + 1));
|
||||
Assert.IsType<NotFoundResult>(await api.GetSubRoomPermissions(RoomId, SubRoomId + 1));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The whole point of storing them: the table the client applies when it spawns has to carry
|
||||
/// the subroom's override over the default for that pair, and a pair the defaults do not
|
||||
/// carry has to be added - otherwise a creator's checkbox is saved and then ignored.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task TheTokenTableCarriesTheSubroomsOverrides()
|
||||
{
|
||||
var (factory, db) = CreateContext();
|
||||
await using var _db = db;
|
||||
await SeedRoomAsync(db);
|
||||
|
||||
const long instanceId = 900;
|
||||
db.RoomInstances.Add(new RoomInstance
|
||||
{
|
||||
roomInstanceId = instanceId,
|
||||
roomId = RoomId,
|
||||
subRoomId = SubRoomId,
|
||||
maxCapacity = 16,
|
||||
});
|
||||
db.Presences.Add(new RealPresence
|
||||
{
|
||||
playerId = CreatorId,
|
||||
isOnline = true,
|
||||
roomInstance = instanceId,
|
||||
});
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
var set = Api(db, CreatorId);
|
||||
SetJsonBody(set, """
|
||||
[
|
||||
{ "Permission": "CAN_SPAWN_INVENTIONS", "Role": 0, "Override": true, "Type": 0, "Value": "False" },
|
||||
{ "Permission": "CAN_INVITE", "Role": 0, "Override": true, "Type": 0, "Value": "True" }
|
||||
]
|
||||
""");
|
||||
await set.SetSubRoomPermissions(RoomId, SubRoomId);
|
||||
|
||||
var token = Api(db, CreatorId);
|
||||
var result = Assert.IsType<OkObjectResult>(await token.GetPhotonAccessToken());
|
||||
var table = RootPropertyList(result.Value, "Permissions");
|
||||
|
||||
// The default table includes CAN_INVITE (permission id 4), which the keepsake
|
||||
// authorization gate also reads; both overrides replace defaults in place.
|
||||
Assert.Equal(12, table.Count);
|
||||
|
||||
using (var doc = JsonDocument.Parse(JsonSerializer.Serialize(result.Value, ServerJson)))
|
||||
Assert.Equal(instanceId, doc.RootElement.GetProperty("RoomInstanceId").GetInt64());
|
||||
|
||||
var spawn = Assert.Single(table.Where(x =>
|
||||
x.GetProperty("Permission").GetString() == "CAN_SPAWN_INVENTIONS" && x.GetProperty("Role").GetInt64() == 0));
|
||||
Assert.Equal("False", spawn.GetProperty("Value").GetString());
|
||||
Assert.True(spawn.GetProperty("Override").GetBoolean());
|
||||
|
||||
// Replaced where it stood rather than appended, so the order cannot shift under the client
|
||||
// and no pair is listed twice with two different values.
|
||||
Assert.Equal(("CAN_SPAWN_INVENTIONS", 0L), Pair(table[3]));
|
||||
Assert.Equal(("CAN_INVITE", 0L), Pair(table[5]));
|
||||
Assert.Equal("True", table[5].GetProperty("Value").GetString());
|
||||
|
||||
// The default that ships as Override:false is the client's "this is not an override", and a
|
||||
// subroom override must not have disturbed it. It follows the new CAN_INVITE row.
|
||||
Assert.Equal(("CAN_USE_MAKER_PEN", 30L), Pair(table[6]));
|
||||
Assert.False(table[6].GetProperty("Override").GetBoolean());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Every spelling of the same payload has to land on the same stored entry, because the
|
||||
/// table that matters is keyed by the permission's NAME. A body carrying the enum's number,
|
||||
/// or the client's own `PermissionType` field, or the role tier as a name, would otherwise
|
||||
/// save an entry that matches no default: the checkbox would look saved and change nothing.
|
||||
/// </summary>
|
||||
[Theory]
|
||||
[InlineData("""
|
||||
[ { "Permission": 20, "Role": 0, "Override": true, "Type": 0, "Value": "False" } ]
|
||||
""", "CAN_SPAWN_INVENTIONS", 0L)]
|
||||
[InlineData("""
|
||||
[ { "PermissionType": "CAN_SPAWN_INVENTIONS", "Role": 0, "Override": true, "Type": 0, "Value": "False" } ]
|
||||
""", "CAN_SPAWN_INVENTIONS", 0L)]
|
||||
[InlineData("""
|
||||
[ { "PermissionTypeOld": "CAN_SPAWN_INVENTIONS", "Role": "0", "Override": true, "Type": 0, "Value": "False" } ]
|
||||
""", "CAN_SPAWN_INVENTIONS", 0L)]
|
||||
[InlineData("""
|
||||
[ { "Permission": "CAN_SPAWN_INVENTIONS", "Role": "CoOwner", "Override": true, "Type": 0, "Value": "False" } ]
|
||||
""", "CAN_SPAWN_INVENTIONS", 30L)]
|
||||
public async Task ASpellingOfTheSameEntryStoresTheSameKey(string body, string permission, long role)
|
||||
{
|
||||
var (factory, db) = CreateContext();
|
||||
await using var _db = db;
|
||||
await SeedRoomAsync(db);
|
||||
|
||||
var api = Api(db, CreatorId);
|
||||
SetJsonBody(api, body);
|
||||
Assert.IsType<OkObjectResult>(await api.SetSubRoomPermissions(RoomId, SubRoomId));
|
||||
|
||||
var row = Assert.Single(await StoredRowsAsync(factory));
|
||||
Assert.Equal(permission, row.permission);
|
||||
Assert.Equal(role, row.role);
|
||||
Assert.Equal("False", row.value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A subroom override is the creator's decision for that room and wins over the developer
|
||||
/// grant, so nobody can build their way around it by being staff.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task ASubRoomOverrideBeatsTheDeveloperGrant()
|
||||
{
|
||||
var (factory, db) = CreateContext();
|
||||
await using var _db = db;
|
||||
await SeedRoomAsync(db);
|
||||
|
||||
const long instanceId = 901;
|
||||
db.Profiles.Add(new Profile
|
||||
{
|
||||
accountId = CreatorId,
|
||||
username = "dev",
|
||||
displayName = "dev",
|
||||
role = "developer",
|
||||
profileImage = DBConfig.DefaultProfileImage,
|
||||
});
|
||||
db.RoomInstances.Add(new RoomInstance
|
||||
{
|
||||
roomInstanceId = instanceId,
|
||||
roomId = RoomId,
|
||||
subRoomId = SubRoomId,
|
||||
});
|
||||
db.Presences.Add(new RealPresence
|
||||
{
|
||||
playerId = CreatorId,
|
||||
isOnline = true,
|
||||
roomInstance = instanceId,
|
||||
});
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
var plain = Api(db, CreatorId);
|
||||
var beforeResult = Assert.IsType<OkObjectResult>(await plain.GetPhotonAccessToken());
|
||||
var before = RootPropertyList(beforeResult.Value, "Permissions");
|
||||
Assert.Equal(("CAN_USE_MAKER_PEN", 0L), Pair(before[0]));
|
||||
Assert.True(before[0].GetProperty("Override").GetBoolean());
|
||||
Assert.Equal(13, before.Count);
|
||||
|
||||
var set = Api(db, CreatorId);
|
||||
SetJsonBody(set, """
|
||||
[ { "Permission": "CAN_USE_MAKER_PEN", "Role": 0, "Override": true, "Type": 0, "Value": "False" } ]
|
||||
""");
|
||||
await set.SetSubRoomPermissions(RoomId, SubRoomId);
|
||||
|
||||
var after = Api(db, CreatorId);
|
||||
var afterResult = Assert.IsType<OkObjectResult>(await after.GetPhotonAccessToken());
|
||||
var table = RootPropertyList(afterResult.Value, "Permissions");
|
||||
|
||||
Assert.Equal("False", Assert.Single(table.Where(x =>
|
||||
x.GetProperty("Permission").GetString() == "CAN_USE_MAKER_PEN" && x.GetProperty("Role").GetInt64() == 0))
|
||||
.GetProperty("Value").GetString());
|
||||
Assert.Equal(13, table.Count);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A player standing in no instance still gets a usable table: the default, without the
|
||||
/// subroom merge. An empty one is what left every permission unchecked before.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task StandingNowhereStillAnswersTheDefaultTable()
|
||||
{
|
||||
var (factory, db) = CreateContext();
|
||||
await using var _db = db;
|
||||
await SeedRoomAsync(db);
|
||||
|
||||
var api = Api(db, CreatorId);
|
||||
var result = Assert.IsType<OkObjectResult>(await api.GetPhotonAccessToken());
|
||||
var table = RootPropertyList(result.Value, "Permissions");
|
||||
|
||||
Assert.Equal(12, table.Count);
|
||||
var invite = Assert.Single(table.Where(x =>
|
||||
x.GetProperty("Permission").GetString() == "CAN_INVITE" && x.GetProperty("Role").GetInt64() == 0));
|
||||
Assert.True(invite.GetProperty("Override").GetBoolean());
|
||||
Assert.Equal("True", invite.GetProperty("Value").GetString());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user