From 760c6d140c0b436dd99c579ceda1d107a61f4964 Mon Sep 17 00:00:00 2001 From: Teesofttech Date: Sat, 18 Jul 2026 17:02:25 +0100 Subject: [PATCH 1/2] Harden enabled gateway config validation at startup --- .../Unit/IServiceCollectionExtensionsTests.cs | 51 +++++++++++++++++++ .../IServiceCollectionExtensions.cs | 37 +++++++++++++- 2 files changed, 86 insertions(+), 2 deletions(-) diff --git a/PayBridge.SDK.Test/Unit/IServiceCollectionExtensionsTests.cs b/PayBridge.SDK.Test/Unit/IServiceCollectionExtensionsTests.cs index 7205267..5a2a20a 100644 --- a/PayBridge.SDK.Test/Unit/IServiceCollectionExtensionsTests.cs +++ b/PayBridge.SDK.Test/Unit/IServiceCollectionExtensionsTests.cs @@ -135,6 +135,57 @@ public void AddPayBridge_WhenEnabledGatewaysContainsAutomatic_Throws() .WithMessage("*cannot include Automatic*"); } + [Fact] + public void AddPayBridge_WhenEnabledGatewaysContainsDuplicateValues_Throws() + { + var configuration = BuildConfiguration(new Dictionary + { + ["PaymentGatewayConfig:EnabledGateways:0"] = "Paystack", + ["PaymentGatewayConfig:EnabledGateways:1"] = "Paystack", + ["PaymentGatewayConfig:Paystack:SecretKey"] = "sk_test_paystack" + }); + var services = new ServiceCollection(); + services.AddLogging(); + + var action = () => services.AddPayBridge(configuration); + + action.Should().Throw() + .WithMessage("*appears more than once*"); + } + + [Fact] + public void AddPayBridge_WhenEnabledGatewayIsUndefinedEnumValue_Throws() + { + var configuration = BuildConfiguration(new Dictionary + { + ["PaymentGatewayConfig:EnabledGateways:0"] = "999" + }); + var services = new ServiceCollection(); + services.AddLogging(); + + var action = () => services.AddPayBridge(configuration); + + action.Should().Throw() + .WithMessage("*not a defined PaymentGatewayType*"); + } + + [Fact] + public void AddPayBridge_WhenEnabledGatewayUsesPlaceholderCredential_Throws() + { + var configuration = BuildConfiguration(new Dictionary + { + ["PaymentGatewayConfig:EnabledGateways:0"] = "Paystack", + ["PaymentGatewayConfig:Paystack:SecretKey"] = "YOUR_PAYSTACK_SECRET_KEY" + }); + var services = new ServiceCollection(); + services.AddLogging(); + + var action = () => services.AddPayBridge(configuration); + + action.Should().Throw() + .WithMessage("*Paystack*missing required configuration values*"); + } + [Fact] public void AddPayBridge_WhenDefaultGatewayIsMissingRequiredConfig_Throws() { diff --git a/PayBridge.SDK/Externsions/IServiceCollectionExtensions.cs b/PayBridge.SDK/Externsions/IServiceCollectionExtensions.cs index 3227362..0ac01ca 100644 --- a/PayBridge.SDK/Externsions/IServiceCollectionExtensions.cs +++ b/PayBridge.SDK/Externsions/IServiceCollectionExtensions.cs @@ -178,25 +178,58 @@ private static void RegisterGateways(IServiceCollection services, PaymentGateway private static void ValidateEnabledGatewayConfiguration(PaymentGatewayConfig config) { + var errors = new List(); + var seenGateways = new HashSet(); + foreach (var gateway in config.EnabledGateways) { + if (!Enum.IsDefined(typeof(PaymentGatewayType), gateway)) + { + errors.Add( + $"Enabled gateway value '{(int)gateway}' is not a defined PaymentGatewayType."); + continue; + } + if (gateway == PaymentGatewayType.Automatic) { - throw new PaymentConfigurationException( + errors.Add( "PaymentGatewayConfig:EnabledGateways cannot include Automatic. " + "Use concrete gateway types only."); + continue; + } + + if (!seenGateways.Add(gateway)) + { + errors.Add( + $"Enabled gateway '{gateway}' appears more than once. " + + "Remove duplicate entries."); + continue; } if (!IsGatewayConfigured(config, gateway)) { - throw new PaymentConfigurationException( + errors.Add( $"Enabled gateway '{gateway}' is missing required configuration values."); } } + + if (errors.Count > 0) + { + throw new PaymentConfigurationException( + "Invalid PaymentGatewayConfig:EnabledGateways configuration:" + + Environment.NewLine + + string.Join(Environment.NewLine, errors.Select(error => $" - {error}"))); + } } private static void ValidateDefaultGatewayConfiguration(PaymentGatewayConfig config) { + if (!Enum.IsDefined(typeof(PaymentGatewayType), config.DefaultGateway)) + { + throw new PaymentConfigurationException( + $"Default gateway value '{(int)config.DefaultGateway}' is not a defined PaymentGatewayType."); + } + if (config.DefaultGateway == PaymentGatewayType.Automatic) { return; From a12c04c3bc357c7ea306128ca067ec2f3f0da54f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 18 Jul 2026 19:07:05 +0000 Subject: [PATCH 2/2] fix: include missing enabled gateway config keys in validation error --- .../Unit/IServiceCollectionExtensionsTests.cs | 2 +- .../IServiceCollectionExtensions.cs | 77 ++++++++++++++++++- 2 files changed, 77 insertions(+), 2 deletions(-) diff --git a/PayBridge.SDK.Test/Unit/IServiceCollectionExtensionsTests.cs b/PayBridge.SDK.Test/Unit/IServiceCollectionExtensionsTests.cs index 5a2a20a..85e9160 100644 --- a/PayBridge.SDK.Test/Unit/IServiceCollectionExtensionsTests.cs +++ b/PayBridge.SDK.Test/Unit/IServiceCollectionExtensionsTests.cs @@ -183,7 +183,7 @@ public void AddPayBridge_WhenEnabledGatewayUsesPlaceholderCredential_Throws() var action = () => services.AddPayBridge(configuration); action.Should().Throw() - .WithMessage("*Paystack*missing required configuration values*"); + .WithMessage("*Paystack*missing required configuration values:*PaymentGatewayConfig:Paystack:SecretKey*"); } [Fact] diff --git a/PayBridge.SDK/Externsions/IServiceCollectionExtensions.cs b/PayBridge.SDK/Externsions/IServiceCollectionExtensions.cs index 0ac01ca..34e867b 100644 --- a/PayBridge.SDK/Externsions/IServiceCollectionExtensions.cs +++ b/PayBridge.SDK/Externsions/IServiceCollectionExtensions.cs @@ -208,8 +208,9 @@ private static void ValidateEnabledGatewayConfiguration(PaymentGatewayConfig con if (!IsGatewayConfigured(config, gateway)) { + var missingSettings = GetMissingGatewayConfigurationKeys(config, gateway); errors.Add( - $"Enabled gateway '{gateway}' is missing required configuration values."); + $"Enabled gateway '{gateway}' is missing required configuration values: {string.Join(", ", missingSettings)}."); } } @@ -344,6 +345,80 @@ private static bool IsGatewayConfigured(PaymentGatewayConfig config, PaymentGate }; } + private static IReadOnlyList GetMissingGatewayConfigurationKeys(PaymentGatewayConfig config, PaymentGatewayType gateway) + { + var missingSettings = new List(); + + void AddIfMissing(string key, string value) + { + if (!HasValue(value)) + { + missingSettings.Add(key); + } + } + + switch (gateway) + { + case PaymentGatewayType.Paystack: + AddIfMissing("PaymentGatewayConfig:Paystack:SecretKey", config.Paystack.SecretKey); + break; + case PaymentGatewayType.Flutterwave: + AddIfMissing("PaymentGatewayConfig:FlutterwaveConfig:SecretKey", config.FlutterwaveConfig.SecretKey); + break; + case PaymentGatewayType.Stripe: + AddIfMissing("PaymentGatewayConfig:Stripe:SecretKey", config.Stripe.SecretKey); + break; + case PaymentGatewayType.Checkout: + AddIfMissing("PaymentGatewayConfig:Checkout:SecretKey", config.Checkout.SecretKey); + break; + case PaymentGatewayType.BenefitPay: + AddIfMissing("PaymentGatewayConfig:BenefitPay:MerchantId", config.BenefitPay.MerchantId); + AddIfMissing("PaymentGatewayConfig:BenefitPay:ApiKey", config.BenefitPay.ApiKey); + break; + case PaymentGatewayType.Knet: + AddIfMissing("PaymentGatewayConfig:Knet:TransportId", config.Knet.TransportId); + AddIfMissing("PaymentGatewayConfig:Knet:Password", config.Knet.Password); + break; + case PaymentGatewayType.Monnify: + AddIfMissing("PaymentGatewayConfig:Monnify:ApiKey", config.Monnify.ApiKey); + AddIfMissing("PaymentGatewayConfig:Monnify:SecretKey", config.Monnify.SecretKey); + AddIfMissing("PaymentGatewayConfig:Monnify:ContractCode", config.Monnify.ContractCode); + break; + case PaymentGatewayType.Squad: + AddIfMissing("PaymentGatewayConfig:Squad:SecretKey", config.Squad.SecretKey); + break; + case PaymentGatewayType.Korapay: + AddIfMissing("PaymentGatewayConfig:Korapay:SecretKey", config.Korapay.SecretKey); + break; + case PaymentGatewayType.Interswitch: + AddIfMissing("PaymentGatewayConfig:Interswitch:ClientId", config.Interswitch.ClientId); + AddIfMissing("PaymentGatewayConfig:Interswitch:ClientSecret", config.Interswitch.ClientSecret); + AddIfMissing("PaymentGatewayConfig:Interswitch:MerchantCode", config.Interswitch.MerchantCode); + break; + case PaymentGatewayType.Remita: + AddIfMissing("PaymentGatewayConfig:Remita:MerchantId", config.Remita.MerchantId); + AddIfMissing("PaymentGatewayConfig:Remita:ServiceTypeId", config.Remita.ServiceTypeId); + AddIfMissing("PaymentGatewayConfig:Remita:ApiKey", config.Remita.ApiKey); + break; + case PaymentGatewayType.Opay: + AddIfMissing("PaymentGatewayConfig:Opay:MerchantId", config.Opay.MerchantId); + AddIfMissing("PaymentGatewayConfig:Opay:SecretKey", config.Opay.SecretKey); + break; + case PaymentGatewayType.DpoGroup: + AddIfMissing("PaymentGatewayConfig:DpoGroup:CompanyToken", config.DpoGroup.CompanyToken); + break; + case PaymentGatewayType.PawaPay: + AddIfMissing("PaymentGatewayConfig:PawaPay:ApiToken", config.PawaPay.ApiToken); + break; + case PaymentGatewayType.PeachPayments: + AddIfMissing("PaymentGatewayConfig:PeachPayments:EntityId", config.PeachPayments.EntityId); + AddIfMissing("PaymentGatewayConfig:PeachPayments:AccessToken", config.PeachPayments.AccessToken); + break; + } + + return missingSettings; + } + private static bool HasValue(string value) { return !string.IsNullOrWhiteSpace(value) &&