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
71
src/Yavsc.Org.Tests/Fakes/RecordingSmtpClient.cs
Normal file
71
src/Yavsc.Org.Tests/Fakes/RecordingSmtpClient.cs
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
using MailKit.Security;
|
||||
using MimeKit;
|
||||
using Yavsc.Interfaces;
|
||||
|
||||
namespace Yavsc.Org.Tests.Fakes
|
||||
{
|
||||
/// <summary>
|
||||
/// In-memory test double for <see cref="ISmtpClient"/>. Records
|
||||
/// every call for assertion and short-circuits the SMTP
|
||||
/// roundtrip.
|
||||
/// </summary>
|
||||
public sealed class RecordingSmtpClient : ISmtpClient
|
||||
{
|
||||
public List<RecordingSmtpCall> Calls { get; } = new();
|
||||
|
||||
public int Timeout { get; set; }
|
||||
public string? LastConnectedHost { get; private set; }
|
||||
public int LastConnectedPort { get; private set; }
|
||||
public SecureSocketOptions LastConnectedOptions { get; private set; }
|
||||
public string? LastAuthenticatedUser { get; private set; }
|
||||
public MimeMessage? LastSentMessage { get; private set; }
|
||||
public bool WasDisconnected { get; private set; }
|
||||
public bool Disposed { get; private set; }
|
||||
|
||||
public void Connect(string host, int port, SecureSocketOptions options)
|
||||
{
|
||||
LastConnectedHost = host;
|
||||
LastConnectedPort = port;
|
||||
LastConnectedOptions = options;
|
||||
Calls.Add(new RecordingSmtpCall(RecordingSmtpCallKind.Connect, host, port, options));
|
||||
}
|
||||
|
||||
public void Authenticate(string userName, string password)
|
||||
{
|
||||
LastAuthenticatedUser = userName;
|
||||
Calls.Add(new RecordingSmtpCall(RecordingSmtpCallKind.Authenticate, userName, null, null));
|
||||
}
|
||||
|
||||
public Task SendAsync(MimeMessage message, CancellationToken cancellationToken = default)
|
||||
{
|
||||
LastSentMessage = message;
|
||||
Calls.Add(new RecordingSmtpCall(RecordingSmtpCallKind.Send, message.Subject, null, null));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public void Disconnect(bool quit)
|
||||
{
|
||||
WasDisconnected = quit;
|
||||
Calls.Add(new RecordingSmtpCall(RecordingSmtpCallKind.Disconnect, null, null, null));
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Disposed = true;
|
||||
}
|
||||
}
|
||||
|
||||
public enum RecordingSmtpCallKind
|
||||
{
|
||||
Connect,
|
||||
Authenticate,
|
||||
Send,
|
||||
Disconnect,
|
||||
}
|
||||
|
||||
public readonly record struct RecordingSmtpCall(
|
||||
RecordingSmtpCallKind Kind,
|
||||
string? HostOrSubjectOrUser,
|
||||
int? Port,
|
||||
SecureSocketOptions? Options);
|
||||
}
|
||||
21
src/Yavsc.Org.Tests/Fakes/RecordingSmtpClientFactory.cs
Normal file
21
src/Yavsc.Org.Tests/Fakes/RecordingSmtpClientFactory.cs
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
using Yavsc.Interfaces;
|
||||
|
||||
namespace Yavsc.Org.Tests.Fakes
|
||||
{
|
||||
/// <summary>
|
||||
/// Test double for <see cref="ISmtpClientFactory"/>. Hands out
|
||||
/// <see cref="RecordingSmtpClient"/> instances and tracks them so
|
||||
/// tests can assert on every SMTP interaction.
|
||||
/// </summary>
|
||||
public sealed class RecordingSmtpClientFactory : ISmtpClientFactory
|
||||
{
|
||||
public List<RecordingSmtpClient> Created { get; } = new();
|
||||
|
||||
public ISmtpClient CreateClient()
|
||||
{
|
||||
var client = new RecordingSmtpClient();
|
||||
Created.Add(client);
|
||||
return client;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Yavsc.Interface;
|
||||
using Yavsc.Interfaces;
|
||||
using Yavsc.Org.Tests.Fakes;
|
||||
|
||||
namespace Yavsc.Org.Tests
|
||||
{
|
||||
|
|
@ -26,6 +28,8 @@ namespace Yavsc.Org.Tests
|
|||
|
||||
using IServiceScope scope = _serverFixture.Services.CreateScope();
|
||||
ITrueEmailSender mailSender = scope.ServiceProvider.GetRequiredService<ITrueEmailSender>();
|
||||
var factory = Assert.IsType<RecordingSmtpClientFactory>(
|
||||
scope.ServiceProvider.GetRequiredService<ISmtpClientFactory>());
|
||||
|
||||
output.WriteLine("SendEMailSynchrone ...");
|
||||
mailSender.SendEmailAsync
|
||||
|
|
@ -35,6 +39,21 @@ namespace Yavsc.Org.Tests
|
|||
$"monthly email",
|
||||
"test boby monthly email").Wait();
|
||||
|
||||
// Assert the SMTP roundtrip was short-circuited by the
|
||||
// recording fake installed in WebServerFixture: exactly
|
||||
// one client was created and it saw the expected sequence
|
||||
// of Connect → Authenticate → Send → Disconnect.
|
||||
var client = Assert.Single(factory.Created);
|
||||
Assert.Equal(
|
||||
new[]
|
||||
{
|
||||
RecordingSmtpCallKind.Connect,
|
||||
RecordingSmtpCallKind.Authenticate,
|
||||
RecordingSmtpCallKind.Send,
|
||||
RecordingSmtpCallKind.Disconnect,
|
||||
},
|
||||
client.Calls.Select(c => c.Kind).ToArray());
|
||||
Assert.Equal(_serverFixture.SiteSettings.Owner.EMail, client.LastSentMessage?.To.Mailboxes.First().Address);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,9 +17,11 @@ using System.Security.Cryptography;
|
|||
using System.Security.Cryptography.X509Certificates;
|
||||
using Yavsc;
|
||||
using Yavsc.Extensions;
|
||||
using Yavsc.Interfaces;
|
||||
using Yavsc.Models;
|
||||
using Yavsc.Server.Helpers;
|
||||
using Client = IdentityServer8.EntityFramework.Entities.Client;
|
||||
using Yavsc.Org.Tests.Fakes;
|
||||
|
||||
|
||||
namespace Yavsc.Org.Tests
|
||||
|
|
@ -40,6 +42,7 @@ namespace Yavsc.Org.Tests
|
|||
private static string? _sharedTestingUserName;
|
||||
private static string? _sharedTestingUserPassword;
|
||||
private static string? _sharedTestingUserEmail;
|
||||
private static RecordingSmtpClientFactory? _sharedSmtpClientFactory;
|
||||
private static IServiceProvider? _sharedServices;
|
||||
private static IConfiguration? _sharedConfiguration;
|
||||
private static SiteSettings? _sharedSiteSettings;
|
||||
|
|
@ -64,6 +67,7 @@ namespace Yavsc.Org.Tests
|
|||
public SiteSettings? SiteSettings { get => siteSettings; set => siteSettings = value; }
|
||||
public string? TestClientSecret { get; set; }
|
||||
public string? TestingUserEmail { get; set; }
|
||||
public RecordingSmtpClientFactory? SmtpClientFactory { get; private set; }
|
||||
public WebServerFixture()
|
||||
{
|
||||
lock (_sync)
|
||||
|
|
@ -100,6 +104,7 @@ namespace Yavsc.Org.Tests
|
|||
_sharedTestingUserName = null;
|
||||
_sharedTestingUserPassword = null;
|
||||
_sharedTestingUserEmail = null;
|
||||
_sharedSmtpClientFactory = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -116,6 +121,7 @@ namespace Yavsc.Org.Tests
|
|||
TestingUserName = _sharedTestingUserName;
|
||||
TestingUserPassword = _sharedTestingUserPassword;
|
||||
TestingUserEmail = _sharedTestingUserEmail;
|
||||
SmtpClientFactory = _sharedSmtpClientFactory;
|
||||
}
|
||||
|
||||
public async Task SetupHost()
|
||||
|
|
@ -156,8 +162,23 @@ namespace Yavsc.Org.Tests
|
|||
// IServiceCollection.AddSingleton.
|
||||
builder.Services.AddSingleton<IAuthorizationPolicyProvider, TestAuthPolicyProvider>();
|
||||
|
||||
// Replace the production ISmtpClientFactory (added later
|
||||
// by ConfigureWebAppServices via TryAddSingleton) with a
|
||||
// recording fake. By pre-registering here, the prod
|
||||
// TryAdd becomes a no-op and tests get a single shared
|
||||
// fake they can assert against.
|
||||
var smtpFactory = new RecordingSmtpClientFactory();
|
||||
builder.Services.AddSingleton<ISmtpClientFactory>(smtpFactory);
|
||||
_sharedSmtpClientFactory = smtpFactory;
|
||||
|
||||
_app = builder.ConfigureWebAppServices();
|
||||
|
||||
// Note: a recording ISmtpClientFactory is registered
|
||||
// BEFORE ConfigureWebAppServices() above (further up in
|
||||
// this method) so the production TryAddSingleton inside
|
||||
// ConfigureWebAppServices becomes a no-op.
|
||||
// _sharedSmtpClientFactory is captured there.
|
||||
|
||||
// The MSBuild target CopyYavscOrgStaticAssets in
|
||||
// Yavsc.Org.Tests.csproj mirrors the Yavsc.Org static
|
||||
// assets manifest into the test bin directory. Call
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue