using System.Linq.Expressions; using System.Text; using System.Text.Json; using DigitalData.MessagingService.Application.Common.Dto; using DigitalData.MessagingService.Application.Common.Interfaces.Repositories; using DigitalData.MessagingService.Client; using DigitalData.MessagingService.Client.DependencyInjection; 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 MessagingServiceClient _client; private readonly RabbitMqConnectionFactory _factory; public MessagingServiceClientTests() { var services = new ServiceCollection(); services.AddLogging(); services.AddMessagingServiceClient(LoadLuckyPennySoftLicenseKey(), RabbitMqTestConfig.Apply); services.AddSingleton>( 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(); _factory = _serviceProvider.GetRequiredService(); } /// /// Sends an email through 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. /// [Fact] public async Task SendAsync_WithDependencyInjectionAndRealBroker_PublishesExpectedMessage() { var subject = $"MessagingServiceClient Integration Test {Guid.NewGuid():N}"; var email = new Email { From = SenderAddress, To = [RecipientAddress], Subject = subject, Body = "

Realistic test mail via MessagingServiceClient.

", IsHtml = true }; await _client.SendAsync(email); Assert.True(true, "Verify in mailbox."); } private async Task FindMessageBySubjectAsync(string subject, int maxMessages = 100) { var connection = await _factory.GetDefaultConnectionAsync(); await using var channel = await connection.CreateChannelAsync(); var inspectedTags = new List(); 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(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 accounts) : IRepository { private readonly List _accounts = accounts.ToList(); public Task> FindAsync(Expression> 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>(query.ToList()); } public Task CreateAsync(TDto dto, CancellationToken cancellationToken = default) => throw new NotSupportedException(); public Task> CreateRangeAsync(IEnumerable dtos, CancellationToken cancellationToken = default) => throw new NotSupportedException(); public Task GetByIdAsync(int id, CancellationToken cancellationToken = default) => throw new NotSupportedException(); public Task> GetAllAsync(CancellationToken cancellationToken = default) => throw new NotSupportedException(); public Task FindFirstAsync(Expression> predicate, CancellationToken cancellationToken = default) => throw new NotSupportedException(); public Task FindSingleAsync(Expression> predicate, CancellationToken cancellationToken = default) => throw new NotSupportedException(); public Task CountAsync(Expression>? predicate = null, CancellationToken cancellationToken = default) => throw new NotSupportedException(); public Task AnyAsync(Expression> predicate, CancellationToken cancellationToken = default) => throw new NotSupportedException(); public Task<(EmailAccount Entity, bool Created)> UpsertAsync(Expression> predicate, TDto dto, CancellationToken cancellationToken = default) => throw new NotSupportedException(); public Task<(EmailAccount Entity, bool Created)> UpsertSingleAsync(Expression> predicate, TDto dto, CancellationToken cancellationToken = default) => throw new NotSupportedException(); public Task UpdateSingleAsync(Expression> predicate, TDto dto, CancellationToken cancellationToken = default) => throw new NotSupportedException(); public Task UpdateAsync(Expression> predicate, TDto dto, CancellationToken cancellationToken = default) => throw new NotSupportedException(); public Task DeleteSingleAsync(Expression> predicate, CancellationToken cancellationToken = default) => throw new NotSupportedException(); public Task DeleteAsync(Expression> predicate, CancellationToken cancellationToken = default) => throw new NotSupportedException(); } }