sqlinsight-pro/tests/SQLInsight.IntegrationTests/HubEndToEndTests.cs
Claude 4269f6f9e8
Some checks are pending
CI / Build og unit tests (push) Waiting to run
CI / Integration (mcr.microsoft.com/mssql/server:2017-latest) (push) Blocked by required conditions
CI / Integration (mcr.microsoft.com/mssql/server:2019-latest) (push) Blocked by required conditions
CI / Integration (mcr.microsoft.com/mssql/server:2022-latest) (push) Blocked by required conditions
CI / Integration (mcr.microsoft.com/mssql/server:2025-latest) (push) Blocked by required conditions
Fase 4: experience across customers
The hub records when an instance changes build and works out which
findings went away within three days of an update and which were still
there a week later (version rules excluded). Staff see every effect in
the portal; effects seen at three or more customers, resolved more
often than not, are served to licensed agents as anonymous counts
(rule, major version, build, CU, customers, instances). The agent
fetches them every six hours and the web UI shows them on matching
findings: 'at 3 customers this went away after updating to CU21'.
Data processing documentation covers the aggregation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E3EExwndy8Z22EK2Qpt6BK
2026-09-29 10:33:37 +00:00

150 lines
8.2 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");
Assert.True(await db.RuleStates.CountAsync(r => r.InstanceId == instance.Id) >= BuiltInRules.Create().Count);
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);
}
}