From ed4293d852393b32604a404c8b9c6cf702f375b9 Mon Sep 17 00:00:00 2001 From: Anatolii Hrynchuk Date: Sat, 31 Oct 2026 10:14:48 +0200 Subject: [PATCH] fix(admin): filter template keys by substring Match both channels case-insensitively while keeping API and delivery lookups exact. Ref: IT-1627 --- .../TemplateKeyFilterTests.cs | 33 +++++++++++++++++++ .../Admin/AdminTemplatesController.cs | 7 ++-- .../Views/AdminTemplates/Index.cshtml | 2 +- docs/channel-template-model.md | 10 ++++++ 4 files changed, 48 insertions(+), 4 deletions(-) create mode 100644 HrynCo.NotificationService.Web.IntegrationTests/TemplateKeyFilterTests.cs diff --git a/HrynCo.NotificationService.Web.IntegrationTests/TemplateKeyFilterTests.cs b/HrynCo.NotificationService.Web.IntegrationTests/TemplateKeyFilterTests.cs new file mode 100644 index 0000000..38238f7 --- /dev/null +++ b/HrynCo.NotificationService.Web.IntegrationTests/TemplateKeyFilterTests.cs @@ -0,0 +1,33 @@ +using System.Net; +using System.Net.Http.Json; +using HrynCo.NotificationService.DAL.Abstract.Repositories; +using HrynCo.NotificationService.DAL.Abstract.Templates; +using Microsoft.Extensions.DependencyInjection; + +namespace HrynCo.NotificationService.Web.IntegrationTests; + +public sealed class TemplateKeyFilterTests(TemplateDatabaseFixture fixture) : IClassFixture +{ + [Theory] + [InlineData("contact", true, false)] + [InlineData(" CONTACT ", true, false)] + [InlineData("", true, true)] + [InlineData(" ", true, true)] + [InlineData("missing", false, false)] + [InlineData("%", false, false)] + public async Task AdminFilterMatchesLiteralSubstringAcrossBothChannels(string filter, bool contacts, bool other) + { + await using var c = await DefinitionApiTestContext.CreateAsync(fixture); + await c.UpsertAsync(DefinitionApiTestContext.Body(null, "en"), "TrustedContactAdded"); + await c.UpsertAsync(DefinitionApiTestContext.Body(null, "en"), "PasswordReset"); + using var scope = c.App.Services.CreateScope(); + var pushes = scope.ServiceProvider.GetRequiredService(); + await pushes.SaveAsync(new PushTemplateDefinition { ServiceId = c.ServiceId, Key = "TrustedContactRemoved" }, true); + var html = await c.Client.GetStringAsync($"/admin/services/{c.ServiceId}?key={Uri.EscapeDataString(filter)}"); + Assert.Equal(contacts, html.Contains(">TrustedContactAdded<", StringComparison.Ordinal)); + Assert.Equal(contacts, html.Contains(">TrustedContactRemoved<", StringComparison.Ordinal)); + Assert.Equal(other, html.Contains(">PasswordReset<", StringComparison.Ordinal)); + Assert.Contains("Clear", html); + Assert.Equal(HttpStatusCode.NotFound, (await c.Client.GetAsync(c.DefinitionUrl("contact"))).StatusCode); + } +} diff --git a/HrynCo.NotificationService.Web/Controllers/Admin/AdminTemplatesController.cs b/HrynCo.NotificationService.Web/Controllers/Admin/AdminTemplatesController.cs index 5a121f4..5a6c454 100644 --- a/HrynCo.NotificationService.Web/Controllers/Admin/AdminTemplatesController.cs +++ b/HrynCo.NotificationService.Web/Controllers/Admin/AdminTemplatesController.cs @@ -27,10 +27,11 @@ public sealed class AdminTemplatesController(IMediator mediator, IEmailTemplateD ModelState.AddModelError("", previousError); try { + var filter = key?.Trim() ?? ""; model.PushDefinitions = (await pushes.GetDefinitionsAsync(serviceId, ct)) - .Where(d => string.IsNullOrWhiteSpace(key) || d.Key == key).ToList(); - model.Definitions = (await definitions.GetDefinitionsAsync(service.Name, key, ct)) - .Where(d => d.ServiceId == serviceId).ToList(); + .Where(d => d.Key.Contains(filter, StringComparison.OrdinalIgnoreCase)).ToList(); + model.Definitions = (await definitions.GetDefinitionsAsync(serviceId, ct)) + .Where(d => d.Key.Contains(filter, StringComparison.OrdinalIgnoreCase)).ToList(); } catch (Exception error) when (error is not OperationCanceledException) { diff --git a/HrynCo.NotificationService.Web/Views/AdminTemplates/Index.cshtml b/HrynCo.NotificationService.Web/Views/AdminTemplates/Index.cshtml index 1a8b0d3..a8cb649 100644 --- a/HrynCo.NotificationService.Web/Views/AdminTemplates/Index.cshtml +++ b/HrynCo.NotificationService.Web/Views/AdminTemplates/Index.cshtml @@ -34,7 +34,7 @@
- + Clear
diff --git a/docs/channel-template-model.md b/docs/channel-template-model.md index aa8f411..3ee405c 100644 --- a/docs/channel-template-model.md +++ b/docs/channel-template-model.md @@ -342,3 +342,13 @@ verification, and authorized stopping the Rider Web process. The Rider process w stopped. A second background Web launch was rejected by the execution tool with only `blocked by policy`; this is an execution-tool restriction, not a request for more owner permission. The automated API/PostgreSQL checks remain available. + +## Administration key substring filter (2026-10-31) + +The service Templates page filters both Email and Push definitions by a literal, +case-insensitive substring of Template Key. Leading/trailing search whitespace is +ignored; empty/whitespace-only input and Clear show the full service catalog. +For example, `contact` matches `TrustedContactAdded` and `trusted_contact_removed`. +`%` and `_` are ordinary characters, not SQL wildcard patterns. The filter stays in +navigation links as before. Exact template identity, management API reads and delivery +lookups are unchanged; substring matching is limited to this administration list. -- 2.52.0