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 { [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 { ["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("Channels are never deleted automatically", 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(); 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 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", filtered); Assert.DoesNotContain("Create First Template", filtered); var service = (await context.Client.GetFromJsonAsync(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())); 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 templates for this service yet.", html); } private static void AssertConfirmationReset(string html) => Assert.DoesNotContain("checked", Regex.Match(html, "]+type=\"checkbox\"[^>]*>").Value); }