From 5de862f73f6781f0f21779b71dac01d42c7b4d0b Mon Sep 17 00:00:00 2001 From: root Date: Fri, 30 Jan 2026 01:05:23 +0000 Subject: [PATCH] =?UTF-8?q?refactor:=20=E5=88=A0=E9=99=A4=20tests=20?= =?UTF-8?q?=E7=9B=AE=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- TakeoutSaaS.sln | 56 +- .../GetAnnouncementByIdQueryHandlerTests.cs | 122 ---- ...etTenantsAnnouncementsQueryHandlerTests.cs | 163 ----- ...GetUnreadAnnouncementsQueryHandlerTests.cs | 72 --- ...CreateAnnouncementCommandValidatorTests.cs | 126 ---- ...ublishAnnouncementCommandValidatorTests.cs | 62 -- ...RevokeAnnouncementCommandValidatorTests.cs | 62 -- ...UpdateAnnouncementCommandValidatorTests.cs | 88 --- .../TakeoutSaaS.Application.Tests.csproj | 35 - .../TestUtilities/AnnouncementTestData.cs | 72 --- .../App/Dictionary/DictionaryApiTests.cs | 606 ------------------ .../Tenants/AnnouncementRegressionTests.cs | 99 --- .../App/Tenants/AnnouncementWorkflowTests.cs | 217 ------- .../TenantAnnouncementRepositoryScopeTests.cs | 59 -- .../PermissionAuthorizationHandlerTests.cs | 80 --- .../Fixtures/DictionarySqliteTestDatabase.cs | 49 -- .../Fixtures/SqliteTestDatabase.cs | 46 -- .../Fixtures/TestCurrentUserAccessor.cs | 10 - .../Fixtures/TestDictionaryHybridCache.cs | 46 -- .../Fixtures/TestTenantProvider.cs | 10 - .../AnnouncementQueryPerformanceTests.cs | 77 --- .../TakeoutSaaS.Integration.Tests.csproj | 39 -- 22 files changed, 12 insertions(+), 2184 deletions(-) delete mode 100644 tests/TakeoutSaaS.Application.Tests/App/Tenants/Handlers/GetAnnouncementByIdQueryHandlerTests.cs delete mode 100644 tests/TakeoutSaaS.Application.Tests/App/Tenants/Handlers/GetTenantsAnnouncementsQueryHandlerTests.cs delete mode 100644 tests/TakeoutSaaS.Application.Tests/App/Tenants/Handlers/GetUnreadAnnouncementsQueryHandlerTests.cs delete mode 100644 tests/TakeoutSaaS.Application.Tests/App/Tenants/Validators/CreateAnnouncementCommandValidatorTests.cs delete mode 100644 tests/TakeoutSaaS.Application.Tests/App/Tenants/Validators/PublishAnnouncementCommandValidatorTests.cs delete mode 100644 tests/TakeoutSaaS.Application.Tests/App/Tenants/Validators/RevokeAnnouncementCommandValidatorTests.cs delete mode 100644 tests/TakeoutSaaS.Application.Tests/App/Tenants/Validators/UpdateAnnouncementCommandValidatorTests.cs delete mode 100644 tests/TakeoutSaaS.Application.Tests/TakeoutSaaS.Application.Tests.csproj delete mode 100644 tests/TakeoutSaaS.Application.Tests/TestUtilities/AnnouncementTestData.cs delete mode 100644 tests/TakeoutSaaS.Integration.Tests/App/Dictionary/DictionaryApiTests.cs delete mode 100644 tests/TakeoutSaaS.Integration.Tests/App/Tenants/AnnouncementRegressionTests.cs delete mode 100644 tests/TakeoutSaaS.Integration.Tests/App/Tenants/AnnouncementWorkflowTests.cs delete mode 100644 tests/TakeoutSaaS.Integration.Tests/App/Tenants/TenantAnnouncementRepositoryScopeTests.cs delete mode 100644 tests/TakeoutSaaS.Integration.Tests/Authorization/PermissionAuthorizationHandlerTests.cs delete mode 100644 tests/TakeoutSaaS.Integration.Tests/Fixtures/DictionarySqliteTestDatabase.cs delete mode 100644 tests/TakeoutSaaS.Integration.Tests/Fixtures/SqliteTestDatabase.cs delete mode 100644 tests/TakeoutSaaS.Integration.Tests/Fixtures/TestCurrentUserAccessor.cs delete mode 100644 tests/TakeoutSaaS.Integration.Tests/Fixtures/TestDictionaryHybridCache.cs delete mode 100644 tests/TakeoutSaaS.Integration.Tests/Fixtures/TestTenantProvider.cs delete mode 100644 tests/TakeoutSaaS.Integration.Tests/Performance/AnnouncementQueryPerformanceTests.cs delete mode 100644 tests/TakeoutSaaS.Integration.Tests/TakeoutSaaS.Integration.Tests.csproj diff --git a/TakeoutSaaS.sln b/TakeoutSaaS.sln index 08719a4..5c6b1f8 100644 --- a/TakeoutSaaS.sln +++ b/TakeoutSaaS.sln @@ -45,12 +45,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TakeoutSaaS.Module.Schedule EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TakeoutSaaS.Module.Sms", "src\Modules\TakeoutSaaS.Module.Sms\TakeoutSaaS.Module.Sms.csproj", "{38011EC3-7EC3-40E4-B9B2-E631966B350B}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{0AB3BF05-4346-4AA6-1389-037BE0695223}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TakeoutSaaS.Application.Tests", "tests\TakeoutSaaS.Application.Tests\TakeoutSaaS.Application.Tests.csproj", "{2601637E-777A-4FA2-81BA-1AFE32E961FF}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TakeoutSaaS.Integration.Tests", "tests\TakeoutSaaS.Integration.Tests\TakeoutSaaS.Integration.Tests.csproj", "{8179CA95-33F8-45F2-BA29-9B1CC7D1E7CB}" -EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TakeoutSaaS.TenantApi", "src\Api\TakeoutSaaS.TenantApi\TakeoutSaaS.TenantApi.csproj", "{F53E274A-838A-477A-8D29-6EEB0DBD62CD}" EndProject Global @@ -62,7 +56,7 @@ Global Release|x64 = Release|x64 Release|x86 = Release|x86 EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution + GlobalSection(ProjectConfigurationPlatforms) = postSolution {022FCF39-EC48-46EA-AC08-FA2EAD1548B7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {022FCF39-EC48-46EA-AC08-FA2EAD1548B7}.Debug|Any CPU.Build.0 = Debug|Any CPU {022FCF39-EC48-46EA-AC08-FA2EAD1548B7}.Debug|x64.ActiveCfg = Debug|Any CPU @@ -228,36 +222,12 @@ Global {38011EC3-7EC3-40E4-B9B2-E631966B350B}.Release|Any CPU.ActiveCfg = Release|Any CPU {38011EC3-7EC3-40E4-B9B2-E631966B350B}.Release|Any CPU.Build.0 = Release|Any CPU {38011EC3-7EC3-40E4-B9B2-E631966B350B}.Release|x64.ActiveCfg = Release|Any CPU - {38011EC3-7EC3-40E4-B9B2-E631966B350B}.Release|x64.Build.0 = Release|Any CPU - {38011EC3-7EC3-40E4-B9B2-E631966B350B}.Release|x86.ActiveCfg = Release|Any CPU - {38011EC3-7EC3-40E4-B9B2-E631966B350B}.Release|x86.Build.0 = Release|Any CPU - {2601637E-777A-4FA2-81BA-1AFE32E961FF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {2601637E-777A-4FA2-81BA-1AFE32E961FF}.Debug|Any CPU.Build.0 = Debug|Any CPU - {2601637E-777A-4FA2-81BA-1AFE32E961FF}.Debug|x64.ActiveCfg = Debug|Any CPU - {2601637E-777A-4FA2-81BA-1AFE32E961FF}.Debug|x64.Build.0 = Debug|Any CPU - {2601637E-777A-4FA2-81BA-1AFE32E961FF}.Debug|x86.ActiveCfg = Debug|Any CPU - {2601637E-777A-4FA2-81BA-1AFE32E961FF}.Debug|x86.Build.0 = Debug|Any CPU - {2601637E-777A-4FA2-81BA-1AFE32E961FF}.Release|Any CPU.ActiveCfg = Release|Any CPU - {2601637E-777A-4FA2-81BA-1AFE32E961FF}.Release|Any CPU.Build.0 = Release|Any CPU - {2601637E-777A-4FA2-81BA-1AFE32E961FF}.Release|x64.ActiveCfg = Release|Any CPU - {2601637E-777A-4FA2-81BA-1AFE32E961FF}.Release|x64.Build.0 = Release|Any CPU - {2601637E-777A-4FA2-81BA-1AFE32E961FF}.Release|x86.ActiveCfg = Release|Any CPU - {2601637E-777A-4FA2-81BA-1AFE32E961FF}.Release|x86.Build.0 = Release|Any CPU - {8179CA95-33F8-45F2-BA29-9B1CC7D1E7CB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {8179CA95-33F8-45F2-BA29-9B1CC7D1E7CB}.Debug|Any CPU.Build.0 = Debug|Any CPU - {8179CA95-33F8-45F2-BA29-9B1CC7D1E7CB}.Debug|x64.ActiveCfg = Debug|Any CPU - {8179CA95-33F8-45F2-BA29-9B1CC7D1E7CB}.Debug|x64.Build.0 = Debug|Any CPU - {8179CA95-33F8-45F2-BA29-9B1CC7D1E7CB}.Debug|x86.ActiveCfg = Debug|Any CPU - {8179CA95-33F8-45F2-BA29-9B1CC7D1E7CB}.Debug|x86.Build.0 = Debug|Any CPU - {8179CA95-33F8-45F2-BA29-9B1CC7D1E7CB}.Release|Any CPU.ActiveCfg = Release|Any CPU - {8179CA95-33F8-45F2-BA29-9B1CC7D1E7CB}.Release|Any CPU.Build.0 = Release|Any CPU - {8179CA95-33F8-45F2-BA29-9B1CC7D1E7CB}.Release|x64.ActiveCfg = Release|Any CPU - {8179CA95-33F8-45F2-BA29-9B1CC7D1E7CB}.Release|x64.Build.0 = Release|Any CPU - {8179CA95-33F8-45F2-BA29-9B1CC7D1E7CB}.Release|x86.ActiveCfg = Release|Any CPU - {8179CA95-33F8-45F2-BA29-9B1CC7D1E7CB}.Release|x86.Build.0 = Release|Any CPU - {F53E274A-838A-477A-8D29-6EEB0DBD62CD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {F53E274A-838A-477A-8D29-6EEB0DBD62CD}.Debug|Any CPU.Build.0 = Debug|Any CPU - {F53E274A-838A-477A-8D29-6EEB0DBD62CD}.Debug|x64.ActiveCfg = Debug|Any CPU + {38011EC3-7EC3-40E4-B9B2-E631966B350B}.Release|x64.Build.0 = Release|Any CPU + {38011EC3-7EC3-40E4-B9B2-E631966B350B}.Release|x86.ActiveCfg = Release|Any CPU + {38011EC3-7EC3-40E4-B9B2-E631966B350B}.Release|x86.Build.0 = Release|Any CPU + {F53E274A-838A-477A-8D29-6EEB0DBD62CD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F53E274A-838A-477A-8D29-6EEB0DBD62CD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F53E274A-838A-477A-8D29-6EEB0DBD62CD}.Debug|x64.ActiveCfg = Debug|Any CPU {F53E274A-838A-477A-8D29-6EEB0DBD62CD}.Debug|x64.Build.0 = Debug|Any CPU {F53E274A-838A-477A-8D29-6EEB0DBD62CD}.Debug|x86.ActiveCfg = Debug|Any CPU {F53E274A-838A-477A-8D29-6EEB0DBD62CD}.Debug|x86.Build.0 = Debug|Any CPU @@ -289,11 +259,9 @@ Global {05058F44-6FB7-43AF-8648-8BF538E283EF} = {EC447DCF-ABFA-6E24-52A5-D7FD48A5C558} {5C12177E-6C25-4F78-BFD4-AA073CFC0650} = {EC447DCF-ABFA-6E24-52A5-D7FD48A5C558} {5ADA37B6-09A0-48F7-8633-C266FD5BBFD1} = {EC447DCF-ABFA-6E24-52A5-D7FD48A5C558} - {FE49A9E7-1228-45BA-9B71-337AA353FE98} = {EC447DCF-ABFA-6E24-52A5-D7FD48A5C558} - {9C2F510E-4054-482D-AFD3-D2E374D60304} = {EC447DCF-ABFA-6E24-52A5-D7FD48A5C558} - {38011EC3-7EC3-40E4-B9B2-E631966B350B} = {EC447DCF-ABFA-6E24-52A5-D7FD48A5C558} - {2601637E-777A-4FA2-81BA-1AFE32E961FF} = {0AB3BF05-4346-4AA6-1389-037BE0695223} - {8179CA95-33F8-45F2-BA29-9B1CC7D1E7CB} = {0AB3BF05-4346-4AA6-1389-037BE0695223} - {F53E274A-838A-477A-8D29-6EEB0DBD62CD} = {81034408-37C8-1011-444E-4C15C2FADA8E} - EndGlobalSection + {FE49A9E7-1228-45BA-9B71-337AA353FE98} = {EC447DCF-ABFA-6E24-52A5-D7FD48A5C558} + {9C2F510E-4054-482D-AFD3-D2E374D60304} = {EC447DCF-ABFA-6E24-52A5-D7FD48A5C558} + {38011EC3-7EC3-40E4-B9B2-E631966B350B} = {EC447DCF-ABFA-6E24-52A5-D7FD48A5C558} + {F53E274A-838A-477A-8D29-6EEB0DBD62CD} = {81034408-37C8-1011-444E-4C15C2FADA8E} + EndGlobalSection EndGlobal diff --git a/tests/TakeoutSaaS.Application.Tests/App/Tenants/Handlers/GetAnnouncementByIdQueryHandlerTests.cs b/tests/TakeoutSaaS.Application.Tests/App/Tenants/Handlers/GetAnnouncementByIdQueryHandlerTests.cs deleted file mode 100644 index 57a6470..0000000 --- a/tests/TakeoutSaaS.Application.Tests/App/Tenants/Handlers/GetAnnouncementByIdQueryHandlerTests.cs +++ /dev/null @@ -1,122 +0,0 @@ -using FluentAssertions; -using Moq; -using TakeoutSaaS.Application.App.Tenants.Handlers; -using TakeoutSaaS.Application.App.Tenants.Queries; -using TakeoutSaaS.Application.Tests.TestUtilities; -using TakeoutSaaS.Domain.Tenants.Entities; -using TakeoutSaaS.Domain.Tenants.Repositories; -using TakeoutSaaS.Shared.Abstractions.Security; -using TakeoutSaaS.Shared.Abstractions.Tenancy; - -namespace TakeoutSaaS.Application.Tests.App.Tenants.Handlers; - -public sealed class GetAnnouncementByIdQueryHandlerTests -{ - [Fact] - public async Task GivenAnnouncementMissing_WhenHandle_ThenReturnsNull() - { - // Arrange - var tenantProvider = new Mock(); - tenantProvider.Setup(x => x.GetCurrentTenantId()).Returns(99); - - var announcementRepository = new Mock(); - announcementRepository - .Setup(x => x.FindByIdAsync(99, 500, It.IsAny())) - .ReturnsAsync((TenantAnnouncement?)null); - - var readRepository = new Mock(); - - var handler = new GetAnnouncementByIdQueryHandler( - announcementRepository.Object, - readRepository.Object, - tenantProvider.Object); - - // Act - var result = await handler.Handle(new GetAnnouncementByIdQuery { AnnouncementId = 500 }, CancellationToken.None); - - // Assert - result.Should().BeNull(); - } - - [Fact] - public async Task GivenTargetNotMatched_WhenHandle_ThenReturnsNullAndSkipsReadLookup() - { - // Arrange - var tenantProvider = new Mock(); - tenantProvider.Setup(x => x.GetCurrentTenantId()).Returns(100); - - var currentUserAccessor = new Mock(); - currentUserAccessor.SetupGet(x => x.UserId).Returns(123); - currentUserAccessor.SetupGet(x => x.IsAuthenticated).Returns(true); - - var announcement = AnnouncementTestData.CreateAnnouncement(1, 100, 1, DateTime.UtcNow); - announcement.TargetType = "SPECIFIC_USERS"; - announcement.TargetParameters = "{\"userIds\":[999]}"; - - var announcementRepository = new Mock(); - announcementRepository - .Setup(x => x.FindByIdAsync(100, 1, It.IsAny())) - .ReturnsAsync(announcement); - - var readRepository = new Mock(); - - var handler = new GetAnnouncementByIdQueryHandler( - announcementRepository.Object, - readRepository.Object, - tenantProvider.Object, - currentUserAccessor.Object); - - // Act - var result = await handler.Handle(new GetAnnouncementByIdQuery { AnnouncementId = 1 }, CancellationToken.None); - - // Assert - result.Should().BeNull(); - readRepository.Verify(x => x.GetByAnnouncementAsync( - It.IsAny(), - It.IsAny>(), - It.IsAny(), - It.IsAny()), Times.Never); - } - - [Fact] - public async Task GivenUserReadRecord_WhenHandle_ThenReturnsDtoWithReadState() - { - // Arrange - var tenantProvider = new Mock(); - tenantProvider.Setup(x => x.GetCurrentTenantId()).Returns(200); - - var currentUserAccessor = new Mock(); - currentUserAccessor.SetupGet(x => x.UserId).Returns(321); - currentUserAccessor.SetupGet(x => x.IsAuthenticated).Returns(true); - - var announcement = AnnouncementTestData.CreateAnnouncement(10, 200, 1, DateTime.UtcNow); - var readAt = DateTime.UtcNow.AddMinutes(-3); - - var announcementRepository = new Mock(); - announcementRepository - .Setup(x => x.FindByIdAsync(200, 10, It.IsAny())) - .ReturnsAsync(announcement); - - var readRepository = new Mock(); - readRepository - .Setup(x => x.GetByAnnouncementAsync(200, It.IsAny>(), 321, It.IsAny())) - .ReturnsAsync(new List - { - new() { AnnouncementId = 10, TenantId = 200, UserId = 321, ReadAt = readAt } - }); - - var handler = new GetAnnouncementByIdQueryHandler( - announcementRepository.Object, - readRepository.Object, - tenantProvider.Object, - currentUserAccessor.Object); - - // Act - var result = await handler.Handle(new GetAnnouncementByIdQuery { AnnouncementId = 10 }, CancellationToken.None); - - // Assert - result.Should().NotBeNull(); - result!.IsRead.Should().BeTrue(); - result.ReadAt.Should().BeCloseTo(readAt, TimeSpan.FromSeconds(1)); - } -} diff --git a/tests/TakeoutSaaS.Application.Tests/App/Tenants/Handlers/GetTenantsAnnouncementsQueryHandlerTests.cs b/tests/TakeoutSaaS.Application.Tests/App/Tenants/Handlers/GetTenantsAnnouncementsQueryHandlerTests.cs deleted file mode 100644 index a58c775..0000000 --- a/tests/TakeoutSaaS.Application.Tests/App/Tenants/Handlers/GetTenantsAnnouncementsQueryHandlerTests.cs +++ /dev/null @@ -1,163 +0,0 @@ -using FluentAssertions; -using Moq; -using TakeoutSaaS.Application.App.Tenants.Handlers; -using TakeoutSaaS.Application.App.Tenants.Queries; -using TakeoutSaaS.Application.Tests.TestUtilities; -using TakeoutSaaS.Domain.Tenants.Repositories; -using TakeoutSaaS.Shared.Abstractions.Security; -using TakeoutSaaS.Shared.Abstractions.Tenancy; - -namespace TakeoutSaaS.Application.Tests.App.Tenants.Handlers; - -public sealed class GetTenantsAnnouncementsQueryHandlerTests -{ - [Fact] - public async Task GivenQuery_WhenHandle_ThenUsesTenantProviderAndOrdersAndPaginates() - { - // Arrange - var tenantProvider = new Mock(); - tenantProvider.Setup(x => x.GetCurrentTenantId()).Returns(42); - - var currentUserAccessor = new Mock(); - currentUserAccessor.SetupGet(x => x.UserId).Returns(0); - currentUserAccessor.SetupGet(x => x.IsAuthenticated).Returns(false); - - var announcements = new List - { - AnnouncementTestData.CreateAnnouncement(1, 42, priority: 1, effectiveFrom: DateTime.UtcNow.AddDays(-1)), - AnnouncementTestData.CreateAnnouncement(2, 42, priority: 2, effectiveFrom: DateTime.UtcNow.AddDays(-3)), - AnnouncementTestData.CreateAnnouncement(3, 42, priority: 2, effectiveFrom: DateTime.UtcNow.AddDays(-1)), - AnnouncementTestData.CreateAnnouncement(4, 42, priority: 0, effectiveFrom: DateTime.UtcNow) - }; - - // 模拟数据库端排序:按 priority DESC, effectiveFrom DESC - var sortedAnnouncements = announcements - .OrderByDescending(x => x.Priority) - .ThenByDescending(x => x.EffectiveFrom) - .ToList(); - - var announcementRepository = new Mock(); - announcementRepository - .Setup(x => x.SearchAsync( - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny())) - .ReturnsAsync(sortedAnnouncements); - - var announcementReadRepository = new Mock(); - announcementReadRepository - .Setup(x => x.GetByAnnouncementAsync( - It.IsAny(), - It.IsAny>(), - It.IsAny(), - It.IsAny())) - .ReturnsAsync(Array.Empty()); - - var handler = new GetTenantsAnnouncementsQueryHandler( - announcementRepository.Object, - announcementReadRepository.Object, - tenantProvider.Object, - currentUserAccessor.Object); - - var query = new GetTenantsAnnouncementsQuery - { - TenantId = 999, - Page = 2, - PageSize = 2 - }; - - // Act - var result = await handler.Handle(query, CancellationToken.None); - - // Assert - announcementRepository.Verify(x => x.SearchAsync( - 42, - query.Keyword, - query.Status, - query.AnnouncementType, - query.IsActive, - query.EffectiveFrom, - query.EffectiveTo, - null, - true, - 12, // estimatedLimit = page * size * 3 = 2 * 2 * 3 = 12 - It.IsAny()), Times.Once); - - result.TotalCount.Should().Be(4); - result.Items.Select(x => x.Id).Should().Equal(1, 4); - result.Page.Should().Be(2); - result.PageSize.Should().Be(2); - } - - [Fact] - public async Task GivenOnlyEffective_WhenHandle_ThenFiltersScheduledPublish() - { - // Arrange - var tenantProvider = new Mock(); - tenantProvider.Setup(x => x.GetCurrentTenantId()).Returns(42); - - var announcement1 = AnnouncementTestData.CreateAnnouncement(1, 42, priority: 1, effectiveFrom: DateTime.UtcNow.AddDays(-1)); - announcement1.ScheduledPublishAt = DateTime.UtcNow.AddMinutes(-10); - - var announcement2 = AnnouncementTestData.CreateAnnouncement(2, 42, priority: 1, effectiveFrom: DateTime.UtcNow.AddDays(-1)); - announcement2.ScheduledPublishAt = DateTime.UtcNow.AddMinutes(30); - - var announcements = new List - { - announcement1, - announcement2 - }; - - var announcementRepository = new Mock(); - announcementRepository - .Setup(x => x.SearchAsync( - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny())) - .ReturnsAsync(announcements); - - var announcementReadRepository = new Mock(); - announcementReadRepository - .Setup(x => x.GetByAnnouncementAsync( - It.IsAny(), - It.IsAny>(), - It.IsAny(), - It.IsAny())) - .ReturnsAsync(Array.Empty()); - - var handler = new GetTenantsAnnouncementsQueryHandler( - announcementRepository.Object, - announcementReadRepository.Object, - tenantProvider.Object); - - var query = new GetTenantsAnnouncementsQuery - { - OnlyEffective = true, - Page = 1, - PageSize = 10 - }; - - // Act - var result = await handler.Handle(query, CancellationToken.None); - - // Assert - result.Items.Should().ContainSingle(); - result.Items[0].Id.Should().Be(1); - } -} diff --git a/tests/TakeoutSaaS.Application.Tests/App/Tenants/Handlers/GetUnreadAnnouncementsQueryHandlerTests.cs b/tests/TakeoutSaaS.Application.Tests/App/Tenants/Handlers/GetUnreadAnnouncementsQueryHandlerTests.cs deleted file mode 100644 index 81f60f7..0000000 --- a/tests/TakeoutSaaS.Application.Tests/App/Tenants/Handlers/GetUnreadAnnouncementsQueryHandlerTests.cs +++ /dev/null @@ -1,72 +0,0 @@ -using FluentAssertions; -using Moq; -using TakeoutSaaS.Application.App.Tenants.Handlers; -using TakeoutSaaS.Application.App.Tenants.Queries; -using TakeoutSaaS.Application.Tests.TestUtilities; -using TakeoutSaaS.Domain.Tenants.Enums; -using TakeoutSaaS.Domain.Tenants.Repositories; -using TakeoutSaaS.Shared.Abstractions.Security; -using TakeoutSaaS.Shared.Abstractions.Tenancy; - -namespace TakeoutSaaS.Application.Tests.App.Tenants.Handlers; - -public sealed class GetUnreadAnnouncementsQueryHandlerTests -{ - [Fact] - public async Task GivenUnreadAnnouncements_WhenHandle_ThenUsesTenantProviderAndPaginates() - { - // Arrange - var tenantProvider = new Mock(); - tenantProvider.Setup(x => x.GetCurrentTenantId()).Returns(55); - - var currentUserAccessor = new Mock(); - currentUserAccessor.SetupGet(x => x.UserId).Returns(0); - currentUserAccessor.SetupGet(x => x.IsAuthenticated).Returns(false); - - var announcements = new List - { - AnnouncementTestData.CreateAnnouncement(1, 55, priority: 1, effectiveFrom: DateTime.UtcNow.AddDays(-1), status: AnnouncementStatus.Published), - AnnouncementTestData.CreateAnnouncement(2, 55, priority: 3, effectiveFrom: DateTime.UtcNow.AddDays(-2), status: AnnouncementStatus.Published), - AnnouncementTestData.CreateAnnouncement(3, 55, priority: 2, effectiveFrom: DateTime.UtcNow, status: AnnouncementStatus.Published) - }; - - var announcementRepository = new Mock(); - announcementRepository - .Setup(x => x.SearchUnreadAsync( - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny())) - .ReturnsAsync(announcements); - - var handler = new GetUnreadAnnouncementsQueryHandler( - announcementRepository.Object, - tenantProvider.Object, - currentUserAccessor.Object); - - var query = new GetUnreadAnnouncementsQuery - { - Page = 1, - PageSize = 2 - }; - - // Act - var result = await handler.Handle(query, CancellationToken.None); - - // Assert - announcementRepository.Verify(x => x.SearchUnreadAsync( - 55, - null, - AnnouncementStatus.Published, - true, - It.IsAny(), - It.IsAny()), Times.Once); - - result.Items.Select(x => x.Id).Should().Equal(2, 3); - result.TotalCount.Should().Be(3); - result.Page.Should().Be(1); - result.PageSize.Should().Be(2); - } -} diff --git a/tests/TakeoutSaaS.Application.Tests/App/Tenants/Validators/CreateAnnouncementCommandValidatorTests.cs b/tests/TakeoutSaaS.Application.Tests/App/Tenants/Validators/CreateAnnouncementCommandValidatorTests.cs deleted file mode 100644 index af52c42..0000000 --- a/tests/TakeoutSaaS.Application.Tests/App/Tenants/Validators/CreateAnnouncementCommandValidatorTests.cs +++ /dev/null @@ -1,126 +0,0 @@ -using FluentValidation.TestHelper; -using TakeoutSaaS.Application.App.Tenants.Validators; -using TakeoutSaaS.Application.Tests.TestUtilities; -using TakeoutSaaS.Domain.Tenants.Enums; - -namespace TakeoutSaaS.Application.Tests.App.Tenants.Validators; - -public sealed class CreateAnnouncementCommandValidatorTests -{ - private readonly CreateAnnouncementCommandValidator _validator = new(); - - [Fact] - public void GivenEmptyTitle_WhenValidate_ThenShouldHaveError() - { - // Arrange - var command = AnnouncementTestData.CreateValidCreateCommand() with { Title = "" }; - - // Act - var result = _validator.TestValidate(command); - - // Assert - result.ShouldHaveValidationErrorFor(x => x.Title); - } - - [Fact] - public void GivenTitleTooLong_WhenValidate_ThenShouldHaveError() - { - // Arrange - var command = AnnouncementTestData.CreateValidCreateCommand() with { Title = new string('A', 129) }; - - // Act - var result = _validator.TestValidate(command); - - // Assert - result.ShouldHaveValidationErrorFor(x => x.Title); - } - - [Fact] - public void GivenEmptyContent_WhenValidate_ThenShouldHaveError() - { - // Arrange - var command = AnnouncementTestData.CreateValidCreateCommand() with { Content = "" }; - - // Act - var result = _validator.TestValidate(command); - - // Assert - result.ShouldHaveValidationErrorFor(x => x.Content); - } - - [Fact] - public void GivenEmptyTargetType_WhenValidate_ThenShouldHaveError() - { - // Arrange - var command = AnnouncementTestData.CreateValidCreateCommand() with { TargetType = "" }; - - // Act - var result = _validator.TestValidate(command); - - // Assert - result.ShouldHaveValidationErrorFor(x => x.TargetType); - } - - [Fact] - public void GivenTenantIdZero_WhenValidate_ThenShouldHaveError() - { - // Arrange - var command = AnnouncementTestData.CreateValidCreateCommand() with - { - TenantId = 0, - PublisherScope = PublisherScope.Tenant - }; - - // Act - var result = _validator.TestValidate(command); - - // Assert - result.ShouldHaveValidationErrorFor(x => x.TenantId); - } - - [Fact] - public void GivenPlatformPublisherScope_WhenValidate_ThenShouldHaveError() - { - // Arrange - var command = AnnouncementTestData.CreateValidCreateCommand() with - { - PublisherScope = PublisherScope.System - }; - - // Act - var result = _validator.TestValidate(command); - - // Assert - result.ShouldHaveValidationErrorFor(x => x.PublisherScope); - } - - [Fact] - public void GivenEffectiveToBeforeFrom_WhenValidate_ThenShouldHaveError() - { - // Arrange - var command = AnnouncementTestData.CreateValidCreateCommand() with - { - EffectiveFrom = DateTime.UtcNow, - EffectiveTo = DateTime.UtcNow.AddMinutes(-5) - }; - - // Act - var result = _validator.TestValidate(command); - - // Assert - result.ShouldHaveValidationErrorFor(x => x.EffectiveTo); - } - - [Fact] - public void GivenValidCommand_WhenValidate_ThenShouldNotHaveErrors() - { - // Arrange - var command = AnnouncementTestData.CreateValidCreateCommand(); - - // Act - var result = _validator.TestValidate(command); - - // Assert - result.ShouldNotHaveAnyValidationErrors(); - } -} diff --git a/tests/TakeoutSaaS.Application.Tests/App/Tenants/Validators/PublishAnnouncementCommandValidatorTests.cs b/tests/TakeoutSaaS.Application.Tests/App/Tenants/Validators/PublishAnnouncementCommandValidatorTests.cs deleted file mode 100644 index e325755..0000000 --- a/tests/TakeoutSaaS.Application.Tests/App/Tenants/Validators/PublishAnnouncementCommandValidatorTests.cs +++ /dev/null @@ -1,62 +0,0 @@ -using FluentValidation.TestHelper; -using TakeoutSaaS.Application.App.Tenants.Validators; -using TakeoutSaaS.Application.Tests.TestUtilities; - -namespace TakeoutSaaS.Application.Tests.App.Tenants.Validators; - -public sealed class PublishAnnouncementCommandValidatorTests -{ - private readonly PublishAnnouncementCommandValidator _validator = new(); - - [Fact] - public void GivenAnnouncementIdZero_WhenValidate_ThenShouldHaveError() - { - // Arrange - var command = AnnouncementTestData.CreateValidPublishCommand() with { AnnouncementId = 0 }; - - // Act - var result = _validator.TestValidate(command); - - // Assert - result.ShouldHaveValidationErrorFor(x => x.AnnouncementId); - } - - [Fact] - public void GivenNullRowVersion_WhenValidate_ThenShouldHaveError() - { - // Arrange - var command = AnnouncementTestData.CreateValidPublishCommand() with { RowVersion = null! }; - - // Act - var result = _validator.TestValidate(command); - - // Assert - result.ShouldHaveValidationErrorFor(x => x.RowVersion); - } - - [Fact] - public void GivenEmptyRowVersion_WhenValidate_ThenShouldHaveError() - { - // Arrange - var command = AnnouncementTestData.CreateValidPublishCommand() with { RowVersion = Array.Empty() }; - - // Act - var result = _validator.TestValidate(command); - - // Assert - result.ShouldHaveValidationErrorFor(x => x.RowVersion); - } - - [Fact] - public void GivenValidCommand_WhenValidate_ThenShouldNotHaveErrors() - { - // Arrange - var command = AnnouncementTestData.CreateValidPublishCommand(); - - // Act - var result = _validator.TestValidate(command); - - // Assert - result.ShouldNotHaveAnyValidationErrors(); - } -} diff --git a/tests/TakeoutSaaS.Application.Tests/App/Tenants/Validators/RevokeAnnouncementCommandValidatorTests.cs b/tests/TakeoutSaaS.Application.Tests/App/Tenants/Validators/RevokeAnnouncementCommandValidatorTests.cs deleted file mode 100644 index 694c8a5..0000000 --- a/tests/TakeoutSaaS.Application.Tests/App/Tenants/Validators/RevokeAnnouncementCommandValidatorTests.cs +++ /dev/null @@ -1,62 +0,0 @@ -using FluentValidation.TestHelper; -using TakeoutSaaS.Application.App.Tenants.Validators; -using TakeoutSaaS.Application.Tests.TestUtilities; - -namespace TakeoutSaaS.Application.Tests.App.Tenants.Validators; - -public sealed class RevokeAnnouncementCommandValidatorTests -{ - private readonly RevokeAnnouncementCommandValidator _validator = new(); - - [Fact] - public void GivenAnnouncementIdZero_WhenValidate_ThenShouldHaveError() - { - // Arrange - var command = AnnouncementTestData.CreateValidRevokeCommand() with { AnnouncementId = 0 }; - - // Act - var result = _validator.TestValidate(command); - - // Assert - result.ShouldHaveValidationErrorFor(x => x.AnnouncementId); - } - - [Fact] - public void GivenNullRowVersion_WhenValidate_ThenShouldHaveError() - { - // Arrange - var command = AnnouncementTestData.CreateValidRevokeCommand() with { RowVersion = null! }; - - // Act - var result = _validator.TestValidate(command); - - // Assert - result.ShouldHaveValidationErrorFor(x => x.RowVersion); - } - - [Fact] - public void GivenEmptyRowVersion_WhenValidate_ThenShouldHaveError() - { - // Arrange - var command = AnnouncementTestData.CreateValidRevokeCommand() with { RowVersion = Array.Empty() }; - - // Act - var result = _validator.TestValidate(command); - - // Assert - result.ShouldHaveValidationErrorFor(x => x.RowVersion); - } - - [Fact] - public void GivenValidCommand_WhenValidate_ThenShouldNotHaveErrors() - { - // Arrange - var command = AnnouncementTestData.CreateValidRevokeCommand(); - - // Act - var result = _validator.TestValidate(command); - - // Assert - result.ShouldNotHaveAnyValidationErrors(); - } -} diff --git a/tests/TakeoutSaaS.Application.Tests/App/Tenants/Validators/UpdateAnnouncementCommandValidatorTests.cs b/tests/TakeoutSaaS.Application.Tests/App/Tenants/Validators/UpdateAnnouncementCommandValidatorTests.cs deleted file mode 100644 index bed8efe..0000000 --- a/tests/TakeoutSaaS.Application.Tests/App/Tenants/Validators/UpdateAnnouncementCommandValidatorTests.cs +++ /dev/null @@ -1,88 +0,0 @@ -using FluentValidation.TestHelper; -using TakeoutSaaS.Application.App.Tenants.Validators; -using TakeoutSaaS.Application.Tests.TestUtilities; - -namespace TakeoutSaaS.Application.Tests.App.Tenants.Validators; - -public sealed class UpdateAnnouncementCommandValidatorTests -{ - private readonly UpdateAnnouncementCommandValidator _validator = new(); - - [Fact] - public void GivenEmptyTitle_WhenValidate_ThenShouldHaveError() - { - // Arrange - var command = AnnouncementTestData.CreateValidUpdateCommand() with { Title = "" }; - - // Act - var result = _validator.TestValidate(command); - - // Assert - result.ShouldHaveValidationErrorFor(x => x.Title); - } - - [Fact] - public void GivenTitleTooLong_WhenValidate_ThenShouldHaveError() - { - // Arrange - var command = AnnouncementTestData.CreateValidUpdateCommand() with { Title = new string('A', 129) }; - - // Act - var result = _validator.TestValidate(command); - - // Assert - result.ShouldHaveValidationErrorFor(x => x.Title); - } - - [Fact] - public void GivenEmptyContent_WhenValidate_ThenShouldHaveError() - { - // Arrange - var command = AnnouncementTestData.CreateValidUpdateCommand() with { Content = "" }; - - // Act - var result = _validator.TestValidate(command); - - // Assert - result.ShouldHaveValidationErrorFor(x => x.Content); - } - - [Fact] - public void GivenNullRowVersion_WhenValidate_ThenShouldHaveError() - { - // Arrange - var command = AnnouncementTestData.CreateValidUpdateCommand() with { RowVersion = null! }; - - // Act - var result = _validator.TestValidate(command); - - // Assert - result.ShouldHaveValidationErrorFor(x => x.RowVersion); - } - - [Fact] - public void GivenEmptyRowVersion_WhenValidate_ThenShouldHaveError() - { - // Arrange - var command = AnnouncementTestData.CreateValidUpdateCommand() with { RowVersion = Array.Empty() }; - - // Act - var result = _validator.TestValidate(command); - - // Assert - result.ShouldHaveValidationErrorFor(x => x.RowVersion); - } - - [Fact] - public void GivenValidCommand_WhenValidate_ThenShouldNotHaveErrors() - { - // Arrange - var command = AnnouncementTestData.CreateValidUpdateCommand(); - - // Act - var result = _validator.TestValidate(command); - - // Assert - result.ShouldNotHaveAnyValidationErrors(); - } -} diff --git a/tests/TakeoutSaaS.Application.Tests/TakeoutSaaS.Application.Tests.csproj b/tests/TakeoutSaaS.Application.Tests/TakeoutSaaS.Application.Tests.csproj deleted file mode 100644 index 0e333a1..0000000 --- a/tests/TakeoutSaaS.Application.Tests/TakeoutSaaS.Application.Tests.csproj +++ /dev/null @@ -1,35 +0,0 @@ - - - - net10.0 - enable - enable - false - - - - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - - - - - - - diff --git a/tests/TakeoutSaaS.Application.Tests/TestUtilities/AnnouncementTestData.cs b/tests/TakeoutSaaS.Application.Tests/TestUtilities/AnnouncementTestData.cs deleted file mode 100644 index 3544770..0000000 --- a/tests/TakeoutSaaS.Application.Tests/TestUtilities/AnnouncementTestData.cs +++ /dev/null @@ -1,72 +0,0 @@ -using TakeoutSaaS.Application.App.Tenants.Commands; -using TakeoutSaaS.Domain.Tenants.Entities; -using TakeoutSaaS.Domain.Tenants.Enums; - -namespace TakeoutSaaS.Application.Tests.TestUtilities; - -public static class AnnouncementTestData -{ - public static CreateTenantAnnouncementCommand CreateValidCreateCommand() - => new() - { - TenantId = 100, - Title = "公告标题", - Content = "公告内容", - AnnouncementType = TenantAnnouncementType.System, - Priority = 1, - EffectiveFrom = DateTime.UtcNow.AddHours(-1), - EffectiveTo = DateTime.UtcNow.AddHours(2), - PublisherScope = PublisherScope.Tenant, - TargetType = "TENANT_ALL", - TargetParameters = null - }; - - public static UpdateTenantAnnouncementCommand CreateValidUpdateCommand() - => new() - { - TenantId = 100, - AnnouncementId = 9001, - Title = "更新公告", - Content = "更新内容", - TargetType = "TENANT_ALL", - TargetParameters = null, - RowVersion = new byte[] { 1, 2, 3 } - }; - - public static PublishAnnouncementCommand CreateValidPublishCommand() - => new() - { - AnnouncementId = 9001, - RowVersion = new byte[] { 1, 2, 3 } - }; - - public static RevokeAnnouncementCommand CreateValidRevokeCommand() - => new() - { - AnnouncementId = 9001, - RowVersion = new byte[] { 1, 2, 3 } - }; - - public static TenantAnnouncement CreateAnnouncement( - long id, - long tenantId, - int priority, - DateTime effectiveFrom, - AnnouncementStatus status = AnnouncementStatus.Draft) - => new() - { - Id = id, - TenantId = tenantId, - Title = $"公告-{id}", - Content = "内容", - AnnouncementType = TenantAnnouncementType.System, - Priority = priority, - EffectiveFrom = effectiveFrom, - EffectiveTo = null, - PublisherScope = PublisherScope.Tenant, - Status = status, - TargetType = string.Empty, - TargetParameters = null, - RowVersion = new byte[] { 1, 1, 1 } - }; -} diff --git a/tests/TakeoutSaaS.Integration.Tests/App/Dictionary/DictionaryApiTests.cs b/tests/TakeoutSaaS.Integration.Tests/App/Dictionary/DictionaryApiTests.cs deleted file mode 100644 index 96c6d80..0000000 --- a/tests/TakeoutSaaS.Integration.Tests/App/Dictionary/DictionaryApiTests.cs +++ /dev/null @@ -1,606 +0,0 @@ -using System.Text; -using System.Text.Json; -using FluentAssertions; -using Microsoft.AspNetCore.Http; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging.Abstractions; -using TakeoutSaaS.Application.Dictionary.Contracts; -using TakeoutSaaS.Application.Dictionary.Services; -using TakeoutSaaS.Domain.Dictionary.Entities; -using TakeoutSaaS.Domain.Dictionary.Enums; -using TakeoutSaaS.Domain.Dictionary.ValueObjects; -using TakeoutSaaS.Infrastructure.Dictionary.ImportExport; -using TakeoutSaaS.Infrastructure.Dictionary.Repositories; -using TakeoutSaaS.Integration.Tests.Fixtures; -using TakeoutSaaS.Shared.Abstractions.Constants; -using TakeoutSaaS.Shared.Abstractions.Exceptions; - -namespace TakeoutSaaS.Integration.Tests.App.Dictionary; - -public sealed class DictionaryApiTests -{ - [Fact] - public async Task Test_CreateDictionaryGroup_ReturnsCreated() - { - using var database = new DictionarySqliteTestDatabase(); - using var context = database.CreateContext(tenantId: 0, userId: 11); - var tenantProvider = new TestTenantProvider(0); - var cache = new TestDictionaryHybridCache(); - - var service = BuildCommandService(context, tenantProvider, cache); - var result = await service.CreateGroupAsync(new CreateDictionaryGroupRequest - { - Code = "ORDER_STATUS", - Name = "Order Status", - Scope = DictionaryScope.System, - AllowOverride = true, - Description = "Order lifecycle" - }); - - result.Id.Should().NotBe(0); - result.Code.Should().Be("order_status"); - result.Scope.Should().Be(DictionaryScope.System); - - using var verifyContext = database.CreateContext(tenantId: 0); - var stored = await verifyContext.DictionaryGroups - .IgnoreQueryFilters() - .FirstOrDefaultAsync(group => group.Id == result.Id); - - stored.Should().NotBeNull(); - stored!.TenantId.Should().Be(0); - stored.Code.Value.Should().Be("order_status"); - } - - [Fact] - public async Task Test_GetDictionaryGroups_ReturnsPaged() - { - using var database = new DictionarySqliteTestDatabase(); - using var context = database.CreateContext(tenantId: 0, userId: 11); - - context.DictionaryGroups.AddRange( - CreateSystemGroup(101, "order_status"), - CreateSystemGroup(102, "payment_method")); - await context.SaveChangesAsync(); - context.ChangeTracker.Clear(); - - var tenantProvider = new TestTenantProvider(0); - var cache = new TestDictionaryHybridCache(); - var service = BuildQueryService(context, tenantProvider, cache); - - var page = await service.GetGroupsAsync(new DictionaryGroupQuery - { - Scope = DictionaryScope.System, - Page = 1, - PageSize = 1 - }); - - page.TotalCount.Should().Be(2); - page.Items.Should().HaveCount(1); - } - - [Fact] - public async Task Test_UpdateDictionaryGroup_WithValidRowVersion_ReturnsOk() - { - using var database = new DictionarySqliteTestDatabase(); - using var context = database.CreateContext(tenantId: 0, userId: 11); - var tenantProvider = new TestTenantProvider(0); - var cache = new TestDictionaryHybridCache(); - var service = BuildCommandService(context, tenantProvider, cache); - - var created = await service.CreateGroupAsync(new CreateDictionaryGroupRequest - { - Code = "PAYMENT_METHOD", - Name = "Payment Method", - Scope = DictionaryScope.System, - AllowOverride = true - }); - - var updated = await service.UpdateGroupAsync(created.Id, new UpdateDictionaryGroupRequest - { - Name = "Payment Method Updated", - Description = "Updated", - AllowOverride = false, - IsEnabled = false, - RowVersion = created.RowVersion - }); - - updated.Name.Should().Be("Payment Method Updated"); - updated.AllowOverride.Should().BeFalse(); - updated.IsEnabled.Should().BeFalse(); - } - - [Fact] - public async Task Test_UpdateDictionaryGroup_WithStaleRowVersion_ReturnsConflict() - { - using var database = new DictionarySqliteTestDatabase(); - using var context = database.CreateContext(tenantId: 0, userId: 11); - var tenantProvider = new TestTenantProvider(0); - var cache = new TestDictionaryHybridCache(); - var service = BuildCommandService(context, tenantProvider, cache); - - var created = await service.CreateGroupAsync(new CreateDictionaryGroupRequest - { - Code = "SHIPPING_METHOD", - Name = "Shipping Method", - Scope = DictionaryScope.System, - AllowOverride = true - }); - - var request = new UpdateDictionaryGroupRequest - { - Name = "Shipping Method Updated", - AllowOverride = true, - IsEnabled = true, - RowVersion = new byte[] { 9 } - }; - - Func act = async () => await service.UpdateGroupAsync(created.Id, request); - - var exception = await act.Should().ThrowAsync(); - exception.Which.ErrorCode.Should().Be(ErrorCodes.Conflict); - } - - [Fact] - public async Task Test_DeleteDictionaryGroup_SoftDeletesGroup() - { - using var database = new DictionarySqliteTestDatabase(); - using var context = database.CreateContext(tenantId: 0, userId: 11); - - var group = CreateSystemGroup(201, "user_role"); - var item = CreateSystemItem(210, group.Id, "ADMIN", 10); - context.DictionaryGroups.Add(group); - context.DictionaryItems.Add(item); - await context.SaveChangesAsync(); - context.ChangeTracker.Clear(); - - var tenantProvider = new TestTenantProvider(0); - var cache = new TestDictionaryHybridCache(); - var service = BuildCommandService(context, tenantProvider, cache); - - var result = await service.DeleteGroupAsync(group.Id); - result.Should().BeTrue(); - - using var verifyContext = database.CreateContext(tenantId: 0); - var deletedGroup = await verifyContext.DictionaryGroups - .IgnoreQueryFilters() - .FirstAsync(x => x.Id == group.Id); - deletedGroup.DeletedAt.Should().NotBeNull(); - - var deletedItem = await verifyContext.DictionaryItems - .IgnoreQueryFilters() - .FirstAsync(x => x.Id == item.Id); - deletedItem.DeletedAt.Should().NotBeNull(); - } - - [Fact] - public async Task Test_EnableOverride_CreatesOverrideConfig() - { - using var database = new DictionarySqliteTestDatabase(); - - using (var systemContext = database.CreateContext(tenantId: 0, userId: 11)) - { - systemContext.DictionaryGroups.Add(CreateSystemGroup(301, "order_status", allowOverride: true)); - await systemContext.SaveChangesAsync(); - } - - using var tenantContext = database.CreateContext(tenantId: 100, userId: 21); - var cache = new TestDictionaryHybridCache(); - var service = BuildOverrideService(tenantContext, cache); - - var result = await service.EnableOverrideAsync(100, "ORDER_STATUS"); - - result.OverrideEnabled.Should().BeTrue(); - result.SystemDictionaryGroupCode.Should().Be("order_status"); - - var stored = await tenantContext.TenantDictionaryOverrides - .IgnoreQueryFilters() - .FirstOrDefaultAsync(x => x.TenantId == 100 && x.SystemDictionaryGroupId == 301); - stored.Should().NotBeNull(); - stored!.OverrideEnabled.Should().BeTrue(); - } - - [Fact] - public async Task Test_DisableOverride_ClearsCustomization() - { - using var database = new DictionarySqliteTestDatabase(); - - using (var systemContext = database.CreateContext(tenantId: 0, userId: 11)) - { - systemContext.DictionaryGroups.Add(CreateSystemGroup(401, "payment_method", allowOverride: true)); - await systemContext.SaveChangesAsync(); - } - - using var tenantContext = database.CreateContext(tenantId: 100, userId: 21); - var cache = new TestDictionaryHybridCache(); - var service = BuildOverrideService(tenantContext, cache); - - await service.EnableOverrideAsync(100, "payment_method"); - var disabled = await service.DisableOverrideAsync(100, "payment_method"); - - disabled.Should().BeTrue(); - - var stored = await tenantContext.TenantDictionaryOverrides - .IgnoreQueryFilters() - .FirstAsync(x => x.TenantId == 100 && x.SystemDictionaryGroupId == 401); - stored.OverrideEnabled.Should().BeFalse(); - } - - [Fact] - public async Task Test_UpdateHiddenItems_FiltersSystemItems() - { - using var database = new DictionarySqliteTestDatabase(); - - using (var systemContext = database.CreateContext(tenantId: 0, userId: 11)) - { - var group = CreateSystemGroup(501, "shipping_method", allowOverride: true); - systemContext.DictionaryGroups.Add(group); - systemContext.DictionaryItems.AddRange( - CreateSystemItem(510, group.Id, "PLATFORM", 10), - CreateSystemItem(511, group.Id, "MERCHANT", 20)); - await systemContext.SaveChangesAsync(); - } - - using var tenantContext = database.CreateContext(tenantId: 200, userId: 22); - var cache = new TestDictionaryHybridCache(); - var overrideService = BuildOverrideService(tenantContext, cache); - var mergeService = BuildMergeService(tenantContext); - - await overrideService.UpdateHiddenItemsAsync(200, "shipping_method", new[] { 511L }); - var merged = await mergeService.MergeItemsAsync(200, 501); - - merged.Should().Contain(item => item.Id == 510); - merged.Should().NotContain(item => item.Id == 511); - } - - [Fact] - public async Task Test_GetMergedDictionary_ReturnsMergedResult() - { - using var database = new DictionarySqliteTestDatabase(); - - using (var systemContext = database.CreateContext(tenantId: 0, userId: 11)) - { - var group = CreateSystemGroup(601, "order_status", allowOverride: true); - systemContext.DictionaryGroups.Add(group); - systemContext.DictionaryItems.AddRange( - CreateSystemItem(610, group.Id, "PENDING", 10), - CreateSystemItem(611, group.Id, "ACCEPTED", 20)); - await systemContext.SaveChangesAsync(); - } - - using (var tenantContext = database.CreateContext(tenantId: 300, userId: 33)) - { - var tenantGroup = CreateTenantGroup(701, 300, "order_status"); - tenantContext.DictionaryGroups.Add(tenantGroup); - tenantContext.DictionaryItems.Add(CreateTenantItem(720, tenantGroup.Id, "CUSTOM", 15)); - await tenantContext.SaveChangesAsync(); - } - - using var queryContext = database.CreateContext(tenantId: 300, userId: 33); - var cache = new TestDictionaryHybridCache(); - var overrideService = BuildOverrideService(queryContext, cache); - await overrideService.EnableOverrideAsync(300, "order_status"); - await overrideService.UpdateHiddenItemsAsync(300, "order_status", new[] { 611L }); - await overrideService.UpdateCustomSortOrderAsync(300, "order_status", new Dictionary - { - [720L] = 1, - [610L] = 2 - }); - - var queryService = BuildQueryService(queryContext, new TestTenantProvider(300), cache); - var merged = await queryService.GetMergedDictionaryAsync("order_status"); - - merged.Should().Contain(item => item.Id == 610); - merged.Should().Contain(item => item.Id == 720 && item.Source == "tenant"); - merged.Should().NotContain(item => item.Id == 611); - merged.First().Id.Should().Be(720); - } - - [Fact] - public async Task Test_ExportCsv_GeneratesValidFile() - { - using var database = new DictionarySqliteTestDatabase(); - - using (var systemContext = database.CreateContext(tenantId: 0, userId: 11)) - { - var group = CreateSystemGroup(801, "payment_method", allowOverride: true); - systemContext.DictionaryGroups.Add(group); - systemContext.DictionaryItems.Add(CreateSystemItem(810, group.Id, "ALIPAY", 10)); - await systemContext.SaveChangesAsync(); - } - - using var exportContext = database.CreateContext(tenantId: 0, userId: 11); - var cache = new TestDictionaryHybridCache(); - var service = BuildImportExportService(exportContext, new TestTenantProvider(0), new TestCurrentUserAccessor(11), cache); - - await using var stream = new MemoryStream(); - await service.ExportToCsvAsync(801, stream); - var csv = Encoding.UTF8.GetString(stream.ToArray()); - - csv.Should().Contain("code,key,value,sortOrder,isEnabled,description,source"); - csv.Should().Contain("payment_method"); - csv.Should().Contain("ALIPAY"); - } - - [Fact] - public async Task Test_ImportCsv_WithSkipMode_SkipsDuplicates() - { - using var database = new DictionarySqliteTestDatabase(); - - using (var systemContext = database.CreateContext(tenantId: 0, userId: 11)) - { - var group = CreateSystemGroup(901, "order_status", allowOverride: true); - systemContext.DictionaryGroups.Add(group); - systemContext.DictionaryItems.Add(CreateSystemItem(910, group.Id, "PENDING", 10)); - await systemContext.SaveChangesAsync(); - } - - using var importContext = database.CreateContext(tenantId: 0, userId: 11); - var cache = new TestDictionaryHybridCache(); - var service = BuildImportExportService(importContext, new TestTenantProvider(0), new TestCurrentUserAccessor(11), cache); - - var csv = BuildCsv( - new[] { "code", "key", "value", "sortOrder", "isEnabled", "description", "source" }, - new[] - { - new[] { "order_status", "PENDING", BuildValueJson("待接单", "Pending"), "10", "true", "重复项", "system" }, - new[] { "order_status", "COMPLETED", BuildValueJson("已完成", "Completed"), "20", "true", "新增项", "system" } - }); - - var result = await service.ImportFromCsvAsync(new DictionaryImportRequest - { - GroupId = 901, - FileName = "import.csv", - FileSize = Encoding.UTF8.GetByteCount(csv), - ConflictMode = ConflictResolutionMode.Skip, - FileStream = new MemoryStream(Encoding.UTF8.GetBytes(csv)) - }); - - result.SkipCount.Should().Be(1); - result.SuccessCount.Should().Be(1); - } - - [Fact] - public async Task Test_ImportCsv_WithOverwriteMode_UpdatesExisting() - { - using var database = new DictionarySqliteTestDatabase(); - - using (var systemContext = database.CreateContext(tenantId: 0, userId: 11)) - { - var group = CreateSystemGroup(1001, "payment_method", allowOverride: true); - systemContext.DictionaryGroups.Add(group); - systemContext.DictionaryItems.Add(CreateSystemItem(1010, group.Id, "ALIPAY", 10)); - await systemContext.SaveChangesAsync(); - } - - using var importContext = database.CreateContext(tenantId: 0, userId: 11); - var cache = new TestDictionaryHybridCache(); - var service = BuildImportExportService(importContext, new TestTenantProvider(0), new TestCurrentUserAccessor(11), cache); - - var csv = BuildCsv( - new[] { "code", "key", "value", "sortOrder", "isEnabled", "description", "source" }, - new[] - { - new[] { "payment_method", "ALIPAY", BuildValueJson("支付宝新版", "Alipay New"), "15", "true", "覆盖", "system" } - }); - - await service.ImportFromCsvAsync(new DictionaryImportRequest - { - GroupId = 1001, - FileName = "import.csv", - FileSize = Encoding.UTF8.GetByteCount(csv), - ConflictMode = ConflictResolutionMode.Overwrite, - FileStream = new MemoryStream(Encoding.UTF8.GetBytes(csv)) - }); - - var updated = await importContext.DictionaryItems - .IgnoreQueryFilters() - .FirstAsync(item => item.GroupId == 1001 && item.Key == "ALIPAY"); - - updated.Value.Should().Contain("支付宝新版"); - } - - [Fact] - public async Task Test_ImportCsv_WithInvalidData_ReturnsErrors() - { - using var database = new DictionarySqliteTestDatabase(); - - using (var systemContext = database.CreateContext(tenantId: 0, userId: 11)) - { - systemContext.DictionaryGroups.Add(CreateSystemGroup(1101, "user_role", allowOverride: false)); - await systemContext.SaveChangesAsync(); - } - - using var importContext = database.CreateContext(tenantId: 0, userId: 11); - var cache = new TestDictionaryHybridCache(); - var service = BuildImportExportService(importContext, new TestTenantProvider(0), new TestCurrentUserAccessor(11), cache); - - var csv = BuildCsv( - new[] { "code", "key", "value", "sortOrder", "isEnabled", "description", "source" }, - new[] - { - new[] { "user_role", "ADMIN", "invalid-json", "10", "true", "bad", "system" } - }); - - var result = await service.ImportFromCsvAsync(new DictionaryImportRequest - { - GroupId = 1101, - FileName = "import.csv", - FileSize = Encoding.UTF8.GetByteCount(csv), - ConflictMode = ConflictResolutionMode.Skip, - FileStream = new MemoryStream(Encoding.UTF8.GetBytes(csv)) - }); - - result.ErrorCount.Should().Be(1); - result.SuccessCount.Should().Be(0); - } - - private static DictionaryCommandService BuildCommandService( - TakeoutSaaS.Infrastructure.Dictionary.Persistence.DictionaryDbContext context, - TestTenantProvider tenantProvider, - TestDictionaryHybridCache cache) - { - return new DictionaryCommandService( - new DictionaryGroupRepository(context), - new DictionaryItemRepository(context), - cache, - tenantProvider, - NullLogger.Instance); - } - - private static DictionaryQueryService BuildQueryService( - TakeoutSaaS.Infrastructure.Dictionary.Persistence.DictionaryDbContext context, - TestTenantProvider tenantProvider, - TestDictionaryHybridCache cache) - { - var groupRepository = new DictionaryGroupRepository(context); - var itemRepository = new DictionaryItemRepository(context); - var overrideRepository = new TenantDictionaryOverrideRepository(context); - var mergeService = new DictionaryMergeService(groupRepository, itemRepository, overrideRepository); - - return new DictionaryQueryService(groupRepository, itemRepository, mergeService, cache, tenantProvider); - } - - private static DictionaryOverrideService BuildOverrideService( - TakeoutSaaS.Infrastructure.Dictionary.Persistence.DictionaryDbContext context, - TestDictionaryHybridCache cache) - { - return new DictionaryOverrideService( - new DictionaryGroupRepository(context), - new DictionaryItemRepository(context), - new TenantDictionaryOverrideRepository(context), - cache); - } - - private static DictionaryMergeService BuildMergeService( - TakeoutSaaS.Infrastructure.Dictionary.Persistence.DictionaryDbContext context) - { - var groupRepository = new DictionaryGroupRepository(context); - var itemRepository = new DictionaryItemRepository(context); - var overrideRepository = new TenantDictionaryOverrideRepository(context); - return new DictionaryMergeService(groupRepository, itemRepository, overrideRepository); - } - - private static DictionaryImportExportService BuildImportExportService( - TakeoutSaaS.Infrastructure.Dictionary.Persistence.DictionaryDbContext context, - TestTenantProvider tenantProvider, - TestCurrentUserAccessor currentUser, - TestDictionaryHybridCache cache) - { - return new DictionaryImportExportService( - new CsvDictionaryParser(), - new JsonDictionaryParser(), - new DictionaryGroupRepository(context), - new DictionaryItemRepository(context), - new DictionaryImportLogRepository(context), - cache, - tenantProvider, - currentUser, - NullLogger.Instance); - } - - private static DictionaryGroup CreateSystemGroup(long id, string code, bool allowOverride = true) - { - return new DictionaryGroup - { - Id = id, - TenantId = 0, - Code = new DictionaryCode(code), - Name = code, - Scope = DictionaryScope.System, - AllowOverride = allowOverride, - Description = "Test group", - IsEnabled = true, - RowVersion = new byte[] { 1 } - }; - } - - private static DictionaryGroup CreateTenantGroup(long id, long tenantId, string code) - { - return new DictionaryGroup - { - Id = id, - TenantId = tenantId, - Code = new DictionaryCode(code), - Name = code, - Scope = DictionaryScope.Business, - AllowOverride = false, - Description = "Tenant group", - IsEnabled = true, - RowVersion = new byte[] { 1 } - }; - } - - private static DictionaryItem CreateSystemItem(long id, long groupId, string key, int sortOrder) - { - return new DictionaryItem - { - Id = id, - TenantId = 0, - GroupId = groupId, - Key = key, - Value = BuildValueJson("测试", "Test"), - IsDefault = false, - IsEnabled = true, - SortOrder = sortOrder, - Description = "System item", - RowVersion = new byte[] { 1 } - }; - } - - private static DictionaryItem CreateTenantItem(long id, long groupId, string key, int sortOrder) - { - return new DictionaryItem - { - Id = id, - TenantId = 300, - GroupId = groupId, - Key = key, - Value = BuildValueJson("租户值", "Tenant Value"), - IsDefault = false, - IsEnabled = true, - SortOrder = sortOrder, - Description = "Tenant item", - RowVersion = new byte[] { 1 } - }; - } - - private static string BuildValueJson(string zh, string en) - { - var value = new I18nValue(new Dictionary - { - ["zh-CN"] = zh, - ["en"] = en - }); - return value.ToJson(); - } - - private static string BuildCsv(string[] headers, IEnumerable rows) - { - var builder = new StringBuilder(); - builder.AppendLine(string.Join(",", headers)); - foreach (var row in rows) - { - builder.AppendLine(string.Join(",", row.Select(EscapeCsvField))); - } - - return builder.ToString(); - } - - private static string EscapeCsvField(string value) - { - if (value.Contains('"', StringComparison.Ordinal)) - { - value = value.Replace("\"", "\"\""); - } - - if (value.Contains(',', StringComparison.Ordinal) || - value.Contains('\n', StringComparison.Ordinal) || - value.Contains('\r', StringComparison.Ordinal) || - value.Contains('"', StringComparison.Ordinal)) - { - return $"\"{value}\""; - } - - return value; - } -} diff --git a/tests/TakeoutSaaS.Integration.Tests/App/Tenants/AnnouncementRegressionTests.cs b/tests/TakeoutSaaS.Integration.Tests/App/Tenants/AnnouncementRegressionTests.cs deleted file mode 100644 index af0e2c7..0000000 --- a/tests/TakeoutSaaS.Integration.Tests/App/Tenants/AnnouncementRegressionTests.cs +++ /dev/null @@ -1,99 +0,0 @@ -using FluentAssertions; -using TakeoutSaaS.Application.App.Tenants.Commands; -using TakeoutSaaS.Application.App.Tenants.Handlers; -using TakeoutSaaS.Domain.Tenants.Entities; -using TakeoutSaaS.Domain.Tenants.Enums; -using TakeoutSaaS.Infrastructure.App.Repositories; -using TakeoutSaaS.Integration.Tests.Fixtures; - -namespace TakeoutSaaS.Integration.Tests.App.Tenants; - -public sealed class AnnouncementRegressionTests -{ - [Fact] - public async Task GivenPublishedAnnouncement_WhenSearchByIsActive_ThenReturns() - { - // Arrange - using var database = new SqliteTestDatabase(); - using var context = database.CreateContext(tenantId: 600, userId: 12); - - var legacy = CreateAnnouncement(tenantId: 600, id: 9100); - legacy.Status = AnnouncementStatus.Published; - - context.TenantAnnouncements.Add(legacy); - await context.SaveChangesAsync(); - context.ChangeTracker.Clear(); - - var repository = new EfTenantAnnouncementRepository(context); - - // Act - var results = await repository.SearchAsync( - tenantId: 600, - keyword: null, - status: null, - type: null, - isActive: true, - effectiveFrom: null, - effectiveTo: null, - effectiveAt: null, - cancellationToken: CancellationToken.None); - - // Assert - results.Should().ContainSingle(x => x.Id == legacy.Id); - } - - [Fact] - public async Task GivenDraftAnnouncement_WhenUpdate_ThenUpdatesFieldsAndKeepsInactive() - { - // Arrange - using var database = new SqliteTestDatabase(); - using var context = database.CreateContext(tenantId: 700, userId: 12); - - var announcement = CreateAnnouncement(tenantId: 700, id: 9101); - announcement.Status = AnnouncementStatus.Draft; - context.TenantAnnouncements.Add(announcement); - await context.SaveChangesAsync(); - context.ChangeTracker.Clear(); - - var repository = new EfTenantAnnouncementRepository(context); - var tenantProvider = new TestTenantProvider(700); - var handler = new UpdateTenantAnnouncementCommandHandler(repository, tenantProvider); - - var command = new UpdateTenantAnnouncementCommand - { - TenantId = 700, - AnnouncementId = announcement.Id, - Title = "更新后的标题", - Content = "更新后的内容", - TargetType = "TENANT_ALL", - RowVersion = announcement.RowVersion - }; - - // Act - var result = await handler.Handle(command, CancellationToken.None); - - // Assert - result.Should().NotBeNull(); - result!.Title.Should().Be("更新后的标题"); - result.Content.Should().Be("更新后的内容"); - result.IsActive.Should().BeFalse(); - } - - private static TenantAnnouncement CreateAnnouncement(long tenantId, long id) - => new() - { - Id = id, - TenantId = tenantId, - Title = "旧公告", - Content = "内容", - AnnouncementType = TenantAnnouncementType.System, - Priority = 1, - EffectiveFrom = DateTime.UtcNow.AddMinutes(-5), - EffectiveTo = null, - PublisherScope = PublisherScope.Tenant, - Status = AnnouncementStatus.Draft, - TargetType = "TENANT_ALL", - TargetParameters = null, - RowVersion = new byte[] { 1 } - }; -} diff --git a/tests/TakeoutSaaS.Integration.Tests/App/Tenants/AnnouncementWorkflowTests.cs b/tests/TakeoutSaaS.Integration.Tests/App/Tenants/AnnouncementWorkflowTests.cs deleted file mode 100644 index f545a28..0000000 --- a/tests/TakeoutSaaS.Integration.Tests/App/Tenants/AnnouncementWorkflowTests.cs +++ /dev/null @@ -1,217 +0,0 @@ -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 = "TENANT_ALL", - 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 = "TENANT_ALL", - 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 = "TENANT_ALL", - TargetParameters = null, - RowVersion = new byte[] { 1 } - }; -} diff --git a/tests/TakeoutSaaS.Integration.Tests/App/Tenants/TenantAnnouncementRepositoryScopeTests.cs b/tests/TakeoutSaaS.Integration.Tests/App/Tenants/TenantAnnouncementRepositoryScopeTests.cs deleted file mode 100644 index a4f439d..0000000 --- a/tests/TakeoutSaaS.Integration.Tests/App/Tenants/TenantAnnouncementRepositoryScopeTests.cs +++ /dev/null @@ -1,59 +0,0 @@ -using FluentAssertions; -using TakeoutSaaS.Domain.Tenants.Entities; -using TakeoutSaaS.Domain.Tenants.Enums; -using TakeoutSaaS.Infrastructure.App.Repositories; -using TakeoutSaaS.Integration.Tests.Fixtures; - -namespace TakeoutSaaS.Integration.Tests.App.Tenants; - -public sealed class TenantAnnouncementRepositoryScopeTests -{ - [Fact] - public async Task GivenDifferentTenantsAnnouncements_WhenSearchAsync_ThenReturnsOnlyCurrentTenant() - { - // Arrange - using var database = new SqliteTestDatabase(); - using var context = database.CreateContext(tenantId: 800); - - var tenantAnnouncement = CreateAnnouncement(tenantId: 800, id: 9200); - var otherTenantAnnouncement = CreateAnnouncement(tenantId: 801, id: 9201); - - context.TenantAnnouncements.AddRange(tenantAnnouncement, otherTenantAnnouncement); - await context.SaveChangesAsync(); - context.ChangeTracker.Clear(); - - var repository = new EfTenantAnnouncementRepository(context); - - // Act - var results = await repository.SearchAsync( - tenantId: 800, - keyword: null, - status: null, - type: null, - isActive: null, - effectiveFrom: null, - effectiveTo: null, - effectiveAt: null, - cancellationToken: CancellationToken.None); - - // Assert - results.Select(x => x.Id).Should().Contain(tenantAnnouncement.Id); - results.Select(x => x.Id).Should().NotContain(otherTenantAnnouncement.Id); - } - - private static TenantAnnouncement CreateAnnouncement(long tenantId, long id) - => new() - { - Id = id, - TenantId = tenantId, - Title = "公告", - Content = "内容", - AnnouncementType = TenantAnnouncementType.System, - Priority = 1, - EffectiveFrom = DateTime.UtcNow.AddMinutes(-5), - PublisherScope = PublisherScope.Tenant, - Status = AnnouncementStatus.Draft, - TargetType = "TENANT_ALL", - RowVersion = new byte[] { 1 } - }; -} diff --git a/tests/TakeoutSaaS.Integration.Tests/Authorization/PermissionAuthorizationHandlerTests.cs b/tests/TakeoutSaaS.Integration.Tests/Authorization/PermissionAuthorizationHandlerTests.cs deleted file mode 100644 index d530f34..0000000 --- a/tests/TakeoutSaaS.Integration.Tests/Authorization/PermissionAuthorizationHandlerTests.cs +++ /dev/null @@ -1,80 +0,0 @@ -using System.Security.Claims; -using FluentAssertions; -using Microsoft.AspNetCore.Authentication; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Authorization.Policy; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.DependencyInjection; -using TakeoutSaaS.Module.Authorization.Policies; - -namespace TakeoutSaaS.Integration.Tests.Authorization; - -public sealed class PermissionAuthorizationHandlerTests -{ - [Theory] - [InlineData("platform-announcement:create")] - [InlineData("platform-announcement:publish")] - [InlineData("platform-announcement:revoke")] - [InlineData("tenant-announcement:publish")] - [InlineData("tenant-announcement:revoke")] - public async Task GivenUserWithPermission_WhenAuthorize_ThenSucceeds(string permission) - { - // Arrange - var requirement = new PermissionRequirement(new[] { permission }); - var identity = new ClaimsIdentity(new[] - { - new Claim(PermissionAuthorizationHandler.PermissionClaimType, permission) - }, "Test"); - var user = new ClaimsPrincipal(identity); - var context = new AuthorizationHandlerContext(new[] { requirement }, user, null); - var handler = new PermissionAuthorizationHandler(); - - // Act - await handler.HandleAsync(context); - - // Assert - context.HasSucceeded.Should().BeTrue(); - } - - [Fact] - public async Task GivenUserWithoutPermission_WhenAuthorize_ThenFails() - { - // Arrange - var requirement = new PermissionRequirement(new[] { "platform-announcement:create" }); - var user = new ClaimsPrincipal(new ClaimsIdentity(authenticationType: "Test")); - var context = new AuthorizationHandlerContext(new[] { requirement }, user, null); - var handler = new PermissionAuthorizationHandler(); - - // Act - await handler.HandleAsync(context); - - // Assert - context.HasSucceeded.Should().BeFalse(); - } - - [Fact] - public async Task GivenAuthenticatedUserWithoutPermission_WhenEvaluatingPolicy_ThenForbidden() - { - // Arrange - var services = new ServiceCollection(); - services.AddAuthorization(); - services.AddSingleton(); - services.AddSingleton(); - var provider = services.BuildServiceProvider(); - - var policyProvider = provider.GetRequiredService(); - var policy = await policyProvider.GetPolicyAsync( - PermissionAuthorizationPolicyProvider.BuildPolicyName(new[] { "platform-announcement:create" })); - - var user = new ClaimsPrincipal(new ClaimsIdentity(authenticationType: "Test")); - var authenticateResult = AuthenticateResult.Success(new AuthenticationTicket(user, "Test")); - var httpContext = new DefaultHttpContext { RequestServices = provider, User = user }; - var evaluator = provider.GetRequiredService(); - - // Act - var result = await evaluator.AuthorizeAsync(policy!, authenticateResult, httpContext, resource: null); - - // Assert - result.Forbidden.Should().BeTrue(); - } -} diff --git a/tests/TakeoutSaaS.Integration.Tests/Fixtures/DictionarySqliteTestDatabase.cs b/tests/TakeoutSaaS.Integration.Tests/Fixtures/DictionarySqliteTestDatabase.cs deleted file mode 100644 index ccc9201..0000000 --- a/tests/TakeoutSaaS.Integration.Tests/Fixtures/DictionarySqliteTestDatabase.cs +++ /dev/null @@ -1,49 +0,0 @@ -using Microsoft.Data.Sqlite; -using Microsoft.EntityFrameworkCore; -using TakeoutSaaS.Infrastructure.Dictionary.Persistence; - -namespace TakeoutSaaS.Integration.Tests.Fixtures; - -public sealed class DictionarySqliteTestDatabase : IDisposable -{ - private readonly SqliteConnection _connection; - private bool _initialized; - - public DictionarySqliteTestDatabase() - { - _connection = new SqliteConnection("Filename=:memory:"); - _connection.Open(); - Options = new DbContextOptionsBuilder() - .UseSqlite(_connection) - .EnableSensitiveDataLogging() - .Options; - } - - public DbContextOptions Options { get; } - - public DictionaryDbContext CreateContext(long tenantId, long userId = 0) - { - EnsureCreated(); - return new DictionaryDbContext( - Options, - new TestTenantProvider(tenantId), - userId == 0 ? null : new TestCurrentUserAccessor(userId)); - } - - public void EnsureCreated() - { - if (_initialized) - { - return; - } - - using var context = new DictionaryDbContext(Options, new TestTenantProvider(1)); - context.Database.EnsureCreated(); - _initialized = true; - } - - public void Dispose() - { - _connection.Dispose(); - } -} diff --git a/tests/TakeoutSaaS.Integration.Tests/Fixtures/SqliteTestDatabase.cs b/tests/TakeoutSaaS.Integration.Tests/Fixtures/SqliteTestDatabase.cs deleted file mode 100644 index c87c19a..0000000 --- a/tests/TakeoutSaaS.Integration.Tests/Fixtures/SqliteTestDatabase.cs +++ /dev/null @@ -1,46 +0,0 @@ -using Microsoft.Data.Sqlite; -using Microsoft.EntityFrameworkCore; -using TakeoutSaaS.Infrastructure.App.Persistence; - -namespace TakeoutSaaS.Integration.Tests.Fixtures; - -public sealed class SqliteTestDatabase : IDisposable -{ - private readonly SqliteConnection _connection; - private bool _initialized; - - public SqliteTestDatabase() - { - _connection = new SqliteConnection("Filename=:memory:"); - _connection.Open(); - Options = new DbContextOptionsBuilder() - .UseSqlite(_connection) - .EnableSensitiveDataLogging() - .Options; - } - - public DbContextOptions Options { get; } - - public TakeoutAppDbContext CreateContext(long tenantId, long userId = 0) - { - EnsureCreated(); - return new TakeoutAppDbContext(Options, new TestTenantProvider(tenantId), new TestCurrentUserAccessor(userId)); - } - - public void EnsureCreated() - { - if (_initialized) - { - return; - } - - using var context = new TakeoutAppDbContext(Options, new TestTenantProvider(1)); - context.Database.EnsureCreated(); - _initialized = true; - } - - public void Dispose() - { - _connection.Dispose(); - } -} diff --git a/tests/TakeoutSaaS.Integration.Tests/Fixtures/TestCurrentUserAccessor.cs b/tests/TakeoutSaaS.Integration.Tests/Fixtures/TestCurrentUserAccessor.cs deleted file mode 100644 index b7ab4d3..0000000 --- a/tests/TakeoutSaaS.Integration.Tests/Fixtures/TestCurrentUserAccessor.cs +++ /dev/null @@ -1,10 +0,0 @@ -using TakeoutSaaS.Shared.Abstractions.Security; - -namespace TakeoutSaaS.Integration.Tests.Fixtures; - -public sealed class TestCurrentUserAccessor(long userId) : ICurrentUserAccessor -{ - public long UserId { get; set; } = userId; - - public bool IsAuthenticated => UserId != 0; -} diff --git a/tests/TakeoutSaaS.Integration.Tests/Fixtures/TestDictionaryHybridCache.cs b/tests/TakeoutSaaS.Integration.Tests/Fixtures/TestDictionaryHybridCache.cs deleted file mode 100644 index 9bd13c4..0000000 --- a/tests/TakeoutSaaS.Integration.Tests/Fixtures/TestDictionaryHybridCache.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System.Collections.Concurrent; -using TakeoutSaaS.Application.Dictionary.Abstractions; -using TakeoutSaaS.Domain.Dictionary.Enums; - -namespace TakeoutSaaS.Integration.Tests.Fixtures; - -public sealed class TestDictionaryHybridCache : IDictionaryHybridCache -{ - private readonly ConcurrentDictionary _cache = new(StringComparer.OrdinalIgnoreCase); - - public async Task GetOrCreateAsync( - string key, - TimeSpan ttl, - Func> factory, - CancellationToken cancellationToken = default) - { - if (_cache.TryGetValue(key, out var cached) && cached is T typed) - { - return typed; - } - - var value = await factory(cancellationToken); - if (value is not null) - { - _cache[key] = value; - } - - return value; - } - - public Task InvalidateAsync( - string prefix, - CacheInvalidationOperation operation = CacheInvalidationOperation.Update, - CancellationToken cancellationToken = default) - { - foreach (var key in _cache.Keys) - { - if (key.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) - { - _cache.TryRemove(key, out _); - } - } - - return Task.CompletedTask; - } -} diff --git a/tests/TakeoutSaaS.Integration.Tests/Fixtures/TestTenantProvider.cs b/tests/TakeoutSaaS.Integration.Tests/Fixtures/TestTenantProvider.cs deleted file mode 100644 index 89c0e4d..0000000 --- a/tests/TakeoutSaaS.Integration.Tests/Fixtures/TestTenantProvider.cs +++ /dev/null @@ -1,10 +0,0 @@ -using TakeoutSaaS.Shared.Abstractions.Tenancy; - -namespace TakeoutSaaS.Integration.Tests.Fixtures; - -public sealed class TestTenantProvider(long tenantId) : ITenantProvider -{ - public long TenantId { get; set; } = tenantId; - - public long GetCurrentTenantId() => TenantId; -} diff --git a/tests/TakeoutSaaS.Integration.Tests/Performance/AnnouncementQueryPerformanceTests.cs b/tests/TakeoutSaaS.Integration.Tests/Performance/AnnouncementQueryPerformanceTests.cs deleted file mode 100644 index 0293fea..0000000 --- a/tests/TakeoutSaaS.Integration.Tests/Performance/AnnouncementQueryPerformanceTests.cs +++ /dev/null @@ -1,77 +0,0 @@ -using System.Diagnostics; -using FluentAssertions; -using TakeoutSaaS.Application.App.Tenants.Handlers; -using TakeoutSaaS.Application.App.Tenants.Queries; -using TakeoutSaaS.Domain.Tenants.Entities; -using TakeoutSaaS.Domain.Tenants.Enums; -using TakeoutSaaS.Infrastructure.App.Repositories; -using TakeoutSaaS.Integration.Tests.Fixtures; - -namespace TakeoutSaaS.Integration.Tests.Performance; - -public sealed class AnnouncementQueryPerformanceTests -{ - [Fact] - public async Task GivenLargeDataset_WhenQueryingAnnouncements_ThenCompletesWithinThreshold() - { - // Arrange - using var database = new SqliteTestDatabase(); - using var context = database.CreateContext(tenantId: 900); - - var announcements = new List(); - for (var i = 0; i < 1000; i++) - { - var tenantId = 900; - var targetType = i % 10 == 0 ? "ROLES" : "TENANT_ALL"; - var targetParameters = i % 10 == 0 ? "{\"roles\":[\"ops\"]}" : null; - - announcements.Add(new TenantAnnouncement - { - Id = 10000 + i, - TenantId = tenantId, - Title = "公告", - Content = "内容", - AnnouncementType = TenantAnnouncementType.System, - Priority = i % 5, - EffectiveFrom = DateTime.UtcNow.AddDays(-1), - PublisherScope = PublisherScope.Tenant, - Status = AnnouncementStatus.Published, - TargetType = targetType, - TargetParameters = targetParameters, - RowVersion = new byte[] { 1 } - }); - } - - context.TenantAnnouncements.AddRange(announcements); - await context.SaveChangesAsync(); - context.ChangeTracker.Clear(); - - var announcementRepository = new EfTenantAnnouncementRepository(context); - var readRepository = new EfTenantAnnouncementReadRepository(context); - var tenantProvider = new TestTenantProvider(900); - var handler = new GetTenantsAnnouncementsQueryHandler( - announcementRepository, - readRepository, - tenantProvider); - - var query = new GetTenantsAnnouncementsQuery - { - Page = 1, - PageSize = 50 - }; - - // Act - var stopwatch = Stopwatch.StartNew(); - var result = await handler.Handle(query, CancellationToken.None); - stopwatch.Stop(); - - // Assert - // 注意:由于性能优化,TotalCount 不再是精确的全局总数, - // 而是基于估算查询限制(page * size * 3)过滤后的结果数 - // 这是性能优化的权衡:牺牲精确性换取性能 - result.Items.Count.Should().Be(50); // 请求的页大小 - result.TotalCount.Should().BeLessThanOrEqualTo(150); // 最多是 estimatedLimit - result.TotalCount.Should().BeGreaterThan(0); // 至少有一些结果 - stopwatch.Elapsed.Should().BeLessThan(TimeSpan.FromSeconds(5)); - } -} diff --git a/tests/TakeoutSaaS.Integration.Tests/TakeoutSaaS.Integration.Tests.csproj b/tests/TakeoutSaaS.Integration.Tests/TakeoutSaaS.Integration.Tests.csproj deleted file mode 100644 index 24ff938..0000000 --- a/tests/TakeoutSaaS.Integration.Tests/TakeoutSaaS.Integration.Tests.csproj +++ /dev/null @@ -1,39 +0,0 @@ - - - - net10.0 - enable - enable - false - - - - - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - - - - - - - - - -