using System.Text.Json; using System.Text.Json.Nodes; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using RadicadeServer.Admin; using RadicadeServer.DB; using RadicadeServer.RecNet.Auth; using RadicadeServer.RecNet.Enums; using RadicadeServer.RecNet.Rooms; using RadicadeServer.Utils; using Xunit; namespace RadicadeServer.Tests; /// /// IsDeveloperOwned is the flag that files a room under the client's Dev Rooms row, and /// the room payload never carried it at all - so every room read as "not developer-owned" and a /// developer's own rooms were missing from that list. It is a property of the room, stamped when /// the room is made (a player makes a room by CLONING a template - there is no separate create /// route) from whether the maker holds the developer role. /// /// These pin the three ways it can come out wrong: stamped from nothing (always false), stamped /// on a dorm (a dev's dorm is a home, not a project), and stamping the room row while the room /// the client is handed still defaults to false. /// [Collection(DbFactoryCollection.Name)] public class RoomDeveloperOwnedTests { private const long DeveloperId = 2; private const long PlainPlayerId = 3; private const long MakerRoomTemplateId = 23; private const long DormTemplateId = 1; private sealed class TestDbContextFactory : IDbContextFactory { private readonly DbContextOptions options; public TestDbContextFactory() { options = TestDbOptions.Create() .UseInMemoryDatabase(Guid.NewGuid().ToString()) .Options; } public RecDBContext CreateDbContext() => new(options); } /// /// The shape a player's create-room flow runs against: a clonable template (room 23 is the /// Maker Room the client clones) and the per-player dorm template. Room 1 is what /// GetOrCreateDorm clones, so it has to be a dorm for the dorm case to be real. /// private static async Task<(TestDbContextFactory Factory, RecDBContext Db)> TemplatesAsync(bool withDeveloperProfile = true) { var factory = new TestDbContextFactory(); RoomUtils.DbFactory = factory; var db = factory.CreateDbContext(); db.Rooms.Add(new RoomData { RoomId = MakerRoomTemplateId, Name = "MakerRoom", CloningAllowed = true, Accessibility = Accessibility.Public, CreatorAccountId = 1, MaxPlayers = 8, CreatedAt = DateTime.UtcNow, }); db.SubRooms.Add(new SubRoom { RoomId = MakerRoomTemplateId, Name = "Home", UnitySceneId = SceneLocations.MakerRoom, Accessibility = Accessibility.Public, MaxPlayers = 8, DataBlob = "", }); db.Rooms.Add(new RoomData { RoomId = DormTemplateId, Name = "DormRoom", IsDorm = true, CloningAllowed = true, Accessibility = Accessibility.Private, CreatorAccountId = 1, MaxPlayers = 4, CreatedAt = DateTime.UtcNow, }); db.SubRooms.Add(new SubRoom { RoomId = DormTemplateId, Name = "DormRoom", UnitySceneId = "76d98498-60a1-430c-ab76-b54a29b7a163", Accessibility = Accessibility.Private, MaxPlayers = 4, DataBlob = "", }); db.Profiles.Add(new Profile { accountId = DeveloperId, role = withDeveloperProfile ? "developer" : null, username = "devplayer", displayName = "Dev Player", profileImage = DBConfig.DefaultProfileImage, }); await db.SaveChangesAsync(); return (factory, db); } [Fact] public async Task ARoomMadeByADeveloperIsDeveloperOwned() { var (_, db) = await TemplatesAsync(); var made = await RoomUtils.CloneRoom(MakerRoomTemplateId, DeveloperId, "My Dev Room"); Assert.NotNull(made); Assert.Equal(DeveloperId, made!.CreatorAccountId); Assert.True(made.IsDeveloperOwned); // The room the client is handed and the row it came from have to agree - the response is // what the Dev Rooms row is built from, and the row is what the next read answers with. Assert.True(db.Rooms.Single(x => x.Name == "My Dev Room").IsDeveloperOwned); } [Fact] public async Task ARoomMadeByAPlainPlayerIsNotDeveloperOwned() { var (_, db) = await TemplatesAsync(); var made = await RoomUtils.CloneRoom(MakerRoomTemplateId, PlainPlayerId, "Plain Room"); Assert.NotNull(made); Assert.False(made!.IsDeveloperOwned); Assert.False(db.Rooms.Single(x => x.Name == "Plain Room").IsDeveloperOwned); } [Fact] public async Task ADevelopersDormIsNotDeveloperOwned() { var (_, db) = await TemplatesAsync(); var dorm = await RoomUtils.GetOrCreateDorm(DeveloperId); Assert.NotNull(dorm); Assert.True(dorm!.IsDorm); // The dorm is cloned by the same code path that creates rooms, so without the dorm guard // a developer's own dorm came back developer-owned. Assert.False(dorm.IsDeveloperOwned); Assert.False(db.Rooms.Single(x => x.IsDorm && x.CreatorAccountId == DeveloperId).IsDeveloperOwned); } [Fact] public async Task TheRoomTheClientReadsCarriesTheFlag() { await TemplatesAsync(); await RoomUtils.CloneRoom(MakerRoomTemplateId, DeveloperId, "Read Back"); var read = await RoomUtils.GetRoom("Read Back"); Assert.NotNull(read); Assert.True(read!.IsDeveloperOwned); // The 2023 client reads room payloads by their declared names (Program.cs pins the // serializer to no naming policy), the same PascalCase the real API answers with. var json = JsonSerializer.Serialize(read); Assert.Contains("\"IsDeveloperOwned\":true", json); } [Fact] public void APayloadWithoutTheFieldReadsAsFalse() { // Every room that predates the column, and every room an importer does not say otherwise // about, serializes the field explicitly - a missing property is what makes the client's // parser fall back to its own default. var json = JsonSerializer.Serialize(new FullRoom { Name = "Imported" }); Assert.Contains("\"IsDeveloperOwned\":false", json); } // ---- The admin create route, the path every import uses ------------------------------- private const string Secret = "test-bot-secret"; private static async Task CreateRoomAsync(RecDBContext db, long newOwner, string json) { var api = new AdminRoomAPI(db); var original = DBConfig.AdminApiKey; DBConfig.AdminApiKey = Secret; try { return await api.CreateRoom(Secret, newOwner, JsonNode.Parse(json)!); } finally { DBConfig.AdminApiKey = original; } } [Fact] public async Task AnImportedRoomWithNoOpinionTakesTheOwnersRoles() { var factory = new TestDbContextFactory(); var db = factory.CreateDbContext(); db.Profiles.Add(new Profile { accountId = DeveloperId, role = "developer", username = "devplayer", displayName = "Dev Player", profileImage = DBConfig.DefaultProfileImage, }); await db.SaveChangesAsync(); Assert.IsType(await CreateRoomAsync(db, DeveloperId, """{ "Name": "DerivedDevRoom" }""")); Assert.True(db.Rooms.Single(x => x.Name == "DerivedDevRoom").IsDeveloperOwned); } [Fact] public async Task AnImportedRoomThatSaysItIsDeveloperOwnedIsBelieved() { var factory = new TestDbContextFactory(); var db = factory.CreateDbContext(); // A captured RRO room is developer-owned on the real service even though it is recreated // here under a local (non-developer) owner, so an explicit value wins. Assert.IsType(await CreateRoomAsync(db, PlainPlayerId, """{ "Name": "ImportedRRO", "IsRRO": true, "IsDeveloperOwned": true }""")); Assert.True(db.Rooms.Single(x => x.Name == "ImportedRRO").IsDeveloperOwned); } [Fact] public async Task ANonDeveloperOwnerMakesAPlainRoom() { var factory = new TestDbContextFactory(); var db = factory.CreateDbContext(); Assert.IsType(await CreateRoomAsync(db, PlainPlayerId, """{ "Name": "PlayerRoom", "IsDeveloperOwned": false }""")); Assert.False(db.Rooms.Single(x => x.Name == "PlayerRoom").IsDeveloperOwned); } }