using IM.Application.Chat.Queries;
using IM.Domain.Enums;
using IM.Infrastructure.Mongo;
using MongoDB.Entities;
using Xunit;
using Microsoft.AspNetCore.SignalR.Client;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.DependencyInjection;
using MediatR;
namespace BuildFlow.IM.Integration.Tests;
///
/// 权限隔离与消息历史分页用例。
/// 通过 MediatR 直接驱动查询/命令处理器,并验证 ChatHub 的非法访问会被 query 的鉴权副作用拦截。
///
[Xunit.Collection(ImServerCollection.Name)]
public class ChatHubAuthAndHistoryTests : IAsyncLifetime
{
private readonly ImServerFixture _fixture;
private readonly List _connections = new();
public ChatHubAuthAndHistoryTests(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 Non_participant_cannot_join_conversation_group()
{
// Arrange:Alice 与 Bob 的私聊,Outsider 不参与
_fixture.UserInfoService.SetUser(ImServerFixture.AliceId, "Alice");
_fixture.UserInfoService.SetUser(ImServerFixture.BobId, "Bob");
var convId = await ImTestSeeder.SeedPrivateConversationAsync(_fixture, ImServerFixture.AliceId, ImServerFixture.BobId);
var outsider = await ConnectAsync(ImServerFixture.OutSiderId, "Outsider");
// Act / Assert:Outsider 调 JoinConversation 应被鉴权拦截(Hub 抛异常)
// SignalR 客户端会把 hub 抛出的异常转为 HubException
var ex = await Assert.ThrowsAsync(() =>
outsider.InvokeAsync("JoinConversation", convId));
Assert.Contains("无权访问该会话", ex.Message);
}
[Fact]
public async Task Get_conversation_participants_rejects_non_participant()
{
// Arrange
var convId = await ImTestSeeder.SeedPrivateConversationAsync(_fixture, ImServerFixture.AliceId, ImServerFixture.BobId);
await using var scope = _fixture.Services.CreateAsyncScope();
var mediator = scope.ServiceProvider.GetRequiredService();
// Act / Assert:非参与者查询参与者列表抛 UnauthorizedException
await Assert.ThrowsAsync(() =>
mediator.Send(new GetConversationParticipantIdsQuery(convId, ImServerFixture.OutSiderId)));
}
[Fact]
public async Task Get_message_history_rejects_non_participant()
{
// Arrange
var convId = await ImTestSeeder.SeedPrivateConversationAsync(_fixture, ImServerFixture.AliceId, ImServerFixture.BobId);
await using var scope = _fixture.Services.CreateAsyncScope();
var mediator = scope.ServiceProvider.GetRequiredService();
// Act / Assert:非参与者拉取历史抛 UnauthorizedException
await Assert.ThrowsAsync(() =>
mediator.Send(new GetMessageHistoryQuery(ImServerFixture.OutSiderId, convId, null, 20)));
}
[Fact]
public async Task Message_history_cursor_pagination_returns_correct_page_and_hasmore()
{
// Arrange:直接落多条消息到 MongoDB(绕过 Hub,聚焦分页逻辑)
_fixture.UserInfoService.SetUser(ImServerFixture.AliceId, "Alice");
var convId = await ImTestSeeder.SeedPrivateConversationAsync(_fixture, ImServerFixture.AliceId, ImServerFixture.BobId);
var messageIds = await SeedMessagesAsync(convId, count: 5);
await using var scope = _fixture.Services.CreateAsyncScope();
var mediator = scope.ServiceProvider.GetRequiredService();
// Act ①:第一页取 3 条(Limit+1 → 实际查 4 条,应判定 hasMore=true)
var page1 = await mediator.Send(new GetMessageHistoryQuery(ImServerFixture.AliceId, convId, null, 3));
// Assert:hasMore 为 true,返回恰好 3 条,且按时间倒序(最新在前)
Assert.True(page1.HasMore);
Assert.Equal(3, page1.Messages.Count);
Assert.Equal(messageIds[4], page1.Messages[0].Id); // 最新的在第 5 个位置插入
// Act ②:以第一页最后一条为游标取下一页(取 3 条,剩 2 条 → hasMore=false)
var cursor = page1.Messages[^1].Id;
var page2 = await mediator.Send(new GetMessageHistoryQuery(ImServerFixture.AliceId, convId, cursor, 3));
// Assert:剩余 2 条,无更多
Assert.False(page2.HasMore);
Assert.Equal(2, page2.Messages.Count);
}
/// 向指定会话连续写入 N 条消息,返回按插入顺序的 ObjectId 列表。
private async Task> SeedMessagesAsync(Guid conversationId, int count)
{
var docs = new List();
var baseTime = DateTime.UtcNow.AddMinutes(-count);
for (var i = 0; i < count; i++)
{
docs.Add(new MessageDocument
{
ConversationId = conversationId,
SenderId = ImServerFixture.AliceId,
SenderName = "Alice",
ContentType = MessageContentType.Text,
Content = $"history-{i}",
CreatedAt = baseTime.AddSeconds(i),
});
}
await DB.Default.SaveAsync(docs);
return docs.Select(d => d.ID).ToList();
}
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("等待条件在超时内未满足");
}
}