chore(deps): update github/codeql-action action to v3.28.4
[ArchiSteamFarm.git] / ArchiSteamFarm.Tests / Bot.cs
blob59e654ca18ddb62bd990800ef479bc3db21f922a
1 // ----------------------------------------------------------------------------------------------
2 // _ _ _ ____ _ _____
3 // / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
4 // / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
5 // / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
6 // /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
7 // ----------------------------------------------------------------------------------------------
8 // |
9 // Copyright 2015-2025 Ɓukasz "JustArchi" Domeradzki
10 // Contact: JustArchi@JustArchi.net
11 // |
12 // Licensed under the Apache License, Version 2.0 (the "License");
13 // you may not use this file except in compliance with the License.
14 // You may obtain a copy of the License at
15 // |
16 // http://www.apache.org/licenses/LICENSE-2.0
17 // |
18 // Unless required by applicable law or agreed to in writing, software
19 // distributed under the License is distributed on an "AS IS" BASIS,
20 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
21 // See the License for the specific language governing permissions and
22 // limitations under the License.
24 using System;
25 using System.Collections.Generic;
26 using System.Diagnostics.CodeAnalysis;
27 using System.Linq;
28 using System.Reflection;
29 using System.Text.Json;
30 using System.Text.Json.Nodes;
31 using ArchiSteamFarm.Core;
32 using ArchiSteamFarm.Helpers.Json;
33 using ArchiSteamFarm.Steam.Data;
34 using ArchiSteamFarm.Steam.Storage;
35 using ArchiSteamFarm.Storage;
36 using Microsoft.VisualStudio.TestTools.UnitTesting;
37 using static ArchiSteamFarm.Steam.Bot;
39 namespace ArchiSteamFarm.Tests;
41 #pragma warning disable CA1812 // False positive, the class is used during MSTest
42 [TestClass]
43 internal sealed class Bot {
44 internal static Steam.Bot GenerateBot(string botName = "Test") {
45 ArgumentException.ThrowIfNullOrEmpty(botName);
47 ConstructorInfo? constructor = typeof(Steam.Bot).GetConstructor(BindingFlags.Instance | BindingFlags.NonPublic, [typeof(string), typeof(BotConfig), typeof(BotDatabase)]);
49 if (constructor == null) {
50 throw new InvalidOperationException(nameof(constructor));
53 JsonElement emptyObject = new JsonObject().ToJsonElement();
55 BotConfig? botConfig = emptyObject.ToJsonObject<BotConfig>();
57 if (botConfig == null) {
58 throw new InvalidOperationException(nameof(botConfig));
61 BotDatabase? botDatabase = emptyObject.ToJsonObject<BotDatabase>();
63 if (botDatabase == null) {
64 throw new InvalidOperationException(nameof(botDatabase));
67 ASF.GlobalDatabase ??= emptyObject.ToJsonObject<GlobalDatabase>();
69 if (constructor.Invoke([botName, botConfig, botDatabase]) is not Steam.Bot result) {
70 throw new InvalidOperationException(nameof(result));
73 return result;
76 [TestMethod]
77 internal void MaxItemsBarelyEnoughForOneSet() {
78 const uint relevantAppID = 42;
80 Dictionary<uint, byte> itemsPerSet = new() {
81 { relevantAppID, MinCardsPerBadge },
82 { 43, MinCardsPerBadge + 1 }
85 HashSet<Asset> items = [];
87 foreach ((uint appID, byte cards) in itemsPerSet) {
88 for (byte i = 1; i <= cards; i++) {
89 items.Add(CreateCard(i, realAppID: appID));
93 HashSet<Asset> itemsToSend = GetItemsForFullBadge(items, itemsPerSet, MinCardsPerBadge);
95 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));
97 AssertResultMatchesExpectation(expectedResult, itemsToSend);
100 [TestMethod]
101 internal void MaxItemsTooSmall() {
102 const uint appID = 42;
104 HashSet<Asset> items = [
105 CreateCard(1, realAppID: appID),
106 CreateCard(2, realAppID: appID)
109 Assert.ThrowsException<ArgumentOutOfRangeException>(() => GetItemsForFullBadge(items, 2, appID, MinCardsPerBadge - 1));
112 [TestMethod]
113 internal void MoreCardsThanNeeded() {
114 const uint appID = 42;
116 HashSet<Asset> items = [
117 CreateCard(1, realAppID: appID),
118 CreateCard(1, realAppID: appID),
119 CreateCard(2, realAppID: appID),
120 CreateCard(3, realAppID: appID)
123 HashSet<Asset> itemsToSend = GetItemsForFullBadge(items, 3, appID);
125 Dictionary<(uint RealAppID, ulong ContextID, ulong ClassID), uint> expectedResult = new() {
126 { (appID, Asset.SteamCommunityContextID, 1), 1 },
127 { (appID, Asset.SteamCommunityContextID, 2), 1 },
128 { (appID, Asset.SteamCommunityContextID, 3), 1 }
131 AssertResultMatchesExpectation(expectedResult, itemsToSend);
134 [TestMethod]
135 internal void MultipleSets() {
136 const uint appID = 42;
138 HashSet<Asset> items = [
139 CreateCard(1, realAppID: appID),
140 CreateCard(1, realAppID: appID),
141 CreateCard(2, realAppID: appID),
142 CreateCard(2, realAppID: appID)
145 HashSet<Asset> itemsToSend = GetItemsForFullBadge(items, 2, appID);
147 Dictionary<(uint RealAppID, ulong ContextID, ulong ClassID), uint> expectedResult = new() {
148 { (appID, Asset.SteamCommunityContextID, 1), 2 },
149 { (appID, Asset.SteamCommunityContextID, 2), 2 }
152 AssertResultMatchesExpectation(expectedResult, itemsToSend);
155 [TestMethod]
156 internal void MultipleSetsDifferentAmount() {
157 const uint appID = 42;
159 HashSet<Asset> items = [
160 CreateCard(1, amount: 2, realAppID: appID),
161 CreateCard(2, realAppID: appID),
162 CreateCard(2, realAppID: appID)
165 HashSet<Asset> itemsToSend = GetItemsForFullBadge(items, 2, appID);
167 Dictionary<(uint RealAppID, ulong ContextID, ulong ClassID), uint> expectedResult = new() {
168 { (appID, Asset.SteamCommunityContextID, 1), 2 },
169 { (appID, Asset.SteamCommunityContextID, 2), 2 }
172 AssertResultMatchesExpectation(expectedResult, itemsToSend);
175 [TestMethod]
176 internal void MutliRarityAndType() {
177 const uint appID = 42;
179 HashSet<Asset> items = [
180 CreateCard(1, realAppID: appID, type: EAssetType.TradingCard, rarity: EAssetRarity.Common),
181 CreateCard(2, realAppID: appID, type: EAssetType.TradingCard, rarity: EAssetRarity.Common),
183 CreateCard(1, realAppID: appID, type: EAssetType.FoilTradingCard, rarity: EAssetRarity.Uncommon),
184 CreateCard(2, realAppID: appID, type: EAssetType.FoilTradingCard, rarity: EAssetRarity.Uncommon),
186 CreateCard(1, realAppID: appID, type: EAssetType.FoilTradingCard, rarity: EAssetRarity.Rare),
187 CreateCard(2, realAppID: appID, type: EAssetType.FoilTradingCard, rarity: EAssetRarity.Rare),
189 // 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
190 CreateCard(1, realAppID: appID, type: EAssetType.TradingCard, rarity: EAssetRarity.Uncommon),
191 CreateCard(2, realAppID: appID, type: EAssetType.TradingCard, rarity: EAssetRarity.Uncommon),
192 CreateCard(3, realAppID: appID, type: EAssetType.TradingCard, rarity: EAssetRarity.Uncommon),
194 CreateCard(1, realAppID: appID, type: EAssetType.FoilTradingCard, rarity: EAssetRarity.Common),
195 CreateCard(3, realAppID: appID, type: EAssetType.FoilTradingCard, rarity: EAssetRarity.Common),
196 CreateCard(7, realAppID: appID, type: EAssetType.FoilTradingCard, rarity: EAssetRarity.Common),
198 CreateCard(2, realAppID: appID, type: EAssetType.Unknown, rarity: EAssetRarity.Rare),
199 CreateCard(3, realAppID: appID, type: EAssetType.Unknown, rarity: EAssetRarity.Rare),
200 CreateCard(4, realAppID: appID, type: EAssetType.Unknown, rarity: EAssetRarity.Rare)
203 HashSet<Asset> itemsToSend = GetItemsForFullBadge(items, 3, appID);
205 Dictionary<(uint RealAppID, ulong ContextID, ulong ClassID), uint> expectedResult = new() {
206 { (appID, Asset.SteamCommunityContextID, 1), 2 },
207 { (appID, Asset.SteamCommunityContextID, 2), 2 },
208 { (appID, Asset.SteamCommunityContextID, 3), 3 },
209 { (appID, Asset.SteamCommunityContextID, 4), 1 },
210 { (appID, Asset.SteamCommunityContextID, 7), 1 }
213 AssertResultMatchesExpectation(expectedResult, itemsToSend);
216 [TestMethod]
217 internal void NotAllCardsPresent() {
218 const uint appID = 42;
220 HashSet<Asset> items = [
221 CreateCard(1, realAppID: appID),
222 CreateCard(2, realAppID: appID)
225 HashSet<Asset> itemsToSend = GetItemsForFullBadge(items, 3, appID);
227 Dictionary<(uint RealAppID, ulong ContextID, ulong ClassID), uint> expectedResult = new(0);
228 AssertResultMatchesExpectation(expectedResult, itemsToSend);
231 [TestMethod]
232 internal void OneSet() {
233 const uint appID = 42;
235 HashSet<Asset> items = [
236 CreateCard(1, realAppID: appID),
237 CreateCard(2, realAppID: appID)
240 HashSet<Asset> itemsToSend = GetItemsForFullBadge(items, 2, appID);
242 Dictionary<(uint RealAppID, ulong ContextID, ulong ClassID), uint> expectedResult = new() {
243 { (appID, Asset.SteamCommunityContextID, 1), 1 },
244 { (appID, Asset.SteamCommunityContextID, 2), 1 }
247 AssertResultMatchesExpectation(expectedResult, itemsToSend);
250 [TestMethod]
251 internal void OtherAppIDFullSets() {
252 const uint appID0 = 42;
253 const uint appID1 = 43;
255 HashSet<Asset> items = [
256 CreateCard(1, realAppID: appID0),
257 CreateCard(1, realAppID: appID1)
260 HashSet<Asset> itemsToSend = GetItemsForFullBadge(
261 items, new Dictionary<uint, byte> {
262 { appID0, 1 },
263 { appID1, 1 }
267 Dictionary<(uint RealAppID, ulong ContextID, ulong ClassID), uint> expectedResult = new() {
268 { (appID0, Asset.SteamCommunityContextID, 1), 1 },
269 { (appID1, Asset.SteamCommunityContextID, 1), 1 }
272 AssertResultMatchesExpectation(expectedResult, itemsToSend);
275 [TestMethod]
276 internal void OtherAppIDNoSets() {
277 const uint appID0 = 42;
278 const uint appID1 = 43;
280 HashSet<Asset> items = [
281 CreateCard(1, realAppID: appID0),
282 CreateCard(1, realAppID: appID1)
285 HashSet<Asset> itemsToSend = GetItemsForFullBadge(
286 items, new Dictionary<uint, byte> {
287 { appID0, 2 },
288 { appID1, 2 }
292 Dictionary<(uint RealAppID, ulong ContextID, ulong ClassID), uint> expectedResult = new(0);
294 AssertResultMatchesExpectation(expectedResult, itemsToSend);
297 [TestMethod]
298 internal void OtherAppIDOneSet() {
299 const uint appID0 = 42;
300 const uint appID1 = 43;
301 const uint appID2 = 44;
303 HashSet<Asset> items = [
304 CreateCard(1, realAppID: appID0),
305 CreateCard(2, realAppID: appID0),
307 CreateCard(1, realAppID: appID1),
308 CreateCard(2, realAppID: appID1),
309 CreateCard(3, realAppID: appID1)
312 HashSet<Asset> itemsToSend = GetItemsForFullBadge(
313 items, new Dictionary<uint, byte> {
314 { appID0, 3 },
315 { appID1, 3 },
316 { appID2, 3 }
320 Dictionary<(uint RealAppID, ulong ContextID, ulong ClassID), uint> expectedResult = new() {
321 { (appID1, Asset.SteamCommunityContextID, 1), 1 },
322 { (appID1, Asset.SteamCommunityContextID, 2), 1 },
323 { (appID1, Asset.SteamCommunityContextID, 3), 1 }
326 AssertResultMatchesExpectation(expectedResult, itemsToSend);
329 [TestMethod]
330 internal void OtherRarityFullSets() {
331 const uint appID = 42;
333 HashSet<Asset> items = [
334 CreateCard(1, realAppID: appID, rarity: EAssetRarity.Common),
335 CreateCard(1, realAppID: appID, rarity: EAssetRarity.Rare)
338 HashSet<Asset> itemsToSend = GetItemsForFullBadge(items, 1, appID);
340 Dictionary<(uint RealAppID, ulong ContextID, ulong ClassID), uint> expectedResult = new() {
341 { (appID, Asset.SteamCommunityContextID, 1), 2 }
344 AssertResultMatchesExpectation(expectedResult, itemsToSend);
347 [TestMethod]
348 internal void OtherRarityNoSets() {
349 const uint appID = 42;
351 HashSet<Asset> items = [
352 CreateCard(1, realAppID: appID, rarity: EAssetRarity.Common),
353 CreateCard(1, realAppID: appID, rarity: EAssetRarity.Rare)
356 HashSet<Asset> itemsToSend = GetItemsForFullBadge(items, 2, appID);
358 Dictionary<(uint RealAppID, ulong ContextID, ulong ClassID), uint> expectedResult = new(0);
360 AssertResultMatchesExpectation(expectedResult, itemsToSend);
363 [TestMethod]
364 internal void OtherRarityOneSet() {
365 const uint appID = 42;
367 HashSet<Asset> items = [
368 CreateCard(1, realAppID: appID, rarity: EAssetRarity.Common),
369 CreateCard(2, realAppID: appID, rarity: EAssetRarity.Common),
370 CreateCard(1, realAppID: appID, rarity: EAssetRarity.Uncommon),
371 CreateCard(2, realAppID: appID, rarity: EAssetRarity.Uncommon),
372 CreateCard(3, realAppID: appID, rarity: EAssetRarity.Uncommon)
375 HashSet<Asset> itemsToSend = GetItemsForFullBadge(items, 3, appID);
377 Dictionary<(uint RealAppID, ulong ContextID, ulong ClassID), uint> expectedResult = new() {
378 { (appID, Asset.SteamCommunityContextID, 1), 1 },
379 { (appID, Asset.SteamCommunityContextID, 2), 1 },
380 { (appID, Asset.SteamCommunityContextID, 3), 1 }
383 AssertResultMatchesExpectation(expectedResult, itemsToSend);
386 [TestMethod]
387 internal void OtherTypeFullSets() {
388 const uint appID = 42;
390 HashSet<Asset> items = [
391 CreateCard(1, realAppID: appID, type: EAssetType.TradingCard),
392 CreateCard(1, realAppID: appID, type: EAssetType.FoilTradingCard)
395 HashSet<Asset> itemsToSend = GetItemsForFullBadge(items, 1, appID);
397 Dictionary<(uint RealAppID, ulong ContextID, ulong ClassID), uint> expectedResult = new() {
398 { (appID, Asset.SteamCommunityContextID, 1), 2 }
401 AssertResultMatchesExpectation(expectedResult, itemsToSend);
404 [TestMethod]
405 internal void OtherTypeNoSets() {
406 const uint appID = 42;
408 HashSet<Asset> items = [
409 CreateCard(1, realAppID: appID, type: EAssetType.TradingCard),
410 CreateCard(1, realAppID: appID, type: EAssetType.FoilTradingCard)
413 HashSet<Asset> itemsToSend = GetItemsForFullBadge(items, 2, appID);
415 Dictionary<(uint RealAppID, ulong ContextID, ulong ClassID), uint> expectedResult = new(0);
417 AssertResultMatchesExpectation(expectedResult, itemsToSend);
420 [TestMethod]
421 internal void OtherTypeOneSet() {
422 const uint appID = 42;
424 HashSet<Asset> items = [
425 CreateCard(1, realAppID: appID, type: EAssetType.TradingCard),
426 CreateCard(2, realAppID: appID, type: EAssetType.TradingCard),
427 CreateCard(1, realAppID: appID, type: EAssetType.FoilTradingCard),
428 CreateCard(2, realAppID: appID, type: EAssetType.FoilTradingCard),
429 CreateCard(3, realAppID: appID, type: EAssetType.FoilTradingCard)
432 HashSet<Asset> itemsToSend = GetItemsForFullBadge(items, 3, appID);
434 Dictionary<(uint RealAppID, ulong ContextID, ulong ClassID), uint> expectedResult = new() {
435 { (appID, Asset.SteamCommunityContextID, 1), 1 },
436 { (appID, Asset.SteamCommunityContextID, 2), 1 },
437 { (appID, Asset.SteamCommunityContextID, 3), 1 }
440 AssertResultMatchesExpectation(expectedResult, itemsToSend);
443 [TestMethod]
444 internal void TooHighAmount() {
445 const uint appID0 = 42;
447 HashSet<Asset> items = [
448 CreateCard(1, amount: 2, realAppID: appID0),
449 CreateCard(2, realAppID: appID0)
452 HashSet<Asset> itemsToSend = GetItemsForFullBadge(items, 2, appID0);
454 Dictionary<(uint RealAppID, ulong ContextID, ulong ClassID), uint> expectedResult = new() {
455 { (appID0, Asset.SteamCommunityContextID, 1), 1 },
456 { (appID0, Asset.SteamCommunityContextID, 2), 1 }
459 AssertResultMatchesExpectation(expectedResult, itemsToSend);
462 [TestMethod]
463 internal void TooManyCardsForSingleTrade() {
464 const uint appID = 42;
466 HashSet<Asset> items = [];
468 for (byte i = 0; i < Steam.Exchange.Trading.MaxItemsPerTrade; i++) {
469 items.Add(CreateCard(1, realAppID: appID));
470 items.Add(CreateCard(2, realAppID: appID));
473 HashSet<Asset> itemsToSend = GetItemsForFullBadge(items, 2, appID);
475 Assert.IsTrue(itemsToSend.Count <= Steam.Exchange.Trading.MaxItemsPerTrade);
478 [TestMethod]
479 internal void TooManyCardsForSingleTradeMultipleAppIDs() {
480 const uint appID0 = 42;
481 const uint appID1 = 43;
483 HashSet<Asset> items = [];
485 for (byte i = 0; i < 100; i++) {
486 items.Add(CreateCard(1, realAppID: appID0));
487 items.Add(CreateCard(2, realAppID: appID0));
488 items.Add(CreateCard(1, realAppID: appID1));
489 items.Add(CreateCard(2, realAppID: appID1));
492 Dictionary<uint, byte> itemsPerSet = new() {
493 { appID0, 2 },
494 { appID1, 2 }
497 HashSet<Asset> itemsToSend = GetItemsForFullBadge(items, itemsPerSet);
499 Assert.IsTrue(itemsToSend.Count <= Steam.Exchange.Trading.MaxItemsPerTrade);
502 [TestMethod]
503 internal void TooManyCardsPerSet() {
504 const uint appID0 = 42;
505 const uint appID1 = 43;
506 const uint appID2 = 44;
508 HashSet<Asset> items = [
509 CreateCard(1, realAppID: appID0),
510 CreateCard(2, realAppID: appID0),
511 CreateCard(3, realAppID: appID0),
512 CreateCard(4, realAppID: appID0)
515 Assert.ThrowsException<InvalidOperationException>(
516 () => GetItemsForFullBadge(
517 items, new Dictionary<uint, byte> {
518 { appID0, 3 },
519 { appID1, 3 },
520 { appID2, 3 }
526 private static void AssertResultMatchesExpectation(Dictionary<(uint RealAppID, ulong ContextID, ulong ClassID), uint> expectedResult, IReadOnlyCollection<Asset> itemsToSend) {
527 ArgumentNullException.ThrowIfNull(expectedResult);
528 ArgumentNullException.ThrowIfNull(itemsToSend);
530 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));
531 Assert.AreEqual(expectedResult.Count, realResult.Count);
532 Assert.IsTrue(expectedResult.All(expectation => realResult.TryGetValue(expectation.Key, out long reality) && (expectation.Value == reality)));
535 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));
537 private static HashSet<Asset> GetItemsForFullBadge(IReadOnlyCollection<Asset> inventory, byte cardsPerSet, uint appID, ushort maxItems = Steam.Exchange.Trading.MaxItemsPerTrade) => GetItemsForFullBadge(inventory, new Dictionary<uint, byte> { { appID, cardsPerSet } }, maxItems);
539 private static HashSet<Asset> GetItemsForFullBadge(IReadOnlyCollection<Asset> inventory, [SuppressMessage("ReSharper", "SuggestBaseTypeForParameter")] Dictionary<uint, byte> cardsPerSet, ushort maxItems = Steam.Exchange.Trading.MaxItemsPerTrade) {
540 Dictionary<(uint RealAppID, EAssetType Type, EAssetRarity Rarity), List<uint>> inventorySets = Steam.Exchange.Trading.GetInventorySets(inventory);
542 return GetItemsForFullSets(inventory, inventorySets.ToDictionary(static kv => kv.Key, kv => (SetsToExtract: inventorySets[kv.Key][0], cardsPerSet[kv.Key.RealAppID])), maxItems).ToHashSet();
545 #pragma warning restore CA1812 // False positive, the class is used during MSTest