Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 90 additions & 3 deletions BookLibrary.Tests/IntegrationTests/AuthorsControllerTests.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,93 @@
namespace BookLibrary.Tests.IntegrationTests;
using System.Net.Http.Json;
using BookLibrary.Data.Responses;
using BookLibrary.Models;

public class AuthorsControllerTests
namespace BookLibrary.Tests.IntegrationTests;

[Collection("TestContainers")]
[Trait("Category", "Integration")]
public class AuthorsControllerTests(BookLibraryTestContainersFactory factory)
{
private readonly HttpClient _client = factory.CreateClient();

// ========================================================================
// Тесты ListAuthorsController
// ========================================================================

[Fact]
public async Task GetAuthors_ReturnsListOfAuthors()
{
// Act
// Выполняем GET-запрос к эндпоинту с параметрами сортировки и пагинации
var response = await _client.GetAsync("/api/authors?sortBy=Name&PageSize=5");

// Assert
// Проверяем, что ответ успешный
response.EnsureSuccessStatusCode();

// Десериализуем JSON-ответ в общий DTO-обертку
var apiResponse = await response.Content.ReadFromJsonAsync<ApiResponseDto<List<Author>>>();

Assert.NotNull(apiResponse);
Assert.True(apiResponse.Success);
Assert.NotNull(apiResponse.Data);
Assert.NotEmpty(apiResponse.Data);

// Проверяем сообщение об успехе
Assert.Equal("Operation completed successfully", apiResponse.Message);

// Проверяем, что данные отсортированы по имени по возрастанию
for (int i = 0; i < apiResponse.Data.Count - 1; i++)
{
Assert.True(string.Compare(apiResponse.Data[i].Name, apiResponse.Data[i + 1].Name, StringComparison.Ordinal) <= 0);
}

// Проверяем, что размер страницы не превышает запрошенный
Assert.True(apiResponse.Data.Count <= 5);
}

// ========================================================================
// Тесты AuthorBooksController
// ========================================================================

[Fact]
public async Task GetAuthorBooks_ReturnsBooks_WithRelatedData()
{
// Arrange
// Сначала получаем список всех авторов, чтобы выбрать существующий ID
var listResponse = await _client.GetAsync("/api/authors");
listResponse.EnsureSuccessStatusCode();
var listApiResponse = await listResponse.Content.ReadFromJsonAsync<ApiResponseDto<List<Author>>>();
Assert.NotNull(listApiResponse?.Data);
Assert.NotEmpty(listApiResponse.Data);

}
var authorId = listApiResponse.Data.First().Id; // Берем ID первого автора

// Act
// Выполняем GET-запрос с флагами для подгрузки связанных данных
var response = await _client.GetAsync($"/api/authors/{authorId}/books?withAuthors=true&withCategories=true");

// Assert
response.EnsureSuccessStatusCode();
var apiResponse = await response.Content.ReadFromJsonAsync<ApiResponseDto<List<Book>>>();

Assert.NotNull(apiResponse);
Assert.True(apiResponse.Success);
Assert.NotNull(apiResponse.Data);

// Проверяем, что хотя бы одна книга была найдена
Assert.True(apiResponse.Data.Count > 0);

// Проверяем, что для каждой книги загружены связанные Author и Category
foreach (var book in apiResponse.Data)
{
Assert.NotNull(book.Author);
Assert.NotNull(book.Category);

// Проверяем, что идентификаторы совпадают
Assert.Equal(authorId, book.Author.Id);
Assert.True(!string.IsNullOrEmpty(book.Title));
Assert.True(book.PublicationYear > 1000);
}
}
}
112 changes: 112 additions & 0 deletions BookLibrary.Tests/IntegrationTests/BookLibraryTestContainersFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
using System.Text;
using BookLibrary.Database.Context;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Testcontainers.PostgreSql; // Работает ТОЛЬКО после установки Testcontainers.PostgreSql

namespace BookLibrary.Tests.IntegrationTests;

