Enhance MailService: refactor constructor to accept a custom SMTP client factory and add unit tests for SendEmailAsync method
This commit is contained in:
@@ -1,3 +1,4 @@
|
|||||||
|
using System;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using FoodsharingSiegen.Contracts.Model;
|
using FoodsharingSiegen.Contracts.Model;
|
||||||
using MailKit.Net.Smtp;
|
using MailKit.Net.Smtp;
|
||||||
@@ -15,15 +16,17 @@ namespace FoodsharingSiegen.Server.Service
|
|||||||
{
|
{
|
||||||
private readonly MailSettings _mailSettings;
|
private readonly MailSettings _mailSettings;
|
||||||
private readonly TermSettings _termSettings;
|
private readonly TermSettings _termSettings;
|
||||||
|
private readonly Func<ISmtpClient> _smtpClientFactory;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="MailService"/> class.
|
/// Initializes a new instance of the <see cref="MailService"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="appSettings">The configured application settings injected by DI, containing the <see cref="MailSettings"/>.</param>
|
/// <param name="appSettings">The configured application settings injected by DI, containing the <see cref="MailSettings"/>.</param>
|
||||||
public MailService(IOptions<AppSettings> appSettings)
|
public MailService(IOptions<AppSettings> appSettings, Func<ISmtpClient>? smtpClientFactory = null)
|
||||||
{
|
{
|
||||||
_mailSettings = appSettings.Value.Mail;
|
_mailSettings = appSettings.Value.Mail;
|
||||||
_termSettings = appSettings.Value.Terms;
|
_termSettings = appSettings.Value.Terms;
|
||||||
|
_smtpClientFactory = smtpClientFactory ?? (() => new SmtpClient());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
@@ -40,7 +43,7 @@ namespace FoodsharingSiegen.Server.Service
|
|||||||
};
|
};
|
||||||
email.Body = textPart;
|
email.Body = textPart;
|
||||||
|
|
||||||
using var smtp = new SmtpClient();
|
using var smtp = _smtpClientFactory();
|
||||||
var secureOptions = _mailSettings.UseSsl ? SecureSocketOptions.StartTls : SecureSocketOptions.Auto;
|
var secureOptions = _mailSettings.UseSsl ? SecureSocketOptions.StartTls : SecureSocketOptions.Auto;
|
||||||
|
|
||||||
await smtp.ConnectAsync(_mailSettings.Host, _mailSettings.Port, secureOptions);
|
await smtp.ConnectAsync(_mailSettings.Host, _mailSettings.Port, secureOptions);
|
||||||
|
|||||||
110
FoodsharingSiegen.Tests/MailServiceTests.cs
Normal file
110
FoodsharingSiegen.Tests/MailServiceTests.cs
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using FoodsharingSiegen.Contracts.Model;
|
||||||
|
using FoodsharingSiegen.Server.Service;
|
||||||
|
using MailKit.Net.Smtp;
|
||||||
|
using MailKit.Security;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using MimeKit;
|
||||||
|
using Moq;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace FoodsharingSiegen.Tests
|
||||||
|
{
|
||||||
|
public class MailServiceTests
|
||||||
|
{
|
||||||
|
private readonly Mock<IOptions<AppSettings>> _mockOptions;
|
||||||
|
private readonly AppSettings _appSettings;
|
||||||
|
private readonly Mock<ISmtpClient> _mockSmtpClient;
|
||||||
|
|
||||||
|
public MailServiceTests()
|
||||||
|
{
|
||||||
|
_appSettings = new AppSettings
|
||||||
|
{
|
||||||
|
Mail = new MailSettings
|
||||||
|
{
|
||||||
|
Host = "smtp.test.com",
|
||||||
|
Port = 587,
|
||||||
|
UseSsl = false,
|
||||||
|
Username = "user@test.com",
|
||||||
|
Password = "password123",
|
||||||
|
FromAddress = "no-reply@test.com"
|
||||||
|
},
|
||||||
|
Terms = new TermSettings
|
||||||
|
{
|
||||||
|
Title = "Foodsharing Test"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
_mockOptions = new Mock<IOptions<AppSettings>>();
|
||||||
|
_mockOptions.Setup(o => o.Value).Returns(_appSettings);
|
||||||
|
|
||||||
|
_mockSmtpClient = new Mock<ISmtpClient>();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task SendEmailAsync_ConnectsAuthenticatesAndSendsEmail()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var service = new MailService(_mockOptions.Object, () => _mockSmtpClient.Object);
|
||||||
|
var toEmail = "recipient@test.com";
|
||||||
|
var subject = "Test Subject";
|
||||||
|
var body = "<p>Test Body</p>";
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await service.SendEmailAsync(toEmail, subject, body);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
_mockSmtpClient.Verify(
|
||||||
|
x => x.ConnectAsync(
|
||||||
|
"smtp.test.com",
|
||||||
|
587,
|
||||||
|
SecureSocketOptions.Auto,
|
||||||
|
It.IsAny<CancellationToken>()),
|
||||||
|
Times.Once);
|
||||||
|
|
||||||
|
_mockSmtpClient.Verify(
|
||||||
|
x => x.AuthenticateAsync(
|
||||||
|
"user@test.com",
|
||||||
|
"password123",
|
||||||
|
It.IsAny<CancellationToken>()),
|
||||||
|
Times.Once);
|
||||||
|
|
||||||
|
// Verify a MimeMessage is passed to SendAsync with correct attributes
|
||||||
|
_mockSmtpClient.Verify(
|
||||||
|
x => x.SendAsync(
|
||||||
|
It.Is<MimeMessage>(m => m.Subject == subject),
|
||||||
|
It.IsAny<CancellationToken>(),
|
||||||
|
It.IsAny<MailKit.ITransferProgress>()),
|
||||||
|
Times.Once);
|
||||||
|
|
||||||
|
_mockSmtpClient.Verify(
|
||||||
|
x => x.DisconnectAsync(
|
||||||
|
true,
|
||||||
|
It.IsAny<CancellationToken>()),
|
||||||
|
Times.Once);
|
||||||
|
|
||||||
|
_mockSmtpClient.Verify(
|
||||||
|
x => x.Dispose(),
|
||||||
|
Times.Once);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task SendEmailAsync_SkipsAuthentication_WhenUsernameIsBlank()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
_appSettings.Mail.Username = "";
|
||||||
|
_appSettings.Mail.Password = "";
|
||||||
|
var service = new MailService(_mockOptions.Object, () => _mockSmtpClient.Object);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await service.SendEmailAsync("recipient@test.com", "Subject", "Body");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
_mockSmtpClient.Verify(
|
||||||
|
x => x.AuthenticateAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<CancellationToken>()),
|
||||||
|
Times.Never);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user