using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.Data.SqlClient;
using SQLInsight.Collectors.Concurrency;
using SQLInsight.Core.Data;
using SQLInsight.Core.Instances;
using SQLInsight.Repository;
namespace SQLInsight.IntegrationTests;
/// Open transactions and long-running statements, as the least-privilege agent sees them.
[Collection(SqlServerTestGroup.Name)]
[Trait("Category", "Integration")]
public class ActivityTests(SqlServerFixture sql)
{
private SqlConnection Open(string application) => new(new SqlConnectionStringBuilder(sql.DatabaseConnectionString(SqlServerFixture.TestDatabase))
{
ApplicationName = application, CommandTimeout = 120,
}.ConnectionString);
[Fact]
public async Task An_open_transaction_and_a_statement_blocked_by_it_are_seen_but_not_the_agent()
{
var table = "Activity_" + Guid.NewGuid().ToString("N")[..6];
await sql.ExecuteAsSaAsync($"USE [{SqlServerFixture.TestDatabase}]; CREATE TABLE dbo.{table} (Id int PRIMARY KEY, Name nvarchar(20)); INSERT dbo.{table} VALUES (1, N'a');");
await using var holder = Open("Lager-job");
await holder.OpenAsync();
await using var transaction = holder.BeginTransaction();
await using (var update = new SqlCommand($"UPDATE dbo.{table} SET Name = N'b' WHERE Id = 1;", holder, transaction))
{
await update.ExecuteNonQueryAsync();
}
await using var reader = Open("Rapport");
await reader.OpenAsync();
var blocked = new SqlCommand($"SELECT Name FROM dbo.{table} WHERE Id = 1;", reader).ExecuteScalarAsync();
await using var agentLike = Open("SQLInsight Pro (test)");
await agentLike.OpenAsync();
await using var agentTransaction = agentLike.BeginTransaction();
await using (var touch = new SqlCommand("SELECT 1;", agentLike, agentTransaction))
{
await touch.ExecuteScalarAsync();
}
try
{
await Task.Delay(1500);
await using var session = await AgentSession.OpenAsync(sql);
var snapshot = await session.CollectAsync(new ActivityCollector(transactionMinutes: 0, requestMinutes: 0));
var open = Assert.Single(snapshot.Transactions, t => t.ProgramName == "Lager-job");
Assert.Equal(SqlServerFixture.TestDatabase, open.DatabaseName);
Assert.True(open.LogBytesUsed > 0);
Assert.Contains(table, open.Statement, StringComparison.Ordinal);
Assert.DoesNotContain(snapshot.Transactions, t => t.ProgramName?.StartsWith("SQLInsight Pro", StringComparison.Ordinal) == true);
var waiting = Assert.Single(snapshot.LongRequests, r => r.ProgramName == "Rapport");
Assert.Equal("LCK_M_S", waiting.WaitType);
Assert.Equal(open.SessionId, waiting.BlockingSessionId);
}
finally
{
await transaction.RollbackAsync();
await agentTransaction.RollbackAsync();
await blocked;
await sql.ExecuteAsSaAsync($"USE [{SqlServerFixture.TestDatabase}]; DROP TABLE dbo.{table};");
}
}
[Fact]
public async Task Page_analysis_and_api_show_transactions_with_sensitive_values_left_out()
{
var database = "SQLInsightAct_" + 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);
await repository.SaveCollectedAsync(id, "activity", new ActivitySnapshot
{
CapturedAt = now,
Transactions = [new OpenTransaction(61, 4711, "user_transaction", now.AddMinutes(-25), 300L * 1024 * 1024, 0, "AXDB", 1, @"KUNDE\anne", "PC01", "Ax32.exe", "sleeping", null, "UPDATE INVENTSUM SET POSTEDQTY = 0")],
});
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");
var api = await client.GetStringAsync("/api/v1/instances/prod-sql01/activity");
var analysis = await client.GetStringAsync("/api/v1/instances/prod-sql01/analysis?from=" + Uri.EscapeDataString(now.AddHours(-1).ToString("o")));
Assert.Contains("UPDATE INVENTSUM", page, StringComparison.Ordinal);
Assert.Contains("AXDB", api, StringComparison.Ordinal);
Assert.DoesNotContain("INVENTSUM", api, StringComparison.Ordinal);
Assert.DoesNotContain("anne", api, StringComparison.Ordinal);
Assert.Contains("\"longTransactions\":[{", analysis, StringComparison.Ordinal);
Assert.DoesNotContain("INVENTSUM", analysis, StringComparison.Ordinal);
Assert.Single(await repository.GetLongTransactionsAsync(id, now.AddHours(-1), now.AddMinutes(1)));
}
finally
{
await sql.ExecuteAsSaAsync($"ALTER DATABASE [{database}] SET SINGLE_USER WITH ROLLBACK IMMEDIATE; DROP DATABASE [{database}];");
}
}
}