using FluentAssertions;
using Microsoft.EntityFrameworkCore;
using RAG.Application.Notifications;
using RAG.Application.Notifications.Commands;
using RAG.Domain.Entities;
using RAG.Infrastructure.Persistence;
using Xunit;
namespace RAG.Application.Tests.Notifications;
///
/// 验证消息中心 Fanout 扇出策略:一条业务消息为每个接收人生成一条独立记录,
/// 支持按用户 ID 精确指定与按角色展开,去重后落库。
///
public class PublishNotificationCommandTests
{
private static async Task<(RagDbContext db, PublishNotificationCommandHandler handler, List<(Guid id, string role)> users)>
SetupAsync(string? roleName = "Admin")
{
var options = new DbContextOptionsBuilder()
.UseInMemoryDatabase($"notifications-{Guid.NewGuid()}")
.Options;
var db = new RagDbContext(options);
var dispatcher = new NoOpDispatcher();
var handler = new PublishNotificationCommandHandler(db, dispatcher);
// 构造角色 + 用户(用于角色展开测试)。审计字段在测试中手动填充(生产由 AuditInterceptor 填充)
var role = Audit(new Role { Id = Guid.NewGuid(), Name = roleName ?? "Admin" });
var user1 = Audit(new User { Id = Guid.NewGuid(), Username = "u1", Email = "u1@e.com", PasswordHash = "x" });
var user2 = Audit(new User { Id = Guid.NewGuid(), Username = "u2", Email = "u2@e.com", PasswordHash = "x" });
db.Roles.Add(role);
db.Users.AddRange(user1, user2);
db.UserRoles.AddRange(
Audit(new UserRole { UserId = user1.Id, RoleId = role.Id }),
Audit(new UserRole { UserId = user2.Id, RoleId = role.Id }));
await db.SaveChangesAsync();
return (db, handler, new() { (user1.Id, roleName ?? "Admin"), (user2.Id, roleName ?? "Admin") });
}
/// 填充 IAuditable / IHasOperatorIP 审计字段(测试中绕过 AuditInterceptor)。
private static T Audit(T entity) where T : RAG.Domain.Common.IAuditable
{
var now = DateTime.UtcNow;
entity.CreatedBy = "test";
entity.CreatedAt = now;
entity.UpdatedBy = "test";
entity.UpdatedAt = now;
// IFullAudit 实体还要求 OperatorIP 非空
if (entity is RAG.Domain.Common.IHasOperatorIP hasIp)
hasIp.OperatorIP = "127.0.0.1";
return entity;
}
[Fact]
public async Task Publish_with_explicit_user_ids_creates_one_record_per_recipient()
{
var (db, handler, users) = await SetupAsync();
var recipientIds = users.Select(u => u.id).ToList();
var count = await handler.Handle(new PublishNotificationCommand(
Type: "system", Title: "你好", Content: "测试通知", Source: "test",
RelatedId: null, RelatedType: null,
RecipientUserIds: recipientIds, RecipientRoles: null), default);
count.Should().Be(2);
var notifications = await db.Notifications.ToListAsync();
notifications.Should().HaveCount(2);
notifications.Select(n => n.RecipientUserId).Should().BeEquivalentTo(recipientIds);
notifications.All(n => !n.IsRead && n.Title == "你好").Should().BeTrue();
}
[Fact]
public async Task Publish_by_role_expands_to_all_users_with_that_role()
{
var (db, handler, users) = await SetupAsync(roleName: "Admin");
var count = await handler.Handle(new PublishNotificationCommand(
Type: "task-arrived", Title: "新待办", Content: "请处理", Source: "workflow",
RelatedId: "inst-123", RelatedType: "workflow-instance",
RecipientUserIds: null, RecipientRoles: new List { "Admin" }), default);
count.Should().Be(2);
var notifications = await db.Notifications.ToListAsync();
notifications.Should().HaveCount(2);
notifications.All(n => n.Source == "workflow" && n.RelatedId == "inst-123").Should().BeTrue();
}
[Fact]
public async Task Publish_dedupes_when_user_both_in_ids_and_role()
{
var (db, handler, users) = await SetupAsync(roleName: "Admin");
var firstUser = users[0].id;
// 该用户既在 RecipientUserIds 又拥有 Admin 角色 → 只落一条
var count = await handler.Handle(new PublishNotificationCommand(
Type: "system", Title: "去重测试", Content: "只一条", Source: "test",
RelatedId: null, RelatedType: null,
RecipientUserIds: new List { firstUser },
RecipientRoles: new List { "Admin" }), default);
// 两个 Admin 用户都该收到 = 2 条(去重发生在同一用户内)
count.Should().Be(2);
var notifications = await db.Notifications.Where(n => n.RecipientUserId == firstUser).ToListAsync();
notifications.Should().HaveCount(1);
}
[Fact]
public async Task Publish_with_no_recipients_returns_zero()
{
var (db, handler, _) = await SetupAsync();
var count = await handler.Handle(new PublishNotificationCommand(
Type: "system", Title: "无人接收", Content: "x", Source: "test",
RelatedId: null, RelatedType: null,
RecipientUserIds: null, RecipientRoles: null), default);
count.Should().Be(0);
(await db.Notifications.ToListAsync()).Should().BeEmpty();
}
/// 空操作分发器:测试中不依赖 SignalR,只验证落库。
private sealed class NoOpDispatcher : INotificationDispatcher
{
public Task DispatchAsync(IReadOnlyList notifications, CancellationToken ct = default)
=> Task.CompletedTask;
}
}