using System.Data; using System.Data.Common; using System.Diagnostics.CodeAnalysis; using System.Security.Authentication; namespace SQLInsight.Connectivity.Tests; public class SqlConnectionFactoryTests { private static readonly ConnectionTarget Target = new() { Server = "sql01" }; [Fact] public async Task Auto_falls_back_to_odbc_on_legacy_failure() { var odbc = new FakeConnection(); var factory = new SqlConnectionFactory( _ => new FakeConnection(new AuthenticationException("TLS 1.0 only")), _ => odbc); await using var opened = await factory.OpenAsync(Target); Assert.Equal(ConnectionProvider.Odbc, opened.Provider); Assert.True(opened.UsedLegacyFallback); Assert.Equal(SqlConnectionFactory.SessionSettings, odbc.ExecutedCommands.Single()); } [Fact] public async Task Auto_does_not_fall_back_on_login_failure() { var odbcCreated = false; var factory = new SqlConnectionFactory( _ => new FakeConnection(new InvalidOperationException("Login failed for user 'x'.")), _ => { odbcCreated = true; return new FakeConnection(); }); await Assert.ThrowsAsync(() => factory.OpenAsync(Target)); Assert.False(odbcCreated); } [Fact] public async Task Explicit_sqlclient_never_falls_back() { var factory = new SqlConnectionFactory( _ => new FakeConnection(new AuthenticationException("TLS 1.0 only")), _ => new FakeConnection()); await Assert.ThrowsAsync( () => factory.OpenAsync(Target with { Provider = ConnectionProvider.SqlClient })); } [Fact] public async Task Session_settings_are_applied_on_open() { var sqlClient = new FakeConnection(); var factory = new SqlConnectionFactory(_ => sqlClient, _ => new FakeConnection()); await using var opened = await factory.OpenAsync(Target); Assert.False(opened.UsedLegacyFallback); Assert.Equal(SqlConnectionFactory.SessionSettings, sqlClient.ExecutedCommands.Single()); } [Fact] public async Task Failed_connection_is_disposed() { var failing = new FakeConnection(new InvalidOperationException("Login failed")); var factory = new SqlConnectionFactory(_ => failing, _ => new FakeConnection()); await Assert.ThrowsAsync(() => factory.OpenAsync(Target)); Assert.True(failing.IsDisposed); } [SuppressMessage("Design", "CA1065", Justification = "Test double; unused members throw.")] private sealed class FakeConnection(Exception? openFailure = null) : DbConnection { private ConnectionState _state = ConnectionState.Closed; public List ExecutedCommands { get; } = []; public bool IsDisposed { get; private set; } [AllowNull] public override string ConnectionString { get; set; } = string.Empty; public override string Database => "master"; public override string DataSource => "fake"; public override string ServerVersion => "16.0.0.0"; public override ConnectionState State => _state; public override void Open() { if (openFailure is not null) throw openFailure; _state = ConnectionState.Open; } public override Task OpenAsync(CancellationToken cancellationToken) { Open(); return Task.CompletedTask; } public override void Close() => _state = ConnectionState.Closed; public override void ChangeDatabase(string databaseName) => throw new NotSupportedException(); protected override DbTransaction BeginDbTransaction(IsolationLevel isolationLevel) => throw new NotSupportedException(); protected override DbCommand CreateDbCommand() => new FakeCommand(this); protected override void Dispose(bool disposing) { IsDisposed = true; base.Dispose(disposing); } public override ValueTask DisposeAsync() { IsDisposed = true; return base.DisposeAsync(); } private sealed class FakeCommand(FakeConnection owner) : DbCommand { [AllowNull] public override string CommandText { get; set; } = string.Empty; public override int CommandTimeout { get; set; } public override CommandType CommandType { get; set; } public override bool DesignTimeVisible { get; set; } public override UpdateRowSource UpdatedRowSource { get; set; } protected override DbConnection? DbConnection { get; set; } protected override DbParameterCollection DbParameterCollection => throw new NotSupportedException(); protected override DbTransaction? DbTransaction { get; set; } public override int ExecuteNonQuery() { owner.ExecutedCommands.Add(CommandText); return 0; } public override Task ExecuteNonQueryAsync(CancellationToken cancellationToken) => Task.FromResult(ExecuteNonQuery()); public override object? ExecuteScalar() => throw new NotSupportedException(); public override void Prepare() { } public override void Cancel() { } protected override DbParameter CreateDbParameter() => throw new NotSupportedException(); protected override DbDataReader ExecuteDbDataReader(CommandBehavior behavior) => throw new NotSupportedException(); } } }