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, 0, 100));
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.CalculateLevelAsync(Arg.Any()).Returns(Task.FromResult(1));
stub.GetLevelProgressAsync(Arg.Any()).Returns(Task.FromResult(new LevelProgressDto(0, 100)));
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;
}
}