- 新增 GetRootFilesEndpoint 端点,支持 GET /api/files/root 请求 - 添加 GetRootFilesQuery 查询对象用于获取根级文件 - 实现 GetRootFilesQueryHandler 处理器,查询 FolderId 为 null 的文件 - 统一根目录与子文件夹的数据源,均使用数据库而非 S3 列表 - 修复集成测试中的 MongoDB 访问方式,改用 MongoDB.Entities 静态 DB.Default - 更新权限配置,使 Admin 角色拥有全部权限而非仅部分权限
149 lines
6.1 KiB
C#
149 lines
6.1 KiB
C#
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;
|
||
|
||
/// <summary>
|
||
/// 权限隔离与消息历史分页用例。
|
||
/// 通过 MediatR 直接驱动查询/命令处理器,并验证 ChatHub 的非法访问会被 query 的鉴权副作用拦截。
|
||
/// </summary>
|
||
[Xunit.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)
|
||
{
|
||
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 DB.Default.SaveAsync(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("等待条件在超时内未满足");
|
||
}
|
||
}
|