From 901cd12bd4606fce954fb7cbdda279ca74fddb7d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 14 Nov 2025 13:36:38 +0000 Subject: [PATCH 01/21] Initial plan From 809a61b5f54aea8c864188c036beaa2c373834f5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 14 Nov 2025 13:40:38 +0000 Subject: [PATCH 02/21] Add CertificateStoreLocation selection for Frends.HTTP.Request module Co-authored-by: jefim <1387820+jefim@users.noreply.github.com> --- .../Definitions/CertificateStoreLocation.cs | 16 ++++++++++++++++ .../Frends.HTTP.Request/Definitions/Options.cs | 9 +++++++++ .../Frends.HTTP.Request/Extensions.cs | 15 +++++++++++---- 3 files changed, 36 insertions(+), 4 deletions(-) create mode 100644 Frends.HTTP.Request/Frends.HTTP.Request/Definitions/CertificateStoreLocation.cs diff --git a/Frends.HTTP.Request/Frends.HTTP.Request/Definitions/CertificateStoreLocation.cs b/Frends.HTTP.Request/Frends.HTTP.Request/Definitions/CertificateStoreLocation.cs new file mode 100644 index 0000000..7cbf8cb --- /dev/null +++ b/Frends.HTTP.Request/Frends.HTTP.Request/Definitions/CertificateStoreLocation.cs @@ -0,0 +1,16 @@ +namespace Frends.HTTP.Request.Definitions; + +/// +/// Certificate store location. +/// +public enum CertificateStoreLocation +{ + /// + /// The X.509 certificate store assigned to the current user. + /// + CurrentUser, + /// + /// The X.509 certificate store assigned to the local machine. + /// + LocalMachine +} diff --git a/Frends.HTTP.Request/Frends.HTTP.Request/Definitions/Options.cs b/Frends.HTTP.Request/Frends.HTTP.Request/Definitions/Options.cs index 27d1ccd..73f27d4 100644 --- a/Frends.HTTP.Request/Frends.HTTP.Request/Definitions/Options.cs +++ b/Frends.HTTP.Request/Frends.HTTP.Request/Definitions/Options.cs @@ -80,6 +80,15 @@ public class Options [UIHint(nameof(Authentication), "", Authentication.ClientCertificate)] public string CertificateThumbprint { get; set; } + /// + /// Applicable only when Certificate Source is "CertificateStore". + /// Store location for the certificate. + /// + /// CurrentUser + [UIHint(nameof(Authentication), "", Authentication.ClientCertificate)] + [DefaultValue(CertificateStoreLocation.CurrentUser)] + public CertificateStoreLocation CertificateStoreLocation { get; set; } + /// /// Should the entire certificate chain be loaded from the certificate store and included in the request. Only valid when using Certificate Store as the Certificate Source /// diff --git a/Frends.HTTP.Request/Frends.HTTP.Request/Extensions.cs b/Frends.HTTP.Request/Frends.HTTP.Request/Extensions.cs index 5c41542..ba4efdf 100644 --- a/Frends.HTTP.Request/Frends.HTTP.Request/Extensions.cs +++ b/Frends.HTTP.Request/Frends.HTTP.Request/Extensions.cs @@ -61,7 +61,7 @@ private static X509Certificate[] GetCertificates(Options options) { case CertificateSource.CertificateStore: var thumbprint = options.CertificateThumbprint; - certificates = GetCertificatesFromStore(thumbprint, options.LoadEntireChainForCertificate); + certificates = GetCertificatesFromStore(thumbprint, options.LoadEntireChainForCertificate, options.CertificateStoreLocation); break; case CertificateSource.File: certificates = GetCertificatesFromFile(options.ClientCertificateFilePath, options.ClientCertificateKeyPhrase); @@ -105,17 +105,24 @@ private static X509Certificate2[] GetCertificatesFromFile(string clientCertifica } private static X509Certificate2[] GetCertificatesFromStore(string thumbprint, - bool loadEntireChain) + bool loadEntireChain, CertificateStoreLocation storeLocation) { thumbprint = Regex.Replace(thumbprint, @"[^\da-zA-z]", string.Empty).ToUpper(); - using (var store = new X509Store(StoreName.My, StoreLocation.CurrentUser)) + var location = storeLocation == CertificateStoreLocation.CurrentUser + ? StoreLocation.CurrentUser + : StoreLocation.LocalMachine; + var locationText = storeLocation == CertificateStoreLocation.CurrentUser + ? "current user" + : "local machine"; + + using (var store = new X509Store(StoreName.My, location)) { store.Open(OpenFlags.ReadOnly); var signingCert = store.Certificates.Find(X509FindType.FindByThumbprint, thumbprint, false); if (signingCert.Count == 0) { throw new FileNotFoundException( - $"Certificate with thumbprint: '{thumbprint}' not found in current user cert store."); + $"Certificate with thumbprint: '{thumbprint}' not found in {locationText} cert store."); } var certificate = signingCert[0]; From 5b3990967d4232044d568a3646ab415d97c38584 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 14 Nov 2025 13:44:27 +0000 Subject: [PATCH 03/21] Add CertificateStoreLocation selection to all HTTP modules Co-authored-by: jefim <1387820+jefim@users.noreply.github.com> --- .../Definitions/CertificateStoreLocation.cs | 16 ++++++++++++++++ .../Definitions/Options.cs | 9 +++++++++ .../Frends.HTTP.DownloadFile/Extensions.cs | 15 +++++++++++---- .../Definitions/CertificateStoreLocation.cs | 16 ++++++++++++++++ .../Definitions/Options.cs | 9 +++++++++ .../Frends.HTTP.RequestBytes/Extensions.cs | 15 +++++++++++---- .../Definitions/CertificateStoreLocation.cs | 16 ++++++++++++++++ .../Definitions/Options.cs | 9 +++++++++ .../Extensions.cs | 15 +++++++++++---- .../Definitions/CertificateStoreLocation.cs | 16 ++++++++++++++++ .../Frends.HTTP.SendBytes/Definitions/Options.cs | 9 +++++++++ .../Frends.HTTP.SendBytes/Extensions.cs | 15 +++++++++++---- 12 files changed, 144 insertions(+), 16 deletions(-) create mode 100644 Frends.HTTP.DownloadFile/Frends.HTTP.DownloadFile/Definitions/CertificateStoreLocation.cs create mode 100644 Frends.HTTP.RequestBytes/Frends.HTTP.RequestBytes/Definitions/CertificateStoreLocation.cs create mode 100644 Frends.HTTP.SendAndReceiveBytes/Frends.HTTP.SendAndReceiveBytes/Definitions/CertificateStoreLocation.cs create mode 100644 Frends.HTTP.SendBytes/Frends.HTTP.SendBytes/Definitions/CertificateStoreLocation.cs diff --git a/Frends.HTTP.DownloadFile/Frends.HTTP.DownloadFile/Definitions/CertificateStoreLocation.cs b/Frends.HTTP.DownloadFile/Frends.HTTP.DownloadFile/Definitions/CertificateStoreLocation.cs new file mode 100644 index 0000000..83cfeb7 --- /dev/null +++ b/Frends.HTTP.DownloadFile/Frends.HTTP.DownloadFile/Definitions/CertificateStoreLocation.cs @@ -0,0 +1,16 @@ +namespace Frends.HTTP.DownloadFile.Definitions; + +/// +/// Certificate store location. +/// +public enum CertificateStoreLocation +{ + /// + /// The X.509 certificate store assigned to the current user. + /// + CurrentUser, + /// + /// The X.509 certificate store assigned to the local machine. + /// + LocalMachine +} diff --git a/Frends.HTTP.DownloadFile/Frends.HTTP.DownloadFile/Definitions/Options.cs b/Frends.HTTP.DownloadFile/Frends.HTTP.DownloadFile/Definitions/Options.cs index d2d70a0..9bb7b41 100644 --- a/Frends.HTTP.DownloadFile/Frends.HTTP.DownloadFile/Definitions/Options.cs +++ b/Frends.HTTP.DownloadFile/Frends.HTTP.DownloadFile/Definitions/Options.cs @@ -81,6 +81,15 @@ public class Options [UIHint(nameof(Authentication), "", Authentication.ClientCertificate)] public string CertificateThumbprint { get; set; } + /// + /// Applicable only when Certificate Source is "CertificateStore". + /// Store location for the certificate. + /// + /// CurrentUser + [UIHint(nameof(Authentication), "", Authentication.ClientCertificate)] + [DefaultValue(CertificateStoreLocation.CurrentUser)] + public CertificateStoreLocation CertificateStoreLocation { get; set; } + /// /// Should the entire certificate chain be loaded from the certificate store and included in the request. Only valid when using Certificate Store as the Certificate Source /// diff --git a/Frends.HTTP.DownloadFile/Frends.HTTP.DownloadFile/Extensions.cs b/Frends.HTTP.DownloadFile/Frends.HTTP.DownloadFile/Extensions.cs index 3ab6b87..0182151 100644 --- a/Frends.HTTP.DownloadFile/Frends.HTTP.DownloadFile/Extensions.cs +++ b/Frends.HTTP.DownloadFile/Frends.HTTP.DownloadFile/Extensions.cs @@ -54,7 +54,7 @@ private static X509Certificate[] GetCertificates(Options options) { case CertificateSource.CertificateStore: var thumbprint = options.CertificateThumbprint; - certificates = GetCertificatesFromStore(thumbprint, options.LoadEntireChainForCertificate); + certificates = GetCertificatesFromStore(thumbprint, options.LoadEntireChainForCertificate, options.CertificateStoreLocation); break; case CertificateSource.File: certificates = GetCertificatesFromFile(options.ClientCertificateFilePath, options.ClientCertificateKeyPhrase); @@ -94,14 +94,21 @@ private static X509Certificate2[] GetCertificatesFromFile(string clientCertifica } private static X509Certificate2[] GetCertificatesFromStore(string thumbprint, - bool loadEntireChain) + bool loadEntireChain, CertificateStoreLocation storeLocation) { thumbprint = Regex.Replace(thumbprint, @"[^\da-zA-z]", string.Empty).ToUpper(); - using var store = new X509Store(StoreName.My, StoreLocation.CurrentUser); + var location = storeLocation == CertificateStoreLocation.CurrentUser + ? StoreLocation.CurrentUser + : StoreLocation.LocalMachine; + var locationText = storeLocation == CertificateStoreLocation.CurrentUser + ? "current user" + : "local machine"; + + using var store = new X509Store(StoreName.My, location); store.Open(OpenFlags.ReadOnly); var signingCert = store.Certificates.Find(X509FindType.FindByThumbprint, thumbprint, false); if (signingCert.Count == 0) - throw new FileNotFoundException($"Certificate with thumbprint: '{thumbprint}' not found in current user cert store."); + throw new FileNotFoundException($"Certificate with thumbprint: '{thumbprint}' not found in {locationText} cert store."); var certificate = signingCert[0]; diff --git a/Frends.HTTP.RequestBytes/Frends.HTTP.RequestBytes/Definitions/CertificateStoreLocation.cs b/Frends.HTTP.RequestBytes/Frends.HTTP.RequestBytes/Definitions/CertificateStoreLocation.cs new file mode 100644 index 0000000..cdeadc7 --- /dev/null +++ b/Frends.HTTP.RequestBytes/Frends.HTTP.RequestBytes/Definitions/CertificateStoreLocation.cs @@ -0,0 +1,16 @@ +namespace Frends.HTTP.RequestBytes.Definitions; + +/// +/// Certificate store location. +/// +public enum CertificateStoreLocation +{ + /// + /// The X.509 certificate store assigned to the current user. + /// + CurrentUser, + /// + /// The X.509 certificate store assigned to the local machine. + /// + LocalMachine +} diff --git a/Frends.HTTP.RequestBytes/Frends.HTTP.RequestBytes/Definitions/Options.cs b/Frends.HTTP.RequestBytes/Frends.HTTP.RequestBytes/Definitions/Options.cs index c1df7ed..eb3953f 100644 --- a/Frends.HTTP.RequestBytes/Frends.HTTP.RequestBytes/Definitions/Options.cs +++ b/Frends.HTTP.RequestBytes/Frends.HTTP.RequestBytes/Definitions/Options.cs @@ -80,6 +80,15 @@ public class Options [UIHint(nameof(Authentication), "", Authentication.ClientCertificate)] public string CertificateThumbprint { get; set; } + /// + /// Applicable only when Certificate Source is "CertificateStore". + /// Store location for the certificate. + /// + /// CurrentUser + [UIHint(nameof(Authentication), "", Authentication.ClientCertificate)] + [DefaultValue(CertificateStoreLocation.CurrentUser)] + public CertificateStoreLocation CertificateStoreLocation { get; set; } + /// /// Should the entire certificate chain be loaded from the certificate store and included in the request. Only valid when using Certificate Store as the Certificate Source /// diff --git a/Frends.HTTP.RequestBytes/Frends.HTTP.RequestBytes/Extensions.cs b/Frends.HTTP.RequestBytes/Frends.HTTP.RequestBytes/Extensions.cs index d73fc0b..48b4807 100644 --- a/Frends.HTTP.RequestBytes/Frends.HTTP.RequestBytes/Extensions.cs +++ b/Frends.HTTP.RequestBytes/Frends.HTTP.RequestBytes/Extensions.cs @@ -61,7 +61,7 @@ private static X509Certificate[] GetCertificates(Options options) { case CertificateSource.CertificateStore: var thumbprint = options.CertificateThumbprint; - certificates = GetCertificatesFromStore(thumbprint, options.LoadEntireChainForCertificate); + certificates = GetCertificatesFromStore(thumbprint, options.LoadEntireChainForCertificate, options.CertificateStoreLocation); break; case CertificateSource.File: certificates = GetCertificatesFromFile(options.ClientCertificateFilePath, options.ClientCertificateKeyPhrase); @@ -105,17 +105,24 @@ private static X509Certificate2[] GetCertificatesFromFile(string clientCertifica } private static X509Certificate2[] GetCertificatesFromStore(string thumbprint, - bool loadEntireChain) + bool loadEntireChain, CertificateStoreLocation storeLocation) { thumbprint = Regex.Replace(thumbprint, @"[^\da-zA-z]", string.Empty).ToUpper(); - using (var store = new X509Store(StoreName.My, StoreLocation.CurrentUser)) + var location = storeLocation == CertificateStoreLocation.CurrentUser + ? StoreLocation.CurrentUser + : StoreLocation.LocalMachine; + var locationText = storeLocation == CertificateStoreLocation.CurrentUser + ? "current user" + : "local machine"; + + using (var store = new X509Store(StoreName.My, location)) { store.Open(OpenFlags.ReadOnly); var signingCert = store.Certificates.Find(X509FindType.FindByThumbprint, thumbprint, false); if (signingCert.Count == 0) { throw new FileNotFoundException( - $"Certificate with thumbprint: '{thumbprint}' not found in current user cert store."); + $"Certificate with thumbprint: '{thumbprint}' not found in {locationText} cert store."); } var certificate = signingCert[0]; diff --git a/Frends.HTTP.SendAndReceiveBytes/Frends.HTTP.SendAndReceiveBytes/Definitions/CertificateStoreLocation.cs b/Frends.HTTP.SendAndReceiveBytes/Frends.HTTP.SendAndReceiveBytes/Definitions/CertificateStoreLocation.cs new file mode 100644 index 0000000..e225084 --- /dev/null +++ b/Frends.HTTP.SendAndReceiveBytes/Frends.HTTP.SendAndReceiveBytes/Definitions/CertificateStoreLocation.cs @@ -0,0 +1,16 @@ +namespace Frends.HTTP.SendAndReceiveBytes.Definitions; + +/// +/// Certificate store location. +/// +public enum CertificateStoreLocation +{ + /// + /// The X.509 certificate store assigned to the current user. + /// + CurrentUser, + /// + /// The X.509 certificate store assigned to the local machine. + /// + LocalMachine +} diff --git a/Frends.HTTP.SendAndReceiveBytes/Frends.HTTP.SendAndReceiveBytes/Definitions/Options.cs b/Frends.HTTP.SendAndReceiveBytes/Frends.HTTP.SendAndReceiveBytes/Definitions/Options.cs index 0a3e108..22e4c4b 100644 --- a/Frends.HTTP.SendAndReceiveBytes/Frends.HTTP.SendAndReceiveBytes/Definitions/Options.cs +++ b/Frends.HTTP.SendAndReceiveBytes/Frends.HTTP.SendAndReceiveBytes/Definitions/Options.cs @@ -83,6 +83,15 @@ public class Options [UIHint(nameof(Authentication), "", Authentication.ClientCertificate)] public string CertificateThumbprint { get; set; } + /// + /// Applicable only when Certificate Source is "CertificateStore". + /// Store location for the certificate. + /// + /// CurrentUser + [UIHint(nameof(Authentication), "", Authentication.ClientCertificate)] + [DefaultValue(CertificateStoreLocation.CurrentUser)] + public CertificateStoreLocation CertificateStoreLocation { get; set; } + /// /// Should the entire certificate chain be loaded from the certificate store and included in the request. Only valid when using Certificate Store as the Certificate Source /// diff --git a/Frends.HTTP.SendAndReceiveBytes/Frends.HTTP.SendAndReceiveBytes/Extensions.cs b/Frends.HTTP.SendAndReceiveBytes/Frends.HTTP.SendAndReceiveBytes/Extensions.cs index 308f16d..8340723 100644 --- a/Frends.HTTP.SendAndReceiveBytes/Frends.HTTP.SendAndReceiveBytes/Extensions.cs +++ b/Frends.HTTP.SendAndReceiveBytes/Frends.HTTP.SendAndReceiveBytes/Extensions.cs @@ -59,7 +59,7 @@ private static X509Certificate[] GetCertificates(Options options) { case CertificateSource.CertificateStore: var thumbprint = options.CertificateThumbprint; - certificates = GetCertificatesFromStore(thumbprint, options.LoadEntireChainForCertificate); + certificates = GetCertificatesFromStore(thumbprint, options.LoadEntireChainForCertificate, options.CertificateStoreLocation); break; case CertificateSource.File: certificates = GetCertificatesFromFile(options.ClientCertificateFilePath, options.ClientCertificateKeyPhrase); @@ -103,17 +103,24 @@ private static X509Certificate2[] GetCertificatesFromFile(string clientCertifica } private static X509Certificate2[] GetCertificatesFromStore(string thumbprint, - bool loadEntireChain) + bool loadEntireChain, CertificateStoreLocation storeLocation) { thumbprint = Regex.Replace(thumbprint, @"[^\da-zA-z]", string.Empty).ToUpper(); - using (var store = new X509Store(StoreName.My, StoreLocation.CurrentUser)) + var location = storeLocation == CertificateStoreLocation.CurrentUser + ? StoreLocation.CurrentUser + : StoreLocation.LocalMachine; + var locationText = storeLocation == CertificateStoreLocation.CurrentUser + ? "current user" + : "local machine"; + + using (var store = new X509Store(StoreName.My, location)) { store.Open(OpenFlags.ReadOnly); var signingCert = store.Certificates.Find(X509FindType.FindByThumbprint, thumbprint, false); if (signingCert.Count == 0) { throw new FileNotFoundException( - $"Certificate with thumbprint: '{thumbprint}' not found in current user cert store."); + $"Certificate with thumbprint: '{thumbprint}' not found in {locationText} cert store."); } var certificate = signingCert[0]; diff --git a/Frends.HTTP.SendBytes/Frends.HTTP.SendBytes/Definitions/CertificateStoreLocation.cs b/Frends.HTTP.SendBytes/Frends.HTTP.SendBytes/Definitions/CertificateStoreLocation.cs new file mode 100644 index 0000000..56069bd --- /dev/null +++ b/Frends.HTTP.SendBytes/Frends.HTTP.SendBytes/Definitions/CertificateStoreLocation.cs @@ -0,0 +1,16 @@ +namespace Frends.HTTP.SendBytes.Definitions; + +/// +/// Certificate store location. +/// +public enum CertificateStoreLocation +{ + /// + /// The X.509 certificate store assigned to the current user. + /// + CurrentUser, + /// + /// The X.509 certificate store assigned to the local machine. + /// + LocalMachine +} diff --git a/Frends.HTTP.SendBytes/Frends.HTTP.SendBytes/Definitions/Options.cs b/Frends.HTTP.SendBytes/Frends.HTTP.SendBytes/Definitions/Options.cs index 42ca4ab..2ff9451 100644 --- a/Frends.HTTP.SendBytes/Frends.HTTP.SendBytes/Definitions/Options.cs +++ b/Frends.HTTP.SendBytes/Frends.HTTP.SendBytes/Definitions/Options.cs @@ -80,6 +80,15 @@ public class Options [UIHint(nameof(Authentication), "", Authentication.ClientCertificate)] public string CertificateThumbprint { get; set; } + /// + /// Applicable only when Certificate Source is "CertificateStore". + /// Store location for the certificate. + /// + /// CurrentUser + [UIHint(nameof(Authentication), "", Authentication.ClientCertificate)] + [DefaultValue(CertificateStoreLocation.CurrentUser)] + public CertificateStoreLocation CertificateStoreLocation { get; set; } + /// /// Should the entire certificate chain be loaded from the certificate store and included in the request. Only valid when using Certificate Store as the Certificate Source /// diff --git a/Frends.HTTP.SendBytes/Frends.HTTP.SendBytes/Extensions.cs b/Frends.HTTP.SendBytes/Frends.HTTP.SendBytes/Extensions.cs index 2602daa..3adee62 100644 --- a/Frends.HTTP.SendBytes/Frends.HTTP.SendBytes/Extensions.cs +++ b/Frends.HTTP.SendBytes/Frends.HTTP.SendBytes/Extensions.cs @@ -59,7 +59,7 @@ private static X509Certificate[] GetCertificates(Options options) { case CertificateSource.CertificateStore: var thumbprint = options.CertificateThumbprint; - certificates = GetCertificatesFromStore(thumbprint, options.LoadEntireChainForCertificate); + certificates = GetCertificatesFromStore(thumbprint, options.LoadEntireChainForCertificate, options.CertificateStoreLocation); break; case CertificateSource.File: certificates = GetCertificatesFromFile(options.ClientCertificateFilePath, options.ClientCertificateKeyPhrase); @@ -103,17 +103,24 @@ private static X509Certificate2[] GetCertificatesFromFile(string clientCertifica } private static X509Certificate2[] GetCertificatesFromStore(string thumbprint, - bool loadEntireChain) + bool loadEntireChain, CertificateStoreLocation storeLocation) { thumbprint = Regex.Replace(thumbprint, @"[^\da-zA-z]", string.Empty).ToUpper(); - using (var store = new X509Store(StoreName.My, StoreLocation.CurrentUser)) + var location = storeLocation == CertificateStoreLocation.CurrentUser + ? StoreLocation.CurrentUser + : StoreLocation.LocalMachine; + var locationText = storeLocation == CertificateStoreLocation.CurrentUser + ? "current user" + : "local machine"; + + using (var store = new X509Store(StoreName.My, location)) { store.Open(OpenFlags.ReadOnly); var signingCert = store.Certificates.Find(X509FindType.FindByThumbprint, thumbprint, false); if (signingCert.Count == 0) { throw new FileNotFoundException( - $"Certificate with thumbprint: '{thumbprint}' not found in current user cert store."); + $"Certificate with thumbprint: '{thumbprint}' not found in {locationText} cert store."); } var certificate = signingCert[0]; From 244982c1b1febed0a240ef0088659a860c4d8bd2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 18 Nov 2025 04:06:13 +0000 Subject: [PATCH 04/21] Bump versions and add changelog entries for CertificateStoreLocation feature Co-authored-by: jefim <1387820+jefim@users.noreply.github.com> --- Frends.HTTP.DownloadFile/CHANGELOG.md | 4 ++++ .../Frends.HTTP.DownloadFile/Frends.HTTP.DownloadFile.csproj | 2 +- Frends.HTTP.Request/CHANGELOG.md | 4 ++++ .../Frends.HTTP.Request/Frends.HTTP.Request.csproj | 2 +- Frends.HTTP.RequestBytes/CHANGELOG.md | 4 ++++ .../Frends.HTTP.RequestBytes/Frends.HTTP.RequestBytes.csproj | 2 +- Frends.HTTP.SendAndReceiveBytes/CHANGELOG.md | 4 ++++ .../Frends.HTTP.SendAndReceiveBytes.csproj | 2 +- Frends.HTTP.SendBytes/CHANGELOG.md | 4 ++++ .../Frends.HTTP.SendBytes/Frends.HTTP.SendBytes.csproj | 2 +- 10 files changed, 25 insertions(+), 5 deletions(-) diff --git a/Frends.HTTP.DownloadFile/CHANGELOG.md b/Frends.HTTP.DownloadFile/CHANGELOG.md index 597e1b9..20f6d39 100644 --- a/Frends.HTTP.DownloadFile/CHANGELOG.md +++ b/Frends.HTTP.DownloadFile/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## [1.4.0] - 2025-11-18 +### Added +- Added CertificateStoreLocation option to allow selection between CurrentUser and LocalMachine certificate stores when using certificate authentication. + ## [1.3.0] - 2025-05-15 ### Changed - Added new Overwrite parameter to control whether the downloaded file should replace an existing one. diff --git a/Frends.HTTP.DownloadFile/Frends.HTTP.DownloadFile/Frends.HTTP.DownloadFile.csproj b/Frends.HTTP.DownloadFile/Frends.HTTP.DownloadFile/Frends.HTTP.DownloadFile.csproj index 524cdd5..a870dfb 100644 --- a/Frends.HTTP.DownloadFile/Frends.HTTP.DownloadFile/Frends.HTTP.DownloadFile.csproj +++ b/Frends.HTTP.DownloadFile/Frends.HTTP.DownloadFile/Frends.HTTP.DownloadFile.csproj @@ -2,7 +2,7 @@ net6.0 - 1.3.0 + 1.4.0 Frends Frends Frends diff --git a/Frends.HTTP.Request/CHANGELOG.md b/Frends.HTTP.Request/CHANGELOG.md index 6b27b00..e9a5705 100644 --- a/Frends.HTTP.Request/CHANGELOG.md +++ b/Frends.HTTP.Request/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## [1.6.0] - 2025-11-18 +### Added +- Added CertificateStoreLocation option to allow selection between CurrentUser and LocalMachine certificate stores when using certificate authentication. + ## [1.5.0] - 2025-10-03 ### Changed - Changed default return format from String to JToken diff --git a/Frends.HTTP.Request/Frends.HTTP.Request/Frends.HTTP.Request.csproj b/Frends.HTTP.Request/Frends.HTTP.Request/Frends.HTTP.Request.csproj index 382e8e1..3eb843e 100644 --- a/Frends.HTTP.Request/Frends.HTTP.Request/Frends.HTTP.Request.csproj +++ b/Frends.HTTP.Request/Frends.HTTP.Request/Frends.HTTP.Request.csproj @@ -2,7 +2,7 @@ net6.0 - 1.5.0 + 1.6.0 Frends Frends Frends diff --git a/Frends.HTTP.RequestBytes/CHANGELOG.md b/Frends.HTTP.RequestBytes/CHANGELOG.md index d03e15b..e972dc9 100644 --- a/Frends.HTTP.RequestBytes/CHANGELOG.md +++ b/Frends.HTTP.RequestBytes/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## [1.4.0] - 2025-11-18 +### Added +- Added CertificateStoreLocation option to allow selection between CurrentUser and LocalMachine certificate stores when using certificate authentication. + ## [1.3.0] - 2025-10-10 ### Changed - Changed the return type of RequestBytes from Task to Task diff --git a/Frends.HTTP.RequestBytes/Frends.HTTP.RequestBytes/Frends.HTTP.RequestBytes.csproj b/Frends.HTTP.RequestBytes/Frends.HTTP.RequestBytes/Frends.HTTP.RequestBytes.csproj index 8e84d7a..4d7743f 100644 --- a/Frends.HTTP.RequestBytes/Frends.HTTP.RequestBytes/Frends.HTTP.RequestBytes.csproj +++ b/Frends.HTTP.RequestBytes/Frends.HTTP.RequestBytes/Frends.HTTP.RequestBytes.csproj @@ -2,7 +2,7 @@ net6.0 - 1.3.0 + 1.4.0 Frends Frends Frends diff --git a/Frends.HTTP.SendAndReceiveBytes/CHANGELOG.md b/Frends.HTTP.SendAndReceiveBytes/CHANGELOG.md index 3f1633e..a3ea9bc 100644 --- a/Frends.HTTP.SendAndReceiveBytes/CHANGELOG.md +++ b/Frends.HTTP.SendAndReceiveBytes/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## [1.4.0] - 2025-11-18 +### Added +- Added CertificateStoreLocation option to allow selection between CurrentUser and LocalMachine certificate stores when using certificate authentication. + ## [1.3.0] - 2025-10-10 ### Changed - Changed the return type of SendAndReceiveBytes from Task to Task diff --git a/Frends.HTTP.SendAndReceiveBytes/Frends.HTTP.SendAndReceiveBytes/Frends.HTTP.SendAndReceiveBytes.csproj b/Frends.HTTP.SendAndReceiveBytes/Frends.HTTP.SendAndReceiveBytes/Frends.HTTP.SendAndReceiveBytes.csproj index 21b2249..1d53546 100644 --- a/Frends.HTTP.SendAndReceiveBytes/Frends.HTTP.SendAndReceiveBytes/Frends.HTTP.SendAndReceiveBytes.csproj +++ b/Frends.HTTP.SendAndReceiveBytes/Frends.HTTP.SendAndReceiveBytes/Frends.HTTP.SendAndReceiveBytes.csproj @@ -2,7 +2,7 @@ net6.0 - 1.3.0 + 1.4.0 Frends Frends Frends diff --git a/Frends.HTTP.SendBytes/CHANGELOG.md b/Frends.HTTP.SendBytes/CHANGELOG.md index f633c78..fac8fe5 100644 --- a/Frends.HTTP.SendBytes/CHANGELOG.md +++ b/Frends.HTTP.SendBytes/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## [1.5.0] - 2025-11-18 +### Added +- Added CertificateStoreLocation option to allow selection between CurrentUser and LocalMachine certificate stores when using certificate authentication. + ## [1.4.0] - 2025-10-10 ### Changed - Changed the return type of SendBytes from Task to Task diff --git a/Frends.HTTP.SendBytes/Frends.HTTP.SendBytes/Frends.HTTP.SendBytes.csproj b/Frends.HTTP.SendBytes/Frends.HTTP.SendBytes/Frends.HTTP.SendBytes.csproj index e0f3a22..781c5c0 100644 --- a/Frends.HTTP.SendBytes/Frends.HTTP.SendBytes/Frends.HTTP.SendBytes.csproj +++ b/Frends.HTTP.SendBytes/Frends.HTTP.SendBytes/Frends.HTTP.SendBytes.csproj @@ -2,7 +2,7 @@ net6.0 - 1.4.0 + 1.5.0 Frends Frends Frends From bd9c3677e69380d75f921e8e87777ec891e4b776 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 26 Nov 2025 06:30:23 +0000 Subject: [PATCH 05/21] Fix DownloadFile tests by replacing external URL dependency with MockHttp Co-authored-by: jefim <1387820+jefim@users.noreply.github.com> --- .../Frends.HTTP.DownloadFile.Tests.csproj | 1 + .../MockHttpClientFactory.cs | 19 +++++++++++++++++++ .../UnitTests.cs | 11 ++++++++++- 3 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 Frends.HTTP.DownloadFile/Frends.HTTP.DownloadFile.Tests/MockHttpClientFactory.cs diff --git a/Frends.HTTP.DownloadFile/Frends.HTTP.DownloadFile.Tests/Frends.HTTP.DownloadFile.Tests.csproj b/Frends.HTTP.DownloadFile/Frends.HTTP.DownloadFile.Tests/Frends.HTTP.DownloadFile.Tests.csproj index 40a3537..9163594 100644 --- a/Frends.HTTP.DownloadFile/Frends.HTTP.DownloadFile.Tests/Frends.HTTP.DownloadFile.Tests.csproj +++ b/Frends.HTTP.DownloadFile/Frends.HTTP.DownloadFile.Tests/Frends.HTTP.DownloadFile.Tests.csproj @@ -13,6 +13,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/Frends.HTTP.DownloadFile/Frends.HTTP.DownloadFile.Tests/MockHttpClientFactory.cs b/Frends.HTTP.DownloadFile/Frends.HTTP.DownloadFile.Tests/MockHttpClientFactory.cs new file mode 100644 index 0000000..c553184 --- /dev/null +++ b/Frends.HTTP.DownloadFile/Frends.HTTP.DownloadFile.Tests/MockHttpClientFactory.cs @@ -0,0 +1,19 @@ +using System.Net.Http; +using Frends.HTTP.DownloadFile.Definitions; +using RichardSzalay.MockHttp; + +namespace Frends.HTTP.DownloadFile.Tests; + +public class MockHttpClientFactory : IHttpClientFactory +{ + private readonly MockHttpMessageHandler _mockHttpMessageHandler; + + public MockHttpClientFactory(MockHttpMessageHandler mockHttpMessageHandler) + { + _mockHttpMessageHandler = mockHttpMessageHandler; + } + public HttpClient CreateClient(Options options) + { + return _mockHttpMessageHandler.ToHttpClient(); + } +} diff --git a/Frends.HTTP.DownloadFile/Frends.HTTP.DownloadFile.Tests/UnitTests.cs b/Frends.HTTP.DownloadFile/Frends.HTTP.DownloadFile.Tests/UnitTests.cs index 00db7d0..c4d1e62 100644 --- a/Frends.HTTP.DownloadFile/Frends.HTTP.DownloadFile.Tests/UnitTests.cs +++ b/Frends.HTTP.DownloadFile/Frends.HTTP.DownloadFile.Tests/UnitTests.cs @@ -1,9 +1,12 @@ using Frends.HTTP.DownloadFile.Definitions; using Microsoft.VisualStudio.TestTools.UnitTesting; using Pluralsight.Crypto; +using RichardSzalay.MockHttp; using System; using System.Collections.Generic; using System.IO; +using System.Net; +using System.Net.Http; using System.Security.Cryptography.X509Certificates; using System.Threading.Tasks; @@ -14,14 +17,20 @@ public class UnitTests { private static readonly string _directory = Path.Combine(Environment.CurrentDirectory, "testfiles"); private static readonly string _filePath = Path.Combine(_directory, "picture.jpg"); - private static readonly string _targetFileAddress = @"https://upload.wikimedia.org/wikipedia/commons/thumb/2/2f/Google_2015_logo.svg/1200px-Google_2015_logo.svg.png"; + private static readonly string _targetFileAddress = @"http://localhost:9999/testfile.png"; private readonly string _certificatePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "TestFiles", "certwithpk.pfx"); private readonly string _privateKeyPassword = "password"; + private MockHttpMessageHandler _mockHttpMessageHandler; + private static readonly byte[] _mockFileContent = new byte[] { 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A }; // PNG header bytes [TestInitialize] public void TestInitialize() { + _mockHttpMessageHandler = new MockHttpMessageHandler(); + _mockHttpMessageHandler.When(_targetFileAddress) + .Respond(HttpStatusCode.OK, new ByteArrayContent(_mockFileContent)); HTTP.ClearClientCache(); + HTTP.ClientFactory = new MockHttpClientFactory(_mockHttpMessageHandler); Directory.CreateDirectory(_directory); } From 9eaf7b080eab3f7a3544548dc6235d625c700d70 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 26 Nov 2025 06:48:29 +0000 Subject: [PATCH 06/21] Fix MockHttp to create fresh response content for each request Co-authored-by: jefim <1387820+jefim@users.noreply.github.com> --- .../Frends.HTTP.DownloadFile.Tests/UnitTests.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Frends.HTTP.DownloadFile/Frends.HTTP.DownloadFile.Tests/UnitTests.cs b/Frends.HTTP.DownloadFile/Frends.HTTP.DownloadFile.Tests/UnitTests.cs index c4d1e62..981fc98 100644 --- a/Frends.HTTP.DownloadFile/Frends.HTTP.DownloadFile.Tests/UnitTests.cs +++ b/Frends.HTTP.DownloadFile/Frends.HTTP.DownloadFile.Tests/UnitTests.cs @@ -28,7 +28,10 @@ public void TestInitialize() { _mockHttpMessageHandler = new MockHttpMessageHandler(); _mockHttpMessageHandler.When(_targetFileAddress) - .Respond(HttpStatusCode.OK, new ByteArrayContent(_mockFileContent)); + .Respond(req => new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new ByteArrayContent(_mockFileContent) + }); HTTP.ClearClientCache(); HTTP.ClientFactory = new MockHttpClientFactory(_mockHttpMessageHandler); Directory.CreateDirectory(_directory); From 2accf6c8f8d987a7a6cc6eb8f324bc5e9d78d8e5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 26 Nov 2025 07:04:39 +0000 Subject: [PATCH 07/21] Add unit tests for CertificateStoreLocation and error cases to increase coverage Co-authored-by: jefim <1387820+jefim@users.noreply.github.com> --- .../UnitTests.cs | 136 ++++++++++++++++++ 1 file changed, 136 insertions(+) diff --git a/Frends.HTTP.DownloadFile/Frends.HTTP.DownloadFile.Tests/UnitTests.cs b/Frends.HTTP.DownloadFile/Frends.HTTP.DownloadFile.Tests/UnitTests.cs index 981fc98..053dc7a 100644 --- a/Frends.HTTP.DownloadFile/Frends.HTTP.DownloadFile.Tests/UnitTests.cs +++ b/Frends.HTTP.DownloadFile/Frends.HTTP.DownloadFile.Tests/UnitTests.cs @@ -340,4 +340,140 @@ public async Task TestFileDownload_WithOverwriteTrue_ShouldOverwriteExistingFile var actualContent = File.ReadAllText(_filePath); Assert.AreNotEqual("OLD CONTENT", actualContent, "File should have been overwritten."); } + + [TestMethod] + [ExpectedException(typeof(ArgumentNullException))] + public async Task TestFileDownload_WithEmptyUrl_ShouldThrowException() + { + var input = new Input + { + Url = "", + FilePath = _filePath, + Headers = null + }; + + var options = new Options + { + AllowInvalidCertificate = true, + Authentication = Authentication.None, + ConnectionTimeoutSeconds = 60 + }; + + await HTTP.DownloadFile(input, options, default); + } + + [TestMethod] + public async Task TestFileDownload_WithCertificateStoreLocation_CurrentUser() + { + var tp = CertificateHandler(_certificatePath, _privateKeyPassword, false, null); + + var input = new Input + { + Url = _targetFileAddress, + FilePath = _filePath, + Headers = null + }; + + var options = new Options + { + AllowInvalidCertificate = true, + AllowInvalidResponseContentTypeCharSet = true, + Authentication = Authentication.ClientCertificate, + AutomaticCookieHandling = true, + CertificateThumbprint = tp, + ClientCertificateFilePath = _certificatePath, + ClientCertificateInBase64 = "", + ClientCertificateKeyPhrase = _privateKeyPassword, + ClientCertificateSource = CertificateSource.CertificateStore, + CertificateStoreLocation = CertificateStoreLocation.CurrentUser, + ConnectionTimeoutSeconds = 60, + FollowRedirects = true, + LoadEntireChainForCertificate = false, + Password = "", + ThrowExceptionOnErrorResponse = true, + Token = "", + Username = "domain\\username" + }; + + var result = await HTTP.DownloadFile(input, options, default); + + Assert.IsNotNull(result); + Assert.IsTrue(result.Success); + Assert.IsNotNull(result.FilePath); + Assert.IsTrue(File.Exists(result.FilePath)); + + CertificateHandler(_certificatePath, _privateKeyPassword, true, tp); + } + + [TestMethod] + [ExpectedException(typeof(Exception))] + public async Task TestFileDownload_WithCertificateStoreLocation_LocalMachine_NotFound() + { + var input = new Input + { + Url = _targetFileAddress, + FilePath = _filePath, + Headers = null + }; + + var options = new Options + { + AllowInvalidCertificate = true, + AllowInvalidResponseContentTypeCharSet = true, + Authentication = Authentication.ClientCertificate, + AutomaticCookieHandling = true, + CertificateThumbprint = "NONEXISTENTTHUMBPRINT", + ClientCertificateFilePath = "", + ClientCertificateInBase64 = "", + ClientCertificateKeyPhrase = "", + ClientCertificateSource = CertificateSource.CertificateStore, + CertificateStoreLocation = CertificateStoreLocation.LocalMachine, + ConnectionTimeoutSeconds = 60, + FollowRedirects = true, + LoadEntireChainForCertificate = false, + Password = "", + ThrowExceptionOnErrorResponse = true, + Token = "", + Username = "domain\\username" + }; + + await HTTP.DownloadFile(input, options, default); + } + + [TestMethod] + public async Task TestFileDownload_WithOverwriteFalse_ExistingFile_ShouldThrow() + { + File.WriteAllText(_filePath, "OLD CONTENT"); + + var input = new Input + { + Url = _targetFileAddress, + FilePath = _filePath, + Headers = null + }; + + var options = new Options + { + AllowInvalidCertificate = true, + AllowInvalidResponseContentTypeCharSet = true, + Authentication = Authentication.None, + AutomaticCookieHandling = true, + CertificateThumbprint = "", + ClientCertificateFilePath = "", + ClientCertificateInBase64 = "", + ClientCertificateKeyPhrase = "", + ClientCertificateSource = CertificateSource.File, + ConnectionTimeoutSeconds = 60, + FollowRedirects = true, + LoadEntireChainForCertificate = true, + Password = "", + ThrowExceptionOnErrorResponse = true, + Token = "", + Username = "domain\\username", + Overwrite = false + }; + + await Assert.ThrowsExceptionAsync(async () => + await HTTP.DownloadFile(input, options, default)); + } } \ No newline at end of file From ad1d4fd6573559dca39249fc77842c3581e049c1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 26 Nov 2025 07:34:34 +0000 Subject: [PATCH 08/21] Fix TestFileDownload_WithCertificateStoreLocation_LocalMachine_NotFound test to use real HttpClientFactory Co-authored-by: jefim <1387820+jefim@users.noreply.github.com> --- .../Frends.HTTP.DownloadFile.Tests/UnitTests.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Frends.HTTP.DownloadFile/Frends.HTTP.DownloadFile.Tests/UnitTests.cs b/Frends.HTTP.DownloadFile/Frends.HTTP.DownloadFile.Tests/UnitTests.cs index 053dc7a..ef176a1 100644 --- a/Frends.HTTP.DownloadFile/Frends.HTTP.DownloadFile.Tests/UnitTests.cs +++ b/Frends.HTTP.DownloadFile/Frends.HTTP.DownloadFile.Tests/UnitTests.cs @@ -409,6 +409,9 @@ public async Task TestFileDownload_WithCertificateStoreLocation_CurrentUser() [ExpectedException(typeof(Exception))] public async Task TestFileDownload_WithCertificateStoreLocation_LocalMachine_NotFound() { + // Use real HTTP client factory to test certificate lookup failure + HTTP.ClientFactory = new HttpClientFactory(); + var input = new Input { Url = _targetFileAddress, From 1aa4c77d3d20d6dae62b7dc06dc50d6c9af919cc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 2 Dec 2025 07:06:19 +0000 Subject: [PATCH 09/21] Fix whitespace formatting issues in UnitTests.cs and Extensions.cs Co-authored-by: jefim <1387820+jefim@users.noreply.github.com> --- .../Frends.HTTP.DownloadFile.Tests/UnitTests.cs | 8 ++++---- .../Frends.HTTP.DownloadFile/Extensions.cs | 10 +++++----- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Frends.HTTP.DownloadFile/Frends.HTTP.DownloadFile.Tests/UnitTests.cs b/Frends.HTTP.DownloadFile/Frends.HTTP.DownloadFile.Tests/UnitTests.cs index ef176a1..008e0da 100644 --- a/Frends.HTTP.DownloadFile/Frends.HTTP.DownloadFile.Tests/UnitTests.cs +++ b/Frends.HTTP.DownloadFile/Frends.HTTP.DownloadFile.Tests/UnitTests.cs @@ -28,9 +28,9 @@ public void TestInitialize() { _mockHttpMessageHandler = new MockHttpMessageHandler(); _mockHttpMessageHandler.When(_targetFileAddress) - .Respond(req => new HttpResponseMessage(HttpStatusCode.OK) - { - Content = new ByteArrayContent(_mockFileContent) + .Respond(req => new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new ByteArrayContent(_mockFileContent) }); HTTP.ClearClientCache(); HTTP.ClientFactory = new MockHttpClientFactory(_mockHttpMessageHandler); @@ -476,7 +476,7 @@ public async Task TestFileDownload_WithOverwriteFalse_ExistingFile_ShouldThrow() Overwrite = false }; - await Assert.ThrowsExceptionAsync(async () => + await Assert.ThrowsExceptionAsync(async () => await HTTP.DownloadFile(input, options, default)); } } \ No newline at end of file diff --git a/Frends.HTTP.DownloadFile/Frends.HTTP.DownloadFile/Extensions.cs b/Frends.HTTP.DownloadFile/Frends.HTTP.DownloadFile/Extensions.cs index 0182151..fa9ca6e 100644 --- a/Frends.HTTP.DownloadFile/Frends.HTTP.DownloadFile/Extensions.cs +++ b/Frends.HTTP.DownloadFile/Frends.HTTP.DownloadFile/Extensions.cs @@ -97,13 +97,13 @@ private static X509Certificate2[] GetCertificatesFromStore(string thumbprint, bool loadEntireChain, CertificateStoreLocation storeLocation) { thumbprint = Regex.Replace(thumbprint, @"[^\da-zA-z]", string.Empty).ToUpper(); - var location = storeLocation == CertificateStoreLocation.CurrentUser - ? StoreLocation.CurrentUser + var location = storeLocation == CertificateStoreLocation.CurrentUser + ? StoreLocation.CurrentUser : StoreLocation.LocalMachine; - var locationText = storeLocation == CertificateStoreLocation.CurrentUser - ? "current user" + var locationText = storeLocation == CertificateStoreLocation.CurrentUser + ? "current user" : "local machine"; - + using var store = new X509Store(StoreName.My, location); store.Open(OpenFlags.ReadOnly); var signingCert = store.Certificates.Find(X509FindType.FindByThumbprint, thumbprint, false); From c5cd0d72de041c061e5cc6be8214188c9c36c80d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 2 Dec 2025 07:44:15 +0000 Subject: [PATCH 10/21] Add additional unit tests to increase DownloadFile code coverage to 80%+ Co-authored-by: jefim <1387820+jefim@users.noreply.github.com> --- .../UnitTests.cs | 310 ++++++++++++++++++ 1 file changed, 310 insertions(+) diff --git a/Frends.HTTP.DownloadFile/Frends.HTTP.DownloadFile.Tests/UnitTests.cs b/Frends.HTTP.DownloadFile/Frends.HTTP.DownloadFile.Tests/UnitTests.cs index 008e0da..de50a27 100644 --- a/Frends.HTTP.DownloadFile/Frends.HTTP.DownloadFile.Tests/UnitTests.cs +++ b/Frends.HTTP.DownloadFile/Frends.HTTP.DownloadFile.Tests/UnitTests.cs @@ -479,4 +479,314 @@ public async Task TestFileDownload_WithOverwriteFalse_ExistingFile_ShouldThrow() await Assert.ThrowsExceptionAsync(async () => await HTTP.DownloadFile(input, options, default)); } + + [TestMethod] + public async Task TestFileDownload_WithAuthorizationHeader_ShouldNotAddDuplicateAuth() + { + var headers = new[] { new Header() { Name = "Authorization", Value = "Bearer mytoken" } }; + + var input = new Input + { + Url = _targetFileAddress, + FilePath = _filePath, + Headers = headers + }; + + var options = new Options + { + AllowInvalidCertificate = true, + AllowInvalidResponseContentTypeCharSet = true, + Authentication = Authentication.OAuth, + AutomaticCookieHandling = true, + CertificateThumbprint = "", + ClientCertificateFilePath = "", + ClientCertificateInBase64 = "", + ClientCertificateKeyPhrase = "", + ClientCertificateSource = CertificateSource.File, + ConnectionTimeoutSeconds = 60, + FollowRedirects = true, + LoadEntireChainForCertificate = false, + Password = "", + ThrowExceptionOnErrorResponse = false, + Token = "different_token", + Username = "" + }; + + var result = await HTTP.DownloadFile(input, options, default); + + Assert.IsNotNull(result); + Assert.IsTrue(result.Success); + } + + [TestMethod] + [ExpectedException(typeof(Exception))] + public async Task TestFileDownload_WindowsAuth_InvalidUsername_ShouldThrow() + { + // Use real HTTP client factory to test username validation + HTTP.ClientFactory = new HttpClientFactory(); + + var input = new Input + { + Url = _targetFileAddress, + FilePath = _filePath, + Headers = null + }; + + var options = new Options + { + AllowInvalidCertificate = true, + AllowInvalidResponseContentTypeCharSet = true, + Authentication = Authentication.WindowsAuthentication, + AutomaticCookieHandling = true, + CertificateThumbprint = "", + ClientCertificateFilePath = "", + ClientCertificateInBase64 = "", + ClientCertificateKeyPhrase = "", + ClientCertificateSource = CertificateSource.File, + ConnectionTimeoutSeconds = 60, + FollowRedirects = true, + LoadEntireChainForCertificate = false, + Password = "password", + ThrowExceptionOnErrorResponse = true, + Token = "", + Username = "invalid_username_without_domain" + }; + + await HTTP.DownloadFile(input, options, default); + } + + [TestMethod] + public async Task TestFileDownload_CertificateFromFile_WithoutKeyPhrase() + { + var tp = CertificateHandler(_certificatePath, _privateKeyPassword, false, null); + + var input = new Input + { + Url = _targetFileAddress, + FilePath = _filePath, + Headers = null + }; + + var options = new Options + { + AllowInvalidCertificate = true, + AllowInvalidResponseContentTypeCharSet = true, + Authentication = Authentication.ClientCertificate, + AutomaticCookieHandling = true, + CertificateThumbprint = "", + ClientCertificateFilePath = _certificatePath, + ClientCertificateInBase64 = "", + ClientCertificateKeyPhrase = _privateKeyPassword, + ClientCertificateSource = CertificateSource.File, + ConnectionTimeoutSeconds = 60, + FollowRedirects = true, + LoadEntireChainForCertificate = false, + Password = "", + ThrowExceptionOnErrorResponse = true, + Token = "", + Username = "domain\\username" + }; + + var result = await HTTP.DownloadFile(input, options, default); + + Assert.IsNotNull(result); + Assert.IsTrue(result.Success); + + CertificateHandler(_certificatePath, _privateKeyPassword, true, tp); + } + + [TestMethod] + public async Task TestFileDownload_CertificateFromString_WithKeyPhrase() + { + var tp = CertificateHandler(_certificatePath, _privateKeyPassword, false, null); + + var input = new Input + { + Url = _targetFileAddress, + FilePath = _filePath, + Headers = null + }; + + var options = new Options + { + AllowInvalidCertificate = true, + AllowInvalidResponseContentTypeCharSet = true, + Authentication = Authentication.ClientCertificate, + AutomaticCookieHandling = true, + CertificateThumbprint = "", + ClientCertificateFilePath = "", + ClientCertificateInBase64 = Convert.ToBase64String(File.ReadAllBytes(_certificatePath)), + ClientCertificateKeyPhrase = _privateKeyPassword, + ClientCertificateSource = CertificateSource.String, + ConnectionTimeoutSeconds = 60, + FollowRedirects = true, + LoadEntireChainForCertificate = false, + Password = "", + ThrowExceptionOnErrorResponse = true, + Token = "", + Username = "domain\\username" + }; + + var result = await HTTP.DownloadFile(input, options, default); + + Assert.IsNotNull(result); + Assert.IsTrue(result.Success); + + CertificateHandler(_certificatePath, _privateKeyPassword, true, tp); + } + + [TestMethod] + public async Task TestFileDownload_WithNullHeaders() + { + var input = new Input + { + Url = _targetFileAddress, + FilePath = _filePath, + Headers = null + }; + + var options = new Options + { + AllowInvalidCertificate = true, + AllowInvalidResponseContentTypeCharSet = true, + Authentication = Authentication.Basic, + AutomaticCookieHandling = true, + CertificateThumbprint = "", + ClientCertificateFilePath = "", + ClientCertificateInBase64 = "", + ClientCertificateKeyPhrase = "", + ClientCertificateSource = CertificateSource.File, + ConnectionTimeoutSeconds = 60, + FollowRedirects = true, + LoadEntireChainForCertificate = false, + Password = "password", + ThrowExceptionOnErrorResponse = false, + Token = "", + Username = "domain\\username" + }; + + var result = await HTTP.DownloadFile(input, options, default); + + Assert.IsNotNull(result); + Assert.IsTrue(result.Success); + } + + [TestMethod] + public async Task TestFileDownload_CachedClient() + { + var input = new Input + { + Url = _targetFileAddress, + FilePath = _filePath, + Headers = null + }; + + var options = new Options + { + AllowInvalidCertificate = true, + AllowInvalidResponseContentTypeCharSet = true, + Authentication = Authentication.None, + AutomaticCookieHandling = true, + CertificateThumbprint = "", + ClientCertificateFilePath = "", + ClientCertificateInBase64 = "", + ClientCertificateKeyPhrase = "", + ClientCertificateSource = CertificateSource.File, + ConnectionTimeoutSeconds = 60, + FollowRedirects = true, + LoadEntireChainForCertificate = false, + Password = "", + ThrowExceptionOnErrorResponse = false, + Token = "", + Username = "" + }; + + // First request creates client + var result1 = await HTTP.DownloadFile(input, options, default); + Assert.IsTrue(result1.Success); + + // Cleanup and recreate directory for second request + Cleanup(); + Directory.CreateDirectory(_directory); + + // Second request should use cached client + var result2 = await HTTP.DownloadFile(input, options, default); + Assert.IsTrue(result2.Success); + } + + [TestMethod] + public async Task TestFileDownload_BasicAuth_WithEmptyHeaders() + { + var headers = new Header[0]; + + var input = new Input + { + Url = _targetFileAddress, + FilePath = _filePath, + Headers = headers + }; + + var options = new Options + { + AllowInvalidCertificate = true, + AllowInvalidResponseContentTypeCharSet = true, + Authentication = Authentication.Basic, + AutomaticCookieHandling = true, + CertificateThumbprint = "", + ClientCertificateFilePath = "", + ClientCertificateInBase64 = "", + ClientCertificateKeyPhrase = "", + ClientCertificateSource = CertificateSource.File, + ConnectionTimeoutSeconds = 60, + FollowRedirects = true, + LoadEntireChainForCertificate = false, + Password = "password", + ThrowExceptionOnErrorResponse = false, + Token = "", + Username = "domain\\user" + }; + + var result = await HTTP.DownloadFile(input, options, default); + + Assert.IsNotNull(result); + Assert.IsTrue(result.Success); + } + + [TestMethod] + public async Task TestFileDownload_OAuthAuth_WithEmptyHeaders() + { + var headers = new Header[0]; + + var input = new Input + { + Url = _targetFileAddress, + FilePath = _filePath, + Headers = headers + }; + + var options = new Options + { + AllowInvalidCertificate = true, + AllowInvalidResponseContentTypeCharSet = true, + Authentication = Authentication.OAuth, + AutomaticCookieHandling = true, + CertificateThumbprint = "", + ClientCertificateFilePath = "", + ClientCertificateInBase64 = "", + ClientCertificateKeyPhrase = "", + ClientCertificateSource = CertificateSource.File, + ConnectionTimeoutSeconds = 60, + FollowRedirects = true, + LoadEntireChainForCertificate = false, + Password = "", + ThrowExceptionOnErrorResponse = false, + Token = "my_oauth_token", + Username = "" + }; + + var result = await HTTP.DownloadFile(input, options, default); + + Assert.IsNotNull(result); + Assert.IsTrue(result.Success); + } } \ No newline at end of file From 5335864deed79c0cf2588b3ddb3923933312fc59 Mon Sep 17 00:00:00 2001 From: jefim Date: Thu, 4 Dec 2025 10:50:14 +0200 Subject: [PATCH 11/21] Clean up unit tests, remove http mock --- .../MockHttpClientFactory.cs | 19 --- .../UnitTests.cs | 156 +++--------------- .../Frends.HTTP.DownloadFile/Extensions.cs | 2 +- 3 files changed, 26 insertions(+), 151 deletions(-) delete mode 100644 Frends.HTTP.DownloadFile/Frends.HTTP.DownloadFile.Tests/MockHttpClientFactory.cs diff --git a/Frends.HTTP.DownloadFile/Frends.HTTP.DownloadFile.Tests/MockHttpClientFactory.cs b/Frends.HTTP.DownloadFile/Frends.HTTP.DownloadFile.Tests/MockHttpClientFactory.cs deleted file mode 100644 index c553184..0000000 --- a/Frends.HTTP.DownloadFile/Frends.HTTP.DownloadFile.Tests/MockHttpClientFactory.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System.Net.Http; -using Frends.HTTP.DownloadFile.Definitions; -using RichardSzalay.MockHttp; - -namespace Frends.HTTP.DownloadFile.Tests; - -public class MockHttpClientFactory : IHttpClientFactory -{ - private readonly MockHttpMessageHandler _mockHttpMessageHandler; - - public MockHttpClientFactory(MockHttpMessageHandler mockHttpMessageHandler) - { - _mockHttpMessageHandler = mockHttpMessageHandler; - } - public HttpClient CreateClient(Options options) - { - return _mockHttpMessageHandler.ToHttpClient(); - } -} diff --git a/Frends.HTTP.DownloadFile/Frends.HTTP.DownloadFile.Tests/UnitTests.cs b/Frends.HTTP.DownloadFile/Frends.HTTP.DownloadFile.Tests/UnitTests.cs index de50a27..cfb392a 100644 --- a/Frends.HTTP.DownloadFile/Frends.HTTP.DownloadFile.Tests/UnitTests.cs +++ b/Frends.HTTP.DownloadFile/Frends.HTTP.DownloadFile.Tests/UnitTests.cs @@ -1,14 +1,12 @@ using Frends.HTTP.DownloadFile.Definitions; using Microsoft.VisualStudio.TestTools.UnitTesting; -using Pluralsight.Crypto; -using RichardSzalay.MockHttp; using System; using System.Collections.Generic; using System.IO; -using System.Net; -using System.Net.Http; +using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using System.Threading.Tasks; +using Pluralsight.Crypto; namespace Frends.HTTP.DownloadFile.Tests; @@ -17,23 +15,16 @@ public class UnitTests { private static readonly string _directory = Path.Combine(Environment.CurrentDirectory, "testfiles"); private static readonly string _filePath = Path.Combine(_directory, "picture.jpg"); - private static readonly string _targetFileAddress = @"http://localhost:9999/testfile.png"; + + private static readonly string _targetFileAddress = + "https://frendsfonts.blob.core.windows.net/images/frendsLogo.png";//@"http://localhost:9999/testfile.png"; private readonly string _certificatePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "TestFiles", "certwithpk.pfx"); private readonly string _privateKeyPassword = "password"; - private MockHttpMessageHandler _mockHttpMessageHandler; - private static readonly byte[] _mockFileContent = new byte[] { 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A }; // PNG header bytes [TestInitialize] public void TestInitialize() { - _mockHttpMessageHandler = new MockHttpMessageHandler(); - _mockHttpMessageHandler.When(_targetFileAddress) - .Respond(req => new HttpResponseMessage(HttpStatusCode.OK) - { - Content = new ByteArrayContent(_mockFileContent) - }); HTTP.ClearClientCache(); - HTTP.ClientFactory = new MockHttpClientFactory(_mockHttpMessageHandler); Directory.CreateDirectory(_directory); } @@ -150,9 +141,21 @@ public async Task TestFileDownload_WithHeaders() { var headers = new[] { new Header() { Name = "foo", Value = "bar" } }; - var auths = new List() { Authentication.None, Authentication.Basic, Authentication.WindowsAuthentication, Authentication.WindowsIntegratedSecurity, Authentication.OAuth }; + var auths = new List + { + Authentication.None, + Authentication.Basic, + Authentication.WindowsAuthentication, + Authentication.WindowsIntegratedSecurity, + Authentication.OAuth + }; - var certSource = new List() { CertificateSource.CertificateStore, CertificateSource.File, CertificateSource.String }; + var certSource = new List + { + CertificateSource.CertificateStore, + CertificateSource.File, + CertificateSource.String + }; var input = new Input { @@ -200,7 +203,12 @@ public async Task TestFileDownload_WithHeaders() [TestMethod] public async Task TestFileDownload_Certification() { - var certSources = new List() { CertificateSource.File, CertificateSource.String, CertificateSource.CertificateStore }; + var certSources = new List + { + CertificateSource.File, + CertificateSource.String, + CertificateSource.CertificateStore + }; var input = new Input { @@ -480,44 +488,6 @@ await Assert.ThrowsExceptionAsync(async () => await HTTP.DownloadFile(input, options, default)); } - [TestMethod] - public async Task TestFileDownload_WithAuthorizationHeader_ShouldNotAddDuplicateAuth() - { - var headers = new[] { new Header() { Name = "Authorization", Value = "Bearer mytoken" } }; - - var input = new Input - { - Url = _targetFileAddress, - FilePath = _filePath, - Headers = headers - }; - - var options = new Options - { - AllowInvalidCertificate = true, - AllowInvalidResponseContentTypeCharSet = true, - Authentication = Authentication.OAuth, - AutomaticCookieHandling = true, - CertificateThumbprint = "", - ClientCertificateFilePath = "", - ClientCertificateInBase64 = "", - ClientCertificateKeyPhrase = "", - ClientCertificateSource = CertificateSource.File, - ConnectionTimeoutSeconds = 60, - FollowRedirects = true, - LoadEntireChainForCertificate = false, - Password = "", - ThrowExceptionOnErrorResponse = false, - Token = "different_token", - Username = "" - }; - - var result = await HTTP.DownloadFile(input, options, default); - - Assert.IsNotNull(result); - Assert.IsTrue(result.Success); - } - [TestMethod] [ExpectedException(typeof(Exception))] public async Task TestFileDownload_WindowsAuth_InvalidUsername_ShouldThrow() @@ -713,80 +683,4 @@ public async Task TestFileDownload_CachedClient() var result2 = await HTTP.DownloadFile(input, options, default); Assert.IsTrue(result2.Success); } - - [TestMethod] - public async Task TestFileDownload_BasicAuth_WithEmptyHeaders() - { - var headers = new Header[0]; - - var input = new Input - { - Url = _targetFileAddress, - FilePath = _filePath, - Headers = headers - }; - - var options = new Options - { - AllowInvalidCertificate = true, - AllowInvalidResponseContentTypeCharSet = true, - Authentication = Authentication.Basic, - AutomaticCookieHandling = true, - CertificateThumbprint = "", - ClientCertificateFilePath = "", - ClientCertificateInBase64 = "", - ClientCertificateKeyPhrase = "", - ClientCertificateSource = CertificateSource.File, - ConnectionTimeoutSeconds = 60, - FollowRedirects = true, - LoadEntireChainForCertificate = false, - Password = "password", - ThrowExceptionOnErrorResponse = false, - Token = "", - Username = "domain\\user" - }; - - var result = await HTTP.DownloadFile(input, options, default); - - Assert.IsNotNull(result); - Assert.IsTrue(result.Success); - } - - [TestMethod] - public async Task TestFileDownload_OAuthAuth_WithEmptyHeaders() - { - var headers = new Header[0]; - - var input = new Input - { - Url = _targetFileAddress, - FilePath = _filePath, - Headers = headers - }; - - var options = new Options - { - AllowInvalidCertificate = true, - AllowInvalidResponseContentTypeCharSet = true, - Authentication = Authentication.OAuth, - AutomaticCookieHandling = true, - CertificateThumbprint = "", - ClientCertificateFilePath = "", - ClientCertificateInBase64 = "", - ClientCertificateKeyPhrase = "", - ClientCertificateSource = CertificateSource.File, - ConnectionTimeoutSeconds = 60, - FollowRedirects = true, - LoadEntireChainForCertificate = false, - Password = "", - ThrowExceptionOnErrorResponse = false, - Token = "my_oauth_token", - Username = "" - }; - - var result = await HTTP.DownloadFile(input, options, default); - - Assert.IsNotNull(result); - Assert.IsTrue(result.Success); - } } \ No newline at end of file diff --git a/Frends.HTTP.DownloadFile/Frends.HTTP.DownloadFile/Extensions.cs b/Frends.HTTP.DownloadFile/Frends.HTTP.DownloadFile/Extensions.cs index fa9ca6e..1bc35f0 100644 --- a/Frends.HTTP.DownloadFile/Frends.HTTP.DownloadFile/Extensions.cs +++ b/Frends.HTTP.DownloadFile/Frends.HTTP.DownloadFile/Extensions.cs @@ -121,7 +121,7 @@ private static X509Certificate2[] GetCertificatesFromStore(string thumbprint, // include the whole chain var certificates = chain - .ChainElements.Cast() + .ChainElements .Select(c => c.Certificate) .OrderByDescending(c => c.HasPrivateKey) .ToArray(); From 4e11d2f8a6d57fa2121780825c711559e5aa63fc Mon Sep 17 00:00:00 2001 From: jefim Date: Mon, 5 Jan 2026 09:49:52 +0200 Subject: [PATCH 12/21] Refactor unit tests for file download with certificate handling and improve error handling --- .../UnitTests.cs | 229 ++---------------- 1 file changed, 17 insertions(+), 212 deletions(-) diff --git a/Frends.HTTP.DownloadFile/Frends.HTTP.DownloadFile.Tests/UnitTests.cs b/Frends.HTTP.DownloadFile/Frends.HTTP.DownloadFile.Tests/UnitTests.cs index cfb392a..30bf2a2 100644 --- a/Frends.HTTP.DownloadFile/Frends.HTTP.DownloadFile.Tests/UnitTests.cs +++ b/Frends.HTTP.DownloadFile/Frends.HTTP.DownloadFile.Tests/UnitTests.cs @@ -17,7 +17,7 @@ public class UnitTests private static readonly string _filePath = Path.Combine(_directory, "picture.jpg"); private static readonly string _targetFileAddress = - "https://frendsfonts.blob.core.windows.net/images/frendsLogo.png";//@"http://localhost:9999/testfile.png"; + "https://frendsfonts.blob.core.windows.net/images/frendsLogo.png"; private readonly string _certificatePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "TestFiles", "certwithpk.pfx"); private readonly string _privateKeyPassword = "password"; @@ -139,15 +139,15 @@ public async Task TestFileDownload_WithoutHeaders_AllFalse() [TestMethod] public async Task TestFileDownload_WithHeaders() { - var headers = new[] { new Header() { Name = "foo", Value = "bar" } }; + var headers = new[] { new Header { Name = "foo", Value = "bar" } }; var auths = new List { Authentication.None, - Authentication.Basic, - Authentication.WindowsAuthentication, - Authentication.WindowsIntegratedSecurity, - Authentication.OAuth + //Authentication.Basic, + //Authentication.WindowsAuthentication, + //Authentication.WindowsIntegratedSecurity, + //Authentication.OAuth }; var certSource = new List @@ -188,11 +188,18 @@ public async Task TestFileDownload_WithHeaders() Username = "domain\\username" }; - var result = await HTTP.DownloadFile(input, options, default); + try + { + var result = await HTTP.DownloadFile(input, options, default); - Assert.IsNotNull(result); - Assert.IsTrue(result.Success); - Assert.IsTrue(File.Exists(result.FilePath)); + Assert.IsNotNull(result); + Assert.IsTrue(result.Success); + Assert.IsTrue(File.Exists(result.FilePath)); + } + catch (Exception ex) + { + Assert.Fail($"Authentication: {auth}; Certificate Source: {cert}; Error: {ex.Message}"); + } Cleanup(); Directory.CreateDirectory(_directory); @@ -370,49 +377,6 @@ public async Task TestFileDownload_WithEmptyUrl_ShouldThrowException() await HTTP.DownloadFile(input, options, default); } - [TestMethod] - public async Task TestFileDownload_WithCertificateStoreLocation_CurrentUser() - { - var tp = CertificateHandler(_certificatePath, _privateKeyPassword, false, null); - - var input = new Input - { - Url = _targetFileAddress, - FilePath = _filePath, - Headers = null - }; - - var options = new Options - { - AllowInvalidCertificate = true, - AllowInvalidResponseContentTypeCharSet = true, - Authentication = Authentication.ClientCertificate, - AutomaticCookieHandling = true, - CertificateThumbprint = tp, - ClientCertificateFilePath = _certificatePath, - ClientCertificateInBase64 = "", - ClientCertificateKeyPhrase = _privateKeyPassword, - ClientCertificateSource = CertificateSource.CertificateStore, - CertificateStoreLocation = CertificateStoreLocation.CurrentUser, - ConnectionTimeoutSeconds = 60, - FollowRedirects = true, - LoadEntireChainForCertificate = false, - Password = "", - ThrowExceptionOnErrorResponse = true, - Token = "", - Username = "domain\\username" - }; - - var result = await HTTP.DownloadFile(input, options, default); - - Assert.IsNotNull(result); - Assert.IsTrue(result.Success); - Assert.IsNotNull(result.FilePath); - Assert.IsTrue(File.Exists(result.FilePath)); - - CertificateHandler(_certificatePath, _privateKeyPassword, true, tp); - } - [TestMethod] [ExpectedException(typeof(Exception))] public async Task TestFileDownload_WithCertificateStoreLocation_LocalMachine_NotFound() @@ -524,163 +488,4 @@ public async Task TestFileDownload_WindowsAuth_InvalidUsername_ShouldThrow() await HTTP.DownloadFile(input, options, default); } - - [TestMethod] - public async Task TestFileDownload_CertificateFromFile_WithoutKeyPhrase() - { - var tp = CertificateHandler(_certificatePath, _privateKeyPassword, false, null); - - var input = new Input - { - Url = _targetFileAddress, - FilePath = _filePath, - Headers = null - }; - - var options = new Options - { - AllowInvalidCertificate = true, - AllowInvalidResponseContentTypeCharSet = true, - Authentication = Authentication.ClientCertificate, - AutomaticCookieHandling = true, - CertificateThumbprint = "", - ClientCertificateFilePath = _certificatePath, - ClientCertificateInBase64 = "", - ClientCertificateKeyPhrase = _privateKeyPassword, - ClientCertificateSource = CertificateSource.File, - ConnectionTimeoutSeconds = 60, - FollowRedirects = true, - LoadEntireChainForCertificate = false, - Password = "", - ThrowExceptionOnErrorResponse = true, - Token = "", - Username = "domain\\username" - }; - - var result = await HTTP.DownloadFile(input, options, default); - - Assert.IsNotNull(result); - Assert.IsTrue(result.Success); - - CertificateHandler(_certificatePath, _privateKeyPassword, true, tp); - } - - [TestMethod] - public async Task TestFileDownload_CertificateFromString_WithKeyPhrase() - { - var tp = CertificateHandler(_certificatePath, _privateKeyPassword, false, null); - - var input = new Input - { - Url = _targetFileAddress, - FilePath = _filePath, - Headers = null - }; - - var options = new Options - { - AllowInvalidCertificate = true, - AllowInvalidResponseContentTypeCharSet = true, - Authentication = Authentication.ClientCertificate, - AutomaticCookieHandling = true, - CertificateThumbprint = "", - ClientCertificateFilePath = "", - ClientCertificateInBase64 = Convert.ToBase64String(File.ReadAllBytes(_certificatePath)), - ClientCertificateKeyPhrase = _privateKeyPassword, - ClientCertificateSource = CertificateSource.String, - ConnectionTimeoutSeconds = 60, - FollowRedirects = true, - LoadEntireChainForCertificate = false, - Password = "", - ThrowExceptionOnErrorResponse = true, - Token = "", - Username = "domain\\username" - }; - - var result = await HTTP.DownloadFile(input, options, default); - - Assert.IsNotNull(result); - Assert.IsTrue(result.Success); - - CertificateHandler(_certificatePath, _privateKeyPassword, true, tp); - } - - [TestMethod] - public async Task TestFileDownload_WithNullHeaders() - { - var input = new Input - { - Url = _targetFileAddress, - FilePath = _filePath, - Headers = null - }; - - var options = new Options - { - AllowInvalidCertificate = true, - AllowInvalidResponseContentTypeCharSet = true, - Authentication = Authentication.Basic, - AutomaticCookieHandling = true, - CertificateThumbprint = "", - ClientCertificateFilePath = "", - ClientCertificateInBase64 = "", - ClientCertificateKeyPhrase = "", - ClientCertificateSource = CertificateSource.File, - ConnectionTimeoutSeconds = 60, - FollowRedirects = true, - LoadEntireChainForCertificate = false, - Password = "password", - ThrowExceptionOnErrorResponse = false, - Token = "", - Username = "domain\\username" - }; - - var result = await HTTP.DownloadFile(input, options, default); - - Assert.IsNotNull(result); - Assert.IsTrue(result.Success); - } - - [TestMethod] - public async Task TestFileDownload_CachedClient() - { - var input = new Input - { - Url = _targetFileAddress, - FilePath = _filePath, - Headers = null - }; - - var options = new Options - { - AllowInvalidCertificate = true, - AllowInvalidResponseContentTypeCharSet = true, - Authentication = Authentication.None, - AutomaticCookieHandling = true, - CertificateThumbprint = "", - ClientCertificateFilePath = "", - ClientCertificateInBase64 = "", - ClientCertificateKeyPhrase = "", - ClientCertificateSource = CertificateSource.File, - ConnectionTimeoutSeconds = 60, - FollowRedirects = true, - LoadEntireChainForCertificate = false, - Password = "", - ThrowExceptionOnErrorResponse = false, - Token = "", - Username = "" - }; - - // First request creates client - var result1 = await HTTP.DownloadFile(input, options, default); - Assert.IsTrue(result1.Success); - - // Cleanup and recreate directory for second request - Cleanup(); - Directory.CreateDirectory(_directory); - - // Second request should use cached client - var result2 = await HTTP.DownloadFile(input, options, default); - Assert.IsTrue(result2.Success); - } } \ No newline at end of file From d5919f0db552b1bbe21af2bf629f1f6814e4dd8c Mon Sep 17 00:00:00 2001 From: Mateusz Noga-Wojtania Date: Mon, 2 Mar 2026 09:15:48 +0100 Subject: [PATCH 13/21] cr changes --- Frends.HTTP.DownloadFile/CHANGELOG.md | 2 +- .../Frends.HTTP.DownloadFile/Definitions/Options.cs | 9 ++++----- .../Frends.HTTP.DownloadFile.csproj | 4 ++-- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/Frends.HTTP.DownloadFile/CHANGELOG.md b/Frends.HTTP.DownloadFile/CHANGELOG.md index 20f6d39..efa744c 100644 --- a/Frends.HTTP.DownloadFile/CHANGELOG.md +++ b/Frends.HTTP.DownloadFile/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [1.4.0] - 2025-11-18 +## [1.4.0] - 2026-03-02 ### Added - Added CertificateStoreLocation option to allow selection between CurrentUser and LocalMachine certificate stores when using certificate authentication. diff --git a/Frends.HTTP.DownloadFile/Frends.HTTP.DownloadFile/Definitions/Options.cs b/Frends.HTTP.DownloadFile/Frends.HTTP.DownloadFile/Definitions/Options.cs index 9bb7b41..0b09d00 100644 --- a/Frends.HTTP.DownloadFile/Frends.HTTP.DownloadFile/Definitions/Options.cs +++ b/Frends.HTTP.DownloadFile/Frends.HTTP.DownloadFile/Definitions/Options.cs @@ -30,7 +30,7 @@ public class Options public string Password { get; set; } /// - /// Bearer token to be used for request. + /// Bearer token to be used for request. /// Token will be added as Authorization header. /// /// Token123 @@ -88,10 +88,10 @@ public class Options /// CurrentUser [UIHint(nameof(Authentication), "", Authentication.ClientCertificate)] [DefaultValue(CertificateStoreLocation.CurrentUser)] - public CertificateStoreLocation CertificateStoreLocation { get; set; } + public CertificateStoreLocation CertificateStoreLocation { get; set; } = CertificateStoreLocation.CurrentUser; /// - /// Should the entire certificate chain be loaded from the certificate store and included in the request. Only valid when using Certificate Store as the Certificate Source + /// Should the entire certificate chain be loaded from the certificate store and included in the request. Only valid when using Certificate Store as the Certificate Source /// /// true [UIHint(nameof(Authentication), "", Authentication.ClientCertificate)] @@ -142,6 +142,5 @@ public class Options /// /// false [DefaultValue(false)] - public bool Overwrite { get; set; } = false; - + public bool Overwrite { get; set; } } diff --git a/Frends.HTTP.DownloadFile/Frends.HTTP.DownloadFile/Frends.HTTP.DownloadFile.csproj b/Frends.HTTP.DownloadFile/Frends.HTTP.DownloadFile/Frends.HTTP.DownloadFile.csproj index a870dfb..f1e21d7 100644 --- a/Frends.HTTP.DownloadFile/Frends.HTTP.DownloadFile/Frends.HTTP.DownloadFile.csproj +++ b/Frends.HTTP.DownloadFile/Frends.HTTP.DownloadFile/Frends.HTTP.DownloadFile.csproj @@ -1,7 +1,7 @@  - net6.0 + net6.0 1.4.0 Frends Frends @@ -24,4 +24,4 @@ - \ No newline at end of file + From 44f24ecb4b12b9a883424ded48f4b9b90b6059f9 Mon Sep 17 00:00:00 2001 From: Mateusz Noga-Wojtania Date: Mon, 2 Mar 2026 09:17:46 +0100 Subject: [PATCH 14/21] crchanges --- .../Frends.HTTP.Request/Definitions/Options.cs | 4 ++-- .../Frends.HTTP.Request/Frends.HTTP.Request.csproj | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Frends.HTTP.Request/Frends.HTTP.Request/Definitions/Options.cs b/Frends.HTTP.Request/Frends.HTTP.Request/Definitions/Options.cs index 73f27d4..12bfe0a 100644 --- a/Frends.HTTP.Request/Frends.HTTP.Request/Definitions/Options.cs +++ b/Frends.HTTP.Request/Frends.HTTP.Request/Definitions/Options.cs @@ -87,10 +87,10 @@ public class Options /// CurrentUser [UIHint(nameof(Authentication), "", Authentication.ClientCertificate)] [DefaultValue(CertificateStoreLocation.CurrentUser)] - public CertificateStoreLocation CertificateStoreLocation { get; set; } + public CertificateStoreLocation CertificateStoreLocation { get; set; } = CertificateStoreLocation.CurrentUser; /// - /// Should the entire certificate chain be loaded from the certificate store and included in the request. Only valid when using Certificate Store as the Certificate Source + /// Should the entire certificate chain be loaded from the certificate store and included in the request. Only valid when using Certificate Store as the Certificate Source /// /// true [UIHint(nameof(Authentication), "", Authentication.ClientCertificate)] diff --git a/Frends.HTTP.Request/Frends.HTTP.Request/Frends.HTTP.Request.csproj b/Frends.HTTP.Request/Frends.HTTP.Request/Frends.HTTP.Request.csproj index 3eb843e..fe52021 100644 --- a/Frends.HTTP.Request/Frends.HTTP.Request/Frends.HTTP.Request.csproj +++ b/Frends.HTTP.Request/Frends.HTTP.Request/Frends.HTTP.Request.csproj @@ -1,8 +1,8 @@  - net6.0 - 1.6.0 + net6.0 + 1.7.0 Frends Frends Frends @@ -28,4 +28,4 @@ - \ No newline at end of file + From 8ab8a2d2862f4f60ff1cb0cc13bc0c3fe1839c80 Mon Sep 17 00:00:00 2001 From: Mateusz Noga-Wojtania Date: Mon, 2 Mar 2026 09:22:00 +0100 Subject: [PATCH 15/21] cr changes --- Frends.HTTP.RequestBytes/CHANGELOG.md | 2 +- .../Frends.HTTP.RequestBytes/Definitions/Options.cs | 4 ++-- .../Frends.HTTP.RequestBytes/Frends.HTTP.RequestBytes.csproj | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Frends.HTTP.RequestBytes/CHANGELOG.md b/Frends.HTTP.RequestBytes/CHANGELOG.md index e972dc9..1da35ca 100644 --- a/Frends.HTTP.RequestBytes/CHANGELOG.md +++ b/Frends.HTTP.RequestBytes/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [1.4.0] - 2025-11-18 +## [1.4.0] - 2026-03-02 ### Added - Added CertificateStoreLocation option to allow selection between CurrentUser and LocalMachine certificate stores when using certificate authentication. diff --git a/Frends.HTTP.RequestBytes/Frends.HTTP.RequestBytes/Definitions/Options.cs b/Frends.HTTP.RequestBytes/Frends.HTTP.RequestBytes/Definitions/Options.cs index eb3953f..d652b2a 100644 --- a/Frends.HTTP.RequestBytes/Frends.HTTP.RequestBytes/Definitions/Options.cs +++ b/Frends.HTTP.RequestBytes/Frends.HTTP.RequestBytes/Definitions/Options.cs @@ -87,10 +87,10 @@ public class Options /// CurrentUser [UIHint(nameof(Authentication), "", Authentication.ClientCertificate)] [DefaultValue(CertificateStoreLocation.CurrentUser)] - public CertificateStoreLocation CertificateStoreLocation { get; set; } + public CertificateStoreLocation CertificateStoreLocation { get; set; } = CertificateStoreLocation.CurrentUser; /// - /// Should the entire certificate chain be loaded from the certificate store and included in the request. Only valid when using Certificate Store as the Certificate Source + /// Should the entire certificate chain be loaded from the certificate store and included in the request. Only valid when using Certificate Store as the Certificate Source /// /// true [UIHint(nameof(Authentication), "", Authentication.ClientCertificate)] diff --git a/Frends.HTTP.RequestBytes/Frends.HTTP.RequestBytes/Frends.HTTP.RequestBytes.csproj b/Frends.HTTP.RequestBytes/Frends.HTTP.RequestBytes/Frends.HTTP.RequestBytes.csproj index 4d7743f..94040f2 100644 --- a/Frends.HTTP.RequestBytes/Frends.HTTP.RequestBytes/Frends.HTTP.RequestBytes.csproj +++ b/Frends.HTTP.RequestBytes/Frends.HTTP.RequestBytes/Frends.HTTP.RequestBytes.csproj @@ -1,7 +1,7 @@  - net6.0 + net6.0 1.4.0 Frends Frends @@ -28,4 +28,4 @@ - \ No newline at end of file + From 33d788282642b6c7f45eaf5f1badeb5129ae0efd Mon Sep 17 00:00:00 2001 From: Mateusz Noga-Wojtania Date: Mon, 2 Mar 2026 09:26:23 +0100 Subject: [PATCH 16/21] cr changes --- Frends.HTTP.SendAndReceiveBytes/CHANGELOG.md | 2 +- .../Frends.HTTP.SendAndReceiveBytes/Definitions/Options.cs | 4 ++-- .../Frends.HTTP.SendAndReceiveBytes.csproj | 4 ++-- Frends.HTTP.SendBytes/CHANGELOG.md | 2 +- .../Frends.HTTP.SendBytes/Definitions/Options.cs | 4 ++-- .../Frends.HTTP.SendBytes/Frends.HTTP.SendBytes.csproj | 4 ++-- 6 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Frends.HTTP.SendAndReceiveBytes/CHANGELOG.md b/Frends.HTTP.SendAndReceiveBytes/CHANGELOG.md index a3ea9bc..13c581f 100644 --- a/Frends.HTTP.SendAndReceiveBytes/CHANGELOG.md +++ b/Frends.HTTP.SendAndReceiveBytes/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [1.4.0] - 2025-11-18 +## [1.4.0] - 2026-03-02 ### Added - Added CertificateStoreLocation option to allow selection between CurrentUser and LocalMachine certificate stores when using certificate authentication. diff --git a/Frends.HTTP.SendAndReceiveBytes/Frends.HTTP.SendAndReceiveBytes/Definitions/Options.cs b/Frends.HTTP.SendAndReceiveBytes/Frends.HTTP.SendAndReceiveBytes/Definitions/Options.cs index 22e4c4b..f5cd482 100644 --- a/Frends.HTTP.SendAndReceiveBytes/Frends.HTTP.SendAndReceiveBytes/Definitions/Options.cs +++ b/Frends.HTTP.SendAndReceiveBytes/Frends.HTTP.SendAndReceiveBytes/Definitions/Options.cs @@ -90,10 +90,10 @@ public class Options /// CurrentUser [UIHint(nameof(Authentication), "", Authentication.ClientCertificate)] [DefaultValue(CertificateStoreLocation.CurrentUser)] - public CertificateStoreLocation CertificateStoreLocation { get; set; } + public CertificateStoreLocation CertificateStoreLocation { get; set; } = CertificateStoreLocation.CurrentUser; /// - /// Should the entire certificate chain be loaded from the certificate store and included in the request. Only valid when using Certificate Store as the Certificate Source + /// Should the entire certificate chain be loaded from the certificate store and included in the request. Only valid when using Certificate Store as the Certificate Source /// /// true [UIHint(nameof(Authentication), "", Authentication.ClientCertificate)] diff --git a/Frends.HTTP.SendAndReceiveBytes/Frends.HTTP.SendAndReceiveBytes/Frends.HTTP.SendAndReceiveBytes.csproj b/Frends.HTTP.SendAndReceiveBytes/Frends.HTTP.SendAndReceiveBytes/Frends.HTTP.SendAndReceiveBytes.csproj index 1d53546..faf664b 100644 --- a/Frends.HTTP.SendAndReceiveBytes/Frends.HTTP.SendAndReceiveBytes/Frends.HTTP.SendAndReceiveBytes.csproj +++ b/Frends.HTTP.SendAndReceiveBytes/Frends.HTTP.SendAndReceiveBytes/Frends.HTTP.SendAndReceiveBytes.csproj @@ -1,7 +1,7 @@  - net6.0 + net6.0 1.4.0 Frends Frends @@ -28,4 +28,4 @@ - \ No newline at end of file + diff --git a/Frends.HTTP.SendBytes/CHANGELOG.md b/Frends.HTTP.SendBytes/CHANGELOG.md index fac8fe5..15b2e3f 100644 --- a/Frends.HTTP.SendBytes/CHANGELOG.md +++ b/Frends.HTTP.SendBytes/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [1.5.0] - 2025-11-18 +## [1.5.0] - 2026-03-02 ### Added - Added CertificateStoreLocation option to allow selection between CurrentUser and LocalMachine certificate stores when using certificate authentication. diff --git a/Frends.HTTP.SendBytes/Frends.HTTP.SendBytes/Definitions/Options.cs b/Frends.HTTP.SendBytes/Frends.HTTP.SendBytes/Definitions/Options.cs index 2ff9451..9479810 100644 --- a/Frends.HTTP.SendBytes/Frends.HTTP.SendBytes/Definitions/Options.cs +++ b/Frends.HTTP.SendBytes/Frends.HTTP.SendBytes/Definitions/Options.cs @@ -87,10 +87,10 @@ public class Options /// CurrentUser [UIHint(nameof(Authentication), "", Authentication.ClientCertificate)] [DefaultValue(CertificateStoreLocation.CurrentUser)] - public CertificateStoreLocation CertificateStoreLocation { get; set; } + public CertificateStoreLocation CertificateStoreLocation { get; set; } = CertificateStoreLocation.CurrentUser; /// - /// Should the entire certificate chain be loaded from the certificate store and included in the request. Only valid when using Certificate Store as the Certificate Source + /// Should the entire certificate chain be loaded from the certificate store and included in the request. Only valid when using Certificate Store as the Certificate Source /// /// true [UIHint(nameof(Authentication), "", Authentication.ClientCertificate)] diff --git a/Frends.HTTP.SendBytes/Frends.HTTP.SendBytes/Frends.HTTP.SendBytes.csproj b/Frends.HTTP.SendBytes/Frends.HTTP.SendBytes/Frends.HTTP.SendBytes.csproj index 781c5c0..52a6588 100644 --- a/Frends.HTTP.SendBytes/Frends.HTTP.SendBytes/Frends.HTTP.SendBytes.csproj +++ b/Frends.HTTP.SendBytes/Frends.HTTP.SendBytes/Frends.HTTP.SendBytes.csproj @@ -1,7 +1,7 @@  - net6.0 + net6.0 1.5.0 Frends Frends @@ -27,4 +27,4 @@ - \ No newline at end of file + From 82ee23183f94f0ee37bf6928bdd3d538b0d69a4f Mon Sep 17 00:00:00 2001 From: Mateusz Noga-Wojtania Date: Mon, 2 Mar 2026 09:54:08 +0100 Subject: [PATCH 17/21] linter fixes --- .../Frends.HTTP.DownloadFile/Extensions.cs | 32 ++++++++++++----- .../Frends.HTTP.Request/Extensions.cs | 34 ++++++++++++------ .../Frends.HTTP.RequestBytes/Extensions.cs | 22 ++++++++---- .../Extensions.cs | 36 +++++++++++++------ .../Frends.HTTP.SendBytes/Extensions.cs | 22 ++++++++---- 5 files changed, 103 insertions(+), 43 deletions(-) diff --git a/Frends.HTTP.DownloadFile/Frends.HTTP.DownloadFile/Extensions.cs b/Frends.HTTP.DownloadFile/Frends.HTTP.DownloadFile/Extensions.cs index 1bc35f0..8575040 100644 --- a/Frends.HTTP.DownloadFile/Frends.HTTP.DownloadFile/Extensions.cs +++ b/Frends.HTTP.DownloadFile/Frends.HTTP.DownloadFile/Extensions.cs @@ -17,17 +17,22 @@ internal static void SetHandlerSettingsBasedOnOptions(this HttpClientHandler han { case Authentication.WindowsIntegratedSecurity: handler.UseDefaultCredentials = true; + break; case Authentication.WindowsAuthentication: var domainAndUserName = options.Username.Split('\\'); + if (domainAndUserName.Length != 2) - throw new ArgumentException($@"Username needs to be 'domain\username' now it was '{options.Username}'"); + throw new ArgumentException( + $@"Username needs to be 'domain\username' now it was '{options.Username}'"); handler.Credentials = new NetworkCredential(domainAndUserName[1], options.Password, domainAndUserName[0]); + break; case Authentication.ClientCertificate: handler.ClientCertificates.AddRange(GetCertificates(options)); + break; } @@ -54,13 +59,19 @@ private static X509Certificate[] GetCertificates(Options options) { case CertificateSource.CertificateStore: var thumbprint = options.CertificateThumbprint; - certificates = GetCertificatesFromStore(thumbprint, options.LoadEntireChainForCertificate, options.CertificateStoreLocation); + certificates = GetCertificatesFromStore(thumbprint, options.LoadEntireChainForCertificate, + options.CertificateStoreLocation); + break; case CertificateSource.File: - certificates = GetCertificatesFromFile(options.ClientCertificateFilePath, options.ClientCertificateKeyPhrase); + certificates = GetCertificatesFromFile(options.ClientCertificateFilePath, + options.ClientCertificateKeyPhrase); + break; case CertificateSource.String: - certificates = GetCertificatesFromString(options.ClientCertificateInBase64, options.ClientCertificateKeyPhrase); + certificates = GetCertificatesFromString(options.ClientCertificateInBase64, + options.ClientCertificateKeyPhrase); + break; default: throw new Exception("Unsupported Certificate source"); @@ -72,6 +83,7 @@ private static X509Certificate[] GetCertificates(Options options) private static X509Certificate2[] GetCertificatesFromString(string certificateContentsBase64, string keyPhrase) { var certificateBytes = Convert.FromBase64String(certificateContentsBase64); + return LoadCertificatesFromBytes(certificateBytes, keyPhrase); } @@ -85,7 +97,6 @@ private static X509Certificate2[] LoadCertificatesFromBytes(byte[] certificateBy collection.Import(certificateBytes, null, X509KeyStorageFlags.PersistKeySet); return collection.Cast().OrderByDescending(c => c.HasPrivateKey).ToArray(); - } private static X509Certificate2[] GetCertificatesFromFile(string clientCertificateFilePath, string keyPhrase) @@ -107,13 +118,18 @@ private static X509Certificate2[] GetCertificatesFromStore(string thumbprint, using var store = new X509Store(StoreName.My, location); store.Open(OpenFlags.ReadOnly); var signingCert = store.Certificates.Find(X509FindType.FindByThumbprint, thumbprint, false); + if (signingCert.Count == 0) - throw new FileNotFoundException($"Certificate with thumbprint: '{thumbprint}' not found in {locationText} cert store."); + throw new FileNotFoundException( + $"Certificate with thumbprint: '{thumbprint}' not found in {locationText} cert store."); var certificate = signingCert[0]; if (!loadEntireChain) - return new[] { certificate }; + return new[] + { + certificate + }; var chain = new X509Chain(); chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck; @@ -128,4 +144,4 @@ private static X509Certificate2[] GetCertificatesFromStore(string thumbprint, return certificates; } -} \ No newline at end of file +} diff --git a/Frends.HTTP.Request/Frends.HTTP.Request/Extensions.cs b/Frends.HTTP.Request/Frends.HTTP.Request/Extensions.cs index ba4efdf..eae9dc8 100644 --- a/Frends.HTTP.Request/Frends.HTTP.Request/Extensions.cs +++ b/Frends.HTTP.Request/Frends.HTTP.Request/Extensions.cs @@ -19,9 +19,11 @@ internal static void SetHandlerSettingsBasedOnOptions(this HttpClientHandler han { case Authentication.WindowsIntegratedSecurity: handler.UseDefaultCredentials = true; + break; case Authentication.WindowsAuthentication: var domainAndUserName = options.Username.Split('\\'); + if (domainAndUserName.Length != 2) { throw new ArgumentException( @@ -30,9 +32,11 @@ internal static void SetHandlerSettingsBasedOnOptions(this HttpClientHandler han handler.Credentials = new NetworkCredential(domainAndUserName[1], options.Password, domainAndUserName[0]); + break; case Authentication.ClientCertificate: handler.ClientCertificates.AddRange(GetCertificates(options)); + break; } @@ -61,13 +65,19 @@ private static X509Certificate[] GetCertificates(Options options) { case CertificateSource.CertificateStore: var thumbprint = options.CertificateThumbprint; - certificates = GetCertificatesFromStore(thumbprint, options.LoadEntireChainForCertificate, options.CertificateStoreLocation); + certificates = GetCertificatesFromStore(thumbprint, options.LoadEntireChainForCertificate, + options.CertificateStoreLocation); + break; case CertificateSource.File: - certificates = GetCertificatesFromFile(options.ClientCertificateFilePath, options.ClientCertificateKeyPhrase); + certificates = GetCertificatesFromFile(options.ClientCertificateFilePath, + options.ClientCertificateKeyPhrase); + break; case CertificateSource.String: - certificates = GetCertificatesFromString(options.ClientCertificateInBase64, options.ClientCertificateKeyPhrase); + certificates = GetCertificatesFromString(options.ClientCertificateInBase64, + options.ClientCertificateKeyPhrase); + break; default: throw new Exception("Unsupported Certificate source"); @@ -95,8 +105,8 @@ private static X509Certificate2[] LoadCertificatesFromBytes(byte[] certificateBy { collection.Import(certificateBytes, null, X509KeyStorageFlags.PersistKeySet); } - return collection.Cast().OrderByDescending(c => c.HasPrivateKey).ToArray(); + return collection.Cast().OrderByDescending(c => c.HasPrivateKey).ToArray(); } private static X509Certificate2[] GetCertificatesFromFile(string clientCertificateFilePath, string keyPhrase) @@ -108,17 +118,18 @@ private static X509Certificate2[] GetCertificatesFromStore(string thumbprint, bool loadEntireChain, CertificateStoreLocation storeLocation) { thumbprint = Regex.Replace(thumbprint, @"[^\da-zA-z]", string.Empty).ToUpper(); - var location = storeLocation == CertificateStoreLocation.CurrentUser - ? StoreLocation.CurrentUser + var location = storeLocation == CertificateStoreLocation.CurrentUser + ? StoreLocation.CurrentUser : StoreLocation.LocalMachine; - var locationText = storeLocation == CertificateStoreLocation.CurrentUser - ? "current user" + var locationText = storeLocation == CertificateStoreLocation.CurrentUser + ? "current user" : "local machine"; - + using (var store = new X509Store(StoreName.My, location)) { store.Open(OpenFlags.ReadOnly); var signingCert = store.Certificates.Find(X509FindType.FindByThumbprint, thumbprint, false); + if (signingCert.Count == 0) { throw new FileNotFoundException( @@ -130,7 +141,10 @@ private static X509Certificate2[] GetCertificatesFromStore(string thumbprint, if (!loadEntireChain) { - return new[] { certificate }; + return new[] + { + certificate + }; } using var chain = new X509Chain(); diff --git a/Frends.HTTP.RequestBytes/Frends.HTTP.RequestBytes/Extensions.cs b/Frends.HTTP.RequestBytes/Frends.HTTP.RequestBytes/Extensions.cs index 48b4807..41b5f94 100644 --- a/Frends.HTTP.RequestBytes/Frends.HTTP.RequestBytes/Extensions.cs +++ b/Frends.HTTP.RequestBytes/Frends.HTTP.RequestBytes/Extensions.cs @@ -19,9 +19,11 @@ internal static void SetHandlerSettingsBasedOnOptions(this HttpClientHandler han { case Authentication.WindowsIntegratedSecurity: handler.UseDefaultCredentials = true; + break; case Authentication.WindowsAuthentication: var domainAndUserName = options.Username.Split('\\'); + if (domainAndUserName.Length != 2) { throw new ArgumentException( @@ -30,9 +32,11 @@ internal static void SetHandlerSettingsBasedOnOptions(this HttpClientHandler han handler.Credentials = new NetworkCredential(domainAndUserName[1], options.Password, domainAndUserName[0]); + break; case Authentication.ClientCertificate: handler.ClientCertificates.AddRange(GetCertificates(options)); + break; } @@ -62,12 +66,15 @@ private static X509Certificate[] GetCertificates(Options options) case CertificateSource.CertificateStore: var thumbprint = options.CertificateThumbprint; certificates = GetCertificatesFromStore(thumbprint, options.LoadEntireChainForCertificate, options.CertificateStoreLocation); + break; case CertificateSource.File: certificates = GetCertificatesFromFile(options.ClientCertificateFilePath, options.ClientCertificateKeyPhrase); + break; case CertificateSource.String: certificates = GetCertificatesFromString(options.ClientCertificateInBase64, options.ClientCertificateKeyPhrase); + break; default: throw new Exception("Unsupported Certificate source"); @@ -95,8 +102,8 @@ private static X509Certificate2[] LoadCertificatesFromBytes(byte[] certificateBy { collection.Import(certificateBytes, null, X509KeyStorageFlags.PersistKeySet); } - return collection.Cast().OrderByDescending(c => c.HasPrivateKey).ToArray(); + return collection.Cast().OrderByDescending(c => c.HasPrivateKey).ToArray(); } private static X509Certificate2[] GetCertificatesFromFile(string clientCertificateFilePath, string keyPhrase) @@ -108,17 +115,18 @@ private static X509Certificate2[] GetCertificatesFromStore(string thumbprint, bool loadEntireChain, CertificateStoreLocation storeLocation) { thumbprint = Regex.Replace(thumbprint, @"[^\da-zA-z]", string.Empty).ToUpper(); - var location = storeLocation == CertificateStoreLocation.CurrentUser - ? StoreLocation.CurrentUser + var location = storeLocation == CertificateStoreLocation.CurrentUser + ? StoreLocation.CurrentUser : StoreLocation.LocalMachine; - var locationText = storeLocation == CertificateStoreLocation.CurrentUser - ? "current user" + var locationText = storeLocation == CertificateStoreLocation.CurrentUser + ? "current user" : "local machine"; - + using (var store = new X509Store(StoreName.My, location)) { store.Open(OpenFlags.ReadOnly); var signingCert = store.Certificates.Find(X509FindType.FindByThumbprint, thumbprint, false); + if (signingCert.Count == 0) { throw new FileNotFoundException( @@ -147,4 +155,4 @@ private static X509Certificate2[] GetCertificatesFromStore(string thumbprint, return certificates; } } -} \ No newline at end of file +} diff --git a/Frends.HTTP.SendAndReceiveBytes/Frends.HTTP.SendAndReceiveBytes/Extensions.cs b/Frends.HTTP.SendAndReceiveBytes/Frends.HTTP.SendAndReceiveBytes/Extensions.cs index 8340723..fdd3fe5 100644 --- a/Frends.HTTP.SendAndReceiveBytes/Frends.HTTP.SendAndReceiveBytes/Extensions.cs +++ b/Frends.HTTP.SendAndReceiveBytes/Frends.HTTP.SendAndReceiveBytes/Extensions.cs @@ -19,18 +19,22 @@ internal static void SetHandlerSettingsBasedOnOptions(this HttpClientHandler han { case Authentication.WindowsIntegratedSecurity: handler.UseDefaultCredentials = true; + break; case Authentication.WindowsAuthentication: var domainAndUserName = options.Username.Split('\\'); + if (domainAndUserName.Length != 2) throw new ArgumentException( $@"Username needs to be 'domain\username' now it was '{options.Username}'"); handler.Credentials = new NetworkCredential(domainAndUserName[1], options.Password, domainAndUserName[0]); + break; case Authentication.ClientCertificate: handler.ClientCertificates.AddRange(GetCertificates(options)); + break; } @@ -59,13 +63,19 @@ private static X509Certificate[] GetCertificates(Options options) { case CertificateSource.CertificateStore: var thumbprint = options.CertificateThumbprint; - certificates = GetCertificatesFromStore(thumbprint, options.LoadEntireChainForCertificate, options.CertificateStoreLocation); + certificates = GetCertificatesFromStore(thumbprint, options.LoadEntireChainForCertificate, + options.CertificateStoreLocation); + break; case CertificateSource.File: - certificates = GetCertificatesFromFile(options.ClientCertificateFilePath, options.ClientCertificateKeyPhrase); + certificates = GetCertificatesFromFile(options.ClientCertificateFilePath, + options.ClientCertificateKeyPhrase); + break; case CertificateSource.String: - certificates = GetCertificatesFromString(options.ClientCertificateInBase64, options.ClientCertificateKeyPhrase); + certificates = GetCertificatesFromString(options.ClientCertificateInBase64, + options.ClientCertificateKeyPhrase); + break; default: throw new Exception("Unsupported Certificate source"); @@ -93,8 +103,8 @@ private static X509Certificate2[] LoadCertificatesFromBytes(byte[] certificateBy { collection.Import(certificateBytes, null, X509KeyStorageFlags.PersistKeySet); } - return collection.Cast().OrderByDescending(c => c.HasPrivateKey).ToArray(); + return collection.Cast().OrderByDescending(c => c.HasPrivateKey).ToArray(); } private static X509Certificate2[] GetCertificatesFromFile(string clientCertificateFilePath, string keyPhrase) @@ -106,17 +116,18 @@ private static X509Certificate2[] GetCertificatesFromStore(string thumbprint, bool loadEntireChain, CertificateStoreLocation storeLocation) { thumbprint = Regex.Replace(thumbprint, @"[^\da-zA-z]", string.Empty).ToUpper(); - var location = storeLocation == CertificateStoreLocation.CurrentUser - ? StoreLocation.CurrentUser + var location = storeLocation == CertificateStoreLocation.CurrentUser + ? StoreLocation.CurrentUser : StoreLocation.LocalMachine; - var locationText = storeLocation == CertificateStoreLocation.CurrentUser - ? "current user" + var locationText = storeLocation == CertificateStoreLocation.CurrentUser + ? "current user" : "local machine"; - + using (var store = new X509Store(StoreName.My, location)) { store.Open(OpenFlags.ReadOnly); var signingCert = store.Certificates.Find(X509FindType.FindByThumbprint, thumbprint, false); + if (signingCert.Count == 0) { throw new FileNotFoundException( @@ -128,7 +139,10 @@ private static X509Certificate2[] GetCertificatesFromStore(string thumbprint, if (!loadEntireChain) { - return new[] { certificate }; + return new[] + { + certificate + }; } var chain = new X509Chain(); @@ -145,4 +159,4 @@ private static X509Certificate2[] GetCertificatesFromStore(string thumbprint, return certificates; } } -} \ No newline at end of file +} diff --git a/Frends.HTTP.SendBytes/Frends.HTTP.SendBytes/Extensions.cs b/Frends.HTTP.SendBytes/Frends.HTTP.SendBytes/Extensions.cs index 3adee62..93e6276 100644 --- a/Frends.HTTP.SendBytes/Frends.HTTP.SendBytes/Extensions.cs +++ b/Frends.HTTP.SendBytes/Frends.HTTP.SendBytes/Extensions.cs @@ -19,18 +19,22 @@ internal static void SetHandlerSettingsBasedOnOptions(this HttpClientHandler han { case Authentication.WindowsIntegratedSecurity: handler.UseDefaultCredentials = true; + break; case Authentication.WindowsAuthentication: var domainAndUserName = options.Username.Split('\\'); + if (domainAndUserName.Length != 2) throw new ArgumentException( $@"Username needs to be 'domain\username' now it was '{options.Username}'"); handler.Credentials = new NetworkCredential(domainAndUserName[1], options.Password, domainAndUserName[0]); + break; case Authentication.ClientCertificate: handler.ClientCertificates.AddRange(GetCertificates(options)); + break; } @@ -60,12 +64,15 @@ private static X509Certificate[] GetCertificates(Options options) case CertificateSource.CertificateStore: var thumbprint = options.CertificateThumbprint; certificates = GetCertificatesFromStore(thumbprint, options.LoadEntireChainForCertificate, options.CertificateStoreLocation); + break; case CertificateSource.File: certificates = GetCertificatesFromFile(options.ClientCertificateFilePath, options.ClientCertificateKeyPhrase); + break; case CertificateSource.String: certificates = GetCertificatesFromString(options.ClientCertificateInBase64, options.ClientCertificateKeyPhrase); + break; default: throw new Exception("Unsupported Certificate source"); @@ -93,8 +100,8 @@ private static X509Certificate2[] LoadCertificatesFromBytes(byte[] certificateBy { collection.Import(certificateBytes, null, X509KeyStorageFlags.PersistKeySet); } - return collection.Cast().OrderByDescending(c => c.HasPrivateKey).ToArray(); + return collection.Cast().OrderByDescending(c => c.HasPrivateKey).ToArray(); } private static X509Certificate2[] GetCertificatesFromFile(string clientCertificateFilePath, string keyPhrase) @@ -106,17 +113,18 @@ private static X509Certificate2[] GetCertificatesFromStore(string thumbprint, bool loadEntireChain, CertificateStoreLocation storeLocation) { thumbprint = Regex.Replace(thumbprint, @"[^\da-zA-z]", string.Empty).ToUpper(); - var location = storeLocation == CertificateStoreLocation.CurrentUser - ? StoreLocation.CurrentUser + var location = storeLocation == CertificateStoreLocation.CurrentUser + ? StoreLocation.CurrentUser : StoreLocation.LocalMachine; - var locationText = storeLocation == CertificateStoreLocation.CurrentUser - ? "current user" + var locationText = storeLocation == CertificateStoreLocation.CurrentUser + ? "current user" : "local machine"; - + using (var store = new X509Store(StoreName.My, location)) { store.Open(OpenFlags.ReadOnly); var signingCert = store.Certificates.Find(X509FindType.FindByThumbprint, thumbprint, false); + if (signingCert.Count == 0) { throw new FileNotFoundException( @@ -145,4 +153,4 @@ private static X509Certificate2[] GetCertificatesFromStore(string thumbprint, return certificates; } } -} \ No newline at end of file +} From 24fb38d84308b8c3be920c1cd0f3f746f1bd0c28 Mon Sep 17 00:00:00 2001 From: Mateusz Noga-Wojtania Date: Mon, 2 Mar 2026 11:48:06 +0100 Subject: [PATCH 18/21] add tests --- .../UnitTests.cs | 78 ++++++-- .../Frends.HTTP.Request.Tests/UnitTests.cs | 166 +++++++++++++++--- .../UnitTests.cs | 117 ++++++++++-- .../UnitTests.cs | 98 +++++++++-- .../Frends.HTTP.SendBytes.Tests/UnitTests.cs | 98 +++++++++-- 5 files changed, 472 insertions(+), 85 deletions(-) diff --git a/Frends.HTTP.DownloadFile/Frends.HTTP.DownloadFile.Tests/UnitTests.cs b/Frends.HTTP.DownloadFile/Frends.HTTP.DownloadFile.Tests/UnitTests.cs index 30bf2a2..ff7e677 100644 --- a/Frends.HTTP.DownloadFile/Frends.HTTP.DownloadFile.Tests/UnitTests.cs +++ b/Frends.HTTP.DownloadFile/Frends.HTTP.DownloadFile.Tests/UnitTests.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Net.Http; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using System.Threading.Tasks; @@ -18,7 +19,10 @@ public class UnitTests private static readonly string _targetFileAddress = "https://frendsfonts.blob.core.windows.net/images/frendsLogo.png"; - private readonly string _certificatePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "TestFiles", "certwithpk.pfx"); + + private readonly string _certificatePath = + Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "TestFiles", "certwithpk.pfx"); + private readonly string _privateKeyPassword = "password"; [TestInitialize] @@ -38,9 +42,21 @@ public void Cleanup() [TestMethod] public async Task TestFileDownload_WithoutHeaders_AllTrue() { - var auths = new List() { Authentication.None, Authentication.Basic, Authentication.WindowsAuthentication, Authentication.WindowsIntegratedSecurity, Authentication.OAuth }; + var auths = new List() + { + Authentication.None, + Authentication.Basic, + Authentication.WindowsAuthentication, + Authentication.WindowsIntegratedSecurity, + Authentication.OAuth + }; - var certSource = new List() { CertificateSource.CertificateStore, CertificateSource.File, CertificateSource.String }; + var certSource = new List() + { + CertificateSource.CertificateStore, + CertificateSource.File, + CertificateSource.String + }; var input = new Input { @@ -89,9 +105,21 @@ public async Task TestFileDownload_WithoutHeaders_AllTrue() [TestMethod] public async Task TestFileDownload_WithoutHeaders_AllFalse() { - var auths = new List() { Authentication.None, Authentication.Basic, Authentication.WindowsAuthentication, Authentication.WindowsIntegratedSecurity, Authentication.OAuth }; + var auths = new List() + { + Authentication.None, + Authentication.Basic, + Authentication.WindowsAuthentication, + Authentication.WindowsIntegratedSecurity, + Authentication.OAuth + }; - var certSource = new List() { CertificateSource.CertificateStore, CertificateSource.File, CertificateSource.String }; + var certSource = new List() + { + CertificateSource.CertificateStore, + CertificateSource.File, + CertificateSource.String + }; var input = new Input { @@ -139,7 +167,14 @@ public async Task TestFileDownload_WithoutHeaders_AllFalse() [TestMethod] public async Task TestFileDownload_WithHeaders() { - var headers = new[] { new Header { Name = "foo", Value = "bar" } }; + var headers = new[] + { + new Header + { + Name = "foo", + Value = "bar" + } + }; var auths = new List { @@ -236,7 +271,8 @@ public async Task TestFileDownload_Certification() AutomaticCookieHandling = true, CertificateThumbprint = tp, ClientCertificateFilePath = _certificatePath, - ClientCertificateInBase64 = cert is CertificateSource.String ? Convert.ToBase64String(File.ReadAllBytes(_certificatePath)) : "", + ClientCertificateInBase64 = + cert is CertificateSource.String ? Convert.ToBase64String(File.ReadAllBytes(_certificatePath)) : "", ClientCertificateKeyPhrase = _privateKeyPassword, ClientCertificateSource = cert, ConnectionTimeoutSeconds = 60, @@ -259,7 +295,6 @@ public async Task TestFileDownload_Certification() Directory.CreateDirectory(_directory); CertificateHandler(_certificatePath, _privateKeyPassword, true, tp); } - } private static string CertificateHandler(string path, string password, bool cleanUp, string thumbPrint) @@ -290,6 +325,7 @@ private static string CertificateHandler(string path, string password, bool clea } File.WriteAllBytes(path, certData); + return cert.Thumbprint; } else @@ -297,7 +333,8 @@ private static string CertificateHandler(string path, string password, bool clea using (X509Store store = new(StoreName.My, StoreLocation.CurrentUser)) { store.Open(OpenFlags.ReadWrite | OpenFlags.IncludeArchived); - X509Certificate2Collection col = store.Certificates.Find(X509FindType.FindByThumbprint, thumbPrint, false); + X509Certificate2Collection col = + store.Certificates.Find(X509FindType.FindByThumbprint, thumbPrint, false); foreach (var cert in col) store.Remove(cert); @@ -488,4 +525,25 @@ public async Task TestFileDownload_WindowsAuth_InvalidUsername_ShouldThrow() await HTTP.DownloadFile(input, options, default); } -} \ No newline at end of file + + [DataTestMethod] + [DataRow(CertificateStoreLocation.CurrentUser, "current user")] + [DataRow(CertificateStoreLocation.LocalMachine, "local machine")] + public void CorrectStoreSearched(CertificateStoreLocation storeLocation, string storeLocationText) + { + var handler = new HttpClientHandler(); + var options = new Options + { + Authentication = Authentication.ClientCertificate, + ClientCertificateSource = CertificateSource.CertificateStore, + CertificateStoreLocation = storeLocation, + CertificateThumbprint = "InvalidThumbprint", + }; + var ex = Assert.ThrowsExactly(() => + handler.SetHandlerSettingsBasedOnOptions(options)); + + Assert.IsNotNull(ex); + StringAssert.Contains(ex.Message, + $"Certificate with thumbprint: 'INVALIDTHUMBPRINT' not found in {storeLocationText} cert store."); + } +} diff --git a/Frends.HTTP.Request/Frends.HTTP.Request.Tests/UnitTests.cs b/Frends.HTTP.Request/Frends.HTTP.Request.Tests/UnitTests.cs index 8d7c3d0..79512d9 100644 --- a/Frends.HTTP.Request/Frends.HTTP.Request.Tests/UnitTests.cs +++ b/Frends.HTTP.Request/Frends.HTTP.Request.Tests/UnitTests.cs @@ -13,6 +13,7 @@ using System.Text; using Newtonsoft.Json; using Newtonsoft.Json.Linq; +using NUnit.Framework; using NUnit.Framework.Legacy; namespace Frends.HTTP.Request.Tests; @@ -31,7 +32,8 @@ public void TestInitialize() HTTP.ClientFactory = new MockHttpClientFactory(_mockHttpMessageHandler); } - private static Input GetInputParams(Method.Method method = Method.Method.GET, string url = BasePath, string message = "", + private static Input GetInputParams(Method.Method method = Method.Method.GET, string url = BasePath, + string message = "", params Header[] headers) { return new Input @@ -49,15 +51,22 @@ public async Task RequestTestGetWithParameters() const string expectedReturn = @"'FooBar'"; var dict = new Dictionary() { - {"foo", "bar"}, - {"bar", "foo"} + { + "foo", "bar" + }, + { + "bar", "foo" + } }; _mockHttpMessageHandler.When($"{BasePath}/endpoint").WithQueryString(dict) .Respond("application/json", expectedReturn); var input = GetInputParams(url: "http://localhost:9191/endpoint?foo=bar&bar=foo"); - var options = new Options { ConnectionTimeoutSeconds = 60 }; + var options = new Options + { + ConnectionTimeoutSeconds = 60 + }; var result = (dynamic)await HTTP.Request(input, options, CancellationToken.None); @@ -71,13 +80,23 @@ public async Task RequestTestGetWithContent() _mockHttpMessageHandler.When($"{BasePath}/endpoint").WithHeaders("Content-Type", "text/plain") .Respond("text/plain", expectedReturn); - var contentType = new Header { Name = "Content-Type", Value = "text/plain" }; + var contentType = new Header + { + Name = "Content-Type", + Value = "text/plain" + }; var input = GetInputParams( url: "http://localhost:9191/endpoint", method: Method.Method.GET, - headers: new Header[1] { contentType } + headers: new Header[1] + { + contentType + } ); - var options = new Options { ConnectionTimeoutSeconds = 60 }; + var options = new Options + { + ConnectionTimeoutSeconds = 60 + }; var result = (dynamic)await HTTP.Request(input, options, CancellationToken.None); NUnit.Framework.Legacy.StringAssert.Contains(result.Body, "OK"); @@ -93,7 +112,11 @@ public void RequestShouldThrowExceptionIfUrlEmpty() Headers = new Header[0], Message = "" }; - var options = new Options { ConnectionTimeoutSeconds = 60, ThrowExceptionOnErrorResponse = true }; + var options = new Options + { + ConnectionTimeoutSeconds = 60, + ThrowExceptionOnErrorResponse = true + }; var ex = Assert.ThrowsAsync(async () => await HTTP.Request(input, options, CancellationToken.None)); @@ -116,7 +139,11 @@ public void RequestShuldThrowExceptionIfOptionIsSet() Headers = new Header[0], Message = "" }; - var options = new Options { ConnectionTimeoutSeconds = 60, ThrowExceptionOnErrorResponse = true }; + var options = new Options + { + ConnectionTimeoutSeconds = 60, + ThrowExceptionOnErrorResponse = true + }; var ex = Assert.ThrowsAsync(async () => await HTTP.Request(input, options, CancellationToken.None)); @@ -140,7 +167,11 @@ public async Task RequestShouldNotThrowIfOptionIsNotSet() Headers = new Header[0], Message = "" }; - var options = new Options { ConnectionTimeoutSeconds = 60, ThrowExceptionOnErrorResponse = false }; + var options = new Options + { + ConnectionTimeoutSeconds = 60, + ThrowExceptionOnErrorResponse = false + }; var result = (dynamic)await HTTP.Request(input, options, CancellationToken.None); @@ -217,7 +248,14 @@ public async Task AuthorizationHeaderShouldOverrideOption() { Method = Method.Method.GET, Url = "http://localhost:9191/endpoint", - Headers = new[] { new Header() { Name = "Authorization", Value = "Basic fooToken" } }, + Headers = new[] + { + new Header() + { + Name = "Authorization", + Value = "Basic fooToken" + } + }, Message = "" }; var options = new Options @@ -274,9 +312,19 @@ public async Task RestRequestBodyReturnShouldBeOfTypeJToken() var input = new Input - { Method = Method.Method.GET, Url = "http://localhost:9191/endpoint", Headers = new Header[0], Message = "", ResultMethod = ReturnFormat.JToken }; + { + Method = Method.Method.GET, + Url = "http://localhost:9191/endpoint", + Headers = new Header[0], + Message = "", + ResultMethod = ReturnFormat.JToken + }; var options = new Options - { ConnectionTimeoutSeconds = 60, Authentication = Authentication.OAuth, Token = "fooToken" }; + { + ConnectionTimeoutSeconds = 60, + Authentication = Authentication.OAuth, + Token = "fooToken" + }; _mockHttpMessageHandler.When(input.Url) .Respond("application/json", output); @@ -290,9 +338,19 @@ public async Task RestRequestBodyReturnShouldBeOfTypeJToken() public async Task RestRequestShouldNotThrowIfReturnIsEmpty() { var input = new Input - { Method = Method.Method.GET, Url = "http://localhost:9191/endpoint", Headers = new Header[0], Message = "", ResultMethod = ReturnFormat.JToken }; + { + Method = Method.Method.GET, + Url = "http://localhost:9191/endpoint", + Headers = new Header[0], + Message = "", + ResultMethod = ReturnFormat.JToken + }; var options = new Options - { ConnectionTimeoutSeconds = 60, Authentication = Authentication.OAuth, Token = "fooToken" }; + { + ConnectionTimeoutSeconds = 60, + Authentication = Authentication.OAuth, + Token = "fooToken" + }; _mockHttpMessageHandler.When(input.Url) .Respond("application/json", String.Empty); @@ -306,9 +364,19 @@ public async Task RestRequestShouldNotThrowIfReturnIsEmpty() public void RestRequestShouldThrowIfReturnIsNotValidJson() { var input = new Input - { Method = Method.Method.GET, Url = "http://localhost:9191/endpoint", Headers = new Header[0], Message = "", ResultMethod = ReturnFormat.JToken }; + { + Method = Method.Method.GET, + Url = "http://localhost:9191/endpoint", + Headers = new Header[0], + Message = "", + ResultMethod = ReturnFormat.JToken + }; var options = new Options - { ConnectionTimeoutSeconds = 60, Authentication = Authentication.OAuth, Token = "fooToken" }; + { + ConnectionTimeoutSeconds = 60, + Authentication = Authentication.OAuth, + Token = "fooToken" + }; _mockHttpMessageHandler.When(input.Url) .Respond("application/json", "failbar"); @@ -324,8 +392,17 @@ public async Task HttpRequestBodyReturnShouldBeOfTypeString() const string expectedReturn = "BAR"; var input = new Input - { Method = Method.Method.GET, Url = "http://localhost:9191/endpoint", Headers = new Header[0], Message = "", ResultMethod = ReturnFormat.String }; - var options = new Options { ConnectionTimeoutSeconds = 60 }; + { + Method = Method.Method.GET, + Url = "http://localhost:9191/endpoint", + Headers = new Header[0], + Message = "", + ResultMethod = ReturnFormat.String + }; + var options = new Options + { + ConnectionTimeoutSeconds = 60 + }; _mockHttpMessageHandler.When(input.Url) .Respond("text/plain", expectedReturn); @@ -343,11 +420,16 @@ public async Task PatchShouldComeThrough() { Method = Method.Method.PATCH, Url = "http://localhost:9191/endpoint", - Headers = new Header[] { }, + Headers = new Header[] + { + }, Message = message, ResultMethod = ReturnFormat.String }; - var options = new Options { ConnectionTimeoutSeconds = 60 }; + var options = new Options + { + ConnectionTimeoutSeconds = 60 + }; _mockHttpMessageHandler.Expect(new HttpMethod("PATCH"), input.Url).WithContent(message) .Respond("text/plain", "foo åäö"); @@ -365,18 +447,29 @@ public async Task RequestShouldSetEncodingWithContentTypeCharsetIgnoringCase() var requestMessage = "åäö!"; var expectedContentType = $"text/plain; charset={codePageName}"; - var contentType = new Header { Name = "cONTENT-tYpE", Value = expectedContentType }; + var contentType = new Header + { + Name = "cONTENT-tYpE", + Value = expectedContentType + }; var input = new Input { Method = Method.Method.POST, Url = "http://localhost:9191/endpoint", - Headers = new Header[1] { contentType }, + Headers = new Header[1] + { + contentType + }, Message = requestMessage, ResultMethod = ReturnFormat.String }; - var options = new Options { ConnectionTimeoutSeconds = 60 }; + var options = new Options + { + ConnectionTimeoutSeconds = 60 + }; - _mockHttpMessageHandler.Expect(HttpMethod.Post, input.Url).WithHeaders("cONTENT-tYpE", expectedContentType).WithContent(requestMessage) + _mockHttpMessageHandler.Expect(HttpMethod.Post, input.Url).WithHeaders("cONTENT-tYpE", expectedContentType) + .WithContent(requestMessage) .Respond("text/plain", "foo åäö"); var result = await HTTP.Request(input, options, CancellationToken.None); @@ -400,9 +493,30 @@ public async Task RequestTest_GetMethod_ShouldSendEmptyContent() message: "This should not be sent" ); - var options = new Options { ConnectionTimeoutSeconds = 60 }; + var options = new Options + { + ConnectionTimeoutSeconds = 60 + }; var result = await HTTP.Request(input, options, CancellationToken.None); ClassicAssert.AreEqual(expectedReturn, result.Body); } + + [TestCase(CertificateStoreLocation.CurrentUser, "current user")] + [TestCase(CertificateStoreLocation.LocalMachine, "local machine")] + public void CorrectStoreSearched(CertificateStoreLocation storeLocation, string storeLocationText) + { + var handler = new HttpClientHandler(); + var options = new Options + { + Authentication = Authentication.ClientCertificate, + ClientCertificateSource = CertificateSource.CertificateStore, + CertificateStoreLocation = storeLocation, + CertificateThumbprint = "InvalidThumbprint", + }; + var ex = Assert.Throws(() => handler.SetHandlerSettingsBasedOnOptions(options)); + Assert.That(ex, Is.Not.Null); + Assert.That(ex.Message.Contains( + $"Certificate with thumbprint: 'INVALIDTHUMBPRINT' not found in {storeLocationText} cert store.")); + } } diff --git a/Frends.HTTP.RequestBytes/Frends.HTTP.RequestBytes.Tests/UnitTests.cs b/Frends.HTTP.RequestBytes/Frends.HTTP.RequestBytes.Tests/UnitTests.cs index 1934fc3..83da029 100644 --- a/Frends.HTTP.RequestBytes/Frends.HTTP.RequestBytes.Tests/UnitTests.cs +++ b/Frends.HTTP.RequestBytes/Frends.HTTP.RequestBytes.Tests/UnitTests.cs @@ -11,6 +11,7 @@ using System.Text; using Method = Frends.HTTP.RequestBytes.Definitions.Method; using System.Net; +using NUnit.Framework; using NUnit.Framework.Legacy; namespace Frends.HTTP.RequestBytes.Tests; @@ -41,6 +42,24 @@ private static Input GetInputParams(Method method = Method.GET, string url = _ba }; } + [TestCase(CertificateStoreLocation.CurrentUser, "current user")] + [TestCase(CertificateStoreLocation.LocalMachine, "local machine")] + public void CorrectStoreSearched(CertificateStoreLocation storeLocation, string storeLocationText) + { + var handler = new HttpClientHandler(); + var options = new Options + { + Authentication = Authentication.ClientCertificate, + ClientCertificateSource = CertificateSource.CertificateStore, + CertificateStoreLocation = storeLocation, + CertificateThumbprint = "InvalidThumbprint", + }; + var ex = Assert.Throws(() => handler.SetHandlerSettingsBasedOnOptions(options)); + Assert.That(ex, Is.Not.Null); + Assert.That(ex.Message.Contains( + $"Certificate with thumbprint: 'INVALIDTHUMBPRINT' not found in {storeLocationText} cert store.")); + } + [TestMethod] public void RequestShouldThrowExceptionIfUrlEmpty() { @@ -51,7 +70,11 @@ public void RequestShouldThrowExceptionIfUrlEmpty() Headers = new Header[0], Message = "" }; - var options = new Options { ConnectionTimeoutSeconds = 60, ThrowExceptionOnErrorResponse = true }; + var options = new Options + { + ConnectionTimeoutSeconds = 60, + ThrowExceptionOnErrorResponse = true + }; var ex = Assert.ThrowsAsync(async () => await HTTP.RequestBytes(input, options, CancellationToken.None)); @@ -89,8 +112,17 @@ public void RequestShouldAddClientCertificate() [TestMethod] public async Task HttpRequestBytesReturnShoulReturnEmpty() { - var input = new Input { Method = Method.GET, Url = "http://localhost:9191/endpoint", Headers = new Header[0], Message = "" }; - var options = new Options { ConnectionTimeoutSeconds = 60 }; + var input = new Input + { + Method = Method.GET, + Url = "http://localhost:9191/endpoint", + Headers = new Header[0], + Message = "" + }; + var options = new Options + { + ConnectionTimeoutSeconds = 60 + }; _mockHttpMessageHandler.When(input.Url) .Respond("application/octet-stream", String.Empty); @@ -103,11 +135,20 @@ public async Task HttpRequestBytesReturnShoulReturnEmpty() [TestMethod] public async Task HttpRequestBytesShouldBeAbleToReturnBinary() { - var testFileUriPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, @"../../../TestData/frends_favicon.png"); + var testFileUriPath = + Path.Combine(AppDomain.CurrentDomain.BaseDirectory, @"../../../TestData/frends_favicon.png"); string localTestFilePath = new Uri(testFileUriPath).LocalPath; var input = new Input - { Method = Method.GET, Url = "http://localhost:9191/endpoint", Headers = new Header[0], Message = "" }; - var options = new Options { ConnectionTimeoutSeconds = 60 }; + { + Method = Method.GET, + Url = "http://localhost:9191/endpoint", + Headers = new Header[0], + Message = "" + }; + var options = new Options + { + ConnectionTimeoutSeconds = 60 + }; var actualFileBytes = File.ReadAllBytes(localTestFilePath); @@ -127,12 +168,19 @@ public async Task RequestTestGetWithParameters() var expectedReturn = Encoding.ASCII.GetBytes("FooBar"); var input = GetInputParams(url: "http://localhost:9191/endpoint?foo=bar&bar=foo"); - var options = new Options { ConnectionTimeoutSeconds = 60 }; + var options = new Options + { + ConnectionTimeoutSeconds = 60 + }; var dict = new Dictionary() { - {"foo", "bar"}, - {"bar", "foo"} + { + "foo", "bar" + }, + { + "bar", "foo" + } }; _mockHttpMessageHandler.When(input.Url).WithQueryString(dict) @@ -158,7 +206,11 @@ public void RequestShuldThrowExceptionIfOptionIsSet() Headers = new Header[0], Message = "" }; - var options = new Options { ConnectionTimeoutSeconds = 60, ThrowExceptionOnErrorResponse = true }; + var options = new Options + { + ConnectionTimeoutSeconds = 60, + ThrowExceptionOnErrorResponse = true + }; Assert.ThrowsAsync(async () => await HTTP.RequestBytes(input, options, CancellationToken.None)); @@ -179,7 +231,11 @@ public async Task RequestShouldNotThrowIfOptionIsNotSet() Headers = new Header[0], Message = "" }; - var options = new Options { ConnectionTimeoutSeconds = 60, ThrowExceptionOnErrorResponse = false }; + var options = new Options + { + ConnectionTimeoutSeconds = 60, + ThrowExceptionOnErrorResponse = false + }; var result = (dynamic)await HTTP.RequestBytes(input, options, CancellationToken.None); @@ -254,7 +310,14 @@ public async Task AuthorizationHeaderShouldOverrideOption() { Method = Method.GET, Url = "http://localhost:9191/endpoint", - Headers = new[] { new Header() { Name = "Authorization", Value = "Basic fooToken" } }, + Headers = new[] + { + new Header() + { + Name = "Authorization", + Value = "Basic fooToken" + } + }, Message = "" }; var options = new Options @@ -281,10 +344,15 @@ public async Task PatchShouldComeThrough() { Method = Method.PATCH, Url = "http://localhost:9191/endpoint", - Headers = new Header[] { }, + Headers = new Header[] + { + }, Message = message }; - var options = new Options { ConnectionTimeoutSeconds = 60 }; + var options = new Options + { + ConnectionTimeoutSeconds = 60 + }; _mockHttpMessageHandler.Expect(new HttpMethod("PATCH"), input.Url).WithContent(message) .Respond("application/octet-stream", message); @@ -302,17 +370,28 @@ public async Task RequestShouldSetEncodingWithContentTypeCharsetIgnoringCase() var requestMessage = "åäö!"; var expectedContentType = $"text/plain; charset={codePageName}"; - var contentType = new Header { Name = "cONTENT-tYpE", Value = expectedContentType }; + var contentType = new Header + { + Name = "cONTENT-tYpE", + Value = expectedContentType + }; var input = new Input { Method = Method.POST, Url = "http://localhost:9191/endpoint", - Headers = new Header[1] { contentType }, + Headers = new Header[1] + { + contentType + }, Message = requestMessage }; - var options = new Options { ConnectionTimeoutSeconds = 60 }; + var options = new Options + { + ConnectionTimeoutSeconds = 60 + }; - _mockHttpMessageHandler.Expect(HttpMethod.Post, input.Url).WithHeaders("cONTENT-tYpE", expectedContentType).WithContent(requestMessage) + _mockHttpMessageHandler.Expect(HttpMethod.Post, input.Url).WithHeaders("cONTENT-tYpE", expectedContentType) + .WithContent(requestMessage) .Respond("application/octet-stream", "foo åäö"); var result = (dynamic)await HTTP.RequestBytes(input, options, CancellationToken.None); @@ -330,9 +409,9 @@ public MockHttpClientFactory(MockHttpMessageHandler mockHttpMessageHandler) { _mockHttpMessageHandler = mockHttpMessageHandler; } + public HttpClient CreateClient(Options options) { return _mockHttpMessageHandler.ToHttpClient(); - } } diff --git a/Frends.HTTP.SendAndReceiveBytes/Frends.HTTP.SendAndReceiveBytes.Tests/UnitTests.cs b/Frends.HTTP.SendAndReceiveBytes/Frends.HTTP.SendAndReceiveBytes.Tests/UnitTests.cs index a9cb241..082f9e2 100644 --- a/Frends.HTTP.SendAndReceiveBytes/Frends.HTTP.SendAndReceiveBytes.Tests/UnitTests.cs +++ b/Frends.HTTP.SendAndReceiveBytes/Frends.HTTP.SendAndReceiveBytes.Tests/UnitTests.cs @@ -11,6 +11,7 @@ using System.Text; using Method = Frends.HTTP.SendAndReceiveBytes.Definitions.Method; using System.Net; +using NUnit.Framework; using NUnit.Framework.Legacy; namespace Frends.HTTP.SendAndReceiveBytes.Tests; @@ -29,7 +30,8 @@ public void TestInitialize() HTTP.ClientFactory = new MockHttpClientFactory(_mockHttpMessageHandler); } - private static Input GetInputParams(Method method = Method.POST, string url = _basePath, byte[] message = null, params Header[] headers) + private static Input GetInputParams(Method method = Method.POST, string url = _basePath, byte[] message = null, + params Header[] headers) { return new Input { @@ -50,7 +52,11 @@ public void RequestShouldThrowExceptionIfUrlEmpty() Headers = Array.Empty
(), ContentBytes = Array.Empty() }; - var options = new Options { ConnectionTimeoutSeconds = 60, ThrowExceptionOnErrorResponse = true }; + var options = new Options + { + ConnectionTimeoutSeconds = 60, + ThrowExceptionOnErrorResponse = true + }; var ex = Assert.ThrowsAsync(async () => await HTTP.SendAndReceiveBytes(input, options, CancellationToken.None)); @@ -88,8 +94,17 @@ public void RequestShouldAddClientCertificate() [TestMethod] public async Task HttpSendAndReceiveBytesReturnShoulReturnEmpty() { - var input = new Input { Method = Method.POST, Url = "http://localhost:9191/endpoint", Headers = Array.Empty
(), ContentBytes = Array.Empty() }; - var options = new Options { ConnectionTimeoutSeconds = 60 }; + var input = new Input + { + Method = Method.POST, + Url = "http://localhost:9191/endpoint", + Headers = Array.Empty
(), + ContentBytes = Array.Empty() + }; + var options = new Options + { + ConnectionTimeoutSeconds = 60 + }; _mockHttpMessageHandler.When(input.Url) .Respond("application/octet-stream", string.Empty); @@ -102,12 +117,19 @@ public async Task HttpSendAndReceiveBytesReturnShoulReturnEmpty() public async Task RequestTestPostWithParameters() { var input = GetInputParams(url: "http://localhost:9191/endpoint?foo=bar&bar=foo"); - var options = new Options { ConnectionTimeoutSeconds = 60 }; + var options = new Options + { + ConnectionTimeoutSeconds = 60 + }; var dict = new Dictionary() { - {"foo", "bar"}, - {"bar", "foo"} + { + "foo", "bar" + }, + { + "bar", "foo" + } }; _mockHttpMessageHandler.When(input.Url).WithQueryString(dict) @@ -133,7 +155,11 @@ public void RequestShouldThrowExceptionIfOptionIsSet() Headers = Array.Empty
(), ContentBytes = Array.Empty() }; - var options = new Options { ConnectionTimeoutSeconds = 60, ThrowExceptionOnErrorResponse = true }; + var options = new Options + { + ConnectionTimeoutSeconds = 60, + ThrowExceptionOnErrorResponse = true + }; Assert.ThrowsAsync(async () => await HTTP.SendAndReceiveBytes(input, options, CancellationToken.None)); @@ -152,7 +178,11 @@ public async Task RequestShouldNotThrowIfOptionIsNotSet() Headers = Array.Empty
(), ContentBytes = Array.Empty() }; - var options = new Options { ConnectionTimeoutSeconds = 60, ThrowExceptionOnErrorResponse = false }; + var options = new Options + { + ConnectionTimeoutSeconds = 60, + ThrowExceptionOnErrorResponse = false + }; var result = (Result)await HTTP.SendAndReceiveBytes(input, options, CancellationToken.None); @@ -221,7 +251,14 @@ public async Task AuthorizationHeaderShouldOverrideOption() { Method = Method.PUT, Url = "http://localhost:9191/endpoint", - Headers = new[] { new Header() { Name = "Authorization", Value = "Basic fooToken" } }, + Headers = new[] + { + new Header() + { + Name = "Authorization", + Value = "Basic fooToken" + } + }, ContentBytes = Array.Empty() }; var options = new Options @@ -250,16 +287,29 @@ public async Task PatchShouldComeThrough() Url = "http://localhost:9191/data", Headers = new[] { - new Header {Name = "Content-Type", Value = "text/plain; charset=utf-8"}, - new Header() {Name = "Content-Length", Value = bytes.Length.ToString()} + new Header + { + Name = "Content-Type", + Value = "text/plain; charset=utf-8" }, + new Header() + { + Name = "Content-Length", + Value = bytes.Length.ToString() + } + }, ContentBytes = bytes }; var options = new Options - { ConnectionTimeoutSeconds = 60, Authentication = Authentication.OAuth, Token = "fooToken" }; + { + ConnectionTimeoutSeconds = 60, + Authentication = Authentication.OAuth, + Token = "fooToken" + }; - _mockHttpMessageHandler.Expect(HttpMethod.Patch, input.Url).WithHeaders("Content-Type", "text/plain; charset=utf-8").WithContent(expectedString) + _mockHttpMessageHandler.Expect(HttpMethod.Patch, input.Url) + .WithHeaders("Content-Type", "text/plain; charset=utf-8").WithContent(expectedString) .Respond("text/plain", expectedString); var result = (Result)await HTTP.SendAndReceiveBytes(input, options, CancellationToken.None); @@ -267,6 +317,24 @@ public async Task PatchShouldComeThrough() _mockHttpMessageHandler.VerifyNoOutstandingExpectation(); ClassicAssert.AreEqual(Encoding.UTF8.GetBytes(expectedString), result.BodyBytes); } + + [TestCase(CertificateStoreLocation.CurrentUser, "current user")] + [TestCase(CertificateStoreLocation.LocalMachine, "local machine")] + public void CorrectStoreSearched(CertificateStoreLocation storeLocation, string storeLocationText) + { + var handler = new HttpClientHandler(); + var options = new Options + { + Authentication = Authentication.ClientCertificate, + ClientCertificateSource = CertificateSource.CertificateStore, + CertificateStoreLocation = storeLocation, + CertificateThumbprint = "InvalidThumbprint", + }; + var ex = Assert.Throws(() => handler.SetHandlerSettingsBasedOnOptions(options)); + Assert.That(ex, Is.Not.Null); + Assert.That(ex.Message.Contains( + $"Certificate with thumbprint: 'INVALIDTHUMBPRINT' not found in {storeLocationText} cert store.")); + } } public class MockHttpClientFactory : IHttpClientFactory @@ -277,9 +345,9 @@ public MockHttpClientFactory(MockHttpMessageHandler mockHttpMessageHandler) { _mockHttpMessageHandler = mockHttpMessageHandler; } + public HttpClient CreateClient(Options options) { return _mockHttpMessageHandler.ToHttpClient(); - } } diff --git a/Frends.HTTP.SendBytes/Frends.HTTP.SendBytes.Tests/UnitTests.cs b/Frends.HTTP.SendBytes/Frends.HTTP.SendBytes.Tests/UnitTests.cs index 7e29851..2955928 100644 --- a/Frends.HTTP.SendBytes/Frends.HTTP.SendBytes.Tests/UnitTests.cs +++ b/Frends.HTTP.SendBytes/Frends.HTTP.SendBytes.Tests/UnitTests.cs @@ -11,6 +11,7 @@ using System.Text; using Method = Frends.HTTP.SendBytes.Definitions.Method; using System.Net; +using NUnit.Framework; using NUnit.Framework.Legacy; namespace Frends.HTTP.SendBytes.Tests; @@ -29,7 +30,8 @@ public void TestInitialize() HTTP.ClientFactory = new MockHttpClientFactory(_mockHttpMessageHandler); } - private static Input GetInputParams(Method method = Method.POST, string url = _basePath, byte[] message = null, params Header[] headers) + private static Input GetInputParams(Method method = Method.POST, string url = _basePath, byte[] message = null, + params Header[] headers) { return new Input { @@ -40,6 +42,24 @@ private static Input GetInputParams(Method method = Method.POST, string url = _b }; } + [TestCase(CertificateStoreLocation.CurrentUser, "current user")] + [TestCase(CertificateStoreLocation.LocalMachine, "local machine")] + public void CorrectStoreSearched(CertificateStoreLocation storeLocation, string storeLocationText) + { + var handler = new HttpClientHandler(); + var options = new Options + { + Authentication = Authentication.ClientCertificate, + ClientCertificateSource = CertificateSource.CertificateStore, + CertificateStoreLocation = storeLocation, + CertificateThumbprint = "InvalidThumbprint", + }; + var ex = Assert.Throws(() => handler.SetHandlerSettingsBasedOnOptions(options)); + Assert.That(ex, Is.Not.Null); + Assert.That(ex.Message.Contains( + $"Certificate with thumbprint: 'INVALIDTHUMBPRINT' not found in {storeLocationText} cert store.")); + } + [TestMethod] public void RequestShouldThrowExceptionIfUrlEmpty() { @@ -50,7 +70,11 @@ public void RequestShouldThrowExceptionIfUrlEmpty() Headers = new Header[0], ContentBytes = new byte[0] }; - var options = new Options { ConnectionTimeoutSeconds = 60, ThrowExceptionOnErrorResponse = true }; + var options = new Options + { + ConnectionTimeoutSeconds = 60, + ThrowExceptionOnErrorResponse = true + }; var ex = Assert.ThrowsAsync(async () => await HTTP.SendBytes(input, options, CancellationToken.None)); @@ -88,8 +112,17 @@ public void RequestShouldAddClientCertificate() [TestMethod] public async Task HttpSendBytesReturnShoulReturnEmpty() { - var input = new Input { Method = Method.POST, Url = "http://localhost:9191/endpoint", Headers = new Header[0], ContentBytes = new byte[0] }; - var options = new Options { ConnectionTimeoutSeconds = 60 }; + var input = new Input + { + Method = Method.POST, + Url = "http://localhost:9191/endpoint", + Headers = new Header[0], + ContentBytes = new byte[0] + }; + var options = new Options + { + ConnectionTimeoutSeconds = 60 + }; _mockHttpMessageHandler.When(input.Url) .Respond("application/octet-stream", String.Empty); @@ -102,12 +135,19 @@ public async Task HttpSendBytesReturnShoulReturnEmpty() public async Task RequestTestPostWithParameters() { var input = GetInputParams(url: "http://localhost:9191/endpoint?foo=bar&bar=foo"); - var options = new Options { ConnectionTimeoutSeconds = 60 }; + var options = new Options + { + ConnectionTimeoutSeconds = 60 + }; var dict = new Dictionary() { - {"foo", "bar"}, - {"bar", "foo"} + { + "foo", "bar" + }, + { + "bar", "foo" + } }; _mockHttpMessageHandler.When(input.Url).WithQueryString(dict) @@ -133,7 +173,11 @@ public void RequestShouldThrowExceptionIfOptionIsSet() Headers = new Header[0], ContentBytes = new byte[0] }; - var options = new Options { ConnectionTimeoutSeconds = 60, ThrowExceptionOnErrorResponse = true }; + var options = new Options + { + ConnectionTimeoutSeconds = 60, + ThrowExceptionOnErrorResponse = true + }; Assert.ThrowsAsync(async () => await HTTP.SendBytes(input, options, CancellationToken.None)); @@ -152,7 +196,11 @@ public async Task RequestShouldNotThrowIfOptionIsNotSet() Headers = new Header[0], ContentBytes = new byte[0] }; - var options = new Options { ConnectionTimeoutSeconds = 60, ThrowExceptionOnErrorResponse = false }; + var options = new Options + { + ConnectionTimeoutSeconds = 60, + ThrowExceptionOnErrorResponse = false + }; var result = (dynamic)await HTTP.SendBytes(input, options, CancellationToken.None); @@ -221,7 +269,14 @@ public async Task AuthorizationHeaderShouldOverrideOption() { Method = Method.PUT, Url = "http://localhost:9191/endpoint", - Headers = new[] { new Header() { Name = "Authorization", Value = "Basic fooToken" } }, + Headers = new[] + { + new Header() + { + Name = "Authorization", + Value = "Basic fooToken" + } + }, ContentBytes = new byte[0] }; var options = new Options @@ -250,16 +305,29 @@ public async Task PatchShouldComeThrough() Url = "http://localhost:9191/data", Headers = new[] { - new Header {Name = "Content-Type", Value = "text/plain; charset=utf-8"}, - new Header() {Name = "Content-Length", Value = bytes.Length.ToString()} + new Header + { + Name = "Content-Type", + Value = "text/plain; charset=utf-8" }, + new Header() + { + Name = "Content-Length", + Value = bytes.Length.ToString() + } + }, ContentBytes = bytes }; var options = new Options - { ConnectionTimeoutSeconds = 60, Authentication = Authentication.OAuth, Token = "fooToken" }; + { + ConnectionTimeoutSeconds = 60, + Authentication = Authentication.OAuth, + Token = "fooToken" + }; - _mockHttpMessageHandler.Expect(HttpMethod.Patch, input.Url).WithHeaders("Content-Type", "text/plain; charset=utf-8").WithContent(expectedString) + _mockHttpMessageHandler.Expect(HttpMethod.Patch, input.Url) + .WithHeaders("Content-Type", "text/plain; charset=utf-8").WithContent(expectedString) .Respond("text/plain", "foo åäö"); var result = (Result)await HTTP.SendBytes(input, options, CancellationToken.None); @@ -277,9 +345,9 @@ public MockHttpClientFactory(MockHttpMessageHandler mockHttpMessageHandler) { _mockHttpMessageHandler = mockHttpMessageHandler; } + public HttpClient CreateClient(Options options) { return _mockHttpMessageHandler.ToHttpClient(); - } } From 914fc84894153b89654040810111d55975d4c288 Mon Sep 17 00:00:00 2001 From: Mateusz Noga-Wojtania Date: Mon, 2 Mar 2026 11:56:57 +0100 Subject: [PATCH 19/21] switch tests to windows --- .github/workflows/Request_build_and_test_on_main.yml | 4 ++-- .github/workflows/Request_build_and_test_on_push.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/Request_build_and_test_on_main.yml b/.github/workflows/Request_build_and_test_on_main.yml index 0d46ea3..debade3 100644 --- a/.github/workflows/Request_build_and_test_on_main.yml +++ b/.github/workflows/Request_build_and_test_on_main.yml @@ -2,7 +2,7 @@ name: Request_build_main on: push: - branches: + branches: - main paths: - 'Frends.HTTP.Request/**' @@ -10,7 +10,7 @@ on: jobs: build: - uses: FrendsPlatform/FrendsTasks/.github/workflows/linux_build_main.yml@main + uses: FrendsPlatform/FrendsTasks/.github/workflows/build_main.yml@main with: workdir: Frends.HTTP.Request secrets: diff --git a/.github/workflows/Request_build_and_test_on_push.yml b/.github/workflows/Request_build_and_test_on_push.yml index 9e5e8c2..a261de8 100644 --- a/.github/workflows/Request_build_and_test_on_push.yml +++ b/.github/workflows/Request_build_and_test_on_push.yml @@ -10,7 +10,7 @@ on: jobs: build: - uses: FrendsPlatform/FrendsTasks/.github/workflows/linux_build_test.yml@main + uses: FrendsPlatform/FrendsTasks/.github/workflows/build_test.yml@main with: workdir: Frends.HTTP.Request secrets: From 8e766a8c8d658671fdc24665e37e31fa5c3da39f Mon Sep 17 00:00:00 2001 From: Mateusz Noga-Wojtania Date: Mon, 2 Mar 2026 12:02:10 +0100 Subject: [PATCH 20/21] switch all tests to windows --- .github/workflows/RequestBytes_build_and_test_on_main.yml | 2 +- .github/workflows/RequestBytes_build_and_test_on_push.yml | 2 +- .../workflows/SendAndReceiveBytes_build_and_test_on_main.yml | 2 +- .../workflows/SendAndReceiveBytes_build_and_test_on_push.yml | 2 +- .github/workflows/SendBytes_build_and_test_on_main.yml | 2 +- .github/workflows/SendBytes_build_and_test_on_push.yml | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/RequestBytes_build_and_test_on_main.yml b/.github/workflows/RequestBytes_build_and_test_on_main.yml index aca89f1..c4a24c0 100644 --- a/.github/workflows/RequestBytes_build_and_test_on_main.yml +++ b/.github/workflows/RequestBytes_build_and_test_on_main.yml @@ -10,7 +10,7 @@ on: jobs: build: - uses: FrendsPlatform/FrendsTasks/.github/workflows/linux_build_main.yml@main + uses: FrendsPlatform/FrendsTasks/.github/workflows/build_main.yml@main with: workdir: Frends.HTTP.RequestBytes secrets: diff --git a/.github/workflows/RequestBytes_build_and_test_on_push.yml b/.github/workflows/RequestBytes_build_and_test_on_push.yml index fcb1a04..b28d1d9 100644 --- a/.github/workflows/RequestBytes_build_and_test_on_push.yml +++ b/.github/workflows/RequestBytes_build_and_test_on_push.yml @@ -10,7 +10,7 @@ on: jobs: build: - uses: FrendsPlatform/FrendsTasks/.github/workflows/linux_build_test.yml@main + uses: FrendsPlatform/FrendsTasks/.github/workflows/build_test.yml@main with: workdir: Frends.HTTP.RequestBytes secrets: diff --git a/.github/workflows/SendAndReceiveBytes_build_and_test_on_main.yml b/.github/workflows/SendAndReceiveBytes_build_and_test_on_main.yml index a91db0c..3d8c8de 100644 --- a/.github/workflows/SendAndReceiveBytes_build_and_test_on_main.yml +++ b/.github/workflows/SendAndReceiveBytes_build_and_test_on_main.yml @@ -10,7 +10,7 @@ on: jobs: build: - uses: FrendsPlatform/FrendsTasks/.github/workflows/linux_build_main.yml@main + uses: FrendsPlatform/FrendsTasks/.github/workflows/build_main.yml@main with: workdir: Frends.HTTP.SendAndReceiveBytes secrets: diff --git a/.github/workflows/SendAndReceiveBytes_build_and_test_on_push.yml b/.github/workflows/SendAndReceiveBytes_build_and_test_on_push.yml index 804b858..ba88c68 100644 --- a/.github/workflows/SendAndReceiveBytes_build_and_test_on_push.yml +++ b/.github/workflows/SendAndReceiveBytes_build_and_test_on_push.yml @@ -10,7 +10,7 @@ on: jobs: build: - uses: FrendsPlatform/FrendsTasks/.github/workflows/linux_build_test.yml@main + uses: FrendsPlatform/FrendsTasks/.github/workflows/build_test.yml@main with: workdir: Frends.HTTP.SendAndReceiveBytes secrets: diff --git a/.github/workflows/SendBytes_build_and_test_on_main.yml b/.github/workflows/SendBytes_build_and_test_on_main.yml index 5a4dffc..24647b6 100644 --- a/.github/workflows/SendBytes_build_and_test_on_main.yml +++ b/.github/workflows/SendBytes_build_and_test_on_main.yml @@ -10,7 +10,7 @@ on: jobs: build: - uses: FrendsPlatform/FrendsTasks/.github/workflows/linux_build_main.yml@main + uses: FrendsPlatform/FrendsTasks/.github/workflows/build_main.yml@main with: workdir: Frends.HTTP.SendBytes secrets: diff --git a/.github/workflows/SendBytes_build_and_test_on_push.yml b/.github/workflows/SendBytes_build_and_test_on_push.yml index 00ac852..bcaaad0 100644 --- a/.github/workflows/SendBytes_build_and_test_on_push.yml +++ b/.github/workflows/SendBytes_build_and_test_on_push.yml @@ -10,7 +10,7 @@ on: jobs: build: - uses: FrendsPlatform/FrendsTasks/.github/workflows/linux_build_test.yml@main + uses: FrendsPlatform/FrendsTasks/.github/workflows/build_test.yml@main with: workdir: Frends.HTTP.SendBytes secrets: From d3910cc389ec32d03d80432fca33ca9652c1971b Mon Sep 17 00:00:00 2001 From: Mateusz Noga-Wojtania Date: Tue, 3 Mar 2026 13:03:58 +0100 Subject: [PATCH 21/21] fix date --- Frends.HTTP.Request/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Frends.HTTP.Request/CHANGELOG.md b/Frends.HTTP.Request/CHANGELOG.md index 6b6e53f..c349f64 100644 --- a/Frends.HTTP.Request/CHANGELOG.md +++ b/Frends.HTTP.Request/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [1.7.0] - 2026-03-26 +## [1.7.0] - 2026-03-03 ### Added - Added CertificateStoreLocation option to allow selection between CurrentUser and LocalMachine certificate stores when using certificate authentication.