fix(high-severity): close 9 of 10 open High findings across 8 modules

Comm-016: delete dead HandleConnectionStateChanged + _debugSubscriptions /
_inProgressDeployments tracking + ConnectionStateChanged message record.
Disconnect detection is owned by the transport layers (gRPC keepalive PING
~25s; Ask-timeout at CommunicationService). Updates the
Component-Communication.md design doc to make that explicit.

SnF-018: NotificationForwarder.DeliverAsync now discards a corrupt buffered
payload (Warning log + return true) instead of returning false and parking
the row — honoring the design's "notifications do not park" invariant.

DM-018: reconciliation no longer force-sets Enabled, preserving an
intentional Disabled state after central failover.

ESG-018: DeliverBufferedAsync (both ExternalSystemClient + DatabaseGateway)
catches JsonException and returns false, turning a corrupt buffered row
into a parked operation instead of a retry-forever poison message.

InboundAPI-022: register ActiveNodeGate as IActiveNodeGate in the Central
DI branch so standby-node gating is actually wired up in production.

NS-019: remove orphaned NotificationDeliveryService /
INotificationDeliveryService / NotificationResult; central notification
delivery now lives entirely in NotificationOutbox.

SEL-016: normalise From/To filters to UTC before ISO-string compare so
non-UTC DateTimeOffset clients no longer get spuriously excluded events.

TE-017: include Description on attributes/alarms and a HashableConnections
projection (protocol, endpoint JSON, failover count) in the revision hash
and DiffService; staleness detection now catches description-only and
connection-endpoint edits.

Transport-001 and Transport-002 (also High) remain Open — they're being
handled in a follow-up batch because both touch BundleImporter.cs and
must serialise.
This commit is contained in:
Joseph Doherty
2026-05-28 05:40:15 -04:00
parent f936f55f51
commit ac96b83b08
38 changed files with 852 additions and 1729 deletions
@@ -5,11 +5,9 @@ using System.Text.Json;
using Microsoft.Extensions.DependencyInjection;
using NSubstitute;
using ScadaLink.Commons.Entities.InboundApi;
using ScadaLink.Commons.Entities.Notifications;
using ScadaLink.Commons.Interfaces.Repositories;
using ScadaLink.Commons.Interfaces.Services;
using ScadaLink.InboundAPI;
using ScadaLink.NotificationService;
namespace ScadaLink.IntegrationTests;
@@ -98,42 +96,11 @@ public class IntegrationSurfaceTests
}
// ── Notification: mock SMTP delivery ──
[Fact]
public async Task Notification_Send_MockSmtp_Delivers()
{
var repository = Substitute.For<INotificationRepository>();
var smtpClient = Substitute.For<ISmtpClientWrapper>();
var list = new NotificationList("alerts") { Id = 1 };
var recipients = new List<NotificationRecipient>
{
new("Admin", "admin@example.com") { Id = 1, NotificationListId = 1 }
};
var smtpConfig = new SmtpConfiguration("smtp.example.com", "basic", "noreply@example.com")
{
Id = 1, Port = 587, Credentials = "user:pass"
};
repository.GetListByNameAsync("alerts").Returns(list);
repository.GetRecipientsByListIdAsync(1).Returns(recipients);
repository.GetAllSmtpConfigurationsAsync().Returns(new List<SmtpConfiguration> { smtpConfig });
var service = new NotificationDeliveryService(
repository,
() => smtpClient,
Microsoft.Extensions.Logging.Abstractions.NullLogger<NotificationDeliveryService>.Instance);
var result = await service.SendAsync("alerts", "Test Alert", "Something happened");
Assert.True(result.Success);
await smtpClient.Received(1).SendAsync(
"noreply@example.com",
Arg.Is<IEnumerable<string>>(r => r.Contains("admin@example.com")),
"Test Alert",
"Something happened",
Arg.Any<CancellationToken>());
}
// NS-019: the site-shaped NotificationDeliveryService that this case exercised
// was removed when sites stopped delivering notifications. The central SMTP
// delivery path is now covered end-to-end by
// ScadaLink.NotificationOutbox.Tests.Delivery.EmailNotificationDeliveryAdapterTests;
// no equivalent integration-surface assertion is needed here.
// ── Script Context: integration API wiring ──