Introduce Yavsc.Interfaces.ISmtpClient and a recording test fake
Narrow ISmtpClient to the four operations MailSender actually uses, behind a Yavsc.Interfaces.ISmtpClientFactory. Production wires MailKitSmtpClient (SmtpClientFactory); tests wire a recording fake (RecordingSmtpClientFactory). The fake is pre-registered in WebServerFixture so SMTP calls are short-circuited; the EMailling test now asserts the Connect -> Authenticate -> Send -> Disconnect sequence. Yavsc.Abstract stays free of MailKit/MimeKit.
This commit is contained in:
parent
16508e9fb2
commit
2dec799d71
13 changed files with 264 additions and 10 deletions
25
src/Yavsc.Server/Interfaces/ISmtpClient.cs
Normal file
25
src/Yavsc.Server/Interfaces/ISmtpClient.cs
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MimeKit;
|
||||
|
||||
namespace Yavsc.Interfaces
|
||||
{
|
||||
/// <summary>
|
||||
/// Yavsc's own SMTP client surface. Narrower than
|
||||
/// <c>MailKit.Net.Smtp.ISmtpClient</c>: only the calls that
|
||||
/// <c>MailSender</c> actually makes. Production wires
|
||||
/// <c>MailKitSmtpClient</c>; tests wire a recording fake.
|
||||
/// </summary>
|
||||
public interface ISmtpClient : IDisposable
|
||||
{
|
||||
int Timeout { get; set; }
|
||||
|
||||
void Connect(string host, int port, MailKit.Security.SecureSocketOptions options);
|
||||
|
||||
void Authenticate(string userName, string password);
|
||||
|
||||
Task SendAsync(MimeMessage message, CancellationToken cancellationToken = default);
|
||||
|
||||
void Disconnect(bool quit);
|
||||
}
|
||||
}
|
||||
13
src/Yavsc.Server/Interfaces/ISmtpClientFactory.cs
Normal file
13
src/Yavsc.Server/Interfaces/ISmtpClientFactory.cs
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
namespace Yavsc.Interfaces
|
||||
{
|
||||
/// <summary>
|
||||
/// Builds fresh <see cref="ISmtpClient"/> instances on demand.
|
||||
/// Per-call construction is required because <c>MailSender</c>
|
||||
/// owns its client with a <c>using</c> block: a singleton or
|
||||
/// scoped client would race across concurrent sends.
|
||||
/// </summary>
|
||||
public interface ISmtpClientFactory
|
||||
{
|
||||
ISmtpClient CreateClient();
|
||||
}
|
||||
}
|
||||
53
src/Yavsc.Server/Services/MailKitSmtpClient.cs
Normal file
53
src/Yavsc.Server/Services/MailKitSmtpClient.cs
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
using MailKit.Security;
|
||||
using MimeKit;
|
||||
using Yavsc.Interfaces;
|
||||
|
||||
namespace Yavsc.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// Production adapter from <see cref="Yavsc.Interfaces.ISmtpClient"/>
|
||||
/// to MailKit's <c>SmtpClient</c>. Stateless; the underlying
|
||||
/// MailKit client is created per <c>Connect</c> cycle by the
|
||||
/// factory and disposed by <c>MailSender</c>'s <c>using</c>.
|
||||
/// </summary>
|
||||
public sealed class MailKitSmtpClient : ISmtpClient
|
||||
{
|
||||
private readonly MailKit.Net.Smtp.SmtpClient _client;
|
||||
|
||||
public MailKitSmtpClient()
|
||||
{
|
||||
_client = new MailKit.Net.Smtp.SmtpClient();
|
||||
}
|
||||
|
||||
public int Timeout
|
||||
{
|
||||
get => _client.Timeout;
|
||||
set => _client.Timeout = value;
|
||||
}
|
||||
|
||||
public void Connect(string host, int port, SecureSocketOptions options)
|
||||
{
|
||||
_client.Connect(host, port, options);
|
||||
}
|
||||
|
||||
public void Authenticate(string userName, string password)
|
||||
{
|
||||
_client.Authenticate(userName, password);
|
||||
}
|
||||
|
||||
public Task SendAsync(MimeMessage message, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return _client.SendAsync(message, cancellationToken);
|
||||
}
|
||||
|
||||
public void Disconnect(bool quit)
|
||||
{
|
||||
_client.Disconnect(quit);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_client.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +1,11 @@
|
|||
using System.Net;
|
||||
using MailKit.Net.Smtp;
|
||||
using MailKit.Security;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using MimeKit;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Yavsc.Interface;
|
||||
using Yavsc.Interfaces;
|
||||
using Yavsc.Settings;
|
||||
using Yavsc.Models;
|
||||
using Microsoft.AspNetCore.Identity.UI.Services;
|
||||
|
|
@ -16,21 +16,26 @@ namespace Yavsc.Services
|
|||
{
|
||||
public class MailSender : IEmailSender<ApplicationUser>, IEmailSender, ITrueEmailSender
|
||||
{
|
||||
|
||||
private readonly IStringLocalizer<MailSender> localizer;
|
||||
readonly SiteSettings siteSettings;
|
||||
readonly SmtpSettings smtpSettings;
|
||||
private readonly ILogger logger;
|
||||
private readonly ISmtpClientFactory _smtpClientFactory;
|
||||
|
||||
public MailSender(
|
||||
IOptions<SiteSettings> sitesOptions,
|
||||
IOptions<SmtpSettings> smtpOptions,
|
||||
ILoggerFactory loggerFactory,
|
||||
IStringLocalizer<MailSender> localizer
|
||||
IStringLocalizer<MailSender> localizer,
|
||||
ISmtpClientFactory smtpClientFactory
|
||||
)
|
||||
{
|
||||
this.localizer = localizer;
|
||||
siteSettings = sitesOptions.Value;
|
||||
smtpSettings = smtpOptions.Value;
|
||||
logger = loggerFactory.CreateLogger<MailSender>();
|
||||
_smtpClientFactory = smtpClientFactory;
|
||||
}
|
||||
|
||||
public Task SendConfirmationLinkAsync(ApplicationUser user, string email, string confirmationLink)
|
||||
|
|
@ -69,7 +74,7 @@ namespace Yavsc.Services
|
|||
msg.MessageId = MimeKit.Utils.MimeUtils.GenerateMessageId(
|
||||
siteSettings.Authority
|
||||
);
|
||||
using (SmtpClient sc = new())
|
||||
using ISmtpClient sc = _smtpClientFactory.CreateClient();
|
||||
{
|
||||
sc.Timeout = 30000;
|
||||
sc.Connect(
|
||||
|
|
|
|||
14
src/Yavsc.Server/Services/SmtpClientFactory.cs
Normal file
14
src/Yavsc.Server/Services/SmtpClientFactory.cs
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
using Yavsc.Interfaces;
|
||||
|
||||
namespace Yavsc.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// Production <see cref="ISmtpClientFactory"/>: hands out a
|
||||
/// fresh <see cref="MailKitSmtpClient"/> per call. Stateless;
|
||||
/// safe to register as a singleton.
|
||||
/// </summary>
|
||||
public sealed class SmtpClientFactory : ISmtpClientFactory
|
||||
{
|
||||
public ISmtpClient CreateClient() => new MailKitSmtpClient();
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue