diff --git a/README.md b/README.md index 0309ea1..e041892 100644 --- a/README.md +++ b/README.md @@ -65,16 +65,35 @@ StackifyLib.Logger.GlobalEnvironment = ""; By default the library will use the WebRequest.DefaultWebProxy. If you want to set a specific proxy server only for StackifyLib, you can do so in code OR via config. +For proxy using custom credentials: ```xml ``` +For proxy using default credentials: +```xml + + + +``` + +For proxy that needs to be tailor fit ```csharp StackifyLib.Utils.HttpClient.CustomWebProxy = new WebProxy(); ``` +In case it need some web request intervention, a delegate has been provided to customize the http request +```csharp +StackifyLib.Utils.HttpClient.CustomRequestModifier = (request) => +{ + // Examples: + request.PreAuthenticate = true; // If proxy needs pre-authentication + request.Timeout = 30000; +}; +``` + If you are having problems you can get logging out of the framework by hooking in to its custom logging. ```csharp diff --git a/Src/NLog.Targets.Stackify/NLog.Targets.Stackify.csproj b/Src/NLog.Targets.Stackify/NLog.Targets.Stackify.csproj index 742c498..ff29ab3 100644 --- a/Src/NLog.Targets.Stackify/NLog.Targets.Stackify.csproj +++ b/Src/NLog.Targets.Stackify/NLog.Targets.Stackify.csproj @@ -2,7 +2,7 @@ NLog.Targets.Stackify - 2.0.0 + 3.0.0 netstandard2.0;net40;net45;net461 NLog.Targets.Stackify NLog.Targets.Stackify @@ -15,7 +15,7 @@ false false false - 2.2.0-beta1 + 3.0.0 https://github.com/stackify/stackify-api-dotnet/blob/master/LICENSE https://github.com/stackify/stackify-api-dotnet https://stackify.com/wp-content/uploads/2017/02/stk.png @@ -25,8 +25,8 @@ - - + + diff --git a/Src/NLog.Web.Stackify/NLog.Web.Stackify.csproj b/Src/NLog.Web.Stackify/NLog.Web.Stackify.csproj index 3847e4b..d2db6ba 100644 --- a/Src/NLog.Web.Stackify/NLog.Web.Stackify.csproj +++ b/Src/NLog.Web.Stackify/NLog.Web.Stackify.csproj @@ -15,7 +15,7 @@ false false false - 2.2.0-beta1 + 2.2.0 https://github.com/stackify/stackify-api-dotnet/blob/master/LICENSE https://github.com/stackify/stackify-api-dotnet https://stackify.com/wp-content/uploads/2017/02/stk.png diff --git a/Src/StackifyLib.log4net.Tests/StackifyLib.log4net.Tests.csproj b/Src/StackifyLib.log4net.Tests/StackifyLib.log4net.Tests.csproj index 75acbae..66781be 100644 --- a/Src/StackifyLib.log4net.Tests/StackifyLib.log4net.Tests.csproj +++ b/Src/StackifyLib.log4net.Tests/StackifyLib.log4net.Tests.csproj @@ -33,8 +33,8 @@ 4 - - ..\packages\log4net.2.0.11\lib\net45\log4net.dll + + ..\packages\log4net.2.0.17\lib\net45\log4net.dll ..\packages\Newtonsoft.Json.13.0.1\lib\net45\Newtonsoft.Json.dll @@ -42,9 +42,13 @@ ..\packages\NUnit.3.13.2\lib\net45\nunit.framework.dll + + ..\packages\StackifyLib.2.2.15\lib\net452\StackifyLib.dll + + @@ -65,10 +69,6 @@ {7d98e5e3-a3e7-4613-8bb7-80a60d86a5e8} StackifyLib.log4net - - {03cf7411-65e2-4998-a157-9224eca6199b} - StackifyLib - diff --git a/Src/StackifyLib.log4net.Tests/packages.config b/Src/StackifyLib.log4net.Tests/packages.config index f89ee51..19fcb11 100644 --- a/Src/StackifyLib.log4net.Tests/packages.config +++ b/Src/StackifyLib.log4net.Tests/packages.config @@ -1,6 +1,7 @@  - + + \ No newline at end of file diff --git a/Src/StackifyLib.log4net/StackifyLib.log4net.csproj b/Src/StackifyLib.log4net/StackifyLib.log4net.csproj index a02870b..3fd6483 100644 --- a/Src/StackifyLib.log4net/StackifyLib.log4net.csproj +++ b/Src/StackifyLib.log4net/StackifyLib.log4net.csproj @@ -2,7 +2,8 @@ Stackify Log4Net Appender - 2.0.0 + 2.1.7 + netstandard2.0;net40;net45 StackifyLib.log4net StackifyLib.log4net @@ -13,7 +14,7 @@ false false false - 2.1.6 + 2.1.7 https://github.com/stackify/stackify-api-dotnet/blob/master/LICENSE https://github.com/stackify/stackify-api-dotnet https://github.com/stackify/stackify-api-dotnet @@ -22,11 +23,8 @@ - - - - - + + diff --git a/Src/StackifyLib/Config.cs b/Src/StackifyLib/Config.cs index 23e0c6f..f8322c4 100644 --- a/Src/StackifyLib/Config.cs +++ b/Src/StackifyLib/Config.cs @@ -166,6 +166,13 @@ public static void LoadSettings() { RumKey = rumKey; } + + ProxyServer = Get("Stackify.ProxyServer", string.Empty); + var proxyUseDefaultCredentials = Get("Stackify.ProxyUseDefaultCredentials", string.Empty); + if (string.IsNullOrWhiteSpace(proxyUseDefaultCredentials) == false) + { + ProxyUseDefaultCredentials = proxyUseDefaultCredentials.Equals(bool.TrueString, StringComparison.CurrentCultureIgnoreCase); + } } catch (Exception ex) { @@ -217,6 +224,9 @@ public static void LoadSettings() public static string RumKey { get; set; } + public static string ProxyServer { get; set; } + public static bool? ProxyUseDefaultCredentials { get; set; } = false; + /// /// Attempts to fetch a setting value given the key. diff --git a/Src/StackifyLib/Models/AzureConfig.cs b/Src/StackifyLib/Models/AzureConfig.cs index 8dad9a0..7f1eefa 100644 --- a/Src/StackifyLib/Models/AzureConfig.cs +++ b/Src/StackifyLib/Models/AzureConfig.cs @@ -1,10 +1,8 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Text; -using System.Text.RegularExpressions; +// Copyright (c) 2024-2025 BMC Software, Inc. +// Copyright (c) 2021-2024 Netreo +// Copyright (c) 2019 Stackify +using System; using StackifyLib.Utils; -using System.Linq; #if NETFULL using Microsoft.Win32; #endif @@ -13,22 +11,26 @@ namespace StackifyLib.Models { public class AzureConfig { - private static bool? _inAzure; - private static AzureRoleType _azureRoleType = AzureRoleType.Unknown; - private static string _azureRoleName; - private static string _azureInstanceName; - private static string _entryPoint; + private static AzureConfig _instance = new AzureConfig(); + public static AzureConfig Instance => _instance; - private static readonly DateTime AppStarted = DateTime.UtcNow; + private bool? _inAzure; + private AzureRoleType _azureRoleType = AzureRoleType.Unknown; + private string _azureRoleName; + private string _azureInstanceName; + private string _entryPoint; - private static readonly object Locker = new object(); + private readonly DateTime AppStarted = DateTime.UtcNow; + private readonly object Locker = new object(); + public const string ProductionEnvName = "Production"; - public static string AzureAppWebConfigEnvironment { get; set; } - public static string AzureAppWebConfigApiKey { get; set; } - public static string AzureDriveLetter { get; set; } - public static string AzureInstanceName + public string AzureAppWebConfigEnvironment { get; set; } + public string AzureAppWebConfigApiKey { get; set; } + public string AzureDriveLetter { get; set; } + + public string AzureInstanceName { get { @@ -37,7 +39,7 @@ public static string AzureInstanceName } } - public static bool InAzure + public bool InAzure { get { @@ -62,7 +64,7 @@ public static bool InAzure } } - public static bool IsWebsite + public bool IsWebsite { get { @@ -75,12 +77,24 @@ public static bool IsWebsite } } - private static void EnsureInAzureRan() + private EnvironmentDetail _environmentDetail = null; + + public AzureConfig() + { + + } + + public AzureConfig(EnvironmentDetail environmentDetail) + { + _environmentDetail = environmentDetail; + } + + private void EnsureInAzureRan() { bool ensureTestHasBeenDone = InAzure; } - public static void LoadAzureSettings() + public void LoadAzureSettings() { if (string.IsNullOrEmpty(Environment.GetEnvironmentVariable("WEBSITE_SITE_NAME")) == false && string.IsNullOrEmpty(Environment.GetEnvironmentVariable("WEBSITE_INSTANCE_ID")) == false) { @@ -90,13 +104,13 @@ public static void LoadAzureSettings() var slotId = GetDeploymentSlotId() ?? "0000"; _azureRoleName = Environment.GetEnvironmentVariable("WEBSITE_SITE_NAME"); - _azureInstanceName = $"{Environment.GetEnvironmentVariable("WEBSITE_SITE_NAME")} {ServerConfigHelper.GetEnvironment()} [{slotId}-{Environment.GetEnvironmentVariable("WEBSITE_INSTANCE_ID").Left(6)}]"; + _azureInstanceName = $"{Environment.GetEnvironmentVariable("WEBSITE_SITE_NAME")} {GetEnvironment()} [{slotId}-{Environment.GetEnvironmentVariable("WEBSITE_INSTANCE_ID").Left(6)}]"; return; } } - public static string GetDeploymentSlotId() + public string GetDeploymentSlotId() { try { @@ -113,6 +127,32 @@ public static string GetDeploymentSlotId() return null; } + + public string GetEnvironment() + { + if (IsWebsite) + { + if (_environmentDetail == null) + { + _environmentDetail = EnvironmentDetail.Get(); + } + + var key = Environment.GetEnvironmentVariable("Stackify.Environment"); + if (string.IsNullOrEmpty(key)) + { + key = _environmentDetail.ConfiguredEnvironmentName; + } + + if (string.IsNullOrEmpty(key) == false) + { + return key; + } + + return ProductionEnvName; + } + + return string.Empty; + } } public enum AzureRoleType : short diff --git a/Src/StackifyLib/Models/EnvironmentDetail.cs b/Src/StackifyLib/Models/EnvironmentDetail.cs index f16d4ff..9fbbd04 100644 --- a/Src/StackifyLib/Models/EnvironmentDetail.cs +++ b/Src/StackifyLib/Models/EnvironmentDetail.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2024 BMC Software, Inc. +// Copyright (c) 2024-2025 BMC Software, Inc. // Copyright (c) 2021-2024 Netreo // Copyright (c) 2019 Stackify @@ -30,9 +30,9 @@ public static EnvironmentDetail Get() return _CachedCopy ?? (_CachedCopy = new EnvironmentDetail()); } #if NETSTANDARD - private static System.Net.Http.HttpClient Client => new System.Net.Http.HttpClient(); + private System.Net.Http.HttpClient Client => new System.Net.Http.HttpClient(); #endif - private static bool registryAccessFailure = false; + private bool registryAccessFailure = false; /// /// Figures out if the server is in azure and if so gets the azure instance name @@ -41,9 +41,9 @@ private void GetAzureInfo() { //IsAzureWorkerRole is set when instantiating EnvironmentDetail //Useful in other parts directly referencing AzureInstanceName - if(!IsAzureWorkerRole && AzureConfig.InAzure && AzureConfig.IsWebsite) + if(!IsAzureWorkerRole && AzureConfig.Instance.InAzure && AzureConfig.Instance.IsWebsite) { - AzureInstanceName = AzureConfig.AzureInstanceName; + AzureInstanceName = AzureConfig.Instance.AzureInstanceName; } @@ -110,23 +110,23 @@ private void GetAzureInfo() #endif } - private static bool? _isIMDSv1; + private bool? _isIMDSv1; // http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-metadata.html#d0e30002 private const string EC2InstanceIdUrl = "http://169.254.169.254/latest/meta-data/instance-id"; private const string IMDS_BASE_URL = "http://169.254.169.254/latest"; private const string IMDS_TOKEN_PATH = "/api/token"; private const string IMDS_INSTANCE_ID_PATH = "/meta-data/instance-id"; private const string IMDSV1_BASE_URL = "http://169.254.169.254/latest/meta-data/"; - public static readonly object ec2InstanceLock = new object(); - private static DateTimeOffset? ec2InstanceIdLastUpdate = null; - private static string ec2InstanceId = null; + public static readonly object ec2InstanceLock = new object(); + private DateTimeOffset? ec2InstanceIdLastUpdate = null; + private string ec2InstanceId = string.Empty; /// /// Get the EC2 Instance name if it exists else null /// #if NETFULL - public static string GetDeviceName() + public string GetDeviceName() { var deviceName = Environment.GetEnvironmentVariable("STACKIFY_DEVICE_NAME"); if (!String.IsNullOrEmpty(deviceName)) @@ -139,7 +139,7 @@ public static string GetDeviceName() var isDefaultDeviceNameEc2 = IsEc2MachineName(deviceName); - if (Config.IsEc2 == null || Config.IsEc2 == true || isDefaultDeviceNameEc2) + if ((Config.IsEc2.HasValue && Config.IsEc2 == true) || isDefaultDeviceNameEc2) { var instanceID_task = GetEC2InstanceId(); if (string.IsNullOrWhiteSpace(instanceID_task) == false) @@ -151,7 +151,7 @@ public static string GetDeviceName() return deviceName.Substring(0, deviceName.Length > 60 ? 60 : deviceName.Length); } - public static bool IsIMDSv1() + public bool IsIMDSv1() { if (_isIMDSv1.HasValue) @@ -174,7 +174,7 @@ public static bool IsIMDSv1() } } - public static string GetAccessToken() + public string GetAccessToken() { var url = IMDS_BASE_URL + IMDS_TOKEN_PATH; var httpRequest = (HttpWebRequest)WebRequest.Create(url); @@ -190,7 +190,7 @@ public static string GetAccessToken() } - public static string GetEC2InstanceId() + public string GetEC2InstanceId() { string r = null; @@ -258,7 +258,7 @@ public static string GetEC2InstanceId() return r; } #else - public static string GetDeviceName() + public string GetDeviceName() { var deviceName = Environment.GetEnvironmentVariable("STACKIFY_DEVICE_NAME"); if (!String.IsNullOrEmpty(deviceName)) @@ -271,7 +271,7 @@ public static string GetDeviceName() var isDefaultDeviceNameEc2 = IsEc2MachineName(deviceName); - if (Config.IsEc2 == null || Config.IsEc2 == true || isDefaultDeviceNameEc2) + if ((Config.IsEc2.HasValue && Config.IsEc2 == true) || isDefaultDeviceNameEc2) { var instanceID_task = GetEC2InstanceId(); instanceID_task.Wait(); @@ -284,7 +284,7 @@ public static string GetDeviceName() return deviceName.Substring(0, deviceName.Length > 60 ? 60 : deviceName.Length); } - public static async Task GetAccessTokenAsync() + public async Task GetAccessTokenAsync() { var url = IMDS_BASE_URL + IMDS_TOKEN_PATH; var request = new System.Net.Http.HttpRequestMessage(System.Net.Http.HttpMethod.Put, url); @@ -294,7 +294,7 @@ public static async Task GetAccessTokenAsync() return await response.Content.ReadAsStringAsync().ConfigureAwait(false); } - public static async Task IsIMDSv1() + public async Task IsIMDSv1() { if (_isIMDSv1.HasValue) { @@ -314,7 +314,7 @@ public static async Task IsIMDSv1() } } - public static async Task GetEC2InstanceId() + public async Task GetEC2InstanceId() { string r = null; try @@ -345,7 +345,7 @@ public static async Task GetEC2InstanceId() } #endif - private static bool IsEc2MachineName(string machineName) + private bool IsEc2MachineName(string machineName) { if (string.IsNullOrWhiteSpace(machineName)) { diff --git a/Src/StackifyLib/Models/ErrorItem.cs b/Src/StackifyLib/Models/ErrorItem.cs index b14c7e1..4692b00 100644 --- a/Src/StackifyLib/Models/ErrorItem.cs +++ b/Src/StackifyLib/Models/ErrorItem.cs @@ -298,15 +298,15 @@ public string FramesToString() { StringBuilder sb = new StringBuilder(); - foreach (var item in this.StackTrace) + if (InnerError != null) { - sb.AppendFormat(" at {0}\r\n", item.Method); + sb.Append(InnerError.FramesToString()); + sb.Append("--- End of inner exception stack trace ---\r\n"); } - if (InnerError != null) + foreach (var item in this.StackTrace) { - sb.Append("--- End of inner exception stack trace ---\r\n"); - sb.Append(InnerError.FramesToString()); + sb.AppendFormat(" at {0}\r\n", item.Method); } return sb.ToString(); diff --git a/Src/StackifyLib/StackifyLib.csproj b/Src/StackifyLib/StackifyLib.csproj index 961f4eb..68a1a16 100644 --- a/Src/StackifyLib/StackifyLib.csproj +++ b/Src/StackifyLib/StackifyLib.csproj @@ -2,7 +2,8 @@ Stackify API - 2.2.14 + 2.2.18 + netstandard2.0;net40;net45;net451;net452;net46;net461;net462 StackifyLib StackifyLib @@ -13,15 +14,15 @@ false false false - 2.2.14-beta + 2.2.18 StackifyLib https://github.com/stackify/stackify-api-dotnet https://github.com/stackify/stackify-api-dotnet/blob/master/LICENSE https://github.com/stackify/stackify-api-dotnet git https://stackify.com/wp-content/uploads/2017/02/stk.png - 2.2.14.0 - 2.2.14.0 + 2.2.18.0 + 2.2.18.0 Remove default internal file logger false diff --git a/Src/StackifyLib/Utils/HttpClient.cs b/Src/StackifyLib/Utils/HttpClient.cs index c8619d2..729c6ac 100644 --- a/Src/StackifyLib/Utils/HttpClient.cs +++ b/Src/StackifyLib/Utils/HttpClient.cs @@ -1,4 +1,7 @@ -using System; +// Copyright (c) 2024-2025 BMC Software, Inc. +// Copyright (c) 2021-2024 Netreo +// Copyright (c) 2019 Stackify +using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; @@ -28,7 +31,9 @@ public class AppIdentityInfo public class HttpClient { - public static IWebProxy CustomWebProxy = null; + public static IWebProxy CustomWebProxy = null; + public static Action CustomRequestModifier = null; + public static bool IsUnitTest = false; public string BaseAPIUrl { get; private set; } @@ -80,9 +85,7 @@ public bool IsClientError() static HttpClient() { -#if NETFULL LoadWebProxyConfig(); -#endif } public HttpClient(string apiKey, string apiUrl) @@ -118,39 +121,31 @@ public HttpClient(string apiKey, string apiUrl) BaseAPIUrl += "/"; } -#if NETFULL public static void LoadWebProxyConfig() { try { - string val = Config.Get("Stackify.ProxyServer"); - + string val = Config.ProxyServer; if (!string.IsNullOrEmpty(val)) { - StackifyAPILogger.Log("Setting proxy server based on override config", true); - var uri = new Uri(val); - - var proxy = new WebProxy(uri, false); + WebProxy proxy = new WebProxy(uri); if (!string.IsNullOrEmpty(uri.UserInfo) && uri.UserInfo.Contains(":")) { string[] pieces = uri.UserInfo.Split(':'); - - proxy.Credentials = new NetworkCredential(pieces[0], pieces[1]); + proxy = new WebProxy(uri) + { + Credentials = new NetworkCredential(pieces[0], pieces[1]) + }; } else { - - string settingUseDefault = Config.Get("Stackify.ProxyUseDefaultCredentials"); - - bool useDefault; - - if (!string.IsNullOrEmpty(settingUseDefault) && bool.TryParse(settingUseDefault, out useDefault)) + bool? settingUseDefault = Config.ProxyUseDefaultCredentials; + if (settingUseDefault.HasValue && settingUseDefault.Value) { - //will make it use the user of the running windows service - proxy.UseDefaultCredentials = useDefault; + proxy.UseDefaultCredentials = true; } } CustomWebProxy = proxy; @@ -161,7 +156,6 @@ public static void LoadWebProxyConfig() StackifyAPILogger.Log("Error setting default web proxy " + ex.Message, true); } } -#endif /// /// This method does some throttling when errors happen to control when it should try again. Error backoff logic @@ -313,9 +307,9 @@ public bool IdentifyApp() } //Applicable only for Azure AppService - if(AzureConfig.InAzure && AzureConfig.IsWebsite) + if(AzureConfig.Instance.InAzure && AzureConfig.Instance.IsWebsite) { - env.DeviceName = AzureConfig.AzureInstanceName; + env.DeviceName = AzureConfig.Instance.AzureInstanceName; } string jsonData = JsonConvert.SerializeObject(env, new JsonSerializerSettings() { NullValueHandling = NullValueHandling.Ignore }); @@ -611,19 +605,35 @@ private HttpWebRequest BuildJsonRequest(string url, string jsonData, bool compre request.Headers[HttpRequestHeader.UserAgent] = "StackifyLib-" + _version; #endif - request.Headers["X-Stackify-Key"] = this.APIKey; + request.Headers.Add("X-Stackify-Key", this.APIKey); request.ContentType = "application/json"; - //if (HttpClient.CustomWebProxy != null) - //{ + if (CustomWebProxy != null) + { + request.Proxy = CustomWebProxy; + } - // request.Proxy = HttpClient.CustomWebProxy; - //} + if (CustomRequestModifier != null) + { + try + { + CustomRequestModifier(request); + } + catch (Exception ex) + { + StackifyAPILogger.Log($"Failed to set CustomRequestModifier - Message: {ex.Message}", true); + } + } if (!string.IsNullOrEmpty(jsonData) && compress) { request.Method = "POST"; - request.Headers[HttpRequestHeader.ContentEncoding] = "gzip"; + request.Headers.Add(HttpRequestHeader.ContentEncoding, "gzip"); + + if (IsUnitTest) + { + return request; + } byte[] payload = Encoding.UTF8.GetBytes(jsonData); @@ -644,8 +654,12 @@ private HttpWebRequest BuildJsonRequest(string url, string jsonData, bool compre { request.Method = "POST"; - byte[] payload = Encoding.UTF8.GetBytes(jsonData); + if (IsUnitTest) + { + return request; + } + byte[] payload = Encoding.UTF8.GetBytes(jsonData); #if NETFULL request.ContentLength= payload.Length; using (Stream stream = request.GetRequestStream()) @@ -694,14 +708,30 @@ private HttpWebRequest BuildPOSTRequest(string url, string postdata) request.Headers[HttpRequestHeader.ContentLength] = "0"; #endif - //if (HttpClient.CustomWebProxy != null) - //{ - // request.Proxy = HttpClient.CustomWebProxy; - //} + if (HttpClient.CustomWebProxy != null) + { + request.Proxy = HttpClient.CustomWebProxy; + } + if (CustomRequestModifier != null) + { + try + { + CustomRequestModifier(request); + } + catch (Exception ex) + { + StackifyAPILogger.Log($"Failed to set CustomRequestModifier - Message: {ex.Message}", true); + } + } request.Method = "POST"; + if (IsUnitTest) + { + return request; + } + if (!String.IsNullOrEmpty(postdata)) { byte[] payload = Encoding.UTF8.GetBytes(postdata); diff --git a/Src/StackifyLib/Utils/ServerConfigHelper.cs b/Src/StackifyLib/Utils/ServerConfigHelper.cs deleted file mode 100644 index 67834c6..0000000 --- a/Src/StackifyLib/Utils/ServerConfigHelper.cs +++ /dev/null @@ -1,34 +0,0 @@ -using StackifyLib.Models; -using System; -using System.Collections.Generic; -using System.Text; - -namespace StackifyLib.Utils -{ - public class ServerConfigHelper - { - public const string ProductionEnvName = "Production"; - - public static string GetEnvironment() - { - if (AzureConfig.IsWebsite) - { - var key = Environment.GetEnvironmentVariable("Stackify.Environment"); - - if (string.IsNullOrEmpty(key) == false) - { - return key; - } - - if (string.IsNullOrEmpty(AzureConfig.AzureAppWebConfigEnvironment) == false) - { - return AzureConfig.AzureAppWebConfigEnvironment; - } - - return ProductionEnvName; - } - - return string.Empty; - } - } -} diff --git a/Src/StackifyLibCore.sln b/Src/StackifyLibCore.sln index a726538..6f15531 100644 --- a/Src/StackifyLibCore.sln +++ b/Src/StackifyLibCore.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.30907.101 +# Visual Studio Version 17 +VisualStudioVersion = 17.11.35303.130 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StackifyLib", "StackifyLib\StackifyLib.csproj", "{03CF7411-65E2-4998-A157-9224ECA6199B}" EndProject @@ -71,11 +71,9 @@ Global {D0E1AABD-6397-4CC7-9737-4904A4BF5BCD}.Release|Any CPU.ActiveCfg = Release|Any CPU {D0E1AABD-6397-4CC7-9737-4904A4BF5BCD}.Release|Any CPU.Build.0 = Release|Any CPU {2B832B34-BA30-4B5A-8E78-D6DC78AF16FB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {2B832B34-BA30-4B5A-8E78-D6DC78AF16FB}.Debug|Any CPU.Build.0 = Debug|Any CPU {2B832B34-BA30-4B5A-8E78-D6DC78AF16FB}.Release|Any CPU.ActiveCfg = Release|Any CPU {2B832B34-BA30-4B5A-8E78-D6DC78AF16FB}.Release|Any CPU.Build.0 = Release|Any CPU {BC6FDF2B-E063-4F8B-ABBA-6BD9425D4C64}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {BC6FDF2B-E063-4F8B-ABBA-6BD9425D4C64}.Debug|Any CPU.Build.0 = Debug|Any CPU {BC6FDF2B-E063-4F8B-ABBA-6BD9425D4C64}.Release|Any CPU.ActiveCfg = Release|Any CPU {BC6FDF2B-E063-4F8B-ABBA-6BD9425D4C64}.Release|Any CPU.Build.0 = Release|Any CPU {CB09D719-F393-413D-B5CD-E0125543AD24}.Debug|Any CPU.ActiveCfg = Debug|Any CPU @@ -99,15 +97,12 @@ Global {647F27AD-13D7-40C2-A7BB-97A5D19CE3D5}.Release|Any CPU.ActiveCfg = Release|Any CPU {647F27AD-13D7-40C2-A7BB-97A5D19CE3D5}.Release|Any CPU.Build.0 = Release|Any CPU {ADBD2138-9DE7-4211-AF58-84C7F0F9BEA5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {ADBD2138-9DE7-4211-AF58-84C7F0F9BEA5}.Debug|Any CPU.Build.0 = Debug|Any CPU {ADBD2138-9DE7-4211-AF58-84C7F0F9BEA5}.Release|Any CPU.ActiveCfg = Release|Any CPU {ADBD2138-9DE7-4211-AF58-84C7F0F9BEA5}.Release|Any CPU.Build.0 = Release|Any CPU {99C81723-20FB-4241-862C-32F7ADDB7A9B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {99C81723-20FB-4241-862C-32F7ADDB7A9B}.Debug|Any CPU.Build.0 = Debug|Any CPU {99C81723-20FB-4241-862C-32F7ADDB7A9B}.Release|Any CPU.ActiveCfg = Release|Any CPU {99C81723-20FB-4241-862C-32F7ADDB7A9B}.Release|Any CPU.Build.0 = Release|Any CPU {EC5742DF-C1A8-4A7A-81DC-B6E0A4D3472D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {EC5742DF-C1A8-4A7A-81DC-B6E0A4D3472D}.Debug|Any CPU.Build.0 = Debug|Any CPU {EC5742DF-C1A8-4A7A-81DC-B6E0A4D3472D}.Release|Any CPU.ActiveCfg = Release|Any CPU {EC5742DF-C1A8-4A7A-81DC-B6E0A4D3472D}.Release|Any CPU.Build.0 = Release|Any CPU {18F96933-468E-4104-961E-0C68A5EF2B29}.Debug|Any CPU.ActiveCfg = Debug|Any CPU diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 5aa5fb7..1299979 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -16,14 +16,6 @@ pool: # name: Azure Pipelines parameters: - - name: RUN_VERACODE_SCAN - displayName: 'Upload to Veracode' - type: boolean - default: true - - name: RUN_VERACODE_SCA - displayName: 'Execute Veracode SCA' - type: boolean - default: true - name: 'Build StackifyLib' type: boolean default: true @@ -63,9 +55,21 @@ stages: - task: AzureKeyVault@2 displayName: 'Azure Key Vault: keyvault-build-resources' inputs: - ConnectedServiceName: 514ed7d6-3846-4422-8013-af27483dd22c + ConnectedServiceName: 5313674e-7e2b-426b-9eaf-8d3d5bd22e2c KeyVaultName: keyvault-build-resources RunAsPreJob: true + - task: DotNetCoreCLI@2 + displayName: dotnet restore + inputs: + command: restore + projects: | + Src\StackifyLib\*.csproj + Src\StackifyLib.AspNetCore\*.csproj + Src\StackifyLib.CoreLogger\*.csproj + Src\StackifyLib.log4net\*.csproj + Src\Nlog.Targets.Stackify\*.csproj + Src\NLog.Web.Stackify\*.csproj + Src\StackifyLib.StackifyTraceListener\*.csproj - ${{ if eq(parameters['Build StackifyLib Signed'], true) }}: - task: DownloadSecureFile@1 name: SNK @@ -94,19 +98,6 @@ stages: (gc $_) -replace ([regex]::Escape("//[assembly: AssemblyKeyFile")), "[assembly: AssemblyKeyFile" | Set-Content $_.FullName -Encoding utf8 } displayName: Uncoment signing attribute - - task: DotNetCoreCLI@2 - displayName: dotnet restore - inputs: - command: restore - projects: | - Src\StackifyLib\*.csproj - Src\StackifyLib.AspNetCore\*.csproj - Src\StackifyLib.CoreLogger\*.csproj - Src\StackifyLib.log4net\*.csproj - Src\Nlog.Targets.Stackify\*.csproj - Src\NLog.Web.Stackify\*.csproj - Src\StackifyLib.StackifyTraceListener\*.csproj - - ${{ if eq(parameters['Build StackifyLib'], true) }}: - task: DotNetCoreCLI@2 displayName: dotnet build inputs: @@ -119,7 +110,6 @@ stages: command: pack searchPatternPack: Src\StackifyLib\*.csproj; nobuild: true - - ${{ if eq(parameters['Build StackifyLib Signed'], true) }}: - task: DotNetCoreCLI@2 displayName: Install NuGetKeyVaultSignTool continueOnError: true @@ -184,6 +174,19 @@ stages: targetType: inline script: > (Get-Content -path Src\StackifyLib\StackifyLib.csproj -Raw) -replace 'StackifyLib.signed','StackifyLib' | Set-Content -Path Src\StackifyLib\StackifyLib.csproj + - ${{ if eq(parameters['Build StackifyLib'], true) }}: + - task: DotNetCoreCLI@2 + displayName: dotnet build + inputs: + projects: | + Src\StackifyLib\*.csproj + arguments: '-c $(BuildConfiguration)' + - task: DotNetCoreCLI@2 + displayName: dotnet pack unsigned stackify lib + inputs: + command: pack + searchPatternPack: Src\StackifyLib\*.csproj; + nobuild: true - ${{ if eq(parameters['Build StackifyLib.AspNetCore'], true) }}: - template: templates/build-and-pack.yml parameters: @@ -243,23 +246,4 @@ stages: $version = "$version".Trim() echo "##vso[task.setvariable variable=version]$version" echo "StackifyLib."$version".nupkg" - workingDirectory: $(Build.SourcesDirectory) - - task: Veracode@3 - displayName: 'Upload and scan: $(Build.ArtifactStagingDirectory)/StackifyLib.$(version).nupkg' - condition: and(succeeded(), eq('${{ parameters.RUN_VERACODE_SCAN }}', 'true')) - enabled: True - inputs: - AnalysisService: 51003f89-58ab-463c-8e20-41484888d9c7 - veracodeAppProfile: Retrace .Net StackifyLib - version: AZ-Devops-Build-$(build.buildNumber) - filepath: $(Build.ArtifactStagingDirectory)/StackifyLib.$(version).nupkg - - - job: SCA - pool: - vmImage: 'windows-latest' - steps: - - task: PowerShell@2 - inputs: - targetType: 'inline' - script: $Env:SRCCLR_API_TOKEN="$(SRCCLR_API_TOKEN)"; Set-ExecutionPolicy AllSigned -Scope Process -Force; iex ((New-Object System.Net.WebClient).DownloadString('https://download.srcclr.com/ci.ps1')); srcclr scan --allow-dirty .; - condition: and(succeeded(), eq('${{ parameters.RUN_VERACODE_SCA }}', 'true')) \ No newline at end of file + workingDirectory: $(Build.SourcesDirectory) \ No newline at end of file diff --git a/samples/CoreConsoleApp/CoreConsoleApp.csproj b/samples/CoreConsoleApp/CoreConsoleApp.csproj index 70540e5..0196127 100644 --- a/samples/CoreConsoleApp/CoreConsoleApp.csproj +++ b/samples/CoreConsoleApp/CoreConsoleApp.csproj @@ -13,7 +13,6 @@ - @@ -22,7 +21,8 @@ - + + diff --git a/samples/CoreWebApp/CoreWebApp.csproj b/samples/CoreWebApp/CoreWebApp.csproj index 9dc3766..c8ce504 100644 --- a/samples/CoreWebApp/CoreWebApp.csproj +++ b/samples/CoreWebApp/CoreWebApp.csproj @@ -16,7 +16,6 @@ - @@ -31,7 +30,8 @@ - + + diff --git a/test/StackifyLib.UnitTests/ConfigJSON_Tests.cs b/test/StackifyLib.UnitTests/ConfigJSON_Tests.cs index 3b4fbce..06d23de 100644 --- a/test/StackifyLib.UnitTests/ConfigJSON_Tests.cs +++ b/test/StackifyLib.UnitTests/ConfigJSON_Tests.cs @@ -1,3 +1,7 @@ +// Copyright (c) 2024 BMC Software, Inc. +// Copyright (c) 2021-2024 Netreo +// Copyright (c) 2019 Stackify +#if NETCORE using System; using System.Collections.Generic; using System.Diagnostics; @@ -125,3 +129,4 @@ private void ResetConfig() } } +#endif \ No newline at end of file diff --git a/test/StackifyLib.UnitTests/JsonSerialization_Tests.cs b/test/StackifyLib.UnitTests/JsonSerialization_Tests.cs index 68ca968..14803d5 100644 --- a/test/StackifyLib.UnitTests/JsonSerialization_Tests.cs +++ b/test/StackifyLib.UnitTests/JsonSerialization_Tests.cs @@ -1,3 +1,7 @@ +// Copyright (c) 2024 BMC Software, Inc. +// Copyright (c) 2021-2024 Netreo +// Copyright (c) 2019 Stackify +#if NETCORE using System; using System.Collections.Generic; using System.Diagnostics; @@ -134,3 +138,4 @@ private static object GetTestObject() } } } +#endif \ No newline at end of file diff --git a/test/StackifyLib.UnitTests/Models/AzureConfig_Tests.cs b/test/StackifyLib.UnitTests/Models/AzureConfig_Tests.cs new file mode 100644 index 0000000..7ef334e --- /dev/null +++ b/test/StackifyLib.UnitTests/Models/AzureConfig_Tests.cs @@ -0,0 +1,319 @@ +// Copyright (c) 2024-2025 BMC Software, Inc. +using StackifyLib.Models; +using System; +using System.Collections.Generic; +using Xunit; +using Moq; +#if NETCORE || NETCOREX +using Microsoft.Extensions.Configuration; +#endif + +namespace StackifyLib.UnitTests.Models +{ + public class AzureConfig_Tests + { + private readonly MockRepository _mockRepository; + private readonly Dictionary _originalEnvironment = new Dictionary(); + + public AzureConfig_Tests() + { + _mockRepository = new MockRepository(MockBehavior.Strict); + BackupEnvironmentVariables(); + } + + public void Dispose() + { + RestoreEnvironmentVariables(); + } + + private void BackupEnvironmentVariables() + { + _originalEnvironment["WEBSITE_SITE_NAME"] = Environment.GetEnvironmentVariable("WEBSITE_SITE_NAME"); + _originalEnvironment["WEBSITE_INSTANCE_ID"] = Environment.GetEnvironmentVariable("WEBSITE_INSTANCE_ID"); + _originalEnvironment["WEBSITE_IIS_SITE_NAME"] = Environment.GetEnvironmentVariable("WEBSITE_IIS_SITE_NAME"); + _originalEnvironment["Stackify.Environment"] = Environment.GetEnvironmentVariable("Stackify.Environment"); + } + + private void RestoreEnvironmentVariables() + { + foreach (var kvp in _originalEnvironment) + { + Environment.SetEnvironmentVariable(kvp.Key, kvp.Value); + } + } + + private void ResetAzureConfig() + { + var instanceField = typeof(AzureConfig).GetField("_instance", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static); + instanceField.SetValue(null, new AzureConfig()); +#if NETCORE || NETCOREX + Config.SetConfiguration(null); +#endif + } + + private void SetEnvironmentVariables(string siteName, string instanceId, string iisSiteName = null) + { + Environment.SetEnvironmentVariable("WEBSITE_SITE_NAME", siteName); + Environment.SetEnvironmentVariable("WEBSITE_INSTANCE_ID", instanceId); + Environment.SetEnvironmentVariable("WEBSITE_IIS_SITE_NAME", iisSiteName); + } + + [Fact] + public void InAzure_ReturnsTrue_WhenAzureEnvironmentDetected() + { + SetEnvironmentVariables("my-site", "instance-id"); + ResetAzureConfig(); + + var result = new AzureConfig(new EnvironmentDetail()).InAzure; + Assert.True(result); + } + + [Fact] + public void InAzure_ReturnsFalse_WhenNotInAzure() + { + SetEnvironmentVariables(null, null); + ResetAzureConfig(); + + var result = new AzureConfig(new EnvironmentDetail()).InAzure; + Assert.False(result); + } + + [Fact] + public void IsWebsite_ReturnsTrue_WhenWebAppDetected() + { + SetEnvironmentVariables("my-site", "instance-id"); + ResetAzureConfig(); + + var result = new AzureConfig(new EnvironmentDetail()).IsWebsite; + Assert.True(result); + } + + [Fact] + public void AzureInstanceName_CorrectFormat_WhenInWebApp() + { + SetEnvironmentVariables("my-site", "full-instance-id-12345", "site__1234"); + ResetAzureConfig(); + + var instanceName = new AzureConfig(new EnvironmentDetail()).AzureInstanceName; + Assert.Equal("my-site Production [1234-full-i]", instanceName); + } + + [Fact] + public void GetDeploymentSlotId_ReturnsLast4Chars_WhenSlotPresent() + { + SetEnvironmentVariables(null, null, "site-name__abcd"); + ResetAzureConfig(); + + var slotId = new AzureConfig(new EnvironmentDetail()).GetDeploymentSlotId(); + Assert.Equal("abcd", slotId); + } + + [Fact] + public void GetDeploymentSlotId_ReturnsNull_WhenNoSlotPresent() + { + SetEnvironmentVariables(null, null, "site-name"); + ResetAzureConfig(); + + var slotId = new AzureConfig(new EnvironmentDetail()).GetDeploymentSlotId(); + Assert.Null(slotId); + } + + [Fact(Skip = "Environment set is dynamic. Skip and run manually")] + public void GetEnvironment_ReturnsConfiguredEnvironment_WhenSet() + { + SetEnvironmentVariables("site", "instance"); + Environment.SetEnvironmentVariable("Stackify.Environment", "Staging"); + ResetAzureConfig(); + + var env = new AzureConfig(new EnvironmentDetail()).GetEnvironment(); + Assert.Equal("Staging", env); + } + + [Fact(Skip = "Environment set is dynamic. Skip and run manually")] + public void AzureInstanceName_CorrectFormat_WhenEnvStagingTestSet() + { + Environment.SetEnvironmentVariable("Stackify.Environment", "StagingTest"); + SetEnvironmentVariables("my-site", "full-instance-id-12345", "site__1234"); + ResetAzureConfig(); + + var instanceName = new AzureConfig(new EnvironmentDetail()).AzureInstanceName; + Assert.Equal("my-site StagingTest [1234-full-i]", instanceName); + } + + [Fact] + public void GetEnvironment_ReturnsProduction_WhenNoConfigurationSet() + { + SetEnvironmentVariables("site", "instance"); + ResetAzureConfig(); + Config.LoadSettings(); + + var env = new AzureConfig(new EnvironmentDetail()).GetEnvironment(); + Assert.Equal("Production", env); + } + + [Fact] + public void GetEnvironment_ReturnsEmptyString_WhenNotWebsite() + { + SetEnvironmentVariables(null, null); // Not in Azure + ResetAzureConfig(); + + var env = new AzureConfig().GetEnvironment(); + Assert.Equal(string.Empty, env); + } + +#if NETFULL + [Fact(Skip = "Environment set is dynamic. Skip and run manually")] + public void GetEnvironment_ReturnsTest_WhenAppSettingsSet() + { + Environment.SetEnvironmentVariable("Stackify.Environment", ""); + System.Configuration.ConfigurationManager.AppSettings["Stackify.Environment"] = "Test"; + SetEnvironmentVariables("site", "instance"); + ResetAzureConfig(); + + var env = new AzureConfig(new EnvironmentDetail()).GetEnvironment(); + Assert.Equal("Test", env); + } + + [Fact(Skip = "Environment set is dynamic. Skip and run manually")] + public void GetEnvironment_ReturnsTest_WhenEnvSetFirst_AppSettingsSet() + { + Environment.SetEnvironmentVariable("Stackify.Environment", "StagingFirstApp"); + System.Configuration.ConfigurationManager.AppSettings["Stackify.Environment"] = "Test"; + SetEnvironmentVariables("site", "instance"); + ResetAzureConfig(); + + var env = new AzureConfig(new EnvironmentDetail()).GetEnvironment(); + Assert.Equal("StagingFirstApp", env); + } + + [Fact(Skip = "Environment set is dynamic. Skip and run manually")] + public void AzureInstanceName_CorrectFormat_WhenAppSettingsEnvStagingAppSet() + { + Environment.SetEnvironmentVariable("Stackify.Environment", ""); + System.Configuration.ConfigurationManager.AppSettings["Stackify.Environment"] = "StagingApp"; + SetEnvironmentVariables("my-site", "full-instance-id-12345", "site__1234"); + ResetAzureConfig(); + + var instanceName = new AzureConfig(new EnvironmentDetail()).AzureInstanceName; + Assert.Equal("my-site StagingApp [1234-full-i]", instanceName); + } + + [Fact(Skip = "Environment set is dynamic. Skip and run manually")] + public void AzureInstanceName_CorrectFormat_WhenEnvSetFirst_AppSettings() + { + Environment.SetEnvironmentVariable("Stackify.Environment", "StagingFirst"); + System.Configuration.ConfigurationManager.AppSettings["Stackify.Environment"] = "StagingApp"; + SetEnvironmentVariables("my-site", "full-instance-id-12345", "site__1234"); + ResetAzureConfig(); + + var instanceName = new AzureConfig(new EnvironmentDetail()).AzureInstanceName; + Assert.Equal("my-site StagingFirst [1234-full-i]", instanceName); + } +#endif + +#if NETCORE || NETCOREX + [Fact(Skip = "Environment set is dynamic. Skip and run manually")] + public void GetEnvironment_ReturnsTestCore_WhenIConfigurationSet() + { + Environment.SetEnvironmentVariable("Stackify.Environment", ""); + SetEnvironmentVariables("site", "instance"); + ResetAzureConfig(); + + var iconfig = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + ["Stackify:Environment"] = "TestCore" + }) + .Build(); + Config.SetConfiguration(iconfig); + Config.LoadSettings(); + + var env = new AzureConfig(new EnvironmentDetail()).GetEnvironment(); + Assert.Equal("TestCore", env); + } + + [Fact(Skip = "Environment set is dynamic. Skip and run manually")] + public void GetEnvironment_ReturnsTestCore_WhenEnvFirst_IConfigurationSet() + { + Environment.SetEnvironmentVariable("Stackify.Environment", "TestCoreFirst"); + SetEnvironmentVariables("site", "instance"); + ResetAzureConfig(); + + var iconfig = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + ["Stackify:Environment"] = "TestCore" + }) + .Build(); + Config.SetConfiguration(iconfig); + Config.LoadSettings(); + + var env = new AzureConfig(new EnvironmentDetail()).GetEnvironment(); + Assert.Equal("TestCoreFirst", env); + } + + [Fact(Skip = "Environment set is dynamic. Skip and run manually")] + public void AzureInstanceName_CorrectFormat_WhenEnvSetFirst_IConfigurationEnvStagingConfigSet() + { + Environment.SetEnvironmentVariable("Stackify.Environment", "StagingConfigFirst"); + SetEnvironmentVariables("my-site", "full-instance-id-12345", "site__1234"); + ResetAzureConfig(); + + var iconfig = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + ["Stackify:Environment"] = "StagingConfig" + }) + .Build(); + Config.SetConfiguration(iconfig); + Config.LoadSettings(); + + var instanceName = new AzureConfig(new EnvironmentDetail()).AzureInstanceName; + Assert.Equal("my-site StagingConfigFirst [1234-full-i]", instanceName); + } + + [Fact(Skip = "Environment set is dynamic. Skip and run manually")] + public void AzureInstanceName_CorrectFormat_WhenIConfigurationEnvStagingConfigSet() + { + Environment.SetEnvironmentVariable("Stackify.Environment", ""); + SetEnvironmentVariables("my-site", "full-instance-id-12345", "site__1234"); + ResetAzureConfig(); + + var iconfig = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + ["Stackify:Environment"] = "StagingConfig" + }) + .Build(); + Config.SetConfiguration(iconfig); + Config.LoadSettings(); + + var instanceName = new AzureConfig(new EnvironmentDetail()).AzureInstanceName; + Assert.Equal("my-site StagingConfig [1234-full-i]", instanceName); + } +#endif + + [Fact] + public void AzureRoleType_SetToWebApp_WhenWebsiteDetected() + { + // Arrange + SetEnvironmentVariables("site", "instance"); + ResetAzureConfig(); + + // Act + bool _ = AzureConfig.Instance.InAzure; // Force initialization + var roleType = GetPrivateField("_azureRoleType"); + + // Assert + Assert.Equal(AzureRoleType.WebApp, roleType); + } + + // Helper to access private fields + private T GetPrivateField(string fieldName) + { + var field = typeof(AzureConfig).GetField(fieldName, + System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); + return (T)field.GetValue(AzureConfig.Instance); + } + } +} diff --git a/test/StackifyLib.UnitTests/Models/EnvironmentDetail_Tests.cs b/test/StackifyLib.UnitTests/Models/EnvironmentDetail_Tests.cs new file mode 100644 index 0000000..c571ec0 --- /dev/null +++ b/test/StackifyLib.UnitTests/Models/EnvironmentDetail_Tests.cs @@ -0,0 +1,43 @@ +// Copyright (c) 2024 BMC Software, Inc. +// Copyright (c) 2021-2024 Netreo +// Copyright (c) 2019 Stackify +using StackifyLib.Models; +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using Xunit; +using Moq; + +namespace StackifyLib.UnitTests.Models +{ + public class EnvironmentDetail_Tests + { +#if NETCORE + [Fact] + public async Task TestIfConfigEc2IsNull() + { + var envService = new EnvironmentDetail(); + Assert.Equal(Environment.MachineName, envService.GetDeviceName()); + + var type = envService.GetType(); + var property = type.GetField("ec2InstanceId", BindingFlags.NonPublic | BindingFlags.Instance); + Assert.Equal(property.GetValue(envService) as string, string.Empty); + } +#endif +#if NETFULL + [Fact] + public void TestIfConfigEc2IsNull() + { + var envService = new EnvironmentDetail(); + Assert.Equal(Environment.MachineName, envService.GetDeviceName()); + + var type = envService.GetType(); + var property = type.GetField("ec2InstanceId", BindingFlags.NonPublic | BindingFlags.Instance); + Assert.Equal(property.GetValue(envService) as string, string.Empty); + } +#endif + } +} diff --git a/test/StackifyLib.UnitTests/Models/ErrorItem_Tests.cs b/test/StackifyLib.UnitTests/Models/ErrorItem_Tests.cs new file mode 100644 index 0000000..633b046 --- /dev/null +++ b/test/StackifyLib.UnitTests/Models/ErrorItem_Tests.cs @@ -0,0 +1,78 @@ +// Copyright (c) 2024 BMC Software, Inc. +// Copyright (c) 2021-2024 Netreo +// Copyright (c) 2019 Stackify +using StackifyLib.Models; +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using Xunit; +using Moq; + +namespace StackifyLib.UnitTests.Models +{ + public class ErrorItem_Tests + { + + [Fact] + public async Task TestExceptionStringWithInnerException() + { + var expectedString = string.Join( + Environment.NewLine, + "System.Exception: Wrapping the exception to see the differences between outer and inner exceptions. ---> System.Exception: Throwing this exception to see all properties like stacktrace filled", + " at StackifyLib.UnitTests.Models.ExampleService.ThrowInnerException()", + " at StackifyLib.UnitTests.Models.ExampleService.CreateCaughtException()", + "--- End of inner exception stack trace ---", + " at StackifyLib.UnitTests.Models.ExampleService.ThrowOuterException(System.Exception)", + " at StackifyLib.UnitTests.Models.ExampleService.CreateCaughtException()", + " at StackifyLib.UnitTests.Models.ErrorItem_Tests+d__0.MoveNext()", + "" + ); + try + { + //ExampleService service = new ExampleService(); + ExampleService.CreateCaughtException(); + } + catch (Exception ex) + { + //PrintExceptionInfo(ex); + var exceptionString = new ErrorItem(ex).ToString(); + Assert.Contains(expectedString, exceptionString); + } + } + } + + class ExampleService + { + public static Exception CreateCaughtException() + { + try + { + try + { + ThrowInnerException(); + } + catch (Exception exception) + { + ThrowOuterException(exception); + } + } + catch (Exception) + { + throw; + } + // NOTE: never reached, but needed for the compiler. + return null; + } + private static void ThrowOuterException(Exception exception) + { + throw new Exception("Wrapping the exception to see the differences between outer and inner exceptions.", exception); + } + private static void ThrowInnerException() + { + throw new Exception("Throwing this exception to see all properties like stacktrace filled"); + } + } +} diff --git a/test/StackifyLib.UnitTests/StackifyLib.UnitTests.csproj b/test/StackifyLib.UnitTests/StackifyLib.UnitTests.csproj index 4408082..d9c07c3 100644 --- a/test/StackifyLib.UnitTests/StackifyLib.UnitTests.csproj +++ b/test/StackifyLib.UnitTests/StackifyLib.UnitTests.csproj @@ -1,16 +1,25 @@  - netcoreapp2.1 + net6.0;net462 + false + true + + NETCORE + + + + NETFULL + + - - - all - runtime; build; native; contentfiles; analyzers - + + + + diff --git a/test/StackifyLib.UnitTests/Utils/HttpClient_Loading_Tests.cs b/test/StackifyLib.UnitTests/Utils/HttpClient_Loading_Tests.cs new file mode 100644 index 0000000..4a3316d --- /dev/null +++ b/test/StackifyLib.UnitTests/Utils/HttpClient_Loading_Tests.cs @@ -0,0 +1,55 @@ +// Copyright (c) 2024-2025 BMC Software, Inc. +// Copyright (c) 2021-2024 Netreo +// Copyright (c) 2019 Stackify +using Moq; +using StackifyLib.Models; +using StackifyLib.Utils; +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Reflection; +using System.Security.Policy; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +namespace StackifyLib.UnitTests.Utils +{ + public class HttpClient_Loading_Tests : IDisposable + { + private readonly HttpClient _httpClient; + + public HttpClient_Loading_Tests() + { + // Store original values to restore after tests + Config.ProxyServer = "http://testuser:testpass@docker-hyperv.local:8881"; + _httpClient = new HttpClient("test-api-key", "https://test.api.com/"); + HttpClient.IsUnitTest = true; + } + + public void Dispose() + { + HttpClient.IsUnitTest = false; + } + + [Fact(Skip = "This test should be run explicitly (due to static loading nature of HttpClient)")] + public void LoadWebProxyConfig_DefaultBehavior() + { + // Arrange - Mock the Config.Get method to return true for UseDefaultCredentials + try + { + var customProxy = HttpClient.CustomWebProxy as WebProxy; + Assert.NotNull(customProxy); + + // Note: We can't easily test UseDefaultCredentials without mocking Config.Get + // This test verifies the method runs without error when proxy is configured + Assert.Equal("docker-hyperv.local", customProxy.Address.Host); + Assert.Equal(8881, customProxy.Address.Port); + } + finally + { + } + } + } +} diff --git a/test/StackifyLib.UnitTests/Utils/HttpClient_Tests.cs b/test/StackifyLib.UnitTests/Utils/HttpClient_Tests.cs new file mode 100644 index 0000000..102160c --- /dev/null +++ b/test/StackifyLib.UnitTests/Utils/HttpClient_Tests.cs @@ -0,0 +1,860 @@ +// Copyright (c) 2024-2025 BMC Software, Inc. +// Copyright (c) 2021-2024 Netreo +// Copyright (c) 2019 Stackify +using Moq; +using StackifyLib.Models; +using StackifyLib.Utils; +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Reflection; +using System.Security.Policy; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +namespace StackifyLib.UnitTests.Utils +{ + public class HttpClient_Tests : IDisposable + { + private readonly HttpClient _httpClient; + private IWebProxy _originalProxy; + private Action _originalRequestModifier; + + public HttpClient_Tests() + { + // Store original values to restore after tests + _originalProxy = HttpClient.CustomWebProxy; + _originalRequestModifier = HttpClient.CustomRequestModifier; + + _httpClient = new HttpClient("test-api-key", "https://test.api.com/"); + HttpClient.IsUnitTest = true; + } + + public void Dispose() + { + // Restore original values + HttpClient.CustomWebProxy = _originalProxy; + HttpClient.CustomRequestModifier = _originalRequestModifier; + HttpClient.IsUnitTest = false; + } + + [Fact] + public void Constructor_SetsBaseAPIUrl_WithTrailingSlash() + { + var client = new HttpClient("test-key", "https://api.test.com"); + + Assert.Equal("https://api.test.com/", client.BaseAPIUrl); + } + + [Fact] + public void Constructor_PreservesTrailingSlash_WhenAlreadyPresent() + { + var client = new HttpClient("test-key", "https://api.test.com/"); + + Assert.Equal("https://api.test.com/", client.BaseAPIUrl); + } + + [Fact] + public void CustomWebProxy_CanBeSetAndRetrieved() + { + var testProxy = new WebProxy("http://proxy.test.com:8080"); + + HttpClient.CustomWebProxy = testProxy; + + Assert.Equal(testProxy, HttpClient.CustomWebProxy); + } + + [Fact] + public void CustomRequestModifier_CanBeSetAndRetrieved() + { + var testModifier = new Action(req => req.Timeout = 30000); + + HttpClient.CustomRequestModifier = testModifier; + + Assert.Equal(testModifier, HttpClient.CustomRequestModifier); + } + + [Fact] + public void CustomRequestModifier_ModifiesRequest() + { + bool modifierCalled = false; + string capturedUserAgent = null; + + HttpClient.CustomRequestModifier = (request) => + { + modifierCalled = true; + capturedUserAgent = request.Headers["User-Agent"]; + request.Timeout = 30000; + }; + + // Use reflection to test the BuildJsonRequest method + var method = typeof(HttpClient).GetMethod("BuildJsonRequest", BindingFlags.NonPublic | BindingFlags.Instance); + Assert.NotNull(method); + + var finalRequest = (HttpWebRequest)method.Invoke(_httpClient, new object[] { "https://test.api.com/test", "{}", false }); + + Assert.True(modifierCalled); + Assert.Equal(30000, finalRequest.Timeout); + Assert.NotNull(capturedUserAgent); + } + + [Fact] + public void CustomRequestModifier_ModifiesPOSTRequest() + { + bool modifierCalled = false; + + HttpClient.CustomRequestModifier = (request) => + { + modifierCalled = true; + request.Timeout = 45000; + }; + + // Use reflection to test the BuildPOSTRequest method + var method = typeof(HttpClient).GetMethod("BuildPOSTRequest", BindingFlags.NonPublic | BindingFlags.Instance); + Assert.NotNull(method); + + var finalRequest = (HttpWebRequest)method.Invoke(_httpClient, new object[] { "https://test.api.com/test", "data=test" }); + + Assert.True(modifierCalled); + Assert.Equal(45000, finalRequest.Timeout); + } + + [Fact] + public void BuildJsonRequest_SetsCustomWebProxy_WhenProvided() + { + var testProxy = new WebProxy("http://proxy1.test.com:8080"); + HttpClient.CustomWebProxy = testProxy; + + // Use reflection to access private method + var method = typeof(HttpClient).GetMethod("BuildJsonRequest", BindingFlags.NonPublic | BindingFlags.Instance); + Assert.NotNull(method); + + var request = (HttpWebRequest)method.Invoke(_httpClient, new object[] { "https://test.api.com/test", "{}", false }); + + Assert.Equal(testProxy, request.Proxy); + } + + [Fact] + public void BuildPOSTRequest_SetsCustomWebProxy_WhenProvided() + { + var testProxy = new WebProxy("http://proxy.test.com:8080"); + HttpClient.CustomWebProxy = testProxy; + + // Use reflection to access private method + var method = typeof(HttpClient).GetMethod("BuildPOSTRequest", BindingFlags.NonPublic | BindingFlags.Instance); + Assert.NotNull(method); + + var request = (HttpWebRequest)method.Invoke(_httpClient, new object[] { "https://test.api.com/test", "data=test" }); + + Assert.Equal(testProxy, request.Proxy); + } + + [Fact] + public void BuildJsonRequest_DoesNotSetProxy_WhenCustomWebProxyIsNull() + { + HttpClient.CustomWebProxy = null; + + // Use reflection to access private method + var method = typeof(HttpClient).GetMethod("BuildJsonRequest", BindingFlags.NonPublic | BindingFlags.Instance); + Assert.NotNull(method); + + var request = (HttpWebRequest)method.Invoke(_httpClient, new object[] { "https://test.api.com/test", "{}", false }); + + // When CustomWebProxy is null, the request should use default proxy settings + Assert.NotEqual(HttpClient.CustomWebProxy, request.Proxy); + } + + [Fact] + public void BuildJsonRequest_SetsCorrectHeaders() + { + // Use reflection to access private method + var method = typeof(HttpClient).GetMethod("BuildJsonRequest", BindingFlags.NonPublic | BindingFlags.Instance); + Assert.NotNull(method); + + var request = (HttpWebRequest)method.Invoke(_httpClient, new object[] { "https://test.api.com/test", "{\"test\": \"data\"}", false }); + + Assert.Equal("application/json", request.ContentType); + Assert.Equal("test-api-key", request.Headers["X-Stackify-Key"]); + Assert.Equal("POST", request.Method); +#if NETFULL + Assert.Contains("StackifyLib-", request.UserAgent); +#else + Assert.Contains("StackifyLib-", request.Headers["User-Agent"]); +#endif + } + + [Fact] + public void BuildJsonRequest_WithCompression_SetsCorrectHeaders() + { + // Use reflection to access private method + var method = typeof(HttpClient).GetMethod("BuildJsonRequest", BindingFlags.NonPublic | BindingFlags.Instance); + Assert.NotNull(method); + + var request = (HttpWebRequest)method.Invoke(_httpClient, new object[] { "https://test.api.com/test", "{\"test\": \"data\"}", true }); + + Assert.Equal("application/json", request.ContentType); + Assert.Equal("gzip", request.Headers["Content-Encoding"]); + Assert.Equal("POST", request.Method); + } + + [Fact] + public void BuildJsonRequest_WithEmptyJsonData_UsesGETMethod() + { + // Use reflection to access private method + var method = typeof(HttpClient).GetMethod("BuildJsonRequest", BindingFlags.NonPublic | BindingFlags.Instance); + Assert.NotNull(method); + + var request = (HttpWebRequest)method.Invoke(_httpClient, new object[] { "https://test.api.com/test", "", false }); + + Assert.Equal("GET", request.Method); + } + + [Fact] + public void BuildPOSTRequest_SetsCorrectHeaders() + { + // Use reflection to access private method + var method = typeof(HttpClient).GetMethod("BuildPOSTRequest", BindingFlags.NonPublic | BindingFlags.Instance); + Assert.NotNull(method); + + var request = (HttpWebRequest)method.Invoke(_httpClient, new object[] { "https://test.api.com/test", "data=test" }); + + Assert.Equal("application/x-www-form-urlencoded", request.ContentType); + Assert.Equal("test-api-key", request.Headers["X-Stackify-Key"]); + Assert.Equal("POST", request.Method); +#if NETFULL + Assert.Contains("StackifyLib-", request.UserAgent); +#else + Assert.Contains("StackifyLib-", request.Headers["User-Agent"]); +#endif + } + + [Fact] + public void ProxyAndCustomModifier_WorkTogether() + { + var testProxy = new WebProxy("http://proxy.test.com:8080"); + HttpClient.CustomWebProxy = testProxy; + + bool modifierCalled = false; + HttpClient.CustomRequestModifier = (request) => + { + modifierCalled = true; + request.Timeout = 60000; + }; + + // Use reflection to access private method + var method = typeof(HttpClient).GetMethod("BuildJsonRequest", BindingFlags.NonPublic | BindingFlags.Instance); + Assert.NotNull(method); + + var finalRequest = (HttpWebRequest)method.Invoke(_httpClient, new object[] { "https://test.api.com/test", "{}", false }); + + Assert.Equal(testProxy, finalRequest.Proxy); + Assert.True(modifierCalled); + Assert.Equal(60000, finalRequest.Timeout); + } + + [Fact] + public void CustomRequestModifier_DoesNotThrow_WhenNull() + { + HttpClient.CustomRequestModifier = null; + + // Use reflection to access private method + var method = typeof(HttpClient).GetMethod("BuildJsonRequest", BindingFlags.NonPublic | BindingFlags.Instance); + Assert.NotNull(method); + + // Should not throw exception when CustomRequestModifier is null + var exception = Record.Exception(() => + method.Invoke(_httpClient, new object[] { "https://test.api.com/test", "{}", false }) + ); + + Assert.Null(exception); + } + +#if NETFULL + [Fact] + public void LoadWebProxyConfig_SetsCustomWebProxy_FromConfig() + { + // This test would need to mock the Config.ProxyServer property + // For now, we'll test that the method exists and can be called + var method = typeof(HttpClient).GetMethod("LoadWebProxyConfig", BindingFlags.Public | BindingFlags.Static); + Assert.NotNull(method); + + // Should not throw exception + var exception = Record.Exception(() => method.Invoke(null, null)); + Assert.Null(exception); + } +#endif + + [Theory] + [InlineData("http://proxy.example.com:8080")] + [InlineData("https://secure-proxy.example.com:443", true)] + [InlineData("http://user:pass@proxy.example.com:3128")] + public void CustomWebProxy_AcceptsDifferentProxyUrls(string proxyUrl, bool throwsException = false) + { + var testProxy = new WebProxy(proxyUrl); + HttpClient.CustomWebProxy = testProxy; + + // Use reflection to access private method + var method = typeof(HttpClient).GetMethod("BuildJsonRequest", BindingFlags.NonPublic | BindingFlags.Instance); + Assert.NotNull(method); + + if (throwsException) + { +#if NETFULL + Assert.Throws(() => (HttpWebRequest)method.Invoke(_httpClient, new object[] { "https://test.api.com/test", "{}", false })); +#else + var request = (HttpWebRequest)method.Invoke(_httpClient, new object[] { "https://test.api.com/test", "{}", false }); + Assert.Equal(testProxy, request.Proxy); +#endif + } + else + { + var request = (HttpWebRequest)method.Invoke(_httpClient, new object[] { "https://test.api.com/test", "{}", false }); + Assert.Equal(testProxy, request.Proxy); + } + } + + [Fact] + public void CustomRequestModifier_CanModifyMultipleProperties() + { + string customHeaderValue = "CustomValue123"; + + HttpClient.CustomRequestModifier = (request) => + { + request.Timeout = 120000; + request.Headers.Add("X-Custom-Header", customHeaderValue); + request.KeepAlive = false; + }; + + // Use reflection to access private method + var method = typeof(HttpClient).GetMethod("BuildJsonRequest", BindingFlags.NonPublic | BindingFlags.Instance); + Assert.NotNull(method); + + var finalRequest = (HttpWebRequest)method.Invoke(_httpClient, new object[] { "https://test.api.com/test", "{}", false }); + + Assert.Equal(120000, finalRequest.Timeout); + Assert.Equal(customHeaderValue, finalRequest.Headers["X-Custom-Header"]); + Assert.False(finalRequest.KeepAlive); + } + + [Fact] + public void ProxyConfiguration_RespectsOrder_ProxyThenCustomModifier() + { + var testProxy = new WebProxy("http://proxy.test.com:8080"); + HttpClient.CustomWebProxy = testProxy; + + // Custom modifier that verifies proxy was set first + bool proxyWasSetBeforeModifier = false; + HttpClient.CustomRequestModifier = (request) => + { + proxyWasSetBeforeModifier = (request.Proxy == testProxy); + request.Timeout = 25000; + }; + + // Use reflection to access private method + var method = typeof(HttpClient).GetMethod("BuildJsonRequest", BindingFlags.NonPublic | BindingFlags.Instance); + Assert.NotNull(method); + + var finalRequest = (HttpWebRequest)method.Invoke(_httpClient, new object[] { "https://test.api.com/test", "{}", false }); + + Assert.True(proxyWasSetBeforeModifier, "Proxy should be set before CustomRequestModifier is called"); + Assert.Equal(testProxy, finalRequest.Proxy); + Assert.Equal(25000, finalRequest.Timeout); + } + + [Fact] + public void CustomRequestModifier_CanOverrideProxySettings() + { + var initialProxy = new WebProxy("http://initial-proxy.test.com:8080"); + var overrideProxy = new WebProxy("http://override-proxy.test.com:3128"); + + HttpClient.CustomWebProxy = initialProxy; + + HttpClient.CustomRequestModifier = (request) => + { + // Override the proxy set by the framework + request.Proxy = overrideProxy; + }; + + // Use reflection to access private method + var method = typeof(HttpClient).GetMethod("BuildJsonRequest", BindingFlags.NonPublic | BindingFlags.Instance); + Assert.NotNull(method); + + var finalRequest = (HttpWebRequest)method.Invoke(_httpClient, new object[] { "https://test.api.com/test", "{}", false }); + + Assert.Equal(overrideProxy, finalRequest.Proxy); + Assert.NotEqual(initialProxy, finalRequest.Proxy); + } + + [Fact] + public void BuildPOSTRequest_WithoutProxy_DoesNotSetProxy() + { + HttpClient.CustomWebProxy = null; + + // Use reflection to access private method + + HttpWebRequest requestDefault = (HttpWebRequest)WebRequest.Create("http://test.com"); + var method = typeof(HttpClient).GetMethod("BuildPOSTRequest", BindingFlags.NonPublic | BindingFlags.Instance); + Assert.NotNull(method); + + var request = (HttpWebRequest)method.Invoke(_httpClient, new object[] { "https://test.api.com/test", "data=test" }); + Assert.Equivalent(request.Proxy, requestDefault.Proxy); + } + + [Theory] + [InlineData("http://proxy.test.com:8080", "")] + [InlineData("http://proxy.test.com:8080", "data=test")] + [InlineData("https://secure-proxy.test.com:443", "param1=value1¶m2=value2", true)] + public void BuildPOSTRequest_WithDifferentProxyAndData_SetsProxyCorrectly(string proxyUrl, string postData, bool throwsException = false) + { + var testProxy = new WebProxy(proxyUrl); + HttpClient.CustomWebProxy = testProxy; + + // Use reflection to access private method + var method = typeof(HttpClient).GetMethod("BuildPOSTRequest", BindingFlags.NonPublic | BindingFlags.Instance); + Assert.NotNull(method); + + + if (throwsException) + { +#if NETFULL + Assert.Throws(() => (HttpWebRequest)method.Invoke(_httpClient, new object[] { "https://test.api.com/test", postData })); +#else + var request = (HttpWebRequest)method.Invoke(_httpClient, new object[] { "https://test.api.com/test", postData }); + Assert.Equal(testProxy, request.Proxy); + Assert.Equal("POST", request.Method); + Assert.Equal("application/x-www-form-urlencoded", request.ContentType); +#endif + } + else + { + var request = (HttpWebRequest)method.Invoke(_httpClient, new object[] { "https://test.api.com/test", postData }); + Assert.Equal(testProxy, request.Proxy); + Assert.Equal("POST", request.Method); + Assert.Equal("application/x-www-form-urlencoded", request.ContentType); + } + + } + + [Fact] + public void APIKey_Property_ReturnsCorrectValue() + { + var client = new HttpClient("test-api-key", "https://test.api.com/"); + + // Access the APIKey property + Assert.Equal("test-api-key", client.APIKey); + } + + [Fact] + public void StackifyWebResponse_IsClientError_ReturnsCorrectValues() + { + var response = new HttpClient.StackifyWebResponse(); + + // Test 4xx status codes (client errors) + response.StatusCode = HttpStatusCode.BadRequest; + Assert.True(response.IsClientError()); + + response.StatusCode = HttpStatusCode.Unauthorized; + Assert.True(response.IsClientError()); + + response.StatusCode = HttpStatusCode.NotFound; + Assert.True(response.IsClientError()); + + // Test non-4xx status codes + response.StatusCode = HttpStatusCode.OK; + Assert.False(response.IsClientError()); + + response.StatusCode = HttpStatusCode.InternalServerError; + Assert.False(response.IsClientError()); + + response.StatusCode = HttpStatusCode.BadGateway; + Assert.False(response.IsClientError()); + } + + [Fact] + public void LoadWebProxyConfig_SetsUseDefaultCredentials_WhenConfigIsTrue() + { + // Arrange - Mock the Config.Get method to return true for UseDefaultCredentials + var originalProxyServer = Config.ProxyServer; + var configType = typeof(Config); + var proxyServerProperty = configType.GetProperty("ProxyServer", BindingFlags.Public | BindingFlags.Static); + + try + { + // Set up a proxy server without credentials in URL + proxyServerProperty.SetValue(null, "http://proxy.test.com:8080"); + + // We'll test this by calling LoadWebProxyConfig and checking the result + HttpClient.LoadWebProxyConfig(); + + var customProxy = HttpClient.CustomWebProxy as WebProxy; + Assert.NotNull(customProxy); + + // Note: We can't easily test UseDefaultCredentials without mocking Config.Get + // This test verifies the method runs without error when proxy is configured + Assert.Equal("proxy.test.com", customProxy.Address.Host); + Assert.Equal(8080, customProxy.Address.Port); + } + finally + { + // Restore original state + proxyServerProperty.SetValue(null, originalProxyServer); + HttpClient.CustomWebProxy = null; + } + } + + [Fact] + public void LoadWebProxyConfig_SetsCredentials_WhenProxyHasUserInfo() + { + // Arrange + var originalProxyServer = Config.ProxyServer; + var configType = typeof(Config); + var proxyServerProperty = configType.GetProperty("ProxyServer", BindingFlags.Public | BindingFlags.Static); + + try + { + // Set up a proxy server with credentials in URL + proxyServerProperty.SetValue(null, "http://testuser:testpass@proxy.test.com:8080"); + + // Act + HttpClient.LoadWebProxyConfig(); + + // Assert + var customProxy = HttpClient.CustomWebProxy as WebProxy; + Assert.NotNull(customProxy); + Assert.Equal("proxy.test.com", customProxy.Address.Host); + Assert.Equal(8080, customProxy.Address.Port); + + // Verify credentials were set + Assert.NotNull(customProxy.Credentials); + var networkCredential = customProxy.Credentials as NetworkCredential; + Assert.NotNull(networkCredential); + Assert.Equal("testuser", networkCredential.UserName); + Assert.Equal("testpass", networkCredential.Password); + } + finally + { + // Restore original state + proxyServerProperty.SetValue(null, originalProxyServer); + HttpClient.CustomWebProxy = null; + } + } + + [Fact] + public void LoadWebProxyConfig_DoesNotSetProxy_WhenProxyServerIsEmpty() + { + // Arrange + var originalProxyServer = Config.ProxyServer; + var configType = typeof(Config); + var proxyServerProperty = configType.GetProperty("ProxyServer", BindingFlags.Public | BindingFlags.Static); + + try + { + // Set empty proxy server + proxyServerProperty.SetValue(null, string.Empty); + + // Act + HttpClient.LoadWebProxyConfig(); + + // Assert - CustomWebProxy should remain null + Assert.Null(HttpClient.CustomWebProxy); + } + finally + { + // Restore original state + proxyServerProperty.SetValue(null, originalProxyServer); + HttpClient.CustomWebProxy = null; + } + } + + [Fact] + public void LoadWebProxyConfig_HandlesException_WhenInvalidProxyUrl() + { + // Arrange + var originalProxyServer = Config.ProxyServer; + var configType = typeof(Config); + var proxyServerProperty = configType.GetProperty("ProxyServer", BindingFlags.Public | BindingFlags.Static); + + try + { + // Set invalid proxy server URL + proxyServerProperty.SetValue(null, "invalid-url-format"); + + // Act - should not throw exception + var exception = Record.Exception(() => HttpClient.LoadWebProxyConfig()); + + // Assert - should handle exception gracefully + Assert.Null(exception); + // CustomWebProxy might be null or unchanged depending on the exception handling + } + finally + { + // Restore original state + proxyServerProperty.SetValue(null, originalProxyServer); + HttpClient.CustomWebProxy = null; + } + } + + [Theory] + [InlineData("http://proxy.test.com:8080")] + [InlineData("https://secure-proxy.test.com:443")] + [InlineData("http://proxy.company.com:3128")] + public void LoadWebProxyConfig_SetsCorrectProxyAddress_ForValidUrls(string proxyUrl) + { + // Arrange + var originalProxyServer = Config.ProxyServer; + var configType = typeof(Config); + var proxyServerProperty = configType.GetProperty("ProxyServer", BindingFlags.Public | BindingFlags.Static); + + try + { + // Set proxy server + proxyServerProperty.SetValue(null, proxyUrl); + + // Act + HttpClient.LoadWebProxyConfig(); + + // Assert + var customProxy = HttpClient.CustomWebProxy as WebProxy; + Assert.NotNull(customProxy); + + var expectedUri = new Uri(proxyUrl); + Assert.Equal(expectedUri.Host, customProxy.Address.Host); + Assert.Equal(expectedUri.Port, customProxy.Address.Port); + Assert.Equal(expectedUri.Scheme, customProxy.Address.Scheme); + } + finally + { + // Restore original state + proxyServerProperty.SetValue(null, originalProxyServer); + HttpClient.CustomWebProxy = null; + } + } + [Fact] + public void LoadWebProxyConfig_WithProxyUseDefaultCredentials_True() + { + // Arrange - Save original values + var originalProxyServer = Config.ProxyServer; + var originalProxyUseDefaultCredentials = Config.ProxyUseDefaultCredentials; + + try + { + // Set up Config values + var configType = typeof(Config); + var proxyServerProperty = configType.GetProperty("ProxyServer", BindingFlags.Public | BindingFlags.Static); + var proxyUseDefaultCredentialsProperty = configType.GetProperty("ProxyUseDefaultCredentials", BindingFlags.Public | BindingFlags.Static); + + proxyServerProperty.SetValue(null, "http://corporate-proxy.test.com:8080"); + proxyUseDefaultCredentialsProperty.SetValue(null, true); + + // Act + HttpClient.LoadWebProxyConfig(); + + // Assert + var customProxy = HttpClient.CustomWebProxy as WebProxy; + Assert.NotNull(customProxy); + Assert.Equal("corporate-proxy.test.com", customProxy.Address.Host); + Assert.Equal(8080, customProxy.Address.Port); + Assert.True(customProxy.UseDefaultCredentials); + Assert.NotNull(customProxy.Credentials); // No explicit credentials when using default + Assert.Empty((customProxy.Credentials as NetworkCredential).UserName); + } + finally + { + // Restore original values + var configType = typeof(Config); + var proxyServerProperty = configType.GetProperty("ProxyServer", BindingFlags.Public | BindingFlags.Static); + var proxyUseDefaultCredentialsProperty = configType.GetProperty("ProxyUseDefaultCredentials", BindingFlags.Public | BindingFlags.Static); + + proxyServerProperty.SetValue(null, originalProxyServer); + proxyUseDefaultCredentialsProperty.SetValue(null, originalProxyUseDefaultCredentials); + HttpClient.CustomWebProxy = null; + } + } + + [Fact] + public void LoadWebProxyConfig_WithProxyUseDefaultCredentials_False() + { + // Arrange - Save original values + var originalProxyServer = Config.ProxyServer; + var originalProxyUseDefaultCredentials = Config.ProxyUseDefaultCredentials; + + try + { + // Set up Config values + var configType = typeof(Config); + var proxyServerProperty = configType.GetProperty("ProxyServer", BindingFlags.Public | BindingFlags.Static); + var proxyUseDefaultCredentialsProperty = configType.GetProperty("ProxyUseDefaultCredentials", BindingFlags.Public | BindingFlags.Static); + + proxyServerProperty.SetValue(null, "http://corporate-proxy.test.com:8080"); + proxyUseDefaultCredentialsProperty.SetValue(null, false); + + // Act + HttpClient.LoadWebProxyConfig(); + + // Assert + var customProxy = HttpClient.CustomWebProxy as WebProxy; + Assert.NotNull(customProxy); + Assert.Equal("corporate-proxy.test.com", customProxy.Address.Host); + Assert.Equal(8080, customProxy.Address.Port); + Assert.False(customProxy.UseDefaultCredentials); + Assert.Null(customProxy.Credentials); + } + finally + { + // Restore original values + var configType = typeof(Config); + var proxyServerProperty = configType.GetProperty("ProxyServer", BindingFlags.Public | BindingFlags.Static); + var proxyUseDefaultCredentialsProperty = configType.GetProperty("ProxyUseDefaultCredentials", BindingFlags.Public | BindingFlags.Static); + + proxyServerProperty.SetValue(null, originalProxyServer); + proxyUseDefaultCredentialsProperty.SetValue(null, originalProxyUseDefaultCredentials); + HttpClient.CustomWebProxy = null; + } + } + + [Fact] + public void LoadWebProxyConfig_WithProxyUseDefaultCredentials_Null() + { + // Arrange - Save original values + var originalProxyServer = Config.ProxyServer; + var originalProxyUseDefaultCredentials = Config.ProxyUseDefaultCredentials; + + try + { + // Set up Config values + var configType = typeof(Config); + var proxyServerProperty = configType.GetProperty("ProxyServer", BindingFlags.Public | BindingFlags.Static); + var proxyUseDefaultCredentialsProperty = configType.GetProperty("ProxyUseDefaultCredentials", BindingFlags.Public | BindingFlags.Static); + + proxyServerProperty.SetValue(null, "http://corporate-proxy.test.com:8080"); + proxyUseDefaultCredentialsProperty.SetValue(null, null); // Explicitly set to null + + // Act + HttpClient.LoadWebProxyConfig(); + + // Assert + var customProxy = HttpClient.CustomWebProxy as WebProxy; + Assert.NotNull(customProxy); + Assert.Equal("corporate-proxy.test.com", customProxy.Address.Host); + Assert.Equal(8080, customProxy.Address.Port); + Assert.False(customProxy.UseDefaultCredentials); // Should remain false when null + Assert.Null(customProxy.Credentials); + } + finally + { + // Restore original values + var configType = typeof(Config); + var proxyServerProperty = configType.GetProperty("ProxyServer", BindingFlags.Public | BindingFlags.Static); + var proxyUseDefaultCredentialsProperty = configType.GetProperty("ProxyUseDefaultCredentials", BindingFlags.Public | BindingFlags.Static); + + proxyServerProperty.SetValue(null, originalProxyServer); + proxyUseDefaultCredentialsProperty.SetValue(null, originalProxyUseDefaultCredentials); + HttpClient.CustomWebProxy = null; + } + } + + [Fact] + public void LoadWebProxyConfig_WithEmbeddedCredentials_IgnoresProxyUseDefaultCredentials() + { + // Arrange - Save original values + var originalProxyServer = Config.ProxyServer; + var originalProxyUseDefaultCredentials = Config.ProxyUseDefaultCredentials; + + try + { + // Set up Config values - proxy with embedded credentials + var configType = typeof(Config); + var proxyServerProperty = configType.GetProperty("ProxyServer", BindingFlags.Public | BindingFlags.Static); + var proxyUseDefaultCredentialsProperty = configType.GetProperty("ProxyUseDefaultCredentials", BindingFlags.Public | BindingFlags.Static); + + proxyServerProperty.SetValue(null, "http://testuser:testpass@corporate-proxy.test.com:8080"); + proxyUseDefaultCredentialsProperty.SetValue(null, true); // This should be ignored + + // Act + HttpClient.LoadWebProxyConfig(); + + // Assert - Embedded credentials take precedence over UseDefaultCredentials + var customProxy = HttpClient.CustomWebProxy as WebProxy; + Assert.NotNull(customProxy); + Assert.Equal("corporate-proxy.test.com", customProxy.Address.Host); + Assert.Equal(8080, customProxy.Address.Port); + Assert.False(customProxy.UseDefaultCredentials); // Should be false when explicit credentials are used + + // Verify explicit credentials were set + Assert.NotNull(customProxy.Credentials); + var networkCredential = customProxy.Credentials as NetworkCredential; + Assert.NotNull(networkCredential); + Assert.Equal("testuser", networkCredential.UserName); + Assert.Equal("testpass", networkCredential.Password); + } + finally + { + // Restore original values + var configType = typeof(Config); + var proxyServerProperty = configType.GetProperty("ProxyServer", BindingFlags.Public | BindingFlags.Static); + var proxyUseDefaultCredentialsProperty = configType.GetProperty("ProxyUseDefaultCredentials", BindingFlags.Public | BindingFlags.Static); + + proxyServerProperty.SetValue(null, originalProxyServer); + proxyUseDefaultCredentialsProperty.SetValue(null, originalProxyUseDefaultCredentials); + HttpClient.CustomWebProxy = null; + } + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + [InlineData(null)] + public void LoadWebProxyConfig_ProxyUseDefaultCredentials_VariousValues(bool? configValue) + { + // Arrange - Save original values + var originalProxyServer = Config.ProxyServer; + var originalProxyUseDefaultCredentials = Config.ProxyUseDefaultCredentials; + + try + { + // Set up Config values + var configType = typeof(Config); + var proxyServerProperty = configType.GetProperty("ProxyServer", BindingFlags.Public | BindingFlags.Static); + var proxyUseDefaultCredentialsProperty = configType.GetProperty("ProxyUseDefaultCredentials", BindingFlags.Public | BindingFlags.Static); + + proxyServerProperty.SetValue(null, "http://test-proxy.company.com:3128"); + proxyUseDefaultCredentialsProperty.SetValue(null, configValue); + + // Act + HttpClient.LoadWebProxyConfig(); + + // Assert + var customProxy = HttpClient.CustomWebProxy as WebProxy; + Assert.NotNull(customProxy); + Assert.Equal("test-proxy.company.com", customProxy.Address.Host); + Assert.Equal(3128, customProxy.Address.Port); + + // UseDefaultCredentials should only be true when configValue is true + bool expectedUseDefault = configValue.HasValue && configValue.Value; + Assert.Equal(expectedUseDefault, customProxy.UseDefaultCredentials); + if (configValue.HasValue && configValue.Value) + { + Assert.NotNull(customProxy.Credentials); + Assert.Empty((customProxy.Credentials as NetworkCredential).UserName); + } + else + { + Assert.Null(customProxy.Credentials); + } + } + finally + { + // Restore original values + var configType = typeof(Config); + var proxyServerProperty = configType.GetProperty("ProxyServer", BindingFlags.Public | BindingFlags.Static); + var proxyUseDefaultCredentialsProperty = configType.GetProperty("ProxyUseDefaultCredentials", BindingFlags.Public | BindingFlags.Static); + + proxyServerProperty.SetValue(null, originalProxyServer); + proxyUseDefaultCredentialsProperty.SetValue(null, originalProxyUseDefaultCredentials); + HttpClient.CustomWebProxy = null; + } + } + } +}