using Microsoft.EntityFrameworkCore; using RadicadeServer.DB; using RadicadeServer.RecNet.CDN; using RadicadeServer.RecNet.Config; using RadicadeServer.RecNet.Enums; using RadicadeServer.RecNet.Enums.Rooms; using RadicadeServer.RecNet.Events; using RadicadeServer.RecNet.Leaderboards; using RadicadeServer.RecNet.Messages; using RadicadeServer.RecNet.Rooms; using RadicadeServer.Utils; using Xunit; namespace RadicadeServer.Tests; /// /// Showdown belongs in the shipped catalog, but it used to be built by an importer of its own: it /// never got the image images.txt ships for it (it kept default.png), and because it sat outside /// nothing that rebuilt the catalog would have brought it back. /// These pin both halves of the repair - the catalog entry itself, and the one-time reset that /// deletes every room in the table so the catalog can seed the shipped set from nothing. /// /// The reset is destructive by design, so the tests say what it takes: every room, the RecCenter /// and every player's dorm included, with the rows that hang off them. What they check hardest is /// that the database is left *usable* - the dorm template every player's own dorm is cloned from is /// put back on the same boot, before anyone can join. /// public class DefaultRoomsResetTests { private const long CoachId = 1; private const long PlayerId = 2; private const long DriftedShowdownId = 32; private const long DriftedBowlingId = 12; private const long RecCenterId = 3; private const long DormId = 1; private const long PlayerRoomId = 40; private const string DriftedShowdownTags = "rro,2|games,0|beta,1"; private static RecDBContext NewDb() => new(TestDbOptions.Create() .UseInMemoryDatabase(Guid.NewGuid().ToString()) .Options); private static RoomData Room( long id, string name, long creator, bool isDorm = false, string tags = "", string image = "default.png", int maxPlayers = 10, Accessibility accessibility = Accessibility.Public, bool cloningAllowed = false) => new() { RoomId = id, Name = name, Description = name, ImageName = image, Tags = tags, CreatorAccountId = creator, IsDorm = isDorm, MaxPlayers = maxPlayers, Accessibility = accessibility, CloningAllowed = cloningAllowed, State = RoomState.Active, CreatedAt = DateTime.UtcNow, }; private static SubRoom Sub( long id, long roomId, string name = "Home", string scene = "old-scene", string save = "", string bundle = "") => new() { SubRoomId = id, RoomId = roomId, Name = name, DataBlob = save, UnityAssetId = bundle, UnitySceneId = scene, MaxPlayers = 10, Accessibility = Accessibility.Public, SavedByAccountId = CoachId, CreatedAt = DateTime.UtcNow, }; /// /// The shape the live database was in: the importer's Showdown with the default picture, a /// catalog room that drifted, the RecCenter, the dorm template, a room a player made, and one /// row in every table that hangs off a room - so the cleanup is checked table by table rather /// than only for the room row. The alignment marker is already set, which is the trap: without /// the reset clearing it, the catalog would decide this database was fine and do nothing. /// private static void SeedDriftedState(RecDBContext db) { db.Rooms.AddRange( Room(DormId, "DormRoom", CoachId, isDorm: true, tags: "dormroom,0", accessibility: Accessibility.Private, cloningAllowed: true), Room(RecCenterId, "RecCenter", CoachId, tags: "rro,2", image: "RecCenter", maxPlayers: 12), Room(DriftedShowdownId, "Showdown", CoachId, tags: DriftedShowdownTags, maxPlayers: 16, cloningAllowed: true), Room(DriftedBowlingId, "Bowling", CoachId, tags: "rro,2|sports,0", maxPlayers: 99), Room(PlayerRoomId, "MyCoolRoom", PlayerId, image: "myimage")); db.SubRooms.AddRange( Sub(1, DormId, scene: SceneLocations.Dorm), Sub(3, RecCenterId), Sub(DriftedShowdownId, DriftedShowdownId, scene: SceneLocations.MakerRoom, save: "Showdown.room", bundle: "Showdown_0_1.assetbundle"), Sub(DriftedBowlingId, DriftedBowlingId), Sub(PlayerRoomId, PlayerRoomId)); db.RoomStats.AddRange( new RoomStats { RoomId = DormId }, new RoomStats { RoomId = RecCenterId }, new RoomStats { RoomId = DriftedShowdownId }, new RoomStats { RoomId = PlayerRoomId }); db.RoomRoles.AddRange( new RoomRole { RoomId = DriftedShowdownId, AccountId = CoachId, Role = RoleType.Creator }, new RoomRole { RoomId = PlayerRoomId, AccountId = PlayerId, Role = RoleType.Creator }); db.RoomKeys.Add(new RoomKey { RoomId = DriftedShowdownId, ReplicationId = "key", Name = "Key", Description = "" }); db.RoomBans.Add(new RoomBan { roomId = DriftedShowdownId, actorId = CoachId, bannedPlayerId = PlayerId }); db.RoomComments.Add(new RoomComment { RoomId = DriftedShowdownId, CreatorAccountId = PlayerId, Comment = "on the wrong room" }); db.RoomComments.Add(new RoomComment { RoomId = PlayerRoomId, CreatorAccountId = PlayerId, Comment = "kept for nobody" }); db.RoomInteractions.Add(new PlayerInteractionDB { PlayerId = PlayerId, RoomId = DriftedShowdownId, Visited = true }); db.RoomInteractions.Add(new PlayerInteractionDB { PlayerId = PlayerId, RoomId = PlayerRoomId, Visited = true }); db.RoomInstances.Add(new RoomInstance { roomId = DriftedShowdownId, subRoomId = DriftedShowdownId }); db.LeaderboardScores.Add(new LeaderboardScore { RoomId = DriftedShowdownId, PlayerId = PlayerId, Score = 12 }); db.SubRoomPermissions.Add(new SubRoomPermission { subRoomId = DriftedShowdownId, permission = "AnyoneCanSave" }); db.SubRoomHistory.Add(new RestorableSubRoom { RoomId = DriftedShowdownId, SubRoomId = DriftedShowdownId, DataBlob = "old" }); db.PlayerEvents.Add(new PlayerEventDB { PlayerEventId = 7, Name = "Event in a room that is about to go", CreatorPlayerId = PlayerId, RoomId = DriftedShowdownId, StartTime = DateTime.UtcNow, EndTime = DateTime.UtcNow, }); db.PlayerEventResponses.Add(new PlayerEventResponse { PlayerEventId = 7, PlayerId = PlayerId }); // These three only point at a room, so their rows are kept and the reference cleared. db.Images.Add(new ImageDB { Name = "photo-in-showdown", OwnerId = PlayerId, RoomId = DriftedShowdownId }); db.Messages.Add(new Message { ToPlayerId = PlayerId, FromPlayerId = CoachId, RoomId = DriftedShowdownId, SentTime = DateTime.UtcNow }); // Already aligned: the trap the reset has to clear. db.GameConfig.Add(new ConfigVal("DefaultRooms.AlignmentVersion", "1")); db.SaveChanges(); } [Fact] public void TheCatalogShipsShowdownWithItsOwnImageAndRoomFile() { var showdown = DefaultRooms.All.Single(room => room.Name == "Showdown"); Assert.Equal("Showdown", showdown.ImageName); Assert.Equal(16, showdown.MaxPlayers); Assert.True(showdown.CloningAllowed); Assert.True(showdown.IsRRO); Assert.Equal(Accessibility.Public, showdown.Accessibility); Assert.Equal(SceneLocations.MakerRoom, showdown.SceneId); Assert.Equal("Showdown.room", showdown.SaveName); Assert.Equal("Showdown_0_1.assetbundle", showdown.BundleName); // Spelled out rather than through RroWith, because "beta" is stored as an Auto tag. Assert.Equal(DriftedShowdownTags, StringTools.CreateTagString(showdown.Tags.ToList())); } [Fact] public async Task TheResetEmptiesTheTableAndPutsBackTheDormTemplateAndTheShippedRooms() { var db = NewDb(); SeedDriftedState(db); await DefaultRooms.EnsureAsync(db); db.ChangeTracker.Clear(); // Every room went, with no exception list - the old ids are all gone and nothing is left // in the tables that hung off them. foreach (var oldId in new[] { DormId, RecCenterId, DriftedShowdownId, DriftedBowlingId, PlayerRoomId }) Assert.False(await db.Rooms.AnyAsync(room => room.RoomId == oldId)); Assert.Empty(await db.RoomComments.ToListAsync()); Assert.Empty(await db.RoomInteractions.ToListAsync()); Assert.Empty(await db.RoomInstances.ToListAsync()); Assert.Empty(await db.LeaderboardScores.ToListAsync()); Assert.Empty(await db.RoomKeys.ToListAsync()); Assert.Empty(await db.RoomBans.ToListAsync()); Assert.Empty(await db.SubRoomPermissions.ToListAsync()); Assert.Empty(await db.SubRoomHistory.ToListAsync()); Assert.Empty(await db.PlayerEvents.ToListAsync()); Assert.Empty(await db.PlayerEventResponses.ToListAsync()); // A photo, a message and a moderation record outlive the room, without the dead reference. Assert.Null((await db.Images.SingleAsync(image => image.Name == "photo-in-showdown")).RoomId); Assert.Null((await db.Messages.SingleAsync()).RoomId); // The dorm template is back before anyone can join - it is the row every player's own dorm // is cloned from, so a server without it gives out no dorms at all. var dorm = await db.Rooms.SingleAsync(room => room.IsDorm); Assert.Equal("DormRoom", dorm.Name); Assert.Equal("Dorm", dorm.ImageName); Assert.Equal(CoachId, dorm.CreatorAccountId); Assert.True(dorm.CloningAllowed); var dormSub = await db.SubRooms.SingleAsync(sub => sub.RoomId == dorm.RoomId); Assert.Equal("Home", dormSub.Name); Assert.Equal(SceneLocations.Dorm, dormSub.UnitySceneId); Assert.True(dormSub.IsSandbox); Assert.True(await db.RoomStats.AnyAsync(stats => stats.RoomId == dorm.RoomId)); // ...and the whole shipped set was built from the catalog on the same boot, Showdown with // its own picture and its room file attached. var showdown = await db.Rooms.SingleAsync(room => room.Name == "Showdown"); Assert.NotEqual(DriftedShowdownId, showdown.RoomId); Assert.Equal("Showdown", showdown.ImageName); Assert.Equal(16, showdown.MaxPlayers); Assert.Equal(DriftedShowdownTags, showdown.Tags); Assert.True(showdown.CloningAllowed); Assert.True(showdown.IsRRO); Assert.Equal(Accessibility.Public, showdown.Accessibility); Assert.Equal(RoomState.Active, showdown.State); Assert.Equal(CoachId, showdown.CreatorAccountId); var sub = await db.SubRooms.SingleAsync(room => room.RoomId == showdown.RoomId); Assert.Equal("Home", sub.Name); Assert.Equal(SceneLocations.MakerRoom, sub.UnitySceneId); Assert.Equal("Showdown.room", sub.DataBlob); Assert.Equal("Showdown_0_1.assetbundle", sub.UnityAssetId); Assert.Equal(16, sub.MaxPlayers); Assert.False(sub.IsSandbox); Assert.True(await db.RoomStats.AnyAsync(stats => stats.RoomId == showdown.RoomId)); Assert.True(await db.RoomRoles.AnyAsync(role => role.RoomId == showdown.RoomId && role.AccountId == CoachId)); var recCenter = await db.Rooms.SingleAsync(room => room.Name == "RecCenter"); Assert.NotEqual(RecCenterId, recCenter.RoomId); Assert.Equal("RecCenter", recCenter.ImageName); var bowling = await db.Rooms.SingleAsync(room => room.Name == "Bowling"); Assert.Equal("Bowling", bowling.ImageName); Assert.Equal(8, bowling.MaxPlayers); Assert.True(await db.Rooms.AnyAsync(room => room.Name == "Orientation")); // Exactly the shipped set, plus the one dorm template. Assert.Equal(DefaultRooms.All.Count + 1, await db.Rooms.CountAsync()); Assert.Equal("1", (await db.GameConfig.SingleAsync(config => config.Key == "DefaultRooms.AlignmentVersion")).Value); Assert.Equal("3", (await db.GameConfig.SingleAsync(config => config.Key == "DefaultRooms.ShippedRoomResetVersion")).Value); } [Fact] public async Task TheResetRunsOnceAndDoesNotWipeTheRoomsItJustSeeded() { var db = NewDb(); SeedDriftedState(db); await DefaultRooms.EnsureAsync(db); var seeded = await db.Rooms.SingleAsync(room => room.Name == "Showdown"); var dorm = await db.Rooms.SingleAsync(room => room.IsDorm); // A second boot: the marker stops the wipe, so the rooms the first one built are still // there - not deleted and rebuilt every time the server starts. await DefaultRooms.EnsureAsync(db); db.ChangeTracker.Clear(); Assert.Equal(seeded.RoomId, (await db.Rooms.SingleAsync(room => room.Name == "Showdown")).RoomId); Assert.Equal(dorm.RoomId, (await db.Rooms.SingleAsync(room => room.IsDorm)).RoomId); Assert.Equal(DefaultRooms.All.Count + 1, await db.Rooms.CountAsync()); } }