Some checks are pending
CI / Build og unit tests (push) Waiting to run
CI / Integration (mcr.microsoft.com/mssql/server:2017-latest) (push) Blocked by required conditions
CI / Integration (mcr.microsoft.com/mssql/server:2019-latest) (push) Blocked by required conditions
CI / Integration (mcr.microsoft.com/mssql/server:2022-latest) (push) Blocked by required conditions
CI / Integration (mcr.microsoft.com/mssql/server:2025-latest) (push) Blocked by required conditions
- .NET 10 solution with Core, Connectivity, Rules, Contracts, Agent, Web and Hub - Capability model gating collectors and rules by SQL Server version (2000 and later); rules that cannot run are reported as not measurable, never as passed - Connectivity via Microsoft.Data.SqlClient with ODBC fallback for legacy instances, hardened monitoring session settings valid back to SQL Server 2000 - Support lifecycle rule (SQLI-VER-001) with lifecycle data shipped as a rule pack - Version-aware least-privilege permissions script, verified by integration tests against SQL Server in Docker (reads monitoring sources, cannot read customer data) - Forgejo Actions CI, contributing guide, code of conduct, issue templates, legacy version notes Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E3EExwndy8Z22EK2Qpt6BK
146 lines
5.6 KiB
C#
146 lines
5.6 KiB
C#
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<InvalidOperationException>(() => 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<AuthenticationException>(
|
|
() => 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<InvalidOperationException>(() => 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<string> 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<int> 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();
|
|
}
|
|
}
|
|
}
|