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(); var job = await jobs.EnqueueAsync(new CreateBackgroundJobCommand( tenantId, "content_export", JsonSerializer.SerializeToElement(new { exportType = "feature-test" }))); var dbContext = scope.ServiceProvider.GetRequiredService(); 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(); 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(); 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 { 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); }