sqlinsight-pro/tests/SQLInsight.IntegrationTests/HubEndToEndTests.cs
Claude 19fbb02045
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 3: data processing, hub retention and deployment
Hub: nightly retention of resolved findings, the submission log and
silent instances; delete-customer erases a customer's data and blocks
its licenses; persistent data protection keys and trusted reverse
proxies. Docker image and compose file with PostgreSQL. The agent
refuses to send the license to a non-https hub. Documentation for the
hub, for data processing (GDPR basis for a data processing agreement)
and the README roadmap for phase 3.

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

108 lines
5.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");
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);
}
}