sqlinsight-pro/tests/SQLInsight.IntegrationTests/WebTests.cs
Claude 5bb1c37fef
Some checks are pending
CI / Integration (mcr.microsoft.com/mssql/server:2019-latest) (push) Blocked by required conditions
CI / Integration (mcr.microsoft.com/mssql/server:2022-latest) (push) Blocked by required conditions
CI / Integration (mcr.microsoft.com/mssql/server:2017-latest) (push) Blocked by required conditions
CI / Integration (mcr.microsoft.com/mssql/server:2025-latest) (push) Blocked by required conditions
CI / Build og unit tests (push) Waiting to run
Fase 1: on-prem web UI (IIS or Windows service), light design system
- ASP.NET Core Blazor static SSR: overview with KPI tiles, instance page
  (findings, CPU/batch/PLE trends, top waits, file latency, blocking,
  deadlocks with .xdl download, not-measurable rules, resolved), finding
  detail with explanation, evidence, fix script and references
- Light-first design (sidebar, cards, self-hosted Inter, teal accent),
  optional dark theme via toggle; strict CSP, no inline styles
- Windows authentication (Negotiate/IIS) with optional group restriction;
  refuses to run without authentication outside Development
- Hosting: IIS (in-process ASP.NET Core Module), Windows service with
  Kestrel, or systemd; web reads the repository with db_datareader only
- docs/installation.md for repository, permissions, agent and web
- Fix: CPU used by other processes is not derived on Linux (SystemIdle is
  unreliable there); deadlocks are not re-reported or stored twice after
  an agent restart; Dapper sends datetime2 so time comparisons are exact

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E3EExwndy8Z22EK2Qpt6BK
2026-09-29 06:58:51 +00:00

160 lines
7 KiB
C#

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<Program> _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 <script>alert(1)</script>",
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 = "<deadlock><victim-list /></deadlock>", 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<Program>().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 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("<script>alert(1)</script>", html, StringComparison.Ordinal);
Assert.Contains("&lt;script&gt;", 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("<deadlock>", 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<Program>().WithWebHostBuilder(b =>
{
b.UseEnvironment("Production");
b.UseSetting("SQLInsight:Repository:ConnectionString", sql.DatabaseConnectionString(_database));
b.UseSetting("SQLInsight:Web:Authentication", "None");
});
var ex = Assert.ThrowsAny<Exception>(() => production.CreateClient());
Assert.Contains("kun tilladt i Development", ex.ToString(), StringComparison.Ordinal);
}
}