using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Connections.Features;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using NSubstitute;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Instances;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories;
using ZB.MOM.WW.ScadaBridge.Commons.Messages.DebugView;
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Streaming;
using ZB.MOM.WW.ScadaBridge.Communication;
using ZB.MOM.WW.ScadaBridge.Communication.Grpc;
namespace ZB.MOM.WW.ScadaBridge.ManagementService.Tests;
///
/// Regression tests for arch-review 2026-08-19 P6-6 on the hub side. SubscribeInstance
/// awaits StartStreamAsync — up to 30s — and only then stores the session id on the
/// connection. A client that disconnects inside that window ran OnDisconnectedAsync
/// while there was still no session id to release, so the session stayed registered in the
/// singleton and its keepalive renewed it forever (the
/// orphan reaper only collects UNregistered sessions). The fix publishes the id and
/// re-checks connection liveness under the per-connection Items monitor, so exactly
/// one side tears the session down.
///
public class DebugStreamHubSubscribeRaceTests
{
private sealed class GatedDebugStreamService : DebugStreamService
{
private readonly TaskCompletionSource _gate = new(TaskCreationOptions.RunContinuationsAsynchronously);
private readonly TaskCompletionSource _entered = new(TaskCreationOptions.RunContinuationsAsynchronously);
private readonly List _stopped = new();
public GatedDebugStreamService()
: base(
new CommunicationService(Options.Create(new CommunicationOptions()), NullLogger.Instance),
new ServiceCollection().BuildServiceProvider(),
new SiteStreamGrpcClientFactory(NullLoggerFactory.Instance),
NullLogger.Instance)
{
}
public string SessionId => "session-under-test";
/// Completes once has been entered.
public Task Entered => _entered.Task;
/// Session ids passed to , in call order.
public IReadOnlyList Stopped
{
get { lock (_stopped) { return _stopped.ToList(); } }
}
public void ReleaseStart() => _gate.TrySetResult();
public override async Task StartStreamAsync(
int instanceId, Action