Phase 8: Production readiness — failover tests, security hardening, sandboxing, deployment docs
- WP-1-3: Central/site failover + dual-node recovery tests (17 tests) - WP-4: Performance testing framework for target scale (7 tests) - WP-5: Security hardening (LDAPS, JWT key length, no secrets in logs) (11 tests) - WP-6: Script sandboxing adversarial tests (28 tests, all forbidden APIs) - WP-7: Recovery drill test scaffolds (5 tests) - WP-8: Observability validation (structured logs, correlation IDs, metrics) (6 tests) - WP-9: Message contract compatibility (forward/backward compat) (18 tests) - WP-10: Deployment packaging (installation guide, production checklist, topology) - WP-11: Operational runbooks (failover, troubleshooting, maintenance) 92 new tests, all passing. Zero warnings.
This commit is contained in:
@@ -0,0 +1,148 @@
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using NSubstitute;
|
||||
using NSubstitute.ExceptionExtensions;
|
||||
using ScadaLink.Commons.Entities.Notifications;
|
||||
using ScadaLink.Commons.Interfaces.Repositories;
|
||||
|
||||
namespace ScadaLink.NotificationService.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// WP-11/12: Tests for notification delivery — SMTP delivery, error classification, S&F integration.
|
||||
/// </summary>
|
||||
public class NotificationDeliveryServiceTests
|
||||
{
|
||||
private readonly INotificationRepository _repository = Substitute.For<INotificationRepository>();
|
||||
private readonly ISmtpClientWrapper _smtpClient = Substitute.For<ISmtpClientWrapper>();
|
||||
|
||||
private NotificationDeliveryService CreateService(StoreAndForward.StoreAndForwardService? sf = null)
|
||||
{
|
||||
return new NotificationDeliveryService(
|
||||
_repository,
|
||||
() => _smtpClient,
|
||||
NullLogger<NotificationDeliveryService>.Instance,
|
||||
tokenService: null,
|
||||
storeAndForward: sf);
|
||||
}
|
||||
|
||||
private void SetupHappyPath()
|
||||
{
|
||||
var list = new NotificationList("ops-team") { Id = 1 };
|
||||
var recipients = new List<NotificationRecipient>
|
||||
{
|
||||
new("Alice", "alice@example.com") { Id = 1, NotificationListId = 1 },
|
||||
new("Bob", "bob@example.com") { Id = 2, NotificationListId = 1 }
|
||||
};
|
||||
var smtpConfig = new SmtpConfiguration("smtp.example.com", "basic", "noreply@example.com")
|
||||
{
|
||||
Id = 1, Port = 587, Credentials = "user:pass", TlsMode = "starttls"
|
||||
};
|
||||
|
||||
_repository.GetListByNameAsync("ops-team").Returns(list);
|
||||
_repository.GetRecipientsByListIdAsync(1).Returns(recipients);
|
||||
_repository.GetAllSmtpConfigurationsAsync().Returns(new List<SmtpConfiguration> { smtpConfig });
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Send_ListNotFound_ReturnsError()
|
||||
{
|
||||
_repository.GetListByNameAsync("nonexistent").Returns((NotificationList?)null);
|
||||
var service = CreateService();
|
||||
|
||||
var result = await service.SendAsync("nonexistent", "Subject", "Body");
|
||||
|
||||
Assert.False(result.Success);
|
||||
Assert.Contains("not found", result.ErrorMessage);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Send_NoRecipients_ReturnsError()
|
||||
{
|
||||
var list = new NotificationList("empty-list") { Id = 1 };
|
||||
_repository.GetListByNameAsync("empty-list").Returns(list);
|
||||
_repository.GetRecipientsByListIdAsync(1).Returns(new List<NotificationRecipient>());
|
||||
|
||||
var service = CreateService();
|
||||
var result = await service.SendAsync("empty-list", "Subject", "Body");
|
||||
|
||||
Assert.False(result.Success);
|
||||
Assert.Contains("no recipients", result.ErrorMessage);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Send_NoSmtpConfig_ReturnsError()
|
||||
{
|
||||
var list = new NotificationList("test") { Id = 1 };
|
||||
var recipients = new List<NotificationRecipient>
|
||||
{
|
||||
new("Alice", "alice@example.com") { Id = 1, NotificationListId = 1 }
|
||||
};
|
||||
_repository.GetListByNameAsync("test").Returns(list);
|
||||
_repository.GetRecipientsByListIdAsync(1).Returns(recipients);
|
||||
_repository.GetAllSmtpConfigurationsAsync().Returns(new List<SmtpConfiguration>());
|
||||
|
||||
var service = CreateService();
|
||||
var result = await service.SendAsync("test", "Subject", "Body");
|
||||
|
||||
Assert.False(result.Success);
|
||||
Assert.Contains("No SMTP configuration", result.ErrorMessage);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Send_Successful_ReturnsSuccess()
|
||||
{
|
||||
SetupHappyPath();
|
||||
var service = CreateService();
|
||||
|
||||
var result = await service.SendAsync("ops-team", "Alert", "Something happened");
|
||||
|
||||
Assert.True(result.Success);
|
||||
Assert.Null(result.ErrorMessage);
|
||||
Assert.False(result.WasBuffered);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Send_SmtpConnectsWithCorrectParams()
|
||||
{
|
||||
SetupHappyPath();
|
||||
var service = CreateService();
|
||||
|
||||
await service.SendAsync("ops-team", "Alert", "Body");
|
||||
|
||||
await _smtpClient.Received().ConnectAsync("smtp.example.com", 587, true, Arg.Any<CancellationToken>());
|
||||
await _smtpClient.Received().AuthenticateAsync("basic", "user:pass", Arg.Any<CancellationToken>());
|
||||
await _smtpClient.Received().SendAsync(
|
||||
"noreply@example.com",
|
||||
Arg.Is<IEnumerable<string>>(bcc => bcc.Count() == 2),
|
||||
"Alert",
|
||||
"Body",
|
||||
Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Send_PermanentSmtpError_ReturnsErrorDirectly()
|
||||
{
|
||||
SetupHappyPath();
|
||||
_smtpClient.SendAsync(Arg.Any<string>(), Arg.Any<IEnumerable<string>>(), Arg.Any<string>(), Arg.Any<string>(), Arg.Any<CancellationToken>())
|
||||
.Throws(new SmtpPermanentException("550 Mailbox not found"));
|
||||
|
||||
var service = CreateService();
|
||||
var result = await service.SendAsync("ops-team", "Alert", "Body");
|
||||
|
||||
Assert.False(result.Success);
|
||||
Assert.Contains("Permanent SMTP error", result.ErrorMessage);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Send_TransientError_NoStoreAndForward_ReturnsError()
|
||||
{
|
||||
SetupHappyPath();
|
||||
_smtpClient.SendAsync(Arg.Any<string>(), Arg.Any<IEnumerable<string>>(), Arg.Any<string>(), Arg.Any<string>(), Arg.Any<CancellationToken>())
|
||||
.Throws(new TimeoutException("Connection timed out"));
|
||||
|
||||
var service = CreateService(sf: null);
|
||||
var result = await service.SendAsync("ops-team", "Alert", "Body");
|
||||
|
||||
Assert.False(result.Success);
|
||||
Assert.Contains("store-and-forward not available", result.ErrorMessage);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user