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}];");
}
}
}