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);
}
}