- 将AI模型相关端点、命令、处理器移动到模块内部统一管理 - 优化数据库迁移脚本,添加对不同环境表状态的兼容处理逻辑 - 为异常处理添加错误码枚举参数,统一错误处理方式 - 重构多处业务逻辑中的异常抛出方式,增强代码一致性 - 移除独立的组件清单分组和项目定义,简化前端物料面板数据结构 - 修复测试代码中实体引用路径错误问题 - 添加新的监控模块测试项目到解决方案文件中
152 lines
6.0 KiB
C#
152 lines
6.0 KiB
C#
using BuildFlow.Platform.Application.Notifications;
|
|
using BuildFlow.Platform.Application.Notifications.Commands;
|
|
using BuildFlow.Platform.Application.Notifications.Handlers;
|
|
using BuildFlow.Platform.Application.Notifications.Queries;
|
|
using BuildFlow.Platform.Domain.Entities.Notification;
|
|
using BuildFlow.Platform.Infrastructure.Persistence;
|
|
using FluentAssertions;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using NSubstitute;
|
|
using Xunit;
|
|
|
|
namespace BuildFlow.Platform.Tests.Notifications;
|
|
|
|
/// <summary>
|
|
/// [SOP-PLATFORM-02] 通知系统全 Handler 覆盖测试。
|
|
/// 覆盖: Publish(Fanout) / GetNotifications(分页) / GetUnreadCount / MarkRead / MarkAllRead。
|
|
/// 飞书矩阵: platform/Notifications 子域(5 Handler 零覆盖→全补)
|
|
/// </summary>
|
|
public class NotificationHandlerTests
|
|
{
|
|
private static PlatformDbContext CreateDb([System.Runtime.CompilerServices.CallerMemberName] string name = "")
|
|
=> PlatformTestDb.CreateDbContext(name);
|
|
|
|
private static NotificationEntity NewNotification(Guid recipient, bool read = false, string type = "system")
|
|
=> PlatformTestDb.WithAudit(new NotificationEntity
|
|
{
|
|
RecipientUserId = recipient,
|
|
Type = type,
|
|
Title = "测试通知",
|
|
Content = "内容",
|
|
Source = "system",
|
|
IsRead = read,
|
|
ReadAt = read ? DateTime.UtcNow : null
|
|
});
|
|
|
|
[Fact]
|
|
public async Task Publish_WithRecipientUserIds_FanoutAndDispatch()
|
|
{
|
|
var db = CreateDb();
|
|
var dispatcher = Substitute.For<INotificationDispatcher>();
|
|
var u1 = Guid.NewGuid();
|
|
var u2 = Guid.NewGuid();
|
|
var handler = new PublishNotificationCommandHandler(db, dispatcher);
|
|
var count = await handler.Handle(new PublishNotificationCommand(
|
|
"task-arrived", "标题", "内容", "workflow", null, "workflow-instance",
|
|
new[] { u1, u2 }, null), CancellationToken.None);
|
|
count.Should().Be(2);
|
|
(await db.Notifications.IgnoreQueryFilters().ToListAsync())
|
|
.Should().HaveCount(2);
|
|
await dispatcher.Received(1).DispatchAsync(
|
|
Arg.Is<IReadOnlyList<NotificationEntity>>(l => l.Count == 2),
|
|
Arg.Any<CancellationToken>());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Publish_NoRecipients_ReturnsZeroAndNoDispatch()
|
|
{
|
|
var db = CreateDb();
|
|
var dispatcher = Substitute.For<INotificationDispatcher>();
|
|
var handler = new PublishNotificationCommandHandler(db, dispatcher);
|
|
var count = await handler.Handle(new PublishNotificationCommand(
|
|
"system", "t", "c", "system", null, null, null, null), CancellationToken.None);
|
|
count.Should().Be(0);
|
|
await dispatcher.DidNotReceiveWithAnyArgs().DispatchAsync(default!, default);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetNotifications_Paginated_ReturnsUserOnly()
|
|
{
|
|
var db = CreateDb();
|
|
var me = Guid.NewGuid();
|
|
var other = Guid.NewGuid();
|
|
db.Notifications.AddRange(NewNotification(me), NewNotification(me), NewNotification(other));
|
|
await db.SaveChangesAsync();
|
|
var handler = new GetNotificationsQueryHandler(db);
|
|
var result = await handler.Handle(
|
|
new GetNotificationsQuery(me, false, 1, 10), CancellationToken.None);
|
|
result.Items.Should().HaveCount(2);
|
|
result.Items.Should().OnlyContain(n => n.RecipientUserId == me);
|
|
result.Total.Should().Be(2);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetNotifications_UnreadOnly_FiltersRead()
|
|
{
|
|
var db = CreateDb();
|
|
var me = Guid.NewGuid();
|
|
db.Notifications.AddRange(NewNotification(me, read: true), NewNotification(me, read: false));
|
|
await db.SaveChangesAsync();
|
|
var handler = new GetNotificationsQueryHandler(db);
|
|
var result = await handler.Handle(
|
|
new GetNotificationsQuery(me, true, 1, 10), CancellationToken.None);
|
|
result.Items.Should().HaveCount(1);
|
|
result.Items[0].IsRead.Should().BeFalse();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetUnreadCount_ReturnsOnlyUnread()
|
|
{
|
|
var db = CreateDb();
|
|
var me = Guid.NewGuid();
|
|
db.Notifications.AddRange(
|
|
NewNotification(me, read: false),
|
|
NewNotification(me, read: true));
|
|
await db.SaveChangesAsync();
|
|
var handler = new GetUnreadNotificationCountQueryHandler(db);
|
|
var count = await handler.Handle(new GetUnreadNotificationCountQuery(me), CancellationToken.None);
|
|
count.Should().Be(1);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task MarkRead_ExistingUnread_MarksAndReturnsTrue()
|
|
{
|
|
var db = CreateDb();
|
|
var me = Guid.NewGuid();
|
|
var n = NewNotification(me, read: false);
|
|
db.Notifications.Add(n);
|
|
await db.SaveChangesAsync();
|
|
var handler = new MarkNotificationReadCommandHandler(db);
|
|
var ok = await handler.Handle(new MarkNotificationReadCommand(n.Id, me), CancellationToken.None);
|
|
ok.Should().BeTrue();
|
|
(await db.Notifications.FindAsync(n.Id))!.IsRead.Should().BeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task MarkRead_NonExistent_ReturnsFalse()
|
|
{
|
|
var db = CreateDb();
|
|
var handler = new MarkNotificationReadCommandHandler(db);
|
|
var ok = await handler.Handle(
|
|
new MarkNotificationReadCommand(Guid.NewGuid(), Guid.NewGuid()), CancellationToken.None);
|
|
ok.Should().BeFalse();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task MarkAllRead_MarksAllForUser()
|
|
{
|
|
var db = CreateDb();
|
|
var me = Guid.NewGuid();
|
|
var other = Guid.NewGuid();
|
|
db.Notifications.AddRange(
|
|
NewNotification(me, read: false),
|
|
NewNotification(me, read: false),
|
|
NewNotification(other, read: false));
|
|
await db.SaveChangesAsync();
|
|
var handler = new MarkAllNotificationsReadCommandHandler(db);
|
|
var affected = await handler.Handle(new MarkAllNotificationsReadCommand(me), CancellationToken.None);
|
|
affected.Should().Be(2);
|
|
(await db.Notifications.CountAsync(n => n.RecipientUserId == me && !n.IsRead)).Should().Be(0);
|
|
}
|
|
}
|