using System.Net; using System.Net.Http.Headers; using System.Net.Http.Json; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using SQLInsight.Contracts; using SQLInsight.Hub.Data; using SQLInsight.Hub.Insights; namespace SQLInsight.IntegrationTests; [Trait("Category", "Integration")] public class HubInsightTests(HubFixture hub) : IClassFixture { public const string OldBuild = "15.0.4385.2"; public const string NewBuild = "15.0.4415.2"; internal static FindingsSubmission Submission(string build, string level, DateTimeOffset at, params string[] rules) => new() { SubmissionId = Guid.NewGuid(), AgentVersion = "1.0.0", CollectedAt = at, Instance = new InstanceSnapshot { InstanceKey = "sql01", Release = "SQL Server 2019", ProductVersion = build, ProductUpdateLevel = level }, Rules = rules.Select(r => new RuleOutcomeDto { RuleId = r, Outcome = "Findings" }).ToList(), Findings = rules.Select(r => new FindingDto { RuleId = r, Subject = "", Severity = "High", Title = r, Explanation = "e", Recommendation = "r", FirstSeenAt = at.AddHours(-1), }).ToList(), }; internal static async Task SubmitAsync(HttpClient client, string license, FindingsSubmission submission) { using var request = new HttpRequestMessage(HttpMethod.Post, "/api/v1/findings") { Content = JsonContent.Create(submission) }; request.Headers.Authorization = new AuthenticationHeaderValue("SQLInsight-License", license); (await client.SendAsync(request)).EnsureSuccessStatusCode(); } /// A customer whose instance had the rules' findings on the old build, then updated: went away. internal static async Task UpgradeAsync(HubFixture hub, string customer, string[] before, string[] after) { var client = hub.Factory.CreateClient(); var license = hub.License(customer); var t0 = DateTimeOffset.UtcNow.AddMinutes(-30); await SubmitAsync(client, license, Submission(OldBuild, "CU20", t0, before)); await SubmitAsync(client, license, Submission(NewBuild, "CU21", t0.AddMinutes(15), after)); } private async Task InsightsAsync(string license) { InsightsEndpoint.ClearCache(); using var request = new HttpRequestMessage(HttpMethod.Get, "/api/v1/insights"); request.Headers.Authorization = new AuthenticationHeaderValue("SQLInsight-License", license); var response = await hub.Factory.CreateClient().SendAsync(request); response.EnsureSuccessStatusCode(); return (await response.Content.ReadFromJsonAsync())!; } [Fact] public async Task Findings_that_went_away_after_an_update_at_three_customers_are_shared() { var tag = Guid.NewGuid().ToString("N")[..6]; for (var i = 0; i < 3; i++) { await UpgradeAsync(hub, $"Kunde {tag} {i}", ["SQLI-CFG-005", "SQLI-VER-002", "SQLI-IDX-001"], ["SQLI-IDX-001"]); } await UpgradeAsync(hub, $"Kunde {tag} x", ["SQLI-CFG-003"], []); await UpgradeAsync(hub, $"Kunde {tag} y", ["SQLI-CFG-003"], []); var insights = await InsightsAsync(hub.License($"Kunde {tag} 0")); var shared = Assert.Single(insights.Upgrades, u => u.RuleId == "SQLI-CFG-005"); Assert.Equal((15, NewBuild, "CU21", 3, 3, 0), (shared.Major, shared.ToBuild, shared.ToLevel, shared.Customers, shared.Resolved, shared.Persisted)); Assert.DoesNotContain(insights.Upgrades, u => u.RuleId == "SQLI-VER-002"); Assert.DoesNotContain(insights.Upgrades, u => u.RuleId == "SQLI-CFG-003"); // only two customers Assert.DoesNotContain(insights.Upgrades, u => u.RuleId == "SQLI-IDX-001"); // still there Assert.Equal(3, insights.MinCustomers); } [Fact] public async Task Findings_still_there_a_week_after_the_update_count_against_it() { var tag = Guid.NewGuid().ToString("N")[..6]; for (var i = 0; i < 3; i++) { await UpgradeAsync(hub, $"Stadig {tag} {i}", ["SQLI-STAT-001"], ["SQLI-STAT-001"]); } await using var scope = hub.Factory.Services.CreateAsyncScope(); var db = scope.ServiceProvider.GetRequiredService(); var instances = db.Instances.Where(i => i.Customer!.Name.StartsWith($"Stadig {tag}")).Select(i => i.Id); await db.VersionChanges.Where(v => instances.Contains(v.InstanceId)).ExecuteUpdateAsync(v => v.SetProperty(x => x.ChangedAt, x => x.ChangedAt.AddDays(-10))); await db.Findings.Where(f => instances.Contains(f.InstanceId)).ExecuteUpdateAsync(f => f.SetProperty(x => x.FirstSeenAt, x => x.FirstSeenAt.AddDays(-11))); var effects = await scope.ServiceProvider.GetRequiredService().UpgradeEffectsAsync(CancellationToken.None); var stat = Assert.Single(effects, e => e.RuleId == "SQLI-STAT-001"); Assert.Equal((0, 3), (stat.Resolved, stat.Persisted)); Assert.DoesNotContain((await InsightsAsync(hub.License($"Stadig {tag} 0"))).Upgrades, u => u.RuleId == "SQLI-STAT-001"); } [Fact] public async Task Insights_require_a_license() { Assert.Equal(HttpStatusCode.Unauthorized, (await hub.Factory.CreateClient().GetAsync("/api/v1/insights")).StatusCode); } [Fact] public void Matcher_picks_newer_builds_of_the_same_major_version() { var insights = new HubInsights(DateTimeOffset.UtcNow, 3, [ new UpgradeInsight("SQLI-CFG-005", 15, "15.0.4415.2", "CU21", 3, 3, 0), new UpgradeInsight("SQLI-CFG-005", 15, "15.0.4430.1", "CU24", 5, 5, 1), new UpgradeInsight("SQLI-CFG-005", 16, "16.0.4175.1", "CU20", 9, 9, 0), ]); Assert.Equal(["CU24", "CU21"], HubInsightMatcher.For(insights, "SQLI-CFG-005", "15.0.4385.2").Select(i => i.ToLevel)); Assert.Equal(["CU24"], HubInsightMatcher.For(insights, "SQLI-CFG-005", "15.0.4420.2").Select(i => i.ToLevel)); Assert.Empty(HubInsightMatcher.For(insights, "SQLI-CFG-004", "15.0.4385.2")); Assert.Empty(HubInsightMatcher.For(null, "SQLI-CFG-005", "15.0.4385.2")); } }