8e81572386
Add revision-safe service and template APIs, channel mapping, and validated admin controls. Ref: IT-1601
138 lines
8.4 KiB
C#
138 lines
8.4 KiB
C#
using System.Net;
|
|
using System.Net.Http.Json;
|
|
using System.Text.Json;
|
|
using System.Text.RegularExpressions;
|
|
using HrynCo.NotificationService.DAL.Abstract.Repositories;
|
|
using Microsoft.AspNetCore.TestHost;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
|
|
namespace HrynCo.NotificationService.Web.IntegrationTests;
|
|
|
|
public sealed class AdminDeletionTests(TemplateDatabaseFixture fixture) : IClassFixture<TemplateDatabaseFixture>
|
|
{
|
|
[Fact]
|
|
public async Task DefinitionDeleteRequiresAntiforgeryAndConfirmation_AndPreservesFilter()
|
|
{
|
|
await using var context = await DefinitionApiTestContext.CreateAsync(fixture);
|
|
using var client = context.App.CreateClient(new() { AllowAutoRedirect = false });
|
|
await context.UpsertAsync(DefinitionApiTestContext.Body(null, "en", "uk"));
|
|
var definition = await context.ReadAsync();
|
|
var url = $"/admin/services/{context.ServiceId}/templates/{definition.GetProperty("id").GetGuid()}/delete";
|
|
var html = await client.GetStringAsync(url + "?keyFilter=Welcome");
|
|
Assert.Contains("2 translation(s)", html);
|
|
Assert.Contains("Languages: en, uk", html);
|
|
var fields = TemplateEditorTestFields.Read(html);
|
|
Assert.Equal(HttpStatusCode.BadRequest, (await client.PostAsync(url, new FormUrlEncodedContent(new Dictionary<string, string> { ["ConfirmDelete"] = "true" }))).StatusCode);
|
|
var unconfirmed = await (await client.PostAsync(url, new FormUrlEncodedContent(fields))).Content.ReadAsStringAsync();
|
|
Assert.Contains("Confirm deletion", unconfirmed);
|
|
AssertConfirmationReset(unconfirmed);
|
|
fields = TemplateEditorTestFields.Read(unconfirmed);
|
|
fields["ConfirmDelete"] = "true";
|
|
fields["Key"] = "Different";
|
|
var deleted = await client.PostAsync(url, new FormUrlEncodedContent(fields));
|
|
Assert.Equal(HttpStatusCode.Redirect, deleted.StatusCode);
|
|
Assert.Equal($"/admin/services/{context.ServiceId}?key=Welcome", deleted.Headers.Location!.OriginalString);
|
|
Assert.Equal(HttpStatusCode.NotFound, (await client.GetAsync(url)).StatusCode);
|
|
Assert.Equal(HttpStatusCode.OK, (await client.GetAsync($"/admin/services/{context.ServiceId}")).StatusCode);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task StaleDefinitionConfirmationRefreshesCountRevisionAndUncheckedConfirmation()
|
|
{
|
|
await using var context = await DefinitionApiTestContext.CreateAsync(fixture);
|
|
await context.UpsertAsync(DefinitionApiTestContext.Body(null));
|
|
var definition = await context.ReadAsync();
|
|
var url = $"/admin/services/{context.ServiceId}/templates/{definition.GetProperty("id").GetGuid()}/delete";
|
|
var fields = TemplateEditorTestFields.Read(await context.Client.GetStringAsync(url));
|
|
var oldRevision = fields["Revision"];
|
|
await context.UpsertAsync(DefinitionApiTestContext.Body(definition.GetProperty("revision").GetGuid(), "en"));
|
|
fields["ConfirmDelete"] = "true";
|
|
var html = await (await context.Client.PostAsync(url, new FormUrlEncodedContent(fields))).Content.ReadAsStringAsync();
|
|
Assert.Contains("revision changed", html);
|
|
Assert.Contains("1 translation(s)", html);
|
|
Assert.NotEqual(oldRevision, TemplateEditorTestFields.Read(html)["Revision"]);
|
|
AssertConfirmationReset(html);
|
|
Assert.Single((await context.ReadAsync()).GetProperty("translations").EnumerateArray());
|
|
var wrongScope = $"/admin/services/{Guid.NewGuid()}/templates/{definition.GetProperty("id").GetGuid()}/delete";
|
|
Assert.Equal(HttpStatusCode.NotFound, (await context.Client.GetAsync(wrongScope)).StatusCode);
|
|
Assert.Equal(HttpStatusCode.NotFound, (await context.Client.PostAsync(wrongScope, new FormUrlEncodedContent(fields))).StatusCode);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ServiceDeleteIsBlockedByEmptyDefinition_AndRequiresFreshConfirmationAfterRename()
|
|
{
|
|
await using var context = await DefinitionApiTestContext.CreateAsync(fixture);
|
|
using var client = context.App.CreateClient(new() { AllowAutoRedirect = false });
|
|
await context.UpsertAsync(DefinitionApiTestContext.Body(null));
|
|
var url = $"/admin/services/{context.ServiceId}/delete";
|
|
var blocked = await client.GetStringAsync(url);
|
|
Assert.Contains("1 template definition(s)", blocked);
|
|
Assert.Contains("Email channels are retained", blocked);
|
|
var fields = TemplateEditorTestFields.Read(blocked);
|
|
fields["ConfirmDelete"] = "true";
|
|
Assert.Contains("Remove every template definition", await (await client.PostAsync(url, new FormUrlEncodedContent(fields))).Content.ReadAsStringAsync());
|
|
var definition = await context.ReadAsync();
|
|
await client.DeleteAsync(context.DefinitionUrl() + "&revision=" + definition.GetProperty("revision").GetGuid());
|
|
fields = TemplateEditorTestFields.Read(await client.GetStringAsync(url));
|
|
var oldRevision = fields["Revision"];
|
|
using (var scope = context.App.Services.CreateScope())
|
|
{
|
|
var services = scope.ServiceProvider.GetRequiredService<INotificationServiceRepository>();
|
|
var service = (await services.GetAsync(context.ServiceId))!;
|
|
service.Name = "Renamed Before Delete";
|
|
await services.SaveAsync(service, false);
|
|
}
|
|
fields["ConfirmDelete"] = "true";
|
|
var stale = await (await client.PostAsync(url, new FormUrlEncodedContent(fields))).Content.ReadAsStringAsync();
|
|
Assert.Contains("revision changed", stale);
|
|
Assert.Contains("Renamed Before Delete", stale);
|
|
Assert.NotEqual(oldRevision, TemplateEditorTestFields.Read(stale)["Revision"]);
|
|
AssertConfirmationReset(stale);
|
|
fields = TemplateEditorTestFields.Read(stale);
|
|
Assert.Contains("Confirm deletion", await (await client.PostAsync(url, new FormUrlEncodedContent(fields))).Content.ReadAsStringAsync());
|
|
fields["ConfirmDelete"] = "true";
|
|
var deleted = await client.PostAsync(url, new FormUrlEncodedContent(fields));
|
|
Assert.Equal(HttpStatusCode.Redirect, deleted.StatusCode);
|
|
Assert.Equal("/admin/services", deleted.Headers.Location!.OriginalString);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task EmptyStatesOfferScopedFirstCreateOrClearFilterActions()
|
|
{
|
|
await using var context = await DefinitionApiTestContext.CreateAsync(fixture);
|
|
var root = $"/admin/services/{context.ServiceId}";
|
|
var templates = await context.Client.GetStringAsync(root);
|
|
Assert.Contains("No email templates for this service yet.", templates);
|
|
Assert.Contains("Create First Template", templates);
|
|
Assert.Contains(root + "/templates/create", templates);
|
|
Assert.Contains("template-key-filter", templates);
|
|
var filtered = await context.Client.GetStringAsync(root + "?key=Missing");
|
|
Assert.Contains("No templates match this key.", filtered);
|
|
Assert.Contains(">Clear filter</a>", filtered);
|
|
Assert.DoesNotContain("Create First Template", filtered);
|
|
var service = (await context.Client.GetFromJsonAsync<JsonElement>(context.ServiceRoot)).GetProperty("data");
|
|
await context.Client.DeleteAsync(context.ServiceRoot + "?revision=" + service.GetProperty("revision").GetGuid());
|
|
var services = await context.Client.GetStringAsync("/admin/services");
|
|
Assert.Contains("No services yet.", services);
|
|
Assert.Contains("Create First Service", services);
|
|
Assert.Contains("aria-hidden=\"true\"", services);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task TemplateLoadFailureDoesNotPretendTheServiceHasNoTemplates()
|
|
{
|
|
await using var context = await DefinitionApiTestContext.CreateAsync(fixture);
|
|
using var failing = context.App.WithWebHostBuilder(builder => builder.ConfigureTestServices(services =>
|
|
services.AddScoped<IEmailTemplateDefinitionRepository, UnavailableEmailTemplateDefinitionRepository>()));
|
|
using var client = failing.CreateClient();
|
|
var html = await client.GetStringAsync($"/admin/services/{context.ServiceId}");
|
|
Assert.Contains("Could not load templates", html);
|
|
Assert.Contains("Templates are unavailable", html);
|
|
Assert.DoesNotContain("Create First Template", html);
|
|
Assert.DoesNotContain("No email templates for this service yet.", html);
|
|
}
|
|
|
|
private static void AssertConfirmationReset(string html) =>
|
|
Assert.DoesNotContain("checked", Regex.Match(html, "<input[^>]+type=\"checkbox\"[^>]*>").Value);
|
|
}
|