public class BookLibraryTestContainersFactory : WebApplicationFactory<Program>, IAsyncLifetime
{
private readonly PostgreSqlContainer _dbContainer = new PostgreSqlBuilder("postgres:15-alpine")
.WithDatabase("booklibrary_integration_test")
.WithUsername("testuser")
.WithPassword("SecureTestPass123!")
.WithCleanUp(true)
.Build();

// Для отладки: публичное свойство строки подключения
public string TestConnectionString => _dbContainer.GetConnectionString();

public async Task InitializeAsync()
{
// 🔤 Исправляем кодировку КОНСОЛИ
Console.OutputEncoding = Encoding.UTF8;
Console.InputEncoding = Encoding.UTF8;

// 🔇 Отключаем логи Testcontainers
Environment.SetEnvironmentVariable("TESTCONTAINERS_LOGGING_DISABLED", "true");

// 🔒 КРИТИЧЕСКАЯ ПРОВЕРКА: Защита от случайного подключения к боевой БД
if (Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT")?.ToLower() == "production")
{
throw new InvalidOperationException(
"❌ ЗАПРЕЩЕНО запускать интеграционные тесты в окружении Production! " +
"Установите переменную окружения ASPNETCORE_ENVIRONMENT=Test");
}

await _dbContainer.StartAsync();

Console.WriteLine($"✅ Тестовый контейнер запущен. ConnectionString: {TestConnectionString}");

// Применяем миграции ТОЛЬКО к тестовой БД в контейнере
using var scope = Services.CreateScope();
var dbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();

// Дополнительная проверка: убеждаемся, что подключены к контейнеру
var actualConnection = dbContext.Database.GetDbConnection().ConnectionString;
if (!actualConnection.Contains("booklibrary_integration_test"))
{
throw new InvalidOperationException(
$"❌ КРИТИЧЕСКАЯ ОШИБКА: Приложение подключено к НЕПРАВИЛЬНОЙ БД!\n" +
$"Подключено к: {actualConnection}\n" +
$"Ожидалось подключение к: {TestConnectionString}");
}

await dbContext.Database.MigrateAsync();
Console.WriteLine("✅ Миграции применены к тестовой БД");
}

public new async Task DisposeAsync()
{
Console.WriteLine("🧹 Очистка: остановка и удаление контейнера...");
await _dbContainer.DisposeAsync();
Console.WriteLine("✅ Контейнер удален. БД полностью уничтожена.");
}

protected override void ConfigureWebHost(IWebHostBuilder builder)
{
// 🔒 ШАГ 1: Явно устанавливаем окружение ТЕСТОВОЕ
Environment.SetEnvironmentVariable("ASPNETCORE_ENVIRONMENT", "Test");

// 🔒 ШАГ 2: Переопределяем строку подключения ЧЕРЕЗ ПЕРЕМЕННУЮ ОКРУЖЕНИЯ (высший приоритет!)
Environment.SetEnvironmentVariable("ConnectionStrings__DefaultConnection", _dbContainer.GetConnectionString());

// 🔒 ШАГ 3: Дополнительно переопределяем через конфигурацию
builder.ConfigureAppConfiguration((context, config) =>
{
config.AddInMemoryCollection(new Dictionary<string, string>
{
["ConnectionStrings:DefaultConnection"] = _dbContainer.GetConnectionString()
}!);
});

// 🔒 ШАГ 4: Заменяем контекст БД на тот, что использует строку из контейнера
builder.ConfigureServices(services =>
{
var descriptor = services.SingleOrDefault(
d => d.ServiceType == typeof(DbContextOptions<AppDbContext>));

if (descriptor != null)
services.Remove(descriptor);

services.AddDbContext<AppDbContext>(options =>
options.UseNpgsql(_dbContainer.GetConnectionString()));
});

// 🔒 ШАГ 5: 🔇 Отключаем логи приложения (только Critical)
builder.ConfigureLogging(logging =>
{
logging.ClearProviders();
logging.AddFilter("Microsoft", LogLevel.Critical);
logging.AddFilter("System", LogLevel.Critical);
logging.AddFilter("Testcontainers", LogLevel.None);
logging.AddConsole();
logging.SetMinimumLevel(LogLevel.Critical);
});
}
}
Loading