Files
tiku-backend.net/Tiku.IntegrationTests/Api/CapabilityAuthorizationTests.cs
xiong c497a3ca8d
Some checks failed
ci / release-gate (push) Has been cancelled
清理代码
2026-08-03 12:31:39 +08:00

193 lines
7.4 KiB
C#

using System.Text.Json;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Tiku.Application.Jobs;
using Tiku.Application.Security;
using Tiku.Domain.Operations;
using Tiku.Domain.Platform;
using Tiku.Domain.Tenancy;
using Tiku.Infrastructure.Persistence;
namespace Tiku.IntegrationTests.Api;
public sealed class CapabilityAuthorizationTests
{
[Fact]
public async Task Background_job_rechecks_feature_access_after_enqueue_before_execution()
{
await using var factory = new ApiTestFactory();
var tenantId = Guid.NewGuid();
var fixture = CreateSubscriptionFixture(tenantId, SaasFeatureCatalog.PrivateQuestionBank, true);
await factory.SeedAsync(fixture.Entities);
using var scope = factory.CreateSystemScope("Verify job feature access at consumption");
var jobs = scope.ServiceProvider.GetRequiredService<IBackgroundJobService>();
var job = await jobs.EnqueueAsync(new CreateBackgroundJobCommand(
tenantId,
"content_export",
JsonSerializer.SerializeToElement(new { exportType = "feature-test" })));
var dbContext = scope.ServiceProvider.GetRequiredService<TikuDbContext>();
dbContext.TenantFeatureOverrides.Add(new TenantFeatureOverride
{
TenantId = tenantId,
FeatureCode = SaasFeatureCatalog.PrivateQuestionBank,
Mode = TenantFeatureOverrideMode.Disabled,
Reason = "Integration test revocation"
});
await dbContext.SaveChangesAsync();
Assert.True(await jobs.ProcessRequestedAsync(
job.Id, tenantId, job.JobType, "feature-test-worker"));
var stored = await dbContext.BackgroundJobs.AsNoTracking().SingleAsync(item => item.Id == job.Id);
Assert.Equal(BackgroundJobStatus.Failed, stored.Status);
Assert.Equal("Tenant feature entitlement was revoked before job execution.", stored.LastError);
}
[Fact]
public async Task Feature_access_is_database_backed_supports_overrides_and_makes_past_due_read_only()
{
await using var factory = new ApiTestFactory();
var tenantId = Guid.NewGuid();
var fixture = CreateSubscriptionFixture(tenantId, SaasFeatureCatalog.PrivateQuestionBank, true);
await factory.SeedAsync(fixture.Entities);
await factory.SeedAsync(new SaasFeature
{
Code = SaasFeatureCatalog.Video,
Name = "Video",
Category = "content",
Status = SaasFeatureStatus.Active
});
using var scope = factory.CreateSystemScope("Verify feature authorization");
var access = scope.ServiceProvider.GetRequiredService<IFeatureAccessService>();
var missing = await access.EvaluateAsync(
tenantId,
SaasFeatureCatalog.Video,
FeatureAccessOperation.Read);
Assert.False(missing.Allowed);
Assert.Equal("feature_not_purchased", missing.DenialCode);
var dbContext = scope.ServiceProvider.GetRequiredService<TikuDbContext>();
Assert.True((await access.EvaluateAsync(
tenantId,
SaasFeatureCatalog.PrivateQuestionBank,
FeatureAccessOperation.Write)).Allowed);
var featureOverride = new TenantFeatureOverride
{
TenantId = tenantId,
FeatureCode = SaasFeatureCatalog.PrivateQuestionBank,
Mode = TenantFeatureOverrideMode.Disabled,
Reason = "Integration test disable"
};
dbContext.TenantFeatureOverrides.Add(featureOverride);
await dbContext.SaveChangesAsync();
var disabled = await access.EvaluateAsync(
tenantId,
SaasFeatureCatalog.PrivateQuestionBank,
FeatureAccessOperation.Read);
Assert.False(disabled.Allowed);
Assert.Equal("feature_disabled", disabled.DenialCode);
featureOverride.Mode = TenantFeatureOverrideMode.Enabled;
await dbContext.SaveChangesAsync();
Assert.True((await access.EvaluateAsync(
tenantId,
SaasFeatureCatalog.PrivateQuestionBank,
FeatureAccessOperation.Write)).Allowed);
var subscription =
await dbContext.TenantSaasSubscriptions.SingleAsync(item => item.Id == fixture.SubscriptionId);
subscription.Status = TenantSaasSubscriptionStatus.PastDue;
await dbContext.SaveChangesAsync();
Assert.True((await access.EvaluateAsync(
tenantId,
SaasFeatureCatalog.PrivateQuestionBank,
FeatureAccessOperation.Read)).Allowed);
var pastDueWrite = await access.EvaluateAsync(
tenantId,
SaasFeatureCatalog.PrivateQuestionBank,
FeatureAccessOperation.Write);
Assert.False(pastDueWrite.Allowed);
Assert.Equal("subscription_read_only", pastDueWrite.DenialCode);
}
private static SubscriptionFixture CreateSubscriptionFixture(
Guid tenantId,
string featureCode,
bool includeEntitlement)
{
var offeringId = Guid.NewGuid();
var versionId = Guid.NewGuid();
var subscriptionId = Guid.NewGuid();
var now = DateTimeOffset.UtcNow;
var entities = new List<object>
{
new Tenant
{
Id = tenantId,
Slug = tenantId.ToString("N"),
Name = "Feature Access Tenant"
},
new SaasFeature
{
Code = featureCode,
Name = "Private Question Bank",
Category = "content",
Status = SaasFeatureStatus.Active
},
new SaasOffering
{
Id = offeringId,
Code = $"feature-test-{tenantId:N}",
Name = "Feature Access Test",
Type = SaasOfferingType.BasePlan,
Status = SaasOfferingStatus.Active
},
new SaasOfferingVersion
{
Id = versionId,
OfferingId = offeringId,
Version = 1,
Status = SaasOfferingVersionStatus.Published,
PublishedAt = now.AddDays(-1),
EffectiveAt = now.AddDays(-1)
},
new TenantSaasSubscription
{
Id = subscriptionId,
TenantId = tenantId,
BaseOfferingVersionId = versionId,
Status = TenantSaasSubscriptionStatus.Active,
StartsAt = now.AddDays(-1),
CurrentPeriodStart = now.AddDays(-1),
CurrentPeriodEnd = now.AddDays(30)
},
new TenantSaasSubscriptionItem
{
TenantId = tenantId,
SubscriptionId = subscriptionId,
OfferingVersionId = versionId,
ItemType = TenantSaasSubscriptionItemType.BasePlan,
Status = TenantSaasSubscriptionItemStatus.Active,
StartsAt = now.AddDays(-1),
EndsAt = now.AddDays(30)
}
};
if (includeEntitlement)
entities.Add(new SaasOfferingVersionFeature
{
OfferingVersionId = versionId,
FeatureCode = featureCode
});
return new SubscriptionFixture(versionId, subscriptionId, entities.ToArray());
}
private sealed record SubscriptionFixture(
Guid VersionId,
Guid SubscriptionId,
object[] Entities);
}