mirror of
https://github.com/JogmansonReal/Radicade.git
synced 2026-10-26 07:29:00 -07:00
259 lines
9.9 KiB
C#
259 lines
9.9 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>
|
|
/// Who a room's join box is from. The box a player receives on joining already travelled with
|
|
/// RecNet's <c>Gift.FromPlayerId</c> - it is the field the client reads to put a name on the box -
|
|
/// but a room's join gift left it at zero, so every join box arrived from nobody.
|
|
///
|
|
/// The room's Room Details modal now carries a FromPlayerId, which lands in
|
|
/// <c>Rooms.JoinGiftFromPlayerId</c> and from there on the box. The value crosses a JSON boundary
|
|
/// twice, and an account id is eighteen digits, so the panel and the route keep it as digits in a
|
|
/// string rather than as a number: a JSON number is read back as a double and its last digits are
|
|
/// rounded away, which would name a different account.
|
|
///
|
|
/// These tests drive the real admin routes and then read the box the way a client does -
|
|
/// GET api/avatar/v2/gifts - rather than inspecting the row the route answered with.
|
|
[Collection(DbFactoryCollection.Name)]
|
|
public class RoomJoinGiftFromTests
|
|
{
|
|
private const string Secret = "room-join-gift-from-key";
|
|
private const long RoomId = 247;
|
|
private const long Joiner = 114903603133502227;
|
|
private const long Sender = 733901112234567890;
|
|
|
|
private static RecDBContext NewDb() => new(
|
|
TestDbOptions.Create()
|
|
.UseInMemoryDatabase(Guid.NewGuid().ToString())
|
|
.Options);
|
|
|
|
/// <summary>A room that hands every player a box on join, which is what makes the sender matter.</summary>
|
|
private static async Task<RecDBContext> RoomWithJoinGiftAsync()
|
|
{
|
|
var db = NewDb();
|
|
db.Rooms.Add(new RoomData
|
|
{
|
|
RoomId = RoomId,
|
|
Name = "RecCenter",
|
|
CreatorAccountId = 1,
|
|
JoinGiftEnabled = true,
|
|
JoinGiftOncePerPlayer = true,
|
|
JoinGiftTitle = "Room Visit Gift",
|
|
JoinGiftMessage = "Thanks for visiting!",
|
|
JoinGiftCurrencyType = RadicadeServer.RecNet.Enums.Store.CurrencyType.RecCenterTokens,
|
|
JoinGiftCurrencyAmount = 25
|
|
});
|
|
await db.SaveChangesAsync();
|
|
return db;
|
|
}
|
|
|
|
/// <summary>Patches one room the way the panel does, and returns the answer's status.</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;
|
|
}
|
|
}
|
|
|
|
/// <summary>The box as the recipient's client reads it: 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();
|
|
}
|
|
|
|
/// <summary>The one box the joiner's client reads, with the server's own naming on the wire.</summary>
|
|
private static async Task<JsonElement> JoinerBoxOnTheWireAsync(RecDBContext db)
|
|
{
|
|
Assert.True(await RoomJoinGiftUtils.TryDeliverAsync(db, Joiner, RoomId));
|
|
|
|
var read = Assert.Single(await ClientGiftsAsync(db, Joiner));
|
|
return JsonDocument.Parse(JsonSerializer.Serialize(read)).RootElement;
|
|
}
|
|
|
|
[Fact]
|
|
public async Task TheSenderThePanelSavesIsTheSenderOnTheJoinBox()
|
|
{
|
|
var db = await RoomWithJoinGiftAsync();
|
|
|
|
var result = await SaveRoomAsync(db, $$"""
|
|
{
|
|
"join_gift_from_player_id": "{{Sender}}",
|
|
"join_gift_title": "Room Visit Gift"
|
|
}
|
|
""");
|
|
Assert.IsType<OkObjectResult>(result);
|
|
|
|
var stored = await db.Rooms.SingleAsync(x => x.RoomId == RoomId);
|
|
Assert.Equal(Sender, stored.JoinGiftFromPlayerId);
|
|
|
|
// And what the joiner's client reads off the box it was handed.
|
|
var wire = await JoinerBoxOnTheWireAsync(db);
|
|
Assert.Equal(Sender, wire.GetProperty("FromPlayerId").GetInt64());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ASeventeenDigitSenderIsNotRoundedOnItsWayThroughThePanel()
|
|
{
|
|
var db = await RoomWithJoinGiftAsync();
|
|
Assert.True(Sender > int.MaxValue);
|
|
|
|
await SaveRoomAsync(db, $$"""
|
|
{ "join_gift_from_player_id": "{{Sender}}" }
|
|
""");
|
|
|
|
// The panel reads the id back as digits in a string, so the modal shows the digits that
|
|
// were saved rather than a rounded double, and saving again cannot drift the account.
|
|
var api = new AdminPanelAPI(db);
|
|
var originalKey = DBConfig.AdminApiKey;
|
|
DBConfig.AdminApiKey = Secret;
|
|
string? asPanelReadsIt;
|
|
try
|
|
{
|
|
var details = Assert.IsType<OkObjectResult>(await api.RoomPanelDetails(RoomId, Secret));
|
|
using var doc = JsonDocument.Parse(JsonSerializer.Serialize(details.Value));
|
|
var field = doc.RootElement.GetProperty("room").GetProperty("join_gift_from_player_id");
|
|
Assert.Equal(JsonValueKind.String, field.ValueKind);
|
|
asPanelReadsIt = field.GetString();
|
|
}
|
|
finally
|
|
{
|
|
DBConfig.AdminApiKey = originalKey;
|
|
}
|
|
|
|
Assert.Equal(Sender.ToString(), asPanelReadsIt);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task AChangedSenderReissuesTheBox()
|
|
{
|
|
var db = await RoomWithJoinGiftAsync();
|
|
var before = (await db.Rooms.SingleAsync(x => x.RoomId == RoomId)).JoinGiftVersion;
|
|
|
|
await SaveRoomAsync(db, $$"""
|
|
{ "join_gift_from_player_id": "{{Sender}}" }
|
|
""");
|
|
|
|
// A new version means a new gift drop id, so a player who already opened the old box is
|
|
// handed the one that now names a sender.
|
|
var after = await db.Rooms.SingleAsync(x => x.RoomId == RoomId);
|
|
Assert.True(after.JoinGiftVersion > before);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ASaveThatLeavesTheSenderAloneDoesNotReissueTheBox()
|
|
{
|
|
var db = await RoomWithJoinGiftAsync();
|
|
await SaveRoomAsync(db, $$"""
|
|
{ "join_gift_from_player_id": "{{Sender}}" }
|
|
""");
|
|
var afterFirstSave = (await db.Rooms.SingleAsync(x => x.RoomId == RoomId)).JoinGiftVersion;
|
|
|
|
// The same id again - the modal posts every field on every save.
|
|
await SaveRoomAsync(db, $$"""
|
|
{ "join_gift_from_player_id": "{{Sender}}", "join_gift_enabled": true, "join_gift_once_per_player": true }
|
|
""");
|
|
|
|
var again = await db.Rooms.SingleAsync(x => x.RoomId == RoomId);
|
|
Assert.Equal(afterFirstSave, again.JoinGiftVersion);
|
|
Assert.Equal(Sender, again.JoinGiftFromPlayerId);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task AClearedSenderLeavesTheBoxWithNoSender()
|
|
{
|
|
var db = await RoomWithJoinGiftAsync();
|
|
await SaveRoomAsync(db, $$"""
|
|
{ "join_gift_from_player_id": "{{Sender}}" }
|
|
""");
|
|
|
|
// The blank field is what the modal sends for no sender, and it has to clear the room
|
|
// rather than leave the old account on the box.
|
|
Assert.IsType<OkObjectResult>(await SaveRoomAsync(db, """{ "join_gift_from_player_id": "" }"""));
|
|
|
|
Assert.Equal(0, (await db.Rooms.SingleAsync(x => x.RoomId == RoomId)).JoinGiftFromPlayerId);
|
|
|
|
var wire = await JoinerBoxOnTheWireAsync(db);
|
|
Assert.Equal(0, wire.GetProperty("FromPlayerId").GetInt64());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ARoomNoPanelFieldArrivedForKeepsItsSender()
|
|
{
|
|
var db = await RoomWithJoinGiftAsync();
|
|
await SaveRoomAsync(db, $$"""
|
|
{ "join_gift_from_player_id": "{{Sender}}" }
|
|
""");
|
|
|
|
// An older panel, an import or a partial save leaves the field out entirely; that is not
|
|
// a request to clear it.
|
|
Assert.IsType<OkObjectResult>(await SaveRoomAsync(db, """{ "min_level": 5 }"""));
|
|
|
|
Assert.Equal(Sender, (await db.Rooms.SingleAsync(x => x.RoomId == RoomId)).JoinGiftFromPlayerId);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ASenderThatIsNotAnAccountIdIsRefusedRatherThanKept()
|
|
{
|
|
var db = await RoomWithJoinGiftAsync();
|
|
await SaveRoomAsync(db, $$"""
|
|
{ "join_gift_from_player_id": "{{Sender}}" }
|
|
""");
|
|
|
|
// The panel refuses this before it posts; if it ever reaches the route anyway, the room
|
|
// must not be left believing the sender changed.
|
|
var result = await SaveRoomAsync(db, """{ "join_gift_from_player_id": "not-a-player" }""");
|
|
|
|
var bad = Assert.IsType<BadRequestObjectResult>(result);
|
|
Assert.Contains("join_gift_from_player_id", bad.Value?.ToString());
|
|
Assert.Equal(Sender, (await db.Rooms.SingleAsync(x => x.RoomId == RoomId)).JoinGiftFromPlayerId);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ARoomThatNamesNoSenderStillHandsOutItsBox()
|
|
{
|
|
var db = await RoomWithJoinGiftAsync();
|
|
|
|
// Nothing here sets a sender: the box is still delivered, it just names nobody, which is
|
|
// how every join box behaved before the room could name a sender.
|
|
var wire = await JoinerBoxOnTheWireAsync(db);
|
|
|
|
Assert.Equal(0, wire.GetProperty("FromPlayerId").GetInt64());
|
|
Assert.Equal(25, wire.GetProperty("Currency").GetInt32());
|
|
}
|
|
}
|