Update mock email system

This commit is contained in:
Aaron Po
2026-02-15 22:34:08 -05:00
parent e13691df36
commit 584c47e162
9 changed files with 155 additions and 51 deletions

View File

@@ -0,0 +1,54 @@
using Infrastructure.Email;
namespace API.Specs.Mocks;
/// <summary>
/// Mock email provider for testing that doesn't actually send emails.
/// Tracks sent emails for verification in tests if needed.
/// </summary>
public class MockEmailProvider : IEmailProvider
{
public List<SentEmail> SentEmails { get; } = new();
public Task SendAsync(string to, string subject, string body, bool isHtml = true)
{
SentEmails.Add(new SentEmail
{
To = [to],
Subject = subject,
Body = body,
IsHtml = isHtml,
SentAt = DateTime.UtcNow
});
return Task.CompletedTask;
}
public Task SendAsync(IEnumerable<string> to, string subject, string body, bool isHtml = true)
{
SentEmails.Add(new SentEmail
{
To = to.ToList(),
Subject = subject,
Body = body,
IsHtml = isHtml,
SentAt = DateTime.UtcNow
});
return Task.CompletedTask;
}
public void Clear()
{
SentEmails.Clear();
}
public class SentEmail
{
public List<string> To { get; init; } = new();
public string Subject { get; init; } = string.Empty;
public string Body { get; init; } = string.Empty;
public bool IsHtml { get; init; }
public DateTime SentAt { get; init; }
}
}

View File

@@ -1,37 +1,18 @@
using Infrastructure.Email;
using Domain.Entities;
using Service.Emails;
namespace API.Specs.Mocks;
/// <summary>
/// Mock email service for testing that doesn't actually send emails.
/// Tracks sent emails for verification in tests if needed.
/// </summary>
public class MockEmailProvider : IEmailProvider
public class MockEmailService : IEmailService
{
public List<SentEmail> SentEmails { get; } = new();
public List<RegistrationEmail> SentRegistrationEmails { get; } = new();
public Task SendAsync(string to, string subject, string body, bool isHtml = true)
public Task SendRegistrationEmailAsync(UserAccount createdUser, string confirmationToken)
{
SentEmails.Add(new SentEmail
SentRegistrationEmails.Add(new RegistrationEmail
{
To = [to],
Subject = subject,
Body = body,
IsHtml = isHtml,
SentAt = DateTime.UtcNow
});
return Task.CompletedTask;
}
public Task SendAsync(IEnumerable<string> to, string subject, string body, bool isHtml = true)
{
SentEmails.Add(new SentEmail
{
To = to.ToList(),
Subject = subject,
Body = body,
IsHtml = isHtml,
UserAccount = createdUser,
ConfirmationToken = confirmationToken,
SentAt = DateTime.UtcNow
});
@@ -40,15 +21,13 @@ public class MockEmailProvider : IEmailProvider
public void Clear()
{
SentEmails.Clear();
SentRegistrationEmails.Clear();
}
public class SentEmail
public class RegistrationEmail
{
public List<string> To { get; init; } = new();
public string Subject { get; init; } = string.Empty;
public string Body { get; init; } = string.Empty;
public bool IsHtml { get; init; }
public UserAccount UserAccount { get; init; } = null!;
public string ConfirmationToken { get; init; } = string.Empty;
public DateTime SentAt { get; init; }
}
}

View File

@@ -5,6 +5,7 @@ using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Service.Emails;
namespace API.Specs
{
@@ -16,16 +17,27 @@ namespace API.Specs
builder.ConfigureServices(services =>
{
// Replace the real email service with mock for testing
var descriptor = services.SingleOrDefault(
// Replace the real email provider with mock for testing
var emailProviderDescriptor = services.SingleOrDefault(
d => d.ServiceType == typeof(IEmailProvider));
if (descriptor != null)
if (emailProviderDescriptor != null)
{
services.Remove(descriptor);
services.Remove(emailProviderDescriptor);
}
services.AddScoped<IEmailProvider, MockEmailProvider>();
// Replace the real email service with mock for testing
var emailServiceDescriptor = services.SingleOrDefault(
d => d.ServiceType == typeof(IEmailService));
if (emailServiceDescriptor != null)
{
services.Remove(emailServiceDescriptor);
}
services.AddScoped<IEmailService, MockEmailService>();
});
}
}

View File

@@ -21,6 +21,7 @@
</Folder>
<Folder Name="/Service/">
<Project Path="Service/Service.Auth.Tests/Service.Auth.Tests.csproj" />
<Project Path="Service/Service.Emails/Service.Emails.csproj" />
<Project Path="Service/Service.UserManagement/Service.UserManagement.csproj" />
<Project Path="Service\Service.Auth\Service.Auth.csproj" />
</Folder>

View File

@@ -4,6 +4,7 @@ using FluentAssertions;
using Infrastructure.PasswordHashing;
using Infrastructure.Repository.Auth;
using Moq;
using Service.Emails;
namespace Service.Auth.Tests;
@@ -12,6 +13,7 @@ public class RegisterServiceTest
private readonly Mock<IAuthRepository> _authRepoMock;
private readonly Mock<IPasswordInfrastructure> _passwordInfraMock;
private readonly Mock<ITokenService> _tokenServiceMock;
private readonly Mock<IEmailService> _emailServiceMock; // todo handle email related test cases here
private readonly RegisterService _registerService;
public RegisterServiceTest()
@@ -19,11 +21,14 @@ public class RegisterServiceTest
_authRepoMock = new Mock<IAuthRepository>();
_passwordInfraMock = new Mock<IPasswordInfrastructure>();
_tokenServiceMock = new Mock<ITokenService>();
_emailServiceMock = new Mock<IEmailService>();
_registerService = new RegisterService(
_authRepoMock.Object,
_passwordInfraMock.Object,
_tokenServiceMock.Object
_tokenServiceMock.Object,
_emailServiceMock.Object
);
}
@@ -92,6 +97,7 @@ public class RegisterServiceTest
.Setup(x => x.GenerateRefreshToken(It.IsAny<UserAccount>()))
.Returns("refresh-token");
// Act
var result = await _registerService.RegisterAsync(
userAccount,
@@ -128,6 +134,8 @@ public class RegisterServiceTest
),
Times.Once
);
_emailServiceMock.Verify(x => x.SendRegistrationEmailAsync(It.IsAny<UserAccount>(), It.IsAny<string>()),
Times.Once);
}
[Fact]

