- AddServerRequestAsync returns a ServerTest/SyntheticCheckRun already waiting for the agent (Pending, same kind and target) instead of queueing another one, like AddAgentRequestAsync; AgentRequestRow carries TargetId. - Active server findings and the overview counts leave out removed servers and checks, and paused ones across all (a paused one's own list keeps its latest findings, as for instances); findings say ScopeRemoved/ScopePaused. GetServerFindingsAsync pages with OFFSET/FETCH. - Removing a server (in the web UI or a SQL Server host by the agent's sync) detaches its synthetic checks in the same transaction (ServerId NULL, serviceName out of the settings); check queries only join servers in use. - Removing a check clears its sealed password, key id, secret error and last message; a late agent result does not write to a removed check. (0018: UPDATE on LastMessage for the web login.) - 0018: settings and service lists as nvarchar(max) (no 500 on long æøå queries); indexes on SampledAt/LastAt/RunAt for the retention deletes. Run details that do not fit are trimmed from the end instead of all dropped. - One query for the overview's latest CPU/memory and disk collection per server. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
400 lines
27 KiB
C#
400 lines
27 KiB
C#
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;
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
[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<InsightRepository> 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<SqlException>(() => web.RecordServerContactAsync(server, new ServerOsInfo { Caption = "x" }, "DCOM", null, Now));
|
|
await Assert.ThrowsAsync<SqlException>(() => web.SaveServerPerformanceAsync(server, new ServerPerformanceSample(Now, 1, 1, 1, 1, 1)));
|
|
await Assert.ThrowsAsync<SqlException>(() => web.RecordSyntheticCheckResultAsync(check, new SyntheticCheckResult { At = Now, Success = true, Message = "ok" }));
|
|
await Assert.ThrowsAsync<SqlException>(() => web.RecordServerAreasAsync(server, []));
|
|
await Assert.ThrowsAsync<SqlException>(() => web.SaveServerSnapshotAsync(server, ServerArea.Services, new ServerServiceSnapshot { CapturedAt = Now }, Now));
|
|
await Assert.ThrowsAsync<SqlException>(() => ExecuteAsWebAsync($"UPDATE insight.WindowsServers SET Host = N'evil' WHERE ServerId = {server}"));
|
|
await Assert.ThrowsAsync<SqlException>(() => ExecuteAsWebAsync($"UPDATE insight.SyntheticChecks SET Kind = 'Sql' WHERE CheckId = {check}"));
|
|
await Assert.ThrowsAsync<SqlException>(() => ExecuteAsWebAsync($"DELETE FROM insight.SyntheticChecks WHERE CheckId = {check}"));
|
|
await Assert.ThrowsAsync<SqlException>(() => 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<SqlException>(() => _repository.AddWindowsServerAsync(new NewWindowsServer("Fil", "FIL01", ServerRoles.File, [], []), null, Now));
|
|
var name = await Assert.ThrowsAsync<SqlException>(() => _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<ServerServiceSnapshot>(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);
|
|
}
|
|
|
|
/// <summary>What a removed check still has stored (the repository does not read removed checks).</summary>
|
|
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));
|
|
}
|
|
}
|