diff --git a/src/Core/API/API.Specs/Mocks/MockEmailProvider.cs b/src/Core/API/API.Specs/Mocks/MockEmailProvider.cs new file mode 100644 index 0000000..28ed309 --- /dev/null +++ b/src/Core/API/API.Specs/Mocks/MockEmailProvider.cs @@ -0,0 +1,54 @@ +using Infrastructure.Email; + +namespace API.Specs.Mocks; + +/// +/// Mock email provider for testing that doesn't actually send emails. +/// Tracks sent emails for verification in tests if needed. +/// +public class MockEmailProvider : IEmailProvider +{ + public List 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 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 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; } + } +} diff --git a/src/Core/API/API.Specs/Mocks/MockEmailService.cs b/src/Core/API/API.Specs/Mocks/MockEmailService.cs index e864a0a..b320288 100644 --- a/src/Core/API/API.Specs/Mocks/MockEmailService.cs +++ b/src/Core/API/API.Specs/Mocks/MockEmailService.cs @@ -1,37 +1,18 @@ -using Infrastructure.Email; +using Domain.Entities; +using Service.Emails; namespace API.Specs.Mocks; -/// -/// Mock email service for testing that doesn't actually send emails. -/// Tracks sent emails for verification in tests if needed. -/// -public class MockEmailProvider : IEmailProvider +public class MockEmailService : IEmailService { - public List SentEmails { get; } = new(); + public List 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 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 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; } } } diff --git a/src/Core/API/API.Specs/TestApiFactory.cs b/src/Core/API/API.Specs/TestApiFactory.cs index ad63065..fef08d0 100644 --- a/src/Core/API/API.Specs/TestApiFactory.cs +++ b/src/Core/API/API.Specs/TestApiFactory.cs @@ -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(); + + // 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(); }); } } diff --git a/src/Core/Core.slnx b/src/Core/Core.slnx index a088dcd..4d6a6f1 100644 --- a/src/Core/Core.slnx +++ b/src/Core/Core.slnx @@ -21,6 +21,7 @@ + diff --git a/src/Core/Service/Service.Auth.Tests/RegisterService.test.cs b/src/Core/Service/Service.Auth.Tests/RegisterService.test.cs index b5d2675..d299785 100644 --- a/src/Core/Service/Service.Auth.Tests/RegisterService.test.cs +++ b/src/Core/Service/Service.Auth.Tests/RegisterService.test.cs @@ -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 _authRepoMock; private readonly Mock _passwordInfraMock; private readonly Mock _tokenServiceMock; + private readonly Mock _emailServiceMock; // todo handle email related test cases here private readonly RegisterService _registerService; public RegisterServiceTest() @@ -19,11 +21,14 @@ public class RegisterServiceTest _authRepoMock = new Mock(); _passwordInfraMock = new Mock(); _tokenServiceMock = new Mock(); + _emailServiceMock = new Mock(); + _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())) .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(), It.IsAny()), + Times.Once); } [Fact] @@ -307,4 +315,4 @@ public class RegisterServiceTest Times.Once ); } -} +} \ No newline at end of file diff --git a/src/Core/Service/Service.Auth/RegisterService.cs b/src/Core/Service/Service.Auth/RegisterService.cs index f701a4e..8ba257e 100644 --- a/src/Core/Service/Service.Auth/RegisterService.cs +++ b/src/Core/Service/Service.Auth/RegisterService.cs @@ -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); } } diff --git a/src/Core/Service/Service.Auth/Service.Auth.csproj b/src/Core/Service/Service.Auth/Service.Auth.csproj index 6b707d9..0057017 100644 --- a/src/Core/Service/Service.Auth/Service.Auth.csproj +++ b/src/Core/Service/Service.Auth/Service.Auth.csproj @@ -5,20 +5,18 @@ enable - - - - - - + + - - + + + diff --git a/src/Core/Service/Service.Emails/EmailService.cs b/src/Core/Service/Service.Emails/EmailService.cs new file mode 100644 index 0000000..e73aa32 --- /dev/null +++ b/src/Core/Service/Service.Emails/EmailService.cs @@ -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 + ); + } +} diff --git a/src/Core/Service/Service.Emails/Service.Emails.csproj b/src/Core/Service/Service.Emails/Service.Emails.csproj new file mode 100644 index 0000000..9e4d568 --- /dev/null +++ b/src/Core/Service/Service.Emails/Service.Emails.csproj @@ -0,0 +1,15 @@ + + + + net10.0 + enable + enable + + + + + + + + +