using System.Collections.Concurrent; using System.Security.Claims; using Mbproxy.Admin; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.SignalR; namespace Mbproxy.Tests.Admin; /// /// Minimal hand-written test doubles for the SignalR surface /// and touch. The project carries no mocking framework, /// so these record just enough to assert behaviour. /// internal sealed class FakeHubCallerContext : HubCallerContext { public FakeHubCallerContext(string connectionId) => ConnectionId = connectionId; public override string ConnectionId { get; } public override string? UserIdentifier => null; public override ClaimsPrincipal? User => null; public override IDictionary Items { get; } = new Dictionary(); public override IFeatureCollection Features { get; } = new FeatureCollection(); public override CancellationToken ConnectionAborted => CancellationToken.None; public override void Abort() { } } /// Records every group join so tests can assert membership changes. internal sealed class FakeGroupManager : IGroupManager { public List<(string ConnectionId, string Group)> Added { get; } = []; public Task AddToGroupAsync(string connectionId, string groupName, CancellationToken cancellationToken = default) { Added.Add((connectionId, groupName)); return Task.CompletedTask; } // StatusHub.OnDisconnectedAsync never calls RemoveFromGroupAsync — SignalR removes a // disconnected connection from its groups implicitly. Nothing to record here. public Task RemoveFromGroupAsync(string connectionId, string groupName, CancellationToken cancellationToken = default) => Task.CompletedTask; } /// Records every push so tests can assert routing. internal sealed class FakeStatusPushSink : IStatusPushSink { private readonly ConcurrentBag _fleet = []; private readonly ConcurrentBag<(string Plc, PlcDetailResponse Detail)> _plc = []; public IReadOnlyCollection FleetPushes => _fleet; public IReadOnlyCollection<(string Plc, PlcDetailResponse Detail)> PlcPushes => _plc; public Task PushFleetAsync(StatusResponse snapshot, CancellationToken ct) { _fleet.Add(snapshot); return Task.CompletedTask; } public Task PushPlcAsync(string plcName, PlcDetailResponse detail, CancellationToken ct) { _plc.Add((plcName, detail)); return Task.CompletedTask; } } /// /// An whose every push fails with an exception from the /// supplied factory — used to prove swallows /// a transport fault (and that its when (ex is not OperationCanceledException) filter /// still lets a cancellation propagate). /// internal sealed class ThrowingStatusPushSink(Func exceptionFactory) : IStatusPushSink { public Task PushFleetAsync(StatusResponse snapshot, CancellationToken ct) => throw exceptionFactory(); public Task PushPlcAsync(string plcName, PlcDetailResponse detail, CancellationToken ct) => throw exceptionFactory(); }