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

401 lines
15 KiB
C#

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;
/// <summary>
/// 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<RecDBContext> NewRoomAsync(string name = "RecCenter")
{
var db = NewDb();
db.Rooms.Add(new RoomData
{
RoomId = RoomId,
Name = name,
CreatorAccountId = 1
});
await db.SaveChangesAsync();
return db;
}
/// <summary>Patches the room the way the panel does, and returns the answer.</summary>
private static async Task<IActionResult> 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<IActionResult> SaveGiftsAsync(RecDBContext db, params object[] gifts)
{
var payload = JsonSerializer.Serialize(new { join_gifts = gifts });
return SaveRoomAsync(db, payload);
}
/// <summary>One box as the panel sends it.</summary>
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"
};
}
/// <summary>The boxes as the recipient's client reads them: GET api/avatar/v2/gifts.</summary>
private static async Task<List<Gift>> 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<OkObjectResult>(result);
return Assert.IsAssignableFrom<IEnumerable<Gift>>(ok.Value).ToList();
}
[Fact]
public async Task TwoBoxesAreBothHandedToTheJoiner()
{
var db = await NewRoomAsync();
Assert.IsType<OkObjectResult>(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<OkObjectResult>(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<OkObjectResult>(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<OkObjectResult>(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<BadRequestObjectResult>(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<BadRequestObjectResult>(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));
}
}