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.
This commit is contained in:
@@ -31,4 +31,10 @@ public class SecureStoreOptions
|
|||||||
/// List of secret keys that must exist in the store for the application to start.
|
/// List of secret keys that must exist in the store for the application to start.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public List<string> RequiredKeys { get; set; } = [];
|
public List<string> RequiredKeys { get; set; } = [];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// List of connection string names that must exist in the store.
|
||||||
|
/// Populated automatically from the ConnectionStrings configuration section.
|
||||||
|
/// </summary>
|
||||||
|
public List<string> RequiredConnectionStrings { get; set; } = [];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
|
using JdeScoping.Core.Interfaces;
|
||||||
using JdeScoping.DataAccess.Exceptions;
|
using JdeScoping.DataAccess.Exceptions;
|
||||||
using JdeScoping.DataAccess.Interfaces;
|
using JdeScoping.DataAccess.Interfaces;
|
||||||
using Microsoft.Data.SqlClient;
|
using Microsoft.Data.SqlClient;
|
||||||
using Microsoft.Extensions.Configuration;
|
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Oracle.ManagedDataAccess.Client;
|
using Oracle.ManagedDataAccess.Client;
|
||||||
|
|
||||||
@@ -9,35 +9,29 @@ namespace JdeScoping.DataAccess;
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Factory for creating database connections to all data sources.
|
/// Factory for creating database connections to all data sources.
|
||||||
|
/// Retrieves connection strings from SecureStore.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class DbConnectionFactory : IDbConnectionFactory
|
public class DbConnectionFactory : IDbConnectionFactory
|
||||||
{
|
{
|
||||||
private readonly IConfiguration _configuration;
|
private readonly ISecureStoreService _secureStore;
|
||||||
private readonly ILogger<DbConnectionFactory> _logger;
|
private readonly ILogger<DbConnectionFactory> _logger;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="DbConnectionFactory"/> class.
|
/// Initializes a new instance of the <see cref="DbConnectionFactory"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="configuration">Application configuration.</param>
|
/// <param name="secureStore">SecureStore service for retrieving connection strings.</param>
|
||||||
/// <param name="logger">Logger instance.</param>
|
/// <param name="logger">Logger instance.</param>
|
||||||
public DbConnectionFactory(IConfiguration configuration, ILogger<DbConnectionFactory> logger)
|
public DbConnectionFactory(ISecureStoreService secureStore, ILogger<DbConnectionFactory> logger)
|
||||||
{
|
{
|
||||||
_configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
|
_secureStore = secureStore ?? throw new ArgumentNullException(nameof(secureStore));
|
||||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public async Task<SqlConnection> CreateLotFinderConnectionAsync(CancellationToken ct = default)
|
public async Task<SqlConnection> CreateLotFinderConnectionAsync(CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
const string dataSource = "LotFinderDB";
|
const string dataSource = "LotFinder";
|
||||||
var connectionString = _configuration.GetConnectionString(dataSource);
|
var connectionString = GetConnectionString(dataSource);
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(connectionString))
|
|
||||||
{
|
|
||||||
throw new ConnectionException(
|
|
||||||
$"{dataSource}: Connection string not found in configuration.",
|
|
||||||
dataSource);
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -93,17 +87,24 @@ public class DbConnectionFactory : IDbConnectionFactory
|
|||||||
return await CreateOracleConnectionAsync("GIW", ct).ConfigureAwait(false);
|
return await CreateOracleConnectionAsync("GIW", ct).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<OracleConnection> CreateOracleConnectionAsync(string dataSource, CancellationToken ct)
|
private string GetConnectionString(string dataSource)
|
||||||
{
|
{
|
||||||
var connectionString = _configuration.GetConnectionString(dataSource);
|
var connectionString = _secureStore.Get(dataSource);
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(connectionString))
|
if (string.IsNullOrEmpty(connectionString))
|
||||||
{
|
{
|
||||||
throw new ConnectionException(
|
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);
|
dataSource);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return connectionString;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<OracleConnection> CreateOracleConnectionAsync(string dataSource, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var connectionString = GetConnectionString(dataSource);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_logger.LogDebug("Creating connection to {DataSource}", dataSource);
|
_logger.LogDebug("Creating connection to {DataSource}", dataSource);
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
{
|
{
|
||||||
"ConnectionStrings": {
|
"ConnectionStrings": {
|
||||||
"LotFinder": "Server=localhost,1434;Database=ScopingTool;User Id=scopingapp;Password=Sc0ping@pp_Dev#2024;TrustServerCertificate=true",
|
"LotFinder": "",
|
||||||
"JDE": "Data Source=jde-server:1521/JDEPROD;User Id=${JDE_USER};Password=${JDE_PASSWORD}",
|
"JDE": "",
|
||||||
"CMS": "Data Source=cms-server:1521/CMSPROD;User Id=${CMS_USER};Password=${CMS_PASSWORD}",
|
"CMS": "",
|
||||||
"GIW": "Data Source=giw-server:1521/GIWPROD;User Id=${GIW_USER};Password=${GIW_PASSWORD}"
|
"GIW": ""
|
||||||
},
|
},
|
||||||
"DataAccess": {
|
"DataAccess": {
|
||||||
"CommandTimeoutSeconds": 120,
|
"CommandTimeoutSeconds": 120,
|
||||||
@@ -54,13 +54,7 @@
|
|||||||
"RequiredKeys": [
|
"RequiredKeys": [
|
||||||
"RsaPrivateKey",
|
"RsaPrivateKey",
|
||||||
"ExcelExport:CriteriaSheetPassword",
|
"ExcelExport:CriteriaSheetPassword",
|
||||||
"ExcelExport:DataSheetPassword",
|
"ExcelExport:DataSheetPassword"
|
||||||
"JdeUser",
|
|
||||||
"JdePassword",
|
|
||||||
"GiwUser",
|
|
||||||
"GiwPassword",
|
|
||||||
"CmsUser",
|
|
||||||
"CmsPassword"
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"WorkProcessor": {
|
"WorkProcessor": {
|
||||||
|
|||||||
@@ -43,8 +43,14 @@ public static class InfrastructureDependencyInjection
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Register SecureStore for encrypted secrets storage
|
// Register SecureStore for encrypted secrets storage
|
||||||
services.Configure<SecureStoreOptions>(
|
services.Configure<SecureStoreOptions>(opts =>
|
||||||
configuration.GetSection(SecureStoreOptions.SectionName));
|
{
|
||||||
|
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<ISecureStoreService, SecureStoreService>();
|
services.AddSingleton<ISecureStoreService, SecureStoreService>();
|
||||||
|
|
||||||
|
|||||||
@@ -44,6 +44,9 @@ public class SecureStoreService : ISecureStoreService
|
|||||||
_secretsManager = SecretsManager.LoadStore(_storePath);
|
_secretsManager = SecretsManager.LoadStore(_storePath);
|
||||||
_secretsManager.LoadKeyFromFile(keyFilePath);
|
_secretsManager.LoadKeyFromFile(keyFilePath);
|
||||||
LoadKeys();
|
LoadKeys();
|
||||||
|
|
||||||
|
// Ensure all required entries exist
|
||||||
|
EnsureRequiredEntries(opts);
|
||||||
}
|
}
|
||||||
else if (opts.AutoCreateStore)
|
else if (opts.AutoCreateStore)
|
||||||
{
|
{
|
||||||
@@ -59,6 +62,9 @@ public class SecureStoreService : ISecureStoreService
|
|||||||
|
|
||||||
// Save empty store
|
// Save empty store
|
||||||
_secretsManager.SaveStore(_storePath);
|
_secretsManager.SaveStore(_storePath);
|
||||||
|
|
||||||
|
// Ensure all required entries exist
|
||||||
|
EnsureRequiredEntries(opts);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -218,4 +224,31 @@ public class SecureStoreService : ISecureStoreService
|
|||||||
Directory.CreateDirectory(directory);
|
Directory.CreateDirectory(directory);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ensures all required entries exist in the store, creating empty values for missing keys.
|
||||||
|
/// </summary>
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
using System.Text.RegularExpressions;
|
|
||||||
using JdeScoping.Core.Interfaces;
|
using JdeScoping.Core.Interfaces;
|
||||||
using JdeScoping.Core.Validation;
|
using JdeScoping.Core.Validation;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
@@ -7,9 +6,9 @@ using Microsoft.Extensions.Logging;
|
|||||||
namespace JdeScoping.Infrastructure.Validation;
|
namespace JdeScoping.Infrastructure.Validation;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Validates connection strings by resolving SecureStore placeholders and verifying format.
|
/// Validates that all connection strings defined in configuration exist in SecureStore.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public partial class ConnectionStringValidator : IConfigurationValidator
|
public class ConnectionStringValidator : IConfigurationValidator
|
||||||
{
|
{
|
||||||
private readonly IConfiguration _configuration;
|
private readonly IConfiguration _configuration;
|
||||||
private readonly ISecureStoreService _secureStore;
|
private readonly ISecureStoreService _secureStore;
|
||||||
@@ -21,9 +20,6 @@ public partial class ConnectionStringValidator : IConfigurationValidator
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public string Name => "ConnectionStrings";
|
public string Name => "ConnectionStrings";
|
||||||
|
|
||||||
[GeneratedRegex(@"\$\{([^}]+)\}")]
|
|
||||||
private static partial Regex PlaceholderPattern();
|
|
||||||
|
|
||||||
public ConnectionStringValidator(
|
public ConnectionStringValidator(
|
||||||
IConfiguration configuration,
|
IConfiguration configuration,
|
||||||
ISecureStoreService secureStore,
|
ISecureStoreService secureStore,
|
||||||
@@ -48,96 +44,43 @@ public partial class ConnectionStringValidator : IConfigurationValidator
|
|||||||
return result;
|
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)
|
foreach (var connectionString in connectionStrings)
|
||||||
{
|
{
|
||||||
ValidateConnectionString(connectionString.Key, connectionString.Value, result);
|
ValidateConnectionString(connectionString.Key, result);
|
||||||
}
|
}
|
||||||
|
|
||||||
return 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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_logger.LogDebug("Validating connection string: {Name}", name);
|
// Get the value from SecureStore
|
||||||
|
var connectionStringValue = _secureStore.Get(name);
|
||||||
|
|
||||||
// Extract and validate all placeholders
|
if (string.IsNullOrWhiteSpace(connectionStringValue))
|
||||||
var placeholders = ExtractPlaceholders(connectionString);
|
|
||||||
var resolvedConnectionString = connectionString;
|
|
||||||
var hasPlaceholderErrors = false;
|
|
||||||
|
|
||||||
foreach (var placeholder in placeholders)
|
|
||||||
{
|
{
|
||||||
var resolvedValue = ResolvePlaceholder(placeholder, name, result);
|
result.AddError($"Connection string '{name}' is empty in SecureStore. Use ConfigManager to set the connection string value.");
|
||||||
if (resolvedValue is null)
|
return;
|
||||||
{
|
|
||||||
hasPlaceholderErrors = true;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Replace placeholder with resolved value for format validation
|
|
||||||
resolvedConnectionString = resolvedConnectionString.Replace($"${{{placeholder}}}", resolvedValue);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only validate format if all placeholders were resolved
|
// Validate format
|
||||||
if (!hasPlaceholderErrors)
|
ValidateConnectionStringFormat(name, connectionStringValue, result);
|
||||||
{
|
|
||||||
ValidateConnectionStringFormat(name, resolvedConnectionString, result);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static List<string> ExtractPlaceholders(string connectionString)
|
|
||||||
{
|
|
||||||
var placeholders = new List<string>();
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ValidateConnectionStringFormat(string name, string connectionString, ConfigurationValidationResult result)
|
private void ValidateConnectionStringFormat(string name, string connectionString, ConfigurationValidationResult result)
|
||||||
{
|
{
|
||||||
// Check for required connection string components
|
// Check for required connection string components
|
||||||
// Support both "Server=" (SQL Server) and "Data Source=" (Oracle/generic)
|
|
||||||
var hasServer = connectionString.Contains("Server=", StringComparison.OrdinalIgnoreCase) ||
|
var hasServer = connectionString.Contains("Server=", StringComparison.OrdinalIgnoreCase) ||
|
||||||
connectionString.Contains("Data Source=", StringComparison.OrdinalIgnoreCase) ||
|
connectionString.Contains("Data Source=", StringComparison.OrdinalIgnoreCase) ||
|
||||||
connectionString.Contains("Host=", StringComparison.OrdinalIgnoreCase);
|
connectionString.Contains("Host=", StringComparison.OrdinalIgnoreCase);
|
||||||
|
|||||||
@@ -71,11 +71,15 @@ public interface ISecureStoreManager
|
|||||||
void RemoveSecret(string key);
|
void RemoveSecret(string key);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 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.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="requiredKeys">List of keys that must exist.</param>
|
/// <param name="requiredKeys">List of general secret keys that must exist.</param>
|
||||||
|
/// <param name="connectionStringNames">List of connection string names that must exist.</param>
|
||||||
/// <returns>List of keys that were added.</returns>
|
/// <returns>List of keys that were added.</returns>
|
||||||
IReadOnlyList<string> EnsureRequiredKeys(IEnumerable<string> requiredKeys);
|
IReadOnlyList<string> EnsureAllRequiredEntries(
|
||||||
|
IEnumerable<string> requiredKeys,
|
||||||
|
IEnumerable<string> connectionStringNames);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Generates a new key file for use with store encryption.
|
/// Generates a new key file for use with store encryption.
|
||||||
|
|||||||
@@ -193,7 +193,9 @@ public class SecureStoreManager : ISecureStoreManager, IDisposable
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public IReadOnlyList<string> EnsureRequiredKeys(IEnumerable<string> requiredKeys)
|
public IReadOnlyList<string> EnsureAllRequiredEntries(
|
||||||
|
IEnumerable<string> requiredKeys,
|
||||||
|
IEnumerable<string> connectionStringNames)
|
||||||
{
|
{
|
||||||
ThrowIfDisposed();
|
ThrowIfDisposed();
|
||||||
|
|
||||||
@@ -201,11 +203,15 @@ public class SecureStoreManager : ISecureStoreManager, IDisposable
|
|||||||
throw new InvalidOperationException("No store is currently open.");
|
throw new InvalidOperationException("No store is currently open.");
|
||||||
|
|
||||||
var addedKeys = new List<string>();
|
var addedKeys = new List<string>();
|
||||||
foreach (var key in requiredKeys)
|
var allRequired = requiredKeys
|
||||||
|
.Concat(connectionStringNames)
|
||||||
|
.Distinct();
|
||||||
|
|
||||||
|
foreach (var key in allRequired)
|
||||||
{
|
{
|
||||||
if (!_keys.Contains(key))
|
if (!_keys.Contains(key))
|
||||||
{
|
{
|
||||||
_logger.LogInformation("Adding missing required key: {Key}", key);
|
_logger.LogInformation("Adding missing required entry: {Key}", key);
|
||||||
SetSecret(key, string.Empty);
|
SetSecret(key, string.Empty);
|
||||||
addedKeys.Add(key);
|
addedKeys.Add(key);
|
||||||
}
|
}
|
||||||
|
|||||||
+71
-8
@@ -2,16 +2,18 @@ using System.Collections.ObjectModel;
|
|||||||
using System.Windows.Input;
|
using System.Windows.Input;
|
||||||
using JdeScoping.ConfigManager.Models;
|
using JdeScoping.ConfigManager.Models;
|
||||||
using JdeScoping.ConfigManager.Services;
|
using JdeScoping.ConfigManager.Services;
|
||||||
|
using JdeScoping.ConfigManager.Services.SecureStore;
|
||||||
|
|
||||||
namespace JdeScoping.ConfigManager.ViewModels.Forms;
|
namespace JdeScoping.ConfigManager.ViewModels.Forms;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ViewModel for editing the ConnectionStrings configuration section.
|
/// ViewModel for editing connection strings stored in SecureStore.
|
||||||
/// Manages a collection of connection string entries with add, delete, validate, and test commands.
|
/// Connection string names come from configuration, values come from SecureStore.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class ConnectionStringsFormViewModel : ViewModelBase
|
public class ConnectionStringsFormViewModel : ViewModelBase
|
||||||
{
|
{
|
||||||
private readonly ConnectionStringsSection _model;
|
private readonly ConnectionStringsSection _model;
|
||||||
|
private readonly ISecureStoreManager _secureStoreManager;
|
||||||
private readonly Action _onChanged;
|
private readonly Action _onChanged;
|
||||||
private readonly IDialogService _dialogService;
|
private readonly IDialogService _dialogService;
|
||||||
private readonly IConnectionTestService _connectionTestService;
|
private readonly IConnectionTestService _connectionTestService;
|
||||||
@@ -21,27 +23,42 @@ public class ConnectionStringsFormViewModel : ViewModelBase
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="ConnectionStringsFormViewModel"/> class.
|
/// Initializes a new instance of the <see cref="ConnectionStringsFormViewModel"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="model">The connection strings section model.</param>
|
/// <param name="model">The connection strings section model (provides names).</param>
|
||||||
|
/// <param name="secureStoreManager">The SecureStore manager for reading/writing values.</param>
|
||||||
/// <param name="onChanged">The action to invoke when any property changes.</param>
|
/// <param name="onChanged">The action to invoke when any property changes.</param>
|
||||||
/// <param name="dialogService">The dialog service for showing messages and confirmations.</param>
|
/// <param name="dialogService">The dialog service for showing messages and confirmations.</param>
|
||||||
/// <param name="connectionTestService">The service for testing database connections.</param>
|
/// <param name="connectionTestService">The service for testing database connections.</param>
|
||||||
public ConnectionStringsFormViewModel(
|
public ConnectionStringsFormViewModel(
|
||||||
ConnectionStringsSection model,
|
ConnectionStringsSection model,
|
||||||
|
ISecureStoreManager secureStoreManager,
|
||||||
Action onChanged,
|
Action onChanged,
|
||||||
IDialogService dialogService,
|
IDialogService dialogService,
|
||||||
IConnectionTestService connectionTestService)
|
IConnectionTestService connectionTestService)
|
||||||
{
|
{
|
||||||
_model = model ?? throw new ArgumentNullException(nameof(model));
|
_model = model ?? throw new ArgumentNullException(nameof(model));
|
||||||
|
_secureStoreManager = secureStoreManager ?? throw new ArgumentNullException(nameof(secureStoreManager));
|
||||||
_onChanged = onChanged ?? throw new ArgumentNullException(nameof(onChanged));
|
_onChanged = onChanged ?? throw new ArgumentNullException(nameof(onChanged));
|
||||||
_dialogService = dialogService ?? throw new ArgumentNullException(nameof(dialogService));
|
_dialogService = dialogService ?? throw new ArgumentNullException(nameof(dialogService));
|
||||||
_connectionTestService = connectionTestService ?? throw new ArgumentNullException(nameof(connectionTestService));
|
_connectionTestService = connectionTestService ?? throw new ArgumentNullException(nameof(connectionTestService));
|
||||||
|
|
||||||
Connections = new ObservableCollection<ConnectionStringEntryViewModel>();
|
Connections = new ObservableCollection<ConnectionStringEntryViewModel>();
|
||||||
|
|
||||||
// Initialize view models from model entries
|
// Initialize view models from model entries, loading values from SecureStore
|
||||||
foreach (var entry in _model.Entries)
|
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
|
// Initialize commands
|
||||||
@@ -51,6 +68,32 @@ public class ConnectionStringsFormViewModel : ViewModelBase
|
|||||||
TestConnectionCommand = new AsyncRelayCommand(TestConnectionAsync, () => HasSelection && !IsTesting);
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the collection of connection string entry view models.
|
/// Gets the collection of connection string entry view models.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -136,12 +179,19 @@ public class ConnectionStringsFormViewModel : ViewModelBase
|
|||||||
{
|
{
|
||||||
var entry = new ConnectionStringEntry
|
var entry = new ConnectionStringEntry
|
||||||
{
|
{
|
||||||
Name = "NewConnection"
|
Name = "NewConnection",
|
||||||
|
Provider = ConnectionProvider.Generic
|
||||||
};
|
};
|
||||||
|
|
||||||
_model.Entries.Add(entry);
|
_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);
|
Connections.Add(viewModel);
|
||||||
|
|
||||||
SelectedConnection = viewModel;
|
SelectedConnection = viewModel;
|
||||||
@@ -160,11 +210,24 @@ public class ConnectionStringsFormViewModel : ViewModelBase
|
|||||||
var name = SelectedConnection.Name;
|
var name = SelectedConnection.Name;
|
||||||
var confirmed = await _dialogService.ShowConfirmationAsync(
|
var confirmed = await _dialogService.ShowConfirmationAsync(
|
||||||
"Delete Connection",
|
"Delete Connection",
|
||||||
$"Delete connection '{name}'?");
|
$"Delete connection '{name}'? This will also remove the SecureStore entry.");
|
||||||
|
|
||||||
if (!confirmed)
|
if (!confirmed)
|
||||||
return;
|
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
|
// Find the model entry to remove
|
||||||
var modelEntry = _model.Entries.FirstOrDefault(e => e.Name == name);
|
var modelEntry = _model.Entries.FirstOrDefault(e => e.Name == name);
|
||||||
if (modelEntry != null)
|
if (modelEntry != null)
|
||||||
|
|||||||
@@ -346,14 +346,18 @@ public class MainWindowViewModel : ViewModelBase
|
|||||||
_secureStoreManager.OpenStore(storePath, keyFilePath);
|
_secureStoreManager.OpenStore(storePath, keyFilePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure all required keys exist
|
// Ensure all required entries exist (both RequiredKeys and connection strings)
|
||||||
if (secureStoreConfig.RequiredKeys?.Count > 0)
|
var connectionStringNames = _appSettings?.ConnectionStrings?.Entries
|
||||||
|
.Select(e => e.Name)
|
||||||
|
.Where(n => !string.IsNullOrEmpty(n))
|
||||||
|
?? Enumerable.Empty<string>();
|
||||||
|
|
||||||
|
var requiredKeys = secureStoreConfig.RequiredKeys ?? new List<string>();
|
||||||
|
|
||||||
|
var addedKeys = _secureStoreManager.EnsureAllRequiredEntries(requiredKeys, connectionStringNames);
|
||||||
|
if (addedKeys.Count > 0)
|
||||||
{
|
{
|
||||||
var addedKeys = _secureStoreManager.EnsureRequiredKeys(secureStoreConfig.RequiredKeys);
|
_logger?.LogInformation("Added {Count} missing required SecureStore entries", addedKeys.Count);
|
||||||
if (addedKeys.Count > 0)
|
|
||||||
{
|
|
||||||
_logger?.LogInformation("Added {Count} missing required keys", addedKeys.Count);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -575,6 +579,7 @@ public class MainWindowViewModel : ViewModelBase
|
|||||||
"ExcelExport" => new ExcelExportFormViewModel(_appSettings.ExcelExport, MarkAsChanged),
|
"ExcelExport" => new ExcelExportFormViewModel(_appSettings.ExcelExport, MarkAsChanged),
|
||||||
"ConnectionStrings" when _dialogService != null => new ConnectionStringsFormViewModel(
|
"ConnectionStrings" when _dialogService != null => new ConnectionStringsFormViewModel(
|
||||||
_appSettings.ConnectionStrings,
|
_appSettings.ConnectionStrings,
|
||||||
|
_secureStoreManager,
|
||||||
MarkAsChanged,
|
MarkAsChanged,
|
||||||
_dialogService,
|
_dialogService,
|
||||||
_connectionTestService),
|
_connectionTestService),
|
||||||
|
|||||||
+135
@@ -294,4 +294,139 @@ public class SecureStoreManagerTests : IDisposable
|
|||||||
// Act & Assert
|
// Act & Assert
|
||||||
Should.Throw<InvalidOperationException>(() => _sut.GetSecret("key"));
|
Should.Throw<InvalidOperationException>(() => _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<string>());
|
||||||
|
|
||||||
|
// 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<string>());
|
||||||
|
|
||||||
|
// 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<string>());
|
||||||
|
|
||||||
|
// 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<InvalidOperationException>(() => _sut.EnsureAllRequiredEntries(
|
||||||
|
new[] { "key" },
|
||||||
|
Array.Empty<string>()));
|
||||||
|
}
|
||||||
|
|
||||||
|
[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");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+25
-8
@@ -1,18 +1,24 @@
|
|||||||
using JdeScoping.ConfigManager.Models;
|
using JdeScoping.ConfigManager.Models;
|
||||||
using JdeScoping.ConfigManager.Services;
|
using JdeScoping.ConfigManager.Services;
|
||||||
|
using JdeScoping.ConfigManager.Services.SecureStore;
|
||||||
using JdeScoping.ConfigManager.ViewModels.Forms;
|
using JdeScoping.ConfigManager.ViewModels.Forms;
|
||||||
|
|
||||||
namespace JdeScoping.ConfigManager.Tests.ViewModels.Forms;
|
namespace JdeScoping.ConfigManager.Tests.ViewModels.Forms;
|
||||||
|
|
||||||
public class ConnectionStringsFormViewModelTests
|
public class ConnectionStringsFormViewModelTests
|
||||||
{
|
{
|
||||||
|
private readonly ISecureStoreManager _secureStoreManager;
|
||||||
private readonly IDialogService _dialogService;
|
private readonly IDialogService _dialogService;
|
||||||
private readonly IConnectionTestService _connectionTestService;
|
private readonly IConnectionTestService _connectionTestService;
|
||||||
|
|
||||||
public ConnectionStringsFormViewModelTests()
|
public ConnectionStringsFormViewModelTests()
|
||||||
{
|
{
|
||||||
|
_secureStoreManager = Substitute.For<ISecureStoreManager>();
|
||||||
_dialogService = Substitute.For<IDialogService>();
|
_dialogService = Substitute.For<IDialogService>();
|
||||||
_connectionTestService = Substitute.For<IConnectionTestService>();
|
_connectionTestService = Substitute.For<IConnectionTestService>();
|
||||||
|
|
||||||
|
// Setup default behavior - SecureStore is not open by default in tests
|
||||||
|
_secureStoreManager.IsStoreOpen.Returns(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -39,7 +45,7 @@ public class ConnectionStringsFormViewModelTests
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var sut = new ConnectionStringsFormViewModel(model, () => { }, _dialogService, _connectionTestService);
|
var sut = new ConnectionStringsFormViewModel(model, _secureStoreManager, () => { }, _dialogService, _connectionTestService);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
sut.Connections.Count.ShouldBe(2);
|
sut.Connections.Count.ShouldBe(2);
|
||||||
@@ -56,7 +62,18 @@ public class ConnectionStringsFormViewModelTests
|
|||||||
{
|
{
|
||||||
// Act & Assert
|
// Act & Assert
|
||||||
Should.Throw<ArgumentNullException>(() =>
|
Should.Throw<ArgumentNullException>(() =>
|
||||||
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<ArgumentNullException>(() =>
|
||||||
|
new ConnectionStringsFormViewModel(model, null!, () => { }, _dialogService, _connectionTestService));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -67,7 +84,7 @@ public class ConnectionStringsFormViewModelTests
|
|||||||
|
|
||||||
// Act & Assert
|
// Act & Assert
|
||||||
Should.Throw<ArgumentNullException>(() =>
|
Should.Throw<ArgumentNullException>(() =>
|
||||||
new ConnectionStringsFormViewModel(model, null!, _dialogService, _connectionTestService));
|
new ConnectionStringsFormViewModel(model, _secureStoreManager, null!, _dialogService, _connectionTestService));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -78,7 +95,7 @@ public class ConnectionStringsFormViewModelTests
|
|||||||
|
|
||||||
// Act & Assert
|
// Act & Assert
|
||||||
Should.Throw<ArgumentNullException>(() =>
|
Should.Throw<ArgumentNullException>(() =>
|
||||||
new ConnectionStringsFormViewModel(model, () => { }, _dialogService, null!));
|
new ConnectionStringsFormViewModel(model, _secureStoreManager, () => { }, _dialogService, null!));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -87,7 +104,7 @@ public class ConnectionStringsFormViewModelTests
|
|||||||
// Arrange
|
// Arrange
|
||||||
var model = new ConnectionStringsSection();
|
var model = new ConnectionStringsSection();
|
||||||
var changedInvoked = false;
|
var changedInvoked = false;
|
||||||
var sut = new ConnectionStringsFormViewModel(model, () => changedInvoked = true, _dialogService, _connectionTestService);
|
var sut = new ConnectionStringsFormViewModel(model, _secureStoreManager, () => changedInvoked = true, _dialogService, _connectionTestService);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
sut.AddConnectionCommand.Execute(null);
|
sut.AddConnectionCommand.Execute(null);
|
||||||
@@ -111,7 +128,7 @@ public class ConnectionStringsFormViewModelTests
|
|||||||
new ConnectionStringEntry { Name = "Conn1" }
|
new ConnectionStringEntry { Name = "Conn1" }
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
var sut = new ConnectionStringsFormViewModel(model, () => { }, _dialogService, _connectionTestService);
|
var sut = new ConnectionStringsFormViewModel(model, _secureStoreManager, () => { }, _dialogService, _connectionTestService);
|
||||||
|
|
||||||
// Assert - no selection by default
|
// Assert - no selection by default
|
||||||
sut.SelectedConnection.ShouldBeNull();
|
sut.SelectedConnection.ShouldBeNull();
|
||||||
@@ -129,7 +146,7 @@ public class ConnectionStringsFormViewModelTests
|
|||||||
new ConnectionStringEntry { Name = "Conn1" }
|
new ConnectionStringEntry { Name = "Conn1" }
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
var sut = new ConnectionStringsFormViewModel(model, () => { }, _dialogService, _connectionTestService);
|
var sut = new ConnectionStringsFormViewModel(model, _secureStoreManager, () => { }, _dialogService, _connectionTestService);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
sut.SelectedConnection = sut.Connections[0];
|
sut.SelectedConnection = sut.Connections[0];
|
||||||
@@ -153,7 +170,7 @@ public class ConnectionStringsFormViewModelTests
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var sut = new ConnectionStringsFormViewModel(model, () => { }, _dialogService, _connectionTestService);
|
var sut = new ConnectionStringsFormViewModel(model, _secureStoreManager, () => { }, _dialogService, _connectionTestService);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
sut.ConnectionCount.ShouldBe(3);
|
sut.ConnectionCount.ShouldBe(3);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
|
using JdeScoping.Core.Interfaces;
|
||||||
using JdeScoping.DataAccess.Exceptions;
|
using JdeScoping.DataAccess.Exceptions;
|
||||||
using Microsoft.Extensions.Configuration;
|
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using NSubstitute;
|
using NSubstitute;
|
||||||
using Shouldly;
|
using Shouldly;
|
||||||
@@ -9,15 +9,16 @@ namespace JdeScoping.DataAccess.Tests;
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Unit tests for DbConnectionFactory GIW connection support.
|
/// Unit tests for DbConnectionFactory GIW connection support.
|
||||||
|
/// Connection strings are retrieved from ISecureStoreService.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class DbConnectionFactoryGiwTests
|
public class DbConnectionFactoryGiwTests
|
||||||
{
|
{
|
||||||
private readonly IConfiguration _configuration;
|
private readonly ISecureStoreService _secureStore;
|
||||||
private readonly ILogger<DbConnectionFactory> _logger;
|
private readonly ILogger<DbConnectionFactory> _logger;
|
||||||
|
|
||||||
public DbConnectionFactoryGiwTests()
|
public DbConnectionFactoryGiwTests()
|
||||||
{
|
{
|
||||||
_configuration = Substitute.For<IConfiguration>();
|
_secureStore = Substitute.For<ISecureStoreService>();
|
||||||
_logger = Substitute.For<ILogger<DbConnectionFactory>>();
|
_logger = Substitute.For<ILogger<DbConnectionFactory>>();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -25,8 +26,8 @@ public class DbConnectionFactoryGiwTests
|
|||||||
public async Task CreateGiwConnectionAsync_MissingConnectionString_ThrowsConnectionException()
|
public async Task CreateGiwConnectionAsync_MissingConnectionString_ThrowsConnectionException()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
_configuration.GetConnectionString("GIW").Returns((string?)null);
|
_secureStore.Get("GIW").Returns((string?)null);
|
||||||
var factory = new DbConnectionFactory(_configuration, _logger);
|
var factory = new DbConnectionFactory(_secureStore, _logger);
|
||||||
|
|
||||||
// Act & Assert
|
// Act & Assert
|
||||||
var ex = await Should.ThrowAsync<ConnectionException>(
|
var ex = await Should.ThrowAsync<ConnectionException>(
|
||||||
@@ -40,8 +41,8 @@ public class DbConnectionFactoryGiwTests
|
|||||||
public async Task CreateGiwConnectionAsync_EmptyConnectionString_ThrowsConnectionException()
|
public async Task CreateGiwConnectionAsync_EmptyConnectionString_ThrowsConnectionException()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
_configuration.GetConnectionString("GIW").Returns(string.Empty);
|
_secureStore.Get("GIW").Returns(string.Empty);
|
||||||
var factory = new DbConnectionFactory(_configuration, _logger);
|
var factory = new DbConnectionFactory(_secureStore, _logger);
|
||||||
|
|
||||||
// Act & Assert
|
// Act & Assert
|
||||||
var ex = await Should.ThrowAsync<ConnectionException>(
|
var ex = await Should.ThrowAsync<ConnectionException>(
|
||||||
@@ -55,8 +56,8 @@ public class DbConnectionFactoryGiwTests
|
|||||||
public async Task CreateGiwConnectionAsync_InvalidConnectionString_ThrowsConnectionException()
|
public async Task CreateGiwConnectionAsync_InvalidConnectionString_ThrowsConnectionException()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
_configuration.GetConnectionString("GIW").Returns("Invalid oracle connection");
|
_secureStore.Get("GIW").Returns("Invalid oracle connection");
|
||||||
var factory = new DbConnectionFactory(_configuration, _logger);
|
var factory = new DbConnectionFactory(_secureStore, _logger);
|
||||||
|
|
||||||
// Act & Assert
|
// Act & Assert
|
||||||
var ex = await Should.ThrowAsync<ConnectionException>(
|
var ex = await Should.ThrowAsync<ConnectionException>(
|
||||||
@@ -71,8 +72,8 @@ public class DbConnectionFactoryGiwTests
|
|||||||
public async Task CreateGiwConnectionAsync_CancellationRequested_ThrowsOperationCanceledException()
|
public async Task CreateGiwConnectionAsync_CancellationRequested_ThrowsOperationCanceledException()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
_configuration.GetConnectionString("GIW").Returns("User Id=test;Password=test;Data Source=test");
|
_secureStore.Get("GIW").Returns("User Id=test;Password=test;Data Source=test");
|
||||||
var factory = new DbConnectionFactory(_configuration, _logger);
|
var factory = new DbConnectionFactory(_secureStore, _logger);
|
||||||
using var cts = new CancellationTokenSource();
|
using var cts = new CancellationTokenSource();
|
||||||
cts.Cancel();
|
cts.Cancel();
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
|
using JdeScoping.Core.Interfaces;
|
||||||
using JdeScoping.DataAccess.Exceptions;
|
using JdeScoping.DataAccess.Exceptions;
|
||||||
using Microsoft.Extensions.Configuration;
|
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using NSubstitute;
|
using NSubstitute;
|
||||||
using Shouldly;
|
using Shouldly;
|
||||||
@@ -9,33 +9,34 @@ namespace JdeScoping.DataAccess.Tests;
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Unit tests for DbConnectionFactory.
|
/// Unit tests for DbConnectionFactory.
|
||||||
|
/// Connection strings are retrieved from ISecureStoreService.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class DbConnectionFactoryTests
|
public class DbConnectionFactoryTests
|
||||||
{
|
{
|
||||||
private readonly IConfiguration _configuration;
|
private readonly ISecureStoreService _secureStore;
|
||||||
private readonly ILogger<DbConnectionFactory> _logger;
|
private readonly ILogger<DbConnectionFactory> _logger;
|
||||||
|
|
||||||
public DbConnectionFactoryTests()
|
public DbConnectionFactoryTests()
|
||||||
{
|
{
|
||||||
_configuration = Substitute.For<IConfiguration>();
|
_secureStore = Substitute.For<ISecureStoreService>();
|
||||||
_logger = Substitute.For<ILogger<DbConnectionFactory>>();
|
_logger = Substitute.For<ILogger<DbConnectionFactory>>();
|
||||||
}
|
}
|
||||||
|
|
||||||
#region Constructor Tests
|
#region Constructor Tests
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Constructor_NullConfiguration_ThrowsArgumentNullException()
|
public void Constructor_NullSecureStore_ThrowsArgumentNullException()
|
||||||
{
|
{
|
||||||
// Act & Assert
|
// Act & Assert
|
||||||
Should.Throw<ArgumentNullException>(() => new DbConnectionFactory(null!, _logger))
|
Should.Throw<ArgumentNullException>(() => new DbConnectionFactory(null!, _logger))
|
||||||
.ParamName.ShouldBe("configuration");
|
.ParamName.ShouldBe("secureStore");
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Constructor_NullLogger_ThrowsArgumentNullException()
|
public void Constructor_NullLogger_ThrowsArgumentNullException()
|
||||||
{
|
{
|
||||||
// Act & Assert
|
// Act & Assert
|
||||||
Should.Throw<ArgumentNullException>(() => new DbConnectionFactory(_configuration, null!))
|
Should.Throw<ArgumentNullException>(() => new DbConnectionFactory(_secureStore, null!))
|
||||||
.ParamName.ShouldBe("logger");
|
.ParamName.ShouldBe("logger");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,7 +44,7 @@ public class DbConnectionFactoryTests
|
|||||||
public void Constructor_ValidParameters_CreatesInstance()
|
public void Constructor_ValidParameters_CreatesInstance()
|
||||||
{
|
{
|
||||||
// Act
|
// Act
|
||||||
var factory = new DbConnectionFactory(_configuration, _logger);
|
var factory = new DbConnectionFactory(_secureStore, _logger);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
factory.ShouldNotBeNull();
|
factory.ShouldNotBeNull();
|
||||||
@@ -57,14 +58,14 @@ public class DbConnectionFactoryTests
|
|||||||
public async Task CreateLotFinderConnectionAsync_MissingConnectionString_ThrowsConnectionException()
|
public async Task CreateLotFinderConnectionAsync_MissingConnectionString_ThrowsConnectionException()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
_configuration.GetConnectionString("LotFinderDB").Returns((string?)null);
|
_secureStore.Get("LotFinder").Returns((string?)null);
|
||||||
var factory = new DbConnectionFactory(_configuration, _logger);
|
var factory = new DbConnectionFactory(_secureStore, _logger);
|
||||||
|
|
||||||
// Act & Assert
|
// Act & Assert
|
||||||
var ex = await Should.ThrowAsync<ConnectionException>(
|
var ex = await Should.ThrowAsync<ConnectionException>(
|
||||||
async () => await factory.CreateLotFinderConnectionAsync());
|
async () => await factory.CreateLotFinderConnectionAsync());
|
||||||
|
|
||||||
ex.DataSource.ShouldBe("LotFinderDB");
|
ex.DataSource.ShouldBe("LotFinder");
|
||||||
ex.Message.ShouldContain("Connection string not found");
|
ex.Message.ShouldContain("Connection string not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,14 +73,14 @@ public class DbConnectionFactoryTests
|
|||||||
public async Task CreateLotFinderConnectionAsync_EmptyConnectionString_ThrowsConnectionException()
|
public async Task CreateLotFinderConnectionAsync_EmptyConnectionString_ThrowsConnectionException()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
_configuration.GetConnectionString("LotFinderDB").Returns(string.Empty);
|
_secureStore.Get("LotFinder").Returns(string.Empty);
|
||||||
var factory = new DbConnectionFactory(_configuration, _logger);
|
var factory = new DbConnectionFactory(_secureStore, _logger);
|
||||||
|
|
||||||
// Act & Assert
|
// Act & Assert
|
||||||
var ex = await Should.ThrowAsync<ConnectionException>(
|
var ex = await Should.ThrowAsync<ConnectionException>(
|
||||||
async () => await factory.CreateLotFinderConnectionAsync());
|
async () => await factory.CreateLotFinderConnectionAsync());
|
||||||
|
|
||||||
ex.DataSource.ShouldBe("LotFinderDB");
|
ex.DataSource.ShouldBe("LotFinder");
|
||||||
ex.Message.ShouldContain("Connection string not found");
|
ex.Message.ShouldContain("Connection string not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,14 +88,14 @@ public class DbConnectionFactoryTests
|
|||||||
public async Task CreateLotFinderConnectionAsync_InvalidConnectionString_ThrowsConnectionException()
|
public async Task CreateLotFinderConnectionAsync_InvalidConnectionString_ThrowsConnectionException()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
_configuration.GetConnectionString("LotFinderDB").Returns("Invalid connection string");
|
_secureStore.Get("LotFinder").Returns("Invalid connection string");
|
||||||
var factory = new DbConnectionFactory(_configuration, _logger);
|
var factory = new DbConnectionFactory(_secureStore, _logger);
|
||||||
|
|
||||||
// Act & Assert
|
// Act & Assert
|
||||||
var ex = await Should.ThrowAsync<ConnectionException>(
|
var ex = await Should.ThrowAsync<ConnectionException>(
|
||||||
async () => await factory.CreateLotFinderConnectionAsync());
|
async () => await factory.CreateLotFinderConnectionAsync());
|
||||||
|
|
||||||
ex.DataSource.ShouldBe("LotFinderDB");
|
ex.DataSource.ShouldBe("LotFinder");
|
||||||
ex.Message.ShouldContain("Failed to open connection");
|
ex.Message.ShouldContain("Failed to open connection");
|
||||||
ex.InnerException.ShouldNotBeNull();
|
ex.InnerException.ShouldNotBeNull();
|
||||||
}
|
}
|
||||||
@@ -103,8 +104,8 @@ public class DbConnectionFactoryTests
|
|||||||
public async Task CreateLotFinderConnectionAsync_CancellationRequested_ThrowsOperationCanceledException()
|
public async Task CreateLotFinderConnectionAsync_CancellationRequested_ThrowsOperationCanceledException()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
_configuration.GetConnectionString("LotFinderDB").Returns("Server=test;Database=test;");
|
_secureStore.Get("LotFinder").Returns("Server=test;Database=test;");
|
||||||
var factory = new DbConnectionFactory(_configuration, _logger);
|
var factory = new DbConnectionFactory(_secureStore, _logger);
|
||||||
using var cts = new CancellationTokenSource();
|
using var cts = new CancellationTokenSource();
|
||||||
cts.Cancel();
|
cts.Cancel();
|
||||||
|
|
||||||
@@ -121,8 +122,8 @@ public class DbConnectionFactoryTests
|
|||||||
public async Task CreateJdeConnectionAsync_MissingConnectionString_ThrowsConnectionException()
|
public async Task CreateJdeConnectionAsync_MissingConnectionString_ThrowsConnectionException()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
_configuration.GetConnectionString("JDE").Returns((string?)null);
|
_secureStore.Get("JDE").Returns((string?)null);
|
||||||
var factory = new DbConnectionFactory(_configuration, _logger);
|
var factory = new DbConnectionFactory(_secureStore, _logger);
|
||||||
|
|
||||||
// Act & Assert
|
// Act & Assert
|
||||||
var ex = await Should.ThrowAsync<ConnectionException>(
|
var ex = await Should.ThrowAsync<ConnectionException>(
|
||||||
@@ -136,8 +137,8 @@ public class DbConnectionFactoryTests
|
|||||||
public async Task CreateJdeConnectionAsync_InvalidConnectionString_ThrowsConnectionException()
|
public async Task CreateJdeConnectionAsync_InvalidConnectionString_ThrowsConnectionException()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
_configuration.GetConnectionString("JDE").Returns("Invalid oracle connection");
|
_secureStore.Get("JDE").Returns("Invalid oracle connection");
|
||||||
var factory = new DbConnectionFactory(_configuration, _logger);
|
var factory = new DbConnectionFactory(_secureStore, _logger);
|
||||||
|
|
||||||
// Act & Assert
|
// Act & Assert
|
||||||
var ex = await Should.ThrowAsync<ConnectionException>(
|
var ex = await Should.ThrowAsync<ConnectionException>(
|
||||||
@@ -155,8 +156,8 @@ public class DbConnectionFactoryTests
|
|||||||
public async Task CreateJdeStageConnectionAsync_MissingConnectionString_ThrowsConnectionException()
|
public async Task CreateJdeStageConnectionAsync_MissingConnectionString_ThrowsConnectionException()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
_configuration.GetConnectionString("JDEStage").Returns((string?)null);
|
_secureStore.Get("JDEStage").Returns((string?)null);
|
||||||
var factory = new DbConnectionFactory(_configuration, _logger);
|
var factory = new DbConnectionFactory(_secureStore, _logger);
|
||||||
|
|
||||||
// Act & Assert
|
// Act & Assert
|
||||||
var ex = await Should.ThrowAsync<ConnectionException>(
|
var ex = await Should.ThrowAsync<ConnectionException>(
|
||||||
@@ -174,8 +175,8 @@ public class DbConnectionFactoryTests
|
|||||||
public async Task CreateCmsConnectionAsync_MissingConnectionString_ThrowsConnectionException()
|
public async Task CreateCmsConnectionAsync_MissingConnectionString_ThrowsConnectionException()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
_configuration.GetConnectionString("CMS").Returns((string?)null);
|
_secureStore.Get("CMS").Returns((string?)null);
|
||||||
var factory = new DbConnectionFactory(_configuration, _logger);
|
var factory = new DbConnectionFactory(_secureStore, _logger);
|
||||||
|
|
||||||
// Act & Assert
|
// Act & Assert
|
||||||
var ex = await Should.ThrowAsync<ConnectionException>(
|
var ex = await Should.ThrowAsync<ConnectionException>(
|
||||||
@@ -189,8 +190,8 @@ public class DbConnectionFactoryTests
|
|||||||
public async Task CreateCmsConnectionAsync_InvalidConnectionString_ThrowsConnectionException()
|
public async Task CreateCmsConnectionAsync_InvalidConnectionString_ThrowsConnectionException()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
_configuration.GetConnectionString("CMS").Returns("Invalid oracle connection");
|
_secureStore.Get("CMS").Returns("Invalid oracle connection");
|
||||||
var factory = new DbConnectionFactory(_configuration, _logger);
|
var factory = new DbConnectionFactory(_secureStore, _logger);
|
||||||
|
|
||||||
// Act & Assert
|
// Act & Assert
|
||||||
var ex = await Should.ThrowAsync<ConnectionException>(
|
var ex = await Should.ThrowAsync<ConnectionException>(
|
||||||
@@ -208,8 +209,8 @@ public class DbConnectionFactoryTests
|
|||||||
public async Task CreateLotFinderConnectionAsync_InvalidConnection_LogsError()
|
public async Task CreateLotFinderConnectionAsync_InvalidConnection_LogsError()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
_configuration.GetConnectionString("LotFinderDB").Returns("Invalid connection string");
|
_secureStore.Get("LotFinder").Returns("Invalid connection string");
|
||||||
var factory = new DbConnectionFactory(_configuration, _logger);
|
var factory = new DbConnectionFactory(_secureStore, _logger);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
try
|
try
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using Dapper;
|
using Dapper;
|
||||||
|
using JdeScoping.Core.Interfaces;
|
||||||
using JdeScoping.DataAccess;
|
using JdeScoping.DataAccess;
|
||||||
using JdeScoping.DataAccess.Interfaces;
|
using JdeScoping.DataAccess.Interfaces;
|
||||||
using JdeScoping.DataSync.Dev.Configuration;
|
using JdeScoping.DataSync.Dev.Configuration;
|
||||||
@@ -33,7 +34,17 @@ public class DevEtlPipelineFactoryTests
|
|||||||
.AddEnvironmentVariables()
|
.AddEnvironmentVariables()
|
||||||
.Build();
|
.Build();
|
||||||
|
|
||||||
_connectionFactory = new DbConnectionFactory(config, NullLogger<DbConnectionFactory>.Instance);
|
// Create a mock SecureStore that returns connection strings from configuration
|
||||||
|
var secureStore = Substitute.For<ISecureStoreService>();
|
||||||
|
|
||||||
|
// 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<DbConnectionFactory>.Instance);
|
||||||
_logger = NullLogger<EtlPipeline>.Instance;
|
_logger = NullLogger<EtlPipeline>.Instance;
|
||||||
_cacheDirectory = config["DevEtl:CacheDirectory"]
|
_cacheDirectory = config["DevEtl:CacheDirectory"]
|
||||||
?? Path.Combine(Directory.GetCurrentDirectory(), "..", "..", "..", "..", "..", "CACHED_DB_FILES");
|
?? Path.Combine(Directory.GetCurrentDirectory(), "..", "..", "..", "..", "..", "CACHED_DB_FILES");
|
||||||
|
|||||||
+61
-104
@@ -6,6 +6,10 @@ using Shouldly;
|
|||||||
|
|
||||||
namespace JdeScoping.Infrastructure.Tests.Validation;
|
namespace JdeScoping.Infrastructure.Tests.Validation;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tests for ConnectionStringValidator which validates connection strings stored in SecureStore.
|
||||||
|
/// Connection strings are referenced by name in configuration and retrieved from SecureStore.
|
||||||
|
/// </summary>
|
||||||
public class ConnectionStringValidatorTests : IDisposable
|
public class ConnectionStringValidatorTests : IDisposable
|
||||||
{
|
{
|
||||||
private readonly InMemorySecureStore _secureStore;
|
private readonly InMemorySecureStore _secureStore;
|
||||||
@@ -55,7 +59,7 @@ public class ConnectionStringValidatorTests : IDisposable
|
|||||||
[Fact]
|
[Fact]
|
||||||
public void Validate_NoConnectionStrings_ReturnsValid()
|
public void Validate_NoConnectionStrings_ReturnsValid()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange - no connection strings in config means nothing to validate
|
||||||
var validator = CreateValidator(new Dictionary<string, string?>());
|
var validator = CreateValidator(new Dictionary<string, string?>());
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
@@ -68,12 +72,13 @@ public class ConnectionStringValidatorTests : IDisposable
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[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<string, string?>
|
var validator = CreateValidator(new Dictionary<string, string?>
|
||||||
{
|
{
|
||||||
["ConnectionStrings:SqlServer"] = "Server=localhost;Database=TestDb;Trusted_Connection=true;"
|
["ConnectionStrings:SqlServer"] = "SqlServer" // Name only, value in SecureStore
|
||||||
});
|
});
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
@@ -85,12 +90,31 @@ public class ConnectionStringValidatorTests : IDisposable
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[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<string, string?>
|
var validator = CreateValidator(new Dictionary<string, string?>
|
||||||
{
|
{
|
||||||
["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<string, string?>
|
||||||
|
{
|
||||||
|
["ConnectionStrings:SqlServer"] = "SqlServer"
|
||||||
});
|
});
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
@@ -98,35 +122,17 @@ public class ConnectionStringValidatorTests : IDisposable
|
|||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
result.IsValid.ShouldBeFalse();
|
result.IsValid.ShouldBeFalse();
|
||||||
result.Errors.ShouldContain("Connection string 'SqlServer' is empty");
|
result.Errors.ShouldContain(e => e.Contains("'SqlServer'") && e.Contains("empty in SecureStore"));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[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<string, string?>
|
var validator = CreateValidator(new Dictionary<string, string?>
|
||||||
{
|
{
|
||||||
["ConnectionStrings:SqlServer"] = "Server=localhost;Database=TestDb;Password=${DB_PASSWORD};"
|
["ConnectionStrings:SqlServer"] = "SqlServer"
|
||||||
});
|
|
||||||
// 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<string, string?>
|
|
||||||
{
|
|
||||||
["ConnectionStrings:SqlServer"] = "Server=localhost;Database=TestDb;Password=${DB_PASSWORD};"
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
@@ -134,17 +140,17 @@ public class ConnectionStringValidatorTests : IDisposable
|
|||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
result.IsValid.ShouldBeFalse();
|
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]
|
[Fact]
|
||||||
public void Validate_AllPlaceholdersResolved_ValidatesFormat()
|
public void Validate_ValidSqlServerFormat_ValidatesSuccessfully()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange - SQL Server connection string format
|
||||||
_secureStore.Set("DB_PASSWORD", "secretpassword");
|
_secureStore.Set("SqlServer", "Server=localhost;Database=TestDb;Trusted_Connection=true;");
|
||||||
var validator = CreateValidator(new Dictionary<string, string?>
|
var validator = CreateValidator(new Dictionary<string, string?>
|
||||||
{
|
{
|
||||||
["ConnectionStrings:SqlServer"] = "Server=localhost;Database=TestDb;Password=${DB_PASSWORD};"
|
["ConnectionStrings:SqlServer"] = "SqlServer"
|
||||||
});
|
});
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
@@ -155,53 +161,14 @@ public class ConnectionStringValidatorTests : IDisposable
|
|||||||
result.Errors.ShouldBeEmpty();
|
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<string, string?>
|
|
||||||
{
|
|
||||||
["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<string, string?>
|
|
||||||
{
|
|
||||||
["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]
|
[Fact]
|
||||||
public void Validate_MissingServerOrDataSource_ReturnsError()
|
public void Validate_MissingServerOrDataSource_ReturnsError()
|
||||||
{
|
{
|
||||||
// Arrange - connection string with no Server= or Data Source=
|
// Arrange - connection string with no Server= or Data Source=
|
||||||
|
_secureStore.Set("Invalid", "Database=TestDb;Trusted_Connection=true;");
|
||||||
var validator = CreateValidator(new Dictionary<string, string?>
|
var validator = CreateValidator(new Dictionary<string, string?>
|
||||||
{
|
{
|
||||||
["ConnectionStrings:Invalid"] = "Database=TestDb;Trusted_Connection=true;"
|
["ConnectionStrings:Invalid"] = "Invalid"
|
||||||
});
|
});
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
@@ -216,9 +183,10 @@ public class ConnectionStringValidatorTests : IDisposable
|
|||||||
public void Validate_DataSourceFormat_ValidatesSuccessfully()
|
public void Validate_DataSourceFormat_ValidatesSuccessfully()
|
||||||
{
|
{
|
||||||
// Arrange - Oracle-style connection string
|
// Arrange - Oracle-style connection string
|
||||||
|
_secureStore.Set("Oracle", "Data Source=//localhost:1521/ORCL;User Id=test;Password=test;");
|
||||||
var validator = CreateValidator(new Dictionary<string, string?>
|
var validator = CreateValidator(new Dictionary<string, string?>
|
||||||
{
|
{
|
||||||
["ConnectionStrings:Oracle"] = "Data Source=//localhost:1521/ORCL;User Id=test;Password=test;"
|
["ConnectionStrings:Oracle"] = "Oracle"
|
||||||
});
|
});
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
@@ -232,9 +200,10 @@ public class ConnectionStringValidatorTests : IDisposable
|
|||||||
public void Validate_HostFormat_ValidatesSuccessfully()
|
public void Validate_HostFormat_ValidatesSuccessfully()
|
||||||
{
|
{
|
||||||
// Arrange - PostgreSQL-style connection string
|
// Arrange - PostgreSQL-style connection string
|
||||||
|
_secureStore.Set("Postgres", "Host=localhost;Port=5432;Database=testdb;Username=test;Password=test;");
|
||||||
var validator = CreateValidator(new Dictionary<string, string?>
|
var validator = CreateValidator(new Dictionary<string, string?>
|
||||||
{
|
{
|
||||||
["ConnectionStrings:Postgres"] = "Host=localhost;Port=5432;Database=testdb;Username=test;Password=test;"
|
["ConnectionStrings:Postgres"] = "Postgres"
|
||||||
});
|
});
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
@@ -247,13 +216,16 @@ public class ConnectionStringValidatorTests : IDisposable
|
|||||||
[Fact]
|
[Fact]
|
||||||
public void Validate_MultipleConnectionStrings_ValidatesAll()
|
public void Validate_MultipleConnectionStrings_ValidatesAll()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange - multiple connection strings, one missing from SecureStore
|
||||||
_secureStore.Set("SQL_PASSWORD", "sqlpass");
|
_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<string, string?>
|
var validator = CreateValidator(new Dictionary<string, string?>
|
||||||
{
|
{
|
||||||
["ConnectionStrings:SqlServer"] = "Server=localhost;Database=TestDb;Password=${SQL_PASSWORD};",
|
["ConnectionStrings:SqlServer"] = "SqlServer",
|
||||||
["ConnectionStrings:Oracle"] = "Data Source=//localhost:1521/ORCL;User Id=test;Password=test;",
|
["ConnectionStrings:Oracle"] = "Oracle",
|
||||||
["ConnectionStrings:InvalidEmpty"] = ""
|
["ConnectionStrings:Missing"] = "Missing"
|
||||||
});
|
});
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
@@ -261,17 +233,18 @@ public class ConnectionStringValidatorTests : IDisposable
|
|||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
result.IsValid.ShouldBeFalse();
|
result.IsValid.ShouldBeFalse();
|
||||||
result.Errors.Count.ShouldBe(1); // Only the empty one should fail
|
result.Errors.Count.ShouldBe(1); // Only the missing one should fail
|
||||||
result.Errors.ShouldContain("Connection string 'InvalidEmpty' is empty");
|
result.Errors.ShouldContain(e => e.Contains("'Missing'") && e.Contains("not found in SecureStore"));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Validate_UnknownConnectionFormat_AddsWarning()
|
public void Validate_UnknownConnectionFormat_AddsWarning()
|
||||||
{
|
{
|
||||||
// Arrange - connection string with Server= but no Database=
|
// Arrange - connection string with Server= but no Database=
|
||||||
|
_secureStore.Set("Custom", "Server=localhost;CustomProperty=value;");
|
||||||
var validator = CreateValidator(new Dictionary<string, string?>
|
var validator = CreateValidator(new Dictionary<string, string?>
|
||||||
{
|
{
|
||||||
["ConnectionStrings:Custom"] = "Server=localhost;CustomProperty=value;"
|
["ConnectionStrings:Custom"] = "Custom"
|
||||||
});
|
});
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
@@ -282,30 +255,14 @@ public class ConnectionStringValidatorTests : IDisposable
|
|||||||
result.Warnings.ShouldContain(w => w.Contains("'Custom'") && w.Contains("unknown format"));
|
result.Warnings.ShouldContain(w => w.Contains("'Custom'") && w.Contains("unknown format"));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Validate_WhitespaceOnlyConnectionString_ReturnsError()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var validator = CreateValidator(new Dictionary<string, string?>
|
|
||||||
{
|
|
||||||
["ConnectionStrings:SqlServer"] = " "
|
|
||||||
});
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var result = validator.Validate();
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
result.IsValid.ShouldBeFalse();
|
|
||||||
result.Errors.ShouldContain("Connection string 'SqlServer' is empty");
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Validate_CaseInsensitiveServerCheck_ValidatesSuccessfully()
|
public void Validate_CaseInsensitiveServerCheck_ValidatesSuccessfully()
|
||||||
{
|
{
|
||||||
// Arrange - mixed case Server
|
// Arrange - mixed case Server
|
||||||
|
_secureStore.Set("SqlServer", "server=localhost;Database=TestDb;");
|
||||||
var validator = CreateValidator(new Dictionary<string, string?>
|
var validator = CreateValidator(new Dictionary<string, string?>
|
||||||
{
|
{
|
||||||
["ConnectionStrings:SqlServer"] = "server=localhost;Database=TestDb;"
|
["ConnectionStrings:SqlServer"] = "SqlServer"
|
||||||
});
|
});
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
|
|||||||
Reference in New Issue
Block a user