using System.Text.Json; using System.Net; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Tiku.Application.Jobs; using Tiku.Domain.Operations; using Tiku.Domain.Tenancy; using Tiku.Infrastructure.Persistence; namespace Tiku.IntegrationTests.Api; public sealed class MonolithBackgroundProcessingTests { [Fact] public async Task Anonymous_readiness_is_minimal_and_does_not_expose_dependency_topology() { await using var factory = new ApiTestFactory(); using var client = factory.CreateClient(); var response = await client.GetAsync("/api/health/ready"); using var document = await JsonDocument.ParseAsync(await response.Content.ReadAsStreamAsync()); Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.Equal("ready", document.RootElement.GetProperty("status").GetString()); Assert.False(document.RootElement.TryGetProperty("database", out _)); Assert.False(document.RootElement.TryGetProperty("redis", out _)); Assert.False(document.RootElement.TryGetProperty("rabbitMq", out _)); Assert.False(document.RootElement.TryGetProperty("outbox", out _)); } [Fact] public async Task Api_host_does_not_register_background_processors() { await using var factory = new ApiTestFactory(); using var client = factory.CreateClient(); var hostedServiceNames = factory.Services.GetServices() .Select(service => service.GetType().Name) .ToArray(); Assert.DoesNotContain("TenantDomainWorker", hostedServiceNames); Assert.DoesNotContain("SaasSubscriptionWorker", hostedServiceNames); Assert.DoesNotContain("FeatureUsageWorker", hostedServiceNames); Assert.DoesNotContain("BackgroundJobsWorker", hostedServiceNames); } [Fact] public async Task Postgres_leases_recover_expired_work_without_duplicate_claims() { await using var factory = new ApiTestFactory(); var tenantId = Guid.NewGuid(); await factory.SeedAsync(new Tenant { Id = tenantId, Slug = tenantId.ToString("N"), Name = "Monolith Lease Recovery" }); Guid expiredJobId; Guid pendingJobId; using (var scope = factory.CreateSystemScope("Seed lease recovery jobs")) { var jobs = scope.ServiceProvider.GetRequiredService(); expiredJobId = (await jobs.EnqueueAsync(new CreateBackgroundJobCommand( tenantId, "statistics_aggregation", JsonSerializer.SerializeToElement(new { scope = "tenant" })))).Id; pendingJobId = (await jobs.EnqueueAsync(new CreateBackgroundJobCommand( tenantId, "statistics_aggregation", JsonSerializer.SerializeToElement(new { scope = "tenant" })))).Id; var dbContext = scope.ServiceProvider.GetRequiredService(); await dbContext.BackgroundJobs .Where(job => job.Id == expiredJobId) .ExecuteUpdateAsync(setters => setters .SetProperty(job => job.Status, BackgroundJobStatus.Processing) .SetProperty(job => job.LockedBy, "stopped-worker") .SetProperty(job => job.LockExpiresAt, DateTimeOffset.UtcNow.AddSeconds(-1))); } var processed = await Task.WhenAll( ProcessPendingAsync(factory, "monolith-lease-a"), ProcessPendingAsync(factory, "monolith-lease-b")); Assert.Equal(2, processed.Sum()); Assert.Equal(BackgroundJobStatus.Succeeded, await ReadStatusAsync(factory, expiredJobId)); Assert.Equal(BackgroundJobStatus.Succeeded, await ReadStatusAsync(factory, pendingJobId)); } private static async Task ProcessPendingAsync(ApiTestFactory factory, string workerId) { using var scope = factory.CreateSystemScope($"Process jobs with {workerId}"); return await scope.ServiceProvider.GetRequiredService() .ProcessPendingAsync(workerId, 1); } private static async Task ReadStatusAsync(ApiTestFactory factory, Guid jobId) { using var scope = factory.CreateSystemScope("Read monolith background job status"); return await scope.ServiceProvider.GetRequiredService() .BackgroundJobs.AsNoTracking() .Where(job => job.Id == jobId) .Select(job => job.Status) .SingleAsync(); } }