feat(sitecallaudit): query, KPI and detail backend for the Site Calls page

This commit is contained in:
Joseph Doherty
2026-05-21 04:14:49 -04:00
parent 6f0d2ca499
commit e3519fdb39
17 changed files with 1514 additions and 18 deletions

View File

@@ -2,8 +2,10 @@ using Akka.Actor;
using Akka.TestKit.Xunit2;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using ScadaLink.Commons.Messages.Audit;
using ScadaLink.Commons.Messages.Deployment;
using ScadaLink.Commons.Messages.Notification;
using ScadaLink.Commons.Types.Audit;
using ScadaLink.Commons.Types.Notifications;
namespace ScadaLink.Communication.Tests;
@@ -236,6 +238,150 @@ public class CommunicationServiceTests : TestKit
Assert.Equal("plant-a", result.Sites[0].SourceSiteId);
}
// ── Site Call Audit: central-side audit actor calls ──
[Fact]
public async Task QuerySiteCallsAsync_BeforeSiteCallAuditSet_Throws()
{
var service = new CommunicationService(
Options.Create(new CommunicationOptions()),
NullLogger<CommunicationService>.Instance);
await Assert.ThrowsAsync<InvalidOperationException>(() =>
service.QuerySiteCallsAsync(new SiteCallQueryRequest(
"corr-1", null, null, null, null, false, null, null, null, null, 50)));
}
[Fact]
public async Task GetSiteCallKpisAsync_BeforeSiteCallAuditSet_Throws()
{
var service = new CommunicationService(
Options.Create(new CommunicationOptions()),
NullLogger<CommunicationService>.Instance);
await Assert.ThrowsAsync<InvalidOperationException>(() =>
service.GetSiteCallKpisAsync(new SiteCallKpiRequest("corr-1")));
}
[Fact]
public async Task GetSiteCallDetailAsync_BeforeSiteCallAuditSet_Throws()
{
var service = new CommunicationService(
Options.Create(new CommunicationOptions()),
NullLogger<CommunicationService>.Instance);
await Assert.ThrowsAsync<InvalidOperationException>(() =>
service.GetSiteCallDetailAsync(new SiteCallDetailRequest("corr-1", Guid.NewGuid())));
}
[Fact]
public async Task GetPerSiteSiteCallKpisAsync_BeforeSiteCallAuditSet_Throws()
{
var service = new CommunicationService(
Options.Create(new CommunicationOptions()),
NullLogger<CommunicationService>.Instance);
await Assert.ThrowsAsync<InvalidOperationException>(() =>
service.GetPerSiteSiteCallKpisAsync(new PerSiteSiteCallKpiRequest("corr-1")));
}
[Fact]
public async Task QuerySiteCallsAsync_AsksSiteCallAuditProxyDirectly()
{
// The Site Call Audit actor is central-local: the request must be Asked
// directly to its proxy (no SiteEnvelope wrapping).
var service = new CommunicationService(
Options.Create(new CommunicationOptions()),
NullLogger<CommunicationService>.Instance);
var probe = CreateTestProbe();
service.SetSiteCallAudit(probe.Ref);
var request = new SiteCallQueryRequest(
"corr-q", "Parked", "plant-a", "ApiOutbound", "ERP.GetOrder", true,
null, null, null, null, 25);
var task = service.QuerySiteCallsAsync(request);
var received = probe.ExpectMsg<SiteCallQueryRequest>();
Assert.Same(request, received);
var reply = new SiteCallQueryResponse(
"corr-q", true, null, Array.Empty<SiteCallSummary>(), null, null);
probe.Reply(reply);
Assert.Same(reply, await task);
}
[Fact]
public async Task GetSiteCallDetailAsync_AsksSiteCallAuditProxyDirectly()
{
var service = new CommunicationService(
Options.Create(new CommunicationOptions()),
NullLogger<CommunicationService>.Instance);
var probe = CreateTestProbe();
service.SetSiteCallAudit(probe.Ref);
var request = new SiteCallDetailRequest("corr-d", Guid.NewGuid());
var task = service.GetSiteCallDetailAsync(request);
var received = probe.ExpectMsg<SiteCallDetailRequest>();
Assert.Same(request, received);
var reply = new SiteCallDetailResponse("corr-d", false, "site call not found", null);
probe.Reply(reply);
var result = await task;
Assert.Same(reply, result);
Assert.False(result.Success);
}
[Fact]
public async Task GetSiteCallKpisAsync_AsksSiteCallAuditProxyDirectly()
{
var service = new CommunicationService(
Options.Create(new CommunicationOptions()),
NullLogger<CommunicationService>.Instance);
var probe = CreateTestProbe();
service.SetSiteCallAudit(probe.Ref);
var request = new SiteCallKpiRequest("corr-k");
var task = service.GetSiteCallKpisAsync(request);
var received = probe.ExpectMsg<SiteCallKpiRequest>();
Assert.Same(request, received);
var reply = new SiteCallKpiResponse(
"corr-k", true, null, 4, 1, 2, 9, TimeSpan.FromMinutes(7), 1);
probe.Reply(reply);
var result = await task;
Assert.Same(reply, result);
Assert.Equal(4, result.BufferedCount);
Assert.Equal(1, result.StuckCount);
}
[Fact]
public async Task GetPerSiteSiteCallKpisAsync_AsksSiteCallAuditProxyDirectly()
{
var service = new CommunicationService(
Options.Create(new CommunicationOptions()),
NullLogger<CommunicationService>.Instance);
var probe = CreateTestProbe();
service.SetSiteCallAudit(probe.Ref);
var request = new PerSiteSiteCallKpiRequest("corr-ps");
var task = service.GetPerSiteSiteCallKpisAsync(request);
var received = probe.ExpectMsg<PerSiteSiteCallKpiRequest>();
Assert.Same(request, received);
var reply = new PerSiteSiteCallKpiResponse(
"corr-ps", true, null,
new[] { new SiteCallSiteKpiSnapshot("plant-a", 3, 0, 0, 5, null, 0) });
probe.Reply(reply);
var result = await task;
Assert.Same(reply, result);
Assert.True(result.Success);
Assert.Single(result.Sites);
Assert.Equal("plant-a", result.Sites[0].SourceSite);
}
/// <summary>
/// Stand-in for CentralCommunicationActor: verifies the message is wrapped
/// in a SiteEnvelope targeting the requested site and replies with a typed