using System.Security.Claims; using System.Text; using System.Text.Json; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using RadicadeServer.API.Rooms; using RadicadeServer.API.Rooms.Currency.V1; using RadicadeServer.API.Rooms.Keys.V1; using RadicadeServer.DB; using RadicadeServer.RecNet.Accounts; using RadicadeServer.RecNet.Rooms; using Xunit; namespace RadicadeServer.Tests; public class RoomEconomyPersistenceTests { [Fact] public async Task CurrencyAndConsumableCreationPersistForTheCurrentRoom() { var options = TestDbOptions.Create() .UseInMemoryDatabase(Guid.NewGuid().ToString()) .Options; await using var db = new RecDBContext(options); const long accountId = 71; const long roomId = 902; var instance = new RoomInstance { roomId = roomId }; db.RoomInstances.Add(instance); db.Presences.Add(new RealPresence { playerId = accountId, isOnline = true, roomInstance = instance.roomInstanceId, }); await db.SaveChangesAsync(); var currencyApi = new RoomCurrencyAPI(db); SetUserAndJsonBody(currencyApi, accountId, "{\"Name\":\"Test currency\",\"Description\":\"A test currency\",\"Color\":\"#112233\"}"); var currencyResult = Assert.IsType(await currencyApi.CreateCurrency()); var currency = Assert.IsType>(currencyResult.Value); Assert.Equal(roomId, currency["RoomId"]); Assert.Equal("#112233", currency["Color"]); var consumableApi = new RoomConsumablesAPI(db); SetUserAndJsonBody(consumableApi, accountId, "{\"Name\":\"Test consumable\",\"Description\":\"A test consumable\"}"); var consumableResult = Assert.IsType(await consumableApi.CreateRoomConsumable()); var consumable = Assert.IsType>(consumableResult.Value); Assert.Equal(roomId, consumable["RoomId"]); var listedCurrency = Assert.IsType(await currencyApi.GetRoomCurrencies(roomId)); var listedCurrencies = Assert.IsType>>(listedCurrency.Value); Assert.Single(listedCurrencies); Assert.Equal(currency["CurrencyId"], listedCurrencies[0]["CurrencyId"]); var listedConsumable = Assert.IsType(await consumableApi.GetRoomConsumablesInRoom(roomId)); var listedConsumables = Assert.IsType>>(listedConsumable.Value); Assert.Single(listedConsumables); Assert.Equal(consumable["ConsumableId"], listedConsumables[0]["ConsumableId"]); var storedConfig = await db.ServerConfig.SingleAsync(config => config.Key == "RoomAssets"); Assert.Contains("Test currency", storedConfig.Value); Assert.Contains("Test consumable", storedConfig.Value); // The list routes load records from persisted JSON, not the original request // dictionaries; nested JSON values must remain intact after deserialization. await using var restartedDb = new RecDBContext(options); var reloadedCurrencies = Assert.IsType( await new RoomCurrencyAPI(restartedDb).GetRoomCurrencies(roomId)); var reloadedCurrency = Assert.Single( Assert.IsAssignableFrom>>(reloadedCurrencies.Value)); Assert.Equal("#112233", reloadedCurrency["Color"]?.ToString()); var reloadedConsumables = Assert.IsType( await new RoomConsumablesAPI(restartedDb).GetRoomConsumablesInRoom(roomId)); Assert.Equal("Test consumable", Assert.Single(Assert.IsAssignableFrom>>(reloadedConsumables.Value))["Name"]?.ToString()); } [Fact] public async Task RoomKeyCreationUsesTheCurrentRoomAndPersistsTheKey() { var options = TestDbOptions.Create() .UseInMemoryDatabase(Guid.NewGuid().ToString()) .Options; await using var db = new RecDBContext(options); const long accountId = 81; const long roomId = 903; var instance = new RoomInstance { roomId = roomId }; db.RoomInstances.Add(instance); db.Presences.Add(new RealPresence { playerId = accountId, isOnline = true, roomInstance = instance.roomInstanceId, }); await db.SaveChangesAsync(); var api = new RoomKeysAPI(db); SetUserAndJsonBody(api, accountId, "{\"Name\":\"Test key\",\"Description\":\"A test key\",\"Price\":5}"); var created = Assert.IsType(await api.CreateRoomKey()); var key = Assert.IsType(created.Value); Assert.Equal(roomId, key.RoomId); Assert.Equal("Test key", key.Name); Assert.Equal(key.RoomKeyId, Assert.Single(db.RoomKeys).RoomKeyId); var listed = Assert.IsType(api.GetRoomKeysForRoom(roomId)); Assert.Equal(key.RoomKeyId, Assert.Single(Assert.IsAssignableFrom>(listed.Value)).RoomKeyId); var missingRoomId = Assert.IsType(api.GetRoomKeysForRoom(null)); Assert.Empty(Assert.IsAssignableFrom>(missingRoomId.Value)); // The client endpoint takes the RoomKeyId in its path and answers a bare bool. // Creating a room key doesn't purchase it; model ownership explicitly. db.Unlocks.Add(new PlayerUnlocks { AccountId = accountId, RoomKeys = JsonSerializer.Serialize(new[] { key.RoomKeyId }) }); await db.SaveChangesAsync(); SetUserAndJsonBody(api, accountId, string.Empty); Assert.True(Assert.IsType(Assert.IsType(await api.IsRoomKeyPurchased(key.RoomKeyId)).Value)); Assert.False(Assert.IsType(Assert.IsType(await api.IsRoomKeyPurchased(key.RoomKeyId + 1000)).Value)); Assert.False(Assert.IsType(Assert.IsType(await api.IsRoomKeyPurchased(key.RoomKeyId + 1)).Value)); } [Fact] public void BalanceLookupsAnswerWithAZeroBalanceRatherThanNotFound() { // The 2023 client asks getBalance right after getAllBalances on every room join and // retries an answer it cannot use, so a 404 here is a join that spends a fresh TLS // connection and a round trip (~100 ms RTT) per retry before the room finishes // loading. This server tracks no balances, so the answer is a zero balance. var options = TestDbOptions.Create() .UseInMemoryDatabase(Guid.NewGuid().ToString()) .Options; using var db = new RecDBContext(options); var api = new RoomCurrencyAPI(db); SetUserAndJsonBody(api, 91, "{}"); var result = Assert.IsType(api.GetBalance("d0618354-d68a-4364-a693-b8cf09879973", 91, 25)); var balance = JsonDocument.Parse(JsonSerializer.Serialize(result.Value)).RootElement; Assert.Equal("d0618354-d68a-4364-a693-b8cf09879973", balance.GetProperty("CurrencyId").GetString()); Assert.Equal(91, balance.GetProperty("AccountId").GetInt64()); Assert.Equal(25, balance.GetProperty("RoomId").GetInt64()); Assert.Equal(0, balance.GetProperty("Balance").GetInt32()); // The client does not always send the account it is asking about; the caller's own // id is the only sensible substitute. var anonymous = new RoomCurrencyAPI(db); SetUserAndJsonBody(anonymous, 92, "{}"); var inferred = Assert.IsType(anonymous.GetBalance(null, null, null)); var inferredBalance = JsonDocument.Parse(JsonSerializer.Serialize(inferred.Value)).RootElement; Assert.Equal(92, inferredBalance.GetProperty("AccountId").GetInt64()); Assert.Equal(string.Empty, inferredBalance.GetProperty("CurrencyId").GetString()); } private static void SetUserAndJsonBody(Controller controller, long accountId, string json) { var identity = new ClaimsIdentity("TestAuth"); identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, accountId.ToString())); var request = new DefaultHttpContext { User = new ClaimsPrincipal(identity) }.Request; request.ContentType = "application/json"; request.Body = new MemoryStream(Encoding.UTF8.GetBytes(json)); request.ContentLength = request.Body.Length; controller.ControllerContext = new ControllerContext { HttpContext = request.HttpContext }; } }