- Case numbers SUP-000001, SUP-000002 … per customer, unique per customer (new index; a migration step renames any old duplicates first). The portal suggests the next number on the finding page and under the new "Ny sag" on the customer page; an empty field gets the next number, a typed one that is taken is refused. Concurrent creates retry. - SupportCase gains Source (Staff/Customer), description, contact name, e-mail, phone, the customer's Windows user and ClientRequestId. - POST/GET /api/v1/cases for the customer's agent (license with priority support): creates a case idempotently on ClientRequestId, links the finding it came from, lists the customer's cases incl. staff-created. Customer/license creation is shared with the findings endpoint. - Optional mail to support for customer cases (MailKit; prefers AUTH LOGIN as Microsoft 365 needs). A failed mail is logged, never fails the call. deploy-hub.yml writes optional SUPPORT_SMTP_* secrets to .env. - Supportrapport: created, closed, open now, average/median resolution time, oldest open case and customer-created share per customer for a period (default 90 days), plus the cases; CSV for Danish Excel. - Cases page: newest first, customer cases marked, contact/description. - Customer page: findings can be filtered to one instance (?instans=id, ignored for another customer's instance); active findings per instance by severity. - dotnet-ef as a local tool (dotnet-tools.json) for the migration. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
108 lines
5.7 KiB
C#
108 lines
5.7 KiB
C#
using System.Net;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using SQLInsight.Hub.Data;
|
|
|
|
namespace SQLInsight.IntegrationTests;
|
|
|
|
/// <summary>Support cases in the portal: suggested case numbers and the support report.</summary>
|
|
public partial class HubPortalTests
|
|
{
|
|
private async Task<int> CustomerIdAsync(string customer)
|
|
{
|
|
await using var scope = hub.Factory.Services.CreateAsyncScope();
|
|
return await scope.ServiceProvider.GetRequiredService<HubDbContext>().Customers.Where(c => c.Name == customer).Select(c => c.Id).SingleAsync();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task New_case_suggests_the_next_number_and_refuses_a_taken_one()
|
|
{
|
|
var customer = $"Forslag {Guid.NewGuid():N}"[..22];
|
|
var finding = await SeedAsync(customer, hub.License(customer));
|
|
var id = await CustomerIdAsync(customer);
|
|
var client = await SignedInAsync();
|
|
|
|
Assert.Contains("value=\"SUP-000001\"", await client.GetStringAsync($"/portal/fund/{finding}"), StringComparison.Ordinal);
|
|
var created = await PostFormAsync(client, $"/portal/kunder/{id}", $"/portal/actions/customers/{id}/cases", ("reference", ""), ("title", "Telefonopkald"), ("description", "Ringede om backup"));
|
|
Assert.Equal($"/portal/kunder/{id}", created.Headers.Location?.OriginalString);
|
|
var page = await client.GetStringAsync($"/portal/kunder/{id}");
|
|
Assert.Contains("SUP-000001", page, StringComparison.Ordinal);
|
|
Assert.Contains("value=\"SUP-000002\"", page, StringComparison.Ordinal);
|
|
|
|
var duplicate = await PostFormAsync(client, $"/portal/kunder/{id}", $"/portal/actions/customers/{id}/cases", ("reference", "SUP-000001"), ("title", "Igen"));
|
|
Assert.Equal(HttpStatusCode.BadRequest, duplicate.StatusCode);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Support_report_shows_cases_and_exports_csv()
|
|
{
|
|
var customer = $"Rapport {Guid.NewGuid():N}"[..22];
|
|
await SeedAsync(customer, hub.License(customer));
|
|
var id = await CustomerIdAsync(customer);
|
|
var client = await SignedInAsync();
|
|
await PostFormAsync(client, $"/portal/kunder/{id}", $"/portal/actions/customers/{id}/cases", ("reference", ""), ("title", "Til rapporten"));
|
|
|
|
var response = await client.GetAsync($"/portal/rapporter/support?kunde={id}");
|
|
var page = await response.Content.ReadAsStringAsync();
|
|
Assert.True(response.IsSuccessStatusCode, page);
|
|
var csv = await client.GetAsync($"/portal/actions/reports/support.csv?kunde={id}");
|
|
var bytes = await csv.Content.ReadAsByteArrayAsync();
|
|
|
|
Assert.Contains("Til rapporten", page, StringComparison.Ordinal);
|
|
Assert.Contains("median løsningstid", page, StringComparison.Ordinal);
|
|
Assert.Equal("text/csv", csv.Content.Headers.ContentType?.MediaType);
|
|
Assert.Equal([0xEF, 0xBB, 0xBF], bytes[..3]);
|
|
Assert.Contains("SUP-000001;" + customer + ";Til rapporten;SEOSoft;", System.Text.Encoding.UTF8.GetString(bytes), StringComparison.Ordinal);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Customer_page_filters_findings_to_one_instance()
|
|
{
|
|
var customer = $"Instans {Guid.NewGuid():N}"[..22];
|
|
var license = hub.License(customer);
|
|
await SeedAsync(customer, license);
|
|
using (var request = new HttpRequestMessage(HttpMethod.Post, "/api/v1/findings")
|
|
{
|
|
Content = System.Net.Http.Json.JsonContent.Create(HubApiTests.Submission("sql02", DateTimeOffset.UtcNow, ("SQLI-CFG-001", "", "High"))),
|
|
})
|
|
{
|
|
request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("SQLInsight-License", license);
|
|
(await hub.Factory.CreateClient().SendAsync(request)).EnsureSuccessStatusCode();
|
|
}
|
|
|
|
var other = $"Fremmed {Guid.NewGuid():N}"[..22];
|
|
await SeedAsync(other, hub.License(other));
|
|
int id, sql02, foreign;
|
|
await using (var scope = hub.Factory.Services.CreateAsyncScope())
|
|
{
|
|
var db = scope.ServiceProvider.GetRequiredService<HubDbContext>();
|
|
id = await db.Customers.Where(c => c.Name == customer).Select(c => c.Id).SingleAsync();
|
|
sql02 = await db.Instances.Where(i => i.CustomerId == id && i.InstanceKey == "sql02").Select(i => i.Id).SingleAsync();
|
|
foreign = await db.Instances.Where(i => i.Customer!.Name == other).Select(i => i.Id).SingleAsync();
|
|
}
|
|
|
|
var client = await SignedInAsync();
|
|
var all = await client.GetStringAsync($"/portal/kunder/{id}");
|
|
var filtered = await client.GetStringAsync($"/portal/kunder/{id}?instans={sql02}");
|
|
var ignored = await client.GetStringAsync($"/portal/kunder/{id}?instans={foreign}");
|
|
|
|
Assert.Contains("SQLI-DB-007", all, StringComparison.Ordinal);
|
|
Assert.Contains("SQLI-CFG-001", all, StringComparison.Ordinal);
|
|
Assert.Contains($"?instans={sql02}", all, StringComparison.Ordinal);
|
|
Assert.Contains("Aktive fund · sql02", filtered, StringComparison.Ordinal);
|
|
Assert.Contains("Vis alle instanser", filtered, StringComparison.Ordinal);
|
|
Assert.Contains("SQLI-CFG-001", filtered, StringComparison.Ordinal);
|
|
Assert.DoesNotContain("SQLI-DB-007", filtered, StringComparison.Ordinal);
|
|
Assert.Contains("SQLI-DB-007", ignored, StringComparison.Ordinal);
|
|
Assert.Contains("SQLI-CFG-001", ignored, StringComparison.Ordinal);
|
|
Assert.DoesNotContain("Vis alle instanser", ignored, StringComparison.Ordinal);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Support_report_csv_requires_login()
|
|
{
|
|
var response = await Client(hub.Factory).GetAsync("/portal/actions/reports/support.csv");
|
|
|
|
Assert.NotEqual(HttpStatusCode.OK, response.StatusCode);
|
|
}
|
|
}
|