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>
92 lines
4.6 KiB
C#
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"));
|
|
}
|
|
}
|