using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Hosting; using NSubstitute; using UniVerse.Application.DTOs.Achievements; using UniVerse.Application.DTOs.Auth; using UniVerse.Application.DTOs.Common; using UniVerse.Application.DTOs.Courses; using UniVerse.Application.DTOs.Gamification; using UniVerse.Application.DTOs.Lectures; using UniVerse.Application.DTOs.Locations; using UniVerse.Application.DTOs.Notifications; using UniVerse.Application.DTOs.Reviews; using UniVerse.Application.DTOs.Sync; using UniVerse.Application.DTOs.Tags; using UniVerse.Application.DTOs.Users; using UniVerse.Application.Interfaces; using UniVerse.Domain.Enums; using UniVerse.Infrastructure.Data; namespace UniVerse.Api.Tests.Helpers; /// /// WebApplicationFactory для интеграционных тестов. /// Заменяет Npgsql DbContext на InMemory, создает заглушки для всех интерфейсов внешних сервисов /// и отключает фоновую службу LLM, чтобы тестам не требовалась реальная инфраструктура. /// public class ApiWebApplicationFactory : WebApplicationFactory { protected override void ConfigureWebHost(IWebHostBuilder builder) { // Используем Development, чтобы были включены Swagger и конечная точка DevLogin builder.UseEnvironment("Development"); builder.ConfigureAppConfiguration((_, config) => { // Внедряем настройки тестового JWT — должны совпадать с константами TestJwtFactory var testSettings = new Dictionary { ["Jwt:Secret"] = TestJwtFactory.Secret, ["Jwt:Issuer"] = TestJwtFactory.Issuer, ["Jwt:Audience"] = TestJwtFactory.Audience, // Отключаем оркестрацию Aspire ["Aspire:Enabled"] = "false", // Фиктивные значения Azure AD (маршруты имеют атрибут [AllowAnonymous] или тестируются отдельно) ["AzureAd:TenantId"] = "test-tenant", ["AzureAd:ClientId"] = "test-client", // Фиктивные значения LLM / Modeus (клиенты заменяются ниже) ["Llm:BaseUrl"] = "http://localhost:9999/", ["ModeusApi:BaseUrl"] = "http://localhost:9998/", }; config.AddInMemoryCollection(testSettings); }); builder.ConfigureServices(services => { // ── 1. Заменяем Npgsql DbContext на InMemory ────────────────────────── services.RemoveAll>(); services.RemoveAll(); // Удаляем все регистрации, связанные с DbContext, которые добавил хост var descriptor = services.SingleOrDefault( d => d.ServiceType == typeof(DbContextOptions)); if (descriptor != null) services.Remove(descriptor); // Находим и удаляем все дескрипторы настроек DbContext var dbContextDescriptors = services .Where(d => d.ServiceType == typeof(DbContextOptions) || d.ImplementationType == typeof(AppDbContext)) .ToList(); foreach (var d in dbContextDescriptors) services.Remove(d); services.AddDbContext(options => options.UseInMemoryDatabase($"TestDb_{Guid.NewGuid()}")); // ── 2. Отключаем фоновые службы ──────────────────────────────────── // Удаляем все регистрации IHostedService, чтобы предотвратить запуск фоновой задачи LLM var hostedServices = services .Where(d => d.ServiceType == typeof(IHostedService)) .ToList(); foreach (var d in hostedServices) services.Remove(d); // ── 3. Создаем заглушки для всех интерфейсов Application сервисов ───────── ReplaceWithSubstitute(services, CreateAuthServiceStub()); ReplaceWithSubstitute(services, CreateUserServiceStub()); ReplaceWithSubstitute(services, CreateLectureServiceStub()); ReplaceWithSubstitute(services, CreateReviewServiceStub()); ReplaceWithSubstitute(services, CreateCourseServiceStub()); ReplaceWithSubstitute(services, CreateTagServiceStub()); ReplaceWithSubstitute(services, CreateLocationServiceStub()); ReplaceWithSubstitute(services, CreateAchievementServiceStub()); ReplaceWithSubstitute(services, CreateGamificationServiceStub()); ReplaceWithSubstitute(services, CreateSyncServiceStub()); ReplaceWithSubstitute(services, Substitute.For()); ReplaceWithSubstitute(services, Substitute.For()); ReplaceWithSubstitute(services, CreateNotificationServiceStub()); }); } private static void ReplaceWithSubstitute(IServiceCollection services, TService instance) where TService : class { services.RemoveAll(); services.AddScoped(_ => instance); } // ── Фабрики заглушек ──────────────────────────────────────────────────────────── private static IAuthService CreateAuthServiceStub() { var stub = Substitute.For(); var authResult = new AuthResult( new AuthResponse("access_token", DateTime.UtcNow.AddHours(1), new UserAuthDto(1, "test@test.com", "Test User", [UserRole.Student])), "refresh_token"); stub.LoginWithMicrosoftAsync(Arg.Any(), Arg.Any(), Arg.Any()) .Returns(authResult); stub.DevLoginAsync(Arg.Any(), Arg.Any(), Arg.Any>(), Arg.Any()) .Returns(authResult); stub.RefreshTokenAsync(Arg.Any()).Returns(authResult); stub.GetCurrentUserAsync(Arg.Any()) .Returns(new UserDto(1, "test@test.com", "Test", null, [UserRole.Student], true, 0, 0, 1, DateTime.UtcNow)); return stub; } private static INotificationService CreateNotificationServiceStub() { var stub = Substitute.For(); stub.SendAsync(Arg.Any(), Arg.Any()) .Returns(Task.CompletedTask); stub.ScheduleAsync(Arg.Any(), Arg.Any()) .Returns(new ScheduledNotificationResponse("test-job", DateTimeOffset.UtcNow.AddMinutes(5))); stub.GetUserNotificationsAsync(Arg.Any(), Arg.Any(), Arg.Any()) .Returns(PagedResult.Create([], 0, 1, 20)); stub.MarkAllReadAsync(Arg.Any(), Arg.Any()) .Returns(Task.CompletedTask); stub.CreateUserNotificationAsync( Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) .Returns(new UserNotificationDto(1, "achievement", "Title", "Body", false, DateTime.UtcNow)); return stub; } private static IUserService CreateUserServiceStub() { var stub = Substitute.For(); var userDto = new UserDto(1, "test@test.com", "Test", null, [UserRole.Student], true, 0, 0, 1, DateTime.UtcNow); var pagedUsers = PagedResult.Create([userDto], 1, 1, 20); stub.GetByIdAsync(Arg.Any()).Returns(userDto); stub.UpdateProfileAsync(Arg.Any(), Arg.Any()).Returns(userDto); stub.GetStatsAsync(Arg.Any()).Returns(new UserStatsDto(0, 0, 0, 0, 0, 1, 0)); stub.GetAllAsync(Arg.Any()).Returns(pagedUsers); stub.SetRolesAsync(Arg.Any(), Arg.Any>()).Returns(Task.CompletedTask); stub.SetActiveAsync(Arg.Any(), Arg.Any()).Returns(Task.CompletedTask); return stub; } private static ILectureService CreateLectureServiceStub() { var stub = Substitute.For(); var lectureDto = new LectureDto(1, 1, "Course", null, null, null, null, "Title", null, LectureFormat.Offline, DateTime.UtcNow, DateTime.UtcNow.AddHours(2), true, 30, 0, null, DateTime.UtcNow); var detailDto = new LectureDetailDto(1, 1, "Course", null, null, null, null, "Title", null, LectureFormat.Offline, DateTime.UtcNow, DateTime.UtcNow.AddHours(2), true, 30, 0, null, DateTime.UtcNow, false); var pagedLectures = PagedResult.Create([lectureDto], 1, 1, 20); var pagedEnrollments = PagedResult.Create([], 0, 1, 20); stub.GetAllAsync(Arg.Any(), Arg.Any()).Returns(pagedLectures); stub.GetByIdAsync(Arg.Any(), Arg.Any()).Returns(detailDto); stub.CreateAsync(Arg.Any()).Returns(lectureDto); stub.UpdateAsync(Arg.Any(), Arg.Any()).Returns(lectureDto); stub.DeleteAsync(Arg.Any()).Returns(Task.CompletedTask); stub.EnrollAsync(Arg.Any(), Arg.Any()).Returns(Task.CompletedTask); stub.UnenrollAsync(Arg.Any(), Arg.Any()).Returns(Task.CompletedTask); stub.MarkAttendanceAsync(Arg.Any(), Arg.Any(), Arg.Any()).Returns(Task.CompletedTask); stub.GetEnrollmentsAsync(Arg.Any(), Arg.Any()).Returns(pagedEnrollments); return stub; } private static IReviewService CreateReviewServiceStub() { var stub = Substitute.For(); var reviewDto = new ReviewDto(1, 1, "Lecture", 1, "User", ReviewRating.Like, "Great!", ReviewLlmStatus.Pending, null, null, null, null, DateTime.UtcNow); var pagedReviews = PagedResult.Create([reviewDto], 1, 1, 20); stub.CreateAsync(Arg.Any(), Arg.Any()).Returns(reviewDto); stub.GetByIdAsync(Arg.Any()).Returns(reviewDto); stub.UpdateAsync(Arg.Any(), Arg.Any(), Arg.Any()).Returns(reviewDto); stub.DeleteAsync(Arg.Any(), Arg.Any(), Arg.Any()).Returns(Task.CompletedTask); stub.GetByLectureAsync(Arg.Any(), Arg.Any()).Returns(pagedReviews); stub.GetByUserAsync(Arg.Any(), Arg.Any()).Returns(pagedReviews); stub.GetPendingAsync(Arg.Any()).Returns(pagedReviews); stub.ReanalyzeAsync(Arg.Any()).Returns(Task.CompletedTask); return stub; } private static ICourseService CreateCourseServiceStub() { var stub = Substitute.For(); var courseDto = new CourseDto(1, "Course", null, false, [], DateTime.UtcNow); var paged = PagedResult.Create([courseDto], 1, 1, 20); stub.GetAllAsync(Arg.Any()).Returns(paged); stub.GetByIdAsync(Arg.Any()).Returns(courseDto); stub.CreateAsync(Arg.Any()).Returns(courseDto); stub.UpdateAsync(Arg.Any(), Arg.Any()).Returns(courseDto); stub.DeleteAsync(Arg.Any()).Returns(Task.CompletedTask); stub.AddTagAsync(Arg.Any(), Arg.Any()).Returns(Task.CompletedTask); stub.RemoveTagAsync(Arg.Any(), Arg.Any()).Returns(Task.CompletedTask); return stub; } private static ITagService CreateTagServiceStub() { var stub = Substitute.For(); var tagDto = new TagDto(1, "Tag", TagType.Topic, null, DateTime.UtcNow); stub.GetAllAsync(Arg.Any(), Arg.Any()).Returns([tagDto]); stub.GetByIdAsync(Arg.Any()).Returns(tagDto); stub.CreateAsync(Arg.Any()).Returns(tagDto); stub.UpdateAsync(Arg.Any(), Arg.Any()).Returns(tagDto); stub.DeleteAsync(Arg.Any()).Returns(Task.CompletedTask); stub.GetTreeAsync().Returns(new List()); return stub; } private static ILocationService CreateLocationServiceStub() { var stub = Substitute.For(); var locationDto = new LocationDto(1, "Room 101", null, null, null, DateTime.UtcNow); stub.GetAllAsync().Returns([locationDto]); stub.GetByIdAsync(Arg.Any()).Returns(locationDto); stub.CreateAsync(Arg.Any()).Returns(locationDto); stub.UpdateAsync(Arg.Any(), Arg.Any()).Returns(locationDto); stub.DeleteAsync(Arg.Any()).Returns(Task.CompletedTask); return stub; } private static IAchievementService CreateAchievementServiceStub() { var stub = Substitute.For(); var achievementDto = new AchievementDto(1, "First Review", null, null, 10, 5, null, DateTime.UtcNow); stub.GetAllAsync().Returns([achievementDto]); stub.GetByIdAsync(Arg.Any()).Returns(achievementDto); stub.CreateAsync(Arg.Any()).Returns(achievementDto); stub.UpdateAsync(Arg.Any(), Arg.Any()).Returns(achievementDto); stub.DeleteAsync(Arg.Any()).Returns(Task.CompletedTask); return stub; } private static IGamificationService CreateGamificationServiceStub() { var stub = Substitute.For(); var paged = PagedResult.Create([], 0, 1, 20); stub.GetUserAchievementsAsync(Arg.Any()).Returns(new List()); stub.GetTransactionsAsync(Arg.Any(), Arg.Any()).Returns(paged); stub.AwardCoinsAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()).Returns(Task.CompletedTask); stub.CheckAndAwardAchievementsAsync(Arg.Any()).Returns(Task.CompletedTask); stub.CalculateLevel(Arg.Any()).Returns(1); return stub; } private static IScheduleSyncService CreateSyncServiceStub() { var stub = Substitute.For(); var syncResult = new SyncResultDto(0, 0, 0, null); var syncStatus = new SyncStatusDto(null, "idle", null); stub.SyncScheduleAsync(Arg.Any()).Returns(syncResult); stub.SyncRoomsAsync().Returns(syncResult); stub.SearchEmployeesAsync(Arg.Any()).Returns(new List()); stub.GetLastSyncStatusAsync().Returns(syncStatus); return stub; } }