mirror of
https://github.com/JogmansonReal/Radicade.git
synced 2026-10-25 23:18:56 -07:00
314 lines
13 KiB
C#
314 lines
13 KiB
C#
using System.Security.Claims;
|
|
using System.Text;
|
|
using System.Text.Json;
|
|
using System.Text.Json.Nodes;
|
|
using Microsoft.AspNetCore.Http;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using Microsoft.Data.Sqlite;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using RadicadeServer.API.Compatibility;
|
|
using RadicadeServer.API.Storefront;
|
|
using RadicadeServer.DB;
|
|
using RadicadeServer.RecNet.Auth;
|
|
using RadicadeServer.RecNet.Avatar;
|
|
using RadicadeServer.RecNet.Config;
|
|
using RadicadeServer.RecNet.Enums.Store;
|
|
using RadicadeServer.Utils;
|
|
using Xunit;
|
|
|
|
namespace RadicadeServer.Tests;
|
|
|
|
/// <summary>
|
|
/// The Friendotron: the free gift-for-friends button in the Rec Center
|
|
/// (POST /econ/api/freegifts/v1/sendmultiple).
|
|
///
|
|
/// The route used to answer a success flag and nothing else - no box, no item - so a send looked
|
|
/// like it worked and no friend ever received anything. A send now hands every recipient a gift
|
|
/// box holding a random one to three star consumable, labelled with the sender's message or
|
|
/// "A gift for you <3" when they left it empty, and a player may only send once a day.
|
|
/// </summary>
|
|
public class FriendotronTests
|
|
{
|
|
private const long SenderId = 61;
|
|
private const long FirstFriendId = 62;
|
|
private const long SecondFriendId = 63;
|
|
|
|
/// <summary>
|
|
/// The boxes are rolled from the catalogue the server ships, so these tests read the same
|
|
/// files it does - once per run rather than once per test, because LoadFiles rewrites the
|
|
/// process-wide catalogue every other catalog test in this suite reads.
|
|
/// </summary>
|
|
private static readonly object CatalogLock = new();
|
|
private static bool catalogLoaded;
|
|
|
|
private static void LoadShippedCatalog()
|
|
{
|
|
lock (CatalogLock)
|
|
{
|
|
if (catalogLoaded) return;
|
|
DBConfig.LoadFiles();
|
|
catalogLoaded = true;
|
|
}
|
|
}
|
|
|
|
private static async Task<(SqliteConnection Connection, RecDBContext Db)> OpenDbAsync(params long[] accountIds)
|
|
{
|
|
var connection = new SqliteConnection("DataSource=:memory:");
|
|
await connection.OpenAsync();
|
|
var options = TestDbOptions.Create().UseSqlite(connection).Options;
|
|
var db = new RecDBContext(options);
|
|
await db.Database.EnsureCreatedAsync();
|
|
|
|
foreach (var accountId in accountIds)
|
|
db.Players.Add(new PlayerData { AccountId = accountId, Hashword = "x" });
|
|
await db.SaveChangesAsync();
|
|
|
|
return (connection, db);
|
|
}
|
|
|
|
private static void SetUser(ControllerBase 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) }
|
|
};
|
|
}
|
|
|
|
private static void SetJsonBody(ControllerBase controller, string body)
|
|
{
|
|
var request = controller.HttpContext.Request;
|
|
request.ContentType = "application/json";
|
|
request.Body = new MemoryStream(Encoding.UTF8.GetBytes(body));
|
|
request.ContentLength = request.Body.Length;
|
|
}
|
|
|
|
/// <summary>The client's request DTO: the recipients, the lid message, and the gift context.</summary>
|
|
private static string SendBody(IEnumerable<long> recipients, string? message) =>
|
|
new JsonObject
|
|
{
|
|
["ToPlayerIds"] = new JsonArray(recipients.Select(id => (JsonNode)JsonValue.Create(id)!).ToArray()),
|
|
["Message"] = message ?? string.Empty,
|
|
["GiftContext"] = (int)GiftContext.Friendotron_Gift
|
|
}.ToJsonString();
|
|
|
|
private static async Task<(bool Success, string Error)> SendAsync(
|
|
CompatibilityAPI api, long senderId, IEnumerable<long> recipients, string? message = null)
|
|
{
|
|
SetUser(api, senderId);
|
|
SetJsonBody(api, SendBody(recipients, message));
|
|
|
|
var result = Assert.IsType<OkObjectResult>(await api.SendFreeGifts());
|
|
using var document = JsonDocument.Parse(JsonSerializer.Serialize(result.Value));
|
|
return (
|
|
document.RootElement.GetProperty("ALOCDGMHFKA").GetBoolean(),
|
|
document.RootElement.GetProperty("FBILGKFKFJE").GetString() ?? string.Empty);
|
|
}
|
|
|
|
private static HashSet<string> ShippedConsumableTokens() =>
|
|
ConsumableCatalog.ParseItems(DBConfig.DefaultConsumableItems)
|
|
.Select(item => ConsumableCatalog.ToClientDesc(item.ConsumableItemDesc))
|
|
.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
|
|
|
[Fact]
|
|
public async Task ASendPutsABoxInEveryRecipientsHands()
|
|
{
|
|
LoadShippedCatalog();
|
|
var (connection, db) = await OpenDbAsync(SenderId, FirstFriendId, SecondFriendId);
|
|
await using var disposeConnection = connection;
|
|
await using var disposeDb = db;
|
|
|
|
var tokens = ShippedConsumableTokens();
|
|
Assert.NotEmpty(tokens);
|
|
|
|
var (success, error) = await SendAsync(new CompatibilityAPI(db), SenderId, new[] { FirstFriendId, SecondFriendId });
|
|
|
|
Assert.True(success, error);
|
|
Assert.Equal(string.Empty, error);
|
|
|
|
foreach (var recipient in new[] { FirstFriendId, SecondFriendId })
|
|
{
|
|
var box = Assert.Single(await StorefrontShared.GetPendingGiftsAsync(db, recipient));
|
|
Assert.Equal(SenderId, box.FromPlayerId);
|
|
Assert.Equal(GiftContext.Friendotron_Gift, box.Context);
|
|
// One to three stars on the box, and a real catalogue consumable inside it.
|
|
Assert.Contains(box.Rarity, FriendotronService.StarTiers);
|
|
Assert.Contains(box.ConsumableItemDesc, tokens);
|
|
Assert.Equal("A gift for you <3", box.Tooltip);
|
|
}
|
|
|
|
// Nobody else was sent anything.
|
|
Assert.Empty(await StorefrontShared.GetPendingGiftsAsync(db, SenderId));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task OpeningTheBoxPutsTheConsumableInTheRecipientsInventory()
|
|
{
|
|
LoadShippedCatalog();
|
|
var (connection, db) = await OpenDbAsync(SenderId, FirstFriendId);
|
|
await using var disposeConnection = connection;
|
|
await using var disposeDb = db;
|
|
|
|
Assert.True((await SendAsync(new CompatibilityAPI(db), SenderId, new[] { FirstFriendId })).Success);
|
|
var box = Assert.Single(await StorefrontShared.GetPendingGiftsAsync(db, FirstFriendId));
|
|
|
|
// The claim route's own sequence: grant the gift, then retire the pending box.
|
|
var strategy = db.Database.CreateExecutionStrategy();
|
|
await strategy.ExecuteAsync(async () =>
|
|
{
|
|
await using var tx = await db.Database.BeginTransactionAsync();
|
|
await StorefrontShared.GrantGiftAsync(db, FirstFriendId, box);
|
|
await StorefrontShared.RemovePendingGiftAsync(db, FirstFriendId, box.GiftDropId);
|
|
await tx.CommitAsync();
|
|
});
|
|
|
|
var unlocks = await db.Unlocks.AsNoTracking().FirstAsync(x => x.AccountId == FirstFriendId);
|
|
var owned = unlocks.GetConsumables() ?? new List<Consumable>();
|
|
Assert.Contains(owned, item => item.ConsumableItemDesc == box.ConsumableItemDesc);
|
|
Assert.Empty(await StorefrontShared.GetPendingGiftsAsync(db, FirstFriendId));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task TheSendersMessageRidesAlongOnTheBox()
|
|
{
|
|
LoadShippedCatalog();
|
|
var (connection, db) = await OpenDbAsync(SenderId, FirstFriendId);
|
|
await using var disposeConnection = connection;
|
|
await using var disposeDb = db;
|
|
|
|
var (success, error) = await SendAsync(new CompatibilityAPI(db), SenderId, new[] { FirstFriendId }, "Monkey-Jelly");
|
|
|
|
Assert.True(success, error);
|
|
var box = Assert.Single(await StorefrontShared.GetPendingGiftsAsync(db, FirstFriendId));
|
|
Assert.Equal("Monkey-Jelly", box.Tooltip);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ASecondSendWithinADayIsRefused()
|
|
{
|
|
LoadShippedCatalog();
|
|
var (connection, db) = await OpenDbAsync(SenderId, FirstFriendId);
|
|
await using var disposeConnection = connection;
|
|
await using var disposeDb = db;
|
|
|
|
var api = new CompatibilityAPI(db);
|
|
Assert.True((await SendAsync(api, SenderId, new[] { FirstFriendId })).Success);
|
|
|
|
var (success, error) = await SendAsync(api, SenderId, new[] { FirstFriendId });
|
|
|
|
Assert.False(success);
|
|
Assert.Contains("another friend box", error);
|
|
// The refusal did not slip a second box in behind it.
|
|
Assert.Single(await StorefrontShared.GetPendingGiftsAsync(db, FirstFriendId));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ASendIsAllowedAgainOnceTheDayIsUp()
|
|
{
|
|
LoadShippedCatalog();
|
|
var (connection, db) = await OpenDbAsync(SenderId, FirstFriendId);
|
|
await using var disposeConnection = connection;
|
|
await using var disposeDb = db;
|
|
|
|
var api = new CompatibilityAPI(db);
|
|
Assert.True((await SendAsync(api, SenderId, new[] { FirstFriendId })).Success);
|
|
|
|
// Stand the stamp back a day and a bit: the cooldown is 24 hours, not a session or a lock.
|
|
var key = FriendotronService.CooldownKey(SenderId);
|
|
var stamp = await db.ServerConfig.FirstAsync(x => x.Key == key);
|
|
stamp.Value = DateTime.UtcNow.AddHours(-25).ToString("O");
|
|
await db.SaveChangesAsync();
|
|
|
|
Assert.True((await SendAsync(api, SenderId, new[] { FirstFriendId })).Success);
|
|
Assert.Equal(2, (await StorefrontShared.GetPendingGiftsAsync(db, FirstFriendId)).Count);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SelfGiftingFollowsTheConfigFlag()
|
|
{
|
|
LoadShippedCatalog();
|
|
var (connection, db) = await OpenDbAsync(SenderId);
|
|
await using var disposeConnection = connection;
|
|
await using var disposeDb = db;
|
|
|
|
var api = new CompatibilityAPI(db);
|
|
|
|
// The seeded config allows self gifting, so a box to yourself is a box.
|
|
Assert.True((await SendAsync(api, SenderId, new[] { SenderId })).Success);
|
|
Assert.Single(await StorefrontShared.GetPendingGiftsAsync(db, SenderId));
|
|
|
|
db.GameConfig.Add(new ConfigVal("Friendotron_AllowSelfGifting", "false"));
|
|
var stamp = await db.ServerConfig.FirstAsync(x => x.Key == FriendotronService.CooldownKey(SenderId));
|
|
stamp.Value = DateTime.UtcNow.AddHours(-25).ToString("O");
|
|
await db.SaveChangesAsync();
|
|
|
|
var (success, error) = await SendAsync(api, SenderId, new[] { SenderId });
|
|
|
|
Assert.False(success);
|
|
Assert.Contains("yourself", error);
|
|
Assert.Single(await StorefrontShared.GetPendingGiftsAsync(db, SenderId));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task AnUnknownRecipientIsRefusedAndNothingIsSent()
|
|
{
|
|
LoadShippedCatalog();
|
|
var (connection, db) = await OpenDbAsync(SenderId, FirstFriendId);
|
|
await using var disposeConnection = connection;
|
|
await using var disposeDb = db;
|
|
|
|
var (success, error) = await SendAsync(new CompatibilityAPI(db), SenderId, new[] { FirstFriendId, 999 });
|
|
|
|
Assert.False(success);
|
|
Assert.Contains("999", error);
|
|
// All or nothing: the friend who does exist gets no box from a send that was refused.
|
|
Assert.Empty(await StorefrontShared.GetPendingGiftsAsync(db, FirstFriendId));
|
|
Assert.Empty(db.ServerConfig);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ASendWithNoRecipientsIsRefused()
|
|
{
|
|
LoadShippedCatalog();
|
|
var (connection, db) = await OpenDbAsync(SenderId);
|
|
await using var disposeConnection = connection;
|
|
await using var disposeDb = db;
|
|
|
|
var (success, error) = await SendAsync(new CompatibilityAPI(db), SenderId, Array.Empty<long>());
|
|
|
|
Assert.False(success);
|
|
Assert.Contains("recipients", error);
|
|
}
|
|
|
|
[Fact]
|
|
public void ARolledBoxIsAlwaysAOneTwoOrThreeStarConsumable()
|
|
{
|
|
LoadShippedCatalog();
|
|
var tokens = ShippedConsumableTokens();
|
|
var random = new Random(20261012);
|
|
|
|
var tiers = new HashSet<Rarity>();
|
|
for (var roll = 0; roll < 200; roll++)
|
|
{
|
|
var item = FriendotronService.RollConsumable(DBConfig.DefaultConsumableItems, out var starTier, random);
|
|
|
|
Assert.NotNull(item);
|
|
Assert.Contains(starTier, FriendotronService.StarTiers);
|
|
Assert.Contains(ConsumableCatalog.ToClientDesc(item!.ConsumableItemDesc), tokens);
|
|
tiers.Add(starTier);
|
|
}
|
|
|
|
// All three star levels are reachable, not just the tier every shipped consumable sits in.
|
|
Assert.Equal(FriendotronService.StarTiers.OrderBy(x => x).ToArray(), tiers.OrderBy(x => x).ToArray());
|
|
}
|
|
|
|
[Fact]
|
|
public void TheDefaultLidMessageIsUsedOnlyWhenTheSenderTypedNothing()
|
|
{
|
|
Assert.Equal("A gift for you <3", FriendotronService.MessageOr(null));
|
|
Assert.Equal("A gift for you <3", FriendotronService.MessageOr(" "));
|
|
Assert.Equal("Monkey-Jelly", FriendotronService.MessageOr(" Monkey-Jelly "));
|
|
}
|
|
}
|