refactor: 删除 tests 目录
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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<ITenantProvider>();
|
||||
tenantProvider.Setup(x => x.GetCurrentTenantId()).Returns(99);
|
||||
|
||||
var announcementRepository = new Mock<ITenantAnnouncementRepository>();
|
||||
announcementRepository
|
||||
.Setup(x => x.FindByIdAsync(99, 500, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync((TenantAnnouncement?)null);
|
||||
|
||||
var readRepository = new Mock<ITenantAnnouncementReadRepository>();
|
||||
|
||||
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<ITenantProvider>();
|
||||
tenantProvider.Setup(x => x.GetCurrentTenantId()).Returns(100);
|
||||
|
||||
var currentUserAccessor = new Mock<ICurrentUserAccessor>();
|
||||
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<ITenantAnnouncementRepository>();
|
||||
announcementRepository
|
||||
.Setup(x => x.FindByIdAsync(100, 1, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(announcement);
|
||||
|
||||
var readRepository = new Mock<ITenantAnnouncementReadRepository>();
|
||||
|
||||
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<long>(),
|
||||
It.IsAny<IEnumerable<long>>(),
|
||||
It.IsAny<long?>(),
|
||||
It.IsAny<CancellationToken>()), Times.Never);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GivenUserReadRecord_WhenHandle_ThenReturnsDtoWithReadState()
|
||||
{
|
||||
// Arrange
|
||||
var tenantProvider = new Mock<ITenantProvider>();
|
||||
tenantProvider.Setup(x => x.GetCurrentTenantId()).Returns(200);
|
||||
|
||||
var currentUserAccessor = new Mock<ICurrentUserAccessor>();
|
||||
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<ITenantAnnouncementRepository>();
|
||||
announcementRepository
|
||||
.Setup(x => x.FindByIdAsync(200, 10, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(announcement);
|
||||
|
||||
var readRepository = new Mock<ITenantAnnouncementReadRepository>();
|
||||
readRepository
|
||||
.Setup(x => x.GetByAnnouncementAsync(200, It.IsAny<IEnumerable<long>>(), 321, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new List<TenantAnnouncementRead>
|
||||
{
|
||||
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));
|
||||
}
|
||||
}
|
||||
@@ -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<ITenantProvider>();
|
||||
tenantProvider.Setup(x => x.GetCurrentTenantId()).Returns(42);
|
||||
|
||||
var currentUserAccessor = new Mock<ICurrentUserAccessor>();
|
||||
currentUserAccessor.SetupGet(x => x.UserId).Returns(0);
|
||||
currentUserAccessor.SetupGet(x => x.IsAuthenticated).Returns(false);
|
||||
|
||||
var announcements = new List<TakeoutSaaS.Domain.Tenants.Entities.TenantAnnouncement>
|
||||
{
|
||||
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<ITenantAnnouncementRepository>();
|
||||
announcementRepository
|
||||
.Setup(x => x.SearchAsync(
|
||||
It.IsAny<long>(),
|
||||
It.IsAny<string?>(),
|
||||
It.IsAny<TakeoutSaaS.Domain.Tenants.Enums.AnnouncementStatus?>(),
|
||||
It.IsAny<TakeoutSaaS.Domain.Tenants.Enums.TenantAnnouncementType?>(),
|
||||
It.IsAny<bool?>(),
|
||||
It.IsAny<DateTime?>(),
|
||||
It.IsAny<DateTime?>(),
|
||||
It.IsAny<DateTime?>(),
|
||||
It.IsAny<bool>(),
|
||||
It.IsAny<int?>(),
|
||||
It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(sortedAnnouncements);
|
||||
|
||||
var announcementReadRepository = new Mock<ITenantAnnouncementReadRepository>();
|
||||
announcementReadRepository
|
||||
.Setup(x => x.GetByAnnouncementAsync(
|
||||
It.IsAny<long>(),
|
||||
It.IsAny<IEnumerable<long>>(),
|
||||
It.IsAny<long?>(),
|
||||
It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(Array.Empty<TakeoutSaaS.Domain.Tenants.Entities.TenantAnnouncementRead>());
|
||||
|
||||
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<CancellationToken>()), 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<ITenantProvider>();
|
||||
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<TakeoutSaaS.Domain.Tenants.Entities.TenantAnnouncement>
|
||||
{
|
||||
announcement1,
|
||||
announcement2
|
||||
};
|
||||
|
||||
var announcementRepository = new Mock<ITenantAnnouncementRepository>();
|
||||
announcementRepository
|
||||
.Setup(x => x.SearchAsync(
|
||||
It.IsAny<long>(),
|
||||
It.IsAny<string?>(),
|
||||
It.IsAny<TakeoutSaaS.Domain.Tenants.Enums.AnnouncementStatus?>(),
|
||||
It.IsAny<TakeoutSaaS.Domain.Tenants.Enums.TenantAnnouncementType?>(),
|
||||
It.IsAny<bool?>(),
|
||||
It.IsAny<DateTime?>(),
|
||||
It.IsAny<DateTime?>(),
|
||||
It.IsAny<DateTime?>(),
|
||||
It.IsAny<bool>(),
|
||||
It.IsAny<int?>(),
|
||||
It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(announcements);
|
||||
|
||||
var announcementReadRepository = new Mock<ITenantAnnouncementReadRepository>();
|
||||
announcementReadRepository
|
||||
.Setup(x => x.GetByAnnouncementAsync(
|
||||
It.IsAny<long>(),
|
||||
It.IsAny<IEnumerable<long>>(),
|
||||
It.IsAny<long?>(),
|
||||
It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(Array.Empty<TakeoutSaaS.Domain.Tenants.Entities.TenantAnnouncementRead>());
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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<ITenantProvider>();
|
||||
tenantProvider.Setup(x => x.GetCurrentTenantId()).Returns(55);
|
||||
|
||||
var currentUserAccessor = new Mock<ICurrentUserAccessor>();
|
||||
currentUserAccessor.SetupGet(x => x.UserId).Returns(0);
|
||||
currentUserAccessor.SetupGet(x => x.IsAuthenticated).Returns(false);
|
||||
|
||||
var announcements = new List<TakeoutSaaS.Domain.Tenants.Entities.TenantAnnouncement>
|
||||
{
|
||||
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<ITenantAnnouncementRepository>();
|
||||
announcementRepository
|
||||
.Setup(x => x.SearchUnreadAsync(
|
||||
It.IsAny<long>(),
|
||||
It.IsAny<long?>(),
|
||||
It.IsAny<AnnouncementStatus?>(),
|
||||
It.IsAny<bool?>(),
|
||||
It.IsAny<DateTime?>(),
|
||||
It.IsAny<CancellationToken>()))
|
||||
.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<DateTime?>(),
|
||||
It.IsAny<CancellationToken>()), 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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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<byte>() };
|
||||
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
@@ -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<byte>() };
|
||||
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
@@ -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<byte>() };
|
||||
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.4" />
|
||||
<PackageReference Include="FluentAssertions" Version="8.8.0" />
|
||||
<PackageReference Include="FluentValidation" Version="12.1.1" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
|
||||
<PackageReference Include="Moq" Version="4.20.72" />
|
||||
<PackageReference Include="xunit" Version="2.9.3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Using Include="Xunit" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\Application\TakeoutSaaS.Application\TakeoutSaaS.Application.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Update="System.IO.Packaging" Version="10.0.1" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -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 }
|
||||
};
|
||||
}
|
||||
@@ -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<Task> act = async () => await service.UpdateGroupAsync(created.Id, request);
|
||||
|
||||
var exception = await act.Should().ThrowAsync<BusinessException>();
|
||||
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<long, int>
|
||||
{
|
||||
[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<DictionaryCommandService>.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<DictionaryImportExportService>.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<string, string>
|
||||
{
|
||||
["zh-CN"] = zh,
|
||||
["en"] = en
|
||||
});
|
||||
return value.ToJson();
|
||||
}
|
||||
|
||||
private static string BuildCsv(string[] headers, IEnumerable<string[]> 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;
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
};
|
||||
}
|
||||
@@ -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<IEventPublisher>();
|
||||
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<IEventPublisher>();
|
||||
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<IEventPublisher>();
|
||||
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<Task> act = async () => await handler.Handle(command, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
var exception = await act.Should().ThrowAsync<BusinessException>();
|
||||
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<Task> act = async () => await handler.Handle(command, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
var exception = await act.Should().ThrowAsync<BusinessException>();
|
||||
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 }
|
||||
};
|
||||
}
|
||||
@@ -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 }
|
||||
};
|
||||
}
|
||||
@@ -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<IAuthorizationPolicyProvider, PermissionAuthorizationPolicyProvider>();
|
||||
services.AddSingleton<IAuthorizationHandler, PermissionAuthorizationHandler>();
|
||||
var provider = services.BuildServiceProvider();
|
||||
|
||||
var policyProvider = provider.GetRequiredService<IAuthorizationPolicyProvider>();
|
||||
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<IPolicyEvaluator>();
|
||||
|
||||
// Act
|
||||
var result = await evaluator.AuthorizeAsync(policy!, authenticateResult, httpContext, resource: null);
|
||||
|
||||
// Assert
|
||||
result.Forbidden.Should().BeTrue();
|
||||
}
|
||||
}
|
||||
@@ -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<DictionaryDbContext>()
|
||||
.UseSqlite(_connection)
|
||||
.EnableSensitiveDataLogging()
|
||||
.Options;
|
||||
}
|
||||
|
||||
public DbContextOptions<DictionaryDbContext> 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();
|
||||
}
|
||||
}
|
||||
@@ -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<TakeoutAppDbContext>()
|
||||
.UseSqlite(_connection)
|
||||
.EnableSensitiveDataLogging()
|
||||
.Options;
|
||||
}
|
||||
|
||||
public DbContextOptions<TakeoutAppDbContext> 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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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<string, object> _cache = new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
public async Task<T?> GetOrCreateAsync<T>(
|
||||
string key,
|
||||
TimeSpan ttl,
|
||||
Func<CancellationToken, Task<T?>> 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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<TenantAnnouncement>();
|
||||
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));
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.4" />
|
||||
<PackageReference Include="FluentAssertions" Version="8.8.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.1" />
|
||||
<PackageReference Include="Microsoft.Data.Sqlite" Version="10.0.1" />
|
||||
<PackageReference Include="Moq" Version="4.20.72" />
|
||||
<PackageReference Include="xunit" Version="2.9.3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Using Include="Xunit" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\Application\TakeoutSaaS.Application\TakeoutSaaS.Application.csproj" />
|
||||
<ProjectReference Include="..\..\src\Domain\TakeoutSaaS.Domain\TakeoutSaaS.Domain.csproj" />
|
||||
<ProjectReference Include="..\..\src\Infrastructure\TakeoutSaaS.Infrastructure\TakeoutSaaS.Infrastructure.csproj" />
|
||||
<ProjectReference Include="..\..\src\Modules\TakeoutSaaS.Module.Authorization\TakeoutSaaS.Module.Authorization.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Update="System.IO.Packaging" Version="10.0.1" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
Reference in New Issue
Block a user