sqlinsight-pro/tests/SQLInsight.Servers.Tests/ServerAccessTests.cs
Søren Eggert Lundsteen Olsen 7082b278e3 Collection: Windows servers over CIM (WS-Man, DCOM fallback) and synthetic check runners
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>
2026-10-02 14:58:18 +02:00

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);
}
}