MessagingServiceClient now implements IMessagingServiceClient and takes a PublishEmailViaSmtpCommand instead of the removed Email record. Add MessagingServiceHttpClient, which posts the same command to the API via IHttpClientFactory. Replace DependencyInjectionExtensions with a single DependencyInjection entry point that registers exactly one client implementation and throws when a second one is added. It exposes AddMessagingServiceClient for RabbitMQ and AddMessagingServiceHttpClient for the HTTP API, both with an optional license key. Bump the package to 2.0.0 because the SendAsync signatures and the removed Email type are breaking changes, add Microsoft.Extensions.Http, and update the integration test to exercise the new contract.
177 lines
8.4 KiB
C#
177 lines
8.4 KiB
C#
using System.Linq.Expressions;
|
|
using System.Text;
|
|
using System.Text.Json;
|
|
using DigitalData.MessagingService.Application.Common.Dto;
|
|
using DigitalData.MessagingService.Application.Common.Interfaces;
|
|
using DigitalData.MessagingService.Application.Common.Interfaces.Repositories;
|
|
using DigitalData.MessagingService.Application.EmailAccounts.Queries;
|
|
using DigitalData.MessagingService.Application.EmailSending.Commands;
|
|
using DigitalData.MessagingService.Client;
|
|
using DigitalData.MessagingService.Domain.Entities;
|
|
using DigitalData.MessagingService.Publisher;
|
|
using DigitalData.MessagingService.RabbitMQ;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
|
|
namespace DigitalData.MessagingService.Tests.Integration;
|
|
|
|
public sealed class MessagingServiceClientTests : IAsyncDisposable
|
|
{
|
|
private const string SenderAddress = "test-flow@digitaldata.works";
|
|
private const string RecipientAddress = "htek0100@gmail.com";
|
|
|
|
private readonly ServiceProvider _serviceProvider;
|
|
private readonly IMessagingServiceClient _client;
|
|
private readonly RabbitMqConnectionFactory _factory;
|
|
|
|
public MessagingServiceClientTests()
|
|
{
|
|
var services = new ServiceCollection();
|
|
services.AddLogging();
|
|
|
|
services.AddMessagingServiceClient(RabbitMqTestConfig.Apply, LoadLuckyPennySoftLicenseKey());
|
|
services.AddSingleton<IRepository<EmailAccount>>(
|
|
new InMemoryEmailAccountRepository(
|
|
[
|
|
new EmailAccount
|
|
{
|
|
Id = 1,
|
|
Username = SenderAddress,
|
|
Password = "ddemail108",
|
|
SmtpServer = "kundencenter.triplew.de",
|
|
SmtpPort = 465,
|
|
SmtpUseSsl = true,
|
|
UseOAuth2 = false
|
|
}
|
|
]));
|
|
|
|
_serviceProvider = services.BuildServiceProvider();
|
|
_client = _serviceProvider.GetRequiredService<IMessagingServiceClient>();
|
|
_factory = _serviceProvider.GetRequiredService<RabbitMqConnectionFactory>();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sends an email through <see cref="IMessagingServiceClient"/> using the real RabbitMQ setup.
|
|
/// This test intentionally uses manual mailbox verification because the shared queue can be
|
|
/// consumed by other services during test execution.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task SendAsync_WithDependencyInjectionAndRealBroker_PublishesExpectedMessage()
|
|
{
|
|
var subject = $"MessagingServiceClient Integration Test {Guid.NewGuid():N}";
|
|
|
|
var command = new PublishEmailViaSmtpCommand
|
|
{
|
|
Sender = new GetEmailAccountQuery
|
|
{
|
|
Username = SenderAddress
|
|
},
|
|
Recipients = [RecipientAddress],
|
|
Subject = subject,
|
|
Body = "<p>Realistic test mail via MessagingServiceClient.</p>",
|
|
IsHtml = true
|
|
};
|
|
|
|
await _client.SendAsync(command);
|
|
|
|
Assert.True(true, "Verify in mailbox.");
|
|
}
|
|
|
|
private async Task<SendingEmailEvent?> FindMessageBySubjectAsync(string subject, int maxMessages = 100)
|
|
{
|
|
var connection = await _factory.GetDefaultConnectionAsync();
|
|
await using var channel = await connection.CreateChannelAsync();
|
|
|
|
var inspectedTags = new List<ulong>();
|
|
|
|
for (var i = 0; i < maxMessages; i++)
|
|
{
|
|
var result = await channel.BasicGetAsync(RabbitMqTestConfig.QueueName, autoAck: false);
|
|
if (result is null)
|
|
break;
|
|
|
|
inspectedTags.Add(result.DeliveryTag);
|
|
|
|
var json = Encoding.UTF8.GetString(result.Body.ToArray());
|
|
var message = JsonSerializer.Deserialize<SendingEmailEvent>(json);
|
|
if (message?.Mail.Subject == subject)
|
|
{
|
|
foreach (var tag in inspectedTags)
|
|
await channel.BasicNackAsync(tag, multiple: false, requeue: true);
|
|
|
|
return message;
|
|
}
|
|
}
|
|
|
|
foreach (var tag in inspectedTags)
|
|
await channel.BasicNackAsync(tag, multiple: false, requeue: true);
|
|
|
|
return null;
|
|
}
|
|
|
|
private static string LoadLuckyPennySoftLicenseKey()
|
|
{
|
|
var appsettingsPath = FindApiAppSettingsPath();
|
|
using var doc = JsonDocument.Parse(File.ReadAllText(appsettingsPath));
|
|
|
|
if (doc.RootElement.TryGetProperty("LuckyPennySoftLicenseKey", out var key) &&
|
|
key.ValueKind == JsonValueKind.String &&
|
|
!string.IsNullOrWhiteSpace(key.GetString()))
|
|
return key.GetString()!;
|
|
|
|
throw new InvalidOperationException($"LuckyPennySoftLicenseKey not found in '{appsettingsPath}'.");
|
|
}
|
|
|
|
private static string FindApiAppSettingsPath()
|
|
{
|
|
var current = new DirectoryInfo(AppContext.BaseDirectory);
|
|
|
|
while (current is not null)
|
|
{
|
|
var candidate = Path.Combine(current.FullName, "src", "presentation", "DigitalData.MessagingService.API", "appsettings.json");
|
|
if (File.Exists(candidate))
|
|
return candidate;
|
|
|
|
current = current.Parent;
|
|
}
|
|
|
|
throw new FileNotFoundException("Could not locate API appsettings.json from test output directory.");
|
|
}
|
|
|
|
public async ValueTask DisposeAsync()
|
|
{
|
|
await _factory.DisposeAsync();
|
|
await _serviceProvider.DisposeAsync();
|
|
}
|
|
|
|
private sealed class InMemoryEmailAccountRepository(IEnumerable<EmailAccount> accounts) : IRepository<EmailAccount>
|
|
{
|
|
private readonly List<EmailAccount> _accounts = accounts.ToList();
|
|
|
|
public Task<IEnumerable<EmailAccount>> FindAsync(Expression<Func<EmailAccount, bool>> predicate, int? skip = null, int? take = null, CancellationToken cancellationToken = default)
|
|
{
|
|
var query = _accounts.AsQueryable().Where(predicate);
|
|
if (skip.HasValue)
|
|
query = query.Skip(skip.Value);
|
|
if (take.HasValue)
|
|
query = query.Take(take.Value);
|
|
|
|
return Task.FromResult<IEnumerable<EmailAccount>>(query.ToList());
|
|
}
|
|
|
|
public Task<EmailAccount> CreateAsync<TDto>(TDto dto, CancellationToken cancellationToken = default) => throw new NotSupportedException();
|
|
public Task<IEnumerable<EmailAccount>> CreateRangeAsync<TDto>(IEnumerable<TDto> dtos, CancellationToken cancellationToken = default) => throw new NotSupportedException();
|
|
public Task<EmailAccount?> GetByIdAsync(int id, CancellationToken cancellationToken = default) => throw new NotSupportedException();
|
|
public Task<IEnumerable<EmailAccount>> GetAllAsync(CancellationToken cancellationToken = default) => throw new NotSupportedException();
|
|
public Task<EmailAccount?> FindFirstAsync(Expression<Func<EmailAccount, bool>> predicate, CancellationToken cancellationToken = default) => throw new NotSupportedException();
|
|
public Task<EmailAccount?> FindSingleAsync(Expression<Func<EmailAccount, bool>> predicate, CancellationToken cancellationToken = default) => throw new NotSupportedException();
|
|
public Task<int> CountAsync(Expression<Func<EmailAccount, bool>>? predicate = null, CancellationToken cancellationToken = default) => throw new NotSupportedException();
|
|
public Task<bool> AnyAsync(Expression<Func<EmailAccount, bool>> predicate, CancellationToken cancellationToken = default) => throw new NotSupportedException();
|
|
public Task<(EmailAccount Entity, bool Created)> UpsertAsync<TDto>(Expression<Func<EmailAccount, bool>> predicate, TDto dto, CancellationToken cancellationToken = default) => throw new NotSupportedException();
|
|
public Task<(EmailAccount Entity, bool Created)> UpsertSingleAsync<TDto>(Expression<Func<EmailAccount, bool>> predicate, TDto dto, CancellationToken cancellationToken = default) => throw new NotSupportedException();
|
|
public Task UpdateSingleAsync<TDto>(Expression<Func<EmailAccount, bool>> predicate, TDto dto, CancellationToken cancellationToken = default) => throw new NotSupportedException();
|
|
public Task<int> UpdateAsync<TDto>(Expression<Func<EmailAccount, bool>> predicate, TDto dto, CancellationToken cancellationToken = default) => throw new NotSupportedException();
|
|
public Task DeleteSingleAsync(Expression<Func<EmailAccount, bool>> predicate, CancellationToken cancellationToken = default) => throw new NotSupportedException();
|
|
public Task<int> DeleteAsync(Expression<Func<EmailAccount, bool>> predicate, CancellationToken cancellationToken = default) => throw new NotSupportedException();
|
|
}
|
|
}
|