1 // ----------------------------------------------------------------------------------------------
3 // / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
4 // / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
5 // / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
6 // /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
7 // ----------------------------------------------------------------------------------------------
9 // Copyright 2015-2025 Ćukasz "JustArchi" Domeradzki
10 // Contact: JustArchi@JustArchi.net
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
16 // http://www.apache.org/licenses/LICENSE-2.0
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.
25 using System
.Collections
.Generic
;
26 using System
.Diagnostics
.CodeAnalysis
;
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
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
));
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
);
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));
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
);
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
);
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
);
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
);
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
);
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
);
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> {
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
);
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> {
292 Dictionary
<(uint RealAppID
, ulong ContextID
, ulong ClassID
), uint> expectedResult
= new(0);
294 AssertResultMatchesExpectation(expectedResult
, itemsToSend
);
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> {
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
);
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
);
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
);
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
);
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
);
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
);
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
);
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
);
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
);
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() {
497 HashSet
<Asset
> itemsToSend
= GetItemsForFullBadge(items
, itemsPerSet
);
499 Assert
.IsTrue(itemsToSend
.Count
<= Steam
.Exchange
.Trading
.MaxItemsPerTrade
);
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> {
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