using System.Net; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc.Testing; using SQLInsight.Core.Data; using SQLInsight.Core.Findings; using SQLInsight.Core.Instances; using SQLInsight.Repository; using SQLInsight.Rules.Engine; namespace SQLInsight.IntegrationTests; [Collection(SqlServerTestGroup.Name)] [Trait("Category", "Integration")] public class WebTests(SqlServerFixture sql) : IAsyncLifetime { private readonly string _database = "SQLInsightWeb_" + Guid.NewGuid().ToString("N")[..8]; private WebApplicationFactory _factory = null!; private InsightRepository _repository = null!; private long _findingId; private long _deadlockId; public async Task InitializeAsync() { await sql.ExecuteAsSaAsync($"CREATE DATABASE [{_database}];"); var connectionString = sql.DatabaseConnectionString(_database); _repository = new InsightRepository(connectionString); await _repository.MigrateAsync(); var info = new InstanceInfo { ServerName = "SQL01", Version = SqlServerVersion.Parse("15.0.4355.3"), ProductLevel = "RTM", ProductUpdateLevel = "CU28", Edition = "Standard Edition", EngineEdition = EngineEdition.Standard, }; var id = await _repository.UpsertInstanceAsync("prod-sql01", info, "SqlClient", false, DateTimeOffset.UtcNow); await _repository.SaveRuleResultsAsync(id, [ new RuleResult { RuleId = "SQLI-CFG-001", RuleTitle = "max server memory er ikke sat", Outcome = RuleOutcome.Findings, Findings = [ new Finding { RuleId = "SQLI-CFG-001", Severity = Severity.High, Title = "max server memory er ikke sat", Explanation = "SQL Server må bruge al hukommelse ", Recommendation = "Sæt max server memory.", Evidence = [new Evidence("max server memory (MB)", "2147483647")], FixScript = "EXEC sys.sp_configure N'max server memory (MB)', 58983;", References = [new Reference("Server memory", new Uri("https://learn.microsoft.com/sql/"))], }, ], }, new RuleResult { RuleId = "SQLI-DB-011", RuleTitle = "VLF'er", Outcome = RuleOutcome.NotMeasurable, Reason = "Kræver SQL Server 2016 SP2" }, ], DateTimeOffset.UtcNow); await _repository.SaveCollectedAsync(id, "deadlocks", new DeadlockSnapshot { CapturedAt = DateTimeOffset.UtcNow, NewDeadlocks = [new DeadlockEvent { Time = DateTimeOffset.UtcNow, GraphXml = "", Processes = [new DeadlockProcess { Id = "p1", IsVictim = true, SessionId = 60, ClientApp = "Sales.Api" }] }], }); _findingId = (await _repository.GetActiveFindingsAsync(id)).Single().FindingId; _deadlockId = (await _repository.GetDeadlocksAsync(id, DateTimeOffset.UtcNow.AddHours(-1))).Single().DeadlockId; _factory = new WebApplicationFactory().WithWebHostBuilder(b => { b.UseEnvironment("Development"); b.UseSetting("SQLInsight:Repository:ConnectionString", connectionString); b.UseSetting("SQLInsight:Web:Authentication", "None"); b.UseSetting("SQLInsight:Web:TimeZone", "Europe/Copenhagen"); }); } public async Task DisposeAsync() { await _factory.DisposeAsync(); await sql.ExecuteAsSaAsync($"ALTER DATABASE [{_database}] SET SINGLE_USER WITH ROLLBACK IMMEDIATE; DROP DATABASE [{_database}];"); } [Fact] public async Task Overview_lists_instances_and_urgent_findings() { var html = await _factory.CreateClient().GetStringAsync("/"); Assert.Contains("prod-sql01", html, StringComparison.Ordinal); Assert.Contains("SQL Server 2019", html, StringComparison.Ordinal); Assert.Contains("max server memory er ikke sat", html, StringComparison.Ordinal); } [Fact] public async Task Instance_page_shows_findings_and_not_measurable_rules() { var html = await _factory.CreateClient().GetStringAsync("/instanser/prod-sql01"); Assert.Contains("Aktive fund", html, StringComparison.Ordinal); Assert.Contains("SQLI-DB-011", html, StringComparison.Ordinal); Assert.Contains("Kræver SQL Server 2016 SP2", html, StringComparison.Ordinal); Assert.Contains($"/deadlocks/{_deadlockId}.xdl", html, StringComparison.Ordinal); } [Fact] public async Task Rules_not_applicable_to_the_version_are_collapsed_and_not_counted_as_not_measurable() { var id = (await _repository.GetInstanceAsync("prod-sql01"))!.InstanceId; await _repository.SaveRuleResultsAsync(id, [ new RuleResult { RuleId = "SQLI-QS-001", RuleTitle = "Query Store er slået fra", Outcome = RuleOutcome.NotApplicable, Reason = "Ikke relevant for SQL Server 2019: kræver noget nyere." }, new RuleResult { RuleId = "SQLI-QS-002", RuleTitle = "Query Store er skrivebeskyttet", Outcome = RuleOutcome.NotApplicable, Reason = "Ikke relevant for SQL Server 2019: kræver noget nyere." }, ], DateTimeOffset.UtcNow); var overview = (await _repository.GetInstancesAsync()).Single(i => i.Name == "prod-sql01"); Assert.Equal(1, overview.NotMeasurable); // only SQLI-DB-011, a real gap var html = await _factory.CreateClient().GetStringAsync("/instanser/prod-sql01"); var notMeasurable = html[html.IndexOf("Ikke målbart på denne instans", StringComparison.Ordinal)..]; var collapsed = notMeasurable[notMeasurable.IndexOf("
", StringComparison.Ordinal)..]; Assert.Contains("Ikke relevant for SQL Server 2019 (2)", collapsed, StringComparison.Ordinal); Assert.Contains("SQLI-QS-001", collapsed, StringComparison.Ordinal); // The real gap stays visible, before and outside the collapsed list. Assert.DoesNotContain("SQLI-DB-011", collapsed, StringComparison.Ordinal); Assert.Contains("SQLI-DB-011", notMeasurable, StringComparison.Ordinal); } [Fact] public async Task Finding_page_shows_explanation_evidence_and_script_html_encoded() { var html = await _factory.CreateClient().GetStringAsync($"/fund/{_findingId}"); Assert.Contains("Hvorfor er det et problem?", html, StringComparison.Ordinal); Assert.Contains("2147483647", html, StringComparison.Ordinal); Assert.Contains("sp_configure", html, StringComparison.Ordinal); Assert.DoesNotContain("", html, StringComparison.Ordinal); Assert.Contains("<script>", html, StringComparison.Ordinal); } [Fact] public async Task Deadlock_graph_downloads_as_xdl() { var response = await _factory.CreateClient().GetAsync($"/deadlocks/{_deadlockId}.xdl"); Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.Equal($"deadlock-{_deadlockId}.xdl", response.Content.Headers.ContentDisposition?.FileName); Assert.StartsWith("", await response.Content.ReadAsStringAsync(), StringComparison.Ordinal); } [Fact] public async Task Security_headers_are_set() { var response = await _factory.CreateClient().GetAsync("/"); Assert.Contains("default-src 'self'", response.Headers.GetValues("Content-Security-Policy").Single(), StringComparison.Ordinal); Assert.Equal("DENY", response.Headers.GetValues("X-Frame-Options").Single()); } [Fact] public async Task Unknown_instance_and_finding_are_handled() { var client = _factory.CreateClient(); Assert.Contains("Ukendt instans", await client.GetStringAsync("/instanser/findes-ikke"), StringComparison.Ordinal); Assert.Contains("Ukendt fund", await client.GetStringAsync("/fund/999999"), StringComparison.Ordinal); } [Fact] public void Refuses_to_run_without_authentication_outside_development() { using var production = new WebApplicationFactory().WithWebHostBuilder(b => { b.UseEnvironment("Production"); b.UseSetting("SQLInsight:Repository:ConnectionString", sql.DatabaseConnectionString(_database)); b.UseSetting("SQLInsight:Web:Authentication", "None"); }); var ex = Assert.ThrowsAny(() => production.CreateClient()); Assert.Contains("kun tilladt i Development", ex.ToString(), StringComparison.Ordinal); } }