sqlinsight-pro/tests/SQLInsight.IntegrationTests/HubApiTests.cs
Claude 7088efff52
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: Hub portal for vendor staff
Server-rendered portal on the Hub: staff login (PBKDF2 hashes, rate
limited, antiforgery, local-only return URLs), customer overview with
the most widespread problems across customers, customer and finding
pages, support cases linked to findings, and license revocation that
takes effect on the next agent submission. add-staff CLI command.

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

150 lines
7.2 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.Licensing;
namespace SQLInsight.IntegrationTests;
[Trait("Category", "Integration")]
public class HubApiTests(HubFixture hub) : IClassFixture<HubFixture>
{
internal static FindingsSubmission Submission(string instance, DateTimeOffset collectedAt, params (string Rule, string Subject, string Severity)[] findings) => new()
{
SubmissionId = Guid.NewGuid(),
AgentVersion = "1.0.0",
CollectedAt = collectedAt,
Instance = new InstanceSnapshot { InstanceKey = instance, Release = "SQL Server 2019", ProductVersion = "15.0.4490.9", Edition = "Enterprise" },
Rules = findings.Select(f => f.Rule).Distinct().Select(r => new RuleOutcomeDto { RuleId = r, Outcome = "Findings" }).ToList(),
Findings = findings.Select(f => new FindingDto
{
RuleId = f.Rule, Subject = f.Subject, Severity = f.Severity, Title = $"{f.Rule} {f.Subject}", Explanation = "e", Recommendation = "r",
FirstSeenAt = collectedAt.AddHours(-1), Evidence = [new EvidenceDto("Måling", "42")],
}).ToList(),
};
private async Task<HttpResponseMessage> PostAsync(string license, object body)
{
var client = hub.Factory.CreateClient();
using var request = new HttpRequestMessage(HttpMethod.Post, "/api/v1/findings") { Content = JsonContent.Create(body) };
request.Headers.Authorization = new AuthenticationHeaderValue("SQLInsight-License", license);
return await client.SendAsync(request);
}
private async Task<T> QueryAsync<T>(Func<HubDbContext, Task<T>> query)
{
await using var scope = hub.Factory.Services.CreateAsyncScope();
return await query(scope.ServiceProvider.GetRequiredService<HubDbContext>());
}
[Fact]
public async Task Full_state_is_reconciled_idempotently()
{
var license = hub.License("Reconcile A/S");
var t0 = DateTimeOffset.UtcNow.AddMinutes(-30);
var first = Submission("sql01", t0, ("SQLI-CFG-001", "", "High"), ("SQLI-DB-007", "database: Sales", "Critical"));
var response = await PostAsync(license, first);
var again = await PostAsync(license, first);
var second = await PostAsync(license, Submission("sql01", t0.AddMinutes(15), ("SQLI-CFG-001", "", "Medium")));
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.True((await again.Content.ReadFromJsonAsync<SubmissionReceipt>())!.Duplicate);
Assert.Equal(1, (await second.Content.ReadFromJsonAsync<SubmissionReceipt>())!.ActiveFindings);
var findings = await QueryAsync(db => db.Findings.Include(f => f.Instance!).ThenInclude(i => i.Customer)
.Where(f => f.Instance!.Customer!.Name == "Reconcile A/S").ToListAsync());
Assert.Equal(2, findings.Count);
var cfg = Assert.Single(findings, f => f.RuleId == "SQLI-CFG-001");
Assert.Null(cfg.ResolvedAt);
Assert.Equal(2, cfg.Severity); // Medium now
Assert.NotNull(Assert.Single(findings, f => f.RuleId == "SQLI-DB-007").ResolvedAt);
}
[Fact]
public async Task Late_older_submission_is_not_applied()
{
var license = hub.License("Order A/S");
var now = DateTimeOffset.UtcNow;
await PostAsync(license, Submission("sql01", now, ("SQLI-CFG-001", "", "High")));
var late = await PostAsync(license, Submission("sql01", now.AddMinutes(-15)));
Assert.True((await late.Content.ReadFromJsonAsync<SubmissionReceipt>())!.Duplicate);
Assert.Equal(1, await QueryAsync(db => db.Findings.CountAsync(f => f.Instance!.Customer!.Name == "Order A/S" && f.ResolvedAt == null)));
}
[Fact]
public async Task Concurrent_submissions_for_one_instance_do_not_conflict()
{
var license = hub.License("Parallel A/S");
var start = DateTimeOffset.UtcNow.AddMinutes(-10);
var responses = await Task.WhenAll(Enumerable.Range(0, 8).Select(i =>
PostAsync(license, Submission("sql01", start.AddSeconds(i), ("SQLI-CFG-001", "", "High"), ("SQLI-DB-002", $"database: D{i % 2}", "High")))));
Assert.All(responses, r => Assert.Equal(HttpStatusCode.OK, r.StatusCode));
Assert.Equal(1, await QueryAsync(db => db.Findings.CountAsync(f => f.Instance!.Customer!.Name == "Parallel A/S" && f.RuleId == "SQLI-CFG-001" && f.ResolvedAt == null)));
}
[Theory]
[InlineData("garbage", HttpStatusCode.Unauthorized)]
[InlineData("", HttpStatusCode.Unauthorized)]
public async Task Invalid_license_is_rejected(string license, HttpStatusCode expected)
{
Assert.Equal(expected, (await PostAsync(license, Submission("sql01", DateTimeOffset.UtcNow))).StatusCode);
}
[Fact]
public async Task Community_and_long_expired_licenses_cannot_sync()
{
var community = hub.License("Free A/S", LicensePlan.Community);
var expired = hub.License("Old A/S", expires: DateTimeOffset.UtcNow.AddDays(-45));
Assert.Equal(HttpStatusCode.Forbidden, (await PostAsync(community, Submission("sql01", DateTimeOffset.UtcNow))).StatusCode);
Assert.Equal(HttpStatusCode.Forbidden, (await PostAsync(expired, Submission("sql01", DateTimeOffset.UtcNow))).StatusCode);
}
[Fact]
public async Task Revoked_license_is_rejected()
{
var id = Guid.NewGuid().ToString("N");
var license = hub.License("Revoked A/S", id: id);
Assert.Equal(HttpStatusCode.OK, (await PostAsync(license, Submission("sql01", DateTimeOffset.UtcNow))).StatusCode);
await QueryAsync(async db =>
{
var record = await db.Licenses.SingleAsync(l => l.LicenseId == id);
record.RevokedAt = DateTime.UtcNow;
record.RevokedReason = "Kontrakt opsagt";
return await db.SaveChangesAsync();
});
var response = await PostAsync(license, Submission("sql01", DateTimeOffset.UtcNow.AddMinutes(1)));
Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);
Assert.Contains("Kontrakt opsagt", await response.Content.ReadAsStringAsync(), StringComparison.Ordinal);
}
[Fact]
public async Task Instance_limit_is_enforced()
{
var license = hub.License("Small A/S", maxInstances: 1);
Assert.Equal(HttpStatusCode.OK, (await PostAsync(license, Submission("sql01", DateTimeOffset.UtcNow))).StatusCode);
Assert.Equal(HttpStatusCode.OK, (await PostAsync(license, Submission("sql01", DateTimeOffset.UtcNow.AddMinutes(1)))).StatusCode);
Assert.Equal(HttpStatusCode.Forbidden, (await PostAsync(license, Submission("sql02", DateTimeOffset.UtcNow))).StatusCode);
}
[Fact]
public async Task Malformed_submissions_are_rejected()
{
var license = hub.License("Strict A/S");
var badSeverity = Submission("sql01", DateTimeOffset.UtcNow, ("SQLI-CFG-001", "", "Catastrophic"));
Assert.Equal(HttpStatusCode.BadRequest, (await PostAsync(license, new { schemaVersion = 99 })).StatusCode);
Assert.Equal(HttpStatusCode.UnprocessableEntity, (await PostAsync(license, badSeverity)).StatusCode);
}
}