using System.Security.Claims; using System.Text.Json; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using RadicadeServer.Admin; using RadicadeServer.API.Avatars.V2; using RadicadeServer.DB; using RadicadeServer.RecNet.Avatar; using RadicadeServer.RecNet.Rooms; using RadicadeServer.Utils; using Xunit; namespace RadicadeServer.Tests; /// /// A room hands players one or more boxes when they join. The boxes live in RoomJoinGifts, one row /// each, and the panel edits them as a list behind Room Details. /// /// A room saved before that list existed still describes one box on its own Rooms row; it keeps /// handing that out, which is what every room did before the list existed. A room with a list is /// read from the list alone, and each box is claimed on its own, so editing one box re-issues that /// box without silencing the others. /// /// These tests drive the real admin route and then read the boxes the way a client does - /// GET api/avatar/v2/gifts - rather than inspecting the rows the route answered with. [Collection(DbFactoryCollection.Name)] public class RoomJoinGiftListTests { private const string Secret = "room-join-gift-list-key"; private const long RoomId = 911; private const long Joiner = 114903603133502227; private const long Sender = 733901112234567890; private static RecDBContext NewDb() => new( TestDbOptions.Create() .UseInMemoryDatabase(Guid.NewGuid().ToString()) .Options); private static async Task NewRoomAsync(string name = "RecCenter") { var db = NewDb(); db.Rooms.Add(new RoomData { RoomId = RoomId, Name = name, CreatorAccountId = 1 }); await db.SaveChangesAsync(); return db; } /// Patches the room the way the panel does, and returns the answer. private static async Task SaveRoomAsync(RecDBContext db, string payloadJson) { var api = new AdminPanelAPI(db); var originalKey = DBConfig.AdminApiKey; DBConfig.AdminApiKey = Secret; try { var payload = JsonDocument.Parse(payloadJson).RootElement; return await api.Update("rooms", RoomId, Secret, payload); } finally { DBConfig.AdminApiKey = originalKey; } } private static Task SaveGiftsAsync(RecDBContext db, params object[] gifts) { var payload = JsonSerializer.Serialize(new { join_gifts = gifts }); return SaveRoomAsync(db, payload); } /// One box as the panel sends it. private static object Gift(long id, string title, int currency, bool enabled = true, bool oncePerPlayer = true, string fromPlayerId = "", string message = "") { return new { id, enabled, once_per_player = oncePerPlayer, title, message, from_player_id = fromPlayerId, avatar_item = "", avatar_item_type = "Outfit", equipment_item = "", equipment_modification_guid = "", consumable_item = "", currency_type = "RecCenterTokens", currency_amount = currency, xp = 0, context = "Game_Drop", rarity = "Common" }; } /// The boxes as the recipient's client reads them: GET api/avatar/v2/gifts. private static async Task> ClientGiftsAsync(RecDBContext db, long playerId) { var api = new AvatarV2(db) { ControllerContext = new ControllerContext { HttpContext = new DefaultHttpContext { User = new ClaimsPrincipal(new ClaimsIdentity( [new Claim(ClaimTypes.NameIdentifier, playerId.ToString())], "test")) } } }; var result = await api.GetGifts(); var ok = Assert.IsType(result); return Assert.IsAssignableFrom>(ok.Value).ToList(); } [Fact] public async Task TwoBoxesAreBothHandedToTheJoiner() { var db = await NewRoomAsync(); Assert.IsType(await SaveGiftsAsync(db, Gift(0, "Welcome!", 25), Gift(0, "Thanks!", 5))); Assert.True(await RoomJoinGiftUtils.TryDeliverAsync(db, Joiner, RoomId)); var boxes = await ClientGiftsAsync(db, Joiner); Assert.Equal(2, boxes.Count); Assert.Equal(new[] { "Welcome!", "Thanks!" }, boxes.Select(x => x.FriendlyName).OrderBy(x => x == "Welcome!" ? 0 : 1)); Assert.Equal(2, boxes.Select(x => x.GiftDropId).Distinct().Count()); Assert.Equal([25, 5], boxes.Select(x => x.Currency).OrderByDescending(x => x)); } [Fact] public async Task AnOrderedListIsHandedOutInTheOrderThePanelShowsIt() { var db = await NewRoomAsync(); await SaveGiftsAsync(db, Gift(0, "First", 1), Gift(0, "Second", 2), Gift(0, "Third", 3)); await RoomJoinGiftUtils.TryDeliverAsync(db, Joiner, RoomId); // Each box names itself, so the client's list can be read in the order the room stores. var stored = await db.RoomJoinGifts.Where(x => x.RoomId == RoomId) .OrderBy(x => x.SortOrder).ToListAsync(); Assert.Equal(new[] { "First", "Second", "Third" }, stored.Select(x => x.Title)); Assert.Equal(new[] { 0, 1, 2 }, stored.Select(x => x.SortOrder)); } [Fact] public async Task EachBoxIsClaimedOnItsOwn() { var db = await NewRoomAsync(); await SaveGiftsAsync(db, Gift(0, "Welcome!", 25), Gift(0, "Thanks!", 5)); Assert.True(await RoomJoinGiftUtils.TryDeliverAsync(db, Joiner, RoomId)); // Nothing changed, so the second visit is silent. Assert.False(await RoomJoinGiftUtils.TryDeliverAsync(db, Joiner, RoomId)); Assert.Equal(2, (await ClientGiftsAsync(db, Joiner)).Count); } [Fact] public async Task AChangedBoxIsHandedOutAgainWithoutReissuingTheOther() { var db = await NewRoomAsync(); await SaveGiftsAsync(db, Gift(0, "Welcome!", 25), Gift(0, "Thanks!", 5)); await RoomJoinGiftUtils.TryDeliverAsync(db, Joiner, RoomId); var saved = (await db.RoomJoinGifts.Where(x => x.RoomId == RoomId) .OrderBy(x => x.SortOrder).ToListAsync()); var firstId = saved[0].RoomJoinGiftId; var secondId = saved[1].RoomJoinGiftId; // The panel posts the whole list every save: only the second box's amount changed. await SaveGiftsAsync(db, Gift(firstId, "Welcome!", 25), Gift(secondId, "Thanks!", 50)); Assert.True(await RoomJoinGiftUtils.TryDeliverAsync(db, Joiner, RoomId)); var boxes = await ClientGiftsAsync(db, Joiner); Assert.Equal(3, boxes.Count); Assert.Equal(3, boxes.Select(x => x.GiftDropId).Distinct().Count()); Assert.Single(boxes, x => x.Currency == 50); // The box that did not change was not handed out a second time. var unchanged = await db.RoomJoinGifts.SingleAsync(x => x.RoomJoinGiftId == firstId); Assert.Equal(1, unchanged.Version); Assert.Equal(2, (await db.RoomJoinGifts.SingleAsync(x => x.RoomJoinGiftId == secondId)).Version); } [Fact] public async Task ABoxLeftOutOfTheListIsNoLongerHandedOut() { var db = await NewRoomAsync(); await SaveGiftsAsync(db, Gift(0, "Welcome!", 25), Gift(0, "Thanks!", 5)); var first = (await db.RoomJoinGifts.OrderBy(x => x.SortOrder).FirstAsync()).RoomJoinGiftId; await SaveGiftsAsync(db, Gift(first, "Welcome!", 25)); Assert.Single(await db.RoomJoinGifts.Where(x => x.RoomId == RoomId).ToListAsync()); await RoomJoinGiftUtils.TryDeliverAsync(db, Joiner, RoomId); var boxes = Assert.Single(await ClientGiftsAsync(db, Joiner)); Assert.Equal("Welcome!", boxes.FriendlyName); } [Fact] public async Task ADisabledBoxIsSkippedAndTheRestStillArrive() { var db = await NewRoomAsync(); await SaveGiftsAsync(db, Gift(0, "Welcome!", 25), Gift(0, "Thanks!", 5, enabled: false)); Assert.True(await RoomJoinGiftUtils.TryDeliverAsync(db, Joiner, RoomId)); var boxes = Assert.Single(await ClientGiftsAsync(db, Joiner)); Assert.Equal("Welcome!", boxes.FriendlyName); } [Fact] public async Task ABoxThatGivesNothingIsNotAHandout() { var db = await NewRoomAsync(); // The editor always keeps at least one card, and a blank one gives nothing at all. await SaveGiftsAsync(db, Gift(0, "Room Visit Gift", 0)); Assert.False(await RoomJoinGiftUtils.TryDeliverAsync(db, Joiner, RoomId)); Assert.Empty(await ClientGiftsAsync(db, Joiner)); } [Fact] public async Task ARoomSavedBeforeTheListStillHandsOutItsOwnBox() { var db = await NewRoomAsync(); var room = await db.Rooms.SingleAsync(x => x.RoomId == RoomId); room.JoinGiftEnabled = true; room.JoinGiftOncePerPlayer = true; room.JoinGiftTitle = "Room Visit Gift"; room.JoinGiftCurrencyAmount = 25; await db.SaveChangesAsync(); Assert.True(await RoomJoinGiftUtils.TryDeliverAsync(db, Joiner, RoomId)); var box = Assert.Single(await ClientGiftsAsync(db, Joiner)); Assert.Equal(25, box.Currency); Assert.Empty(await db.RoomJoinGifts.ToListAsync()); } [Fact] public async Task TheRoomsOwnFieldsDescribeTheFirstBox() { var db = await NewRoomAsync(); Assert.IsType(await SaveGiftsAsync(db, Gift(0, "Welcome!", 25, fromPlayerId: Sender.ToString()), Gift(0, "Thanks!", 5, enabled: false))); var room = await db.Rooms.SingleAsync(x => x.RoomId == RoomId); Assert.Equal("Welcome!", room.JoinGiftTitle); Assert.Equal(25, room.JoinGiftCurrencyAmount); Assert.Equal(Sender, room.JoinGiftFromPlayerId); // The room is still handing boxes out even though only its first one is enabled. Assert.True(room.JoinGiftEnabled); } [Fact] public async Task AListWithEveryBoxDisabledClosesTheRoom() { var db = await NewRoomAsync(); await SaveGiftsAsync(db, Gift(0, "Welcome!", 25, enabled: false), Gift(0, "Thanks!", 5, enabled: false)); var room = await db.Rooms.SingleAsync(x => x.RoomId == RoomId); Assert.False(room.JoinGiftEnabled); Assert.False(await RoomJoinGiftUtils.TryDeliverAsync(db, Joiner, RoomId)); } [Fact] public async Task ThePanelReadsTheWholeListBackWithSendersAsDigits() { var db = await NewRoomAsync(); await SaveGiftsAsync(db, Gift(0, "Welcome!", 25, fromPlayerId: Sender.ToString()), Gift(0, "Thanks!", 5)); var api = new AdminPanelAPI(db); var originalKey = DBConfig.AdminApiKey; DBConfig.AdminApiKey = Secret; string? firstSender; int count; try { var details = Assert.IsType(await api.RoomPanelDetails(RoomId, Secret)); using var doc = JsonDocument.Parse(JsonSerializer.Serialize(details.Value)); var gifts = doc.RootElement.GetProperty("room").GetProperty("join_gifts"); count = gifts.GetArrayLength(); var sender = gifts[0].GetProperty("from_player_id"); Assert.Equal(JsonValueKind.String, sender.ValueKind); firstSender = sender.GetString(); } finally { DBConfig.AdminApiKey = originalKey; } Assert.Equal(2, count); Assert.Equal(Sender.ToString(), firstSender); } [Fact] public async Task TheRoomsListThePanelLoadsCarriesEveryBox() { var db = await NewRoomAsync(); await SaveGiftsAsync(db, Gift(0, "Welcome!", 25), Gift(0, "Thanks!", 5)); // The panel's room table is what fills the editor, so the list has to travel with it - the // details route is not what opens the modal. var api = new AdminPanelAPI(db); var originalKey = DBConfig.AdminApiKey; DBConfig.AdminApiKey = Secret; JsonElement room; try { var result = Assert.IsType(await api.List("rooms", Secret, null, null, null)); using var doc = JsonDocument.Parse(JsonSerializer.Serialize(result.Value)); room = doc.RootElement.EnumerateArray() .Single(x => x.GetProperty("id").GetInt64() == RoomId) .Clone(); } finally { DBConfig.AdminApiKey = originalKey; } var gifts = room.GetProperty("join_gifts"); Assert.Equal(2, gifts.GetArrayLength()); Assert.Equal("Welcome!", gifts[0].GetProperty("title").GetString()); Assert.Equal(25, gifts[0].GetProperty("currency_amount").GetInt32()); Assert.Equal("Thanks!", gifts[1].GetProperty("title").GetString()); } [Fact] public async Task ASenderThatIsNotAnAccountIdRefusesTheWholeList() { var db = await NewRoomAsync(); var result = await SaveGiftsAsync(db, Gift(0, "Welcome!", 25), Gift(0, "Thanks!", 5, fromPlayerId: "not-a-player")); var bad = Assert.IsType(result); Assert.Contains("join_gifts[1].from_player_id", bad.Value?.ToString()); // Nothing was written: a save the panel could not make sense of leaves no half-list behind. Assert.Empty(await db.RoomJoinGifts.ToListAsync()); Assert.Equal(0, (await db.Rooms.SingleAsync(x => x.RoomId == RoomId)).JoinGiftCurrencyAmount); } [Fact] public async Task ABoxIdFromAnotherRoomIsRefused() { var db = await NewRoomAsync(); db.RoomJoinGifts.Add(new RoomJoinGift { RoomId = 4242, SortOrder = 0, Title = "Someone else's", CurrencyAmount = 1 }); await db.SaveChangesAsync(); var foreignId = (await db.RoomJoinGifts.SingleAsync()).RoomJoinGiftId; var result = await SaveGiftsAsync(db, Gift(foreignId, "Welcome!", 25)); Assert.IsType(result); Assert.Empty(await db.RoomJoinGifts.Where(x => x.RoomId == RoomId).ToListAsync()); } [Fact] public async Task ReorderingTheListReissuesNothing() { var db = await NewRoomAsync(); await SaveGiftsAsync(db, Gift(0, "First", 1), Gift(0, "Second", 2)); var ids = (await db.RoomJoinGifts.OrderBy(x => x.SortOrder).ToListAsync()) .Select(x => x.RoomJoinGiftId).ToArray(); await SaveGiftsAsync(db, Gift(ids[1], "Second", 2), Gift(ids[0], "First", 1)); var stored = await db.RoomJoinGifts.OrderBy(x => x.SortOrder).ToListAsync(); Assert.Equal(new[] { ids[1], ids[0] }, stored.Select(x => x.RoomJoinGiftId)); Assert.All(stored, x => Assert.Equal(1, x.Version)); } }