fix(admin): filter template keys by substring

Match both channels case-insensitively while keeping API and delivery lookups exact.

Ref: IT-1627
This commit is contained in:
2026-10-31 10:14:48 +02:00
parent 343d67802c
commit ed4293d852
4 changed files with 48 additions and 4 deletions
@@ -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<TemplateDatabaseFixture>
{
[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<IPushTemplateDefinitionRepository>();
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);
}
}
@@ -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)
{
@@ -34,7 +34,7 @@
<form method="get" action="/admin/services/@serviceId" class="template-key-filter">
<label for="key" class="form-label fw-semibold">Template Key</label>
<div class="template-key-filter-controls">
<input id="key" name="key" value="@keyFilter" class="form-control form-control-sm" placeholder="Filter by key" />
<input id="key" name="key" value="@keyFilter" class="form-control form-control-sm" placeholder="Filter by part of key" />
<button type="submit" class="btn btn-primary btn-sm">Filter</button>
<a href="/admin/services/@serviceId" class="btn btn-outline-secondary btn-sm">Clear</a>
</div>
+10
View File
@@ -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.