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() .Where(type => type != StorefrontTypes.None) .Select(type => (long)type) .ToList(); Assert.Equal(44, storefronts.Count); Assert.Empty(storefronts.Where(id => !captured.Contains(id))); } }