mirror of
https://github.com/JogmansonReal/Radicade.git
synced 2026-10-25 15:09:26 -07:00
Radicade Public Code
This commit is contained in:
@@ -0,0 +1,331 @@
|
||||
using System.Security.Claims;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using RadicadeServer.API.Matchmaking;
|
||||
using RadicadeServer.DB;
|
||||
using RadicadeServer.RecNet.Accounts;
|
||||
using RadicadeServer.RecNet.Enums;
|
||||
using RadicadeServer.RecNet.Enums.Rooms;
|
||||
using RadicadeServer.RecNet.Responses;
|
||||
using RadicadeServer.RecNet.Rooms;
|
||||
using RadicadeServer.Utils;
|
||||
using Xunit;
|
||||
|
||||
namespace RadicadeServer.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// `POST matchmaking/matchmake/instance/{instanceId}` is how the 2023 client re-enters one
|
||||
/// specific live instance - the private session its owner just left, picked out of
|
||||
/// `GET matchmaking/room/{roomId}/instances`. Radicade had no route for it at all, so the
|
||||
/// call 404'd and "get back into my private instance" had no path except public
|
||||
/// matchmaking, which hands out a different instance.
|
||||
///
|
||||
/// The route assertion exists because a controller-level test cannot see a missing route -
|
||||
/// that is the failure being fixed. The rest pin the owner gate: entering one's own
|
||||
/// instance (and *only* one's own) has to work, and a private instance must not be refused
|
||||
/// on this path the way `goto/instance/{id}` refuses it for everyone else.
|
||||
/// </summary>
|
||||
[Collection(DbFactoryCollection.Name)]
|
||||
public class MatchmakeInstanceRouteTests
|
||||
{
|
||||
private const long RoomId = 3;
|
||||
private const long CreatorId = 1;
|
||||
private const long CoOwnerId = 2;
|
||||
private const long StrangerId = 9;
|
||||
private const long SubRoomId = 3;
|
||||
|
||||
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 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 (TestDbContextFactory Factory, RecDBContext Db) CreateContext()
|
||||
{
|
||||
var factory = new TestDbContextFactory();
|
||||
RoomUtils.DbFactory = factory;
|
||||
MatchmakingUtils.DbFactory = factory;
|
||||
return (factory, factory.CreateDbContext());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A room with one live private instance - the shape an owner's own RecCenter session
|
||||
/// has - plus a presence row for every account the test needs to move.
|
||||
/// </summary>
|
||||
private static async Task<long> SeedRoomWithPrivateInstanceAsync(
|
||||
RecDBContext db,
|
||||
bool isPrivate = true,
|
||||
long instanceRoomId = RoomId,
|
||||
params long[] presences)
|
||||
{
|
||||
db.Rooms.Add(new RoomData
|
||||
{
|
||||
RoomId = RoomId,
|
||||
Name = "RecCenter",
|
||||
CreatorAccountId = CreatorId,
|
||||
Accessibility = Accessibility.Public,
|
||||
MaxPlayers = 100,
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
});
|
||||
|
||||
db.SubRooms.Add(new SubRoom
|
||||
{
|
||||
SubRoomId = SubRoomId,
|
||||
RoomId = RoomId,
|
||||
Name = "Home",
|
||||
UnitySceneId = "cbad71af-0831-44d8-b8ef-69edafa841f6",
|
||||
MaxPlayers = 100,
|
||||
Accessibility = Accessibility.Public,
|
||||
});
|
||||
|
||||
var instance = new RoomInstance
|
||||
{
|
||||
roomId = instanceRoomId,
|
||||
subRoomId = SubRoomId,
|
||||
roomInstanceType = RoomInstanceType.Private,
|
||||
isPrivate = isPrivate,
|
||||
maxCapacity = 100,
|
||||
name = "^RecCenter",
|
||||
location = "cbad71af-0831-44d8-b8ef-69edafa841f6",
|
||||
};
|
||||
|
||||
db.RoomInstances.Add(instance);
|
||||
|
||||
foreach (var accountId in presences)
|
||||
{
|
||||
db.Presences.Add(new RealPresence
|
||||
{
|
||||
playerId = accountId,
|
||||
isOnline = true,
|
||||
roomInstance = null,
|
||||
errorCode = JoinStatus.Success,
|
||||
});
|
||||
}
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
return instance.roomInstanceId;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SupportsTheSpecificInstanceJoinRoute()
|
||||
{
|
||||
var method = typeof(MatchmakingAPI).GetMethod(nameof(MatchmakingAPI.MatchmakeInstance))!;
|
||||
|
||||
var attributes = method
|
||||
.GetCustomAttributes(typeof(Microsoft.AspNetCore.Mvc.Routing.HttpMethodAttribute), false)
|
||||
.Cast<Microsoft.AspNetCore.Mvc.Routing.HttpMethodAttribute>()
|
||||
.ToList();
|
||||
|
||||
Assert.Contains(attributes, x => x.Template == "matchmake/instance/{instanceId}");
|
||||
Assert.Contains(attributes, x => x is HttpPostAttribute);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TheRoomCreatorCanEnterTheirOwnPrivateInstance()
|
||||
{
|
||||
var (factory, db) = CreateContext();
|
||||
await using var _db = db;
|
||||
|
||||
var instanceId = await SeedRoomWithPrivateInstanceAsync(db, presences: CreatorId);
|
||||
|
||||
var api = new MatchmakingAPI(db);
|
||||
SetUser(api, CreatorId);
|
||||
|
||||
MatchResponse? match = await api.MatchmakeInstance(instanceId, null);
|
||||
|
||||
Assert.NotNull(match);
|
||||
Assert.Equal(JoinStatus.Success, match!.errorCode);
|
||||
Assert.NotNull(match.roomInstance);
|
||||
Assert.Equal(instanceId, match.roomInstance!.roomInstanceId);
|
||||
|
||||
// A private instance is the whole point of this route: it must not come back
|
||||
// RoomInstanceIsPrivate the way goto/instance/{id} answers.
|
||||
Assert.True(match.roomInstance.isPrivate);
|
||||
|
||||
await using var verify = factory.CreateDbContext();
|
||||
var presence = await verify.Presences.FirstAsync(x => x.playerId == CreatorId);
|
||||
Assert.Equal(instanceId, presence.roomInstance);
|
||||
Assert.Equal(JoinStatus.Success, presence.errorCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ACoOwnerCanEnterTheRoomsPrivateInstance()
|
||||
{
|
||||
var (factory, db) = CreateContext();
|
||||
await using var _db = db;
|
||||
|
||||
var instanceId = await SeedRoomWithPrivateInstanceAsync(db, presences: CoOwnerId);
|
||||
|
||||
db.RoomRoles.Add(new RoomRole
|
||||
{
|
||||
RoomId = RoomId,
|
||||
AccountId = CoOwnerId,
|
||||
Role = RoleType.CoOwner,
|
||||
InvitedRole = RoleType.None,
|
||||
});
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
var api = new MatchmakingAPI(db);
|
||||
SetUser(api, CoOwnerId);
|
||||
|
||||
MatchResponse? match = await api.MatchmakeInstance(instanceId, null);
|
||||
|
||||
Assert.NotNull(match);
|
||||
Assert.Equal(JoinStatus.Success, match!.errorCode);
|
||||
|
||||
await using var verify = factory.CreateDbContext();
|
||||
Assert.Equal(instanceId, (await verify.Presences.FirstAsync(x => x.playerId == CoOwnerId)).roomInstance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task APlayerWhoDoesNotManageTheRoomGetsNoSuchRoomAndStaysPut()
|
||||
{
|
||||
var (factory, db) = CreateContext();
|
||||
await using var _db = db;
|
||||
|
||||
var instanceId = await SeedRoomWithPrivateInstanceAsync(db, presences: StrangerId);
|
||||
|
||||
// A Moderator is not enough: the Photon room id is the join coordinate, so only
|
||||
// the room's creator and its co-owners may be handed it.
|
||||
db.RoomRoles.Add(new RoomRole
|
||||
{
|
||||
RoomId = RoomId,
|
||||
AccountId = StrangerId,
|
||||
Role = RoleType.Moderator,
|
||||
InvitedRole = RoleType.None,
|
||||
});
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
var api = new MatchmakingAPI(db);
|
||||
SetUser(api, StrangerId);
|
||||
|
||||
MatchResponse? match = await api.MatchmakeInstance(instanceId, null);
|
||||
|
||||
Assert.NotNull(match);
|
||||
Assert.Equal(JoinStatus.NoSuchRoom, match!.errorCode);
|
||||
Assert.Null(match.roomInstance);
|
||||
|
||||
await using var verify = factory.CreateDbContext();
|
||||
Assert.Null((await verify.Presences.FirstAsync(x => x.playerId == StrangerId)).roomInstance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AnUnknownInstanceIsNoSuchRoom()
|
||||
{
|
||||
var (factory, db) = CreateContext();
|
||||
await using var _db = db;
|
||||
|
||||
await SeedRoomWithPrivateInstanceAsync(db, presences: CreatorId);
|
||||
|
||||
var api = new MatchmakingAPI(db);
|
||||
SetUser(api, CreatorId);
|
||||
|
||||
MatchResponse? match = await api.MatchmakeInstance(9876543, null);
|
||||
|
||||
Assert.NotNull(match);
|
||||
Assert.Equal(JoinStatus.NoSuchRoom, match!.errorCode);
|
||||
Assert.Null(match.roomInstance);
|
||||
|
||||
await using var verify = factory.CreateDbContext();
|
||||
Assert.Null((await verify.Presences.FirstAsync(x => x.playerId == CreatorId)).roomInstance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SupportsTheRoomInstanceBrowserRoute()
|
||||
{
|
||||
var method = typeof(MatchmakingAPI).GetMethod(nameof(MatchmakingAPI.GetRoomInstancesInRoom))!;
|
||||
|
||||
var attributes = method
|
||||
.GetCustomAttributes(typeof(Microsoft.AspNetCore.Mvc.Routing.HttpMethodAttribute), false)
|
||||
.Cast<Microsoft.AspNetCore.Mvc.Routing.HttpMethodAttribute>()
|
||||
.ToList();
|
||||
|
||||
Assert.Contains(attributes, x => x.Template == "room/{roomId}/instances");
|
||||
Assert.Contains(attributes, x => x is HttpGetAttribute);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TheInstanceBrowserAnswersAnArrayToAPlayerWhoDoesNotModerateTheRoom()
|
||||
{
|
||||
var (factory, db) = CreateContext();
|
||||
await using var _db = db;
|
||||
|
||||
var instanceId = await SeedRoomWithPrivateInstanceAsync(db, isPrivate: false, presences: StrangerId);
|
||||
var presence = await db.Presences.FirstAsync(x => x.playerId == StrangerId);
|
||||
presence.roomInstance = instanceId;
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
var api = new MatchmakingAPI(db);
|
||||
SetUser(api, StrangerId);
|
||||
|
||||
// The browser is the watch menu every player opens for the room they are in. It used to
|
||||
// require RoleType.Moderator and answer null to everyone else, which serialises to 204
|
||||
// No Content - the client's "Failed to get room instance browser: Response was empty".
|
||||
var instances = await api.GetRoomInstancesInRoom(RoomId);
|
||||
|
||||
Assert.NotNull(instances);
|
||||
var summary = Assert.IsType<Dictionary<string, object?>>(Assert.Single(instances!));
|
||||
Assert.Equal(instanceId, summary["roomInstanceId"]);
|
||||
Assert.Equal(RoomId, summary["roomId"]);
|
||||
Assert.Equal(new[] { StrangerId },
|
||||
Assert.IsAssignableFrom<IEnumerable<long>>(summary["playerIds"]));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TheInstanceBrowserAnswersAnEmptyArrayToACallerWithNoIdentity()
|
||||
{
|
||||
var (factory, db) = CreateContext();
|
||||
await using var _db = db;
|
||||
|
||||
await SeedRoomWithPrivateInstanceAsync(db, presences: CreatorId);
|
||||
|
||||
var api = new MatchmakingAPI(db);
|
||||
api.ControllerContext = new ControllerContext { HttpContext = new DefaultHttpContext() };
|
||||
|
||||
var instances = await api.GetRoomInstancesInRoom(RoomId);
|
||||
|
||||
Assert.NotNull(instances);
|
||||
Assert.Empty(instances!);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AnInstanceWhoseRoomIsGoneIsNoSuchRoom()
|
||||
{
|
||||
var (factory, db) = CreateContext();
|
||||
await using var _db = db;
|
||||
|
||||
// The instance survives a deleted room: it must not be entered through a room
|
||||
// row that no longer exists (its subroom blob is unreachable anyway).
|
||||
var instanceId = await SeedRoomWithPrivateInstanceAsync(
|
||||
db,
|
||||
instanceRoomId: 4242,
|
||||
presences: CreatorId);
|
||||
|
||||
var api = new MatchmakingAPI(db);
|
||||
SetUser(api, CreatorId);
|
||||
|
||||
MatchResponse? match = await api.MatchmakeInstance(instanceId, null);
|
||||
|
||||
Assert.NotNull(match);
|
||||
Assert.Equal(JoinStatus.NoSuchRoom, match!.errorCode);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user