sqlinsight-pro/tests/SQLInsight.IntegrationTests/WebApiTests.cs
Søren Eggert Lundsteen Olsen 4593ee8b09
Some checks are pending
CI / Integration (mcr.microsoft.com/mssql/server:2017-latest) (push) Blocked by required conditions
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:2025-latest) (push) Blocked by required conditions
CI / Build og unit tests (push) Successful in 59s
deploy community / deploy (push) Successful in 0s
deploy hub / deploy (push) Successful in 20s
CI / MSI-installer (push) Successful in 36s
Test the build_info metric against the current version, not 1.0.0
The version moved to 1.0.1, so the metrics test failed. It now reads the
informational version the endpoint itself reports.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-01 11:34:07 +02:00

137 lines
6.5 KiB
C#

using System.Net;
using System.Net.Http.Headers;
using System.Text.Json;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using SQLInsight.Core.Findings;
using SQLInsight.Core.Instances;
using SQLInsight.Repository;
using SQLInsight.Rules.Engine;
using SQLInsight.Web.Api;
namespace SQLInsight.IntegrationTests;
/// <summary>/api/v1 and /metrics with Windows authentication switched on and bearer tokens for machines.</summary>
[Collection(SqlServerTestGroup.Name)]
[Trait("Category", "Integration")]
public class WebApiTests(SqlServerFixture sql) : IAsyncLifetime
{
private const string Token = "sqli_test-token-for-prometheus";
private readonly string _database = "SQLInsightApi_" + Guid.NewGuid().ToString("N")[..8];
private WebApplicationFactory<Program> _factory = null!;
private int _port;
public async Task InitializeAsync()
{
using (var probe = new System.Net.Sockets.TcpListener(IPAddress.Loopback, 0))
{
probe.Start();
_port = ((IPEndPoint)probe.LocalEndpoint).Port;
}
await sql.ExecuteAsSaAsync($"CREATE DATABASE [{_database}];");
var repository = new InsightRepository(sql.DatabaseConnectionString(_database));
await repository.MigrateAsync();
var id = await repository.UpsertInstanceAsync("prod \"sql01\"", new InstanceInfo
{
ServerName = "SQL01", Version = SqlServerVersion.Parse("16.0.4175.1"), ProductLevel = "RTM", ProductUpdateLevel = "CU20",
Edition = "Enterprise Edition", EngineEdition = EngineEdition.Enterprise,
}, "SqlClient", false, DateTimeOffset.UtcNow);
await repository.SaveRuleResultsAsync(id,
[
new RuleResult
{
RuleId = "SQLI-BLK-001", RuleTitle = "Blocking", Outcome = RuleOutcome.Findings,
Findings =
[
new Finding
{
RuleId = "SQLI-BLK-001", Severity = Severity.High, Title = "Session 87 blokerer 14 sessioner", Explanation = "e", Recommendation = "r",
Subject = "session: 87",
Evidence = [new Evidence("Blokerede sessioner", "14"), new Evidence("Head blockers SQL", "UPDATE dbo.Kunder SET Cpr = '010101-1234'", Sensitive: true)],
},
],
},
], DateTimeOffset.UtcNow);
await sql.ExecuteAsSaAsync($"INSERT [{_database}].insight.MetricSamples (InstanceId, Metric, SampledAt, Value) VALUES ({id}, 'cpu.sqlserver', SYSUTCDATETIME(), 42.5);");
_factory = new WebApplicationFactory<Program>().WithWebHostBuilder(b =>
{
b.UseEnvironment("Development");
b.UseSetting("SQLInsight:Repository:ConnectionString", sql.DatabaseConnectionString(_database));
b.UseSetting("SQLInsight:Web:Authentication", "Windows");
b.UseSetting("SQLInsight:Web:AllowedGroups:0", @"DOMÆNE\SQL-Admins");
b.UseSetting("SQLInsight:Web:ApiTokens:prometheus", "sha256:" + ApiTokenHandler.Hash(Token));
b.UseSetting("urls", $"http://127.0.0.1:{_port}");
});
// Negotiate needs a real server (Kestrel), not the in-memory test server.
_factory.UseKestrel();
_factory.StartServer();
}
public async Task DisposeAsync()
{
await _factory.DisposeAsync();
await sql.ExecuteAsSaAsync($"ALTER DATABASE [{_database}] SET SINGLE_USER WITH ROLLBACK IMMEDIATE; DROP DATABASE [{_database}];");
}
private HttpClient Client(string? token)
{
var client = _factory.CreateClient(new WebApplicationFactoryClientOptions { AllowAutoRedirect = false, BaseAddress = new Uri($"http://127.0.0.1:{_port}") });
if (token is not null)
{
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
}
return client;
}
[Fact]
public async Task Metrics_are_served_in_prometheus_format_with_a_valid_token()
{
var response = await Client(Token).GetAsync("/metrics");
var text = await response.Content.ReadAsStringAsync();
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.StartsWith("text/plain", response.Content.Headers.ContentType!.MediaType, StringComparison.Ordinal);
Assert.Contains("sqlinsight_instance_up{instance=\"prod \\\"sql01\\\"\"} 1", text, StringComparison.Ordinal);
Assert.Contains("sqlinsight_findings{instance=\"prod \\\"sql01\\\"\",severity=\"high\"} 1", text, StringComparison.Ordinal);
Assert.Contains("sqlinsight_rule_findings{instance=\"prod \\\"sql01\\\"\",rule=\"SQLI-BLK-001\",severity=\"high\"} 1", text, StringComparison.Ordinal);
Assert.Contains("sqlinsight_metric{instance=\"prod \\\"sql01\\\"\",metric=\"cpu.sqlserver\"} 42.5", text, StringComparison.Ordinal);
Assert.Contains("# TYPE sqlinsight_findings gauge", text, StringComparison.Ordinal);
// The version from Directory.Build.props (or the release tag), whatever it currently is.
var version = typeof(IntegrationEndpoints).Assembly
.GetCustomAttributes(typeof(System.Reflection.AssemblyInformationalVersionAttribute), false)
.Cast<System.Reflection.AssemblyInformationalVersionAttribute>().Single().InformationalVersion.Split('+')[0];
Assert.Contains($"sqlinsight_build_info{{version=\"{version}\"", text, StringComparison.Ordinal);
}
[Fact]
public async Task Findings_api_leaves_out_sensitive_evidence()
{
var json = await Client(Token).GetStringAsync("/api/v1/findings?minSeverity=High");
using var document = JsonDocument.Parse(json);
var finding = Assert.Single(document.RootElement.EnumerateArray());
Assert.Equal("SQLI-BLK-001", finding.GetProperty("ruleId").GetString());
Assert.Contains("[udeladt]", json, StringComparison.Ordinal);
Assert.DoesNotContain("010101-1234", json, StringComparison.Ordinal);
Assert.Equal(0, JsonDocument.Parse(await Client(Token).GetStringAsync("/api/v1/findings?minSeverity=Critical")).RootElement.GetArrayLength());
}
[Theory]
[InlineData(null, "/metrics")]
[InlineData("forkert-token", "/metrics")]
[InlineData("forkert-token", "/api/v1/instances")]
[InlineData(Token, "/")]
[InlineData(Token, "/fund")]
public async Task Without_a_valid_token_or_outside_the_api_access_is_refused(string? token, string path)
{
var response = await Client(token).GetAsync(path);
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
}
}