using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.Data.SqlClient; using SQLInsight.Collectors.Instance; using SQLInsight.Core.Data; using SQLInsight.Core.Instances; using SQLInsight.Repository; namespace SQLInsight.IntegrationTests; /// The error log as the least-privilege agent sees it: ring buffer without ALTER TRACE, default trace with it. [Collection(SqlServerTestGroup.Name)] [Trait("Category", "Integration")] public class ErrorLogTests(SqlServerFixture sql) { private static async Task UntilAsync(AgentSession session, ErrorLogCollector collector, ErrorLogSnapshot previous, Func done, int seconds = 30) { var latest = previous; // SQL Server writes the default trace in buffers: events can take a while to become readable. for (var i = 0; i < seconds; i++) { await Task.Delay(1000); latest = await session.CollectAsync(collector, latest with { Recent = latest.Recent }); if (done(latest)) { break; } } return latest; } [Fact] public async Task Without_alter_trace_failed_logins_come_from_the_ring_buffer_with_the_reason() { await using var session = await AgentSession.OpenAsync(sql); var collector = new ErrorLogCollector(); var first = await session.CollectAsync(collector); for (var i = 0; i < 3; i++) { var wrong = new SqlConnectionStringBuilder(sql.AgentConnectionString) { Password = "forkert-" + i, Pooling = false }; await using var connection = new SqlConnection(wrong.ConnectionString); await Assert.ThrowsAsync(() => connection.OpenAsync()); } var latest = await UntilAsync(session, collector, first, s => s.Recent.Any(g => g.Category == ErrorLogCategory.LoginFailed && g.Count >= 3)); Assert.Equal(ErrorLogSource.RingBuffer, first.Source); Assert.Contains("ALTER TRACE", first.Coverage, StringComparison.Ordinal); var logins = Assert.Single(latest.Recent, g => g.Category == ErrorLogCategory.LoginFailed); Assert.True(logins.Count >= 3); Assert.Contains("state 8", logins.Sample, StringComparison.Ordinal); } [Fact] public async Task With_alter_trace_the_error_log_and_failed_logins_are_read_from_the_default_trace() { await sql.ExecuteAsSaAsync($"GRANT ALTER TRACE TO [{SqlServerFixture.AgentLogin}];"); try { await using var session = await AgentSession.OpenAsync(sql); var collector = new ErrorLogCollector(loginFailureBurst: 3); var first = await session.CollectAsync(collector); await Assert.ThrowsAsync(() => sql.ExecuteAsSaAsync("RAISERROR (N'SQLInsight test written to the error log', 20, 1) WITH LOG;")); for (var i = 0; i < 4; i++) { var wrong = new SqlConnectionStringBuilder(sql.AgentConnectionString) { Password = "forkert-" + i, Pooling = false }; await using var connection = new SqlConnection(wrong.ConnectionString); await Assert.ThrowsAsync(() => connection.OpenAsync()); } var latest = await UntilAsync(session, collector, first, s => s.Recent.Any(g => g.Error == 50000) && s.Recent.Any(g => g.Category == ErrorLogCategory.LoginFailed && g.Count >= 4), seconds: 120); Assert.Equal(ErrorLogSource.DefaultTrace, latest.Source); Assert.Contains("securityadmin", latest.Coverage, StringComparison.Ordinal); var logged = Assert.Single(latest.Recent, g => g.Error == 50000); Assert.Contains("SQLInsight test written to the error log", logged.Sample, StringComparison.Ordinal); Assert.True(latest.Recent.Single(g => g.Category == ErrorLogCategory.LoginFailed).Count >= 4); } finally { await sql.ExecuteAsSaAsync($"REVOKE ALTER TRACE FROM [{SqlServerFixture.AgentLogin}];"); } } [Fact] public async Task Page_and_api_show_events_with_the_sample_left_out_of_the_api() { var database = "SQLInsightLog_" + Guid.NewGuid().ToString("N")[..8]; await sql.ExecuteAsSaAsync($"CREATE DATABASE [{database}];"); try { var repository = new InsightRepository(sql.DatabaseConnectionString(database)); await repository.MigrateAsync(); var now = DateTimeOffset.UtcNow; var id = await repository.UpsertInstanceAsync("prod-sql01", new InstanceInfo { ServerName = "SQL01", Version = SqlServerVersion.Parse("16.0.4135.4"), ProductLevel = "RTM", Edition = "Standard Edition", EngineEdition = EngineEdition.Standard, }, "SqlClient", false, now); var groups = new[] { new ErrorLogGroup(ErrorLogCategory.Corruption, 824, 24, 2, now.AddMinutes(-4), now.AddMinutes(-1), "incorrect checksum in AX.mdf") }; await repository.SaveCollectedAsync(id, "error-log", new ErrorLogSnapshot { CapturedAt = now, Source = ErrorLogSource.DefaultTrace, ReadUntil = now, Groups = groups, Recent = groups, }); await using var factory = new WebApplicationFactory().WithWebHostBuilder(b => { b.UseEnvironment("Development"); b.UseSetting("SQLInsight:Repository:ConnectionString", sql.DatabaseConnectionString(database)); b.UseSetting("SQLInsight:Web:Authentication", "None"); }); var client = factory.CreateClient(); var page = await client.GetStringAsync("/instanser/prod-sql01/fejllog"); var api = await client.GetStringAsync("/api/v1/instances/prod-sql01/error-log"); Assert.Contains("Beskadigede sider", page, StringComparison.Ordinal); Assert.Contains("incorrect checksum", page, StringComparison.Ordinal); Assert.Contains("824", api, StringComparison.Ordinal); Assert.Contains("[udeladt]", api, StringComparison.Ordinal); Assert.DoesNotContain("incorrect checksum", api, StringComparison.Ordinal); } finally { await sql.ExecuteAsSaAsync($"ALTER DATABASE [{database}] SET SINGLE_USER WITH ROLLBACK IMMEDIATE; DROP DATABASE [{database}];"); } } }