Files
agrynco 1c028313ba fix(notification): save smtp sender without reentering password
Preserve the saved SMTP password when connection settings are unchanged and keep it out of admin forms.

Ref: IT-1601
2026-10-18 00:35:17 +03:00

137 lines
6.4 KiB
C#

using System.Net;
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 AdminSmtpSaveTests(TemplateDatabaseFixture fixture) : IClassFixture<TemplateDatabaseFixture>
{
[Fact]
public async Task BlankPasswordKeepsSavedValueWhenOnlySenderChanges()
{
await using var context = await DefinitionApiTestContext.CreateAsync(fixture);
var channel = await SeedChannel(context);
using var client = context.App.CreateClient(new() { AllowAutoRedirect = false });
var fields = TemplateEditorTestFields.Read(await client.GetStringAsync($"/admin/channels/{channel.Id}"));
fields["UseSsl"] = "true";
fields["Password"] = "";
fields["FromName"] = "Updated sender";
var response = await client.PostAsync("/admin/channels/save", new FormUrlEncodedContent(fields));
Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
var saved = await ReadSettings(context, channel.Id);
Assert.Equal("saved-test-secret", saved.Password);
Assert.Equal("Updated sender", saved.FromName);
}
[Theory]
[InlineData("Host", "other.example.invalid")]
[InlineData("Port", "2525")]
[InlineData("Username", "other-user")]
[InlineData("UseSsl", "false")]
public async Task ChangedConnectionWithBlankPasswordIsRejected(string field, string value)
{
await using var context = await DefinitionApiTestContext.CreateAsync(fixture);
var channel = await SeedChannel(context);
using var client = context.App.CreateClient(new() { AllowAutoRedirect = false });
var fields = TemplateEditorTestFields.Read(await client.GetStringAsync($"/admin/channels/{channel.Id}"));
fields["UseSsl"] = "true";
fields["Password"] = "";
fields[field] = value;
var response = await client.PostAsync("/admin/channels/save", new FormUrlEncodedContent(fields));
var html = await response.Content.ReadAsStringAsync();
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Contains("Enter the password to save changed SMTP connection settings.", html);
Assert.DoesNotContain("saved-test-secret", html);
var saved = await ReadSettings(context, channel.Id);
Assert.Equal("saved-test-secret", saved.Password);
Assert.Equal("smtp.example.invalid", saved.Host);
}
[Fact]
public async Task ExplicitPasswordCanReplaceSavedValue()
{
await using var context = await DefinitionApiTestContext.CreateAsync(fixture);
var channel = await SeedChannel(context);
using var client = context.App.CreateClient(new() { AllowAutoRedirect = false });
var fields = TemplateEditorTestFields.Read(await client.GetStringAsync($"/admin/channels/{channel.Id}"));
fields["UseSsl"] = "true";
fields["Password"] = "replacement-test-secret";
fields["Host"] = "other.example.invalid";
var response = await client.PostAsync("/admin/channels/save", new FormUrlEncodedContent(fields));
Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
var saved = await ReadSettings(context, channel.Id);
Assert.Equal("replacement-test-secret", saved.Password);
Assert.Equal("other.example.invalid", saved.Host);
}
[Fact]
public async Task NewChannelStillRequiresPassword()
{
await using var context = await DefinitionApiTestContext.CreateAsync(fixture);
using var client = context.App.CreateClient(new() { AllowAutoRedirect = false });
var fields = TemplateEditorTestFields.Read(await client.GetStringAsync("/admin/channels/create"));
fields["ServiceName"] = "NewTestService";
fields["Host"] = "smtp.example.invalid";
fields["Username"] = "test-user";
fields["FromEmail"] = "sender@example.invalid";
fields["FromName"] = "Sender";
fields["Password"] = "";
var response = await client.PostAsync("/admin/channels/save", new FormUrlEncodedContent(fields));
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Contains("The Password field is required.", await response.Content.ReadAsStringAsync());
}
[Fact]
public async Task InvalidFormDoesNotRenderEnteredPassword()
{
await using var context = await DefinitionApiTestContext.CreateAsync(fixture);
var channel = await SeedChannel(context);
using var client = context.App.CreateClient(new() { AllowAutoRedirect = false });
var fields = TemplateEditorTestFields.Read(await client.GetStringAsync($"/admin/channels/{channel.Id}"));
fields["Password"] = "replacement-test-secret";
fields["FromEmail"] = "invalid-address";
var response = await client.PostAsync("/admin/channels/save", new FormUrlEncodedContent(fields));
var html = await response.Content.ReadAsStringAsync();
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.DoesNotContain("replacement-test-secret", html);
Assert.Equal("saved-test-secret", (await ReadSettings(context, channel.Id)).Password);
}
private static async Task<EmailChannel> SeedChannel(DefinitionApiTestContext context)
{
using var scope = context.App.Services.CreateScope();
var channel = new EmailChannel
{
ServiceName = "TestService", EmailChannelType = EmailChannelType.Smtp,
Settings = new SmtpChannelSettings
{
Host = "smtp.example.invalid", Port = 587, Username = "test-user",
Password = "saved-test-secret", UseSsl = true,
FromEmail = "sender@example.invalid", FromName = "Original sender"
}
};
await scope.ServiceProvider.GetRequiredService<IEmailChannelRepository>().AddAsync(channel);
await scope.ServiceProvider.GetRequiredService<NotificationDbContext>().SaveChangesAsync();
return channel;
}
private static async Task<SmtpChannelSettings> ReadSettings(DefinitionApiTestContext context, Guid id)
{
using var scope = context.App.Services.CreateScope();
var channel = await scope.ServiceProvider.GetRequiredService<IEmailChannelRepository>().GetByIdAsync(id);
return Assert.IsType<SmtpChannelSettings>(channel?.Settings);
}
}