From bfc1c8064ab16626140fd725d2315d599174b0ae Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Fri, 23 Jan 2026 14:44:04 -0500 Subject: [PATCH] refactor(securestore): store entire connection strings in SecureStore Eliminates placeholder substitution (${KEY}) in favor of storing complete connection strings as single encrypted values. SecureStore now auto-creates entries for all connection strings defined in appsettings. ConfigManager editor reads/writes values directly to SecureStore. --- .../Options/SecureStoreOptions.cs | 6 + .../DbConnectionFactory.cs | 35 ++-- NEW/src/JdeScoping.Host/appsettings.json | 16 +- .../DependencyInjection.cs | 10 +- .../Security/SecureStoreService.cs | 33 ++++ .../Validation/ConnectionStringValidator.cs | 91 ++-------- .../SecureStore/ISecureStoreManager.cs | 10 +- .../SecureStore/SecureStoreManager.cs | 12 +- .../Forms/ConnectionStringsFormViewModel.cs | 79 ++++++++- .../ViewModels/MainWindowViewModel.cs | 19 +- .../SecureStore/SecureStoreManagerTests.cs | 135 ++++++++++++++ .../ConnectionStringsFormViewModelTests.cs | 33 +++- .../DbConnectionFactoryGiwTests.cs | 23 +-- .../DbConnectionFactoryTests.cs | 61 +++---- .../DevEtlPipelineFactoryTests.cs | 13 +- .../ConnectionStringValidatorTests.cs | 165 +++++++----------- 16 files changed, 462 insertions(+), 279 deletions(-) diff --git a/NEW/src/JdeScoping.Core/Options/SecureStoreOptions.cs b/NEW/src/JdeScoping.Core/Options/SecureStoreOptions.cs index 0e6cc9d..0ee7ef9 100644 --- a/NEW/src/JdeScoping.Core/Options/SecureStoreOptions.cs +++ b/NEW/src/JdeScoping.Core/Options/SecureStoreOptions.cs @@ -31,4 +31,10 @@ public class SecureStoreOptions /// List of secret keys that must exist in the store for the application to start. /// public List RequiredKeys { get; set; } = []; + + /// + /// List of connection string names that must exist in the store. + /// Populated automatically from the ConnectionStrings configuration section. + /// + public List RequiredConnectionStrings { get; set; } = []; } diff --git a/NEW/src/JdeScoping.DataAccess/DbConnectionFactory.cs b/NEW/src/JdeScoping.DataAccess/DbConnectionFactory.cs index 68dfb95..c116c46 100644 --- a/NEW/src/JdeScoping.DataAccess/DbConnectionFactory.cs +++ b/NEW/src/JdeScoping.DataAccess/DbConnectionFactory.cs @@ -1,7 +1,7 @@ +using JdeScoping.Core.Interfaces; using JdeScoping.DataAccess.Exceptions; using JdeScoping.DataAccess.Interfaces; using Microsoft.Data.SqlClient; -using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using Oracle.ManagedDataAccess.Client; @@ -9,35 +9,29 @@ namespace JdeScoping.DataAccess; /// /// Factory for creating database connections to all data sources. +/// Retrieves connection strings from SecureStore. /// public class DbConnectionFactory : IDbConnectionFactory { - private readonly IConfiguration _configuration; + private readonly ISecureStoreService _secureStore; private readonly ILogger _logger; /// /// Initializes a new instance of the class. /// - /// Application configuration. + /// SecureStore service for retrieving connection strings. /// Logger instance. - public DbConnectionFactory(IConfiguration configuration, ILogger logger) + public DbConnectionFactory(ISecureStoreService secureStore, ILogger logger) { - _configuration = configuration ?? throw new ArgumentNullException(nameof(configuration)); + _secureStore = secureStore ?? throw new ArgumentNullException(nameof(secureStore)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); } /// public async Task CreateLotFinderConnectionAsync(CancellationToken ct = default) { - const string dataSource = "LotFinderDB"; - var connectionString = _configuration.GetConnectionString(dataSource); - - if (string.IsNullOrEmpty(connectionString)) - { - throw new ConnectionException( - $"{dataSource}: Connection string not found in configuration.", - dataSource); - } + const string dataSource = "LotFinder"; + var connectionString = GetConnectionString(dataSource); try { @@ -93,17 +87,24 @@ public class DbConnectionFactory : IDbConnectionFactory return await CreateOracleConnectionAsync("GIW", ct).ConfigureAwait(false); } - private async Task CreateOracleConnectionAsync(string dataSource, CancellationToken ct) + private string GetConnectionString(string dataSource) { - var connectionString = _configuration.GetConnectionString(dataSource); + var connectionString = _secureStore.Get(dataSource); if (string.IsNullOrEmpty(connectionString)) { throw new ConnectionException( - $"{dataSource}: Connection string not found in configuration.", + $"{dataSource}: Connection string not found in SecureStore. Use ConfigManager to configure the connection string.", dataSource); } + return connectionString; + } + + private async Task CreateOracleConnectionAsync(string dataSource, CancellationToken ct) + { + var connectionString = GetConnectionString(dataSource); + try { _logger.LogDebug("Creating connection to {DataSource}", dataSource); diff --git a/NEW/src/JdeScoping.Host/appsettings.json b/NEW/src/JdeScoping.Host/appsettings.json index 42d178d..d83f6b3 100644 --- a/NEW/src/JdeScoping.Host/appsettings.json +++ b/NEW/src/JdeScoping.Host/appsettings.json @@ -1,9 +1,9 @@ { "ConnectionStrings": { - "LotFinder": "Server=localhost,1434;Database=ScopingTool;User Id=scopingapp;Password=Sc0ping@pp_Dev#2024;TrustServerCertificate=true", - "JDE": "Data Source=jde-server:1521/JDEPROD;User Id=${JDE_USER};Password=${JDE_PASSWORD}", - "CMS": "Data Source=cms-server:1521/CMSPROD;User Id=${CMS_USER};Password=${CMS_PASSWORD}", - "GIW": "Data Source=giw-server:1521/GIWPROD;User Id=${GIW_USER};Password=${GIW_PASSWORD}" + "LotFinder": "", + "JDE": "", + "CMS": "", + "GIW": "" }, "DataAccess": { "CommandTimeoutSeconds": 120, @@ -54,13 +54,7 @@ "RequiredKeys": [ "RsaPrivateKey", "ExcelExport:CriteriaSheetPassword", - "ExcelExport:DataSheetPassword", - "JdeUser", - "JdePassword", - "GiwUser", - "GiwPassword", - "CmsUser", - "CmsPassword" + "ExcelExport:DataSheetPassword" ] }, "WorkProcessor": { diff --git a/NEW/src/JdeScoping.Infrastructure/DependencyInjection.cs b/NEW/src/JdeScoping.Infrastructure/DependencyInjection.cs index ce03edf..69a0446 100644 --- a/NEW/src/JdeScoping.Infrastructure/DependencyInjection.cs +++ b/NEW/src/JdeScoping.Infrastructure/DependencyInjection.cs @@ -43,8 +43,14 @@ public static class InfrastructureDependencyInjection } // Register SecureStore for encrypted secrets storage - services.Configure( - configuration.GetSection(SecureStoreOptions.SectionName)); + services.Configure(opts => + { + configuration.GetSection(SecureStoreOptions.SectionName).Bind(opts); + + // Populate RequiredConnectionStrings from ConnectionStrings section + var connectionStrings = configuration.GetSection("ConnectionStrings").GetChildren(); + opts.RequiredConnectionStrings = connectionStrings.Select(c => c.Key).ToList(); + }); services.AddSingleton(); diff --git a/NEW/src/JdeScoping.Infrastructure/Security/SecureStoreService.cs b/NEW/src/JdeScoping.Infrastructure/Security/SecureStoreService.cs index 7189e3f..2691d7a 100644 --- a/NEW/src/JdeScoping.Infrastructure/Security/SecureStoreService.cs +++ b/NEW/src/JdeScoping.Infrastructure/Security/SecureStoreService.cs @@ -44,6 +44,9 @@ public class SecureStoreService : ISecureStoreService _secretsManager = SecretsManager.LoadStore(_storePath); _secretsManager.LoadKeyFromFile(keyFilePath); LoadKeys(); + + // Ensure all required entries exist + EnsureRequiredEntries(opts); } else if (opts.AutoCreateStore) { @@ -59,6 +62,9 @@ public class SecureStoreService : ISecureStoreService // Save empty store _secretsManager.SaveStore(_storePath); + + // Ensure all required entries exist + EnsureRequiredEntries(opts); } else { @@ -218,4 +224,31 @@ public class SecureStoreService : ISecureStoreService Directory.CreateDirectory(directory); } } + + /// + /// Ensures all required entries exist in the store, creating empty values for missing keys. + /// + private void EnsureRequiredEntries(SecureStoreOptions opts) + { + var allRequired = opts.RequiredKeys + .Concat(opts.RequiredConnectionStrings) + .Distinct(); + + var addedAny = false; + foreach (var key in allRequired) + { + if (!Contains(key)) + { + _logger.LogInformation("Creating missing required SecureStore entry: {Key}", key); + Set(key, string.Empty); + addedAny = true; + } + } + + if (addedAny) + { + SaveKeysMetadata(); + Save(); + } + } } diff --git a/NEW/src/JdeScoping.Infrastructure/Validation/ConnectionStringValidator.cs b/NEW/src/JdeScoping.Infrastructure/Validation/ConnectionStringValidator.cs index 7966be1..3235ce2 100644 --- a/NEW/src/JdeScoping.Infrastructure/Validation/ConnectionStringValidator.cs +++ b/NEW/src/JdeScoping.Infrastructure/Validation/ConnectionStringValidator.cs @@ -1,4 +1,3 @@ -using System.Text.RegularExpressions; using JdeScoping.Core.Interfaces; using JdeScoping.Core.Validation; using Microsoft.Extensions.Configuration; @@ -7,9 +6,9 @@ using Microsoft.Extensions.Logging; namespace JdeScoping.Infrastructure.Validation; /// -/// Validates connection strings by resolving SecureStore placeholders and verifying format. +/// Validates that all connection strings defined in configuration exist in SecureStore. /// -public partial class ConnectionStringValidator : IConfigurationValidator +public class ConnectionStringValidator : IConfigurationValidator { private readonly IConfiguration _configuration; private readonly ISecureStoreService _secureStore; @@ -21,9 +20,6 @@ public partial class ConnectionStringValidator : IConfigurationValidator /// public string Name => "ConnectionStrings"; - [GeneratedRegex(@"\$\{([^}]+)\}")] - private static partial Regex PlaceholderPattern(); - public ConnectionStringValidator( IConfiguration configuration, ISecureStoreService secureStore, @@ -48,96 +44,43 @@ public partial class ConnectionStringValidator : IConfigurationValidator return result; } - _logger.LogDebug("Validating {Count} connection strings", connectionStrings.Count); + _logger.LogDebug("Validating {Count} connection strings from SecureStore", connectionStrings.Count); foreach (var connectionString in connectionStrings) { - ValidateConnectionString(connectionString.Key, connectionString.Value, result); + ValidateConnectionString(connectionString.Key, result); } return result; } - private void ValidateConnectionString(string name, string? connectionString, ConfigurationValidationResult result) + private void ValidateConnectionString(string name, ConfigurationValidationResult result) { - if (string.IsNullOrWhiteSpace(connectionString)) + _logger.LogDebug("Validating connection string: {Name}", name); + + // Check if connection string exists in SecureStore + if (!_secureStore.Contains(name)) { - result.AddError($"Connection string '{name}' is empty"); + result.AddError($"Connection string '{name}' not found in SecureStore"); return; } - _logger.LogDebug("Validating connection string: {Name}", name); + // Get the value from SecureStore + var connectionStringValue = _secureStore.Get(name); - // Extract and validate all placeholders - var placeholders = ExtractPlaceholders(connectionString); - var resolvedConnectionString = connectionString; - var hasPlaceholderErrors = false; - - foreach (var placeholder in placeholders) + if (string.IsNullOrWhiteSpace(connectionStringValue)) { - var resolvedValue = ResolvePlaceholder(placeholder, name, result); - if (resolvedValue is null) - { - hasPlaceholderErrors = true; - } - else - { - // Replace placeholder with resolved value for format validation - resolvedConnectionString = resolvedConnectionString.Replace($"${{{placeholder}}}", resolvedValue); - } + result.AddError($"Connection string '{name}' is empty in SecureStore. Use ConfigManager to set the connection string value."); + return; } - // Only validate format if all placeholders were resolved - if (!hasPlaceholderErrors) - { - ValidateConnectionStringFormat(name, resolvedConnectionString, result); - } - } - - private static List ExtractPlaceholders(string connectionString) - { - var placeholders = new List(); - var matches = PlaceholderPattern().Matches(connectionString); - - foreach (Match match in matches) - { - placeholders.Add(match.Groups[1].Value); - } - - return placeholders; - } - - private string? ResolvePlaceholder(string key, string connectionStringName, ConfigurationValidationResult result) - { - if (!_secureStore.Contains(key)) - { - result.AddError($"Connection string '{connectionStringName}' references placeholder '${{{key}}}' but key '{key}' not found in SecureStore"); - return null; - } - - try - { - var value = _secureStore.Get(key); - if (string.IsNullOrEmpty(value)) - { - result.AddError($"Connection string '{connectionStringName}' references placeholder '${{{key}}}' but key '{key}' has an empty value in SecureStore"); - return null; - } - - _logger.LogDebug("Resolved placeholder '{Key}' for connection string '{Name}'", key, connectionStringName); - return value; - } - catch (Exception ex) - { - result.AddError($"Connection string '{connectionStringName}' references placeholder '${{{key}}}' but failed to retrieve from SecureStore: {ex.Message}"); - return null; - } + // Validate format + ValidateConnectionStringFormat(name, connectionStringValue, result); } private void ValidateConnectionStringFormat(string name, string connectionString, ConfigurationValidationResult result) { // Check for required connection string components - // Support both "Server=" (SQL Server) and "Data Source=" (Oracle/generic) var hasServer = connectionString.Contains("Server=", StringComparison.OrdinalIgnoreCase) || connectionString.Contains("Data Source=", StringComparison.OrdinalIgnoreCase) || connectionString.Contains("Host=", StringComparison.OrdinalIgnoreCase); diff --git a/NEW/src/Utils/JdeScoping.ConfigManager/Services/SecureStore/ISecureStoreManager.cs b/NEW/src/Utils/JdeScoping.ConfigManager/Services/SecureStore/ISecureStoreManager.cs index b8e2987..f64000e 100644 --- a/NEW/src/Utils/JdeScoping.ConfigManager/Services/SecureStore/ISecureStoreManager.cs +++ b/NEW/src/Utils/JdeScoping.ConfigManager/Services/SecureStore/ISecureStoreManager.cs @@ -71,11 +71,15 @@ public interface ISecureStoreManager void RemoveSecret(string key); /// - /// Ensures all required keys exist in the store, creating blank values for any missing keys. + /// Ensures all required entries exist in the store, creating blank values for any missing keys. + /// Handles both general required keys and connection string names. /// - /// List of keys that must exist. + /// List of general secret keys that must exist. + /// List of connection string names that must exist. /// List of keys that were added. - IReadOnlyList EnsureRequiredKeys(IEnumerable requiredKeys); + IReadOnlyList EnsureAllRequiredEntries( + IEnumerable requiredKeys, + IEnumerable connectionStringNames); /// /// Generates a new key file for use with store encryption. diff --git a/NEW/src/Utils/JdeScoping.ConfigManager/Services/SecureStore/SecureStoreManager.cs b/NEW/src/Utils/JdeScoping.ConfigManager/Services/SecureStore/SecureStoreManager.cs index 10ca764..2986102 100644 --- a/NEW/src/Utils/JdeScoping.ConfigManager/Services/SecureStore/SecureStoreManager.cs +++ b/NEW/src/Utils/JdeScoping.ConfigManager/Services/SecureStore/SecureStoreManager.cs @@ -193,7 +193,9 @@ public class SecureStoreManager : ISecureStoreManager, IDisposable } /// - public IReadOnlyList EnsureRequiredKeys(IEnumerable requiredKeys) + public IReadOnlyList EnsureAllRequiredEntries( + IEnumerable requiredKeys, + IEnumerable connectionStringNames) { ThrowIfDisposed(); @@ -201,11 +203,15 @@ public class SecureStoreManager : ISecureStoreManager, IDisposable throw new InvalidOperationException("No store is currently open."); var addedKeys = new List(); - foreach (var key in requiredKeys) + var allRequired = requiredKeys + .Concat(connectionStringNames) + .Distinct(); + + foreach (var key in allRequired) { if (!_keys.Contains(key)) { - _logger.LogInformation("Adding missing required key: {Key}", key); + _logger.LogInformation("Adding missing required entry: {Key}", key); SetSecret(key, string.Empty); addedKeys.Add(key); } diff --git a/NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/Forms/ConnectionStringsFormViewModel.cs b/NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/Forms/ConnectionStringsFormViewModel.cs index d4184b7..7dd399d 100644 --- a/NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/Forms/ConnectionStringsFormViewModel.cs +++ b/NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/Forms/ConnectionStringsFormViewModel.cs @@ -2,16 +2,18 @@ using System.Collections.ObjectModel; using System.Windows.Input; using JdeScoping.ConfigManager.Models; using JdeScoping.ConfigManager.Services; +using JdeScoping.ConfigManager.Services.SecureStore; namespace JdeScoping.ConfigManager.ViewModels.Forms; /// -/// ViewModel for editing the ConnectionStrings configuration section. -/// Manages a collection of connection string entries with add, delete, validate, and test commands. +/// ViewModel for editing connection strings stored in SecureStore. +/// Connection string names come from configuration, values come from SecureStore. /// public class ConnectionStringsFormViewModel : ViewModelBase { private readonly ConnectionStringsSection _model; + private readonly ISecureStoreManager _secureStoreManager; private readonly Action _onChanged; private readonly IDialogService _dialogService; private readonly IConnectionTestService _connectionTestService; @@ -21,27 +23,42 @@ public class ConnectionStringsFormViewModel : ViewModelBase /// /// Initializes a new instance of the class. /// - /// The connection strings section model. + /// The connection strings section model (provides names). + /// The SecureStore manager for reading/writing values. /// The action to invoke when any property changes. /// The dialog service for showing messages and confirmations. /// The service for testing database connections. public ConnectionStringsFormViewModel( ConnectionStringsSection model, + ISecureStoreManager secureStoreManager, Action onChanged, IDialogService dialogService, IConnectionTestService connectionTestService) { _model = model ?? throw new ArgumentNullException(nameof(model)); + _secureStoreManager = secureStoreManager ?? throw new ArgumentNullException(nameof(secureStoreManager)); _onChanged = onChanged ?? throw new ArgumentNullException(nameof(onChanged)); _dialogService = dialogService ?? throw new ArgumentNullException(nameof(dialogService)); _connectionTestService = connectionTestService ?? throw new ArgumentNullException(nameof(connectionTestService)); Connections = new ObservableCollection(); - // Initialize view models from model entries + // Initialize view models from model entries, loading values from SecureStore foreach (var entry in _model.Entries) { - Connections.Add(new ConnectionStringEntryViewModel(entry, _onChanged)); + // Load the actual connection string value from SecureStore + var secureStoreValue = _secureStoreManager.IsStoreOpen && !string.IsNullOrEmpty(entry.Name) + ? TryGetSecret(entry.Name) + : null; + + // Update entry's RawConnectionString with SecureStore value if available + if (!string.IsNullOrEmpty(secureStoreValue)) + { + entry.RawConnectionString = secureStoreValue; + entry.Provider = ConnectionProvider.Generic; // Use Generic since we have the full string + } + + Connections.Add(new ConnectionStringEntryViewModel(entry, OnEntryChanged)); } // Initialize commands @@ -51,6 +68,32 @@ public class ConnectionStringsFormViewModel : ViewModelBase TestConnectionCommand = new AsyncRelayCommand(TestConnectionAsync, () => HasSelection && !IsTesting); } + private string? TryGetSecret(string key) + { + try + { + return _secureStoreManager.GetSecret(key); + } + catch (KeyNotFoundException) + { + return null; + } + } + + private void OnEntryChanged() + { + // When an entry changes, save its value to SecureStore + if (SelectedConnection != null && !string.IsNullOrEmpty(SelectedConnection.Name)) + { + var connectionString = SelectedConnection.GeneratedConnectionString; + if (_secureStoreManager.IsStoreOpen) + { + _secureStoreManager.SetSecret(SelectedConnection.Name, connectionString); + } + } + _onChanged(); + } + /// /// Gets the collection of connection string entry view models. /// @@ -136,12 +179,19 @@ public class ConnectionStringsFormViewModel : ViewModelBase { var entry = new ConnectionStringEntry { - Name = "NewConnection" + Name = "NewConnection", + Provider = ConnectionProvider.Generic }; _model.Entries.Add(entry); - var viewModel = new ConnectionStringEntryViewModel(entry, _onChanged); + // Create empty entry in SecureStore + if (_secureStoreManager.IsStoreOpen) + { + _secureStoreManager.SetSecret(entry.Name, string.Empty); + } + + var viewModel = new ConnectionStringEntryViewModel(entry, OnEntryChanged); Connections.Add(viewModel); SelectedConnection = viewModel; @@ -160,11 +210,24 @@ public class ConnectionStringsFormViewModel : ViewModelBase var name = SelectedConnection.Name; var confirmed = await _dialogService.ShowConfirmationAsync( "Delete Connection", - $"Delete connection '{name}'?"); + $"Delete connection '{name}'? This will also remove the SecureStore entry."); if (!confirmed) return; + // Remove from SecureStore + if (_secureStoreManager.IsStoreOpen && !string.IsNullOrEmpty(name)) + { + try + { + _secureStoreManager.RemoveSecret(name); + } + catch (KeyNotFoundException) + { + // Entry didn't exist in SecureStore, that's OK + } + } + // Find the model entry to remove var modelEntry = _model.Entries.FirstOrDefault(e => e.Name == name); if (modelEntry != null) diff --git a/NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/MainWindowViewModel.cs b/NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/MainWindowViewModel.cs index 74042e2..0ddc68e 100644 --- a/NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/MainWindowViewModel.cs +++ b/NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/MainWindowViewModel.cs @@ -346,14 +346,18 @@ public class MainWindowViewModel : ViewModelBase _secureStoreManager.OpenStore(storePath, keyFilePath); } - // Ensure all required keys exist - if (secureStoreConfig.RequiredKeys?.Count > 0) + // Ensure all required entries exist (both RequiredKeys and connection strings) + var connectionStringNames = _appSettings?.ConnectionStrings?.Entries + .Select(e => e.Name) + .Where(n => !string.IsNullOrEmpty(n)) + ?? Enumerable.Empty(); + + var requiredKeys = secureStoreConfig.RequiredKeys ?? new List(); + + var addedKeys = _secureStoreManager.EnsureAllRequiredEntries(requiredKeys, connectionStringNames); + if (addedKeys.Count > 0) { - var addedKeys = _secureStoreManager.EnsureRequiredKeys(secureStoreConfig.RequiredKeys); - if (addedKeys.Count > 0) - { - _logger?.LogInformation("Added {Count} missing required keys", addedKeys.Count); - } + _logger?.LogInformation("Added {Count} missing required SecureStore entries", addedKeys.Count); } } catch (Exception ex) @@ -575,6 +579,7 @@ public class MainWindowViewModel : ViewModelBase "ExcelExport" => new ExcelExportFormViewModel(_appSettings.ExcelExport, MarkAsChanged), "ConnectionStrings" when _dialogService != null => new ConnectionStringsFormViewModel( _appSettings.ConnectionStrings, + _secureStoreManager, MarkAsChanged, _dialogService, _connectionTestService), diff --git a/NEW/tests/JdeScoping.ConfigManager.Tests/Services/SecureStore/SecureStoreManagerTests.cs b/NEW/tests/JdeScoping.ConfigManager.Tests/Services/SecureStore/SecureStoreManagerTests.cs index f2f5d18..06f9b11 100644 --- a/NEW/tests/JdeScoping.ConfigManager.Tests/Services/SecureStore/SecureStoreManagerTests.cs +++ b/NEW/tests/JdeScoping.ConfigManager.Tests/Services/SecureStore/SecureStoreManagerTests.cs @@ -294,4 +294,139 @@ public class SecureStoreManagerTests : IDisposable // Act & Assert Should.Throw(() => _sut.GetSecret("key")); } + + [Fact] + public void EnsureAllRequiredEntries_AddsMissingKeys() + { + // Arrange + var storePath = Path.Combine(_testDirectory, "test.json"); + var keyPath = Path.Combine(_testDirectory, "test.key"); + _sut.CreateStore(storePath, keyPath); + _sut.SetSecret("existingKey", "existingValue"); + _sut.Save(); + + // Act + var addedKeys = _sut.EnsureAllRequiredEntries( + new[] { "existingKey", "newKey1" }, + new[] { "LotFinder", "JDE" }); + + // Assert + addedKeys.Count.ShouldBe(3); + addedKeys.ShouldContain("newKey1"); + addedKeys.ShouldContain("LotFinder"); + addedKeys.ShouldContain("JDE"); + addedKeys.ShouldNotContain("existingKey"); + + // Verify all keys exist + _sut.GetKeys().ShouldContain("existingKey"); + _sut.GetKeys().ShouldContain("newKey1"); + _sut.GetKeys().ShouldContain("LotFinder"); + _sut.GetKeys().ShouldContain("JDE"); + } + + [Fact] + public void EnsureAllRequiredEntries_CreatesEmptyValues() + { + // Arrange + var storePath = Path.Combine(_testDirectory, "test.json"); + var keyPath = Path.Combine(_testDirectory, "test.key"); + _sut.CreateStore(storePath, keyPath); + + // Act + _sut.EnsureAllRequiredEntries( + new[] { "newKey" }, + Array.Empty()); + + // Assert + _sut.GetSecret("newKey").ShouldBe(string.Empty); + } + + [Fact] + public void EnsureAllRequiredEntries_DoesNotOverwriteExistingValues() + { + // Arrange + var storePath = Path.Combine(_testDirectory, "test.json"); + var keyPath = Path.Combine(_testDirectory, "test.key"); + _sut.CreateStore(storePath, keyPath); + _sut.SetSecret("existingKey", "originalValue"); + _sut.Save(); + + // Act + var addedKeys = _sut.EnsureAllRequiredEntries( + new[] { "existingKey" }, + Array.Empty()); + + // Assert + addedKeys.ShouldBeEmpty(); // Key already existed + _sut.GetSecret("existingKey").ShouldBe("originalValue"); // Value preserved + } + + [Fact] + public void EnsureAllRequiredEntries_AutoSavesWhenKeysAdded() + { + // Arrange + var storePath = Path.Combine(_testDirectory, "test.json"); + var keyPath = Path.Combine(_testDirectory, "test.key"); + _sut.CreateStore(storePath, keyPath); + + // Act + _sut.EnsureAllRequiredEntries( + new[] { "newKey" }, + Array.Empty()); + + // Assert - should auto-save (HasUnsavedChanges should be false after calling EnsureAllRequiredEntries) + _sut.HasUnsavedChanges.ShouldBeFalse(); + + // Verify persistence + _sut.CloseStore(); + _sut.OpenStore(storePath, keyPath); + _sut.GetKeys().ShouldContain("newKey"); + } + + [Fact] + public void EnsureAllRequiredEntries_ReturnsEmptyList_WhenNoMissingKeys() + { + // Arrange + var storePath = Path.Combine(_testDirectory, "test.json"); + var keyPath = Path.Combine(_testDirectory, "test.key"); + _sut.CreateStore(storePath, keyPath); + _sut.SetSecret("key1", "value1"); + _sut.SetSecret("key2", "value2"); + _sut.Save(); + + // Act + var addedKeys = _sut.EnsureAllRequiredEntries( + new[] { "key1" }, + new[] { "key2" }); + + // Assert + addedKeys.ShouldBeEmpty(); + } + + [Fact] + public void EnsureAllRequiredEntries_WhenNoStoreOpen_ThrowsInvalidOperationException() + { + // Act & Assert + Should.Throw(() => _sut.EnsureAllRequiredEntries( + new[] { "key" }, + Array.Empty())); + } + + [Fact] + public void EnsureAllRequiredEntries_HandlesDuplicatesAcrossLists() + { + // Arrange + var storePath = Path.Combine(_testDirectory, "test.json"); + var keyPath = Path.Combine(_testDirectory, "test.key"); + _sut.CreateStore(storePath, keyPath); + + // Act - same key in both lists should not cause issues + var addedKeys = _sut.EnsureAllRequiredEntries( + new[] { "sharedKey" }, + new[] { "sharedKey" }); + + // Assert - should only add once + addedKeys.Count.ShouldBe(1); + addedKeys.ShouldContain("sharedKey"); + } } diff --git a/NEW/tests/JdeScoping.ConfigManager.Tests/ViewModels/Forms/ConnectionStringsFormViewModelTests.cs b/NEW/tests/JdeScoping.ConfigManager.Tests/ViewModels/Forms/ConnectionStringsFormViewModelTests.cs index 87b42fd..c9621d4 100644 --- a/NEW/tests/JdeScoping.ConfigManager.Tests/ViewModels/Forms/ConnectionStringsFormViewModelTests.cs +++ b/NEW/tests/JdeScoping.ConfigManager.Tests/ViewModels/Forms/ConnectionStringsFormViewModelTests.cs @@ -1,18 +1,24 @@ using JdeScoping.ConfigManager.Models; using JdeScoping.ConfigManager.Services; +using JdeScoping.ConfigManager.Services.SecureStore; using JdeScoping.ConfigManager.ViewModels.Forms; namespace JdeScoping.ConfigManager.Tests.ViewModels.Forms; public class ConnectionStringsFormViewModelTests { + private readonly ISecureStoreManager _secureStoreManager; private readonly IDialogService _dialogService; private readonly IConnectionTestService _connectionTestService; public ConnectionStringsFormViewModelTests() { + _secureStoreManager = Substitute.For(); _dialogService = Substitute.For(); _connectionTestService = Substitute.For(); + + // Setup default behavior - SecureStore is not open by default in tests + _secureStoreManager.IsStoreOpen.Returns(false); } [Fact] @@ -39,7 +45,7 @@ public class ConnectionStringsFormViewModelTests }; // Act - var sut = new ConnectionStringsFormViewModel(model, () => { }, _dialogService, _connectionTestService); + var sut = new ConnectionStringsFormViewModel(model, _secureStoreManager, () => { }, _dialogService, _connectionTestService); // Assert sut.Connections.Count.ShouldBe(2); @@ -56,7 +62,18 @@ public class ConnectionStringsFormViewModelTests { // Act & Assert Should.Throw(() => - new ConnectionStringsFormViewModel(null!, () => { }, _dialogService, _connectionTestService)); + new ConnectionStringsFormViewModel(null!, _secureStoreManager, () => { }, _dialogService, _connectionTestService)); + } + + [Fact] + public void Constructor_ThrowsOnNullSecureStoreManager() + { + // Arrange + var model = new ConnectionStringsSection(); + + // Act & Assert + Should.Throw(() => + new ConnectionStringsFormViewModel(model, null!, () => { }, _dialogService, _connectionTestService)); } [Fact] @@ -67,7 +84,7 @@ public class ConnectionStringsFormViewModelTests // Act & Assert Should.Throw(() => - new ConnectionStringsFormViewModel(model, null!, _dialogService, _connectionTestService)); + new ConnectionStringsFormViewModel(model, _secureStoreManager, null!, _dialogService, _connectionTestService)); } [Fact] @@ -78,7 +95,7 @@ public class ConnectionStringsFormViewModelTests // Act & Assert Should.Throw(() => - new ConnectionStringsFormViewModel(model, () => { }, _dialogService, null!)); + new ConnectionStringsFormViewModel(model, _secureStoreManager, () => { }, _dialogService, null!)); } [Fact] @@ -87,7 +104,7 @@ public class ConnectionStringsFormViewModelTests // Arrange var model = new ConnectionStringsSection(); var changedInvoked = false; - var sut = new ConnectionStringsFormViewModel(model, () => changedInvoked = true, _dialogService, _connectionTestService); + var sut = new ConnectionStringsFormViewModel(model, _secureStoreManager, () => changedInvoked = true, _dialogService, _connectionTestService); // Act sut.AddConnectionCommand.Execute(null); @@ -111,7 +128,7 @@ public class ConnectionStringsFormViewModelTests new ConnectionStringEntry { Name = "Conn1" } } }; - var sut = new ConnectionStringsFormViewModel(model, () => { }, _dialogService, _connectionTestService); + var sut = new ConnectionStringsFormViewModel(model, _secureStoreManager, () => { }, _dialogService, _connectionTestService); // Assert - no selection by default sut.SelectedConnection.ShouldBeNull(); @@ -129,7 +146,7 @@ public class ConnectionStringsFormViewModelTests new ConnectionStringEntry { Name = "Conn1" } } }; - var sut = new ConnectionStringsFormViewModel(model, () => { }, _dialogService, _connectionTestService); + var sut = new ConnectionStringsFormViewModel(model, _secureStoreManager, () => { }, _dialogService, _connectionTestService); // Act sut.SelectedConnection = sut.Connections[0]; @@ -153,7 +170,7 @@ public class ConnectionStringsFormViewModelTests }; // Act - var sut = new ConnectionStringsFormViewModel(model, () => { }, _dialogService, _connectionTestService); + var sut = new ConnectionStringsFormViewModel(model, _secureStoreManager, () => { }, _dialogService, _connectionTestService); // Assert sut.ConnectionCount.ShouldBe(3); diff --git a/NEW/tests/JdeScoping.DataAccess.Tests/DbConnectionFactoryGiwTests.cs b/NEW/tests/JdeScoping.DataAccess.Tests/DbConnectionFactoryGiwTests.cs index 6bd8f82..01ba170 100644 --- a/NEW/tests/JdeScoping.DataAccess.Tests/DbConnectionFactoryGiwTests.cs +++ b/NEW/tests/JdeScoping.DataAccess.Tests/DbConnectionFactoryGiwTests.cs @@ -1,5 +1,5 @@ +using JdeScoping.Core.Interfaces; using JdeScoping.DataAccess.Exceptions; -using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using NSubstitute; using Shouldly; @@ -9,15 +9,16 @@ namespace JdeScoping.DataAccess.Tests; /// /// Unit tests for DbConnectionFactory GIW connection support. +/// Connection strings are retrieved from ISecureStoreService. /// public class DbConnectionFactoryGiwTests { - private readonly IConfiguration _configuration; + private readonly ISecureStoreService _secureStore; private readonly ILogger _logger; public DbConnectionFactoryGiwTests() { - _configuration = Substitute.For(); + _secureStore = Substitute.For(); _logger = Substitute.For>(); } @@ -25,8 +26,8 @@ public class DbConnectionFactoryGiwTests public async Task CreateGiwConnectionAsync_MissingConnectionString_ThrowsConnectionException() { // Arrange - _configuration.GetConnectionString("GIW").Returns((string?)null); - var factory = new DbConnectionFactory(_configuration, _logger); + _secureStore.Get("GIW").Returns((string?)null); + var factory = new DbConnectionFactory(_secureStore, _logger); // Act & Assert var ex = await Should.ThrowAsync( @@ -40,8 +41,8 @@ public class DbConnectionFactoryGiwTests public async Task CreateGiwConnectionAsync_EmptyConnectionString_ThrowsConnectionException() { // Arrange - _configuration.GetConnectionString("GIW").Returns(string.Empty); - var factory = new DbConnectionFactory(_configuration, _logger); + _secureStore.Get("GIW").Returns(string.Empty); + var factory = new DbConnectionFactory(_secureStore, _logger); // Act & Assert var ex = await Should.ThrowAsync( @@ -55,8 +56,8 @@ public class DbConnectionFactoryGiwTests public async Task CreateGiwConnectionAsync_InvalidConnectionString_ThrowsConnectionException() { // Arrange - _configuration.GetConnectionString("GIW").Returns("Invalid oracle connection"); - var factory = new DbConnectionFactory(_configuration, _logger); + _secureStore.Get("GIW").Returns("Invalid oracle connection"); + var factory = new DbConnectionFactory(_secureStore, _logger); // Act & Assert var ex = await Should.ThrowAsync( @@ -71,8 +72,8 @@ public class DbConnectionFactoryGiwTests public async Task CreateGiwConnectionAsync_CancellationRequested_ThrowsOperationCanceledException() { // Arrange - _configuration.GetConnectionString("GIW").Returns("User Id=test;Password=test;Data Source=test"); - var factory = new DbConnectionFactory(_configuration, _logger); + _secureStore.Get("GIW").Returns("User Id=test;Password=test;Data Source=test"); + var factory = new DbConnectionFactory(_secureStore, _logger); using var cts = new CancellationTokenSource(); cts.Cancel(); diff --git a/NEW/tests/JdeScoping.DataAccess.Tests/DbConnectionFactoryTests.cs b/NEW/tests/JdeScoping.DataAccess.Tests/DbConnectionFactoryTests.cs index 59be76e..cd1eb82 100644 --- a/NEW/tests/JdeScoping.DataAccess.Tests/DbConnectionFactoryTests.cs +++ b/NEW/tests/JdeScoping.DataAccess.Tests/DbConnectionFactoryTests.cs @@ -1,5 +1,5 @@ +using JdeScoping.Core.Interfaces; using JdeScoping.DataAccess.Exceptions; -using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using NSubstitute; using Shouldly; @@ -9,33 +9,34 @@ namespace JdeScoping.DataAccess.Tests; /// /// Unit tests for DbConnectionFactory. +/// Connection strings are retrieved from ISecureStoreService. /// public class DbConnectionFactoryTests { - private readonly IConfiguration _configuration; + private readonly ISecureStoreService _secureStore; private readonly ILogger _logger; public DbConnectionFactoryTests() { - _configuration = Substitute.For(); + _secureStore = Substitute.For(); _logger = Substitute.For>(); } #region Constructor Tests [Fact] - public void Constructor_NullConfiguration_ThrowsArgumentNullException() + public void Constructor_NullSecureStore_ThrowsArgumentNullException() { // Act & Assert Should.Throw(() => new DbConnectionFactory(null!, _logger)) - .ParamName.ShouldBe("configuration"); + .ParamName.ShouldBe("secureStore"); } [Fact] public void Constructor_NullLogger_ThrowsArgumentNullException() { // Act & Assert - Should.Throw(() => new DbConnectionFactory(_configuration, null!)) + Should.Throw(() => new DbConnectionFactory(_secureStore, null!)) .ParamName.ShouldBe("logger"); } @@ -43,7 +44,7 @@ public class DbConnectionFactoryTests public void Constructor_ValidParameters_CreatesInstance() { // Act - var factory = new DbConnectionFactory(_configuration, _logger); + var factory = new DbConnectionFactory(_secureStore, _logger); // Assert factory.ShouldNotBeNull(); @@ -57,14 +58,14 @@ public class DbConnectionFactoryTests public async Task CreateLotFinderConnectionAsync_MissingConnectionString_ThrowsConnectionException() { // Arrange - _configuration.GetConnectionString("LotFinderDB").Returns((string?)null); - var factory = new DbConnectionFactory(_configuration, _logger); + _secureStore.Get("LotFinder").Returns((string?)null); + var factory = new DbConnectionFactory(_secureStore, _logger); // Act & Assert var ex = await Should.ThrowAsync( async () => await factory.CreateLotFinderConnectionAsync()); - ex.DataSource.ShouldBe("LotFinderDB"); + ex.DataSource.ShouldBe("LotFinder"); ex.Message.ShouldContain("Connection string not found"); } @@ -72,14 +73,14 @@ public class DbConnectionFactoryTests public async Task CreateLotFinderConnectionAsync_EmptyConnectionString_ThrowsConnectionException() { // Arrange - _configuration.GetConnectionString("LotFinderDB").Returns(string.Empty); - var factory = new DbConnectionFactory(_configuration, _logger); + _secureStore.Get("LotFinder").Returns(string.Empty); + var factory = new DbConnectionFactory(_secureStore, _logger); // Act & Assert var ex = await Should.ThrowAsync( async () => await factory.CreateLotFinderConnectionAsync()); - ex.DataSource.ShouldBe("LotFinderDB"); + ex.DataSource.ShouldBe("LotFinder"); ex.Message.ShouldContain("Connection string not found"); } @@ -87,14 +88,14 @@ public class DbConnectionFactoryTests public async Task CreateLotFinderConnectionAsync_InvalidConnectionString_ThrowsConnectionException() { // Arrange - _configuration.GetConnectionString("LotFinderDB").Returns("Invalid connection string"); - var factory = new DbConnectionFactory(_configuration, _logger); + _secureStore.Get("LotFinder").Returns("Invalid connection string"); + var factory = new DbConnectionFactory(_secureStore, _logger); // Act & Assert var ex = await Should.ThrowAsync( async () => await factory.CreateLotFinderConnectionAsync()); - ex.DataSource.ShouldBe("LotFinderDB"); + ex.DataSource.ShouldBe("LotFinder"); ex.Message.ShouldContain("Failed to open connection"); ex.InnerException.ShouldNotBeNull(); } @@ -103,8 +104,8 @@ public class DbConnectionFactoryTests public async Task CreateLotFinderConnectionAsync_CancellationRequested_ThrowsOperationCanceledException() { // Arrange - _configuration.GetConnectionString("LotFinderDB").Returns("Server=test;Database=test;"); - var factory = new DbConnectionFactory(_configuration, _logger); + _secureStore.Get("LotFinder").Returns("Server=test;Database=test;"); + var factory = new DbConnectionFactory(_secureStore, _logger); using var cts = new CancellationTokenSource(); cts.Cancel(); @@ -121,8 +122,8 @@ public class DbConnectionFactoryTests public async Task CreateJdeConnectionAsync_MissingConnectionString_ThrowsConnectionException() { // Arrange - _configuration.GetConnectionString("JDE").Returns((string?)null); - var factory = new DbConnectionFactory(_configuration, _logger); + _secureStore.Get("JDE").Returns((string?)null); + var factory = new DbConnectionFactory(_secureStore, _logger); // Act & Assert var ex = await Should.ThrowAsync( @@ -136,8 +137,8 @@ public class DbConnectionFactoryTests public async Task CreateJdeConnectionAsync_InvalidConnectionString_ThrowsConnectionException() { // Arrange - _configuration.GetConnectionString("JDE").Returns("Invalid oracle connection"); - var factory = new DbConnectionFactory(_configuration, _logger); + _secureStore.Get("JDE").Returns("Invalid oracle connection"); + var factory = new DbConnectionFactory(_secureStore, _logger); // Act & Assert var ex = await Should.ThrowAsync( @@ -155,8 +156,8 @@ public class DbConnectionFactoryTests public async Task CreateJdeStageConnectionAsync_MissingConnectionString_ThrowsConnectionException() { // Arrange - _configuration.GetConnectionString("JDEStage").Returns((string?)null); - var factory = new DbConnectionFactory(_configuration, _logger); + _secureStore.Get("JDEStage").Returns((string?)null); + var factory = new DbConnectionFactory(_secureStore, _logger); // Act & Assert var ex = await Should.ThrowAsync( @@ -174,8 +175,8 @@ public class DbConnectionFactoryTests public async Task CreateCmsConnectionAsync_MissingConnectionString_ThrowsConnectionException() { // Arrange - _configuration.GetConnectionString("CMS").Returns((string?)null); - var factory = new DbConnectionFactory(_configuration, _logger); + _secureStore.Get("CMS").Returns((string?)null); + var factory = new DbConnectionFactory(_secureStore, _logger); // Act & Assert var ex = await Should.ThrowAsync( @@ -189,8 +190,8 @@ public class DbConnectionFactoryTests public async Task CreateCmsConnectionAsync_InvalidConnectionString_ThrowsConnectionException() { // Arrange - _configuration.GetConnectionString("CMS").Returns("Invalid oracle connection"); - var factory = new DbConnectionFactory(_configuration, _logger); + _secureStore.Get("CMS").Returns("Invalid oracle connection"); + var factory = new DbConnectionFactory(_secureStore, _logger); // Act & Assert var ex = await Should.ThrowAsync( @@ -208,8 +209,8 @@ public class DbConnectionFactoryTests public async Task CreateLotFinderConnectionAsync_InvalidConnection_LogsError() { // Arrange - _configuration.GetConnectionString("LotFinderDB").Returns("Invalid connection string"); - var factory = new DbConnectionFactory(_configuration, _logger); + _secureStore.Get("LotFinder").Returns("Invalid connection string"); + var factory = new DbConnectionFactory(_secureStore, _logger); // Act try diff --git a/NEW/tests/JdeScoping.DataSync.Dev.Tests/DevEtlPipelineFactoryTests.cs b/NEW/tests/JdeScoping.DataSync.Dev.Tests/DevEtlPipelineFactoryTests.cs index 4d119aa..487455d 100644 --- a/NEW/tests/JdeScoping.DataSync.Dev.Tests/DevEtlPipelineFactoryTests.cs +++ b/NEW/tests/JdeScoping.DataSync.Dev.Tests/DevEtlPipelineFactoryTests.cs @@ -1,4 +1,5 @@ using Dapper; +using JdeScoping.Core.Interfaces; using JdeScoping.DataAccess; using JdeScoping.DataAccess.Interfaces; using JdeScoping.DataSync.Dev.Configuration; @@ -33,7 +34,17 @@ public class DevEtlPipelineFactoryTests .AddEnvironmentVariables() .Build(); - _connectionFactory = new DbConnectionFactory(config, NullLogger.Instance); + // Create a mock SecureStore that returns connection strings from configuration + var secureStore = Substitute.For(); + + // Setup the mock to return connection strings from config + secureStore.Get("LotFinder").Returns(config.GetConnectionString("LotFinder")); + secureStore.Get("JDE").Returns(config.GetConnectionString("JDE")); + secureStore.Get("JDEStage").Returns(config.GetConnectionString("JDEStage")); + secureStore.Get("CMS").Returns(config.GetConnectionString("CMS")); + secureStore.Get("GIW").Returns(config.GetConnectionString("GIW")); + + _connectionFactory = new DbConnectionFactory(secureStore, NullLogger.Instance); _logger = NullLogger.Instance; _cacheDirectory = config["DevEtl:CacheDirectory"] ?? Path.Combine(Directory.GetCurrentDirectory(), "..", "..", "..", "..", "..", "CACHED_DB_FILES"); diff --git a/NEW/tests/JdeScoping.Infrastructure.Tests/Validation/ConnectionStringValidatorTests.cs b/NEW/tests/JdeScoping.Infrastructure.Tests/Validation/ConnectionStringValidatorTests.cs index 4541f42..0625208 100644 --- a/NEW/tests/JdeScoping.Infrastructure.Tests/Validation/ConnectionStringValidatorTests.cs +++ b/NEW/tests/JdeScoping.Infrastructure.Tests/Validation/ConnectionStringValidatorTests.cs @@ -6,6 +6,10 @@ using Shouldly; namespace JdeScoping.Infrastructure.Tests.Validation; +/// +/// Tests for ConnectionStringValidator which validates connection strings stored in SecureStore. +/// Connection strings are referenced by name in configuration and retrieved from SecureStore. +/// public class ConnectionStringValidatorTests : IDisposable { private readonly InMemorySecureStore _secureStore; @@ -55,7 +59,7 @@ public class ConnectionStringValidatorTests : IDisposable [Fact] public void Validate_NoConnectionStrings_ReturnsValid() { - // Arrange + // Arrange - no connection strings in config means nothing to validate var validator = CreateValidator(new Dictionary()); // Act @@ -68,12 +72,13 @@ public class ConnectionStringValidatorTests : IDisposable } [Fact] - public void Validate_ConnectionStringWithNoPlaceholders_ValidatesFormat() + public void Validate_ConnectionStringInSecureStore_ValidatesFormat() { - // Arrange + // Arrange - connection string name in config, value in SecureStore + _secureStore.Set("SqlServer", "Server=localhost;Database=TestDb;Trusted_Connection=true;"); var validator = CreateValidator(new Dictionary { - ["ConnectionStrings:SqlServer"] = "Server=localhost;Database=TestDb;Trusted_Connection=true;" + ["ConnectionStrings:SqlServer"] = "SqlServer" // Name only, value in SecureStore }); // Act @@ -85,12 +90,31 @@ public class ConnectionStringValidatorTests : IDisposable } [Fact] - public void Validate_EmptyConnectionString_ReturnsError() + public void Validate_ConnectionStringNotInSecureStore_ReturnsError() { - // Arrange + // Arrange - connection string name in config but not in SecureStore var validator = CreateValidator(new Dictionary { - ["ConnectionStrings:SqlServer"] = "" + ["ConnectionStrings:SqlServer"] = "SqlServer" + }); + // Note: Not adding SqlServer to SecureStore + + // Act + var result = validator.Validate(); + + // Assert + result.IsValid.ShouldBeFalse(); + result.Errors.ShouldContain(e => e.Contains("'SqlServer'") && e.Contains("not found in SecureStore")); + } + + [Fact] + public void Validate_EmptyConnectionStringInSecureStore_ReturnsError() + { + // Arrange - connection string exists in SecureStore but is empty + _secureStore.Set("SqlServer", ""); + var validator = CreateValidator(new Dictionary + { + ["ConnectionStrings:SqlServer"] = "SqlServer" }); // Act @@ -98,35 +122,17 @@ public class ConnectionStringValidatorTests : IDisposable // Assert result.IsValid.ShouldBeFalse(); - result.Errors.ShouldContain("Connection string 'SqlServer' is empty"); + result.Errors.ShouldContain(e => e.Contains("'SqlServer'") && e.Contains("empty in SecureStore")); } [Fact] - public void Validate_PlaceholderKeyNotInSecureStore_ReturnsError() + public void Validate_WhitespaceOnlyConnectionStringInSecureStore_ReturnsError() { - // Arrange + // Arrange - connection string exists in SecureStore but is whitespace only + _secureStore.Set("SqlServer", " "); var validator = CreateValidator(new Dictionary { - ["ConnectionStrings:SqlServer"] = "Server=localhost;Database=TestDb;Password=${DB_PASSWORD};" - }); - // Note: Not adding DB_PASSWORD to SecureStore - - // Act - var result = validator.Validate(); - - // Assert - result.IsValid.ShouldBeFalse(); - result.Errors.ShouldContain(e => e.Contains("'${DB_PASSWORD}'") && e.Contains("not found in SecureStore")); - } - - [Fact] - public void Validate_PlaceholderKeyHasEmptyValue_ReturnsError() - { - // Arrange - _secureStore.Set("DB_PASSWORD", ""); - var validator = CreateValidator(new Dictionary - { - ["ConnectionStrings:SqlServer"] = "Server=localhost;Database=TestDb;Password=${DB_PASSWORD};" + ["ConnectionStrings:SqlServer"] = "SqlServer" }); // Act @@ -134,17 +140,17 @@ public class ConnectionStringValidatorTests : IDisposable // Assert result.IsValid.ShouldBeFalse(); - result.Errors.ShouldContain(e => e.Contains("'${DB_PASSWORD}'") && e.Contains("empty value")); + result.Errors.ShouldContain(e => e.Contains("'SqlServer'") && e.Contains("empty in SecureStore")); } [Fact] - public void Validate_AllPlaceholdersResolved_ValidatesFormat() + public void Validate_ValidSqlServerFormat_ValidatesSuccessfully() { - // Arrange - _secureStore.Set("DB_PASSWORD", "secretpassword"); + // Arrange - SQL Server connection string format + _secureStore.Set("SqlServer", "Server=localhost;Database=TestDb;Trusted_Connection=true;"); var validator = CreateValidator(new Dictionary { - ["ConnectionStrings:SqlServer"] = "Server=localhost;Database=TestDb;Password=${DB_PASSWORD};" + ["ConnectionStrings:SqlServer"] = "SqlServer" }); // Act @@ -155,53 +161,14 @@ public class ConnectionStringValidatorTests : IDisposable result.Errors.ShouldBeEmpty(); } - [Fact] - public void Validate_MultiplePlaceholders_ResolvesAll() - { - // Arrange - _secureStore.Set("DB_SERVER", "prodserver.local"); - _secureStore.Set("DB_USER", "app_user"); - _secureStore.Set("DB_PASSWORD", "secretpassword"); - var validator = CreateValidator(new Dictionary - { - ["ConnectionStrings:SqlServer"] = "Server=${DB_SERVER};Database=TestDb;User Id=${DB_USER};Password=${DB_PASSWORD};" - }); - - // Act - var result = validator.Validate(); - - // Assert - result.IsValid.ShouldBeTrue(); - result.Errors.ShouldBeEmpty(); - } - - [Fact] - public void Validate_MultiplePlaceholders_ReportsAllMissing() - { - // Arrange - only set one of three required placeholders - _secureStore.Set("DB_SERVER", "prodserver.local"); - var validator = CreateValidator(new Dictionary - { - ["ConnectionStrings:SqlServer"] = "Server=${DB_SERVER};Database=TestDb;User Id=${DB_USER};Password=${DB_PASSWORD};" - }); - - // Act - var result = validator.Validate(); - - // Assert - result.IsValid.ShouldBeFalse(); - result.Errors.Count.ShouldBe(2); // DB_USER and DB_PASSWORD missing - result.Errors.ShouldContain(e => e.Contains("'${DB_USER}'")); - result.Errors.ShouldContain(e => e.Contains("'${DB_PASSWORD}'")); - } - [Fact] public void Validate_MissingServerOrDataSource_ReturnsError() { // Arrange - connection string with no Server= or Data Source= + _secureStore.Set("Invalid", "Database=TestDb;Trusted_Connection=true;"); var validator = CreateValidator(new Dictionary { - ["ConnectionStrings:Invalid"] = "Database=TestDb;Trusted_Connection=true;" + ["ConnectionStrings:Invalid"] = "Invalid" }); // Act @@ -216,9 +183,10 @@ public class ConnectionStringValidatorTests : IDisposable public void Validate_DataSourceFormat_ValidatesSuccessfully() { // Arrange - Oracle-style connection string + _secureStore.Set("Oracle", "Data Source=//localhost:1521/ORCL;User Id=test;Password=test;"); var validator = CreateValidator(new Dictionary { - ["ConnectionStrings:Oracle"] = "Data Source=//localhost:1521/ORCL;User Id=test;Password=test;" + ["ConnectionStrings:Oracle"] = "Oracle" }); // Act @@ -232,9 +200,10 @@ public class ConnectionStringValidatorTests : IDisposable public void Validate_HostFormat_ValidatesSuccessfully() { // Arrange - PostgreSQL-style connection string + _secureStore.Set("Postgres", "Host=localhost;Port=5432;Database=testdb;Username=test;Password=test;"); var validator = CreateValidator(new Dictionary { - ["ConnectionStrings:Postgres"] = "Host=localhost;Port=5432;Database=testdb;Username=test;Password=test;" + ["ConnectionStrings:Postgres"] = "Postgres" }); // Act @@ -247,13 +216,16 @@ public class ConnectionStringValidatorTests : IDisposable [Fact] public void Validate_MultipleConnectionStrings_ValidatesAll() { - // Arrange - _secureStore.Set("SQL_PASSWORD", "sqlpass"); + // Arrange - multiple connection strings, one missing from SecureStore + _secureStore.Set("SqlServer", "Server=localhost;Database=TestDb;Trusted_Connection=true;"); + _secureStore.Set("Oracle", "Data Source=//localhost:1521/ORCL;User Id=test;Password=test;"); + // Note: Not adding "Missing" to SecureStore + var validator = CreateValidator(new Dictionary { - ["ConnectionStrings:SqlServer"] = "Server=localhost;Database=TestDb;Password=${SQL_PASSWORD};", - ["ConnectionStrings:Oracle"] = "Data Source=//localhost:1521/ORCL;User Id=test;Password=test;", - ["ConnectionStrings:InvalidEmpty"] = "" + ["ConnectionStrings:SqlServer"] = "SqlServer", + ["ConnectionStrings:Oracle"] = "Oracle", + ["ConnectionStrings:Missing"] = "Missing" }); // Act @@ -261,17 +233,18 @@ public class ConnectionStringValidatorTests : IDisposable // Assert result.IsValid.ShouldBeFalse(); - result.Errors.Count.ShouldBe(1); // Only the empty one should fail - result.Errors.ShouldContain("Connection string 'InvalidEmpty' is empty"); + result.Errors.Count.ShouldBe(1); // Only the missing one should fail + result.Errors.ShouldContain(e => e.Contains("'Missing'") && e.Contains("not found in SecureStore")); } [Fact] public void Validate_UnknownConnectionFormat_AddsWarning() { // Arrange - connection string with Server= but no Database= + _secureStore.Set("Custom", "Server=localhost;CustomProperty=value;"); var validator = CreateValidator(new Dictionary { - ["ConnectionStrings:Custom"] = "Server=localhost;CustomProperty=value;" + ["ConnectionStrings:Custom"] = "Custom" }); // Act @@ -282,30 +255,14 @@ public class ConnectionStringValidatorTests : IDisposable result.Warnings.ShouldContain(w => w.Contains("'Custom'") && w.Contains("unknown format")); } - [Fact] - public void Validate_WhitespaceOnlyConnectionString_ReturnsError() - { - // Arrange - var validator = CreateValidator(new Dictionary - { - ["ConnectionStrings:SqlServer"] = " " - }); - - // Act - var result = validator.Validate(); - - // Assert - result.IsValid.ShouldBeFalse(); - result.Errors.ShouldContain("Connection string 'SqlServer' is empty"); - } - [Fact] public void Validate_CaseInsensitiveServerCheck_ValidatesSuccessfully() { // Arrange - mixed case Server + _secureStore.Set("SqlServer", "server=localhost;Database=TestDb;"); var validator = CreateValidator(new Dictionary { - ["ConnectionStrings:SqlServer"] = "server=localhost;Database=TestDb;" + ["ConnectionStrings:SqlServer"] = "SqlServer" }); // Act