From 73d9e87e243d91dc0728dfced02b96b76c3905cd Mon Sep 17 00:00:00 2001 From: Huy Nguyen Date: Fri, 31 Jul 2026 14:31:13 +0930 Subject: [PATCH] Add TpkExtensions for Steam link creation and introduce unit tests --- Extensions/TpkExtensions.cs | 39 +++++++++++ HumbleKeysLibrary.cs | 33 ++-------- HumbleKeysLibrary.csproj | 3 +- HumbleKeysLibrary.sln | 9 +++ HumbleKeysLibrary.sln.DotSettings | 3 + HumbleKeysLibraryTest/ExtensionTests.cs | 66 +++++++++++++++++++ .../HumbleKeysLibraryTest.csproj | 65 ++++++++++++++++++ .../Properties/AssemblyInfo.cs | 35 ++++++++++ 8 files changed, 226 insertions(+), 27 deletions(-) create mode 100644 Extensions/TpkExtensions.cs create mode 100644 HumbleKeysLibraryTest/ExtensionTests.cs create mode 100644 HumbleKeysLibraryTest/HumbleKeysLibraryTest.csproj create mode 100644 HumbleKeysLibraryTest/Properties/AssemblyInfo.cs diff --git a/Extensions/TpkExtensions.cs b/Extensions/TpkExtensions.cs new file mode 100644 index 00000000..3186ebdc --- /dev/null +++ b/Extensions/TpkExtensions.cs @@ -0,0 +1,39 @@ +using System; +using HumbleKeys.Models; +using Playnite.SDK.Models; + +namespace HumbleKeys.Extensions +{ + public static class TpkExtensions + { + public const string SteamGameUrlMask = @"https://store.steampowered.com/app/{0}"; + public const string SteamSearchUrlMask = @"https://store.steampowered.com/search/?term={0}"; + + public static Link MakeSteamLink(string gameKey) => new Link("Steam", string.Format(SteamGameUrlMask, gameKey)); + + public static Link MakeSteamLink(this Order.TpkdDict.Tpk tpk) + { + if (!string.IsNullOrEmpty(tpk.steam_app_id)) { return MakeSteamLink(tpk.steam_app_id); } + + string humanName = tpk.human_name; + + if (string.IsNullOrEmpty(humanName)) + { + humanName = tpk.machine_name?.Replace("_steam", string.Empty).Replace("_choice", string.Empty); + } + + if (string.IsNullOrEmpty(humanName)) return null; + if (humanName.EndsWith(" Steam")) + { + humanName = humanName.Remove(humanName.LastIndexOf(" Steam", StringComparison.Ordinal)); + } + if (humanName.EndsWith(" DLC")) + { + humanName = humanName.Remove(humanName.LastIndexOf(" DLC", StringComparison.Ordinal)); + } + + return new Link("Steam", string.Format(SteamSearchUrlMask, humanName.Replace(" ", "%2B"))); + } + + } +} \ No newline at end of file diff --git a/HumbleKeysLibrary.cs b/HumbleKeysLibrary.cs index 3846246e..283d612b 100644 --- a/HumbleKeysLibrary.cs +++ b/HumbleKeysLibrary.cs @@ -7,6 +7,7 @@ using System.Collections.ObjectModel; using System.Linq; using System.Windows.Controls; +using HumbleKeys.Extensions; using HumbleKeys.Models; using HumbleKeys.Services; @@ -19,8 +20,6 @@ public class HumbleKeysLibrary : LibraryPlugin private static readonly ILogger logger = LogManager.GetLogger(); private const string dbImportMessageId = "humblekeyslibImportError"; private const string humblePurchaseUrlMask = @"https://www.humblebundle.com/downloads?key={0}"; - private const string steamGameUrlMask = @"https://store.steampowered.com/app/{0}"; - private const string steamSearchUrlMask = @"https://store.steampowered.com/search/?term={0}"; private const string REDEEMED_STR = "Key: Redeemed"; private const string UNREDEEMED_STR = "Key: Unredeemed"; private const string UNREDEEMABLE_STR = "Key: Unredeemable"; @@ -335,7 +334,7 @@ protected void ProcessOrders(Dictionary orders, IEnumerable links, Order.TpkdDict.Tpk tpkd, bool useDispatcher) + public bool UpdateStoreLinks(ObservableCollection links, Order.TpkdDict.Tpk tpkd, bool useDispatcher) { var recordChanged = false; @@ -362,29 +361,11 @@ bool UpdateStoreLinks(ObservableCollection links, Order.TpkdDict.Tpk tpkd, } } - if (tpkd.key_type != "steam") return recordChanged; - - Link steamGameLink; - string humanName = string.Empty; - if (!string.IsNullOrEmpty(tpkd.steam_app_id)) - { - steamGameLink = MakeSteamLink(tpkd.steam_app_id); - } - else - { - humanName = tpkd.human_name; - steamGameLink = new Link("Steam", string.Format(steamSearchUrlMask, humanName.Replace(" ", "%2B"))); - } - - if (humanName.EndsWith(" Steam")) - { - humanName = humanName.Remove(humanName.LastIndexOf(" Steam", StringComparison.Ordinal)); - } - if (humanName.EndsWith(" DLC")) - { - humanName = humanName.Remove(humanName.LastIndexOf(" DLC", StringComparison.Ordinal)); - } + if (tpkd?.key_type != "steam") return recordChanged; + var steamGameLink = tpkd.MakeSteamLink(); + if (steamGameLink == null) return false; + var steamLinks = links.Where((link1, i) => link1.Name == "Steam"); var steamLinksList = steamLinks.ToList(); var existingSteamLink = steamLinksList.FirstOrDefault(); @@ -418,6 +399,7 @@ bool UpdateStoreLinks(ObservableCollection links, Order.TpkdDict.Tpk tpkd, } return true; + } Game ImportNewGame(Order.TpkdDict.Tpk tpkd, Tag groupTag = null) @@ -639,7 +621,6 @@ KeyInfo GetKeyInfo(string key_type, bool needSourceId) #region === Helper Methods ============ private static string GetGameId(Order.TpkdDict.Tpk tpk) => $"{tpk.machine_name}_{tpk.gamekey}"; private static Link MakeLink(string gameKey) => new Link("Humble Purchase URL", string.Format(humblePurchaseUrlMask, gameKey) ); - private static Link MakeSteamLink(string gameKey) => new Link("Steam", string.Format(steamGameUrlMask, gameKey)); private static bool IsKeyNull(Order.TpkdDict.Tpk t) => t?.redeemed_key_val == null; private static bool IsKeyPresent(Order.TpkdDict.Tpk t) => !IsKeyNull(t); private static string GetOrderRedemptionTagState(Order.TpkdDict.Tpk t) diff --git a/HumbleKeysLibrary.csproj b/HumbleKeysLibrary.csproj index d7efc902..90b8b3a2 100644 --- a/HumbleKeysLibrary.csproj +++ b/HumbleKeysLibrary.csproj @@ -48,6 +48,7 @@ + @@ -89,7 +90,7 @@ - + diff --git a/HumbleKeysLibrary.sln b/HumbleKeysLibrary.sln index 4ed212c2..b6d14c64 100644 --- a/HumbleKeysLibrary.sln +++ b/HumbleKeysLibrary.sln @@ -5,6 +5,11 @@ VisualStudioVersion = 15.0.28307.572 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HumbleKeysLibrary", "HumbleKeysLibrary.csproj", "{2351B5C1-6E28-4B79-A1D9-90FDA53B5417}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HumbleKeysLibraryTest", "HumbleKeysLibraryTest\HumbleKeysLibraryTest.csproj", "{487D0989-E9EF-4852-A4C2-FB40E1E2ABA9}" + ProjectSection(ProjectDependencies) = postProject + {2351B5C1-6E28-4B79-A1D9-90FDA53B5417} = {2351B5C1-6E28-4B79-A1D9-90FDA53B5417} + EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -15,6 +20,10 @@ Global {2351B5C1-6E28-4B79-A1D9-90FDA53B5417}.Debug|Any CPU.Build.0 = Debug|Any CPU {2351B5C1-6E28-4B79-A1D9-90FDA53B5417}.Release|Any CPU.ActiveCfg = Release|Any CPU {2351B5C1-6E28-4B79-A1D9-90FDA53B5417}.Release|Any CPU.Build.0 = Release|Any CPU + {487D0989-E9EF-4852-A4C2-FB40E1E2ABA9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {487D0989-E9EF-4852-A4C2-FB40E1E2ABA9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {487D0989-E9EF-4852-A4C2-FB40E1E2ABA9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {487D0989-E9EF-4852-A4C2-FB40E1E2ABA9}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/HumbleKeysLibrary.sln.DotSettings b/HumbleKeysLibrary.sln.DotSettings index dd82801f..43df2dbc 100644 --- a/HumbleKeysLibrary.sln.DotSettings +++ b/HumbleKeysLibrary.sln.DotSettings @@ -1,4 +1,5 @@  + DLC <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb"><ExtraRule Prefix="" Suffix="" Style="AA_BB" /></Policy> <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> @@ -12,6 +13,8 @@ <Policy><Descriptor Staticness="Static" AccessRightKinds="Protected, ProtectedInternal, Internal, Public, PrivateProtected" Description="Static fields (not private)"><ElementKinds><Kind Name="FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /></Policy> <Policy><Descriptor Staticness="Static" AccessRightKinds="Protected, ProtectedInternal, Internal, Public, PrivateProtected" Description="Static readonly fields (not private)"><ElementKinds><Kind Name="READONLY_FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /></Policy> <Policy><Descriptor Staticness="Static" AccessRightKinds="Private" Description="Static fields (private)"><ElementKinds><Kind Name="FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /></Policy> + C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\MSBuild\Current\Bin\MSBuild.exe + 1048576 True True True diff --git a/HumbleKeysLibraryTest/ExtensionTests.cs b/HumbleKeysLibraryTest/ExtensionTests.cs new file mode 100644 index 00000000..f45addce --- /dev/null +++ b/HumbleKeysLibraryTest/ExtensionTests.cs @@ -0,0 +1,66 @@ +using HumbleKeys; +using HumbleKeys.Extensions; +using HumbleKeys.Models; +using NSubstitute; +using NUnit.Framework; +using Playnite.SDK; + +namespace HumbleKeysLibraryTest +{ + [TestFixture] + public class ExtensionTests + { + [TestCase] + public void EmptyTpk() + { + var tpk = new Order.TpkdDict.Tpk(); + var link = tpk.MakeSteamLink(); + Assert.That(link, Is.Null); + } + + [TestCase] + public void EmptyHumanNameCreatesValidSteamLinkViaMachineName() + { + var tpk = new Order.TpkdDict.Tpk {machine_name = "flowersanddeities_choice_steam", human_name = string.Empty}; + var link = tpk.MakeSteamLink(); + Assert.That(link, Is.Not.Null); + Assert.That(link.Url, Is.EqualTo(string.Format(TpkExtensions.SteamSearchUrlMask, "flowersanddeities"))); + } + + [TestCase] + public void HumanNameCreatesValidSteamLinkViaHumanName() + { + var tpk = new Order.TpkdDict.Tpk {machine_name = "flowersanddeities_choice_steam", human_name = "Flowers and Deities"}; + var link = tpk.MakeSteamLink(); + Assert.That(link, Is.Not.Null); + Assert.That(link.Url, Is.EqualTo(string.Format(TpkExtensions.SteamSearchUrlMask, "Flowers%2Band%2BDeities"))); + } + + [TestCase] + public void HumanNameDLCCreatesValidSteamLinkViaHumanName() + { + var tpk = new Order.TpkdDict.Tpk {machine_name = "flowersanddeities_choice_steam", human_name = "Flowers and Deities DLC"}; + var link = tpk.MakeSteamLink(); + Assert.That(link, Is.Not.Null); + Assert.That(link.Url, Is.EqualTo(string.Format(TpkExtensions.SteamSearchUrlMask, "Flowers%2Band%2BDeities"))); + } + + [TestCase] + public void HumanNameSteamCreatesValidSteamLinkViaHumanName() + { + var tpk = new Order.TpkdDict.Tpk {machine_name = "flowersanddeities_choice_steam", human_name = "Flowers and Deities Steam"}; + var link = tpk.MakeSteamLink(); + Assert.That(link, Is.Not.Null); + Assert.That(link.Url, Is.EqualTo(string.Format(TpkExtensions.SteamSearchUrlMask, "Flowers%2Band%2BDeities"))); + } + + [TestCase] + public void AppIdCreatesValidSteamLink() + { + var tpk = new Order.TpkdDict.Tpk {steam_app_id = "3816000", machine_name = "flowersanddeities_choice_steam", human_name = "Flowers and Deities"}; + var link = tpk.MakeSteamLink(); + Assert.That(link, Is.Not.Null); + Assert.That(link.Url, Is.EqualTo(string.Format(TpkExtensions.SteamGameUrlMask, "3816000"))); + } + } +} \ No newline at end of file diff --git a/HumbleKeysLibraryTest/HumbleKeysLibraryTest.csproj b/HumbleKeysLibraryTest/HumbleKeysLibraryTest.csproj new file mode 100644 index 00000000..2646d00f --- /dev/null +++ b/HumbleKeysLibraryTest/HumbleKeysLibraryTest.csproj @@ -0,0 +1,65 @@ + + + + + Debug + AnyCPU + {487D0989-E9EF-4852-A4C2-FB40E1E2ABA9} + {FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + Library + Properties + HumbleKeysLibraryTest + HumbleKeysLibraryTest + v4.6.2 + 512 + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + + + + {2351b5c1-6e28-4b79-a1d9-90fda53b5417} + HumbleKeysLibrary + + + + + + diff --git a/HumbleKeysLibraryTest/Properties/AssemblyInfo.cs b/HumbleKeysLibraryTest/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..cb67f619 --- /dev/null +++ b/HumbleKeysLibraryTest/Properties/AssemblyInfo.cs @@ -0,0 +1,35 @@ +using System.Reflection; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("HumbleKeysLibraryTest")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("HumbleKeysLibraryTest")] +[assembly: AssemblyCopyright("Copyright © 2026")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("487D0989-E9EF-4852-A4C2-FB40E1E2ABA9")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] \ No newline at end of file