Files
Radicade/ServerTests/RoomEconomyPersistenceTests.cs
T
2026-10-16 23:33:13 -07:00

169 lines
8.5 KiB
C#

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<OkObjectResult>(await currencyApi.CreateCurrency());
var currency = Assert.IsType<Dictionary<string, object?>>(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<OkObjectResult>(await consumableApi.CreateRoomConsumable());
var consumable = Assert.IsType<Dictionary<string, object?>>(consumableResult.Value);
Assert.Equal(roomId, consumable["RoomId"]);
var listedCurrency = Assert.IsType<OkObjectResult>(await currencyApi.GetRoomCurrencies(roomId));
var listedCurrencies = Assert.IsType<List<Dictionary<string, object?>>>(listedCurrency.Value);
Assert.Single(listedCurrencies);
Assert.Equal(currency["CurrencyId"], listedCurrencies[0]["CurrencyId"]);
var listedConsumable = Assert.IsType<OkObjectResult>(await consumableApi.GetRoomConsumablesInRoom(roomId));
var listedConsumables = Assert.IsType<List<Dictionary<string, object?>>>(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<OkObjectResult>(
await new RoomCurrencyAPI(restartedDb).GetRoomCurrencies(roomId));
var reloadedCurrency = Assert.Single(
Assert.IsAssignableFrom<IEnumerable<Dictionary<string, object?>>>(reloadedCurrencies.Value));
Assert.Equal("#112233", reloadedCurrency["Color"]?.ToString());
var reloadedConsumables = Assert.IsType<OkObjectResult>(
await new RoomConsumablesAPI(restartedDb).GetRoomConsumablesInRoom(roomId));
Assert.Equal("Test consumable",
Assert.Single(Assert.IsAssignableFrom<IEnumerable<Dictionary<string, object?>>>(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<OkObjectResult>(await api.CreateRoomKey());
var key = Assert.IsType<RoomKey>(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<OkObjectResult>(api.GetRoomKeysForRoom(roomId));
Assert.Equal(key.RoomKeyId, Assert.Single(Assert.IsAssignableFrom<IEnumerable<RoomKey>>(listed.Value)).RoomKeyId);
var missingRoomId = Assert.IsType<OkObjectResult>(api.GetRoomKeysForRoom(null));
Assert.Empty(Assert.IsAssignableFrom<IEnumerable<RoomKey>>(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<bool>(Assert.IsType<OkObjectResult>(await api.IsRoomKeyPurchased(key.RoomKeyId)).Value));
Assert.False(Assert.IsType<bool>(Assert.IsType<OkObjectResult>(await api.IsRoomKeyPurchased(key.RoomKeyId + 1000)).Value));
Assert.False(Assert.IsType<bool>(Assert.IsType<OkObjectResult>(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<OkObjectResult>(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<OkObjectResult>(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 };
}
}