using System.Data; using MBN_STOCK_WEBVIEW.Infrastructure; using MMoneyCoderSharp.Data; namespace MBN_STOCK_WEBVIEW.Infrastructure.Tests; public sealed class DatabaseHealthServiceTests { [Fact] public async Task CheckAsync_Unconfigured_ReturnsNotConfiguredWithoutQuery() { var executor = new StubDataQueryExecutor(); var service = new DatabaseHealthService(executor, new DatabaseOptions()); var status = await service.CheckAsync(DataSourceKind.Oracle); Assert.Equal(DatabaseHealthState.NotConfigured, status.State); Assert.Equal(DataSourceKind.Oracle, status.Source); Assert.Equal(TimeSpan.Zero, status.Elapsed); Assert.Null(status.IsTransient); Assert.Null(status.ProviderErrorCode); Assert.Equal(0, executor.CallCount); } [Theory] [InlineData(DataSourceKind.Oracle, "SELECT 1 FROM DUAL")] [InlineData(DataSourceKind.MariaDb, "SELECT 1")] public async Task CheckAsync_ConfiguredAndQuerySucceeds_ReturnsHealthy( DataSourceKind source, string expectedQuery) { var executor = new StubDataQueryExecutor { ExecuteAsyncHandler = (_, _, _, _) => Task.FromResult(new DataTable()) }; var service = new DatabaseHealthService(executor, TestOptions.CreateConfigured()); var status = await service.CheckAsync(source); Assert.Equal(DatabaseHealthState.Healthy, status.State); Assert.Equal(source, status.Source); Assert.Null(status.IsTransient); Assert.Null(status.ProviderErrorCode); Assert.Equal(1, executor.CallCount); Assert.Equal(expectedQuery, executor.LastQuery); } [Fact] public async Task CheckAsync_ConfiguredAndQueryFails_ReturnsUnhealthyWithoutThrowing() { const string host = "health-host-sensitive-sentinel"; var executor = new StubDataQueryExecutor { ExecuteAsyncHandler = (_, _, _, _) => Task.FromException( new DatabaseQueryException( DataSourceKind.MariaDb, isTransient: true, providerErrorCode: 1042)) }; var service = new DatabaseHealthService(executor, TestOptions.CreateConfigured()); var status = await service.CheckAsync(DataSourceKind.MariaDb); Assert.Equal(DatabaseHealthState.Unhealthy, status.State); Assert.Equal(DataSourceKind.MariaDb, status.Source); Assert.True(status.IsTransient); Assert.Equal(1042, status.ProviderErrorCode); Assert.DoesNotContain(host, status.UserMessage, StringComparison.Ordinal); } [Fact] public async Task CheckAsync_ConfiguredAndQueryTimesOut_ReturnsTimedOut() { var executor = new StubDataQueryExecutor { ExecuteAsyncHandler = (_, _, _, _) => Task.FromException( new DatabaseQueryTimeoutException(DataSourceKind.Oracle)) }; var service = new DatabaseHealthService(executor, TestOptions.CreateConfigured()); var status = await service.CheckAsync(DataSourceKind.Oracle); Assert.Equal(DatabaseHealthState.TimedOut, status.State); Assert.Equal(DataSourceKind.Oracle, status.Source); Assert.True(status.IsTransient); Assert.Null(status.ProviderErrorCode); } [Fact] public async Task CheckAsync_NonOperationInfrastructureFailure_DoesNotInventDiagnostics() { var executor = new StubDataQueryExecutor { ExecuteAsyncHandler = (_, _, _, _) => Task.FromException( new DatabaseConfigurationException("safe configuration failure")) }; var service = new DatabaseHealthService(executor, TestOptions.CreateConfigured()); var status = await service.CheckAsync(DataSourceKind.Oracle); Assert.Equal(DatabaseHealthState.Unhealthy, status.State); Assert.Null(status.IsTransient); Assert.Null(status.ProviderErrorCode); } } internal sealed class StubDataQueryExecutor : IDataQueryExecutor { public Func>? ExecuteAsyncHandler { get; init; } public int CallCount { get; private set; } public string? LastQuery { get; private set; } public DataTable Execute(DataSourceKind source, string tableName, string query) => ExecuteAsync(source, tableName, query).GetAwaiter().GetResult(); public Task ExecuteAsync( DataSourceKind source, string tableName, string query, CancellationToken cancellationToken = default) { CallCount++; LastQuery = query; return ExecuteAsyncHandler?.Invoke(source, tableName, query, cancellationToken) ?? Task.FromResult(new DataTable()); } }