From 888f01ea950fcb93f2f3bd5adef723cb869e7cd9 Mon Sep 17 00:00:00 2001 From: TekH Date: Wed, 4 Nov 2026 15:09:51 +0100 Subject: [PATCH] Add integration tests for MessagingServiceClient Introduce `MessagingServiceClientTests` to test email sending via RabbitMQ with dependency injection. Implement methods to load configuration, manage RabbitMQ messages, and ensure proper resource disposal. Include an in-memory email account repository for testing purposes. --- .../MessagingServiceClientTests.cs | 175 ++++++++++++++++++ 1 file changed, 175 insertions(+) create mode 100644 tests/DigitalData.MessagingService.Tests/Integration/MessagingServiceClientTests.cs diff --git a/tests/DigitalData.MessagingService.Tests/Integration/MessagingServiceClientTests.cs b/tests/DigitalData.MessagingService.Tests/Integration/MessagingServiceClientTests.cs new file mode 100644 index 0000000..c58b6fd --- /dev/null +++ b/tests/DigitalData.MessagingService.Tests/Integration/MessagingServiceClientTests.cs @@ -0,0 +1,175 @@ +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(); + } + + [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); + + await Task.Delay(500); + + var evt = await FindMessageBySubjectAsync(subject); + + Assert.NotNull(evt); + Assert.Equal(SenderAddress, evt.Mail.Sender.Username); + Assert.Contains(RecipientAddress, evt.Mail.Recipients); + Assert.Equal(subject, evt.Mail.Subject); + Assert.Equal(email.Body, evt.Mail.Body); + Assert.True(evt.Mail.IsHtml); + } + + 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(); + } +}