View File

@@ -4,13 +4,15 @@ using Infrastructure.Email;
using Infrastructure.Email.Templates.Rendering;
using Infrastructure.PasswordHashing;
using Infrastructure.Repository.Auth;
using Service.Emails;
namespace Service.Auth;
public class RegisterService(
IAuthRepository authRepo,
IPasswordInfrastructure passwordInfrastructure,
ITokenService tokenService
ITokenService tokenService,
IEmailService emailService
) : IRegisterService
{
private async Task ValidateUserDoesNotExist(UserAccount userAccount)
@@ -51,6 +53,9 @@ public class RegisterService(
var accessToken = tokenService.GenerateAccessToken(createdUser);
var refreshToken = tokenService.GenerateRefreshToken(createdUser);
// send confirmation email
await emailService.SendRegistrationEmailAsync(createdUser, "some-confirmation-token");
return new AuthServiceReturn(createdUser, refreshToken, accessToken);
}
}

View File

@@ -5,20 +5,18 @@
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference
Include="Konscious.Security.Cryptography.Argon2"
Version="1.3.1"
/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\Domain.Entities\Domain.Entities.csproj" />
<ProjectReference Include="..\..\Domain.Exceptions\Domain.Exceptions.csproj" />
<ProjectReference Include="..\..\Infrastructure\Infrastructure.Email\Infrastructure.Email.csproj" />
<ProjectReference Include="..\..\Infrastructure\Infrastructure.Email.Templates\Infrastructure.Email.Templates.csproj" />
<ProjectReference
Include="..\..\Infrastructure\Infrastructure.Email\Infrastructure.Email.csproj" />
<ProjectReference
Include="..\..\Infrastructure\Infrastructure.Email.Templates\Infrastructure.Email.Templates.csproj" />
<ProjectReference Include="..\..\Infrastructure\Infrastructure.Jwt\Infrastructure.Jwt.csproj" />
<ProjectReference Include="..\..\Infrastructure\Infrastructure.Repository\Infrastructure.Repository.csproj" />
<ProjectReference Include="..\..\Infrastructure\Infrastructure.PasswordHashing\Infrastructure.PasswordHashing.csproj" />
<ProjectReference
Include="..\..\Infrastructure\Infrastructure.Repository\Infrastructure.Repository.csproj" />
<ProjectReference
Include="..\..\Infrastructure\Infrastructure.PasswordHashing\Infrastructure.PasswordHashing.csproj" />
<ProjectReference Include="..\Service.Emails\Service.Emails.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,32 @@
using Domain.Entities;
using Infrastructure.Email;
using Infrastructure.Email.Templates.Rendering;
namespace Service.Emails;
public interface IEmailService
{
public Task SendRegistrationEmailAsync(UserAccount createdUser, string confirmationToken);
}
public class EmailService(
IEmailProvider emailProvider,
IEmailTemplateProvider emailTemplateProvider) : IEmailService
{
public async Task SendRegistrationEmailAsync(UserAccount createdUser, string confirmationToken)
{
var confirmationLink = $"https://thebiergarten.app/confirm?token={confirmationToken}";
var emailHtml = await emailTemplateProvider.RenderUserRegisteredEmailAsync(
createdUser.FirstName,
confirmationLink
);
await emailProvider.SendAsync(
createdUser.Email,
"Welcome to The Biergarten App!",
emailHtml,
isHtml: true
);
}
}

View File

@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\Domain.Entities\Domain.Entities.csproj" />
<ProjectReference Include="..\..\Infrastructure\Infrastructure.Email.Templates\Infrastructure.Email.Templates.csproj" />
<ProjectReference Include="..\..\Infrastructure\Infrastructure.Email\Infrastructure.Email.csproj" />
</ItemGroup>
</Project>