sqlinsight-pro/tests/SQLInsight.Collectors.Tests/DiscoveryTests.cs
Søren Eggert Lundsteen Olsen 6b945295d0 Dynamics AX collectors: shared detector, batch jobs, number sequences and RecId
dynamics-ax (hourly) lists the monitored AX databases with version, schema,
companies and read access; discovery and the AX community rules (now marked
dynamicsAx) use the same detection. dynamics-ax-batch (5 min) reads BATCH
(3.0/4.0) or BATCHJOB/BATCHJOBHISTORY (2009+), keeps each job's own run
history; dynamics-ax-sequences (hourly) reads NUMBERSEQUENCETABLE and
SYSTEMSEQUENCES and forecasts from SQLInsight's own daily samples. All reads
run through [db].sys.sp_executesql under read committed with a 2 s lock
timeout (snapshot when allowed), bounded with TOP, never NOLOCK, never
written. Blocking now captures CONTEXT_INFO for the AX user mapping.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-02 13:53:34 +02:00

92 lines
4.6 KiB
C#

using System.Text.RegularExpressions;
using SQLInsight.Collectors.Discovery;
using SQLInsight.Core.Instances;
namespace SQLInsight.Collectors.Tests;
/// <summary>Database discovery and the permission check: version-guarded SQL and what is asked for on each version.</summary>
public sealed class DiscoveryTests
{
private static InstanceInfo Instance(string version, EngineEdition edition = EngineEdition.Standard) => new()
{
ServerName = "SQL01", Version = SqlServerVersion.Parse(version), Edition = "Standard Edition", EngineEdition = edition,
};
[Fact]
public void Dynamics_ax_detection_is_the_central_one_run_in_the_database_or_with_three_part_names_on_2000()
{
var modern = DatabaseDiscovery.DynamicsAxSql("AX]30", SqlServerVersion.Parse("9.0.5000.0"));
var legacy = DatabaseDiscovery.DynamicsAxSql("AX30", SqlServerVersion.Parse("8.0.2039.0"));
Assert.StartsWith("EXEC [AX]]30].sys.sp_executesql", modern, StringComparison.Ordinal);
Assert.Contains("FROM sys.tables", modern, StringComparison.Ordinal);
Assert.DoesNotContain("USE ", modern, StringComparison.OrdinalIgnoreCase);
Assert.Contains("[AX30].dbo.sysobjects", legacy, StringComparison.Ordinal);
Assert.DoesNotContain("sys.", legacy, StringComparison.Ordinal);
}
[Fact]
public void Sql_server_2008_r2_is_checked_for_view_server_state_and_a_user_in_each_database()
{
var checks = PermissionCheck.ChecksFor(Instance("10.50.6000.34")).Select(c => c.Name).ToList();
Assert.Contains("VIEW SERVER STATE", checks);
Assert.Contains("VIEW ANY DEFINITION", checks);
Assert.DoesNotContain("CONNECT ANY DATABASE", checks);
Assert.DoesNotContain("VIEW SERVER PERFORMANCE STATE", checks);
Assert.Contains("SELECT ON msdb.dbo.backupset", checks);
}
[Fact]
public void Sql_server_2022_is_checked_for_the_granular_rights_and_managed_instance_like_2019()
{
var sql2022 = PermissionCheck.ChecksFor(Instance("16.0.4135.4")).Select(c => c.Name).ToList();
var managed = PermissionCheck.ChecksFor(Instance("12.0.2000.8", EngineEdition.AzureSqlManagedInstance)).Select(c => c.Name).ToList();
Assert.Contains("VIEW SERVER PERFORMANCE STATE", sql2022);
Assert.Contains("VIEW SERVER SECURITY STATE", sql2022);
Assert.DoesNotContain("VIEW SERVER STATE", sql2022);
Assert.Contains("CONNECT ANY DATABASE", sql2022);
Assert.Contains("VIEW SERVER STATE", managed);
Assert.Contains("CONNECT ANY DATABASE", managed);
Assert.False(PermissionCheck.ChecksFor(Instance("16.0.4135.4")).Single(c => c.Name == "ALTER TRACE").Required);
}
/// <summary>2008+ constructs that make a batch fail on 2005 (as in Sql2005CompatibilityTests).</summary>
internal static readonly Regex[] Sql2008Only =
[
new(@"DECLARE\s+@\w+\s+\w+(\s*\([\d\s,]+\))?\s*=", RegexOptions.IgnoreCase),
new(@"\bMERGE\s", RegexOptions.IgnoreCase),
new(@"\b(CONCAT|IIF|CHOOSE|FORMAT|TRY_CONVERT|TRY_CAST|STRING_AGG|STRING_SPLIT)\s*\(", RegexOptions.IgnoreCase),
new(@"\bOFFSET\s+\S+\s+ROWS\b", RegexOptions.IgnoreCase),
new(@"\bVALUES\s*\([^)]*\)\s*,\s*\(", RegexOptions.IgnoreCase),
];
[Fact]
public void Discovery_and_permission_sql_runs_on_sql_server_2005()
{
var sql2005 = Instance("9.0.5000.0");
var batches = new[]
{
DatabaseDiscovery.Databases2005Sql, DatabaseDiscovery.Databases2000Sql, DatabaseDiscovery.DynamicsAxSql("AX30", sql2005.Version),
DynamicsAx.AxDatabaseDetector.DatabasesSql,
PermissionCheck.BuildSql(PermissionCheck.ChecksFor(sql2005)), PermissionCheck.DatabasesWithoutAccessSql,
};
Assert.All(batches, sql => Assert.DoesNotContain(Sql2008Only, p => p.IsMatch(sql)));
Assert.All(batches, sql => Assert.DoesNotContain("PXI_BATCHMONITORTABLE", sql, StringComparison.OrdinalIgnoreCase));
Assert.DoesNotContain("sys.", DatabaseDiscovery.Databases2000Sql, StringComparison.Ordinal);
}
[Fact]
public void Collector_context_includes_every_database_without_a_selection()
{
var context = new CollectorContext(null!, Instance("16.0.4135.4"), InstanceCapabilities.From(Instance("16.0.4135.4")), DateTimeOffset.UtcNow, null, CancellationToken.None);
var chosen = context with { Databases = new Core.Configuration.DatabaseSelection(false, ["Sales"], [], []) };
Assert.True(context.IncludesDatabase("Anything"));
Assert.True(chosen.IncludesDatabase("Sales"));
Assert.True(chosen.IncludesDatabase("msdb"));
Assert.False(chosen.IncludesDatabase("Anything"));
}
}