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,246 @@
|
||||
using System.Security.Claims;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using RadicadeServer.API.Matchmaking;
|
||||
using RadicadeServer.DB;
|
||||
using RadicadeServer.Extensions;
|
||||
using RadicadeServer.RecNet.Accounts;
|
||||
using RadicadeServer.RecNet.Accounts.Local;
|
||||
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>
|
||||
/// The account-creation flow ends on the launch-game step, and the client asks for its dorm by
|
||||
/// route (<c>matchmake/dorm</c>) rather than by room name. That route resolves the dorm directly,
|
||||
/// so it never ran through the Orientation redirect that lives on the room-name path in
|
||||
/// <see cref="MatchmakingUtils.Matchmake"/> - a brand-new player was dropped in their dorm with
|
||||
/// the Orientation marker still unspent. Orientation is a solo room (MaxPlayers 1), so the
|
||||
/// hand-off mints one instance per player, the same shape the dorm uses.
|
||||
///
|
||||
/// These pin both dorm entry points, that the hand-off happens exactly once, and that a server
|
||||
/// without an Orientation room still lands its players somewhere.
|
||||
/// </summary>
|
||||
[Collection(DbFactoryCollection.Name)]
|
||||
public class OrientationHandoffTests
|
||||
{
|
||||
private const long PlayerId = 812;
|
||||
private const long OrientationRoomId = 13;
|
||||
private const long OrientationSubRoomId = 113;
|
||||
private const long DormRoomId = 8121;
|
||||
private const long DormSubRoomId = 8122;
|
||||
|
||||
private sealed class TestDbContextFactory : IDbContextFactory<RecDBContext>
|
||||
{
|
||||
private readonly DbContextOptions<RecDBContext> options = TestDbOptions.Create()
|
||||
.UseInMemoryDatabase(Guid.NewGuid().ToString())
|
||||
.Options;
|
||||
|
||||
public RecDBContext CreateDbContext() => new(options);
|
||||
}
|
||||
|
||||
private static async Task<(TestDbContextFactory Factory, RecDBContext Db)> CreateContextAsync(
|
||||
bool seedOrientationRoom = true)
|
||||
{
|
||||
var factory = new TestDbContextFactory();
|
||||
RoomUtils.DbFactory = factory;
|
||||
MatchmakingUtils.DbFactory = factory;
|
||||
|
||||
var db = factory.CreateDbContext();
|
||||
|
||||
if (seedOrientationRoom)
|
||||
{
|
||||
db.Rooms.Add(new RoomData
|
||||
{
|
||||
RoomId = OrientationRoomId,
|
||||
Name = "Orientation",
|
||||
Description = "An introductory tour of Rec Room!",
|
||||
Accessibility = Accessibility.Public,
|
||||
MaxPlayers = 1,
|
||||
CreatedAt = DateTime.UtcNow
|
||||
});
|
||||
db.SubRooms.Add(new SubRoom
|
||||
{
|
||||
SubRoomId = OrientationSubRoomId,
|
||||
RoomId = OrientationRoomId,
|
||||
Name = "Home",
|
||||
UnitySceneId = "c79709d8-a31b-48aa-9eb8-cc31ba9505e8",
|
||||
MaxPlayers = 1,
|
||||
Accessibility = Accessibility.Public
|
||||
});
|
||||
}
|
||||
|
||||
// The player's own dorm, which GetOrCreateDorm recognises by owner rather than by name.
|
||||
db.Rooms.Add(new RoomData
|
||||
{
|
||||
RoomId = DormRoomId,
|
||||
Name = "DormRoom",
|
||||
IsDorm = true,
|
||||
CreatorAccountId = PlayerId,
|
||||
Accessibility = Accessibility.Private,
|
||||
CreatedAt = DateTime.UtcNow
|
||||
});
|
||||
db.SubRooms.Add(new SubRoom
|
||||
{
|
||||
SubRoomId = DormSubRoomId,
|
||||
RoomId = DormRoomId,
|
||||
Name = "Home",
|
||||
UnitySceneId = "cbad71af-0831-44d8-b8ef-69edafa841f6",
|
||||
MaxPlayers = 4,
|
||||
Accessibility = Accessibility.Private
|
||||
});
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
return (factory, db);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An account the account-creation flow just created, with the settings that flow writes:
|
||||
/// the Orientation marker, and no <c>HAS_COMPLETED_ORIENTATION</c>.
|
||||
/// </summary>
|
||||
private static async Task SeedOnboardingPlayerAsync(
|
||||
RecDBContext db,
|
||||
long playerId = PlayerId,
|
||||
bool needsOrientation = true)
|
||||
{
|
||||
var settings = new List<Setting>
|
||||
{
|
||||
new() { Key = "Recroom.AccountCreation.HasStarted", Value = "True" },
|
||||
new() { Key = "Recroom.AccountCreation.HasFinished", Value = "False" },
|
||||
};
|
||||
|
||||
if (needsOrientation)
|
||||
settings.Add(new Setting { Key = "Radicade.NeedsOrientation", Value = "1" });
|
||||
|
||||
db.PlayerSettings.Add(new PlayerSettings
|
||||
{
|
||||
AccountId = playerId,
|
||||
Settings = settings.ToJson()
|
||||
});
|
||||
db.Presences.Add(new RealPresence { playerId = playerId });
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
private static MatchmakingAPI CreateMatchmakingApi(RecDBContext db, long accountId)
|
||||
{
|
||||
var identity = new ClaimsIdentity("TestAuth");
|
||||
identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, accountId.ToString()));
|
||||
|
||||
var api = new MatchmakingAPI(db)
|
||||
{
|
||||
ControllerContext = new ControllerContext
|
||||
{
|
||||
HttpContext = new DefaultHttpContext { User = new ClaimsPrincipal(identity) }
|
||||
}
|
||||
};
|
||||
|
||||
return api;
|
||||
}
|
||||
|
||||
private static async Task<MatchResponse> MatchmakeDormAsync(MatchmakingAPI api)
|
||||
{
|
||||
var result = Assert.IsType<OkObjectResult>(await api.MatchmakeDorm());
|
||||
return Assert.IsType<MatchResponse>(result.Value);
|
||||
}
|
||||
|
||||
private static async Task<bool> StillOwesOrientationAsync(TestDbContextFactory factory)
|
||||
{
|
||||
await using var verify = factory.CreateDbContext();
|
||||
var settings = await verify.PlayerSettings.SingleAsync(x => x.AccountId == PlayerId);
|
||||
return settings.DeserializeSettings()
|
||||
.Any(setting => setting.Key == "Radicade.NeedsOrientation");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TheLaunchStepTheOnboardingFlowEndsOnOpensOrientation()
|
||||
{
|
||||
var (factory, db) = await CreateContextAsync();
|
||||
await using var _db = db;
|
||||
await SeedOnboardingPlayerAsync(db);
|
||||
|
||||
var match = await MatchmakeDormAsync(CreateMatchmakingApi(db, PlayerId));
|
||||
|
||||
Assert.Equal(JoinStatus.Success, match.errorCode);
|
||||
Assert.NotNull(match.roomInstance);
|
||||
Assert.Equal(OrientationRoomId, match.roomInstance!.roomId);
|
||||
|
||||
await using var verify = factory.CreateDbContext();
|
||||
var presence = await verify.Presences.SingleAsync(x => x.playerId == PlayerId);
|
||||
Assert.Equal(match.roomInstance.roomInstanceId, presence.roomInstance);
|
||||
Assert.False(await StillOwesOrientationAsync(factory));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TheRoomNameDormPathOpensOrientationToo()
|
||||
{
|
||||
var (factory, db) = await CreateContextAsync();
|
||||
await using var _db = db;
|
||||
await SeedOnboardingPlayerAsync(db);
|
||||
|
||||
var match = await MatchmakingUtils.Matchmake(JoinMode.PublicMatchmaking, "DormRoom", PlayerId);
|
||||
|
||||
Assert.Equal(JoinStatus.Success, match.errorCode);
|
||||
Assert.NotNull(match.roomInstance);
|
||||
Assert.Equal(OrientationRoomId, match.roomInstance!.roomId);
|
||||
Assert.False(await StillOwesOrientationAsync(factory));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task OrientationIsHandedOutOnceAndThenTheDormIsWhereTheyLand()
|
||||
{
|
||||
var (factory, db) = await CreateContextAsync();
|
||||
await using var _db = db;
|
||||
await SeedOnboardingPlayerAsync(db);
|
||||
|
||||
var first = await MatchmakeDormAsync(CreateMatchmakingApi(db, PlayerId));
|
||||
Assert.Equal(OrientationRoomId, first.roomInstance!.roomId);
|
||||
|
||||
// The marker was spent, so the second launch is an ordinary dorm launch: the player is not
|
||||
// sent back to Orientation every time they go home.
|
||||
var second = await MatchmakeDormAsync(CreateMatchmakingApi(db, PlayerId));
|
||||
await using var verify = factory.CreateDbContext();
|
||||
var room = await verify.Rooms.AsNoTracking().SingleAsync(x => x.RoomId == second.roomInstance!.roomId);
|
||||
Assert.True(room.IsDorm);
|
||||
Assert.Equal(DormRoomId, room.RoomId);
|
||||
Assert.NotEqual(first.roomInstance.roomInstanceId, second.roomInstance!.roomInstanceId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task APlayerWhoOwesNoOrientationGoesStraightToTheirDorm()
|
||||
{
|
||||
var (factory, db) = await CreateContextAsync();
|
||||
await using var _db = db;
|
||||
await SeedOnboardingPlayerAsync(db, needsOrientation: false);
|
||||
|
||||
var match = await MatchmakeDormAsync(CreateMatchmakingApi(db, PlayerId));
|
||||
|
||||
await using var verify = factory.CreateDbContext();
|
||||
var room = await verify.Rooms.AsNoTracking().SingleAsync(x => x.RoomId == match.roomInstance!.roomId);
|
||||
Assert.Equal(DormRoomId, room.RoomId);
|
||||
Assert.True(room.IsDorm);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AServerWithNoOrientationRoomStillLandsThePlayerInTheirDorm()
|
||||
{
|
||||
var (factory, db) = await CreateContextAsync(seedOrientationRoom: false);
|
||||
await using var _db = db;
|
||||
await SeedOnboardingPlayerAsync(db);
|
||||
|
||||
var match = await MatchmakeDormAsync(CreateMatchmakingApi(db, PlayerId));
|
||||
|
||||
await using var verify = factory.CreateDbContext();
|
||||
var room = await verify.Rooms.AsNoTracking().SingleAsync(x => x.RoomId == match.roomInstance!.roomId);
|
||||
Assert.Equal(DormRoomId, room.RoomId);
|
||||
|
||||
// Nothing was handed out, so the marker stays: the player still gets Orientation if the room
|
||||
// is added later.
|
||||
Assert.True(await StillOwesOrientationAsync(factory));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user