using System.Net.Http.Json; using System.Text.Json; using DotNet.Testcontainers.Builders; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using SQLInsight.Agent.Reports; using SQLInsight.Agent.Updates; using SQLInsight.Core.Findings; using SQLInsight.Notifications; using SQLInsight.Core.Instances; using SQLInsight.Reports; using SQLInsight.Repository; using SQLInsight.Rules.Engine; namespace SQLInsight.IntegrationTests; [Collection(SqlServerTestGroup.Name)] [Trait("Category", "Integration")] public class ReportTests(SqlServerFixture sql) : IAsyncLifetime { private readonly string _database = "SQLInsightRpt_" + Guid.NewGuid().ToString("N")[..8]; private InsightRepository _repository = null!; public async Task InitializeAsync() { await sql.ExecuteAsSaAsync($"CREATE DATABASE [{_database}];"); _repository = new InsightRepository(sql.DatabaseConnectionString(_database)); await _repository.MigrateAsync(); async Task Instance(string name, string version, string cu, params (string Rule, RuleOutcome Outcome, Severity Severity, string Title)[] rules) { var id = await _repository.UpsertInstanceAsync(name, new InstanceInfo { ServerName = name.ToUpperInvariant(), Version = SqlServerVersion.Parse(version), ProductLevel = "RTM", ProductUpdateLevel = cu, Edition = "Enterprise Edition", EngineEdition = EngineEdition.Enterprise, }, "SqlClient", false, DateTimeOffset.UtcNow); await _repository.SaveRuleResultsAsync(id, rules.Select(r => new RuleResult { RuleId = r.Rule, RuleTitle = r.Title, Outcome = r.Outcome, Findings = r.Outcome == RuleOutcome.Findings ? [new Finding { RuleId = r.Rule, Severity = r.Severity, Title = r.Title, Explanation = "Forklaring.", Recommendation = "Anbefaling." }] : [], }).ToList(), DateTimeOffset.UtcNow); } await Instance("prod-sql01", "15.0.4385.2", "CU20", ("SQLI-VER-001", RuleOutcome.Passed, Severity.Info, "Supportstatus"), ("SQLI-VER-002", RuleOutcome.Findings, Severity.High, "SQL Server 2019 kører 15.0.4385.2; seneste er CU32"), ("SQLI-DB-007", RuleOutcome.Findings, Severity.Critical, "Ingen fuld backup af Økonomi i 9 dage"), ("SQLI-DB-009", RuleOutcome.Passed, Severity.Info, "CHECKDB"), ("SQLI-CFG-008", RuleOutcome.Passed, Severity.Info, "xp_cmdshell")); await Instance("legacy-sql05", "13.0.6300.2", "SP3", ("SQLI-VER-001", RuleOutcome.Findings, Severity.High, "SQL Server 2016 er ude af udvidet support om 3 uger"), ("SQLI-DB-007", RuleOutcome.Passed, Severity.Info, "Backup"), ("SQLI-DB-009", RuleOutcome.NotMeasurable, Severity.Info, "CHECKDB")); } public Task DisposeAsync() => sql.ExecuteAsSaAsync($"ALTER DATABASE [{_database}] SET SINGLE_USER WITH ROLLBACK IMMEDIATE; DROP DATABASE [{_database}];"); private Task LoadAsync() => ReportData.LoadAsync(_repository, "Nordisk Logistik A/S", DateTimeOffset.UtcNow.AddDays(-30), DateTimeOffset.UtcNow.AddMinutes(1), TimeZoneInfo.FindSystemTimeZoneById("Europe/Copenhagen"), TimeProvider.System, CancellationToken.None); [Fact] public async Task Report_is_a_pdf_with_controls_and_a_plain_language_summary() { var data = await LoadAsync(); var pdf = ManagementReport.Render(data); if (Environment.GetEnvironmentVariable("SQLINSIGHT_REPORT_OUT") is { Length: > 0 } path) { await File.WriteAllBytesAsync(path, pdf); } Assert.Equal("%PDF-", System.Text.Encoding.ASCII.GetString(pdf, 0, 5)); var summary = ManagementReport.SummarySentences(data); Assert.Contains(summary, s => s.StartsWith("Der er 1 kritisk fund, som bør håndteres straks, og 2 fund med høj alvor.", StringComparison.Ordinal)); Assert.Contains(summary, s => s.StartsWith("1 af 2 instanser kører en SQL Server-version", StringComparison.Ordinal)); Assert.Contains(summary, s => s.StartsWith("1 instans har databaser uden en nylig fuld backup", StringComparison.Ordinal)); Assert.Contains(summary, s => s == "I perioden er der kommet 3 nye fund til (3 kritiske eller høje), og ingen er løst."); var backup = ComplianceControl.All.Single(c => c.Name == "Fuld backup"); var integrity = ComplianceControl.All.Single(c => c.Name == "Integritetstjek"); Assert.Equal(ControlStatus.NotMet, backup.StatusOn("prod-sql01", data.RuleStates)); Assert.Equal(ControlStatus.Met, backup.StatusOn("legacy-sql05", data.RuleStates)); Assert.Equal(ControlStatus.Met, integrity.StatusOn("prod-sql01", data.RuleStates)); Assert.Equal(ControlStatus.NotMeasured, integrity.StatusOn("legacy-sql05", data.RuleStates)); Assert.Equal(ControlStatus.NotMeasured, ComplianceControl.All.Single(c => c.Name == "Log-backup").StatusOn("prod-sql01", data.RuleStates)); } [Fact] public async Task Web_serves_the_report_as_pdf() { await using var web = new WebApplicationFactory().WithWebHostBuilder(b => { b.UseEnvironment("Development"); b.UseSetting("SQLInsight:Repository:ConnectionString", sql.DatabaseConnectionString(_database)); b.UseSetting("SQLInsight:Web:Authentication", "None"); b.UseSetting("SQLInsight:Web:Organization", "Nordisk Logistik A/S"); }); var response = await web.CreateClient().GetAsync("/rapporter/driftsrapport.pdf?dage=90"); var page = await web.CreateClient().GetStringAsync("/rapporter"); Assert.Equal("application/pdf", response.Content.Headers.ContentType!.MediaType); Assert.Equal("%PDF-", System.Text.Encoding.ASCII.GetString(await response.Content.ReadAsByteArrayAsync(), 0, 5)); Assert.Contains("Nordisk Logistik A/S", page, StringComparison.Ordinal); } [Fact] public async Task Monthly_report_is_mailed_once_with_the_pdf_attached() { var mailpit = new ContainerBuilder("axllent/mailpit:latest") .WithPortBinding(1025, true) .WithPortBinding(8025, true) .WithWaitStrategy(Wait.ForUnixContainer().UntilHttpRequestIsSucceeded(r => r.ForPort(8025).ForPath("/livez"))) .Build(); await mailpit.StartAsync(); var data = Path.Combine(Path.GetTempPath(), "sqlinsight-report-" + Guid.NewGuid().ToString("N")); try { var notifications = new NotificationOptions(); notifications.Email.Host = mailpit.Hostname; notifications.Email.Port = mailpit.GetMappedPublicPort(1025); notifications.Email.Security = "None"; notifications.Email.From = "sqlinsight@test.local"; var reports = new ReportOptions { Organization = "Nordisk Logistik A/S", TimeZone = "Europe/Copenhagen" }; reports.Monthly.Enabled = true; reports.Monthly.To.Add("it-chef@test.local"); var service = new MonthlyReportService(Options.Create(reports), Options.Create(notifications), Options.Create(new UpdateOptions { DataDirectory = data }), _repository, TimeProvider.System, NullLogger.Instance); Assert.True(await service.RunOnceAsync(CancellationToken.None)); Assert.False(await service.RunOnceAsync(CancellationToken.None)); using var http = new HttpClient { BaseAddress = new Uri($"http://{mailpit.Hostname}:{mailpit.GetMappedPublicPort(8025)}/") }; var messages = (await http.GetFromJsonAsync("api/v1/messages")).GetProperty("messages"); Assert.Equal(1, messages.GetArrayLength()); Assert.StartsWith("SQLInsight: Driftsrapport for Nordisk Logistik A/S, ", messages[0].GetProperty("Subject").GetString(), StringComparison.Ordinal); var message = await http.GetFromJsonAsync($"api/v1/message/{messages[0].GetProperty("ID").GetString()}"); var attachment = message.GetProperty("Attachments")[0]; Assert.Equal("application/pdf", attachment.GetProperty("ContentType").GetString()); Assert.Contains("kritisk fund", message.GetProperty("Text").GetString(), StringComparison.Ordinal); } finally { await mailpit.DisposeAsync(); if (Directory.Exists(data)) { Directory.Delete(data, recursive: true); } } } }