refactor: 删除 tests 目录

This commit is contained in:
root
2026-01-30 01:05:23 +00:00
parent cf697b3889
commit 5de862f73f
22 changed files with 12 additions and 2184 deletions

View File

@@ -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

View File

@@ -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));
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -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>

View File

@@ -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 }
};
}

View File

@@ -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;
}
}

View File

@@ -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 }
};
}

View File

@@ -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 }
};
}

View File

@@ -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 }
};
}

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -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;
}

View File

@@ -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;
}
}

View File

@@ -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;
}

View File

@@ -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));
}
}

View File

@@ -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>