- 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>
142 lines
6.2 KiB
C#
142 lines
6.2 KiB
C#
using System.Net;
|
|
using System.Net.Http.Headers;
|
|
using System.Net.Http.Json;
|
|
using System.Text.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 HubSupportApiTests(HubFixture hub) : IClassFixture<HubFixture>
|
|
{
|
|
private static SupportCaseRequest Request(Guid? id = null, string title = "Backup fejler") =>
|
|
new(id ?? Guid.NewGuid(), title, "Siden i går", "Anna Andersen", "anna@kunde.dk", "12345678", @"KUNDE\anna");
|
|
|
|
private async Task<HttpResponseMessage> PostAsync(string license, SupportCaseRequest request)
|
|
{
|
|
using var message = new HttpRequestMessage(HttpMethod.Post, "/api/v1/cases") { Content = JsonContent.Create(request) };
|
|
message.Headers.Authorization = new AuthenticationHeaderValue("SQLInsight-License", license);
|
|
return await hub.Factory.CreateClient().SendAsync(message);
|
|
}
|
|
|
|
private async Task<SupportCaseDto> CreateAsync(string license, SupportCaseRequest request)
|
|
{
|
|
var response = await PostAsync(license, request);
|
|
Assert.True(response.IsSuccessStatusCode, await response.Content.ReadAsStringAsync());
|
|
return (await response.Content.ReadFromJsonAsync<SupportCaseDto>(JsonSerializerOptions.Web))!;
|
|
}
|
|
|
|
private static string Name(string prefix) => $"{prefix} {Guid.NewGuid():N}"[..24];
|
|
|
|
[Fact]
|
|
public async Task Customer_cases_are_numbered_SUP_000001_then_SUP_000002()
|
|
{
|
|
var customer = Name("Nummer");
|
|
var license = hub.License(customer);
|
|
|
|
var first = await CreateAsync(license, Request());
|
|
var second = await CreateAsync(license, Request());
|
|
|
|
Assert.Equal("SUP-000001", first.Reference);
|
|
Assert.Equal("SUP-000002", second.Reference);
|
|
Assert.Equal(SupportCaseSources.Customer, first.Source);
|
|
Assert.True(first.IsOpen);
|
|
|
|
await using var scope = hub.Factory.Services.CreateAsyncScope();
|
|
var stored = await scope.ServiceProvider.GetRequiredService<HubDbContext>().SupportCases.SingleAsync(c => c.Customer!.Name == customer && c.Reference == "SUP-000001");
|
|
Assert.Equal(("Anna Andersen", "anna@kunde.dk", @"KUNDE\anna"), (stored.ContactName, stored.ContactEmail, stored.RequestedBy));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task A_retried_request_returns_the_same_case()
|
|
{
|
|
var customer = Name("Igen");
|
|
var license = hub.License(customer);
|
|
var request = Request();
|
|
|
|
var first = await CreateAsync(license, request);
|
|
var again = await CreateAsync(license, request);
|
|
|
|
Assert.Equal(first.Reference, again.Reference);
|
|
await using var scope = hub.Factory.Services.CreateAsyncScope();
|
|
Assert.Equal(1, await scope.ServiceProvider.GetRequiredService<HubDbContext>().SupportCases.CountAsync(c => c.Customer!.Name == customer));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Each_customer_has_its_own_sequence()
|
|
{
|
|
await CreateAsync(hub.License(Name("Første")), Request());
|
|
|
|
var other = await CreateAsync(hub.License(Name("Anden")), Request());
|
|
|
|
Assert.Equal("SUP-000001", other.Reference);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Community_license_cannot_create_cases()
|
|
{
|
|
var response = await PostAsync(hub.License(Name("Gratis"), LicensePlan.Community), Request());
|
|
|
|
Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Invalid_request_is_rejected()
|
|
{
|
|
var response = await PostAsync(hub.License(Name("Ugyldig")), Request() with { ContactEmail = "ingen-snabel-a" });
|
|
|
|
Assert.Equal(HttpStatusCode.UnprocessableEntity, response.StatusCode);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Customer_sees_its_own_cases_including_those_support_created()
|
|
{
|
|
var customer = Name("Liste");
|
|
var license = hub.License(customer);
|
|
await CreateAsync(license, Request(title: "Fra kunden"));
|
|
await CreateAsync(hub.License(Name("Fremmed")), Request(title: "Andens sag"));
|
|
await using (var scope = hub.Factory.Services.CreateAsyncScope())
|
|
{
|
|
var db = scope.ServiceProvider.GetRequiredService<HubDbContext>();
|
|
var customerId = await db.Customers.Where(c => c.Name == customer).Select(c => c.Id).SingleAsync();
|
|
await SQLInsight.Hub.Support.SupportCaseStore.AddAsync(db, new SupportCase
|
|
{
|
|
CustomerId = customerId, Reference = string.Empty, Title = "Fra SEOSoft", CreatedAt = DateTime.UtcNow, IsOpen = false, ClosedAt = DateTime.UtcNow,
|
|
});
|
|
}
|
|
|
|
using var message = new HttpRequestMessage(HttpMethod.Get, "/api/v1/cases");
|
|
message.Headers.Authorization = new AuthenticationHeaderValue("SQLInsight-License", license);
|
|
var cases = (await (await hub.Factory.CreateClient().SendAsync(message)).Content.ReadFromJsonAsync<List<SupportCaseDto>>(JsonSerializerOptions.Web))!;
|
|
|
|
Assert.Equal(["Fra SEOSoft", "Fra kunden"], cases.Select(c => c.Title));
|
|
Assert.Equal("SUP-000002", cases[0].Reference);
|
|
Assert.False(cases[0].IsOpen);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Case_created_from_a_finding_is_linked_to_it()
|
|
{
|
|
var customer = Name("Fund");
|
|
var license = hub.License(customer);
|
|
using (var submit = new HttpRequestMessage(HttpMethod.Post, "/api/v1/findings")
|
|
{
|
|
Content = JsonContent.Create(HubApiTests.Submission("sql01", DateTimeOffset.UtcNow, ("SQLI-DB-007", "database: Sales", "Critical"))),
|
|
})
|
|
{
|
|
submit.Headers.Authorization = new AuthenticationHeaderValue("SQLInsight-License", license);
|
|
(await hub.Factory.CreateClient().SendAsync(submit)).EnsureSuccessStatusCode();
|
|
}
|
|
|
|
var created = await CreateAsync(license, Request() with { InstanceName = "sql01", RuleId = "SQLI-DB-007", Subject = "database: Sales" });
|
|
|
|
await using var scope = hub.Factory.Services.CreateAsyncScope();
|
|
var finding = await scope.ServiceProvider.GetRequiredService<HubDbContext>().Findings.Include(f => f.SupportCase)
|
|
.SingleAsync(f => f.Instance!.Customer!.Name == customer);
|
|
Assert.Equal(created.Reference, finding.SupportCase?.Reference);
|
|
}
|
|
}
|