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/11] 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/11] 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/11] 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/11] 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/11] 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/11] 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/11] 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/11] 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/11] 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/11] 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/11] 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();