// ---------------------------------------------------------------------------------------------- // _ _ _ ____ _ _____ // / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___ // / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \ // / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | | // /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_| // ---------------------------------------------------------------------------------------------- // | // Copyright 2015-2024 Ɓukasz "JustArchi" Domeradzki // Contact: JustArchi@JustArchi.net // | // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // | // http://www.apache.org/licenses/LICENSE-2.0 // | // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection; using System.Text.Json; using System.Text.Json.Nodes; using ArchiSteamFarm.Core; using ArchiSteamFarm.Helpers.Json; using ArchiSteamFarm.Steam.Data; using ArchiSteamFarm.Steam.Storage; using ArchiSteamFarm.Storage; using Microsoft.VisualStudio.TestTools.UnitTesting; using static ArchiSteamFarm.Steam.Bot; namespace ArchiSteamFarm.Tests; #pragma warning disable CA1812 // False positive, the class is used during MSTest [TestClass] internal sealed class Bot { internal static Steam.Bot GenerateBot(string botName = "Test") { ArgumentException.ThrowIfNullOrEmpty(botName); ConstructorInfo? constructor = typeof(Steam.Bot).GetConstructor(BindingFlags.Instance | BindingFlags.NonPublic, [typeof(string), typeof(BotConfig), typeof(BotDatabase)]); if (constructor == null) { throw new InvalidOperationException(nameof(constructor)); } JsonElement emptyObject = new JsonObject().ToJsonElement(); BotConfig? botConfig = emptyObject.ToJsonObject(); if (botConfig == null) { throw new InvalidOperationException(nameof(botConfig)); } BotDatabase? botDatabase = emptyObject.ToJsonObject(); if (botDatabase == null) { throw new InvalidOperationException(nameof(botDatabase)); } ASF.GlobalDatabase ??= emptyObject.ToJsonObject(); if (constructor.Invoke([botName, botConfig, botDatabase]) is not Steam.Bot result) { throw new InvalidOperationException(nameof(result)); } return result; } [TestMethod] internal void MaxItemsBarelyEnoughForOneSet() { const uint relevantAppID = 42; Dictionary itemsPerSet = new() { { relevantAppID, MinCardsPerBadge }, { 43, MinCardsPerBadge + 1 } }; HashSet items = []; foreach ((uint appID, byte cards) in itemsPerSet) { for (byte i = 1; i <= cards; i++) { items.Add(CreateCard(i, realAppID: appID)); } } HashSet itemsToSend = GetItemsForFullBadge(items, itemsPerSet, MinCardsPerBadge); Dictionary<(uint RealAppID, ulong ContextID, ulong ClassID), uint> expectedResult = items.Where(static item => item.RealAppID == relevantAppID).GroupBy(static item => (item.RealAppID, item.ContextID, item.ClassID)).ToDictionary(static group => group.Key, static group => (uint) group.Sum(static item => item.Amount)); AssertResultMatchesExpectation(expectedResult, itemsToSend); } [TestMethod] internal void MaxItemsTooSmall() { const uint appID = 42; HashSet items = [ CreateCard(1, realAppID: appID), CreateCard(2, realAppID: appID) ]; Assert.ThrowsException(() => GetItemsForFullBadge(items, 2, appID, MinCardsPerBadge - 1)); } [TestMethod] internal void MoreCardsThanNeeded() { const uint appID = 42; HashSet items = [ CreateCard(1, realAppID: appID), CreateCard(1, realAppID: appID), CreateCard(2, realAppID: appID), CreateCard(3, realAppID: appID) ]; HashSet itemsToSend = GetItemsForFullBadge(items, 3, appID); Dictionary<(uint RealAppID, ulong ContextID, ulong ClassID), uint> expectedResult = new() { { (appID, Asset.SteamCommunityContextID, 1), 1 }, { (appID, Asset.SteamCommunityContextID, 2), 1 }, { (appID, Asset.SteamCommunityContextID, 3), 1 } }; AssertResultMatchesExpectation(expectedResult, itemsToSend); } [TestMethod] internal void MultipleSets() { const uint appID = 42; HashSet items = [ CreateCard(1, realAppID: appID), CreateCard(1, realAppID: appID), CreateCard(2, realAppID: appID), CreateCard(2, realAppID: appID) ]; HashSet itemsToSend = GetItemsForFullBadge(items, 2, appID); Dictionary<(uint RealAppID, ulong ContextID, ulong ClassID), uint> expectedResult = new() { { (appID, Asset.SteamCommunityContextID, 1), 2 }, { (appID, Asset.SteamCommunityContextID, 2), 2 } }; AssertResultMatchesExpectation(expectedResult, itemsToSend); } [TestMethod] internal void MultipleSetsDifferentAmount() { const uint appID = 42; HashSet items = [ CreateCard(1, amount: 2, realAppID: appID), CreateCard(2, realAppID: appID), CreateCard(2, realAppID: appID) ]; HashSet itemsToSend = GetItemsForFullBadge(items, 2, appID); Dictionary<(uint RealAppID, ulong ContextID, ulong ClassID), uint> expectedResult = new() { { (appID, Asset.SteamCommunityContextID, 1), 2 }, { (appID, Asset.SteamCommunityContextID, 2), 2 } }; AssertResultMatchesExpectation(expectedResult, itemsToSend); } [TestMethod] internal void MutliRarityAndType() { const uint appID = 42; HashSet items = [ CreateCard(1, realAppID: appID, type: EAssetType.TradingCard, rarity: EAssetRarity.Common), CreateCard(2, realAppID: appID, type: EAssetType.TradingCard, rarity: EAssetRarity.Common), CreateCard(1, realAppID: appID, type: EAssetType.FoilTradingCard, rarity: EAssetRarity.Uncommon), CreateCard(2, realAppID: appID, type: EAssetType.FoilTradingCard, rarity: EAssetRarity.Uncommon), CreateCard(1, realAppID: appID, type: EAssetType.FoilTradingCard, rarity: EAssetRarity.Rare), CreateCard(2, realAppID: appID, type: EAssetType.FoilTradingCard, rarity: EAssetRarity.Rare), // for better readability and easier verification when thinking about this test the items that shall be selected for sending are the ones below this comment CreateCard(1, realAppID: appID, type: EAssetType.TradingCard, rarity: EAssetRarity.Uncommon), CreateCard(2, realAppID: appID, type: EAssetType.TradingCard, rarity: EAssetRarity.Uncommon), CreateCard(3, realAppID: appID, type: EAssetType.TradingCard, rarity: EAssetRarity.Uncommon), CreateCard(1, realAppID: appID, type: EAssetType.FoilTradingCard, rarity: EAssetRarity.Common), CreateCard(3, realAppID: appID, type: EAssetType.FoilTradingCard, rarity: EAssetRarity.Common), CreateCard(7, realAppID: appID, type: EAssetType.FoilTradingCard, rarity: EAssetRarity.Common), CreateCard(2, realAppID: appID, type: EAssetType.Unknown, rarity: EAssetRarity.Rare), CreateCard(3, realAppID: appID, type: EAssetType.Unknown, rarity: EAssetRarity.Rare), CreateCard(4, realAppID: appID, type: EAssetType.Unknown, rarity: EAssetRarity.Rare) ]; HashSet itemsToSend = GetItemsForFullBadge(items, 3, appID); Dictionary<(uint RealAppID, ulong ContextID, ulong ClassID), uint> expectedResult = new() { { (appID, Asset.SteamCommunityContextID, 1), 2 }, { (appID, Asset.SteamCommunityContextID, 2), 2 }, { (appID, Asset.SteamCommunityContextID, 3), 3 }, { (appID, Asset.SteamCommunityContextID, 4), 1 }, { (appID, Asset.SteamCommunityContextID, 7), 1 } }; AssertResultMatchesExpectation(expectedResult, itemsToSend); } [TestMethod] internal void NotAllCardsPresent() { const uint appID = 42; HashSet items = [ CreateCard(1, realAppID: appID), CreateCard(2, realAppID: appID) ]; HashSet itemsToSend = GetItemsForFullBadge(items, 3, appID); Dictionary<(uint RealAppID, ulong ContextID, ulong ClassID), uint> expectedResult = new(0); AssertResultMatchesExpectation(expectedResult, itemsToSend); } [TestMethod] internal void OneSet() { const uint appID = 42; HashSet items = [ CreateCard(1, realAppID: appID), CreateCard(2, realAppID: appID) ]; HashSet itemsToSend = GetItemsForFullBadge(items, 2, appID); Dictionary<(uint RealAppID, ulong ContextID, ulong ClassID), uint> expectedResult = new() { { (appID, Asset.SteamCommunityContextID, 1), 1 }, { (appID, Asset.SteamCommunityContextID, 2), 1 } }; AssertResultMatchesExpectation(expectedResult, itemsToSend); } [TestMethod] internal void OtherAppIDFullSets() { const uint appID0 = 42; const uint appID1 = 43; HashSet items = [ CreateCard(1, realAppID: appID0), CreateCard(1, realAppID: appID1) ]; HashSet itemsToSend = GetItemsForFullBadge( items, new Dictionary { { appID0, 1 }, { appID1, 1 } } ); Dictionary<(uint RealAppID, ulong ContextID, ulong ClassID), uint> expectedResult = new() { { (appID0, Asset.SteamCommunityContextID, 1), 1 }, { (appID1, Asset.SteamCommunityContextID, 1), 1 } }; AssertResultMatchesExpectation(expectedResult, itemsToSend); } [TestMethod] internal void OtherAppIDNoSets() { const uint appID0 = 42; const uint appID1 = 43; HashSet items = [ CreateCard(1, realAppID: appID0), CreateCard(1, realAppID: appID1) ]; HashSet itemsToSend = GetItemsForFullBadge( items, new Dictionary { { appID0, 2 }, { appID1, 2 } } ); Dictionary<(uint RealAppID, ulong ContextID, ulong ClassID), uint> expectedResult = new(0); AssertResultMatchesExpectation(expectedResult, itemsToSend); } [TestMethod] internal void OtherAppIDOneSet() { const uint appID0 = 42; const uint appID1 = 43; const uint appID2 = 44; HashSet items = [ CreateCard(1, realAppID: appID0), CreateCard(2, realAppID: appID0), CreateCard(1, realAppID: appID1), CreateCard(2, realAppID: appID1), CreateCard(3, realAppID: appID1) ]; HashSet itemsToSend = GetItemsForFullBadge( items, new Dictionary { { appID0, 3 }, { appID1, 3 }, { appID2, 3 } } ); Dictionary<(uint RealAppID, ulong ContextID, ulong ClassID), uint> expectedResult = new() { { (appID1, Asset.SteamCommunityContextID, 1), 1 }, { (appID1, Asset.SteamCommunityContextID, 2), 1 }, { (appID1, Asset.SteamCommunityContextID, 3), 1 } }; AssertResultMatchesExpectation(expectedResult, itemsToSend); } [TestMethod] internal void OtherRarityFullSets() { const uint appID = 42; HashSet items = [ CreateCard(1, realAppID: appID, rarity: EAssetRarity.Common), CreateCard(1, realAppID: appID, rarity: EAssetRarity.Rare) ]; HashSet itemsToSend = GetItemsForFullBadge(items, 1, appID); Dictionary<(uint RealAppID, ulong ContextID, ulong ClassID), uint> expectedResult = new() { { (appID, Asset.SteamCommunityContextID, 1), 2 } }; AssertResultMatchesExpectation(expectedResult, itemsToSend); } [TestMethod] internal void OtherRarityNoSets() { const uint appID = 42; HashSet items = [ CreateCard(1, realAppID: appID, rarity: EAssetRarity.Common), CreateCard(1, realAppID: appID, rarity: EAssetRarity.Rare) ]; HashSet itemsToSend = GetItemsForFullBadge(items, 2, appID); Dictionary<(uint RealAppID, ulong ContextID, ulong ClassID), uint> expectedResult = new(0); AssertResultMatchesExpectation(expectedResult, itemsToSend); } [TestMethod] internal void OtherRarityOneSet() { const uint appID = 42; HashSet items = [ CreateCard(1, realAppID: appID, rarity: EAssetRarity.Common), CreateCard(2, realAppID: appID, rarity: EAssetRarity.Common), CreateCard(1, realAppID: appID, rarity: EAssetRarity.Uncommon), CreateCard(2, realAppID: appID, rarity: EAssetRarity.Uncommon), CreateCard(3, realAppID: appID, rarity: EAssetRarity.Uncommon) ]; HashSet itemsToSend = GetItemsForFullBadge(items, 3, appID); Dictionary<(uint RealAppID, ulong ContextID, ulong ClassID), uint> expectedResult = new() { { (appID, Asset.SteamCommunityContextID, 1), 1 }, { (appID, Asset.SteamCommunityContextID, 2), 1 }, { (appID, Asset.SteamCommunityContextID, 3), 1 } }; AssertResultMatchesExpectation(expectedResult, itemsToSend); } [TestMethod] internal void OtherTypeFullSets() { const uint appID = 42; HashSet items = [ CreateCard(1, realAppID: appID, type: EAssetType.TradingCard), CreateCard(1, realAppID: appID, type: EAssetType.FoilTradingCard) ]; HashSet itemsToSend = GetItemsForFullBadge(items, 1, appID); Dictionary<(uint RealAppID, ulong ContextID, ulong ClassID), uint> expectedResult = new() { { (appID, Asset.SteamCommunityContextID, 1), 2 } }; AssertResultMatchesExpectation(expectedResult, itemsToSend); } [TestMethod] internal void OtherTypeNoSets() { const uint appID = 42; HashSet items = [ CreateCard(1, realAppID: appID, type: EAssetType.TradingCard), CreateCard(1, realAppID: appID, type: EAssetType.FoilTradingCard) ]; HashSet itemsToSend = GetItemsForFullBadge(items, 2, appID); Dictionary<(uint RealAppID, ulong ContextID, ulong ClassID), uint> expectedResult = new(0); AssertResultMatchesExpectation(expectedResult, itemsToSend); } [TestMethod] internal void OtherTypeOneSet() { const uint appID = 42; HashSet items = [ CreateCard(1, realAppID: appID, type: EAssetType.TradingCard), CreateCard(2, realAppID: appID, type: EAssetType.TradingCard), CreateCard(1, realAppID: appID, type: EAssetType.FoilTradingCard), CreateCard(2, realAppID: appID, type: EAssetType.FoilTradingCard), CreateCard(3, realAppID: appID, type: EAssetType.FoilTradingCard) ]; HashSet itemsToSend = GetItemsForFullBadge(items, 3, appID); Dictionary<(uint RealAppID, ulong ContextID, ulong ClassID), uint> expectedResult = new() { { (appID, Asset.SteamCommunityContextID, 1), 1 }, { (appID, Asset.SteamCommunityContextID, 2), 1 }, { (appID, Asset.SteamCommunityContextID, 3), 1 } }; AssertResultMatchesExpectation(expectedResult, itemsToSend); } [TestMethod] internal void TooHighAmount() { const uint appID0 = 42; HashSet items = [ CreateCard(1, amount: 2, realAppID: appID0), CreateCard(2, realAppID: appID0) ]; HashSet itemsToSend = GetItemsForFullBadge(items, 2, appID0); Dictionary<(uint RealAppID, ulong ContextID, ulong ClassID), uint> expectedResult = new() { { (appID0, Asset.SteamCommunityContextID, 1), 1 }, { (appID0, Asset.SteamCommunityContextID, 2), 1 } }; AssertResultMatchesExpectation(expectedResult, itemsToSend); } [TestMethod] internal void TooManyCardsForSingleTrade() { const uint appID = 42; HashSet items = []; for (byte i = 0; i < Steam.Exchange.Trading.MaxItemsPerTrade; i++) { items.Add(CreateCard(1, realAppID: appID)); items.Add(CreateCard(2, realAppID: appID)); } HashSet itemsToSend = GetItemsForFullBadge(items, 2, appID); Assert.IsTrue(itemsToSend.Count <= Steam.Exchange.Trading.MaxItemsPerTrade); } [TestMethod] internal void TooManyCardsForSingleTradeMultipleAppIDs() { const uint appID0 = 42; const uint appID1 = 43; HashSet items = []; for (byte i = 0; i < 100; i++) { items.Add(CreateCard(1, realAppID: appID0)); items.Add(CreateCard(2, realAppID: appID0)); items.Add(CreateCard(1, realAppID: appID1)); items.Add(CreateCard(2, realAppID: appID1)); } Dictionary itemsPerSet = new() { { appID0, 2 }, { appID1, 2 } }; HashSet itemsToSend = GetItemsForFullBadge(items, itemsPerSet); Assert.IsTrue(itemsToSend.Count <= Steam.Exchange.Trading.MaxItemsPerTrade); } [TestMethod] internal void TooManyCardsPerSet() { const uint appID0 = 42; const uint appID1 = 43; const uint appID2 = 44; HashSet items = [ CreateCard(1, realAppID: appID0), CreateCard(2, realAppID: appID0), CreateCard(3, realAppID: appID0), CreateCard(4, realAppID: appID0) ]; Assert.ThrowsException( () => GetItemsForFullBadge( items, new Dictionary { { appID0, 3 }, { appID1, 3 }, { appID2, 3 } } ) ); } private static void AssertResultMatchesExpectation(Dictionary<(uint RealAppID, ulong ContextID, ulong ClassID), uint> expectedResult, IReadOnlyCollection itemsToSend) { ArgumentNullException.ThrowIfNull(expectedResult); ArgumentNullException.ThrowIfNull(itemsToSend); Dictionary<(uint RealAppID, ulong ContextID, ulong ClassID), long> realResult = itemsToSend.GroupBy(static asset => (asset.RealAppID, asset.ContextID, asset.ClassID)).ToDictionary(static group => group.Key, static group => group.Sum(static asset => asset.Amount)); Assert.AreEqual(expectedResult.Count, realResult.Count); Assert.IsTrue(expectedResult.All(expectation => realResult.TryGetValue(expectation.Key, out long reality) && (expectation.Value == reality))); } private static Asset CreateCard(ulong classID, ulong instanceID = 0, uint amount = 1, bool marketable = false, bool tradable = false, uint realAppID = Asset.SteamAppID, EAssetType type = EAssetType.TradingCard, EAssetRarity rarity = EAssetRarity.Common) => new(Asset.SteamAppID, Asset.SteamCommunityContextID, classID, amount, new InventoryDescription(Asset.SteamAppID, classID, instanceID, marketable, tradable, realAppID, type, rarity)); private static HashSet GetItemsForFullBadge(IReadOnlyCollection inventory, byte cardsPerSet, uint appID, ushort maxItems = Steam.Exchange.Trading.MaxItemsPerTrade) => GetItemsForFullBadge(inventory, new Dictionary { { appID, cardsPerSet } }, maxItems); private static HashSet GetItemsForFullBadge(IReadOnlyCollection inventory, [SuppressMessage("ReSharper", "SuggestBaseTypeForParameter")] Dictionary cardsPerSet, ushort maxItems = Steam.Exchange.Trading.MaxItemsPerTrade) { Dictionary<(uint RealAppID, EAssetType Type, EAssetRarity Rarity), List> inventorySets = Steam.Exchange.Trading.GetInventorySets(inventory); return GetItemsForFullSets(inventory, inventorySets.ToDictionary(static kv => kv.Key, kv => (SetsToExtract: inventorySets[kv.Key][0], cardsPerSet[kv.Key.RealAppID])), maxItems).ToHashSet(); } } #pragma warning restore CA1812 // False positive, the class is used during MSTest