aba4173827
Share template keys across email and push with independent editors and revisions. Render localized actions and preserve version-one delivery. Ref: IT-1627
174 lines
12 KiB
C#
174 lines
12 KiB
C#
using System.Net;
|
|
using HrynCo.NotificationService.DAL.Abstract.Repositories;
|
|
using HrynCo.NotificationService.DAL.Abstract.Templates;
|
|
using HrynCo.NotificationService.DAL.EF;
|
|
using Microsoft.AspNetCore.Mvc.Testing;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
|
|
namespace HrynCo.NotificationService.Web.IntegrationTests;
|
|
|
|
public sealed class PushTemplateEditorTests(TemplateDatabaseFixture fixture) : IClassFixture<TemplateDatabaseFixture>
|
|
{
|
|
[Fact]
|
|
public async Task PushOnlyKeyCanAcquireEmailWithoutChangingPush_AndListHasOneRow()
|
|
{
|
|
var connection = await fixture.CreateDatabaseAsync();
|
|
await using var app = new TemplateWebApplicationFactory(connection);
|
|
using var client = app.CreateClient(new WebApplicationFactoryClientOptions { AllowAutoRedirect = false });
|
|
var service = await ReadServiceAsync(connection);
|
|
var fields = TemplateEditorTestFields.Read(await client.GetStringAsync($"/admin/services/{service.Id}/push-templates/create"));
|
|
fields["Key"] = "PushOnly";
|
|
SetPush(fields, "en", "Hello {{Name}}", "A shared item changed.");
|
|
fields["VariablesJson"] = "[{\"name\":\"Name\",\"required\":true}]";
|
|
Assert.Equal(HttpStatusCode.Redirect, (await client.PostAsync($"/admin/services/{service.Id}/push-templates/save", new FormUrlEncodedContent(fields))).StatusCode);
|
|
var missing = await client.GetStringAsync($"/admin/services/{service.Id}/push-templates/email?key=PushOnly");
|
|
Assert.Contains("Create Email Template", missing);
|
|
var emailFields = TemplateEditorTestFields.Read(await client.GetStringAsync($"/admin/services/{service.Id}/templates/create?templateKey=PushOnly"));
|
|
Assert.Equal("PushOnly", emailFields["Key"]);
|
|
Assert.Equal(HttpStatusCode.Redirect, (await client.PostAsync($"/admin/services/{service.Id}/templates/save", new FormUrlEncodedContent(emailFields))).StatusCode);
|
|
using var provider = Provider(connection);
|
|
using var scope = provider.CreateScope();
|
|
var push = await scope.ServiceProvider.GetRequiredService<IPushTemplateDefinitionRepository>().GetAsync(service.Id, "PushOnly");
|
|
Assert.Equal("Hello {{Name}}", Assert.Single(push!.Languages).Title);
|
|
var email = await scope.ServiceProvider.GetRequiredService<IEmailTemplateDefinitionRepository>().GetDefinitionAsync(service.Id, "PushOnly");
|
|
Assert.Empty(email!.Variables);
|
|
var list = await client.GetStringAsync($"/admin/services/{service.Id}");
|
|
Assert.Contains("<th scope=\"col\">Email</th>", list);
|
|
Assert.Contains("<th scope=\"col\">Push</th>", list);
|
|
Assert.Equal(1, System.Text.RegularExpressions.Regex.Matches(list, "<td>PushOnly</td>").Count);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("partial")]
|
|
[InlineData("placeholder")]
|
|
[InlineData("json")]
|
|
[InlineData("catalog")]
|
|
public async Task InvalidSaveRetainsInputAndDoesNotCreatePush(string scenario)
|
|
{
|
|
var connection = await fixture.CreateDatabaseAsync();
|
|
await using var app = new TemplateWebApplicationFactory(connection);
|
|
using var client = app.CreateClient();
|
|
var service = await ReadServiceAsync(connection);
|
|
var fields = TemplateEditorTestFields.Read(await client.GetStringAsync($"/admin/services/{service.Id}/push-templates/create"));
|
|
fields["Key"] = "Invalid";
|
|
SetPush(fields, "en", "Retained title", "Body");
|
|
if (scenario == "partial") SetPush(fields, "en", "Retained title", "");
|
|
if (scenario == "placeholder") SetPush(fields, "en", "Retained title", "{{Unknown}}");
|
|
if (scenario == "json") fields["VariablesJson"] = "{}";
|
|
if (scenario == "catalog") fields["Languages[0].LanguageCode"] = "foreign";
|
|
var response = await client.PostAsync($"/admin/services/{service.Id}/push-templates/save", new FormUrlEncodedContent(fields));
|
|
var html = await response.Content.ReadAsStringAsync();
|
|
Assert.Contains("Retained title", html);
|
|
Assert.Contains("validation-summary-errors", html);
|
|
using var provider = Provider(connection);
|
|
using var scope = provider.CreateScope();
|
|
Assert.Null(await scope.ServiceProvider.GetRequiredService<IPushTemplateDefinitionRepository>().GetAsync(service.Id, "Invalid"));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task MissingTabHasNoSideEffects_StaleSaveIsRejected_AndFinalVersionCanBeRemoved()
|
|
{
|
|
var connection = await fixture.CreateDatabaseAsync();
|
|
await using var app = new TemplateWebApplicationFactory(connection);
|
|
using var client = app.CreateClient(new WebApplicationFactoryClientOptions { AllowAutoRedirect = false });
|
|
var service = await ReadServiceAsync(connection);
|
|
Assert.Contains("Create Push Template", await client.GetStringAsync($"/admin/services/{service.Id}/push-templates/edit?key=Shared"));
|
|
using (var provider = Provider(connection))
|
|
using (var scope = provider.CreateScope())
|
|
Assert.Empty(await scope.ServiceProvider.GetRequiredService<IPushTemplateDefinitionRepository>().GetDefinitionsAsync(service.Id));
|
|
var fields = TemplateEditorTestFields.Read(await client.GetStringAsync($"/admin/services/{service.Id}/push-templates/create?key=Shared"));
|
|
SetPush(fields, "uk", "Title", "Body");
|
|
await client.PostAsync($"/admin/services/{service.Id}/push-templates/save", new FormUrlEncodedContent(fields));
|
|
var saved = TemplateEditorTestFields.Read(await client.GetStringAsync($"/admin/services/{service.Id}/push-templates/edit?key=Shared"));
|
|
var stale = new Dictionary<string, string>(saved);
|
|
SetPush(saved, "uk", "Updated title", "Updated body");
|
|
Assert.Equal(HttpStatusCode.Redirect, (await client.PostAsync($"/admin/services/{service.Id}/push-templates/save", new FormUrlEncodedContent(saved))).StatusCode);
|
|
SetPush(stale, "uk", "Retained stale input", "Body");
|
|
var error = await client.PostAsync($"/admin/services/{service.Id}/push-templates/save", new FormUrlEncodedContent(stale));
|
|
Assert.Contains("changed after you opened", await error.Content.ReadAsStringAsync());
|
|
Assert.Contains("Retained stale input", await error.Content.ReadAsStringAsync());
|
|
var current = TemplateEditorTestFields.Read(await client.GetStringAsync($"/admin/services/{service.Id}/push-templates/edit?key=Shared"));
|
|
SetPush(current, "uk", "", "");
|
|
Assert.Equal(HttpStatusCode.Redirect, (await client.PostAsync($"/admin/services/{service.Id}/push-templates/save", new FormUrlEncodedContent(current))).StatusCode);
|
|
using var finalProvider = Provider(connection);
|
|
using var finalScope = finalProvider.CreateScope();
|
|
Assert.Empty((await finalScope.ServiceProvider.GetRequiredService<IPushTemplateDefinitionRepository>().GetAsync(service.Id, "Shared"))!.Languages);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ConfirmedPushDeletionKeepsEmail_AndPostRequiresAntiforgery()
|
|
{
|
|
var connection = await fixture.CreateDatabaseAsync();
|
|
var service = await ReadServiceAsync(connection);
|
|
using (var provider = Provider(connection))
|
|
using (var scope = provider.CreateScope())
|
|
{
|
|
await scope.ServiceProvider.GetRequiredService<IEmailTemplateDefinitionRepository>().SaveDefinitionAsync(new EmailTemplateDefinition { ServiceName = service.Name, Key = "Shared" }, true);
|
|
await scope.ServiceProvider.GetRequiredService<IPushTemplateDefinitionRepository>().SaveAsync(new PushTemplateDefinition { ServiceId = service.Id, Key = "Shared" }, true);
|
|
}
|
|
await using var app = new TemplateWebApplicationFactory(connection);
|
|
using var client = app.CreateClient(new WebApplicationFactoryClientOptions { AllowAutoRedirect = false });
|
|
var route = $"/admin/services/{service.Id}/push-templates/delete";
|
|
Assert.Equal(HttpStatusCode.BadRequest, (await client.PostAsync(route, new FormUrlEncodedContent(new Dictionary<string, string> { ["Key"] = "Shared" }))).StatusCode);
|
|
var fields = TemplateEditorTestFields.Read(await client.GetStringAsync(route + "?key=Shared"));
|
|
fields["ConfirmDelete"] = "true";
|
|
Assert.Equal(HttpStatusCode.Redirect, (await client.PostAsync(route, new FormUrlEncodedContent(fields))).StatusCode);
|
|
using var readProvider = Provider(connection);
|
|
using var readScope = readProvider.CreateScope();
|
|
Assert.NotNull(await readScope.ServiceProvider.GetRequiredService<IEmailTemplateDefinitionRepository>().GetDefinitionAsync(service.Id, "Shared"));
|
|
Assert.Null(await readScope.ServiceProvider.GetRequiredService<IPushTemplateDefinitionRepository>().GetAsync(service.Id, "Shared"));
|
|
}
|
|
|
|
private static ServiceProvider Provider(string connection) => new ServiceCollection().AddNotificationDataAccess(connection).BuildServiceProvider();
|
|
|
|
[Theory]
|
|
[InlineData(false)]
|
|
[InlineData(true)]
|
|
public async Task LocalizedActionsRoundTripAndInvalidEditsRetainInput(bool invalid)
|
|
{
|
|
var connection = await fixture.CreateDatabaseAsync();
|
|
await using var app = new TemplateWebApplicationFactory(connection);
|
|
using var client = app.CreateClient(new WebApplicationFactoryClientOptions { AllowAutoRedirect = false });
|
|
var service = await ReadServiceAsync(connection);
|
|
var fields = TemplateEditorTestFields.Read(await client.GetStringAsync($"/admin/services/{service.Id}/push-templates/create"));
|
|
fields["Key"] = "WithButtons";
|
|
SetPush(fields, "uk", "Потребою поділилися", "Перегляньте деталі");
|
|
var codeField = fields.Single(p => p.Key.EndsWith(".LanguageCode") && p.Value == "uk").Key;
|
|
var actionField = codeField[..^"LanguageCode".Length] + "ActionsJson";
|
|
fields[actionField] = invalid ? "{\"buttons\":null}" : "{\"defaultAction\":\"openNeed\",\"buttons\":[{\"action\":\"openNeed\",\"label\":\"Переглянути\"}]}";
|
|
var response = await client.PostAsync($"/admin/services/{service.Id}/push-templates/save", new FormUrlEncodedContent(fields));
|
|
using var provider = Provider(connection);
|
|
using var scope = provider.CreateScope();
|
|
var saved = await scope.ServiceProvider.GetRequiredService<IPushTemplateDefinitionRepository>().GetAsync(service.Id, "WithButtons");
|
|
if (invalid)
|
|
{
|
|
Assert.Null(saved);
|
|
Assert.Contains("validation-summary-errors", await response.Content.ReadAsStringAsync());
|
|
Assert.Equal(fields[actionField], TemplateEditorTestFields.Read(await response.Content.ReadAsStringAsync())[actionField].Trim());
|
|
return;
|
|
}
|
|
Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
|
|
var actions = PushTemplateActionValidation.Parse(Assert.Single(saved!.Languages).ActionsJson);
|
|
Assert.Equal("openNeed", actions.DefaultAction);
|
|
Assert.Equal("Переглянути", Assert.Single(actions.Buttons).Label);
|
|
var reloaded = TemplateEditorTestFields.Read(await client.GetStringAsync($"/admin/services/{service.Id}/push-templates/edit?key=WithButtons"));
|
|
var reloadedCodeField = reloaded.Single(p => p.Key.EndsWith(".LanguageCode") && p.Value == "uk").Key;
|
|
Assert.Equal("openNeed", PushTemplateActionValidation.Parse(reloaded[reloadedCodeField[..^"LanguageCode".Length] + "ActionsJson"]).DefaultAction);
|
|
}
|
|
|
|
private static async Task<NotificationServiceInfo> ReadServiceAsync(string connection)
|
|
{
|
|
using var provider = Provider(connection);
|
|
using var scope = provider.CreateScope();
|
|
return (await scope.ServiceProvider.GetRequiredService<INotificationServiceRepository>().GetByNameAsync("TestService"))!;
|
|
}
|
|
|
|
private static void SetPush(Dictionary<string, string> fields, string code, string title, string body)
|
|
{
|
|
var codeField = fields.Single(p => p.Key.EndsWith(".LanguageCode") && p.Value == code).Key;
|
|
var prefix = codeField[..^"LanguageCode".Length];
|
|
fields[prefix + "Title"] = title;
|
|
fields[prefix + "Body"] = body;
|
|
}
|
|
}
|