fix(notification): enable web smtp test with saved password
Allow Web SMTP egress on TCP 587 and reuse the saved channel password only when connection settings match. Ref: IT-1601
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
using HrynCo.NotificationService.DAL.Abstract.Providers;
|
||||
using HrynCo.NotificationService.DAL.Abstract.Repositories;
|
||||
using HrynCo.NotificationService.DAL.EF;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace HrynCo.NotificationService.Web.IntegrationTests;
|
||||
|
||||
public sealed class AdminSmtpEditorViewTests(TemplateDatabaseFixture fixture) : IClassFixture<TemplateDatabaseFixture>
|
||||
{
|
||||
[Fact]
|
||||
public async Task ExistingChannelTestIncludesIdWithoutRenderingSavedPassword()
|
||||
{
|
||||
await using var context = await DefinitionApiTestContext.CreateAsync(fixture);
|
||||
using var scope = context.App.Services.CreateScope();
|
||||
var channel = new EmailChannel
|
||||
{
|
||||
ServiceName = "TestService",
|
||||
EmailChannelType = EmailChannelType.Smtp,
|
||||
Settings = new SmtpChannelSettings
|
||||
{
|
||||
Host = "smtp.example.invalid", Username = "test-user",
|
||||
Password = "saved-secret-must-not-render", FromEmail = "sender@example.invalid"
|
||||
}
|
||||
};
|
||||
await scope.ServiceProvider.GetRequiredService<IEmailChannelRepository>().AddAsync(channel);
|
||||
await scope.ServiceProvider.GetRequiredService<NotificationDbContext>().SaveChangesAsync();
|
||||
|
||||
var html = await context.Client.GetStringAsync($"/admin/channels/{channel.Id}");
|
||||
|
||||
Assert.Contains($"value=\"{channel.Id}\"", html);
|
||||
Assert.Contains("const channelId = form.querySelector('[name=\"Id\"]').value", html);
|
||||
Assert.DoesNotContain("saved-secret-must-not-render", html);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
using System.Reflection;
|
||||
using System.Text.Json;
|
||||
using HrynCo.NotificationService.DAL.Abstract.Providers;
|
||||
using HrynCo.NotificationService.Web.Controllers.Admin;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace HrynCo.NotificationService.Web.IntegrationTests;
|
||||
|
||||
public sealed class AdminSmtpTestControllerTests
|
||||
{
|
||||
private static readonly Guid ChannelId = Guid.NewGuid();
|
||||
|
||||
[Fact]
|
||||
public async Task BlankPasswordUsesSavedSecretAndCurrentUnsavedSender()
|
||||
{
|
||||
var (controller, recorder) = CreateController();
|
||||
var response = await controller.TestSmtp(Request(), CancellationToken.None);
|
||||
|
||||
Assert.True(Succeeded(response));
|
||||
Assert.Equal("saved-test-secret", recorder.SentCommand?.Password);
|
||||
Assert.Equal("changed@example.invalid", recorder.SentCommand?.FromEmail);
|
||||
Assert.Equal("Changed sender", recorder.SentCommand?.FromName);
|
||||
Assert.Equal(1, recorder.LookupCount);
|
||||
Assert.DoesNotContain("saved-test-secret", JsonSerializer.Serialize(((OkObjectResult)response).Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExplicitPasswordOverridesSavedWithoutLookup()
|
||||
{
|
||||
var (controller, recorder) = CreateController();
|
||||
var response = await controller.TestSmtp(Request() with { Password = "typed-test-secret" }, CancellationToken.None);
|
||||
|
||||
Assert.True(Succeeded(response));
|
||||
Assert.Equal("typed-test-secret", recorder.SentCommand?.Password);
|
||||
Assert.Equal(0, recorder.LookupCount);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("host")]
|
||||
[InlineData("port")]
|
||||
[InlineData("username")]
|
||||
[InlineData("ssl")]
|
||||
public async Task ChangedConnectionRequiresExplicitPassword(string field)
|
||||
{
|
||||
var (controller, recorder) = CreateController();
|
||||
var request = field switch
|
||||
{
|
||||
"host" => Request() with { Host = "other.example.invalid" },
|
||||
"port" => Request() with { Port = 2525 },
|
||||
"username" => Request() with { Username = "other-user" },
|
||||
_ => Request() with { UseSsl = false }
|
||||
};
|
||||
|
||||
var response = await controller.TestSmtp(request, CancellationToken.None);
|
||||
|
||||
Assert.False(Succeeded(response));
|
||||
Assert.Null(recorder.SentCommand);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UnknownChannelDoesNotSend()
|
||||
{
|
||||
var (controller, recorder) = CreateController();
|
||||
recorder.Channel = null;
|
||||
|
||||
var response = await controller.TestSmtp(Request(), CancellationToken.None);
|
||||
|
||||
Assert.False(Succeeded(response));
|
||||
Assert.Null(recorder.SentCommand);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task NewChannelKeepsAdHocTestBehavior()
|
||||
{
|
||||
var (controller, recorder) = CreateController();
|
||||
|
||||
var response = await controller.TestSmtp(Request() with { ChannelId = null }, CancellationToken.None);
|
||||
|
||||
Assert.True(Succeeded(response));
|
||||
Assert.Equal(string.Empty, recorder.SentCommand?.Password);
|
||||
Assert.Equal(0, recorder.LookupCount);
|
||||
}
|
||||
|
||||
private static (AdminChannelsController Controller, RecordingAdminSmtpMediator Recorder) CreateController()
|
||||
{
|
||||
var mediator = DispatchProxy.Create<IMediator, RecordingAdminSmtpMediator>();
|
||||
var recorder = (RecordingAdminSmtpMediator)mediator;
|
||||
recorder.Channel = new EmailChannel
|
||||
{
|
||||
Id = ChannelId,
|
||||
ServiceName = "TestService",
|
||||
EmailChannelType = EmailChannelType.Smtp,
|
||||
Settings = new SmtpChannelSettings
|
||||
{
|
||||
Host = "smtp.example.invalid", Port = 587,
|
||||
Username = "test-user", Password = "saved-test-secret", UseSsl = true
|
||||
}
|
||||
};
|
||||
return (new AdminChannelsController(mediator), recorder);
|
||||
}
|
||||
|
||||
private static TestSmtpRequest Request() => new(
|
||||
"smtp.example.invalid", 587, "test-user", "", true,
|
||||
"changed@example.invalid", "Changed sender", "recipient@example.invalid", ChannelId);
|
||||
|
||||
private static bool Succeeded(IActionResult response)
|
||||
{
|
||||
var body = JsonSerializer.Serialize(((OkObjectResult)response).Value);
|
||||
return JsonDocument.Parse(body).RootElement.GetProperty("success").GetBoolean();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
using System.Reflection;
|
||||
using HrynCo.NotificationService.DAL.Abstract.Providers;
|
||||
using HrynCo.NotificationService.Services.Core;
|
||||
using HrynCo.NotificationService.Services.EmailChannels.Get;
|
||||
using HrynCo.NotificationService.Services.EmailChannels.TestSmtp;
|
||||
using MediatR;
|
||||
using ServiceUnit = HrynCo.NotificationService.Services.Core.Unit;
|
||||
|
||||
namespace HrynCo.NotificationService.Web.IntegrationTests;
|
||||
|
||||
public class RecordingAdminSmtpMediator : DispatchProxy
|
||||
{
|
||||
public EmailChannel? Channel { get; set; }
|
||||
public TestSmtpCommand? SentCommand { get; private set; }
|
||||
public int LookupCount { get; private set; }
|
||||
|
||||
protected override object Invoke(MethodInfo? targetMethod, object?[]? args)
|
||||
{
|
||||
if (targetMethod?.Name != nameof(IMediator.Send) || args?.Length is null or 0)
|
||||
throw new NotSupportedException();
|
||||
|
||||
return args[0] switch
|
||||
{
|
||||
GetEmailChannelQuery => RecordLookup(),
|
||||
TestSmtpCommand command => RecordSend(command),
|
||||
_ => throw new NotSupportedException()
|
||||
};
|
||||
}
|
||||
|
||||
private Task<ServiceResult<EmailChannel>> RecordLookup()
|
||||
{
|
||||
LookupCount++;
|
||||
return Task.FromResult(Channel is null
|
||||
? ServiceResult<EmailChannel>.Failure("Channel not found.")
|
||||
: ServiceResult<EmailChannel>.Success(Channel));
|
||||
}
|
||||
|
||||
private Task<ServiceResult<ServiceUnit>> RecordSend(TestSmtpCommand command)
|
||||
{
|
||||
SentCommand = command;
|
||||
return Task.FromResult(ServiceResult<ServiceUnit>.Success(ServiceUnit.Value));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user