using IM.Application.Chat.DTOs;
using IM.Domain.Common;
using IM.Domain.Enums;
using IM.Infrastructure.Cache;
using Xunit;
using Microsoft.AspNetCore.SignalR.Client;
using Microsoft.Extensions.DependencyInjection;
namespace BuildFlow.IM.Integration.Tests;
///
/// ChatHub 连接生命周期与在线状态用例。
/// 覆盖:连接即订阅、上下线事件广播、Redis 在线状态追踪。
///
[Collection(ImServerCollection.Name)]
public class ChatHubLifecycleTests : IAsyncLifetime
{
private readonly ImServerFixture _fixture;
private readonly List _connections = new();
public ChatHubLifecycleTests(ImServerFixture fixture) => _fixture = fixture;
public Task InitializeAsync() => _fixture.CleanRedisAsync();
public async Task DisposeAsync()
{
foreach (var conn in _connections)
{
try { await conn.DisposeAsync(); } catch { }
}
}
private async Task ConnectAsync(Guid userId, string userName)
{
var conn = HubConnectionFactory.Create(_fixture, userId, userName);
await conn.StartAsync();
_connections.Add(conn);
return conn;
}
[Fact]
public async Task OnConnected_records_online_presence_and_broadcasts_user_online()
{
// Arrange:观察者 Carol 先连上,监听全网在线事件
var carol = await ConnectAsync(ImServerFixture.CarolId, "Carol");
Guid? onlineUserId = null;
carol.On("OnUserOnline", payload => onlineUserId = payload.UserId);
// Act:Alice 新建连接
var alice = await ConnectAsync(ImServerFixture.AliceId, "Alice");
// Assert ①:全网收到 Alice 上线事件
await WaitForAsync(() => onlineUserId == ImServerFixture.AliceId);
// Assert ②:Redis 记录 Alice 在线
var presence = _fixture.Services.GetRequiredService();
Assert.True(await presence.IsOnlineAsync(ImServerFixture.AliceId));
}
///
/// 在线状态读写契约:连接建立即记录在线(SADD),断开即清除(SREM),最终 IsOnline 反映真实状态。
///
///
/// 说明:ChatHub.OnDisconnectedAsync 的端到端(客户端 StopAsync → 服务端 OnDisconnectedAsync)在 TestServer
/// 内存传输下不可靠(Close 帧不会稳定触发服务端断开钩子),故此处改为直接驱动真实的
/// ,验证 Redis 在线状态读扩散的写读语义。Hub 断开钩子的代码正确性
/// 已由代码评审确认(OnDisconnectedAsync 使用 CancellationToken.None,确保 Redis 清理与离线广播不被中止令牌打断)。
///
[Fact]
public async Task Online_presence_records_connection_and_disconnection_in_redis()
{
var presence = _fixture.Services.GetRequiredService();
var connId = "test-conn-" + Guid.NewGuid();
// 连接前:离线
Assert.False(await presence.IsOnlineAsync(ImServerFixture.AliceId));
// 模拟连接:记录在线
await presence.OnConnectedAsync(ImServerFixture.AliceId, connId);
Assert.True(await presence.IsOnlineAsync(ImServerFixture.AliceId));
// 多端登录:第二个连接也记录,仍在线
var connId2 = "test-conn2-" + Guid.NewGuid();
await presence.OnConnectedAsync(ImServerFixture.AliceId, connId2);
Assert.True(await presence.IsOnlineAsync(ImServerFixture.AliceId));
// 断开其中一个连接:仍在线(另一连接在)
await presence.OnDisconnectedAsync(ImServerFixture.AliceId, connId);
Assert.True(await presence.IsOnlineAsync(ImServerFixture.AliceId));
// 全部断开:离线
await presence.OnDisconnectedAsync(ImServerFixture.AliceId, connId2);
Assert.False(await presence.IsOnlineAsync(ImServerFixture.AliceId));
}
[Fact]
public async Task Connection_auto_subscribes_user_to_all_existing_conversations()
{
// Arrange:先播种一个会话,随后让参与者连接——验证"连接即订阅"
_fixture.UserInfoService.SetUser(ImServerFixture.AliceId, "Alice");
_fixture.UserInfoService.SetUser(ImServerFixture.BobId, "Bob");
var convId = await ImTestSeeder.SeedPrivateConversationAsync(_fixture, ImServerFixture.AliceId, ImServerFixture.BobId);
var bob = await ConnectAsync(ImServerFixture.BobId, "Bob");
// Alice 后连接(触发连接即订阅其所有会话),无需手动 JoinConversation 即能收到该会话消息
var alice = await ConnectAsync(ImServerFixture.AliceId, "Alice");
MessageDto? bobGot = null;
bob.On("OnMessageReceived", m => bobGot = m);
// Act:Alice 立即发消息,Bob 应能收到(证明连接时已自动加入会话分组)
await alice.InvokeAsync("SendMessage", new SendMessageRequest(
convId, MessageContentTypeEnum.Text, "auto-subscribe", null, null));
await WaitForAsync(() => bobGot is not null);
Assert.NotNull(bobGot);
Assert.Equal("auto-subscribe", bobGot!.Content);
}
private static async Task WaitForAsync(Func condition, TimeSpan? timeout = null)
{
var deadline = DateTime.UtcNow + (timeout ?? TimeSpan.FromSeconds(5));
while (DateTime.UtcNow < deadline)
{
if (condition()) return;
await Task.Delay(25);
}
throw new TimeoutException("等待条件在超时内未满足");
}
/// 异步条件重载:谓词返回 Task<bool>,用于轮询 Redis 等 IO 状态。
private static async Task WaitForAsync(Func> condition, TimeSpan? timeout = null)
{
var deadline = DateTime.UtcNow + (timeout ?? TimeSpan.FromSeconds(5));
while (DateTime.UtcNow < deadline)
{
if (await condition()) return;
await Task.Delay(25);
}
throw new TimeoutException("等待条件在超时内未满足");
}
}