Files
ScadaBridge/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Integration/Infrastructure/CombinedTelemetryDispatcher.cs
T
Joseph Doherty 513fa1aa16 docs(tests): fix CommentChecker findings across tests/ — 13,392 → 0 (repo-wide scan clean)
Comment/XML-doc changes only:
- add summaries to ~10k test methods, fixtures, fakes and helpers
  (+ <param> on theories, <returns> on async Task methods,
  <inheritdoc/> on interface implementations the checker can resolve)
- strip ~2.6k project-tracking references (WP*, P*-*, row-NN, Task N,
  #NN, Component-0NN) from comments, keeping the explanatory text;
  reword false positives in comments only
- real docs on fakes implementing BCL/library interfaces
  (IDisposable, ILogger, IOptionsMonitor, IAsyncLifetime)
- reword the last 5 src/ false positives (CWE-1236, failures-1,
  year-0001, M365)

Only non-comment edits: four one-line nested classes split across lines
so members can carry docs. Build 0 errors / 0 warnings.
2026-10-07 15:05:49 -04:00

130 lines
5.5 KiB
C#

using ZB.MOM.WW.ScadaBridge.AuditLog.Site.Telemetry;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services;
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Integration;
using ZB.MOM.WW.ScadaBridge.Commons.Types;
using ZB.MOM.WW.ScadaBridge.Communication.Grpc;
using Google.Protobuf.WellKnownTypes;
using Timestamp = Google.Protobuf.WellKnownTypes.Timestamp;
namespace ZB.MOM.WW.ScadaBridge.AuditLog.Tests.Integration.Infrastructure;
/// <summary>
/// Test-side combined-telemetry dispatcher: wraps a production
/// <see cref="ICachedCallTelemetryForwarder"/> so the local audit + tracking
/// stores still get written, then projects the same packet onto the wire as a
/// <see cref="CachedTelemetryBatch"/> and pushes it through the supplied
/// <see cref="ISiteStreamAuditClient"/>. The bridge can be composed into the
/// existing <see cref="CachedCallLifecycleBridge"/> chain as the
/// <see cref="ICachedCallTelemetryForwarder"/> implementation so a single
/// observer callback fans out to both halves.
/// </summary>
/// <remarks>
/// <para>
/// Production wiring keeps the wire push deferred — the site SQLite hot
/// path is the source of truth and a future drain will push the rows
/// through the gRPC client. For end-to-end testing today we need a way to
/// exercise the central dual-write transaction immediately, so this
/// dispatcher synthesises the wire packet inline and round-trips it through
/// the stub client. The shape mirrors what the future drain will eventually emit.
/// </para>
/// <para>
/// <b>Best-effort:</b> both the inner forwarder call and the wire push are
/// wrapped in independent try/catch blocks. A thrown wire client doesn't
/// abort the local writes (the audit row is already in SQLite); a thrown
/// local forwarder doesn't abort the wire push (central still gets the
/// dual-write attempt).
/// </para>
/// </remarks>
public sealed class CombinedTelemetryDispatcher : ICachedCallTelemetryForwarder
{
private readonly ICachedCallTelemetryForwarder _inner;
private readonly ISiteStreamAuditClient _wireClient;
/// <summary>Initializes the dispatcher with the inner forwarder and the wire client.</summary>
/// <param name="inner">The forwarder that handles site-local telemetry.</param>
/// <param name="wireClient">The client used to push audit rows over the wire.</param>
public CombinedTelemetryDispatcher(
ICachedCallTelemetryForwarder inner,
ISiteStreamAuditClient wireClient)
{
_inner = inner ?? throw new ArgumentNullException(nameof(inner));
_wireClient = wireClient ?? throw new ArgumentNullException(nameof(wireClient));
}
// Forwards the telemetry through the inner forwarder and then pushes its audit rows to the wire client.
/// <inheritdoc />
public async Task ForwardAsync(CachedCallTelemetry telemetry, CancellationToken ct = default)
{
ArgumentNullException.ThrowIfNull(telemetry);
// Inner forwarder writes the audit row to SQLite + updates the
// tracking store. Best-effort — exceptions are already swallowed
// inside the production forwarder, but wrap defensively here too in
// case a test substitutes a stricter inner.
try
{
await _inner.ForwardAsync(telemetry, ct).ConfigureAwait(false);
}
catch
{
// Swallow — alog.md §7 best-effort contract.
}
// Project the same packet onto the wire and push it through the stub
// client. This is the bit a future drain will subsume — until
// then the test wraps the two halves into one observer-driven step.
try
{
var batch = new CachedTelemetryBatch();
batch.Packets.Add(BuildPacket(telemetry));
await _wireClient.IngestCachedTelemetryAsync(batch, ct).ConfigureAwait(false);
}
catch
{
// Swallow — the audit row is still in SQLite for a future drain;
// the central row will materialise the next time the wire path
// is exercised (or via the reconciliation pull).
}
}
private static CachedTelemetryPacket BuildPacket(CachedCallTelemetry telemetry)
{
return new CachedTelemetryPacket
{
AuditEvent = AuditEventDtoMapper.ToDto(telemetry.Audit),
Operational = ToOperationalDto(telemetry.Operational),
};
}
private static SiteCallOperationalDto ToOperationalDto(SiteCallOperational op)
{
var dto = new SiteCallOperationalDto
{
TrackedOperationId = op.TrackedOperationId.Value.ToString("D"),
Channel = op.Channel,
Target = op.Target,
SourceSite = op.SourceSite,
SourceNode = op.SourceNode ?? string.Empty,
Status = op.Status,
RetryCount = op.RetryCount,
LastError = op.LastError ?? string.Empty,
CreatedAtUtc = Timestamp.FromDateTime(EnsureUtc(op.CreatedAtUtc)),
UpdatedAtUtc = Timestamp.FromDateTime(EnsureUtc(op.UpdatedAtUtc)),
};
if (op.HttpStatus.HasValue)
{
dto.HttpStatus = op.HttpStatus.Value;
}
if (op.TerminalAtUtc.HasValue)
{
dto.TerminalAtUtc = Timestamp.FromDateTime(EnsureUtc(op.TerminalAtUtc.Value));
}
return dto;
}
private static DateTime EnsureUtc(DateTime value) =>
value.Kind == DateTimeKind.Utc
? value
: DateTime.SpecifyKind(value.ToUniversalTime(), DateTimeKind.Utc);
}