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