using RadicadeServer.RecNet.Avatar; using Xunit; namespace RadicadeServer.Tests; public class AvatarCatalogParsingTests { [Fact] public void V4UsesExactClientKeysAndNonNullStrings() { var item = new AvatarItem { AvatarItemDesc = "item,,,", FriendlyName = "Test", Rarity = 20, ThumbnailImage = "abc123.png" }; var json = System.Text.Json.JsonSerializer.SerializeToElement(new AvatarItemV4(item)); // PascalCase, matching a real capture of this route. The client has no case-insensitive // fallback, so a lowercased name leaves AvatarItemDesc null and the item unresolvable. Assert.Equal( new[] { "AvatarItemDesc", "AvatarItemId", "AvatarItemType", "CreatedAt", "FriendlyName", "IsBaseAvatarItem", "PlatformMask", "Rarity", "TagList", "ThumbnailImage", "Tooltip" }, json.EnumerateObject().Select(property => property.Name).OrderBy(name => name)); Assert.Equal("item,,,", json.GetProperty("AvatarItemDesc").GetString()); Assert.Equal("", json.GetProperty("TagList").GetString()); Assert.Equal(20, json.GetProperty("Rarity").GetInt32()); Assert.Equal("abc123.png", json.GetProperty("ThumbnailImage").GetString()); // 0 here would read as "usable on no platform" and the wardrobe would hide the item. Assert.Equal(-1, json.GetProperty("PlatformMask").GetInt32()); var legacy = System.Text.Json.JsonSerializer.SerializeToElement(item); Assert.True(legacy.TryGetProperty("AvatarItemDesc", out _)); } [Fact] public void RepositoryCatalogLoadsSupportedItemsWithoutLosingMetadata() { var json = File.ReadAllText(Path.Combine(AppContext.BaseDirectory, "Fixtures", "avatar.json")); var expected = AvatarUnlockCatalog.KeepSupportedItems( System.Text.Json.JsonSerializer.Deserialize>(json)); var actual = AvatarUnlockCatalog.ParseCatalog(json); Assert.NotEmpty(actual); Assert.Equal(expected.Count, actual.Count); Assert.Equal(expected.Select(x => x.AvatarItemDesc), actual.Select(x => x.AvatarItemDesc)); Assert.Equal(expected.Select(x => x.FriendlyName), actual.Select(x => x.FriendlyName)); Assert.Equal(expected.Select(x => x.Rarity), actual.Select(x => x.Rarity)); } [Fact] public void CatalogCanBeEnrichedWithItemIdsAndThumbnails() { var items = AvatarUnlockCatalog.ParseCatalog(File.ReadAllText(Path.Combine(AppContext.BaseDirectory, "Fixtures", "avatar.json"))); var metadata = File.ReadAllText(Path.Combine(AppContext.BaseDirectory, "Fixtures", "merged-avatar.json")); var enriched = AvatarUnlockCatalog.EnrichWithMetadata(items, metadata); var shirt = Assert.Single(enriched.Where(x => x.AvatarItemDesc == "8aa79563-ace1-4ba7-ad0c-f3210a78142f,")); Assert.Equal(1018, shirt.AvatarItemId); Assert.False(string.IsNullOrWhiteSpace(shirt.ThumbnailImage)); } [Fact] public void BadEntriesDoNotDiscardValidItems() { var items = AvatarUnlockCatalog.ParseCatalog(""" [ null, 42, {"AvatarItemDesc":"bad,,,","Rarity":"not-a-number"}, {"AvatarItemDesc":"overflow,,,","AvatarItemId":9223372036854775807}, {"FriendlyName":"Missing description"}, {"AvatarItemDesc":"valid,,,","FriendlyName":"Valid item","Rarity":20} ] """); var item = Assert.Single(items); Assert.Equal("valid,", item.AvatarItemDesc); Assert.Equal(-1, item.PlatformMask); Assert.Equal("Valid item", item.FriendlyName); } [Fact] public void AcceptsCommentsTrailingCommasAndQuotedNumbers() { var items = AvatarUnlockCatalog.ParseCatalog(""" [ // Hand-edited catalog {"avatarItemDesc":"valid,,,","rarity":"20","platformMask":"-1",}, ] """); Assert.Equal(20, Assert.Single(items).Rarity); } [Theory] [InlineData(null)] [InlineData("")] [InlineData("null")] [InlineData("{}")] [InlineData("[broken")] public void InvalidCatalogReturnsEmptyForFallback(string? json) { Assert.Empty(AvatarUnlockCatalog.ParseCatalog(json)); } }