using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Npgsql; using Tiku.Application.Commerce; using Tiku.Application.Auth; using Tiku.Application.Growth; using Tiku.Application.Storage; using Tiku.Application.Security; using Tiku.Application.Tenancy; using Tiku.Api; using Tiku.Domain.Identity; using Tiku.Domain.QuestionBanks; using Tiku.Domain.Content; using Tiku.Domain.Tenancy; using Tiku.IntegrationTests.Infrastructure; using Tiku.Infrastructure.Persistence; namespace Tiku.IntegrationTests.Api; public sealed class ApiTestFactory( IWechatOAuthClient? wechatOAuthClient = null, IObjectStorageService? objectStorageService = null, IReferralQrcodeGenerator? referralQrcodeGenerator = null, IPaymentProviderGateway? paymentProviderGateway = null, IDomainOwnershipVerifier? domainOwnershipVerifier = null, IDomainGatewayProvisioner? domainGatewayProvisioner = null) : WebApplicationFactory { private readonly PostgresTestDatabase database = PostgresTestDatabase.Create(); protected override void ConfigureWebHost(Microsoft.AspNetCore.Hosting.IWebHostBuilder builder) { builder.ConfigureServices(services => { foreach (var descriptor in services .Where(descriptor => descriptor.ServiceType == typeof(NpgsqlDataSource) || descriptor.ServiceType == typeof(TenantIsolationSaveChangesInterceptor) || descriptor.ServiceType == typeof(DbContextOptions) || descriptor.ServiceType.FullName?.Contains(nameof(TikuDbContext), StringComparison.Ordinal) == true) .ToArray()) { services.Remove(descriptor); } services.AddSingleton(_ => NpgsqlDataSource.Create(database.ConnectionString)); services.AddScoped(); services.AddDbContext((serviceProvider, options) => { var dataSource = serviceProvider.GetRequiredService(); options.UseNpgsql(dataSource, npgsql => npgsql.MigrationsAssembly(typeof(TikuDbContext).Assembly.FullName)); options.AddInterceptors(serviceProvider.GetRequiredService()); }); if (wechatOAuthClient is not null) { services.AddSingleton(wechatOAuthClient); } if (objectStorageService is not null) { services.AddSingleton(objectStorageService); } if (referralQrcodeGenerator is not null) { services.AddSingleton(referralQrcodeGenerator); } if (paymentProviderGateway is not null) { services.AddSingleton(paymentProviderGateway); } if (domainOwnershipVerifier is not null) { services.RemoveAll(); services.AddSingleton(domainOwnershipVerifier); } if (domainGatewayProvisioner is not null) { services.RemoveAll(); services.AddSingleton(domainGatewayProvisioner); } }); } public async Task SeedAsync(params object[] entities) { using var scope = Services.CreateScope(); scope.ServiceProvider.GetRequiredService() .InitializeSystem(null, "Integration test fixture seeding"); var dbContext = scope.ServiceProvider.GetRequiredService(); dbContext.AddRange(entities); await dbContext.SaveChangesAsync(); } public IServiceScope CreateSystemScope(string reason = "Integration test verification") { var scope = Services.CreateScope(); scope.ServiceProvider.GetRequiredService() .InitializeSystem(null, reason); return scope; } public IServiceScope CreateTenantScope(Guid tenantId, string? tenantCode = null) { var scope = Services.CreateScope(); scope.ServiceProvider.GetRequiredService() .Initialize(tenantId, tenantCode, TenantResolutionSource.TenantCode); return scope; } public async Task SeedQuestionWithVersionAsync(Question question, QuestionVersion version) { question.CurrentVersionId = null; await SeedAsync(question); await SeedAsync(version); using var scope = Services.CreateScope(); scope.ServiceProvider.GetRequiredService() .InitializeSystem(question.TenantId, "Integration test question version linking"); var dbContext = scope.ServiceProvider.GetRequiredService(); var persistedQuestion = await dbContext.Questions.SingleAsync(item => item.TenantId == question.TenantId && item.Id == question.Id); persistedQuestion.CurrentVersionId = version.Id; await dbContext.SaveChangesAsync(); } public async Task SeedQuestionReferenceAsync( Guid tenantId, Guid questionOwnerTenantId, Guid questionId, QuestionSource source = QuestionSource.Tenant) { var reference = new TenantQuestionReference { Id = Guid.NewGuid(), TenantId = tenantId, QuestionOwnerTenantId = questionOwnerTenantId, QuestionId = questionId, Source = source }; await SeedAsync(reference); return reference; } public async Task SeedActiveSessionAsync( Guid userId, Guid? tenantId = null, string tokenHash = "integration-test-token-hash") { var resolvedTenantId = tenantId ?? Guid.NewGuid(); await SeedAsync( new Tenant { Id = resolvedTenantId, Slug = resolvedTenantId.ToString("N"), Name = "Test Tenant" }, new User { Id = userId, Phone = "13800000000" }, new AuthSession { Id = Guid.NewGuid(), TenantId = resolvedTenantId, UserId = userId, TokenHash = tokenHash, Provider = "test", ExpiresAt = DateTimeOffset.UtcNow.AddHours(1) }); using var scope = Services.CreateScope(); scope.ServiceProvider.GetRequiredService() .InitializeSystem(resolvedTenantId, "Integration test session lookup"); var dbContext = scope.ServiceProvider.GetRequiredService(); return await dbContext.AuthSessions .Where(session => session.UserId == userId) .Select(session => session.Id) .SingleAsync(); } protected override void Dispose(bool disposing) { base.Dispose(disposing); if (disposing) { database.Dispose(); } } }