using BuildFlow.Platform.Application.Notifications.Commands; using BuildFlow.Platform.Application.Notifications.Handlers; using BuildFlow.Platform.Application.Notifications.Queries; using BuildFlow.Platform.Domain.Entities; using BuildFlow.Platform.Infrastructure.Persistence; using FluentAssertions; using Microsoft.EntityFrameworkCore; using NSubstitute; using Xunit; using INotificationDispatcher = BuildFlow.Platform.Application.Notifications.INotificationDispatcher; namespace BuildFlow.Platform.Tests.Notifications; /// /// [SOP-PLATFORM-02] 通知系统全 Handler 覆盖测试。 /// 覆盖:Publish(Fanout) / GetNotifications(分页) / GetUnreadCount / MarkRead / MarkAllRead。 /// 飞书矩阵: platform/Notifications 子域(5 Handler 零覆盖→全补) /// public class NotificationHandlerTests { private static PlatformDbContext CreateDb([System.Runtime.CompilerServices.CallerMemberName] string name = "") => PlatformTestDb.CreateDbContext(name); private static Notification NewNotification(Guid recipient, bool read = false, string type = "system") => PlatformTestDb.WithAudit(new Notification { 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(); 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>(l => l.Count == 2), Arg.Any()); } [Fact] public async Task Publish_NoRecipients_ReturnsZeroAndNoDispatch() { var db = CreateDb(); var dispatcher = Substitute.For(); 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: 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(2); } [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); } }