feat: 实现字典管理后端
This commit is contained in:
@@ -0,0 +1,605 @@
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using FluentAssertions;
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user