using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Npgsql; using Tiku.Application.Commerce; using Tiku.Application.Auth; using Tiku.Application.Growth; using Tiku.Application.Storage; using Tiku.Domain.Identity; using Tiku.Domain.QuestionBanks; 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) : 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(DbContextOptions) || descriptor.ServiceType.FullName?.Contains(nameof(TikuDbContext), StringComparison.Ordinal) == true) .ToArray()) { services.Remove(descriptor); } services.AddSingleton(_ => NpgsqlDataSource.Create(database.ConnectionString)); services.AddDbContextPool((serviceProvider, options) => { var dataSource = serviceProvider.GetRequiredService(); options.UseNpgsql(dataSource, npgsql => npgsql.MigrationsAssembly(typeof(TikuDbContext).Assembly.FullName)); }); 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); } }); } public async Task SeedAsync(params object[] entities) { using var scope = Services.CreateScope(); var dbContext = scope.ServiceProvider.GetRequiredService(); dbContext.AddRange(entities); await dbContext.SaveChangesAsync(); } public async Task SeedQuestionWithVersionAsync(Question question, QuestionVersion version) { question.CurrentVersionId = null; await SeedAsync(question); await SeedAsync(version); using var scope = Services.CreateScope(); 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 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(); 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(); } } }