New project SQLInsight.Servers: - CimWindowsServerSource (Microsoft.Management.Infrastructure, the API behind Get-CimInstance): WS-Man first, DCOM if that fails, a local session for the agent's own computer, as the agent's identity. Only SELECT queries (CimQueries) and Task Scheduler's read-only GetInfo. CPU and paging from raw counters, disks (Win32_Volume, Win32_LogicalDisk fallback), services, scheduled tasks, and the System/Application logs through the Event Log service (MS-EVEN6, XPath filter on the server) with Win32_NTLogEvent as fallback. - ServerErrors: access denied, missing namespace, RPC/WinRM unreachable and timeouts become "ikke målbar" reasons that name the missing right and the grant script; never local administrator. - WindowsServerTester: tries every area once for the web UI's test. - SyntheticCheckRunner: TCP, HTTP(S) with expected status/text and TLS certificate expiry (also for refused certificates), SQL login + read-only query (re-validated, in a transaction that is always rolled back), DNS, and the Dynamics AX logon: AOS port, then a logon through the .NET Business Connector in a separate Windows PowerShell process with a timeout when it is installed; otherwise the result says clearly that only the port was tested. Tests: error mapping, queries, AX command/parsing, runners against local TCP and HTTP(S) servers with a self-signed certificate, the CIM source against this computer, and the SQL check against the test instance. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
150 lines
8.3 KiB
C#
150 lines
8.3 KiB
C#
using SQLInsight.Core.Servers;
|
|
using SQLInsight.Servers;
|
|
using SQLInsight.Servers.Synthetic;
|
|
|
|
namespace SQLInsight.Servers.Tests;
|
|
|
|
public class ServerErrorTests
|
|
{
|
|
private static ServerAccessException Describe(uint? code, string? kind = null, string message = "Windows' tekst", string? area = ServerArea.Services, string protocol = "WS-Man") =>
|
|
ServerErrors.Describe(code, kind, message, area, "aos01", @"FIRMA\AGENT01$", protocol);
|
|
|
|
[Fact]
|
|
public void Access_denied_names_the_right_and_the_script_never_local_admin()
|
|
{
|
|
var error = Describe(ServerErrors.AccessDenied);
|
|
|
|
Assert.Equal(ServerAccessProblem.AccessDenied, error.Problem);
|
|
Assert.Contains(@"FIRMA\AGENT01$", error.Message, StringComparison.Ordinal);
|
|
Assert.Contains("Service Control Manager", error.Message, StringComparison.Ordinal);
|
|
Assert.Contains("grant-windows-server.ps1", error.Message, StringComparison.Ordinal);
|
|
Assert.Contains("ikke være lokal administrator", error.Message, StringComparison.Ordinal);
|
|
Assert.Contains("0x80070005", error.Message, StringComparison.Ordinal);
|
|
Assert.Contains("Event Log Readers", Describe(ServerErrors.WbemAccessDenied, area: ServerArea.Events).Message, StringComparison.Ordinal);
|
|
Assert.Contains("Distributed COM Users", Describe(null, "AccessDenied", area: null, protocol: "DCOM").Message, StringComparison.Ordinal);
|
|
Assert.Contains("Remote Management Users", Describe(null, "AccessDenied", area: null, protocol: "WS-Man").Message, StringComparison.Ordinal);
|
|
}
|
|
|
|
[Fact]
|
|
public void Missing_namespace_unreachable_and_timeouts()
|
|
{
|
|
var tasks = Describe(ServerErrors.InvalidNamespace, "InvalidNamespace", area: ServerArea.Tasks);
|
|
Assert.Equal(ServerAccessProblem.NotAvailable, tasks.Problem);
|
|
Assert.Contains("Windows Server 2012", tasks.Message, StringComparison.Ordinal);
|
|
|
|
var rpc = Describe(ServerErrors.RpcUnavailable, "Failed", "The RPC server is unavailable.", area: null, protocol: "DCOM");
|
|
Assert.Equal(ServerAccessProblem.Unreachable, rpc.Problem);
|
|
Assert.Contains("TCP 135", rpc.Message, StringComparison.Ordinal);
|
|
|
|
var winrm = Describe(null, "Failed", "The client cannot connect to the destination specified in the request. ... WinRM ...", area: null);
|
|
Assert.Equal(ServerAccessProblem.Unreachable, winrm.Problem);
|
|
Assert.Contains("5985", winrm.Message, StringComparison.Ordinal);
|
|
|
|
Assert.Equal(ServerAccessProblem.Timeout, Describe(ServerErrors.MiTimeout, "Failed", "Timed out").Problem);
|
|
Assert.Equal(ServerAccessProblem.Other, Describe(0x80004005, "Failed", "Unspecified error").Problem);
|
|
}
|
|
|
|
[Fact]
|
|
public void Hints_by_problem()
|
|
{
|
|
Assert.Contains("tildelingsscriptet", WindowsServerTester.Hint(ServerAccessProblem.AccessDenied), StringComparison.Ordinal);
|
|
Assert.Contains("5985", WindowsServerTester.Hint(ServerAccessProblem.Unreachable), StringComparison.Ordinal);
|
|
Assert.Equal(UnsupportedWindowsServerSource.Reason, WindowsServerTester.Hint(ServerAccessProblem.NotSupported));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task An_agent_not_on_windows_says_so()
|
|
{
|
|
var error = await Assert.ThrowsAsync<ServerAccessException>(() => new UnsupportedWindowsServerSource().ConnectAsync("aos01", CancellationToken.None));
|
|
var test = await WindowsServerTester.TestAsync(new UnsupportedWindowsServerSource(), "aos01", null, DateTimeOffset.UtcNow, CancellationToken.None);
|
|
|
|
Assert.Equal(ServerAccessProblem.NotSupported, error.Problem);
|
|
Assert.False(test.Reachable);
|
|
Assert.Equal(UnsupportedWindowsServerSource.Reason, test.Error);
|
|
}
|
|
}
|
|
|
|
public class CimQueryTests
|
|
{
|
|
[Fact]
|
|
public void Wql_literals_and_times()
|
|
{
|
|
Assert.Equal(@"AOS60$01", CimQueries.Wql("AOS60$01"));
|
|
Assert.Equal(@"a\\b\'c", CimQueries.Wql(@"a\b'c"));
|
|
Assert.Equal("SELECT Name, DisplayName, State, StartMode, ExitCode FROM Win32_Service WHERE Name = 'O\\'Brien'", CimQueries.SingleServiceQuery("O'Brien"));
|
|
var at = new DateTimeOffset(2026, 10, 2, 14, 5, 6, TimeSpan.FromHours(2));
|
|
Assert.Equal("20261002120506.000000+000", CimQueries.Dmtf(at));
|
|
Assert.Equal("*[System[(Level=1 or Level=2) and TimeCreated[@SystemTime>'2026-10-02T12:05:06.000Z']]]", CimQueries.EventXPath(at));
|
|
Assert.Contains("Logfile = 'System' AND EventType = 1 AND TimeGenerated > '20261002120506.000000+000'", CimQueries.EventQuery("System", at), StringComparison.Ordinal);
|
|
}
|
|
|
|
[Fact]
|
|
public void Queries_only_read()
|
|
{
|
|
var queries = new[]
|
|
{
|
|
CimQueries.OperatingSystemQuery, CimQueries.ComputerSystemQuery, CimQueries.ProcessorQuery, CimQueries.MemoryQuery,
|
|
CimQueries.VolumeQuery, CimQueries.LogicalDiskQuery, CimQueries.ServiceQuery, CimQueries.TaskQuery,
|
|
};
|
|
|
|
Assert.All(queries, q => Assert.StartsWith("SELECT ", q, StringComparison.Ordinal));
|
|
}
|
|
}
|
|
|
|
public class AxBusinessConnectorTests
|
|
{
|
|
private static readonly BusinessConnectorInstallation Ax2012 = BusinessConnectorLocator.Describe(@"C:\Program Files\Microsoft Dynamics AX\60\BusinessConnector\Bin\Microsoft.Dynamics.BusinessConnectorNet.dll");
|
|
private static readonly BusinessConnectorInstallation Ax2009 = BusinessConnectorLocator.Describe(@"C:\Program Files (x86)\Microsoft Dynamics AX\50\Client\Bin\Microsoft.Dynamics.BusinessConnectorNet.dll");
|
|
|
|
[Fact]
|
|
public void Versions_and_bitness_from_the_path()
|
|
{
|
|
Assert.Equal(("AX 2012", false, false), (Ax2012.Version, Ax2012.Is32Bit, Ax2012.NeedsClr2));
|
|
Assert.Equal(("AX 2009", true, true), (Ax2009.Version, Ax2009.Is32Bit, Ax2009.NeedsClr2));
|
|
}
|
|
|
|
[Fact]
|
|
public void Locator_uses_the_configured_path_first()
|
|
{
|
|
var configured = @"D:\AX\Microsoft.Dynamics.BusinessConnectorNet.dll";
|
|
|
|
if (OperatingSystem.IsWindows())
|
|
{
|
|
Assert.Equal(configured, new BusinessConnectorLocator(configured, p => p == configured).Find()!.Path);
|
|
}
|
|
|
|
Assert.Null(new BusinessConnectorLocator(null, _ => false).Find());
|
|
}
|
|
|
|
[Fact]
|
|
public void Command_picks_powershell_and_never_puts_parameters_on_the_command_line()
|
|
{
|
|
var (file2012, args2012) = AxLogonClient.Command(Ax2012, @"C:\Windows");
|
|
var (file2009, args2009) = AxLogonClient.Command(Ax2009, @"C:\Windows");
|
|
var environment = AxLogonClient.Environment(Ax2012, "DAT", "01@aos01:2712", null);
|
|
|
|
Assert.Equal(@"C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe", file2012);
|
|
Assert.DoesNotContain("-Version", args2012);
|
|
Assert.EndsWith(@"WindowsPowerShell\v1.0\powershell.exe", file2009, StringComparison.Ordinal);
|
|
Assert.Equal(["-Version", "2"], args2009.Take(2));
|
|
Assert.Contains("-EncodedCommand", args2012);
|
|
Assert.DoesNotContain(args2012, a => a.Contains("aos01", StringComparison.Ordinal));
|
|
Assert.Equal(("DAT", "01@aos01:2712", string.Empty), (environment["SQLINSIGHT_AX_COMPANY"], environment["SQLINSIGHT_AX_SERVER"], environment["SQLINSIGHT_AX_CONFIG"]));
|
|
var script = System.Text.Encoding.Unicode.GetString(Convert.FromBase64String(args2012[^1]));
|
|
Assert.Contains("Logon(", script, StringComparison.Ordinal);
|
|
Assert.Contains("Logoff()", script, StringComparison.Ordinal);
|
|
}
|
|
|
|
[Fact]
|
|
public void Parse_the_helpers_answer()
|
|
{
|
|
Assert.Equal((true, 812, "Logon i AX lykkedes."), AxLogonClient.Parse(new ProcessResult(0, "noise\r\nSQLINSIGHT-AX-OK 812\r\n", string.Empty, false), Ax2012));
|
|
var notUser = AxLogonClient.Parse(new ProcessResult(1, "SQLINSIGHT-AX-ERROR You are not a recognized user of Microsoft Dynamics AX.", string.Empty, false), Ax2012);
|
|
Assert.False(notUser.Ok);
|
|
Assert.Contains("ikke bruger i AX", notUser.Message, StringComparison.Ordinal);
|
|
Assert.Contains("PowerShell 2.0", AxLogonClient.Parse(new ProcessResult(1, string.Empty, "Version v2.0.50727 of the .NET Framework is not installed", false), Ax2009).Message,
|
|
StringComparison.Ordinal);
|
|
Assert.Contains("i tide", AxLogonClient.Parse(new ProcessResult(-1, string.Empty, string.Empty, true), Ax2012).Message, StringComparison.Ordinal);
|
|
Assert.Contains("kode 3", AxLogonClient.Parse(new ProcessResult(3, string.Empty, string.Empty, false), Ax2012).Message, StringComparison.Ordinal);
|
|
}
|
|
}
|