226 lines
9.0 KiB
C#
226 lines
9.0 KiB
C#
using Microsoft.Extensions.DependencyInjection;
|
|
using Tiku.Application.Learning;
|
|
using Tiku.Domain.Commerce;
|
|
using Tiku.Domain.Identity;
|
|
using Tiku.Domain.Learning;
|
|
using Tiku.Domain.Tenancy;
|
|
|
|
namespace Tiku.IntegrationTests.Api;
|
|
|
|
public sealed class LearningAccessPolicyTests
|
|
{
|
|
[Fact]
|
|
public async Task Target_region_history_is_isolated_per_business_line()
|
|
{
|
|
await using var factory = new ApiTestFactory();
|
|
var tenantId = Guid.NewGuid();
|
|
var userId = Guid.NewGuid();
|
|
var primaryBusinessLineId = Guid.NewGuid();
|
|
var futureBusinessLineId = Guid.NewGuid();
|
|
var primaryRegionId = Guid.NewGuid();
|
|
var futureRegionId = Guid.NewGuid();
|
|
await factory.SeedAsync(
|
|
new Tenant { Id = tenantId, Slug = tenantId.ToString("N"), Name = "Multi-business Tenant" },
|
|
new User { Id = userId, Phone = $"137{Random.Shared.Next(10_000_000, 99_999_999)}" },
|
|
new BusinessLine
|
|
{
|
|
Id = primaryBusinessLineId,
|
|
Code = $"primary-{primaryBusinessLineId:N}",
|
|
Name = "当前主业务",
|
|
RegionAccessStrategy = LearningRegionAccessStrategy.NationalWithStudentTargetRegion
|
|
},
|
|
new BusinessLine
|
|
{
|
|
Id = futureBusinessLineId,
|
|
Code = $"future-{futureBusinessLineId:N}",
|
|
Name = "未来新增业务",
|
|
RegionAccessStrategy = LearningRegionAccessStrategy.NationalWithStudentTargetRegion
|
|
},
|
|
new MarketRegion { Id = primaryRegionId, Code = $"p-{primaryRegionId:N}"[..18], Name = "主业务地区" },
|
|
new MarketRegion { Id = futureRegionId, Code = $"f-{futureRegionId:N}"[..18], Name = "新增业务地区" },
|
|
new TenantLearningLicense
|
|
{
|
|
TenantId = tenantId,
|
|
BusinessLineId = primaryBusinessLineId,
|
|
IsPrimary = true,
|
|
IncludesNational = true
|
|
},
|
|
new TenantLearningLicense
|
|
{
|
|
TenantId = tenantId,
|
|
BusinessLineId = futureBusinessLineId,
|
|
IsPrimary = false,
|
|
IncludesNational = true
|
|
},
|
|
new StudentTargetRegionHistory
|
|
{
|
|
TenantId = tenantId,
|
|
UserId = userId,
|
|
BusinessLineId = primaryBusinessLineId,
|
|
MarketRegionId = primaryRegionId
|
|
},
|
|
new StudentTargetRegionHistory
|
|
{
|
|
TenantId = tenantId,
|
|
UserId = userId,
|
|
BusinessLineId = futureBusinessLineId,
|
|
MarketRegionId = futureRegionId
|
|
});
|
|
|
|
using var scope = factory.CreateTenantScope(tenantId);
|
|
var target = await scope.ServiceProvider.GetRequiredService<ILearningAccessAdministrationService>()
|
|
.GetTargetRegionAsync(new LearningActor(tenantId, userId));
|
|
|
|
Assert.NotNull(target);
|
|
Assert.Equal(primaryRegionId, target.MarketRegionId);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(LearningRegionAccessStrategy.NationalOnly, true, 1, 0)]
|
|
[InlineData(LearningRegionAccessStrategy.LicensedRegions, false, 2, 2)]
|
|
[InlineData(LearningRegionAccessStrategy.NationalWithStudentTargetRegion, true, 2, 1)]
|
|
[InlineData(LearningRegionAccessStrategy.NationalWithLicensedRegions, true, 3, 2)]
|
|
public async Task Configured_region_strategy_compiles_expected_slice_and_region_grants(
|
|
LearningRegionAccessStrategy strategy,
|
|
bool includesNational,
|
|
int expectedSliceCount,
|
|
int expectedRegionCount)
|
|
{
|
|
await using var factory = new ApiTestFactory();
|
|
var tenantId = Guid.NewGuid();
|
|
var userId = Guid.NewGuid();
|
|
var businessLineId = Guid.NewGuid();
|
|
var licenseId = Guid.NewGuid();
|
|
var productId = Guid.NewGuid();
|
|
var regionA = Guid.NewGuid();
|
|
var regionB = Guid.NewGuid();
|
|
var nationalSlice = Slice(tenantId, businessLineId, LearningRegionScopeKind.National, null);
|
|
var regionASlice = Slice(tenantId, businessLineId, LearningRegionScopeKind.Region, regionA);
|
|
var regionBSlice = Slice(tenantId, businessLineId, LearningRegionScopeKind.Region, regionB);
|
|
var slices = new[] { nationalSlice, regionASlice, regionBSlice };
|
|
|
|
var entities = new List<object>
|
|
{
|
|
new Tenant { Id = tenantId, Slug = tenantId.ToString("N"), Name = "Policy Tenant" },
|
|
new User { Id = userId, Phone = $"136{Random.Shared.Next(10_000_000, 99_999_999)}" },
|
|
new BusinessLine
|
|
{
|
|
Id = businessLineId,
|
|
Code = $"p-{(int)strategy}-{tenantId:N}",
|
|
Name = "配置化业务线",
|
|
RegionAccessStrategy = strategy
|
|
},
|
|
new MarketRegion { Id = regionA, Code = $"a-{regionA:N}"[..18], Name = "地区 A" },
|
|
new MarketRegion { Id = regionB, Code = $"b-{regionB:N}"[..18], Name = "地区 B" },
|
|
new TenantLearningLicense
|
|
{
|
|
Id = licenseId,
|
|
TenantId = tenantId,
|
|
BusinessLineId = businessLineId,
|
|
IncludesNational = includesNational,
|
|
StartsAt = DateTimeOffset.UtcNow.AddDays(-1)
|
|
},
|
|
new TenantLearningLicenseRegion
|
|
{
|
|
TenantId = tenantId,
|
|
LicenseId = licenseId,
|
|
MarketRegionId = regionA,
|
|
IsBaseRegion = true
|
|
},
|
|
new TenantLearningLicenseRegion
|
|
{
|
|
TenantId = tenantId,
|
|
LicenseId = licenseId,
|
|
MarketRegionId = regionB
|
|
},
|
|
new LearningProduct
|
|
{
|
|
Id = productId,
|
|
TenantId = tenantId,
|
|
BusinessLineId = businessLineId,
|
|
Code = "all-slices",
|
|
Name = "全部测试切片"
|
|
},
|
|
new Entitlement
|
|
{
|
|
TenantId = tenantId,
|
|
UserId = userId,
|
|
LearningProductId = productId,
|
|
StartsAt = DateTimeOffset.UtcNow.AddDays(-1),
|
|
ExpiresAt = DateTimeOffset.UtcNow.AddDays(1),
|
|
Status = EntitlementStatus.Active,
|
|
SourceType = "integration_test"
|
|
},
|
|
new LearningAccessVersion { TenantId = tenantId, UserId = userId }
|
|
};
|
|
entities.AddRange(slices);
|
|
entities.AddRange(slices.Select(slice => new LearningProductScope
|
|
{
|
|
TenantId = tenantId,
|
|
ProductId = productId,
|
|
ContentSliceOwnerTenantId = tenantId,
|
|
ContentSliceId = slice.Id
|
|
}));
|
|
if (strategy == LearningRegionAccessStrategy.NationalWithStudentTargetRegion)
|
|
entities.Add(new StudentTargetRegionHistory
|
|
{
|
|
TenantId = tenantId,
|
|
UserId = userId,
|
|
BusinessLineId = businessLineId,
|
|
MarketRegionId = regionA
|
|
});
|
|
await factory.SeedAsync([.. entities]);
|
|
|
|
using var scope = factory.CreateTenantScope(tenantId);
|
|
var snapshot = await scope.ServiceProvider.GetRequiredService<ILearningAccessService>()
|
|
.GetSnapshotAsync(new LearningActor(tenantId, userId));
|
|
|
|
Assert.Equal(expectedSliceCount, snapshot.ContentSliceIds.Count);
|
|
Assert.Equal(expectedRegionCount, snapshot.LicensedRegionIds.Count);
|
|
Assert.Equal(includesNational, snapshot.ContentSliceIds.Contains(nationalSlice.Id));
|
|
Assert.Equal(
|
|
strategy == LearningRegionAccessStrategy.NationalWithStudentTargetRegion ? regionA : (Guid?)null,
|
|
snapshot.TargetRegionId);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Snapshot_fails_closed_without_an_active_primary_license()
|
|
{
|
|
await using var factory = new ApiTestFactory();
|
|
var tenantId = Guid.NewGuid();
|
|
var userId = Guid.NewGuid();
|
|
await factory.SeedAsync(new Tenant
|
|
{
|
|
Id = tenantId,
|
|
Slug = tenantId.ToString("N"),
|
|
Name = "Unlicensed Tenant"
|
|
});
|
|
|
|
using var scope = factory.CreateTenantScope(tenantId);
|
|
var exception = await Assert.ThrowsAsync<LearningAccessException>(() =>
|
|
scope.ServiceProvider.GetRequiredService<ILearningAccessService>()
|
|
.GetSnapshotAsync(new LearningActor(tenantId, userId)));
|
|
|
|
Assert.Equal("learning_license_required", exception.Code);
|
|
}
|
|
|
|
private static ContentSlice Slice(
|
|
Guid tenantId,
|
|
Guid businessLineId,
|
|
LearningRegionScopeKind regionScope,
|
|
Guid? regionId)
|
|
{
|
|
return new ContentSlice
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
TenantId = tenantId,
|
|
BusinessLineId = businessLineId,
|
|
RegionScope = regionScope,
|
|
MarketRegionId = regionId,
|
|
ResourceType = LearningContentResourceType.Collection,
|
|
ResourceId = Guid.NewGuid(),
|
|
Status = ContentSliceStatus.Active
|
|
};
|
|
}
|
|
}
|