mirror of
https://github.com/JogmansonReal/Radicade.git
synced 2026-10-26 23:49:02 -07:00
Radicade Public Code
This commit is contained in:
@@ -0,0 +1,300 @@
|
||||
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 Microsoft.EntityFrameworkCore.Diagnostics;
|
||||
using RadicadeServer.Admin;
|
||||
using RadicadeServer.API.Avatars.V2;
|
||||
using RadicadeServer.API.Storefront;
|
||||
using RadicadeServer.DB;
|
||||
using RadicadeServer.RecNet.Accounts;
|
||||
using RadicadeServer.RecNet.Avatar;
|
||||
using Xunit;
|
||||
|
||||
namespace RadicadeServer.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// A gift of equipment is a gift of a *skin*, and a skin is two halves: the tool prefab and the
|
||||
/// modification guid that says which skin of it. The client resolves one from the pair
|
||||
/// (EquipmentSkinCatalog's own key, and what EquipmentCatalogTests pins as "a resolvable skin"),
|
||||
/// so a box carrying only the prefab is a box carrying the bare tool.
|
||||
///
|
||||
/// The shipped catalog has nine rows for [Basketball] alone, all named "[Basketball]", one per
|
||||
/// skin. Two things followed from that, and this file is both of them:
|
||||
///
|
||||
/// - the page the gift is set up on listed those nine rows as one choice, because every row
|
||||
/// arrived with the same prefab name and nothing to tell them apart. The panel keys that box
|
||||
/// by the value it gets here, so the catalogue has to name each row's guid.
|
||||
/// - the box the gift became held the prefab with an empty guid, whatever skin was picked, so
|
||||
/// the recipient was handed a plain basketball. ProcessGiftRowAsync hardcoded that guid to "".
|
||||
///
|
||||
/// The tests drive the real route and then claim the box back through the client's own claim
|
||||
/// route, reading the equipment list the client reads to resolve a skin - so a skin that the
|
||||
/// catalogue can name but the box cannot carry fails here.
|
||||
public class AdminGiftSkinTests
|
||||
{
|
||||
private const string Secret = "test-bot-secret";
|
||||
private const long Recipient = 114903603133502227;
|
||||
|
||||
private static string Fixture(string name) => Path.Combine(AppContext.BaseDirectory, "Fixtures", name);
|
||||
|
||||
private static string ShippedCatalog() => File.ReadAllText(Fixture("equipment.json"));
|
||||
|
||||
private static List<Equipment> Shipped() =>
|
||||
JsonSerializer.Deserialize<List<Equipment>>(ShippedCatalog())!;
|
||||
|
||||
/// <summary>A skin of [Basketball] that the shipped catalogue names, taken from it rather
|
||||
/// than written down here, so the test names a skin the client could actually resolve.</summary>
|
||||
private static Equipment ShippedBasketballSkin() =>
|
||||
Shipped().First(row => row.PrefabName == "[Basketball]" && row.FriendlyName.Contains("Purple"));
|
||||
|
||||
/* The in-memory store is not transactional, and the claim route wraps its work in a
|
||||
* transaction - which the provider reports as a warning raised as an error, so the route
|
||||
* throws before granting anything. The claim is the end of the chain these tests are here to
|
||||
* prove, so the warning is turned off rather than the route skipped: nothing about the store's
|
||||
* lack of transactions changes what a claimed box grants. */
|
||||
private static RecDBContext NewDb() => new(
|
||||
TestDbOptions.Create()
|
||||
.UseInMemoryDatabase(Guid.NewGuid().ToString())
|
||||
.ConfigureWarnings(warnings => warnings.Ignore(InMemoryEventId.TransactionIgnoredWarning))
|
||||
.Options);
|
||||
|
||||
private static void SetUser(Controller controller, long accountId)
|
||||
{
|
||||
var identity = new ClaimsIdentity("TestAuth");
|
||||
identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, accountId.ToString()));
|
||||
controller.ControllerContext = new ControllerContext
|
||||
{
|
||||
HttpContext = new DefaultHttpContext { User = new ClaimsPrincipal(identity) }
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>One catalogue row as the page receives it, whatever shape the route built it in.</summary>
|
||||
private static List<JsonObject> RowsOf(IActionResult result)
|
||||
{
|
||||
var ok = Assert.IsType<OkObjectResult>(result);
|
||||
return JsonNode.Parse(JsonSerializer.Serialize(ok.Value))!.AsArray()
|
||||
.Select(node => node!.AsObject())
|
||||
.ToList();
|
||||
}
|
||||
|
||||
/// <summary>A catalogue, read through the route that serves it to the page.</summary>
|
||||
private static async Task<List<JsonObject>> CatalogAsync(RecDBContext db, string table)
|
||||
{
|
||||
var originalKey = DBConfig.AdminApiKey;
|
||||
DBConfig.AdminApiKey = Secret;
|
||||
try
|
||||
{
|
||||
return RowsOf(await new AdminPanelAPI(db).List(table, Secret, null, null, null));
|
||||
}
|
||||
finally
|
||||
{
|
||||
DBConfig.AdminApiKey = originalKey;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Posts one gift_log row through the route, and returns the row it answered with.</summary>
|
||||
private static async Task<JsonObject> 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-skin-tests-" + Guid.NewGuid().ToString("N"));
|
||||
try
|
||||
{
|
||||
var payload = JsonDocument.Parse(rowJson).RootElement;
|
||||
var ok = Assert.IsType<OkObjectResult>(await api.Insert("gift_log", Secret, payload));
|
||||
return Assert.IsType<JsonObject>(ok.Value);
|
||||
}
|
||||
finally
|
||||
{
|
||||
DBConfig.AdminApiKey = originalKey;
|
||||
DBConfig.PersistentLogs = originalLogs;
|
||||
}
|
||||
}
|
||||
|
||||
/// <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);
|
||||
SetUser(api, playerId);
|
||||
var ok = Assert.IsType<OkObjectResult>(await api.GetGifts());
|
||||
return Assert.IsAssignableFrom<IEnumerable<Gift>>(ok.Value).ToList();
|
||||
}
|
||||
|
||||
/// <summary>The recipient claims every box it holds, through POST api/avatar/v2/gifts/claim.</summary>
|
||||
private static async Task<IActionResult> ClaimAsync(RecDBContext db, long playerId)
|
||||
{
|
||||
var api = new AvatarV2(db);
|
||||
SetUser(api, playerId);
|
||||
return await api.ClaimGifts();
|
||||
}
|
||||
|
||||
/// <summary>The equipment the client is served: GET api/equipment/v2/getUnlocked.</summary>
|
||||
private static List<Equipment> ServedEquipment(RecDBContext db, long playerId)
|
||||
{
|
||||
var api = new EquipmentV2(db);
|
||||
SetUser(api, playerId);
|
||||
return api.GetUnlockedEquipment();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TheEquipmentListNamesTheSkinOfEveryRow()
|
||||
{
|
||||
var previous = DBConfig.DefaultEquipmentItems;
|
||||
try
|
||||
{
|
||||
DBConfig.DefaultEquipmentItems = ShippedCatalog();
|
||||
|
||||
using var db = NewDb();
|
||||
var rows = await CatalogAsync(db, "equipment_items");
|
||||
|
||||
// The rows are real, and the nine basketball ones are the one prefab: the name alone
|
||||
// cannot tell any of them from another, which is why one row used to answer for nine.
|
||||
Assert.NotEmpty(rows);
|
||||
|
||||
var names = rows.Select(row => row["equipment_prefab_name"]!.GetValue<string>()).ToList();
|
||||
Assert.Equal(9, names.Count(name => name == "[Basketball]"));
|
||||
|
||||
// So every row is named by its own guid, and no two rows share one - that is the value
|
||||
// the gift page keys its equipment box by, and one row there is one choice.
|
||||
var keys = rows.Select(row => row["modification_guid"]!.GetValue<string>()).ToList();
|
||||
Assert.All(keys, guid => Assert.False(string.IsNullOrWhiteSpace(guid)));
|
||||
Assert.Equal(keys.Count, keys.Distinct(StringComparer.OrdinalIgnoreCase).Count());
|
||||
}
|
||||
finally
|
||||
{
|
||||
DBConfig.DefaultEquipmentItems = previous;
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ThePickedSkinIsWhatTheBoxCarries()
|
||||
{
|
||||
var db = NewDb();
|
||||
var skin = ShippedBasketballSkin();
|
||||
|
||||
// What the page posts for that row, both halves included.
|
||||
var row = await GiveAsync(db, $$"""
|
||||
{
|
||||
"player_id": "{{Recipient}}",
|
||||
"equip": "{{skin.PrefabName}}",
|
||||
"equipment_modification_guid": "{{skin.ModificationGuid}}",
|
||||
"message": "Here is your ball back.",
|
||||
"context": "Game_Drop",
|
||||
"rarity": "Uncommon"
|
||||
}
|
||||
""");
|
||||
|
||||
Assert.Equal(true, row["sent"]?.GetValue<bool>());
|
||||
|
||||
// The panel's own record of what it did names the skin too.
|
||||
Assert.Equal(skin.ModificationGuid, row["equipment_modification_guid"]!.GetValue<string>());
|
||||
|
||||
// The box itself, stored against the recipient: both halves of the skin, not the bare tool.
|
||||
var stored = Assert.Single(await StorefrontShared.GetPendingGiftsAsync(db, Recipient));
|
||||
Assert.Equal("[Basketball]", stored.EquipmentPrefabName);
|
||||
Assert.Equal(skin.ModificationGuid, stored.EquipmentModificationGuid);
|
||||
|
||||
// And what the client reads off that stored box.
|
||||
var read = Assert.Single(await ClientGiftsAsync(db, Recipient));
|
||||
Assert.Equal("[Basketball]", read.EquipmentPrefabName);
|
||||
Assert.Equal(skin.ModificationGuid, read.EquipmentModificationGuid);
|
||||
}
|
||||
|
||||
/// <summary>One box of one skin, posted the way the page posts it.</summary>
|
||||
private static Task<JsonObject> GiveSkinAsync(RecDBContext db, Equipment skin) => GiveAsync(db, $$"""
|
||||
{
|
||||
"player_id": "{{Recipient}}",
|
||||
"equip": "{{skin.PrefabName}}",
|
||||
"equipment_modification_guid": "{{skin.ModificationGuid}}",
|
||||
"context": "Game_Drop",
|
||||
"rarity": "Uncommon"
|
||||
}
|
||||
""");
|
||||
|
||||
[Fact]
|
||||
public async Task ClaimingTheBoxHandsThePlayerThatSkin()
|
||||
{
|
||||
var db = NewDb();
|
||||
var skin = ShippedBasketballSkin();
|
||||
|
||||
await GiveSkinAsync(db, skin);
|
||||
var box = Assert.Single(await StorefrontShared.GetPendingGiftsAsync(db, Recipient));
|
||||
|
||||
Assert.IsType<OkObjectResult>(await ClaimAsync(db, Recipient));
|
||||
|
||||
// The box is gone...
|
||||
Assert.DoesNotContain(await StorefrontShared.GetPendingGiftsAsync(db, Recipient),
|
||||
gift => gift.GiftDropId == box.GiftDropId);
|
||||
|
||||
// ...and the skin is the player's own. That row is the part no catalogue can account for:
|
||||
// a shipped equipment list is merged into what the client is served either way, so the
|
||||
// gift arriving is only visible here, on the player.
|
||||
var unlocks = await db.Unlocks.FirstAsync(x => x.AccountId == Recipient);
|
||||
var owned = Assert.Single(unlocks.GetEquipment()!, item => item.PrefabName == skin.PrefabName);
|
||||
Assert.Equal(skin.ModificationGuid, owned.ModificationGuid);
|
||||
|
||||
// And the pair is what the client is served from the list it resolves a skin from.
|
||||
Assert.Single(ServedEquipment(db, Recipient),
|
||||
item => item.PrefabName == skin.PrefabName && item.ModificationGuid == skin.ModificationGuid);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ASkinOfAToolThePlayerAlreadyHasIsStillGiven()
|
||||
{
|
||||
var db = NewDb();
|
||||
var basketball = Shipped().Where(row => row.PrefabName == "[Basketball]").Take(2).ToList();
|
||||
var held = basketball[0];
|
||||
var coming = basketball[1];
|
||||
Assert.NotEqual(held.ModificationGuid, coming.ModificationGuid);
|
||||
|
||||
// The player already has one skin of the tool, which is the ordinary case: the gift is
|
||||
// asked for by whoever set it up, and what they are adding is the skin, not the tool.
|
||||
var unlocks = new PlayerUnlocks { AccountId = Recipient };
|
||||
unlocks.AddEquipment(new Equipment
|
||||
{
|
||||
PrefabName = held.PrefabName,
|
||||
ModificationGuid = held.ModificationGuid,
|
||||
FriendlyName = held.FriendlyName
|
||||
});
|
||||
db.Unlocks.Add(unlocks);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
await GiveSkinAsync(db, coming);
|
||||
Assert.IsType<OkObjectResult>(await ClaimAsync(db, Recipient));
|
||||
|
||||
// Both skins, not the first one twice: asking "do they have this?" by prefab alone
|
||||
// answered yes and threw the second skin away, with the box consumed and nothing said.
|
||||
var equipment = (await db.Unlocks.FirstAsync(x => x.AccountId == Recipient)).GetEquipment()!;
|
||||
var ownedSkins = equipment.Where(item => item.PrefabName == "[Basketball]").ToList();
|
||||
|
||||
Assert.Equal(2, ownedSkins.Count);
|
||||
Assert.Contains(ownedSkins, item => item.ModificationGuid == held.ModificationGuid);
|
||||
Assert.Contains(ownedSkins, item => item.ModificationGuid == coming.ModificationGuid);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ABareToolIsStillJustTheTool()
|
||||
{
|
||||
var db = NewDb();
|
||||
|
||||
// A tool with no skin named is a tool with no skin. The route does not reach for a guid to
|
||||
// make the box look complete, because the first guid it found would be some other skin.
|
||||
await GiveAsync(db, $$"""
|
||||
{
|
||||
"player_id": "{{Recipient}}",
|
||||
"equip": "[Basketball]",
|
||||
"context": "Game_Drop",
|
||||
"rarity": "Common"
|
||||
}
|
||||
""");
|
||||
|
||||
var stored = Assert.Single(await StorefrontShared.GetPendingGiftsAsync(db, Recipient));
|
||||
Assert.Equal("[Basketball]", stored.EquipmentPrefabName);
|
||||
Assert.Equal("", stored.EquipmentModificationGuid);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user