rag-backend/tests/RAG.Application.Tests/Notifications/PublishNotificationCommandTests.cs
向宁 5b67551fee feat: add notification center, Obsidian integration, RAG retrieval service and SignalR
- Add Notification entity, SignalR hub and NotificationDispatcher
- Add Obsidian document endpoints and document extractors
- Add RagRetrievalService with chunking/retrieval config
- Add ProcessKnowledgeBase and UpdateKnowledgeBase endpoints
- Add EF migrations for RAG enhancements, chunking modes and notification center
- Add unit/integration tests project
2026-06-14 15:01:07 +08:00

129 lines
5.6 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 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;
/// <summary>
/// 验证消息中心 Fanout 扇出策略:一条业务消息为每个接收人生成一条独立记录,
/// 支持按用户 ID 精确指定与按角色展开,去重后落库。
/// </summary>
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<RagDbContext>()
.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") });
}
/// <summary>填充 IAuditable / IHasOperatorIP 审计字段(测试中绕过 AuditInterceptor。</summary>
private static T Audit<T>(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<string> { "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<Guid> { firstUser },
RecipientRoles: new List<string> { "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();
}
/// <summary>空操作分发器:测试中不依赖 SignalR只验证落库。</summary>
private sealed class NoOpDispatcher : INotificationDispatcher
{
public Task DispatchAsync(IReadOnlyList<Notification> notifications, CancellationToken ct = default)
=> Task.CompletedTask;
}
}