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
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
123 lines
6.1 KiB
C#
123 lines
6.1 KiB
C#
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<HubFixture>
|
|
{
|
|
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();
|
|
}
|
|
|
|
/// <summary>A customer whose instance had the rules' findings on the old build, then updated: <paramref name="fixedRules"/> went away.</summary>
|
|
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<HubInsights> 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<HubInsights>())!;
|
|
}
|
|
|
|
[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<HubDbContext>();
|
|
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<InsightCalculator>().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"));
|
|
}
|
|
}
|