using Microsoft.Data.SqlClient;
using SQLInsight.Core.Findings;
using SQLInsight.Core.Servers;
using SQLInsight.Core.SyntheticChecks;
using SQLInsight.Repository;
using SQLInsight.Rules.Engine;
namespace SQLInsight.IntegrationTests;
///
/// Windows servers and synthetic checks in the repository (0018): what SQLInsight Web may change with its read-only
/// login, the SQL Server hosts kept in step by the agent, samples, events, check results, findings and retention.
///
[Collection(SqlServerTestGroup.Name)]
[Trait("Category", "Integration")]
public class ServerRepositoryTests(SqlServerFixture sql) : IAsyncLifetime
{
private readonly string _database = "SQLInsightSrv_" + Guid.NewGuid().ToString("N")[..8];
private InsightRepository _repository = null!;
private string _webConnectionString = string.Empty;
private static readonly DateTimeOffset Now = DateTimeOffset.UtcNow;
public async Task InitializeAsync()
{
await sql.ExecuteAsSaAsync($"CREATE DATABASE [{_database}];");
_repository = new InsightRepository(sql.DatabaseConnectionString(_database));
await _repository.MigrateAsync();
}
public Task DisposeAsync() => sql.ExecuteAsSaAsync($"ALTER DATABASE [{_database}] SET SINGLE_USER WITH ROLLBACK IMMEDIATE; DROP DATABASE [{_database}];");
private async Task ReadOnlyRepositoryAsync()
{
var login = "web_" + Guid.NewGuid().ToString("N")[..8];
const string password = "Read-Only-Login-1!";
await sql.ExecuteAsSaAsync($"""
CREATE LOGIN [{login}] WITH PASSWORD = N'{password}', CHECK_POLICY = OFF;
USE [{_database}]; CREATE USER [{login}] FOR LOGIN [{login}]; ALTER ROLE db_datareader ADD MEMBER [{login}];
""");
_webConnectionString = new SqlConnectionStringBuilder(sql.DatabaseConnectionString(_database)) { UserID = login, Password = password, IntegratedSecurity = false }
.ConnectionString;
return new InsightRepository(_webConnectionString);
}
private async Task ExecuteAsWebAsync(string sqlText)
{
await using var connection = new SqlConnection(_webConnectionString);
await connection.OpenAsync();
await using var command = connection.CreateCommand();
command.CommandText = sqlText;
await command.ExecuteNonQueryAsync();
}
private static SyntheticCheckInput Check(string name, int? serverId = null, byte[]? secret = null) =>
new(name, SyntheticCheckKind.Tcp, serverId, new SyntheticCheckSettings { Host = "aos01", Port = 2712, ServiceName = serverId is null ? null : "AOS60$01" }, secret,
secret is null ? null : "key-1", 300, 15);
[Fact]
public async Task Web_login_can_add_and_change_servers_and_checks_but_nothing_the_agent_records()
{
var web = await ReadOnlyRepositoryAsync();
var server = await web.AddWindowsServerAsync(new NewWindowsServer("AOS01", "AOS01.firma.local", ServerRoles.Application, ["AOS60$01"], []), @"FIRMA\anna", Now);
Assert.True(await web.UpdateWindowsServerAsync(server, ServerRoles.Application, ["AOS60$01", "W3SVC"], ["MyBatch"], @"FIRMA\anna", Now));
Assert.True(await web.SetWindowsServerPausedAsync(server, true, @"FIRMA\anna", Now));
var check = await web.AddSyntheticCheckAsync(Check("AOS01 port", server, [1, 2, 3]), @"FIRMA\anna", Now);
Assert.True(await web.UpdateSyntheticCheckAsync(check, Check("AOS01 AOS-port", server) with { IntervalSeconds = 120 }, @"FIRMA\anna", Now));
Assert.True(await web.SetSyntheticCheckPausedAsync(check, true, @"FIRMA\anna", Now));
var request = await web.AddServerRequestAsync(ServerRequestKind.ServerTest, server, @"FIRMA\anna", Now);
var row = (await _repository.GetWindowsServerAsync(server))!;
Assert.Equal(("aos01.firma.local", ServerRoles.Application, true, ServerSources.Web), (row.Host, row.Role, row.Paused, row.Source));
Assert.Equal(["AOS60$01", "W3SVC"], row.Watched);
Assert.Equal(["MyBatch"], row.Ignored);
var checkRow = (await _repository.GetSyntheticCheckAsync(check))!;
Assert.Equal(("AOS01 AOS-port", 120, true, "AOS01"), (checkRow.Name, checkRow.IntervalSeconds, checkRow.HasSecret, checkRow.ServerName));
Assert.Equal("AOS60$01", checkRow.Settings.ServiceName);
Assert.Equal(server, await web.GetRequestTargetAsync(request));
Assert.Equal(request, (await web.GetLatestServerRequestAsync(ServerRequestKind.ServerTest, server))!.RequestId);
// What only the agent does is refused for the web login.
await Assert.ThrowsAsync(() => web.RecordServerContactAsync(server, new ServerOsInfo { Caption = "x" }, "DCOM", null, Now));
await Assert.ThrowsAsync(() => web.SaveServerPerformanceAsync(server, new ServerPerformanceSample(Now, 1, 1, 1, 1, 1)));
await Assert.ThrowsAsync(() => web.RecordSyntheticCheckResultAsync(check, new SyntheticCheckResult { At = Now, Success = true, Message = "ok" }));
await Assert.ThrowsAsync(() => web.RecordServerAreasAsync(server, []));
await Assert.ThrowsAsync(() => web.SaveServerSnapshotAsync(server, ServerArea.Services, new ServerServiceSnapshot { CapturedAt = Now }, Now));
await Assert.ThrowsAsync(() => ExecuteAsWebAsync($"UPDATE insight.WindowsServers SET Host = N'evil' WHERE ServerId = {server}"));
await Assert.ThrowsAsync(() => ExecuteAsWebAsync($"UPDATE insight.SyntheticChecks SET Kind = 'Sql' WHERE CheckId = {check}"));
await Assert.ThrowsAsync(() => ExecuteAsWebAsync($"DELETE FROM insight.SyntheticChecks WHERE CheckId = {check}"));
await Assert.ThrowsAsync(() => ExecuteAsWebAsync("DELETE FROM insight.ServerFindings"));
Assert.True(await web.RemoveSyntheticCheckAsync(check, @"FIRMA\anna", Now));
Assert.Equal(ConfigurationChangeOutcome.Changed, await web.RemoveWindowsServerAsync(server, @"FIRMA\anna", Now));
Assert.Empty(await _repository.GetWindowsServersAsync());
Assert.Empty(await _repository.GetSyntheticChecksAsync());
}
[Fact]
public async Task Names_and_hosts_are_unique_among_servers_in_use()
{
await _repository.AddWindowsServerAsync(new NewWindowsServer("FIL01", "fil01", ServerRoles.File, [], []), null, Now);
var host = await Assert.ThrowsAsync(() => _repository.AddWindowsServerAsync(new NewWindowsServer("Fil", "FIL01", ServerRoles.File, [], []), null, Now));
var name = await Assert.ThrowsAsync(() => _repository.AddWindowsServerAsync(new NewWindowsServer("FIL01", "fil02", ServerRoles.File, [], []), null, Now));
Assert.True(host.Number is 2601 or 2627);
Assert.True(name.Number is 2601 or 2627);
}
[Fact]
public async Task Sql_server_hosts_follow_the_instances_and_keep_their_settings()
{
await _repository.AddWindowsServerAsync(new NewWindowsServer("SQL02 (manuelt)", "sql02.firma.local", ServerRoles.Sql, [], []), null, Now);
await _repository.SyncSqlHostServersAsync([("sql01.firma.local", "SQL01"), ("SQL02.firma.local", "SQL02"), ("sql01.firma.local", "SQL01")], Now);
var servers = await _repository.GetWindowsServersAsync();
Assert.Equal(["SQL01", "SQL02 (manuelt)"], servers.Select(s => s.Name).ToList());
var sql01 = servers.Single(s => s.Name == "SQL01");
Assert.True(sql01.IsSqlHost);
Assert.Equal(ServerRoles.Sql, sql01.Role);
await _repository.UpdateWindowsServerAsync(sql01.ServerId, ServerRoles.Sql, ["MSSQLSERVER"], [], "anna", Now);
Assert.Equal(ConfigurationChangeOutcome.FromConfigFile, await _repository.RemoveWindowsServerAsync(sql01.ServerId, "anna", Now));
await _repository.SyncSqlHostServersAsync([("sql01.firma.local", "SQL01")], Now);
Assert.Equal(["MSSQLSERVER"], (await _repository.GetWindowsServerAsync(sql01.ServerId))!.Watched);
await _repository.SyncSqlHostServersAsync([], Now);
Assert.Equal(["SQL02 (manuelt)"], (await _repository.GetWindowsServersAsync()).Select(s => s.Name).ToList());
}
[Fact]
public async Task Samples_disk_history_events_and_snapshots_round_trip()
{
var id = await _repository.AddWindowsServerAsync(new NewWindowsServer("AOS01", "aos01", ServerRoles.Application, [], []), null, Now);
await _repository.RecordServerContactAsync(id, new ServerOsInfo { Caption = "Windows Server 2019", Version = "10.0.17763", TotalMemoryMb = 16384, LogicalProcessors = 4,
LastBootUpTime = Now.AddDays(-3) }, "WS-Man", [new ServerAreaStatus(ServerArea.Events, false, "Adgang nægtet", ServerArea.Right(ServerArea.Events))], Now);
await _repository.RecordServerErrorAsync(id, "RPC-serveren er ikke tilgængelig.", Now.AddMinutes(-5));
for (var m = 0; m < 5; m++)
{
await _repository.SaveServerPerformanceAsync(id, new ServerPerformanceSample(Now.AddMinutes(-m), 10 + m, 4000, 16384, 3, 50));
}
await _repository.SaveServerPerformanceAsync(id, new ServerPerformanceSample(Now, 99, 1, 1, 1, 1)); // same minute: kept once
for (var h = 0; h < 60; h++)
{
await _repository.SaveServerDisksAsync(id, Now.AddHours(-h), [new ServerDisk(@"D:\", "Data", "NTFS", 100, 20 + (h * 0.5)), new ServerDisk(@"C:\", null, "NTFS", 80, 40)]);
}
var (groups, _) = ServerEventAnalysis.Group([new ServerEventEntry("System", "disk", 153, 2, Now.AddMinutes(-3), "Disk 1 svarede ikke")], []);
await _repository.SaveServerEventsAsync(id, new ServerEventSnapshot { CapturedAt = Now, ReadUntil = Now, Groups = groups, Recent = groups });
await _repository.SaveServerSnapshotAsync(id, ServerArea.Services, new ServerServiceSnapshot { CapturedAt = Now, Services = [new ServerService("W3SVC", "IIS", "Running", "Auto")] }, Now);
var row = (await _repository.GetWindowsServerAsync(id))!;
Assert.True(row.Reachable);
Assert.Equal(("WS-Man", "Windows Server 2019", 4), (row.Protocol, row.OperatingSystem, row.LogicalProcessors));
Assert.Equal("Adgang nægtet", Assert.Single(row.Areas).Detail);
var performance = await _repository.GetServerPerformanceAsync(id, Now.AddHours(-1));
Assert.Equal(5, performance.Count);
Assert.Equal(10, performance[^1].CpuPercent!.Value, 3);
var history = await _repository.GetServerDiskHistoryAsync(id, Now.AddDays(-14));
Assert.Equal(2, history.Count);
Assert.InRange(history[@"D:\"].Count, 60, 61);
var forecast = DiskForecaster.Forecast(@"D:\", history[@"D:\"], Now);
Assert.Equal(20 / 12d, forecast.DaysUntilFull!.Value, 1); // 12 GB a day, 20 GB free
var events = await _repository.GetServerEventsAsync(id, Now.AddDays(-1), Now);
Assert.Equal(("disk", 153, "Disk", "Disk 1 svarede ikke"), (events[0].Source, events[0].EventId, events[0].Category, events[0].Sample));
var services = await _repository.GetServerSnapshotAsync(id, ServerArea.Services);
Assert.Equal("W3SVC", Assert.Single(services!.Services).Name);
}
private static RuleResult Found(string ruleId, params Finding[] findings) =>
new() { RuleId = ruleId, RuleTitle = ruleId, Outcome = findings.Length == 0 ? RuleOutcome.Passed : RuleOutcome.Findings, Findings = findings };
private static Finding Finding(string ruleId, string subject, Severity severity = Severity.High) => new()
{
RuleId = ruleId, Subject = subject, Severity = severity, Title = "t " + subject, Explanation = "e", Recommendation = "r",
Evidence = [new Evidence("Eksempel", "\\\\fil01\\hemmeligt", Sensitive: true)],
};
[Fact]
public async Task Findings_open_update_resolve_per_server_and_check_and_survive_a_not_measurable_round()
{
var server = await _repository.AddWindowsServerAsync(new NewWindowsServer("AOS01", "aos01", ServerRoles.Application, [], []), null, Now);
var check = await _repository.AddSyntheticCheckAsync(Check("AOS port"), null, Now);
var first = await _repository.SaveScopedRuleResultsAsync(FindingScope.Server, server,
[Found("SQLI-WIN-005", Finding("SQLI-WIN-005", @"disk: D:\"), Finding("SQLI-WIN-005", @"disk: E:\"))], Now);
await _repository.SaveScopedRuleResultsAsync(FindingScope.Check, check, [Found("SQLI-SYN-001", Finding("SQLI-SYN-001", "check"))], Now);
Assert.Equal(2, first.Opened.Count);
// Not measurable (e.g. rights removed): the findings stay open.
await _repository.SaveScopedRuleResultsAsync(FindingScope.Server, server,
[new RuleResult { RuleId = "SQLI-WIN-005", RuleTitle = "x", Outcome = RuleOutcome.NotMeasurable, Reason = "Adgang nægtet" }], Now.AddMinutes(1));
Assert.Equal(2, (await _repository.GetServerFindingsAsync(FindingScope.Server, server)).Count);
Assert.Equal("Adgang nægtet", Assert.Single(await _repository.GetServerRuleStatesAsync(FindingScope.Server, server)).Reason);
var later = await _repository.SaveScopedRuleResultsAsync(FindingScope.Server, server, [Found("SQLI-WIN-005", Finding("SQLI-WIN-005", @"disk: D:\", Severity.Critical))], Now.AddMinutes(2));
Assert.Empty(later.Opened);
Assert.Equal(@"disk: E:\", Assert.Single(later.Resolved).Subject);
var active = await _repository.GetServerFindingsAsync();
Assert.Equal(2, active.Count);
Assert.Equal(("AOS01", Severity.Critical), (active.Single(f => f.Scope == FindingScope.Server).ScopeName, active.Single(f => f.Scope == FindingScope.Server).Severity));
Assert.Equal("AOS port", active.Single(f => f.Scope == FindingScope.Check).ScopeName);
Assert.True(active[0].Evidence[0].Sensitive);
var counts = await _repository.GetServerFindingCountsAsync();
Assert.Equal(1, counts[(FindingScope.Server, server)][Severity.Critical]);
var resolved = Assert.Single(await _repository.GetServerFindingsAsync(FindingScope.Server, server, FindingStatus.Resolved));
Assert.Equal(resolved.FindingId, (await _repository.GetServerFindingAsync(resolved.FindingId))!.FindingId);
// Back again: a new occurrence, with the old one as history.
var again = await _repository.SaveScopedRuleResultsAsync(FindingScope.Server, server,
[Found("SQLI-WIN-005", Finding("SQLI-WIN-005", @"disk: D:\"), Finding("SQLI-WIN-005", @"disk: E:\"))], Now.AddMinutes(3));
var reopened = Assert.Single(again.Opened);
Assert.Single(await _repository.GetServerFindingHistoryAsync((await _repository.GetServerFindingAsync(reopened.FindingId))!));
}
[Fact]
public async Task Check_results_count_failures_in_a_row_and_retention_bounds_everything()
{
var server = await _repository.AddWindowsServerAsync(new NewWindowsServer("AOS01", "aos01", ServerRoles.Application, [], []), null, Now);
var check = await _repository.AddSyntheticCheckAsync(Check("AOS port", server), null, Now);
await _repository.RecordSyntheticCheckResultAsync(check, new SyntheticCheckResult { At = Now.AddMinutes(-10), Success = true, DurationMs = 5, Message = "ok" });
await _repository.RecordSyntheticCheckResultAsync(check, new SyntheticCheckResult
{
At = Now.AddMinutes(-5), Success = false, DurationMs = 15000, Message = "Timeout", ServiceState = "Running",
Details = [new SyntheticCheckDetail("Fejl", new string('x', 3000))],
});
await _repository.RecordSyntheticCheckResultAsync(check, new SyntheticCheckResult { At = Now, Success = false, DurationMs = 15000, Message = "Timeout", Partial = true });
var row = (await _repository.GetSyntheticCheckAsync(check))!;
Assert.Equal((false, 2, "Timeout"), (row.LastSuccess, row.ConsecutiveFailures, row.LastMessage));
var results = await _repository.GetSyntheticCheckResultsAsync(check);
Assert.Equal([false, false, true], results.Select(r => r.Success).ToList());
Assert.Equal("Running", results[1].ServiceState);
Assert.Equal(400, results[1].Details.Single().Value.Length);
Assert.True(results[0].ToResult().Partial);
// Old data goes; removed and paused servers' findings are resolved.
await _repository.SaveServerPerformanceAsync(server, new ServerPerformanceSample(Now.AddDays(-20), 1, 1, 1, 1, 1));
await _repository.SaveServerDisksAsync(server, Now.AddDays(-40), [new ServerDisk(@"C:\", null, null, 1, 1)]);
await _repository.RecordSyntheticCheckResultAsync(check, new SyntheticCheckResult { At = Now.AddDays(-20), Success = true, Message = "gammel" });
await _repository.SaveScopedRuleResultsAsync(FindingScope.Server, server, [Found("SQLI-WIN-001", Finding("SQLI-WIN-001", "cpu"))], Now);
await _repository.SetWindowsServerPausedAsync(server, true, null, Now);
await _repository.RunServerMaintenanceAsync(new ServerRetention(), Now);
Assert.Empty(await _repository.GetServerPerformanceAsync(server, Now.AddDays(-30)));
Assert.Empty(await _repository.GetServerDiskHistoryAsync(server, Now.AddDays(-60)));
Assert.Equal(3, (await _repository.GetSyntheticCheckResultsAsync(check)).Count);
Assert.Empty(await _repository.GetServerFindingsAsync(FindingScope.Server, server));
}
[Fact]
public async Task A_test_or_run_waiting_for_the_agent_is_reused()
{
var web = await ReadOnlyRepositoryAsync();
var server = await _repository.AddWindowsServerAsync(new NewWindowsServer("AOS01", "aos01", ServerRoles.Application, [], []), null, Now);
var other = await _repository.AddWindowsServerAsync(new NewWindowsServer("AOS02", "aos02", ServerRoles.Application, [], []), null, Now);
var first = await web.AddServerRequestAsync(ServerRequestKind.ServerTest, server, "anna", Now);
Assert.Equal(first, await web.AddServerRequestAsync(ServerRequestKind.ServerTest, server, "bo", Now));
Assert.NotEqual(first, await web.AddServerRequestAsync(ServerRequestKind.ServerTest, other, "anna", Now));
Assert.NotEqual(first, await web.AddServerRequestAsync(ServerRequestKind.SyntheticCheckRun, server, "anna", Now));
// Once the agent has taken it, a new one is queued.
var claimed = await _repository.ClaimAgentRequestAsync(Now);
Assert.Equal(first, claimed!.RequestId);
Assert.Equal(server, claimed.TargetId);
Assert.NotEqual(first, await web.AddServerRequestAsync(ServerRequestKind.ServerTest, server, "anna", Now));
}
[Fact]
public async Task Findings_of_paused_and_removed_servers_and_checks_are_not_active()
{
var web = await ReadOnlyRepositoryAsync();
var paused = await _repository.AddWindowsServerAsync(new NewWindowsServer("FIL01", "fil01", ServerRoles.File, [], []), null, Now);
var removed = await _repository.AddWindowsServerAsync(new NewWindowsServer("FIL02", "fil02", ServerRoles.File, [], []), null, Now);
var watched = await _repository.AddWindowsServerAsync(new NewWindowsServer("FIL03", "fil03", ServerRoles.File, [], []), null, Now);
var check = await _repository.AddSyntheticCheckAsync(Check("Port"), null, Now);
foreach (var server in new[] { paused, removed, watched })
{
await _repository.SaveScopedRuleResultsAsync(FindingScope.Server, server, [Found("SQLI-WIN-005", Finding("SQLI-WIN-005", @"disk: D:\"))], Now);
}
await _repository.SaveScopedRuleResultsAsync(FindingScope.Check, check, [Found("SQLI-SYN-001", Finding("SQLI-SYN-001", "check"))], Now);
await web.SetWindowsServerPausedAsync(paused, true, "anna", Now);
await web.RemoveWindowsServerAsync(removed, "anna", Now);
await web.RemoveSyntheticCheckAsync(check, "anna", Now);
var active = await web.GetServerFindingsAsync();
Assert.Equal([watched], active.Select(f => f.ScopeId).ToList());
Assert.True(active[0].ScopeMonitored);
Assert.Equal([(FindingScope.Server, watched)], (await web.GetServerFindingCountsAsync()).Keys.ToList());
// A paused server's own list keeps its latest findings; a removed one's are history only ("all").
var own = Assert.Single(await web.GetServerFindingsAsync(FindingScope.Server, paused));
Assert.True(own.ScopePaused);
Assert.Empty(await web.GetServerFindingsAsync(FindingScope.Server, removed));
Assert.True(Assert.Single(await web.GetServerFindingsAsync(FindingScope.Server, removed, FindingStatus.All)).ScopeRemoved);
Assert.True((await web.GetServerFindingAsync(Assert.Single(await web.GetServerFindingsAsync(FindingScope.Check, check, FindingStatus.All)).FindingId))!.ScopeRemoved);
Assert.Equal(4, (await web.GetServerFindingsAsync(status: FindingStatus.All)).Count);
}
[Fact]
public async Task Findings_come_a_page_at_a_time()
{
var server = await _repository.AddWindowsServerAsync(new NewWindowsServer("FIL01", "fil01", ServerRoles.File, [], []), null, Now);
await _repository.SaveScopedRuleResultsAsync(FindingScope.Server, server,
[Found("SQLI-WIN-005", Finding("SQLI-WIN-005", @"disk: C:\", Severity.Critical), Finding("SQLI-WIN-005", @"disk: D:\"), Finding("SQLI-WIN-005", @"disk: E:\", Severity.Low))], Now);
var first = await _repository.GetServerFindingsAsync(top: 2);
var rest = await _repository.GetServerFindingsAsync(top: 2, offset: 2);
Assert.Equal([@"disk: C:\", @"disk: D:\"], first.Select(f => f.Subject).ToList());
Assert.Equal(@"disk: E:\", Assert.Single(rest).Subject);
}
[Fact]
public async Task Removing_a_server_keeps_its_checks_without_the_server_and_a_removed_check_forgets_its_password()
{
var web = await ReadOnlyRepositoryAsync();
var server = await web.AddWindowsServerAsync(new NewWindowsServer("AOS01", "aos01", ServerRoles.Application, [], []), "anna", Now);
var tcp = await web.AddSyntheticCheckAsync(Check("AOS port", server), "anna", Now);
var sqlCheck = await web.AddSyntheticCheckAsync(Check("SQL", server, [1, 2, 3]), "anna", Now);
await _repository.RecordSyntheticCheckResultAsync(sqlCheck, new SyntheticCheckResult { At = Now, Success = false, Message = "Login failed for user 'axapp'." });
await _repository.SetSyntheticCheckSecretErrorAsync(sqlCheck, "Kan ikke åbne");
Assert.Equal(ConfigurationChangeOutcome.Changed, await web.RemoveWindowsServerAsync(server, "anna", Now));
var row = (await web.GetSyntheticCheckAsync(tcp))!;
Assert.Equal((null, null, null), (row.ServerId, row.ServerName, row.Settings.ServiceName));
Assert.Equal("aos01", row.Settings.Host);
Assert.True(await web.RemoveSyntheticCheckAsync(sqlCheck, "anna", Now));
var removed = await RemovedCheckAsync(sqlCheck);
Assert.Equal((null, null, null, null), (removed.SecretCipher, removed.SecretKeyId, removed.SecretError, removed.LastMessage));
// A late result of the agent does not bring the message back.
await _repository.RecordSyntheticCheckResultAsync(sqlCheck, new SyntheticCheckResult { At = Now, Success = false, Message = "Login failed for user 'axapp'." });
Assert.Null((await RemovedCheckAsync(sqlCheck)).LastMessage);
}
[Fact]
public async Task A_sql_host_removed_by_the_agent_leaves_its_checks_without_the_server()
{
await _repository.SyncSqlHostServersAsync([("sql01.firma.local", "SQL01")], Now);
var host = Assert.Single(await _repository.GetWindowsServersAsync()).ServerId;
var check = await _repository.AddSyntheticCheckAsync(Check("SQL01 port", host), null, Now);
await _repository.SyncSqlHostServersAsync([], Now);
var row = (await _repository.GetSyntheticCheckAsync(check))!;
Assert.Null(row.ServerId);
Assert.Null(row.Settings.ServiceName);
}
[Fact]
public async Task Long_details_are_trimmed_to_fit_and_the_overview_reads_all_servers_at_once()
{
var server = await _repository.AddWindowsServerAsync(new NewWindowsServer("AOS01", "aos01", ServerRoles.Application, [], []), null, Now);
var idle = await _repository.AddWindowsServerAsync(new NewWindowsServer("AOS02", "aos02", ServerRoles.Application, [], []), null, Now);
var check = await _repository.AddSyntheticCheckAsync(Check("Port"), null, Now);
await _repository.RecordSyntheticCheckResultAsync(check, new SyntheticCheckResult
{
At = Now, Success = false, Message = "x", Details = Enumerable.Range(1, 20).Select(i => new SyntheticCheckDetail($"Felt {i}", new string('æ', 400))).ToList(),
});
var details = Assert.Single(await _repository.GetSyntheticCheckResultsAsync(check)).Details;
Assert.InRange(details.Count, 1, 19);
Assert.Equal("Felt 1", details[0].Name);
await _repository.SaveServerPerformanceAsync(server, new ServerPerformanceSample(Now.AddMinutes(-2), 10, 4000, 16384, 1, 1));
await _repository.SaveServerPerformanceAsync(server, new ServerPerformanceSample(Now.AddMinutes(-1), 20, 3000, 16384, 1, 1));
await _repository.SaveServerSnapshotAsync(server, ServerArea.Disks, new ServerDiskSnapshot
{
CapturedAt = Now,
Disks = [new ServerDisk(@"C:\", null, "NTFS", 100, 50), new ServerDisk(@"D:\", null, "NTFS", 100, 5)],
Forecasts = [new DiskForecast(@"C:\", 30, 1, 100, 14, 0.9), new DiskForecast(@"D:\", null, 0, 100, 14, 0.1, "fyldes ikke op")],
}, Now);
var overviews = await _repository.GetWindowsServerOverviewsAsync(Now.AddMinutes(-10));
Assert.Equal(20, overviews[server].Latest!.CpuPercent!.Value, 3);
Assert.Equal(@"D:\", overviews[server].WorstDisk!.Volume);
Assert.Equal(@"C:\", overviews[server].SoonestFull!.Volume);
Assert.Null(overviews[idle].Latest);
Assert.Null(overviews[idle].Disks);
}
/// What a removed check still has stored (the repository does not read removed checks).
private async Task<(byte[]? SecretCipher, string? SecretKeyId, string? SecretError, string? LastMessage)> RemovedCheckAsync(int id)
{
await using var connection = new SqlConnection(sql.DatabaseConnectionString(_database));
await connection.OpenAsync();
await using var command = connection.CreateCommand();
command.CommandText = "SELECT SecretCipher, SecretKeyId, SecretError, LastMessage FROM insight.SyntheticChecks WHERE CheckId = @id;";
command.Parameters.AddWithValue("@id", id);
await using var reader = await command.ExecuteReaderAsync();
Assert.True(await reader.ReadAsync());
return (reader.IsDBNull(0) ? null : (byte[])reader[0], reader.IsDBNull(1) ? null : reader.GetString(1), reader.IsDBNull(2) ? null : reader.GetString(2),
reader.IsDBNull(3) ? null : reader.GetString(3));
}
}