using System.Security.Claims; using System.Text.Json; using System.Text.Json.Nodes; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using RadicadeServer.Admin; using RadicadeServer.API.Avatars.V2; using RadicadeServer.API.Storefront; using RadicadeServer.DB; using RadicadeServer.RecNet.Avatar; using RadicadeServer.RecNet.Enums.Store; using Xunit; namespace RadicadeServer.Tests; /// /// The Gift Player page in the admin panel posts one gift_log row per account to /// POST admin/panel/gift_log, and AddGiftLogRows turns each row into a real gift box on /// that account. The box carries its sender as RecNet's Gift.FromPlayerId, and that is what the /// player reads off the box - so the panel's "Gift From" field has to land there, not only on the /// panel's own log row. /// /// The panel sends ids (both player_id and from_player_id) as digits in a JSON string, because a /// platform id is seventeen digits and a JSON number cannot hold that many exactly. /// /// These tests drive the real route and then read the box back the way a client does - /// GET api/avatar/v2/gifts - rather than inspecting the row the route answered with, so a sender /// stored in the log but not on the box fails here. public class AdminGiftFromTests { private const string Secret = "test-bot-secret"; private static RecDBContext NewDb() => new( TestDbOptions.Create() .UseInMemoryDatabase(Guid.NewGuid().ToString()) .Options); /// Posts one gift_log row through the route, and returns the row the route answered with. private static async Task GiveAsync(RecDBContext db, string rowJson) { var api = new AdminPanelAPI(db); var originalKey = DBConfig.AdminApiKey; var originalLogs = DBConfig.PersistentLogs; DBConfig.AdminApiKey = Secret; DBConfig.PersistentLogs = Path.Join(Path.GetTempPath(), "radicade-admin-tests-" + Guid.NewGuid().ToString("N")); try { var payload = JsonDocument.Parse(rowJson).RootElement; var result = await api.Insert("gift_log", Secret, payload); var ok = Assert.IsType(result); return Assert.IsType(ok.Value); } finally { DBConfig.AdminApiKey = originalKey; DBConfig.PersistentLogs = originalLogs; } } /// The box as the recipient's client reads it: 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(); } /// The JSON a client actually receives for one box, with the server's own naming. private static JsonElement SerializeAsClient(Gift gift) => JsonDocument.Parse(JsonSerializer.Serialize(gift)).RootElement; private const long Recipient = 114903603133502227; private const long Sender = 733901112234567890; [Fact] public async Task TheGiftFromBecomesTheBoxSenderTheClientReads() { var db = NewDb(); var row = await GiveAsync(db, $$""" { "player_id": "{{Recipient}}", "from_player_id": "{{Sender}}", "message": "Thanks for the help!", "context": "Game_Drop", "rarity": "Rare", "avatar": "8aa79563-ace1-4ba7-ad0c-f3210a78142f," } """); // The route's answer is the panel's record of what it did. Assert.Equal(true, row["sent"]?.GetValue()); Assert.Equal(Sender, row["from_player_id"]!.GetValue()); // The box itself, stored against the recipient. var stored = Assert.Single(await StorefrontShared.GetPendingGiftsAsync(db, Recipient)); Assert.Equal(Sender, stored.FromPlayerId); // And what the client reads off that stored box. var read = Assert.Single(await ClientGiftsAsync(db, Recipient)); Assert.Equal(Sender, read.FromPlayerId); var wire = SerializeAsClient(read); Assert.Equal(Sender, wire.GetProperty("FromPlayerId").GetInt64()); Assert.Equal(Rarity.Rare, read.Rarity); Assert.Equal(GiftContext.Game_Drop, read.Context); } [Fact] public async Task ABlankGiftFromSendsItFromTheRadicadeTeam() { var db = NewDb(); // The panel leaves from_player_id out entirely when the field is blank; the box then // comes from the Radicade account, the same sender the server's own admin gifts use. var row = await GiveAsync(db, $$""" { "player_id": "{{Recipient}}", "context": "Game_Drop", "rarity": "Common", "cons": "00000000-0000-0000-0000-000000000000," } """); Assert.Equal(true, row["sent"]?.GetValue()); Assert.Equal(1L, row["from_player_id"]!.GetValue()); var read = Assert.Single(await ClientGiftsAsync(db, Recipient)); Assert.Equal(1L, read.FromPlayerId); } [Fact] public async Task ASeventeenDigitSenderIsNotRoundedOrOverflowed() { var db = NewDb(); // Both ids are past int.MaxValue, which is why the panel posts them as text. Assert.True(Sender > int.MaxValue); Assert.True(Recipient > int.MaxValue); await GiveAsync(db, $$""" { "player_id": "{{Recipient}}", "from_player_id": "{{Sender}}", "context": "Game_Drop", "rarity": "Common", "equip": "Sword" } """); var read = Assert.Single(await ClientGiftsAsync(db, Recipient)); Assert.Equal(Sender, read.FromPlayerId); } [Fact] public async Task ASenderThePanelDidNotSendDoesNotClearTheTeamDefault() { var db = NewDb(); // Nonsense in the field is refused by the panel before it posts; if it ever reaches the // route anyway the box still has a sender rather than a bare zero. await GiveAsync(db, $$""" { "player_id": "{{Recipient}}", "from_player_id": "not-a-player", "context": "Game_Drop", "rarity": "Common", "cons": "00000000-0000-0000-0000-000000000000," } """); var read = Assert.Single(await ClientGiftsAsync(db, Recipient)); Assert.Equal(1L, read.FromPlayerId); } }