The detector looks again every 5 minutes at an AX database where the agent lacks part of the read access, so a GRANT takes effect quickly. The AX SQL test checks that only standard AX tables are read. The hub end-to-end test expects no AX rule outcomes without the opt-in. Docs: what 1.5's synthetic AX checks can reuse. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
153 lines
8.6 KiB
C#
153 lines
8.6 KiB
C#
using Microsoft.EntityFrameworkCore;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using Microsoft.Extensions.Logging.Abstractions;
|
|
using Microsoft.Extensions.Options;
|
|
using SQLInsight.Agent;
|
|
using SQLInsight.Agent.Hub;
|
|
using SQLInsight.Agent.Monitoring;
|
|
using SQLInsight.Collectors;
|
|
using SQLInsight.Connectivity;
|
|
using SQLInsight.Contracts;
|
|
using SQLInsight.Hub.Data;
|
|
using SQLInsight.Repository;
|
|
using SQLInsight.Rules;
|
|
using SQLInsight.Rules.Engine;
|
|
|
|
namespace SQLInsight.IntegrationTests;
|
|
|
|
/// <summary>
|
|
/// The whole paid path: agent monitors real SQL Server, queues its state, delivers it to the hub, hub stores it in PostgreSQL.
|
|
/// </summary>
|
|
[Collection(SqlServerTestGroup.Name)]
|
|
[Trait("Category", "Integration")]
|
|
public class HubEndToEndTests(SqlServerFixture sql, HubFixture hub) : IClassFixture<HubFixture>, IAsyncLifetime
|
|
{
|
|
private readonly string _database = "SQLInsightE2E_" + Guid.NewGuid().ToString("N")[..8];
|
|
private InsightRepository _repository = null!;
|
|
|
|
public async Task InitializeAsync()
|
|
{
|
|
await sql.ExecuteAsSaAsync($"CREATE DATABASE [{_database}];");
|
|
_repository = new InsightRepository(sql.DatabaseConnectionString(_database));
|
|
await _repository.MigrateAsync();
|
|
}
|
|
|
|
public Task DisposeAsync() => sql.ExecuteAsSaAsync($"ALTER DATABASE [{_database}] SET SINGLE_USER WITH ROLLBACK IMMEDIATE; DROP DATABASE [{_database}];");
|
|
|
|
private sealed class TestServerClients(Func<HttpClient> create) : IHttpClientFactory
|
|
{
|
|
public HttpClient CreateClient(string name) => create();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Agent_state_reaches_the_hub_with_sensitive_evidence_redacted()
|
|
{
|
|
var options = Options.Create(new HubOptions { Enabled = true, Url = new Uri("http://localhost/"), License = hub.License("E2E A/S") });
|
|
options.Value.TrustedKeys[HubFixture.KeyId] = hub.PublicKey;
|
|
var outbox = new HubOutbox(options, _repository);
|
|
var monitor = new InstanceMonitor(
|
|
new MonitoredInstance
|
|
{
|
|
Name = "e2e-sql01", Server = sql.Server, Authentication = AuthenticationMode.SqlLogin, UserName = SqlServerFixture.AgentLogin,
|
|
Password = SqlServerFixture.AgentPassword, Provider = ConnectionProvider.SqlClient, TrustServerCertificate = true,
|
|
},
|
|
BuiltInCollectors.Create(), new Dictionary<string, TimeSpan>(), new SqlConnectionFactory(), new RuleEngine(BuiltInRules.Create()),
|
|
_repository, NullLogger.Instance, hubOutbox: outbox);
|
|
var sync = new HubSyncService(options, _repository, new TestServerClients(() => hub.Factory.CreateClient()), TimeProvider.System, NullLogger<HubSyncService>.Instance);
|
|
|
|
await monitor.RunCycleAsync(DateTimeOffset.UtcNow, CancellationToken.None);
|
|
var sent = await sync.RunOnceAsync(CancellationToken.None);
|
|
|
|
Assert.Equal(1, sent);
|
|
var status = await _repository.GetHubStatusAsync();
|
|
Assert.Equal("Valid", status!.LicenseState);
|
|
Assert.Equal(0, status.Pending);
|
|
Assert.NotNull(status.LastSentAt);
|
|
|
|
await using var scope = hub.Factory.Services.CreateAsyncScope();
|
|
var db = scope.ServiceProvider.GetRequiredService<HubDbContext>();
|
|
var instance = await db.Instances.Include(i => i.Customer).SingleAsync(i => i.InstanceKey == "e2e-sql01");
|
|
Assert.Equal("E2E A/S", instance.Customer!.Name);
|
|
Assert.Null(instance.ServerName); // not sent by default
|
|
var findings = await db.Findings.Where(f => f.InstanceId == instance.Id && f.ResolvedAt == null).ToListAsync();
|
|
Assert.Contains(findings, f => f.RuleId == "SQLI-CFG-001");
|
|
// Every built-in rule's outcome, except the Dynamics AX rules: no database has AX findings to the hub turned on.
|
|
Assert.True(await db.RuleStates.CountAsync(r => r.InstanceId == instance.Id)
|
|
>= BuiltInRules.Create().Count(r => !Core.Configuration.DynamicsAxFindings.IsDynamicsAxRule(r.Id)));
|
|
Assert.False(await db.RuleStates.AnyAsync(r => r.InstanceId == instance.Id && r.RuleId.StartsWith("SQLI-AX-")));
|
|
|
|
var latest = Assert.Single(await _repository.GetLatestSubmissionsAsync());
|
|
Assert.Equal("e2e-sql01", latest.InstanceName);
|
|
Assert.NotNull(latest.SentAt);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Rejected_license_is_given_up_not_retried_forever()
|
|
{
|
|
var community = hub.License("Nope A/S", SQLInsight.Licensing.LicensePlan.Professional, expires: DateTimeOffset.UtcNow.AddDays(-45));
|
|
var options = Options.Create(new HubOptions { Enabled = true, Url = new Uri("http://localhost/"), License = community });
|
|
options.Value.TrustedKeys[HubFixture.KeyId] = hub.PublicKey;
|
|
await _repository.EnqueueSubmissionAsync("x", Guid.NewGuid(), "{}", DateTimeOffset.UtcNow);
|
|
var sync = new HubSyncService(options, _repository, new TestServerClients(() => hub.Factory.CreateClient()), TimeProvider.System, NullLogger<HubSyncService>.Instance);
|
|
|
|
// Locally the agent sees the license is long expired and does not even try.
|
|
Assert.Equal(0, await sync.RunOnceAsync(CancellationToken.None));
|
|
Assert.Equal("Expired", (await _repository.GetHubStatusAsync())!.LicenseState);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task License_is_never_sent_over_plain_http()
|
|
{
|
|
var options = Options.Create(new HubOptions { Enabled = true, Url = new Uri("http://hub.example.com/"), License = hub.License("Klar A/S") });
|
|
options.Value.TrustedKeys[HubFixture.KeyId] = hub.PublicKey;
|
|
await _repository.EnqueueSubmissionAsync("x", Guid.NewGuid(), "{}", DateTimeOffset.UtcNow);
|
|
var calls = 0;
|
|
var sync = new HubSyncService(options, _repository, new TestServerClients(() => { calls++; return hub.Factory.CreateClient(); }), TimeProvider.System, NullLogger<HubSyncService>.Instance);
|
|
|
|
Assert.Equal(0, await sync.RunOnceAsync(CancellationToken.None));
|
|
Assert.Equal(0, calls);
|
|
Assert.Contains("https", (await _repository.GetHubStatusAsync())!.LastError, StringComparison.Ordinal);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Experience_from_other_customers_reaches_the_customers_web_ui()
|
|
{
|
|
for (var i = 0; i < 3; i++)
|
|
{
|
|
await HubInsightTests.UpgradeAsync(hub, $"Erfaring {i}", ["SQLI-CFG-005"], []);
|
|
}
|
|
|
|
SQLInsight.Hub.Insights.InsightsEndpoint.ClearCache();
|
|
var options = new HubOptions { Enabled = true, Url = new Uri("http://localhost/"), License = hub.License("Modtager A/S") };
|
|
options.TrustedKeys[HubFixture.KeyId] = hub.PublicKey;
|
|
var sync = new HubSyncService(Options.Create(options), _repository, new TestServerClients(() => hub.Factory.CreateClient()), TimeProvider.System, NullLogger<HubSyncService>.Instance);
|
|
Assert.True(await sync.FetchInsightsAsync(hub.Factory.CreateClient(), options, CancellationToken.None));
|
|
|
|
var instanceId = await _repository.UpsertInstanceAsync("prod-sql01", new SQLInsight.Core.Instances.InstanceInfo
|
|
{
|
|
ServerName = "SQL01", Version = SQLInsight.Core.Instances.SqlServerVersion.Parse(HubInsightTests.OldBuild), ProductLevel = "RTM", ProductUpdateLevel = "CU20",
|
|
Edition = "Enterprise Edition", EngineEdition = SQLInsight.Core.Instances.EngineEdition.Enterprise,
|
|
}, "SqlClient", false, DateTimeOffset.UtcNow);
|
|
await _repository.SaveRuleResultsAsync(instanceId,
|
|
[
|
|
new RuleResult
|
|
{
|
|
RuleId = "SQLI-CFG-005", RuleTitle = "Plan cache", Outcome = RuleOutcome.Findings,
|
|
Findings = [new SQLInsight.Core.Findings.Finding { RuleId = "SQLI-CFG-005", Severity = SQLInsight.Core.Findings.Severity.Medium, Title = "Plan cache er fyldt med engangsplaner", Explanation = "e", Recommendation = "r" }],
|
|
},
|
|
], DateTimeOffset.UtcNow);
|
|
var findingId = (await _repository.GetActiveFindingsAsync(instanceId)).Single().FindingId;
|
|
|
|
await using var web = new Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactory<Program>().WithWebHostBuilder(b =>
|
|
{
|
|
Microsoft.AspNetCore.Hosting.HostingAbstractionsWebHostBuilderExtensions.UseEnvironment(b, "Development");
|
|
b.UseSetting("SQLInsight:Repository:ConnectionString", sql.DatabaseConnectionString(_database));
|
|
b.UseSetting("SQLInsight:Web:Authentication", "None");
|
|
});
|
|
var html = await web.CreateClient().GetStringAsync($"/fund/{findingId}");
|
|
|
|
Assert.Contains("Erfaring fra SQLInsight-kunder", html, StringComparison.Ordinal);
|
|
Assert.Contains("Hos <strong>3 kunder</strong>", html, StringComparison.Ordinal);
|
|
Assert.Contains("CU21 (15.0.4415.2)", html, StringComparison.Ordinal);
|
|
}
|
|
}
|