using System.Net; using System.Text.RegularExpressions; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.Data.SqlClient; using SQLInsight.Core.Findings; using SQLInsight.Core.Instances; using SQLInsight.Repository; using SQLInsight.Rules.Engine; namespace SQLInsight.IntegrationTests; /// Support cases in SQLInsight Web: form, prefill from a finding, and filing with only read rights. [Collection(SqlServerTestGroup.Name)] [Trait("Category", "Integration")] public partial class WebSupportTests(SqlServerFixture sql) : IAsyncLifetime { private readonly string _database = "SQLInsightSup_" + Guid.NewGuid().ToString("N")[..8]; private WebApplicationFactory _factory = null!; private InsightRepository _repository = null!; private long _findingId; [GeneratedRegex("name=\"__RequestVerificationToken\"[^>]*value=\"([^\"]+)\"")] private static partial Regex AntiforgeryField(); public async Task InitializeAsync() { await sql.ExecuteAsSaAsync($"CREATE DATABASE [{_database}];"); var connectionString = sql.DatabaseConnectionString(_database); _repository = new InsightRepository(connectionString); await _repository.MigrateAsync(); var id = await _repository.UpsertInstanceAsync("prod-sql01", new InstanceInfo { ServerName = "SQL01", Version = SqlServerVersion.Parse("15.0.4355.3"), ProductLevel = "RTM", Edition = "Standard Edition", EngineEdition = EngineEdition.Standard, }, "SqlClient", false, DateTimeOffset.UtcNow); await _repository.SaveRuleResultsAsync(id, [ new RuleResult { RuleId = "SQLI-CFG-001", RuleTitle = "max server memory er ikke sat", Outcome = RuleOutcome.Findings, Findings = [ new Finding { RuleId = "SQLI-CFG-001", Severity = Severity.High, Title = "max server memory er ikke sat", Explanation = "SQL Server må bruge al hukommelse.", Recommendation = "Sæt max server memory.", Evidence = [new Evidence("max server memory (MB)", "2147483647"), new Evidence("login", "KUNDE\\hemmelig", Sensitive: true)], }, ], }, ], DateTimeOffset.UtcNow); _findingId = (await _repository.GetActiveFindingsAsync(id)).Single().FindingId; _factory = new WebApplicationFactory().WithWebHostBuilder(b => { b.UseEnvironment("Development"); b.UseSetting("SQLInsight:Repository:ConnectionString", connectionString); b.UseSetting("SQLInsight:Web:Authentication", "None"); }); } public async Task DisposeAsync() { await _factory.DisposeAsync(); await sql.ExecuteAsSaAsync($"ALTER DATABASE [{_database}] SET SINGLE_USER WITH ROLLBACK IMMEDIATE; DROP DATABASE [{_database}];"); } private Task AllowSupportAsync() => _repository.SaveHubStatusAsync(new HubStatusRow { Enabled = true, HubUrl = "https://hub.example/", LicenseState = "Valid", LicenseMessage = "Professional til Kunde A/S", Customer = "Kunde A/S", LicensePlan = "Professional", LicenseExpires = DateTime.UtcNow.AddYears(1), UpdatedAt = DateTime.UtcNow, }); [Fact] public async Task Without_a_support_license_there_is_no_form_and_no_menu() { var client = _factory.CreateClient(); var support = await client.GetStringAsync("/support"); var finding = await client.GetStringAsync($"/fund/{_findingId}"); Assert.Contains("Supportsager kræver et abonnement", support, StringComparison.Ordinal); Assert.DoesNotContain("name=\"contactEmail\"", support, StringComparison.Ordinal); Assert.DoesNotContain("data-support-available", finding, StringComparison.Ordinal); Assert.DoesNotContain("/support?fund=", finding, StringComparison.Ordinal); } [Fact] public async Task Finding_offers_a_case_that_is_prefilled_and_filed() { await AllowSupportAsync(); var client = _factory.CreateClient(new WebApplicationFactoryClientOptions { AllowAutoRedirect = false }); var finding = await client.GetStringAsync($"/fund/{_findingId}"); Assert.Contains("data-support-available", finding, StringComparison.Ordinal); Assert.Contains($"data-finding-id=\"{_findingId}\"", finding, StringComparison.Ordinal); Assert.Contains($"href=\"/support?fund={_findingId}\"", finding, StringComparison.Ordinal); Assert.Contains("support-menu", finding, StringComparison.Ordinal); var form = await client.GetStringAsync($"/support?fund={_findingId}"); Assert.Contains("value=\"max server memory er ikke sat\"", form, StringComparison.Ordinal); Assert.Contains("Regel: SQLI-CFG-001", form, StringComparison.Ordinal); Assert.Contains("max server memory (MB): 2147483647", form, StringComparison.Ordinal); Assert.DoesNotContain("hemmelig", form, StringComparison.Ordinal); var token = WebUtility.HtmlDecode(AntiforgeryField().Match(form).Groups[1].Value); using var content = new FormUrlEncodedContent(new Dictionary { ["__RequestVerificationToken"] = token, ["findingId"] = _findingId.ToString(System.Globalization.CultureInfo.InvariantCulture), ["title"] = "max server memory er ikke sat", ["description"] = "Hjælp", ["contactName"] = "Anna", ["contactEmail"] = "anna@kunde.dk", }); var posted = await client.PostAsync("/support", content); Assert.Equal("/support?sendt=1", posted.Headers.Location?.OriginalString); var stored = Assert.Single(await _repository.GetSupportCasesAsync()); Assert.Equal((SupportCaseStatus.Pending, "SQLI-CFG-001", "prod-sql01", _findingId), (stored.Status, stored.RuleId, stored.InstanceName, stored.FindingId!.Value)); Assert.Contains("Sendes", await client.GetStringAsync("/support"), StringComparison.Ordinal); } [Fact] public async Task Invalid_request_is_sent_back_with_the_reason() { await AllowSupportAsync(); var client = _factory.CreateClient(new WebApplicationFactoryClientOptions { AllowAutoRedirect = false }); var token = WebUtility.HtmlDecode(AntiforgeryField().Match(await client.GetStringAsync("/support")).Groups[1].Value); using var content = new FormUrlEncodedContent(new Dictionary { ["__RequestVerificationToken"] = token, ["title"] = "Hjælp", ["contactName"] = "Anna", ["contactEmail"] = "uden-at", }); var posted = await client.PostAsync("/support", content); Assert.StartsWith("/support?fejl=", posted.Headers.Location?.OriginalString, StringComparison.Ordinal); Assert.Empty(await _repository.GetSupportCasesAsync()); } [Fact] public async Task A_read_only_login_can_file_a_request_but_not_change_cases() { var login = "web_" + Guid.NewGuid().ToString("N")[..8]; const string password = "Read-Only-Login-1!"; await sql.ExecuteAsSaAsync($""" CREATE LOGIN [{login}] WITH PASSWORD = N'{password}', CHECK_POLICY = OFF; USE [{_database}]; CREATE USER [{login}] FOR LOGIN [{login}]; ALTER ROLE db_datareader ADD MEMBER [{login}]; """); var readOnly = new InsightRepository(new SqlConnectionStringBuilder(sql.DatabaseConnectionString(_database)) { UserID = login, Password = password, IntegratedSecurity = false, }.ConnectionString); var id = await readOnly.AddSupportRequestAsync(new NewSupportRequest(Guid.NewGuid(), "Hjælp", null, "Anna", "anna@kunde.dk", null, "KUNDE\\anna"), DateTimeOffset.UtcNow); Assert.True(id > 0); await Assert.ThrowsAsync(() => readOnly.MarkSupportRequestSentAsync(id, "SUP-000001", true, null, DateTimeOffset.UtcNow)); // Closing is only a request on the close columns: allowed with the same read-only login. await _repository.MarkSupportRequestSentAsync(id, "SUP-000001", true, null, DateTimeOffset.UtcNow); Assert.Equal(SupportCloseOutcome.Requested, await readOnly.RequestSupportCloseAsync(id, "KUNDE\\anna", "Løst", DateTimeOffset.UtcNow)); Assert.Equal(SupportCaseStatus.CloseRequested, (await readOnly.GetSupportCaseAsync(id))!.Status); await Assert.ThrowsAsync(() => readOnly.MarkSupportClosedAsync(id, DateTime.UtcNow, "Kunde: x", DateTimeOffset.UtcNow)); } [Fact] public async Task Hub_cases_are_merged_into_the_local_list() { var request = Guid.NewGuid(); await _repository.AddSupportRequestAsync(new NewSupportRequest(request, "Fra kunden", null, "Anna", "anna@kunde.dk", null, null), DateTimeOffset.UtcNow); var now = DateTime.UtcNow; await _repository.SyncSupportCasesAsync( [ new HubSupportCase("SUP-000001", "Fra kunden", false, now.AddDays(-1), now, "Customer", request), new HubSupportCase("SUP-000002", "Fra SEOSoft", true, now, null, "Staff", null), ], DateTimeOffset.UtcNow); await _repository.SyncSupportCasesAsync([new HubSupportCase("SUP-000002", "Fra SEOSoft", true, now, null, "Staff", null)], DateTimeOffset.UtcNow); var cases = await _repository.GetSupportCasesAsync(); Assert.Equal(2, cases.Count); Assert.Equal((SupportCaseStatus.Closed, "SUP-000001"), cases.Where(c => c.ClientRequestId == request).Select(c => (c.Status, c.Reference)).Single()); Assert.Equal(SupportCaseStatus.Open, cases.Single(c => c.Reference == "SUP-000002").Status); } }