using FluentAssertions; using Microsoft.EntityFrameworkCore; using Moq; using TakeoutSaaS.Application.App.Tenants.Commands; using TakeoutSaaS.Application.App.Tenants.Handlers; using TakeoutSaaS.Application.Messaging.Abstractions; using TakeoutSaaS.Domain.Tenants.Entities; using TakeoutSaaS.Domain.Tenants.Enums; using TakeoutSaaS.Infrastructure.App.Repositories; using TakeoutSaaS.Integration.Tests.Fixtures; using TakeoutSaaS.Shared.Abstractions.Constants; using TakeoutSaaS.Shared.Abstractions.Exceptions; namespace TakeoutSaaS.Integration.Tests.App.Tenants; public sealed class AnnouncementWorkflowTests { [Fact] public async Task GivenDraftAnnouncement_WhenPublish_ThenStatusIsPublishedAndActive() { // Arrange using var database = new SqliteTestDatabase(); using var context = database.CreateContext(tenantId: 100, userId: 11); var announcement = CreateDraftAnnouncement(tenantId: 100, id: 9001); context.TenantAnnouncements.Add(announcement); await context.SaveChangesAsync(); context.ChangeTracker.Clear(); var repository = new EfTenantAnnouncementRepository(context); var tenantProvider = new TestTenantProvider(100); var eventPublisher = new Mock(); var handler = new PublishAnnouncementCommandHandler(repository, tenantProvider, eventPublisher.Object); // Act var result = await handler.Handle(new PublishAnnouncementCommand { AnnouncementId = announcement.Id, RowVersion = announcement.RowVersion }, CancellationToken.None); // Assert result.Should().NotBeNull(); result!.Status.Should().Be(AnnouncementStatus.Published); result.IsActive.Should().BeTrue(); using var verifyContext = database.CreateContext(tenantId: 100); var persisted = await verifyContext.TenantAnnouncements.FirstAsync(x => x.Id == announcement.Id); persisted.Status.Should().Be(AnnouncementStatus.Published); persisted.PublishedAt.Should().NotBeNull(); } [Fact] public async Task GivenPublishedAnnouncement_WhenRevoke_ThenStatusIsRevokedAndInactive() { // Arrange using var database = new SqliteTestDatabase(); using var context = database.CreateContext(tenantId: 200, userId: 11); var announcement = CreateDraftAnnouncement(tenantId: 200, id: 9002); announcement.Status = AnnouncementStatus.Published; context.TenantAnnouncements.Add(announcement); await context.SaveChangesAsync(); context.ChangeTracker.Clear(); var repository = new EfTenantAnnouncementRepository(context); var tenantProvider = new TestTenantProvider(200); var eventPublisher = new Mock(); var handler = new RevokeAnnouncementCommandHandler(repository, tenantProvider, eventPublisher.Object); // Act var result = await handler.Handle(new RevokeAnnouncementCommand { AnnouncementId = announcement.Id, RowVersion = announcement.RowVersion }, CancellationToken.None); // Assert result.Should().NotBeNull(); result!.Status.Should().Be(AnnouncementStatus.Revoked); result.IsActive.Should().BeFalse(); using var verifyContext = database.CreateContext(tenantId: 200); var persisted = await verifyContext.TenantAnnouncements.FirstAsync(x => x.Id == announcement.Id); persisted.Status.Should().Be(AnnouncementStatus.Revoked); persisted.RevokedAt.Should().NotBeNull(); } [Fact] public async Task GivenRevokedAnnouncement_WhenPublish_ThenRepublishAndClearRevokedAt() { // Arrange using var database = new SqliteTestDatabase(); using var context = database.CreateContext(tenantId: 300, userId: 11); var announcement = CreateDraftAnnouncement(tenantId: 300, id: 9003); announcement.Status = AnnouncementStatus.Revoked; announcement.RevokedAt = DateTime.UtcNow.AddMinutes(-5); context.TenantAnnouncements.Add(announcement); await context.SaveChangesAsync(); context.ChangeTracker.Clear(); var repository = new EfTenantAnnouncementRepository(context); var tenantProvider = new TestTenantProvider(300); var eventPublisher = new Mock(); var handler = new PublishAnnouncementCommandHandler(repository, tenantProvider, eventPublisher.Object); // Act var result = await handler.Handle(new PublishAnnouncementCommand { AnnouncementId = announcement.Id, RowVersion = announcement.RowVersion }, CancellationToken.None); // Assert result.Should().NotBeNull(); result!.Status.Should().Be(AnnouncementStatus.Published); result.IsActive.Should().BeTrue(); using var verifyContext = database.CreateContext(tenantId: 300); var persisted = await verifyContext.TenantAnnouncements.FirstAsync(x => x.Id == announcement.Id); persisted.Status.Should().Be(AnnouncementStatus.Published); persisted.RevokedAt.Should().BeNull(); } [Fact] public async Task GivenPublishedAnnouncement_WhenUpdate_ThenThrowsBusinessException() { // Arrange using var database = new SqliteTestDatabase(); using var context = database.CreateContext(tenantId: 400, userId: 11); var announcement = CreateDraftAnnouncement(tenantId: 400, id: 9004); announcement.Status = AnnouncementStatus.Published; context.TenantAnnouncements.Add(announcement); await context.SaveChangesAsync(); context.ChangeTracker.Clear(); var repository = new EfTenantAnnouncementRepository(context); var tenantProvider = new TestTenantProvider(400); var handler = new UpdateTenantAnnouncementCommandHandler(repository, tenantProvider); var command = new UpdateTenantAnnouncementCommand { TenantId = 400, AnnouncementId = announcement.Id, Title = "更新标题", Content = "更新内容", TargetType = "ALL_TENANTS", RowVersion = announcement.RowVersion }; // Act Func act = async () => await handler.Handle(command, CancellationToken.None); // Assert var exception = await act.Should().ThrowAsync(); exception.Which.ErrorCode.Should().Be(ErrorCodes.Conflict); } [Fact] public async Task GivenStaleRowVersion_WhenUpdate_ThenReturnsConflict() { // Arrange using var database = new SqliteTestDatabase(); using var context = database.CreateContext(tenantId: 500, userId: 11); var announcement = CreateDraftAnnouncement(tenantId: 500, id: 9005); announcement.RowVersion = new byte[] { 1 }; context.TenantAnnouncements.Add(announcement); await context.SaveChangesAsync(); await context.Database.ExecuteSqlRawAsync( "UPDATE tenant_announcements SET \"RowVersion\" = {0} WHERE \"Id\" = {1}", new byte[] { 9 }, announcement.Id); context.ChangeTracker.Clear(); var repository = new EfTenantAnnouncementRepository(context); var tenantProvider = new TestTenantProvider(500); var handler = new UpdateTenantAnnouncementCommandHandler(repository, tenantProvider); var command = new UpdateTenantAnnouncementCommand { TenantId = 500, AnnouncementId = announcement.Id, Title = "并发更新", Content = "内容", TargetType = "ALL_TENANTS", RowVersion = new byte[] { 1 } }; // Act Func act = async () => await handler.Handle(command, CancellationToken.None); // Assert var exception = await act.Should().ThrowAsync(); exception.Which.ErrorCode.Should().Be(ErrorCodes.Conflict); } private static TenantAnnouncement CreateDraftAnnouncement(long tenantId, long id) => new() { Id = id, TenantId = tenantId, Title = "公告", Content = "内容", AnnouncementType = TenantAnnouncementType.System, Priority = 1, EffectiveFrom = DateTime.UtcNow.AddMinutes(-10), EffectiveTo = DateTime.UtcNow.AddMinutes(30), PublisherScope = PublisherScope.Tenant, Status = AnnouncementStatus.Draft, TargetType = "ALL_TENANTS", TargetParameters = null, RowVersion = new byte[] { 1 } }; }