feat: добавил интеграционные тесты
This commit is contained in:
@@ -0,0 +1,270 @@
|
||||
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.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;
|
||||
|
||||
/// <summary>
|
||||
/// WebApplicationFactory для интеграционных тестов.
|
||||
/// Заменяет Npgsql DbContext на InMemory, создает заглушки для всех интерфейсов внешних сервисов
|
||||
/// и отключает фоновую службу LLM, чтобы тестам не требовалась реальная инфраструктура.
|
||||
/// </summary>
|
||||
public class ApiWebApplicationFactory : WebApplicationFactory<Program>
|
||||
{
|
||||
protected override void ConfigureWebHost(IWebHostBuilder builder)
|
||||
{
|
||||
// Используем Development, чтобы были включены Swagger и конечная точка DevLogin
|
||||
builder.UseEnvironment("Development");
|
||||
|
||||
builder.ConfigureAppConfiguration((_, config) =>
|
||||
{
|
||||
// Внедряем настройки тестового JWT — должны совпадать с константами TestJwtFactory
|
||||
var testSettings = new Dictionary<string, string?>
|
||||
{
|
||||
["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<DbContextOptions<AppDbContext>>();
|
||||
services.RemoveAll<AppDbContext>();
|
||||
|
||||
// Удаляем все регистрации, связанные с DbContext, которые добавил хост
|
||||
var descriptor = services.SingleOrDefault(
|
||||
d => d.ServiceType == typeof(DbContextOptions<AppDbContext>));
|
||||
if (descriptor != null) services.Remove(descriptor);
|
||||
|
||||
// Находим и удаляем все дескрипторы настроек DbContext
|
||||
var dbContextDescriptors = services
|
||||
.Where(d => d.ServiceType == typeof(DbContextOptions<AppDbContext>)
|
||||
|| d.ImplementationType == typeof(AppDbContext))
|
||||
.ToList();
|
||||
foreach (var d in dbContextDescriptors) services.Remove(d);
|
||||
|
||||
services.AddDbContext<AppDbContext>(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<IAuthService>(services, CreateAuthServiceStub());
|
||||
ReplaceWithSubstitute<IUserService>(services, CreateUserServiceStub());
|
||||
ReplaceWithSubstitute<ILectureService>(services, CreateLectureServiceStub());
|
||||
ReplaceWithSubstitute<IReviewService>(services, CreateReviewServiceStub());
|
||||
ReplaceWithSubstitute<ICourseService>(services, CreateCourseServiceStub());
|
||||
ReplaceWithSubstitute<ITagService>(services, CreateTagServiceStub());
|
||||
ReplaceWithSubstitute<ILocationService>(services, CreateLocationServiceStub());
|
||||
ReplaceWithSubstitute<IAchievementService>(services, CreateAchievementServiceStub());
|
||||
ReplaceWithSubstitute<IGamificationService>(services, CreateGamificationServiceStub());
|
||||
ReplaceWithSubstitute<IScheduleSyncService>(services, CreateSyncServiceStub());
|
||||
ReplaceWithSubstitute<ILlmAnalysisService>(services, Substitute.For<ILlmAnalysisService>());
|
||||
ReplaceWithSubstitute<ILlmClient>(services, Substitute.For<ILlmClient>());
|
||||
});
|
||||
}
|
||||
|
||||
private static void ReplaceWithSubstitute<TService>(IServiceCollection services, TService instance)
|
||||
where TService : class
|
||||
{
|
||||
services.RemoveAll<TService>();
|
||||
services.AddScoped<TService>(_ => instance);
|
||||
}
|
||||
|
||||
// ── Фабрики заглушек ────────────────────────────────────────────────────────────
|
||||
|
||||
private static IAuthService CreateAuthServiceStub()
|
||||
{
|
||||
var stub = Substitute.For<IAuthService>();
|
||||
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<string>(), Arg.Any<string?>())
|
||||
.Returns(authResult);
|
||||
stub.DevLoginAsync(Arg.Any<string>(), Arg.Any<string?>(), Arg.Any<UserRole>())
|
||||
.Returns(authResult);
|
||||
stub.RefreshTokenAsync(Arg.Any<string>()).Returns(authResult);
|
||||
stub.GetCurrentUserAsync(Arg.Any<int>())
|
||||
.Returns(new UserDto(1, "test@test.com", "Test", null, UserRole.Student, true, 0, 0, 1, DateTime.UtcNow));
|
||||
return stub;
|
||||
}
|
||||
|
||||
private static IUserService CreateUserServiceStub()
|
||||
{
|
||||
var stub = Substitute.For<IUserService>();
|
||||
var userDto = new UserDto(1, "test@test.com", "Test", null, UserRole.Student, true, 0, 0, 1, DateTime.UtcNow);
|
||||
var pagedUsers = PagedResult<UserDto>.Create([userDto], 1, 1, 20);
|
||||
|
||||
stub.GetByIdAsync(Arg.Any<int>()).Returns(userDto);
|
||||
stub.UpdateProfileAsync(Arg.Any<int>(), Arg.Any<UpdateUserRequest>()).Returns(userDto);
|
||||
stub.GetStatsAsync(Arg.Any<int>()).Returns(new UserStatsDto(0, 0, 0, 0, 0, 1, 0));
|
||||
stub.GetAllAsync(Arg.Any<UserFilterRequest>()).Returns(pagedUsers);
|
||||
stub.SetRoleAsync(Arg.Any<int>(), Arg.Any<UserRole>()).Returns(Task.CompletedTask);
|
||||
stub.SetActiveAsync(Arg.Any<int>(), Arg.Any<bool>()).Returns(Task.CompletedTask);
|
||||
return stub;
|
||||
}
|
||||
|
||||
private static ILectureService CreateLectureServiceStub()
|
||||
{
|
||||
var stub = Substitute.For<ILectureService>();
|
||||
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<LectureDto>.Create([lectureDto], 1, 1, 20);
|
||||
var pagedEnrollments = PagedResult<EnrollmentDto>.Create([], 0, 1, 20);
|
||||
|
||||
stub.GetAllAsync(Arg.Any<LectureFilterRequest>()).Returns(pagedLectures);
|
||||
stub.GetByIdAsync(Arg.Any<int>(), Arg.Any<int?>()).Returns(detailDto);
|
||||
stub.CreateAsync(Arg.Any<CreateLectureRequest>()).Returns(lectureDto);
|
||||
stub.UpdateAsync(Arg.Any<int>(), Arg.Any<UpdateLectureRequest>()).Returns(lectureDto);
|
||||
stub.DeleteAsync(Arg.Any<int>()).Returns(Task.CompletedTask);
|
||||
stub.EnrollAsync(Arg.Any<int>(), Arg.Any<int>()).Returns(Task.CompletedTask);
|
||||
stub.UnenrollAsync(Arg.Any<int>(), Arg.Any<int>()).Returns(Task.CompletedTask);
|
||||
stub.MarkAttendanceAsync(Arg.Any<int>(), Arg.Any<int>(), Arg.Any<bool>()).Returns(Task.CompletedTask);
|
||||
stub.GetEnrollmentsAsync(Arg.Any<int>(), Arg.Any<PaginationRequest>()).Returns(pagedEnrollments);
|
||||
return stub;
|
||||
}
|
||||
|
||||
private static IReviewService CreateReviewServiceStub()
|
||||
{
|
||||
var stub = Substitute.For<IReviewService>();
|
||||
var reviewDto = new ReviewDto(1, 1, "Lecture", 1, "User",
|
||||
ReviewRating.Like, "Great!", ReviewLlmStatus.Pending,
|
||||
null, null, null, null, DateTime.UtcNow);
|
||||
var pagedReviews = PagedResult<ReviewDto>.Create([reviewDto], 1, 1, 20);
|
||||
|
||||
stub.CreateAsync(Arg.Any<int>(), Arg.Any<CreateReviewRequest>()).Returns(reviewDto);
|
||||
stub.GetByIdAsync(Arg.Any<int>()).Returns(reviewDto);
|
||||
stub.UpdateAsync(Arg.Any<int>(), Arg.Any<int>(), Arg.Any<UpdateReviewRequest>()).Returns(reviewDto);
|
||||
stub.DeleteAsync(Arg.Any<int>(), Arg.Any<int>(), Arg.Any<bool>()).Returns(Task.CompletedTask);
|
||||
stub.GetByLectureAsync(Arg.Any<int>(), Arg.Any<PaginationRequest>()).Returns(pagedReviews);
|
||||
stub.GetByUserAsync(Arg.Any<int>(), Arg.Any<PaginationRequest>()).Returns(pagedReviews);
|
||||
stub.GetPendingAsync(Arg.Any<PaginationRequest>()).Returns(pagedReviews);
|
||||
stub.ReanalyzeAsync(Arg.Any<int>()).Returns(Task.CompletedTask);
|
||||
return stub;
|
||||
}
|
||||
|
||||
private static ICourseService CreateCourseServiceStub()
|
||||
{
|
||||
var stub = Substitute.For<ICourseService>();
|
||||
var courseDto = new CourseDto(1, "Course", null, false, [], DateTime.UtcNow);
|
||||
var paged = PagedResult<CourseDto>.Create([courseDto], 1, 1, 20);
|
||||
|
||||
stub.GetAllAsync(Arg.Any<CourseFilterRequest>()).Returns(paged);
|
||||
stub.GetByIdAsync(Arg.Any<int>()).Returns(courseDto);
|
||||
stub.CreateAsync(Arg.Any<CreateCourseRequest>()).Returns(courseDto);
|
||||
stub.UpdateAsync(Arg.Any<int>(), Arg.Any<UpdateCourseRequest>()).Returns(courseDto);
|
||||
stub.DeleteAsync(Arg.Any<int>()).Returns(Task.CompletedTask);
|
||||
stub.AddTagAsync(Arg.Any<int>(), Arg.Any<int>()).Returns(Task.CompletedTask);
|
||||
stub.RemoveTagAsync(Arg.Any<int>(), Arg.Any<int>()).Returns(Task.CompletedTask);
|
||||
return stub;
|
||||
}
|
||||
|
||||
private static ITagService CreateTagServiceStub()
|
||||
{
|
||||
var stub = Substitute.For<ITagService>();
|
||||
var tagDto = new TagDto(1, "Tag", TagType.Topic, null, DateTime.UtcNow);
|
||||
|
||||
stub.GetAllAsync(Arg.Any<TagType?>(), Arg.Any<int?>()).Returns([tagDto]);
|
||||
stub.GetByIdAsync(Arg.Any<int>()).Returns(tagDto);
|
||||
stub.CreateAsync(Arg.Any<CreateTagRequest>()).Returns(tagDto);
|
||||
stub.UpdateAsync(Arg.Any<int>(), Arg.Any<UpdateTagRequest>()).Returns(tagDto);
|
||||
stub.DeleteAsync(Arg.Any<int>()).Returns(Task.CompletedTask);
|
||||
stub.GetTreeAsync().Returns(new List<TagTreeDto>());
|
||||
return stub;
|
||||
}
|
||||
|
||||
private static ILocationService CreateLocationServiceStub()
|
||||
{
|
||||
var stub = Substitute.For<ILocationService>();
|
||||
var locationDto = new LocationDto(1, "Room 101", null, null, null, DateTime.UtcNow);
|
||||
|
||||
stub.GetAllAsync().Returns([locationDto]);
|
||||
stub.GetByIdAsync(Arg.Any<int>()).Returns(locationDto);
|
||||
stub.CreateAsync(Arg.Any<CreateLocationRequest>()).Returns(locationDto);
|
||||
stub.UpdateAsync(Arg.Any<int>(), Arg.Any<UpdateLocationRequest>()).Returns(locationDto);
|
||||
stub.DeleteAsync(Arg.Any<int>()).Returns(Task.CompletedTask);
|
||||
return stub;
|
||||
}
|
||||
|
||||
private static IAchievementService CreateAchievementServiceStub()
|
||||
{
|
||||
var stub = Substitute.For<IAchievementService>();
|
||||
var achievementDto = new AchievementDto(1, "First Review", null, null, 10, 5, null, DateTime.UtcNow);
|
||||
|
||||
stub.GetAllAsync().Returns([achievementDto]);
|
||||
stub.GetByIdAsync(Arg.Any<int>()).Returns(achievementDto);
|
||||
stub.CreateAsync(Arg.Any<CreateAchievementRequest>()).Returns(achievementDto);
|
||||
stub.UpdateAsync(Arg.Any<int>(), Arg.Any<UpdateAchievementRequest>()).Returns(achievementDto);
|
||||
stub.DeleteAsync(Arg.Any<int>()).Returns(Task.CompletedTask);
|
||||
return stub;
|
||||
}
|
||||
|
||||
private static IGamificationService CreateGamificationServiceStub()
|
||||
{
|
||||
var stub = Substitute.For<IGamificationService>();
|
||||
var paged = PagedResult<CoinTransactionDto>.Create([], 0, 1, 20);
|
||||
|
||||
stub.GetUserAchievementsAsync(Arg.Any<int>()).Returns(new List<UserAchievementDto>());
|
||||
stub.GetTransactionsAsync(Arg.Any<int>(), Arg.Any<PaginationRequest>()).Returns(paged);
|
||||
stub.AwardCoinsAsync(Arg.Any<int>(), Arg.Any<int>(), Arg.Any<CoinTransactionType>(),
|
||||
Arg.Any<int?>(), Arg.Any<int?>(), Arg.Any<string?>()).Returns(Task.CompletedTask);
|
||||
stub.CheckAndAwardAchievementsAsync(Arg.Any<int>()).Returns(Task.CompletedTask);
|
||||
stub.CalculateLevel(Arg.Any<int>()).Returns(1);
|
||||
return stub;
|
||||
}
|
||||
|
||||
private static IScheduleSyncService CreateSyncServiceStub()
|
||||
{
|
||||
var stub = Substitute.For<IScheduleSyncService>();
|
||||
var syncResult = new SyncResultDto(0, 0, 0, null);
|
||||
var syncStatus = new SyncStatusDto(null, "idle", null);
|
||||
|
||||
stub.SyncScheduleAsync(Arg.Any<SyncScheduleRequest>()).Returns(syncResult);
|
||||
stub.SyncRoomsAsync().Returns(syncResult);
|
||||
stub.SearchEmployeesAsync(Arg.Any<string>()).Returns(new List<EmployeeDto>());
|
||||
stub.GetLastSyncStatusAsync().Returns(syncStatus);
|
||||
return stub;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user