feat(templates): add service language template management
Add normalized service and template catalogs, scoped administration, migration, validation, and regression coverage. Ref: IT-1601
This commit is contained in:
@@ -11,6 +11,7 @@ public sealed class DevelopmentDataSeederTests
|
||||
{
|
||||
private readonly IEmailChannelRepository _channels = Substitute.For<IEmailChannelRepository>();
|
||||
private readonly IEmailTemplateRepository _templates = Substitute.For<IEmailTemplateRepository>();
|
||||
private readonly INotificationServiceRepository _services = Substitute.For<INotificationServiceRepository>();
|
||||
|
||||
[Fact]
|
||||
public async Task SeedAsync_WhenConfigurationIsMissing_CreatesMailpitChannelAndTestTemplate()
|
||||
@@ -69,6 +70,7 @@ public sealed class DevelopmentDataSeederTests
|
||||
private DevelopmentDataSeeder CreateSeeder() => new(
|
||||
_channels,
|
||||
_templates,
|
||||
_services,
|
||||
NullLogger<DevelopmentDataSeeder>.Instance);
|
||||
|
||||
private static bool IsMailpitChannel(EmailChannel channel)
|
||||
|
||||
+81
-14
@@ -1,30 +1,97 @@
|
||||
namespace HrynCo.NotificationService.Services.Tests.EmailProcessing;
|
||||
|
||||
using HrynCo.NotificationService.DAL.Abstract.Repositories;
|
||||
using HrynCo.NotificationService.DAL.Abstract.Templates;
|
||||
using HrynCo.NotificationService.Worker.Services.EmailProcessing;
|
||||
using NSubstitute;
|
||||
|
||||
public sealed class EmailTemplateServiceTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData("RU")]
|
||||
[InlineData(" ru ")]
|
||||
[InlineData(" RU ")]
|
||||
public async Task GetAsync_WhenExactVersionIsMissing_RetriesLegacyTrimmedLowercaseCode(string requestedCode)
|
||||
{
|
||||
IEmailTemplateRepository repository = Substitute.For<IEmailTemplateRepository>();
|
||||
var expected = Template("ru");
|
||||
repository.GetAsync("TestService", "Welcome", "ru", Arg.Any<CancellationToken>()).Returns(expected);
|
||||
var service = new EmailTemplateService(repository);
|
||||
|
||||
Assert.Same(expected, await service.GetAsync("TestService", "Welcome", requestedCode, default));
|
||||
Assert.Equal(new[] { requestedCode, "ru" }, RequestedCodes(repository));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetAsync_WhenRequestedLanguageDoesNotExist_DoesNotSilentlyFallback()
|
||||
public async Task GetAsync_WhenBothExactAndLowercaseVersionsExist_PrefersExactVersion()
|
||||
{
|
||||
IEmailTemplateRepository repository = Substitute.For<IEmailTemplateRepository>();
|
||||
var expected = Template("RU");
|
||||
repository.GetAsync("TestService", "Welcome", "RU", Arg.Any<CancellationToken>()).Returns(expected);
|
||||
repository.GetAsync("TestService", "Welcome", "ru", Arg.Any<CancellationToken>()).Returns(Template("ru"));
|
||||
var service = new EmailTemplateService(repository);
|
||||
|
||||
Assert.Same(expected, await service.GetAsync("TestService", "Welcome", "RU", default));
|
||||
Assert.Equal(new[] { "RU" }, RequestedCodes(repository));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetAsync_WhenExactRegionalVersionExists_DoesNotNormalizeIt()
|
||||
{
|
||||
IEmailTemplateRepository repository = Substitute.For<IEmailTemplateRepository>();
|
||||
var expected = Template("ru-RU");
|
||||
repository.GetAsync("TestService", "Welcome", "ru-RU", Arg.Any<CancellationToken>()).Returns(expected);
|
||||
var service = new EmailTemplateService(repository);
|
||||
|
||||
Assert.Same(expected, await service.GetAsync("TestService", "Welcome", "ru-RU", default));
|
||||
Assert.Equal(new[] { "ru-RU" }, RequestedCodes(repository));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetAsync_WhenMissingCodeIsAlreadyCanonical_QueriesOnlyOnce()
|
||||
{
|
||||
IEmailTemplateRepository repository = Substitute.For<IEmailTemplateRepository>();
|
||||
var service = new EmailTemplateService(repository);
|
||||
|
||||
InvalidOperationException exception = await Assert.ThrowsAsync<InvalidOperationException>(
|
||||
() => service.GetAsync("StoreMate-Prod", "EmailVerification", "UK", CancellationToken.None));
|
||||
var error = await Assert.ThrowsAsync<InvalidOperationException>(() => service.GetAsync("TestService", "Welcome", "zz", default));
|
||||
|
||||
Assert.Contains("language='uk'", exception.Message);
|
||||
await repository.Received(1).GetAsync(
|
||||
"StoreMate-Prod",
|
||||
"EmailVerification",
|
||||
"uk",
|
||||
CancellationToken.None);
|
||||
await repository.DidNotReceive().GetAsync(
|
||||
"StoreMate-Prod",
|
||||
"EmailVerification",
|
||||
"en",
|
||||
Arg.Any<CancellationToken>());
|
||||
Assert.Contains("language='zz'", error.Message);
|
||||
Assert.Equal(new[] { "zz" }, RequestedCodes(repository));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetAsync_WhenRegionalLookupMisses_DoesNotScanOtherCaseOrRegionalVariants()
|
||||
{
|
||||
IEmailTemplateRepository repository = Substitute.For<IEmailTemplateRepository>();
|
||||
repository.GetAsync("TestService", "Welcome", "ru-RU", Arg.Any<CancellationToken>()).Returns(Template("ru-RU"));
|
||||
repository.GetAsync("TestService", "Welcome", "ru", Arg.Any<CancellationToken>()).Returns(Template("ru"));
|
||||
var service = new EmailTemplateService(repository);
|
||||
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(() => service.GetAsync("TestService", "Welcome", "RU-ru", default));
|
||||
|
||||
Assert.Equal(new[] { "RU-ru", "ru-ru" }, RequestedCodes(repository));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(null)]
|
||||
[InlineData("")]
|
||||
[InlineData(" ")]
|
||||
public async Task GetAsync_WhenCodeIsBlank_RejectsBeforeRepositoryLookup(string? code)
|
||||
{
|
||||
IEmailTemplateRepository repository = Substitute.For<IEmailTemplateRepository>();
|
||||
var service = new EmailTemplateService(repository);
|
||||
|
||||
await Assert.ThrowsAsync<InvalidDataException>(() => service.GetAsync("TestService", "Welcome", code, default));
|
||||
|
||||
Assert.Empty(repository.ReceivedCalls());
|
||||
}
|
||||
|
||||
private static EmailTemplate Template(string code) => new()
|
||||
{
|
||||
ServiceName = "TestService", Key = "Welcome", LanguageCode = code, Subject = "Subject " + code,
|
||||
HtmlBody = "<p>Hello</p>", TextBody = "Hello"
|
||||
};
|
||||
|
||||
private static string[] RequestedCodes(IEmailTemplateRepository repository) => repository.ReceivedCalls()
|
||||
.Select(call => (string)call.GetArguments()[2]!).ToArray();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user