using SQLInsight.Collectors.Databases;
using SQLInsight.Collectors.Discovery;
using SQLInsight.Connectivity;
using SQLInsight.Core.Configuration;
using SQLInsight.Core.Data;
using SQLInsight.Core.Instances;
namespace SQLInsight.IntegrationTests;
///
/// Database discovery, the permission check and the database selection against a real instance, as the
/// least-privilege agent login (and as a login without any rights).
///
[Collection(SqlServerTestGroup.Name)]
[Trait("Category", "Integration")]
public class DiscoveryTests(SqlServerFixture sql) : IAsyncLifetime
{
private readonly string _axDatabase = "AX30_" + Guid.NewGuid().ToString("N")[..6];
public Task InitializeAsync() => sql.ExecuteAsSaAsync($"""
CREATE DATABASE [{_axDatabase}];
EXEC (N'USE [{_axDatabase}]; EXEC (N''CREATE SCHEMA bmssa'');
CREATE TABLE bmssa.SQLDICTIONARY (TABLEID int NOT NULL, FIELDID int NOT NULL, RECID int NOT NULL);
CREATE TABLE bmssa.DATAAREA (ID nvarchar(3) NOT NULL, RECID int NOT NULL);
CREATE TABLE bmssa.SYSCONFIG (ID int NOT NULL, RECID int NOT NULL);');
""");
public Task DisposeAsync() => sql.ExecuteAsSaAsync($"ALTER DATABASE [{_axDatabase}] SET SINGLE_USER WITH ROLLBACK IMMEDIATE; DROP DATABASE [{_axDatabase}];");
private static ConnectionTarget Target(string server, string login, string password) => new()
{
Server = server, Authentication = AuthenticationMode.SqlLogin, UserName = login, Password = password,
Provider = ConnectionProvider.SqlClient, TrustServerCertificate = true,
};
[Fact]
public async Task Agent_login_sees_every_database_with_its_state_and_recognises_dynamics_ax()
{
await using var opened = await new SqlConnectionFactory().OpenAsync(Target(sql.Server, SqlServerFixture.AgentLogin, SqlServerFixture.AgentPassword));
var info = await InstanceProbe.DetectAsync(opened.Connection);
var databases = (await DatabaseDiscovery.DiscoverAsync(opened.Connection, info, CancellationToken.None)).ToDictionary(d => d.Name);
Assert.True(databases["master"].IsSystem);
var test = databases[SqlServerFixture.TestDatabase];
Assert.Equal(("ONLINE", true, false), (test.State, test.HasAccess, test.IsDynamicsAx));
Assert.True(test.SizeMb > 0);
Assert.NotNull(test.RecoveryModel);
Assert.True(test.CompatibilityLevel >= 90);
Assert.Equal((true, "3.0"), (databases[_axDatabase].IsDynamicsAx, databases[_axDatabase].DynamicsAxVersion));
Assert.Equal("master", (await SingleAsync(opened, "SELECT DB_NAME()"))); // the session never changed database
}
[Fact]
public async Task Agent_login_has_every_required_right_and_a_login_without_rights_is_told_what_is_missing()
{
await using (var agent = await new SqlConnectionFactory().OpenAsync(Target(sql.Server, SqlServerFixture.AgentLogin, SqlServerFixture.AgentPassword)))
{
var info = await InstanceProbe.DetectAsync(agent.Connection);
var result = await PermissionCheck.CheckAsync(agent.Connection, info, new ConnectionTestResult { Connected = true }, CancellationToken.None);
Assert.Equal((SqlServerFixture.AgentLogin, false, true), (result.Login, result.IsSysadmin, result.PermissionsChecked));
Assert.Empty(result.MissingRequired);
Assert.Contains(result.Permissions, p => p.Name == "ALTER TRACE" && !p.Granted && !p.Required);
}
var bare = "bare_" + Guid.NewGuid().ToString("N")[..8];
await sql.ExecuteAsSaAsync($"CREATE LOGIN [{bare}] WITH PASSWORD = N'Bare-Login-1!', CHECK_POLICY = OFF;");
await using var opened = await new SqlConnectionFactory().OpenAsync(Target(sql.Server, bare, "Bare-Login-1!"));
var bareInfo = await InstanceProbe.DetectAsync(opened.Connection);
var missing = (await PermissionCheck.CheckAsync(opened.Connection, bareInfo, new ConnectionTestResult { Connected = true }, CancellationToken.None))
.MissingRequired.Select(p => p.Name).ToList();
Assert.Contains("VIEW ANY DEFINITION", missing);
Assert.Contains("CONNECT ANY DATABASE", missing);
Assert.Contains("SELECT ON msdb.dbo.sysjobs", missing); // backupset is readable by public in msdb
}
[Fact]
public async Task Database_checks_only_run_in_the_chosen_databases()
{
await using var session = await AgentSession.OpenAsync(sql);
var selection = new DatabaseSelection(monitorNew: false, [_axDatabase], [SqlServerFixture.TestDatabase], []);
var snapshot = await session.CollectAsync(new DatabaseCollector(), selection: selection);
var indexes = await session.CollectAsync(new IndexCollector(), selection: selection);
var names = snapshot.Databases.Select(d => d.Name).ToList();
Assert.Contains(_axDatabase, names);
Assert.Contains("msdb", names);
Assert.DoesNotContain(SqlServerFixture.TestDatabase, names);
Assert.DoesNotContain(indexes.Databases, d => d.Name == SqlServerFixture.TestDatabase);
}
private static async Task SingleAsync(OpenedConnection opened, string query)
{
await using var command = opened.Connection.CreateCommand();
command.CommandText = query;
return (string)(await command.ExecuteScalarAsync())!;
}
}