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

155 lines
7.3 KiB
C#

using System.Text.Json;
using RadicadeServer.API.Storefront.V3;
using RadicadeServer.RecNet.Avatar;
using RadicadeServer.RecNet.Enums.Players;
using RadicadeServer.RecNet.Store;
using RadicadeServer.RecNet.Storefront;
using Xunit;
namespace RadicadeServer.Tests;
public class StorefrontRewardConfigTests
{
[Fact]
public void QuestStorefrontProjectionHidesOnlyUnsupportedAvatarOnlyItems()
{
var supported = Purchasable(10, "supported,");
var unsupported = Purchasable(11, "not-in-quest-build,");
var mixedReward = Purchasable(12, "also-unsupported,");
var originalGift = mixedReward.GiftDrops[0];
originalGift.ConsumableItemDesc = "consumable";
originalGift.Currency = 25;
var projected = StorefrontsV3.ProjectStoreItemsForClient(
[supported, unsupported, mixedReward],
((int)Platforms.Standalone).ToString(),
description => description == "supported,");
Assert.Equal(new[] { 10, 12 }, projected.Select(item => item.PurchasableItemId));
Assert.Equal(250, Assert.Single(projected[0].Prices).Price);
Assert.Equal(string.Empty, projected[1].GiftDrops[0].AvatarItemDesc);
Assert.Equal("consumable", projected[1].GiftDrops[0].ConsumableItemDesc);
Assert.Equal(25, projected[1].GiftDrops[0].Currency);
// The response is a projection only; purchase resolution still sees the original gifts.
Assert.Equal("also-unsupported,", originalGift.AvatarItemDesc);
Assert.Equal("not-in-quest-build,", unsupported.GiftDrops[0].AvatarItemDesc);
}
[Fact]
public void QuestStorefrontProjectionLeavesOtherPlatformsAndResolvableGiftDropsAlone()
{
var item = Purchasable(20, "supported,");
item.GiftDrops.Add(new Gift { GiftDropId = 201, AvatarItemDesc = "unsupported," });
var nonStandalone = StorefrontsV3.ProjectStoreItemsForClient(
[item],
((int)Platforms.Oculus).ToString(),
_ => false);
var standalone = StorefrontsV3.ProjectStoreItemsForClient(
[item],
((int)Platforms.Standalone).ToString(),
description => description == "supported,");
Assert.Same(item, Assert.Single(nonStandalone));
Assert.Single(Assert.Single(standalone).GiftDrops);
Assert.Equal("supported,", standalone[0].GiftDrops[0].AvatarItemDesc);
}
[Fact]
public void QuestStorefrontProjectionFiltersCapturedCatalogWithoutChangingPurchaseLookup()
{
using var lookup = JsonDocument.Parse(File.ReadAllText(
Path.Combine(AppContext.BaseDirectory, "Fixtures", "avatar-item-lookup.json")));
var resolvable = lookup.RootElement.GetProperty("descriptions")
.EnumerateArray()
.Select(value => AvatarItemDescription.Normalize(value.GetString()))
.ToHashSet(StringComparer.OrdinalIgnoreCase);
var canonicalItems = StorefrontCatalog.GetItems((long)StorefrontTypes.Watch);
var supportedItem = canonicalItems.First(item =>
resolvable.Contains(AvatarItemDescription.Normalize(item.GiftDrop.AvatarItemDesc)));
var unsupportedItem = canonicalItems.First(item =>
!string.IsNullOrWhiteSpace(item.GiftDrop.AvatarItemDesc)
&& !resolvable.Contains(AvatarItemDescription.Normalize(item.GiftDrop.AvatarItemDesc)));
var projected = StorefrontsV3.ProjectStoreItemsForClient(
canonicalItems,
((int)Platforms.Standalone).ToString(),
description => resolvable.Contains(AvatarItemDescription.Normalize(description)));
Assert.Contains(projected, item => item.PurchasableItemId == supportedItem.PurchasableItemId);
Assert.DoesNotContain(projected, item => item.PurchasableItemId == unsupportedItem.PurchasableItemId);
Assert.All(projected.SelectMany(item => item.GiftDrops)
.Where(gift => !string.IsNullOrWhiteSpace(gift.AvatarItemDesc)),
gift => Assert.Contains(
AvatarItemDescription.Normalize(gift.AvatarItemDesc), resolvable));
var canonicalUnsupported = StorefrontCatalog.FindItem(
(long)StorefrontTypes.Watch, unsupportedItem.PurchasableItemId);
Assert.NotNull(canonicalUnsupported);
Assert.Equal(unsupportedItem.GiftDrop.AvatarItemDesc, canonicalUnsupported.GiftDrop.AvatarItemDesc);
}
private static PurchasableGiftDrop Purchasable(int itemId, string avatarItemDesc) => new()
{
PurchasableItemId = itemId,
Prices = [new PurchasablePrice { Price = 250 }],
GiftDrops = [new Gift { GiftDropId = itemId + 100, AvatarItemDesc = avatarItemDesc }]
};
[Fact]
public void PartialMultiplierSerializesAsNumber()
{
var json = JsonSerializer.SerializeToElement(new StorefrontBalanceAddConfigDTO());
Assert.Equal(JsonValueKind.Number, json.GetProperty("MaxPartialMultiplier").ValueKind);
Assert.Equal(1f, json.GetProperty("MaxPartialMultiplier").GetSingle());
Assert.Equal(JsonValueKind.False, json.GetProperty("IgnorePartialMultiplier").ValueKind);
Assert.Equal(JsonValueKind.False, json.GetProperty("BalanceInGiftBox").ValueKind);
}
// The client asks for a storefront by number and the MVC binder drops any id that is not a
// member of StorefrontTypes down to None, so those ids have to stay defined and keep the
// exact numbers the 20230831 build sends (RecNet.Runtime's StorefrontTypes).
[Theory]
[InlineData(StorefrontTypes.LaserTag, 1)]
[InlineData(StorefrontTypes.Watch, 3)]
[InlineData(StorefrontTypes.Quest_GoldenTrophy, 102)]
[InlineData(StorefrontTypes.Quest_CrimsonCauldron, 103)]
[InlineData(StorefrontTypes.Paintball_SunsetDriveIn, 406)]
[InlineData(StorefrontTypes.StuntRunner, 600)]
[InlineData(StorefrontTypes.DormMirror, 700)]
[InlineData(StorefrontTypes.InventionStore, 800)]
[InlineData(StorefrontTypes.Player_Profile, 1000)]
[InlineData(StorefrontTypes.PopUpShop_1, 1700)]
[InlineData(StorefrontTypes.CustomAvatarItems, 1800)]
[InlineData(StorefrontTypes.ProgressionEventPurchasableXpBoosts, 1900)]
[InlineData(StorefrontTypes.PopUpShop_ProgressionEvent_1, 2000)]
[InlineData(StorefrontTypes.PopUpShop_ProgressionEvent_5, 2004)]
public void GameStorefrontIdStaysBound(StorefrontTypes type, int clientId)
{
Assert.Equal(clientId, (int)type);
Assert.True(
Enum.IsDefined(typeof(StorefrontTypes), clientId),
$"storefront {clientId} must stay a defined StorefrontTypes member or the route binder serves it as None");
}
// Every storefront the client knows carries a capture (RadicadeServer-Config/storefronts/sf{id}.json,
// embedded into the server at build), so the catalog covers the whole game and every storefront is
// editable there. A storefront with no items of its own still serves the shared storefront 0 list.
[Fact]
public void EveryGameStorefrontHasACapture()
{
var captured = StorefrontCatalog.ReadCaptures()
.Select(capture => capture.StorefrontId)
.ToHashSet();
var storefronts = Enum.GetValues<StorefrontTypes>()
.Where(type => type != StorefrontTypes.None)
.Select(type => (long)type)
.ToList();
Assert.Equal(44, storefronts.Count);
Assert.Empty(storefronts.Where(id => !captured.Contains(id)));
}
}