> logger,
+ IEmailService emailService
+ )
+ {
+ _logger = logger;
+ _emailService = emailService;
+ }
+
+ ///
+ public async Task SendConfirmationLinkAsync(TUser user, string email, string confirmationLink)
+ {
+ string subject = "Confirmez votre adresse email";
+ string htmlContent = GetConfirmationEmailTemplate(confirmationLink);
+
+ await SendEmailAsync(email, subject, htmlContent);
+ }
+
+ public async Task SendPasswordResetLinkAsync(TUser user, string email, string resetLink)
+ {
+ string subject = "Réinitialisation de votre mot de passe";
+ string htmlContent = GetPasswordResetTemplate(resetLink);
+
+ await SendEmailAsync(email, subject, htmlContent);
+ }
+
+ public async Task SendPasswordResetCodeAsync(TUser user, string email, string resetCode)
+ {
+ string subject = "Votre code de réinitialisation de mot de passe";
+ string htmlContent = GetPasswordResetTemplate(resetCode);
+
+ await SendEmailAsync(email, subject, htmlContent);
+ }
+
+ protected virtual async Task SendEmailAsync(string to, string subject, string htmlContent)
+ {
+ EmailMessage email = new(to, subject, htmlContent);
+
+ try
+ {
+ await _emailService.SendAsync(email);
+
+ _logger.LogInformation("Email sent successfully to {Email}", email.To);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error sending email to {Email}", email.To);
+ throw;
+ }
+ }
+
+ private string GetConfirmationEmailTemplate(string confirmationLink)
+ {
+ return $@"
+
+
+ Confirmez votre adresse email
+ Pour confirmer votre compte, veuillez cliquer sur le lien ci-dessous :
+ Confirmer mon compte
+ Si le lien ne fonctionne pas, copiez et collez cette URL dans votre navigateur :
+ {confirmationLink}
+
+ ";
+ }
+
+ private string GetPasswordResetTemplate(string resetLink)
+ {
+ return $@"
+
+
+ Réinitialisation de votre mot de passe
+ Pour réinitialiser votre mot de passe, cliquez sur le lien ci-dessous :
+ Réinitialiser mon mot de passe
+ Si le lien ne fonctionne pas, copiez et collez cette URL dans votre navigateur :
+ {resetLink}
+
+ ";
+ }
+
+ private string GetPasswordResetCodeTemplate(string resetCode)
+ {
+ return $@"
+
+
+ Code de réinitialisation de mot de passe
+ Voici votre code de réinitialisation de mot de passe :
+ {resetCode}
+ Ce code est valable pendant une durée limitée.
+
+ ";
+ }
+}
diff --git a/src/Common/Core.Identity/ServiceCollectionExtions.cs b/src/Common/Core.Identity/ServiceCollectionExtions.cs
index 8f4123e..e9f2692 100755
--- a/src/Common/Core.Identity/ServiceCollectionExtions.cs
+++ b/src/Common/Core.Identity/ServiceCollectionExtions.cs
@@ -2,7 +2,6 @@
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Identity;
-using Microsoft.AspNetCore.Identity.UI.Services;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
@@ -81,7 +80,10 @@ IConfiguration configuration
public static IServiceCollection AddEmailSender(this IServiceCollection services)
{
- services.AddTransient();
+ services.AddTransient<
+ IEmailSender,
+ IdentityEmailSender
+ >();
return services;
}
diff --git a/src/Place.API/Place.API.csproj b/src/Place.API/Place.API.csproj
index c8d1128..67cda42 100644
--- a/src/Place.API/Place.API.csproj
+++ b/src/Place.API/Place.API.csproj
@@ -10,6 +10,7 @@
+
@@ -17,6 +18,7 @@
+
diff --git a/src/Place.API/Program.cs b/src/Place.API/Program.cs
index d91a41c..c11ebfd 100644
--- a/src/Place.API/Program.cs
+++ b/src/Place.API/Program.cs
@@ -1,6 +1,5 @@
using Account;
using Core.Framework;
-using Core.Identity;
using Core.MediatR;
using Identity;
using Microsoft.AspNetCore.Builder;
@@ -9,7 +8,11 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Place.API;
+using Place.Notification;
+using Place.Notification.Email;
using Scalar.AspNetCore;
+using SendGrid;
+using SendGrid.Helpers.Mail;
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
IConfiguration configuration = builder.Configuration;
@@ -21,6 +24,8 @@
builder.Services.AddAccountModule(configuration);
+builder.Services.AddEmailService(configuration);
+
builder.Services.AddCoreMediatR(typeof(IIdentityRoot).Assembly);
builder.Services.AddCoreMediatR(typeof(IAccountRoot).Assembly);
diff --git a/src/Place.API/appsettings.Development.json b/src/Place.API/appsettings.Development.json
index 0c208ae..bb58892 100644
--- a/src/Place.API/appsettings.Development.json
+++ b/src/Place.API/appsettings.Development.json
@@ -4,5 +4,80 @@
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
+ },
+ "app": {
+ "name": "Place Profile Api"
+ },
+ "Serilog": {
+ "applicationName": "identity-service",
+ "excludePaths": ["/ping", "/metrics"],
+ "level": "information",
+ "console": {
+ "enabled": true
+ },
+ "file": {
+ "enabled": true,
+ "path": "logs/logs.txt",
+ "interval": "day"
+ },
+ "seq": {
+ "enabled": true,
+ "url": "http://localhost:5341",
+ "token": "secret"
+ }
+ },
+ "Swagger": {
+ "Title": "Place Profile Api",
+ "Description": "Place Profile Api documentation",
+ "Version": "v1",
+ "EnableBearerAuth": true,
+ "SecuritySchemaName": "Bearer",
+ "SecurityScheme": "JWT",
+ "SecurityDescription": "Utiliser le format: Bearer {votre_token}",
+ "EnableVersioning": true,
+ "RoutePrefix": "swagger"
+ },
+ "ApiVersioning": {
+ "DefaultApiVersionMajor": 1,
+ "DefaultApiVersionMinor": 0,
+ "AssumeDefaultVersionWhenUnspecified": true,
+ "ReportApiVersions": true,
+ "ApiVersionReaderType": "Combine",
+ "ReaderOptions": {
+ "HeaderName": "x-api-version",
+ "QueryStringParam": "api-version",
+ "MediaTypeParam": "v"
+ },
+ "GroupNameFormat": "'v'VVV",
+ "SubstituteApiVersionInUrl": true,
+ "DeprecatedVersionOptions": {
+ "DeprecationMessage": "Cette version de l'API est obsolète. Veuillez migrer vers la version la plus récente.",
+ "SunsetDate": "2025-12-31",
+ "DocumentationUrl": "https://api.monsite.com/deprecation-policy"
+ },
+ "ApiExplorerOptions": {
+ "GroupNameFormat": "'v'VVV",
+ "SubstituteApiVersionInUrl": true,
+ "UrlFormat": "v{version:apiVersion}",
+ "AddApiVersionParametersWhenVersionNeutral": true
+ }
+ },
+ "ConnectionStrings": {
+ "PlaceDb": "Server=localhost;Port=5499;Database=place_db;User Id=postgres;Password=postgres;Include Error Detail=true"
+ },
+ "Email": {
+ "Provider": "SendGrid",
+ "From": "support@place.cm",
+ "FromName": "Support Place",
+ "Smtp": {
+ "Host": "smtp.sendgrid.net",
+ "Port": 587,
+ "Username": "username@gmail.com",
+ "Password": "Pass0rd123!"
+ },
+ "SendGrid": {
+ "ApiKey": null
+ }
}
+
}
diff --git a/src/Place.API/appsettings.json b/src/Place.API/appsettings.json
index 683182a..c88437e 100644
--- a/src/Place.API/appsettings.json
+++ b/src/Place.API/appsettings.json
@@ -5,40 +5,8 @@
"Microsoft.AspNetCore": "Warning"
}
},
- "apiVersioning": {
- "enabled": true,
- "defaultVersion": "1.0",
- "assumeDefaultVersionWhenUnspecified": true,
- "reportApiVersions": true,
- "addVersionParamToNeutralEndpoints": false,
- "versionReaderType": "UrlSegment",
- "headerName": "X-Api-Version",
- "queryStringParam": "api-version",
- "mediaTypeParam": "v"
- },
- "swagger": {
- "enabled": true,
- "title": "Place API",
- "versions": [
- "1.0",
- "2.0"
- ],
- "useAuthentication": true,
- "enableDownload": true,
- "useSwaggerUI": true,
- "useReDoc": true,
- "swaggerUIRoute": "swagger",
- "reDocRoute": "api-docs",
- "uiOptions": {
- "docExpansion": "List",
- "defaultModelsExpandDepth": 1
- }
- },
"app": {
- "name": "Place API v1.0"
- },
- "ConnectionStrings": {
- "PlaceDb": "Server=localhost;Port=5499;Database=place_db;User Id=postgres;Password=postgres;Include Error Detail=true"
+ "name": "Place Profile Api"
},
"Serilog": {
"applicationName": "identity-service",
@@ -53,9 +21,48 @@
"interval": "day"
},
"seq": {
- "enabled": false,
+ "enabled": true,
"url": "http://localhost:5341",
"token": "secret"
}
+ },
+ "Swagger": {
+ "Title": "Place Profile Api",
+ "Description": "Place Profile Api documentation",
+ "Version": "v1",
+ "EnableBearerAuth": true,
+ "SecuritySchemaName": "Bearer",
+ "SecurityScheme": "JWT",
+ "SecurityDescription": "Utiliser le format: Bearer {votre_token}",
+ "EnableVersioning": true,
+ "RoutePrefix": "swagger"
+ },
+ "ApiVersioning": {
+ "DefaultApiVersionMajor": 1,
+ "DefaultApiVersionMinor": 0,
+ "AssumeDefaultVersionWhenUnspecified": true,
+ "ReportApiVersions": true,
+ "ApiVersionReaderType": "Combine",
+ "ReaderOptions": {
+ "HeaderName": "x-api-version",
+ "QueryStringParam": "api-version",
+ "MediaTypeParam": "v"
+ },
+ "GroupNameFormat": "'v'VVV",
+ "SubstituteApiVersionInUrl": true,
+ "DeprecatedVersionOptions": {
+ "DeprecationMessage": "Cette version de l'API est obsolète. Veuillez migrer vers la version la plus récente.",
+ "SunsetDate": "2025-12-31",
+ "DocumentationUrl": "https://api.monsite.com/deprecation-policy"
+ },
+ "ApiExplorerOptions": {
+ "GroupNameFormat": "'v'VVV",
+ "SubstituteApiVersionInUrl": true,
+ "UrlFormat": "v{version:apiVersion}",
+ "AddApiVersionParametersWhenVersionNeutral": true
+ }
+ },
+ "ConnectionStrings": {
+ "PlaceDb": "Server=localhost;Port=5499;Database=place_db;User Id=postgres;Password=postgres;Include Error Detail=true"
}
}
diff --git a/src/Place.Notification/Email/EmailMessage.cs b/src/Place.Notification/Email/EmailMessage.cs
new file mode 100644
index 0000000..5c17062
--- /dev/null
+++ b/src/Place.Notification/Email/EmailMessage.cs
@@ -0,0 +1,30 @@
+using System;
+
+namespace Place.Notification.Email;
+
+public class EmailMessage
+{
+ public string To { get; private set; }
+ public string Subject { get; private set; }
+ public string Body { get; private set; }
+
+ public EmailMessage(string to, string subject, string body)
+ {
+ if (string.IsNullOrWhiteSpace(to))
+ {
+ throw new ArgumentNullException(nameof(to));
+ }
+ if (string.IsNullOrWhiteSpace(subject))
+ {
+ throw new ArgumentNullException(nameof(subject));
+ }
+ if (string.IsNullOrWhiteSpace(body))
+ {
+ throw new ArgumentNullException(nameof(body));
+ }
+
+ To = to;
+ Subject = subject;
+ Body = body;
+ }
+}
diff --git a/src/Place.Notification/Email/EmailOptions.cs b/src/Place.Notification/Email/EmailOptions.cs
new file mode 100644
index 0000000..34343b9
--- /dev/null
+++ b/src/Place.Notification/Email/EmailOptions.cs
@@ -0,0 +1,14 @@
+using Place.Notification.Email.SendGrid;
+using Place.Notification.Email.SMTP;
+
+namespace Place.Notification.Email;
+
+public class EmailOptions
+{
+ public static string SectionName = "Email";
+ public EmailProvider? Provider { get; set; } = EmailProvider.SendGrid;
+ public SmtpOptions? Smtp { get; set; }
+ public SendGridOptions? SendGrid { get; set; }
+ public string? FromAddress { get; set; }
+ public string? FromName { get; set; }
+}
diff --git a/src/Place.Notification/Email/EmailProvider.cs b/src/Place.Notification/Email/EmailProvider.cs
new file mode 100644
index 0000000..0bd554a
--- /dev/null
+++ b/src/Place.Notification/Email/EmailProvider.cs
@@ -0,0 +1,7 @@
+namespace Place.Notification.Email;
+
+public enum EmailProvider
+{
+ Smtp,
+ SendGrid,
+}
diff --git a/src/Place.Notification/Email/EmailServiceFactory.cs b/src/Place.Notification/Email/EmailServiceFactory.cs
new file mode 100644
index 0000000..8f214ab
--- /dev/null
+++ b/src/Place.Notification/Email/EmailServiceFactory.cs
@@ -0,0 +1,28 @@
+using System;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Options;
+using Place.Notification.Email.SendGrid;
+using Place.Notification.Email.SMTP;
+
+namespace Place.Notification.Email;
+
+public class EmailServiceFactory(IServiceProvider serviceProvider, IOptions options)
+ : IEmailServiceFactory
+{
+ private readonly EmailOptions _options = options.Value;
+
+ public IEmailService Create()
+ {
+ return _options.Provider switch
+ {
+ EmailProvider.Smtp => serviceProvider.GetRequiredService(),
+ EmailProvider.SendGrid => serviceProvider.GetRequiredService(),
+ _ => throw new ArgumentException("Invalid email provider"),
+ };
+ }
+}
+
+public interface IEmailServiceFactory
+{
+ public IEmailService Create();
+}
diff --git a/src/Place.Notification/Email/SMTP/ISmtpClient.cs b/src/Place.Notification/Email/SMTP/ISmtpClient.cs
new file mode 100644
index 0000000..69807f5
--- /dev/null
+++ b/src/Place.Notification/Email/SMTP/ISmtpClient.cs
@@ -0,0 +1,10 @@
+using System;
+using System.Net.Mail;
+using System.Threading.Tasks;
+
+namespace Place.Notification.Email.SMTP;
+
+public interface ISmtpClient : IDisposable
+{
+ Task SendMailAsync(MailMessage message);
+}
diff --git a/src/Place.Notification/Email/SMTP/ISmtpClientFactory.cs b/src/Place.Notification/Email/SMTP/ISmtpClientFactory.cs
new file mode 100644
index 0000000..0ffb55b
--- /dev/null
+++ b/src/Place.Notification/Email/SMTP/ISmtpClientFactory.cs
@@ -0,0 +1,6 @@
+namespace Place.Notification.Email.SMTP;
+
+public interface ISmtpClientFactory
+{
+ ISmtpClient CreateClient();
+}
diff --git a/src/Place.Notification/Email/SMTP/SmtpClientFactory.cs b/src/Place.Notification/Email/SMTP/SmtpClientFactory.cs
new file mode 100644
index 0000000..914a550
--- /dev/null
+++ b/src/Place.Notification/Email/SMTP/SmtpClientFactory.cs
@@ -0,0 +1,22 @@
+using System.Net;
+using System.Net.Mail;
+using Microsoft.Extensions.Options;
+
+namespace Place.Notification.Email.SMTP;
+
+public class SmtpClientFactory(IOptions settings) : ISmtpClientFactory
+{
+ private readonly EmailOptions _options = settings.Value;
+
+ public ISmtpClient CreateClient()
+ {
+ SmtpClient client =
+ new(_options.Smtp?.Host, _options!.Smtp!.Port)
+ {
+ Credentials = new NetworkCredential(_options.Smtp.Username, _options.Smtp.Password),
+ EnableSsl = true,
+ };
+
+ return new SmtpClientWrapper(client);
+ }
+}
diff --git a/src/Place.Notification/Email/SMTP/SmtpClientWrapper.cs b/src/Place.Notification/Email/SMTP/SmtpClientWrapper.cs
new file mode 100644
index 0000000..73b19be
--- /dev/null
+++ b/src/Place.Notification/Email/SMTP/SmtpClientWrapper.cs
@@ -0,0 +1,17 @@
+using System.Net.Mail;
+using System.Threading.Tasks;
+
+namespace Place.Notification.Email.SMTP;
+
+public class SmtpClientWrapper(SmtpClient smtpClient) : ISmtpClient
+{
+ public async Task SendMailAsync(MailMessage message)
+ {
+ await smtpClient.SendMailAsync(message);
+ }
+
+ public void Dispose()
+ {
+ smtpClient?.Dispose();
+ }
+}
diff --git a/src/Place.Notification/Email/SMTP/SmtpEmailService.cs b/src/Place.Notification/Email/SMTP/SmtpEmailService.cs
new file mode 100644
index 0000000..18bf835
--- /dev/null
+++ b/src/Place.Notification/Email/SMTP/SmtpEmailService.cs
@@ -0,0 +1,41 @@
+using System;
+using System.Net.Mail;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+
+namespace Place.Notification.Email.SMTP;
+
+public class SmtpEmailService(
+ IOptions settings,
+ ISmtpClientFactory smtpClientFactory,
+ ILogger logger
+) : IEmailService
+{
+ private readonly EmailOptions _settings = settings.Value;
+
+ public async Task SendAsync(EmailMessage email)
+ {
+ using ISmtpClient client = smtpClientFactory.CreateClient();
+ using MailMessage mailMessage =
+ new()
+ {
+ From = new MailAddress(_settings!.FromAddress!),
+ Subject = email.Subject,
+ Body = email.Body,
+ IsBodyHtml = true,
+ };
+ mailMessage.To.Add(email.To);
+
+ try
+ {
+ await client.SendMailAsync(mailMessage);
+ logger.LogInformation("Email sent successfully to {Email}", email.To);
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "Error sending email to {Email}", email.To);
+ throw;
+ }
+ }
+}
diff --git a/src/Place.Notification/Email/SMTP/SmtpOptions.cs b/src/Place.Notification/Email/SMTP/SmtpOptions.cs
new file mode 100644
index 0000000..ff72684
--- /dev/null
+++ b/src/Place.Notification/Email/SMTP/SmtpOptions.cs
@@ -0,0 +1,16 @@
+using System.Threading.Tasks;
+
+namespace Place.Notification.Email.SMTP;
+
+public class SmtpOptions
+{
+ public string? Host { get; set; }
+ public int Port { get; set; }
+ public string? Username { get; set; }
+ public string? Password { get; set; }
+}
+
+public interface IEmailService
+{
+ Task SendAsync(EmailMessage message);
+}
diff --git a/src/Place.Notification/Email/SendGrid/SendGridEmailService.cs b/src/Place.Notification/Email/SendGrid/SendGridEmailService.cs
new file mode 100644
index 0000000..cb066ee
--- /dev/null
+++ b/src/Place.Notification/Email/SendGrid/SendGridEmailService.cs
@@ -0,0 +1,55 @@
+using System;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using Place.Notification.Email.SMTP;
+using SendGrid;
+using SendGrid.Helpers.Mail;
+
+namespace Place.Notification.Email.SendGrid;
+
+public class SendGridEmailService(
+ IOptions options,
+ ILogger logger
+) : IEmailService
+{
+ private readonly SendGridClient _client = new(options.Value.SendGrid!.ApiKey);
+ private readonly string _fromAddress = options.Value.FromAddress!;
+ private readonly string _fromName = options.Value.FromName!;
+
+ public async Task SendAsync(EmailMessage email)
+ {
+ try
+ {
+ SendGridMessage msg =
+ new()
+ {
+ From = new EmailAddress(_fromAddress, _fromName),
+ Subject = email.Subject,
+ PlainTextContent = email.Body,
+ HtmlContent = email.Body,
+ };
+ msg.AddTo(new EmailAddress(email.To));
+
+ Response? response = await _client.SendEmailAsync(msg);
+
+ if (response.IsSuccessStatusCode)
+ {
+ logger.LogInformation($"Email sent successfully via SendGrid to {email.To}");
+ }
+ else
+ {
+ throw new Exception($"SendGrid returned status code: {response.StatusCode}");
+ }
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(
+ "Error sending email via SendGrid to {email.To} with error {Error}",
+ email.To,
+ ex
+ );
+ throw;
+ }
+ }
+}
diff --git a/src/Place.Notification/Email/SendGrid/SendGridOptions.cs b/src/Place.Notification/Email/SendGrid/SendGridOptions.cs
new file mode 100644
index 0000000..27eea60
--- /dev/null
+++ b/src/Place.Notification/Email/SendGrid/SendGridOptions.cs
@@ -0,0 +1,6 @@
+namespace Place.Notification.Email.SendGrid;
+
+public class SendGridOptions
+{
+ public string? ApiKey { get; set; }
+}
diff --git a/src/Place.Notification/Email/ServiceCollectionExtensions.cs b/src/Place.Notification/Email/ServiceCollectionExtensions.cs
new file mode 100644
index 0000000..01bb4db
--- /dev/null
+++ b/src/Place.Notification/Email/ServiceCollectionExtensions.cs
@@ -0,0 +1,31 @@
+using Core;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Place.Notification.Email.SendGrid;
+using Place.Notification.Email.SMTP;
+
+namespace Place.Notification.Email;
+
+public static class ServiceCollectionExtensions
+{
+ public static IServiceCollection AddEmailService(
+ this IServiceCollection services,
+ IConfiguration configuration
+ )
+ {
+ IConfigurationSection section = configuration.GetSection(EmailOptions.SectionName);
+
+ section.BindOptions();
+ services.AddScoped();
+ services.AddScoped();
+ services.AddScoped();
+
+ services.AddScoped(sp =>
+ {
+ IEmailServiceFactory factory = sp.GetRequiredService();
+ return factory.Create();
+ });
+
+ return services;
+ }
+}
diff --git a/src/Place.Notification/Place.Notification.csproj b/src/Place.Notification/Place.Notification.csproj
new file mode 100644
index 0000000..b3e0bf0
--- /dev/null
+++ b/src/Place.Notification/Place.Notification.csproj
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/Core.Identity.UnitTests/Core.Identity.UnitTests.csproj b/tests/Core.Identity.UnitTests/Core.Identity.UnitTests.csproj
new file mode 100644
index 0000000..20e5dcc
--- /dev/null
+++ b/tests/Core.Identity.UnitTests/Core.Identity.UnitTests.csproj
@@ -0,0 +1,25 @@
+
+
+
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/Core.Identity.UnitTests/IdentityEmailSenderTests.cs b/tests/Core.Identity.UnitTests/IdentityEmailSenderTests.cs
new file mode 100644
index 0000000..fa130b6
--- /dev/null
+++ b/tests/Core.Identity.UnitTests/IdentityEmailSenderTests.cs
@@ -0,0 +1,122 @@
+using System.Threading.Tasks;
+using FluentAssertions;
+using Microsoft.Extensions.Logging;
+using NSubstitute;
+using Place.Notification;
+using Place.Notification.Email;
+using Place.Notification.Email.SMTP;
+
+namespace Core.Identity.UnitTests;
+
+[Trait("Category", "Unit")]
+[Trait("Category", "IdentitySendEmail")]
+public class IdentityEmailSenderTests
+{
+ private readonly ILogger> _logger = Substitute.For<
+ ILogger>
+ >();
+ private readonly IEmailService _emailService = Substitute.For();
+ private readonly TestUser _testUser = new() { Email = TestEmail };
+ private const string TestEmail = "test@example.com";
+
+ [Fact]
+ public async Task SendConfirmationLinkAsync_Should_SendEmailWithCorrectContent()
+ {
+ // Arrange
+ IdentityEmailSender sender = new(_logger, _emailService);
+ const string confirmationLink = "https://example.com/confirm";
+ EmailMessage? capturedEmail = null;
+
+ _emailService
+ .SendAsync(Arg.Do(email => capturedEmail = email))
+ .Returns(Task.CompletedTask);
+
+ // Act
+ await sender.SendConfirmationLinkAsync(_testUser, TestEmail, confirmationLink);
+
+ // Assert
+ await _emailService.Received(1).SendAsync(Arg.Any());
+
+ capturedEmail.Should().NotBeNull();
+ capturedEmail?.To.Should().Be(TestEmail);
+ capturedEmail?.Subject.Should().Be("Confirmez votre adresse email");
+ capturedEmail?.Body.Should().Contain(confirmationLink);
+ }
+
+ [Fact]
+ public async Task SendPasswordResetLinkAsync_Should_SendEmailWithCorrectContent()
+ {
+ // Arrange
+ IdentityEmailSender sender = new(_logger, _emailService);
+ const string resetLink = "https://example.com/reset";
+ EmailMessage? capturedEmail = null;
+
+ _emailService
+ .SendAsync(Arg.Do(email => capturedEmail = email))
+ .Returns(Task.CompletedTask);
+
+ // Act
+ await sender.SendPasswordResetLinkAsync(_testUser, TestEmail, resetLink);
+
+ // Assert
+ await _emailService.Received(1).SendAsync(Arg.Any());
+
+ capturedEmail.Should().NotBeNull();
+ capturedEmail?.To.Should().Be(TestEmail);
+ capturedEmail?.Subject.Should().Be("Réinitialisation de votre mot de passe");
+ capturedEmail?.Body.Should().Contain(resetLink);
+ }
+
+ [Fact]
+ public async Task SendPasswordResetCodeAsync_Should_SendEmailWithCorrectContent()
+ {
+ // Arrange
+ IdentityEmailSender sender = new(_logger, _emailService);
+ const string resetCode = "123456";
+ EmailMessage? capturedEmail = null;
+
+ _emailService
+ .SendAsync(Arg.Do(email => capturedEmail = email))
+ .Returns(Task.CompletedTask);
+
+ // Act
+ await sender.SendPasswordResetCodeAsync(_testUser, TestEmail, resetCode);
+
+ // Assert
+ await _emailService.Received(1).SendAsync(Arg.Any());
+
+ capturedEmail.Should().NotBeNull();
+ capturedEmail?.To.Should().Be(TestEmail);
+ capturedEmail?.Subject.Should().Be("Votre code de réinitialisation de mot de passe");
+ capturedEmail?.Body.Should().Contain(resetCode);
+ }
+
+ [Theory]
+ [InlineData(EmailProvider.Smtp)]
+ [InlineData(EmailProvider.SendGrid)]
+ public async Task SendEmail_Should_Work_WithDifferentProviders(EmailProvider provider)
+ {
+ // Arrange
+
+ IdentityEmailSender sender = new(_logger, _emailService);
+
+ // Act
+ await sender.SendConfirmationLinkAsync(_testUser, TestEmail, "https://example.com");
+
+ // Assert
+ await _emailService
+ .Received(1)
+ .SendAsync(
+ Arg.Is(email =>
+ email.To == TestEmail
+ && email.Subject == "Confirmez votre adresse email"
+ && email.Body.Contains("https://example.com")
+ )
+ );
+ }
+}
+
+public class TestUser
+{
+ public string Email { get; set; } = null!;
+}