bd6c0b4d3d
Add missing <returns>/<param>/<summary>/<typeparam> tags and clean up misused inheritdoc across 481 files so the documented API surface is complete. Documentation-only (zero code lines changed). The 131 remaining findings are inheritdoc-style warnings deliberately left to preserve hand-written implementation rationale (plan-decision notes, race-condition explanations).
112 lines
4.7 KiB
C#
112 lines
4.7 KiB
C#
using Microsoft.EntityFrameworkCore;
|
|
using Shouldly;
|
|
using Xunit;
|
|
using ZB.MOM.WW.OtOpcUa.Configuration.Entities;
|
|
using ZB.MOM.WW.OtOpcUa.Configuration.Queries;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Configuration.Tests;
|
|
|
|
/// <summary>
|
|
/// Verifies <see cref="ClusterAuditQuery.ForClusterAsync"/> — the cluster-scoped audit view used
|
|
/// by the AdminUI ClusterAudit page. The structured AuditWriterActor path stamps NodeId (not
|
|
/// ClusterId), so before the Task 2.2 fix those rows were invisible to a cluster filtered only on
|
|
/// ClusterId. These tests pin the OR-predicate that joins NodeId back to its cluster.
|
|
/// </summary>
|
|
[Trait("Category", "Unit")]
|
|
public sealed class ClusterAuditQueryTests : IDisposable
|
|
{
|
|
private readonly OtOpcUaConfigDbContext _db;
|
|
|
|
/// <summary>Initializes a new instance with a fresh in-memory config database.</summary>
|
|
public ClusterAuditQueryTests()
|
|
{
|
|
var options = new DbContextOptionsBuilder<OtOpcUaConfigDbContext>()
|
|
.UseInMemoryDatabase($"cluster-audit-{Guid.NewGuid():N}")
|
|
.Options;
|
|
_db = new OtOpcUaConfigDbContext(options);
|
|
}
|
|
|
|
/// <summary>Disposes the database context.</summary>
|
|
public void Dispose() => _db.Dispose();
|
|
|
|
private void SeedNode(string clusterId, string nodeId) =>
|
|
_db.ClusterNodes.Add(new ClusterNode
|
|
{
|
|
NodeId = nodeId,
|
|
ClusterId = clusterId,
|
|
Host = $"{nodeId}.local",
|
|
ApplicationUri = $"urn:{nodeId}",
|
|
CreatedBy = "test",
|
|
});
|
|
|
|
private void SeedAudit(string eventType, string? clusterId, string? nodeId, DateTime ts) =>
|
|
_db.ConfigAuditLogs.Add(new ConfigAuditLog
|
|
{
|
|
Principal = "tester",
|
|
EventType = eventType,
|
|
ClusterId = clusterId,
|
|
NodeId = nodeId,
|
|
Timestamp = ts,
|
|
});
|
|
|
|
/// <summary>Structured rows (ClusterId null, NodeId set) for a node in the cluster are now
|
|
/// visible, alongside the SP-path rows that stamp ClusterId directly.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task Surfaces_both_clusterId_rows_and_structured_nodeId_rows()
|
|
{
|
|
SeedNode("LINE3-OPCUA", "LINE3-OPCUA-A");
|
|
SeedNode("LINE3-OPCUA", "LINE3-OPCUA-B");
|
|
SeedNode("OTHER-CLUSTER", "OTHER-A");
|
|
var t0 = new DateTime(2026, 6, 2, 10, 0, 0, DateTimeKind.Utc);
|
|
|
|
// SP path: stamps ClusterId.
|
|
SeedAudit("Published", clusterId: "LINE3-OPCUA", nodeId: null, ts: t0);
|
|
// Structured AuditWriterActor path: stamps NodeId, ClusterId null — these were invisible.
|
|
SeedAudit("DraftEdited", clusterId: null, nodeId: "LINE3-OPCUA-A", ts: t0.AddMinutes(1));
|
|
SeedAudit("NodeApplied", clusterId: null, nodeId: "LINE3-OPCUA-B", ts: t0.AddMinutes(2));
|
|
// Noise that must NOT appear: other cluster's structured row + an orphan NodeId.
|
|
SeedAudit("Published", clusterId: null, nodeId: "OTHER-A", ts: t0.AddMinutes(3));
|
|
SeedAudit("Published", clusterId: null, nodeId: "UNKNOWN-NODE", ts: t0.AddMinutes(4));
|
|
await _db.SaveChangesAsync();
|
|
|
|
var rows = await ClusterAuditQuery.ForClusterAsync(_db, "LINE3-OPCUA", pageSize: 200);
|
|
|
|
rows.Select(r => r.EventType).ShouldBe(
|
|
["NodeApplied", "DraftEdited", "Published"], // newest first
|
|
ignoreOrder: false);
|
|
}
|
|
|
|
/// <summary>An audit row stamped with another cluster's ClusterId never appears.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task Does_not_surface_other_cluster_rows()
|
|
{
|
|
SeedNode("LINE3-OPCUA", "LINE3-OPCUA-A");
|
|
var t0 = new DateTime(2026, 6, 2, 10, 0, 0, DateTimeKind.Utc);
|
|
SeedAudit("Published", clusterId: "OTHER-CLUSTER", nodeId: null, ts: t0);
|
|
await _db.SaveChangesAsync();
|
|
|
|
var rows = await ClusterAuditQuery.ForClusterAsync(_db, "LINE3-OPCUA", pageSize: 200);
|
|
|
|
rows.ShouldBeEmpty();
|
|
}
|
|
|
|
/// <summary>Respects the page-size cap, newest first.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task Caps_to_page_size_newest_first()
|
|
{
|
|
SeedNode("LINE3-OPCUA", "LINE3-OPCUA-A");
|
|
var t0 = new DateTime(2026, 6, 2, 10, 0, 0, DateTimeKind.Utc);
|
|
for (var i = 0; i < 5; i++)
|
|
SeedAudit("DraftEdited", clusterId: null, nodeId: "LINE3-OPCUA-A", ts: t0.AddMinutes(i));
|
|
await _db.SaveChangesAsync();
|
|
|
|
var rows = await ClusterAuditQuery.ForClusterAsync(_db, "LINE3-OPCUA", pageSize: 3);
|
|
|
|
rows.Count.ShouldBe(3);
|
|
rows.First().Timestamp.ShouldBe(t0.AddMinutes(4)); // newest
|
|
}
|
|
}
|