build-flow.api/tests/BuildFlow.IM.Integration.Tests/ChatHubAuthAndHistoryTests.cs

150 lines
6.3 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using IM.Application.Chat.Queries;
using IM.Domain.Enums;
using IM.Infrastructure.Mongo;
using Xunit;
using Microsoft.AspNetCore.SignalR.Client;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.DependencyInjection;
using MediatR;
namespace BuildFlow.IM.Integration.Tests;
/// <summary>
/// 权限隔离与消息历史分页用例。
/// 通过 MediatR 直接驱动查询/命令处理器,并验证 ChatHub 的非法访问会被 query 的鉴权副作用拦截。
/// </summary>
[Collection(ImServerCollection.Name)]
public class ChatHubAuthAndHistoryTests : IAsyncLifetime
{
private readonly ImServerFixture _fixture;
private readonly List<HubConnection> _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<HubConnection> 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<HubException>(() =>
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<IMediator>();
// Act / Assert:非参与者查询参与者列表抛 UnauthorizedException
await Assert.ThrowsAsync<BuildFlow.Shared.Domain.Exceptions.UnauthorizedException>(() =>
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<IMediator>();
// Act / Assert:非参与者拉取历史抛 UnauthorizedException
await Assert.ThrowsAsync<BuildFlow.Shared.Domain.Exceptions.UnauthorizedException>(() =>
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<IMediator>();
// 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);
}
/// <summary>向指定会话连续写入 N 条消息,返回按插入顺序的 ObjectId 列表。</summary>
private async Task<List<string>> SeedMessagesAsync(Guid conversationId, int count)
{
await using var scope = _fixture.Services.CreateAsyncScope();
var mongo = scope.ServiceProvider.GetRequiredService<MongoDbContext>();
var docs = new List<MessageDocument>();
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 mongo.Messages.InsertManyAsync(docs);
return docs.Select(d => d.Id).ToList();
}
private static async Task WaitForAsync(Func<bool> 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("等待条件在超时内未满足");
}
}