forked from xiongyuxing/tiku-backend.net
195 lines
7.5 KiB
C#
195 lines
7.5 KiB
C#
using Microsoft.EntityFrameworkCore;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using System.Text.Json;
|
|
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, includeEntitlement: 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, includeEntitlement: 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);
|
|
}
|