295 lines
16 KiB
C#
295 lines
16 KiB
C#
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;
|
|
|
|
/// <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>());
|
|
ReplaceWithSubstitute<INotificationService>(services, CreateNotificationServiceStub());
|
|
});
|
|
}
|
|
|
|
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?>(), Arg.Any<string?>())
|
|
.Returns(authResult);
|
|
stub.DevLoginAsync(Arg.Any<string>(), Arg.Any<string?>(), Arg.Any<IReadOnlyCollection<UserRole>>(), Arg.Any<string?>())
|
|
.Returns(authResult);
|
|
stub.RefreshTokenAsync(Arg.Any<string>()).Returns(authResult);
|
|
stub.GetCurrentUserAsync(Arg.Any<int>())
|
|
.Returns(new CurrentUserDto(1, "test@test.com", "Test", null, [UserRole.Student], 0, 0, 1, DateTime.UtcNow));
|
|
return stub;
|
|
}
|
|
|
|
private static INotificationService CreateNotificationServiceStub()
|
|
{
|
|
var stub = Substitute.For<INotificationService>();
|
|
stub.SendAsync(Arg.Any<NotificationMessage>(), Arg.Any<CancellationToken>())
|
|
.Returns(Task.CompletedTask);
|
|
stub.ScheduleAsync(Arg.Any<ScheduleNotificationRequest>(), Arg.Any<CancellationToken>())
|
|
.Returns(new ScheduledNotificationResponse("test-job", DateTimeOffset.UtcNow.AddMinutes(5)));
|
|
stub.GetUserNotificationsAsync(Arg.Any<int>(), Arg.Any<PaginationRequest>(), Arg.Any<CancellationToken>())
|
|
.Returns(PagedResult<UserNotificationDto>.Create([], 0, 1, 20));
|
|
stub.MarkAllReadAsync(Arg.Any<int>(), Arg.Any<CancellationToken>())
|
|
.Returns(Task.CompletedTask);
|
|
stub.CreateUserNotificationAsync(
|
|
Arg.Any<int>(),
|
|
Arg.Any<string>(),
|
|
Arg.Any<string>(),
|
|
Arg.Any<string>(),
|
|
Arg.Any<CancellationToken>())
|
|
.Returns(new UserNotificationDto(1, "achievement", "Title", "Body", false, 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, 0, 100));
|
|
stub.GetAllAsync(Arg.Any<UserFilterRequest>()).Returns(pagedUsers);
|
|
stub.SetRolesAsync(Arg.Any<int>(), Arg.Any<IReadOnlyCollection<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>(), Arg.Any<int?>()).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.CalculateLevelAsync(Arg.Any<int>()).Returns(Task.FromResult(1));
|
|
stub.GetLevelProgressAsync(Arg.Any<int>()).Returns(Task.FromResult(new LevelProgressDto(0, 100)));
|
|
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;
|
|
}
|
|
}
|