# Integration Test Rules / Quy Tắc Integration Testing

Hướng dẫn chi tiết cho việc viết Integration Tests với TestServer, EF Core, và Testcontainers.

## 1. Database Testing Strategies / Chiến Lược Test Database

### 1.1 In-Memory Database (Nhanh nhưng Hạn chế)

```csharp
/// <summary>
/// EN: In-memory database fixture for fast tests.
/// VI: In-memory database fixture cho tests nhanh.
/// </summary>
public class InMemoryDbFixture : IDisposable
{
    public ApplicationDbContext DbContext { get; }

    public InMemoryDbFixture()
    {
        var options = new DbContextOptionsBuilder<ApplicationDbContext>()
            .UseInMemoryDatabase($"TestDb_{Guid.NewGuid()}")
            .Options;

        DbContext = new ApplicationDbContext(options);
        DbContext.Database.EnsureCreated();
    }

    public void Dispose() => DbContext.Dispose();
}
```

**Khi nào dùng In-Memory:**
- ✅ Chạy quick tests trong CI/CD
- ✅ Test LINQ queries đơn giản
- ❌ KHÔNG dùng cho constraint testing (FK, unique)
- ❌ KHÔNG dùng cho stored procedures, raw SQL

### 1.2 Testcontainers (Chính xác cao)

```csharp
/// <summary>
/// EN: PostgreSQL container fixture using Testcontainers.
/// VI: Fixture container PostgreSQL dùng Testcontainers.
/// </summary>
public class PostgresTestFixture : IAsyncLifetime
{
    private readonly PostgreSqlContainer _container = new PostgreSqlBuilder()
        .WithImage("postgres:16-alpine")
        .WithDatabase("integration_tests")
        .WithUsername("test")
        .WithPassword("test123")
        .WithWaitStrategy(Wait.ForUnixContainer()
            .UntilPortIsAvailable(5432))
        .Build();

    public string ConnectionString => _container.GetConnectionString();

    public async Task InitializeAsync()
    {
        await _container.StartAsync();
    }

    public async Task DisposeAsync()
    {
        await _container.DisposeAsync();
    }
}
```

**Khi nào dùng Testcontainers:**
- ✅ Test database constraints (FK, unique, check)
- ✅ Test migrations
- ✅ Test stored procedures, raw SQL
- ✅ Production-like environment

### 1.3 SQL Server Container

```csharp
/// <summary>
/// EN: SQL Server container fixture.
/// VI: Fixture container SQL Server.
/// </summary>
public class SqlServerTestFixture : IAsyncLifetime
{
    private readonly MsSqlContainer _container = new MsSqlBuilder()
        .WithImage("mcr.microsoft.com/mssql/server:2022-latest")
        .WithPassword("YourStrong@Password123")
        .Build();

    public string ConnectionString => _container.GetConnectionString();

    public async Task InitializeAsync()
    {
        await _container.StartAsync();
    }

    public async Task DisposeAsync()
    {
        await _container.DisposeAsync();
    }
}
```

## 2. Test Data Management / Quản Lý Dữ Liệu Test

### 2.1 Seeding Test Data

```csharp
/// <summary>
/// EN: Database seeder for integration tests.
/// VI: Database seeder cho integration tests.
/// </summary>
public static class TestDataSeeder
{
    public static async Task SeedOrdersAsync(ApplicationDbContext context)
    {
        var orders = new[]
        {
            CreateOrder("user-1", OrderStatus.Draft),
            CreateOrder("user-1", OrderStatus.Submitted),
            CreateOrder("user-2", OrderStatus.Completed)
        };

        await context.Orders.AddRangeAsync(orders);
        await context.SaveChangesAsync();
    }

    private static Order CreateOrder(string userId, OrderStatus status)
    {
        var order = Order.Create(userId, CreateAddress());
        order.AddItem(Guid.NewGuid(), 2, 10.00m);
        
        if (status >= OrderStatus.Submitted)
            order.Submit();
        if (status >= OrderStatus.Completed)
            order.Complete();
            
        return order;
    }

    private static Address CreateAddress() =>
        Address.Create("123 St", "City", "State", "12345", "VN");
}
```

### 2.2 Transaction Rollback Pattern

```csharp
/// <summary>
/// EN: Transaction rollback for test isolation.
/// VI: Transaction rollback để cô lập test.
/// </summary>
public class TransactionalTestBase : IAsyncLifetime
{
    protected readonly ApplicationDbContext DbContext;
    private IDbContextTransaction? _transaction;

    public TransactionalTestBase(PostgresTestFixture fixture)
    {
        var options = new DbContextOptionsBuilder<ApplicationDbContext>()
            .UseNpgsql(fixture.ConnectionString)
            .Options;

        DbContext = new ApplicationDbContext(options);
    }

    public async Task InitializeAsync()
    {
        _transaction = await DbContext.Database.BeginTransactionAsync();
    }

    public async Task DisposeAsync()
    {
        if (_transaction != null)
        {
            await _transaction.RollbackAsync();
            await _transaction.DisposeAsync();
        }
        await DbContext.DisposeAsync();
    }
}
```

## 3. EF Core Integration Tests

### 3.1 Repository Testing

```csharp
/// <summary>
/// EN: Integration tests for OrderRepository.
/// VI: Integration tests cho OrderRepository.
/// </summary>
[Collection("Database")]
public class OrderRepositoryTests : IClassFixture<PostgresTestFixture>
{
    private readonly PostgresTestFixture _fixture;
    private readonly ApplicationDbContext _context;
    private readonly OrderRepository _repository;

    public OrderRepositoryTests(PostgresTestFixture fixture)
    {
        _fixture = fixture;
        
        var options = new DbContextOptionsBuilder<ApplicationDbContext>()
            .UseNpgsql(fixture.ConnectionString)
            .Options;

        _context = new ApplicationDbContext(options);
        _repository = new OrderRepository(_context);
    }

    [Fact]
    public async Task AddAsync_ValidOrder_PersistsToDatabase()
    {
        // Arrange
        var order = Order.Create("user-123", CreateAddress());
        order.AddItem(Guid.NewGuid(), 2, 10.00m);

        // Act
        await _repository.AddAsync(order);
        await _repository.UnitOfWork.SaveChangesAsync();

        // Assert - Fresh context to verify persistence
        await using var verifyContext = new ApplicationDbContext(GetDbOptions());
        var savedOrder = await verifyContext.Orders
            .Include(o => o.OrderItems)
            .FirstOrDefaultAsync(o => o.Id == order.Id);

        savedOrder.Should().NotBeNull();
        savedOrder!.OrderItems.Should().HaveCount(1);
        savedOrder.TotalAmount.Should().Be(20.00m);
    }

    [Fact]
    public async Task GetByUserIdAsync_ExistingOrders_ReturnsAllUserOrders()
    {
        // Arrange
        var userId = $"user-{Guid.NewGuid()}";
        var order1 = Order.Create(userId, CreateAddress());
        var order2 = Order.Create(userId, CreateAddress());
        var otherOrder = Order.Create("other-user", CreateAddress());

        await _context.Orders.AddRangeAsync(order1, order2, otherOrder);
        await _context.SaveChangesAsync();

        // Act
        var orders = await _repository.GetByUserIdAsync(userId);

        // Assert
        orders.Should().HaveCount(2);
        orders.Should().OnlyContain(o => o.UserId == userId);
    }

    private DbContextOptions<ApplicationDbContext> GetDbOptions() =>
        new DbContextOptionsBuilder<ApplicationDbContext>()
            .UseNpgsql(_fixture.ConnectionString)
            .Options;

    private static Address CreateAddress() =>
        Address.Create("123 St", "City", "State", "12345", "VN");
}
```

### 3.2 Testing Migrations

```csharp
/// <summary>
/// EN: Tests for database migrations.
/// VI: Tests cho database migrations.
/// </summary>
[Collection("Database")]
public class MigrationTests : IClassFixture<PostgresTestFixture>
{
    private readonly PostgresTestFixture _fixture;

    public MigrationTests(PostgresTestFixture fixture)
    {
        _fixture = fixture;
    }

    [Fact]
    public async Task Migrations_AllApplied_NoErrors()
    {
        // Arrange
        var options = new DbContextOptionsBuilder<ApplicationDbContext>()
            .UseNpgsql(_fixture.ConnectionString)
            .Options;

        await using var context = new ApplicationDbContext(options);

        // Act & Assert - Should not throw
        await context.Database.MigrateAsync();

        var pendingMigrations = await context.Database.GetPendingMigrationsAsync();
        pendingMigrations.Should().BeEmpty();
    }

    [Fact]
    public async Task Migration_UniqueConstraint_EnforcedProperly()
    {
        // Arrange
        var options = new DbContextOptionsBuilder<ApplicationDbContext>()
            .UseNpgsql(_fixture.ConnectionString)
            .Options;

        await using var context = new ApplicationDbContext(options);
        await context.Database.MigrateAsync();

        // Create user with unique email
        var user1 = new User { Email = "test@example.com", Name = "User 1" };
        var user2 = new User { Email = "test@example.com", Name = "User 2" }; // Duplicate!

        context.Users.Add(user1);
        await context.SaveChangesAsync();

        context.Users.Add(user2);

        // Act & Assert
        var act = async () => await context.SaveChangesAsync();
        await act.Should().ThrowAsync<DbUpdateException>();
    }
}
```

## 4. TestServer / WebApplicationFactory

### 4.1 Custom WebApplicationFactory

```csharp
/// <summary>
/// EN: Custom WebApplicationFactory for integration tests.
/// VI: Custom WebApplicationFactory cho integration tests.
/// </summary>
public class IntegrationTestFactory : WebApplicationFactory<Program>, IAsyncLifetime
{
    private readonly PostgreSqlContainer _dbContainer = new PostgreSqlBuilder()
        .WithImage("postgres:16-alpine")
        .WithDatabase("integration_tests")
        .Build();

    public async Task InitializeAsync()
    {
        await _dbContainer.StartAsync();
    }

    public new async Task DisposeAsync()
    {
        await _dbContainer.DisposeAsync();
        await base.DisposeAsync();
    }

    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        builder.ConfigureServices(services =>
        {
            // Remove real DbContext
            var descriptor = services.SingleOrDefault(
                d => d.ServiceType == typeof(DbContextOptions<ApplicationDbContext>));
            if (descriptor != null)
                services.Remove(descriptor);

            // Add test DbContext with real PostgreSQL
            services.AddDbContext<ApplicationDbContext>(options =>
                options.UseNpgsql(_dbContainer.GetConnectionString()));

            // Replace external services with mocks
            services.RemoveAll<IEmailService>();
            services.AddSingleton(Substitute.For<IEmailService>());

            // Apply migrations
            var sp = services.BuildServiceProvider();
            using var scope = sp.CreateScope();
            var db = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
            db.Database.Migrate();
        });

        builder.ConfigureTestServices(services =>
        {
            // Override authentication for testing
            services.AddAuthentication(TestAuthHandler.SchemeName)
                .AddScheme<AuthenticationSchemeOptions, TestAuthHandler>(
                    TestAuthHandler.SchemeName, _ => { });
        });
    }
}
```

### 4.2 Test Authentication Handler

```csharp
/// <summary>
/// EN: Test authentication handler for integration tests.
/// VI: Test authentication handler cho integration tests.
/// </summary>
public class TestAuthHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
    public const string SchemeName = "TestScheme";
    public const string TestUserId = "test-user-id";

    public TestAuthHandler(
        IOptionsMonitor<AuthenticationSchemeOptions> options,
        ILoggerFactory logger,
        UrlEncoder encoder,
        ISystemClock clock) : base(options, logger, encoder, clock)
    {
    }

    protected override Task<AuthenticateResult> HandleAuthenticateAsync()
    {
        var claims = new[]
        {
            new Claim(ClaimTypes.NameIdentifier, TestUserId),
            new Claim(ClaimTypes.Name, "Test User"),
            new Claim(ClaimTypes.Email, "test@example.com"),
            new Claim(ClaimTypes.Role, "User")
        };

        var identity = new ClaimsIdentity(claims, SchemeName);
        var principal = new ClaimsPrincipal(identity);
        var ticket = new AuthenticationTicket(principal, SchemeName);

        return Task.FromResult(AuthenticateResult.Success(ticket));
    }
}
```

### 4.3 API Integration Tests

```csharp
/// <summary>
/// EN: API integration tests with real database.
/// VI: API integration tests với database thật.
/// </summary>
public class OrdersApiIntegrationTests : IClassFixture<IntegrationTestFactory>
{
    private readonly HttpClient _client;
    private readonly IntegrationTestFactory _factory;

    public OrdersApiIntegrationTests(IntegrationTestFactory factory)
    {
        _factory = factory;
        _client = factory.CreateClient();
    }

    [Fact]
    public async Task CreateOrder_ValidRequest_PersistsToDatabase()
    {
        // Arrange
        var request = new CreateOrderRequest
        {
            ShippingAddress = new AddressDto("123 St", "City", "State", "12345", "VN"),
            Items = new[]
            {
                new OrderItemDto { ProductId = Guid.NewGuid(), Quantity = 2, UnitPrice = 10.00m }
            }
        };

        // Act
        var response = await _client.PostAsJsonAsync("/api/v1/orders", request);

        // Assert - Response
        response.StatusCode.Should().Be(HttpStatusCode.Created);
        var created = await response.Content.ReadFromJsonAsync<OrderCreatedResponse>();
        created!.OrderId.Should().NotBeEmpty();

        // Assert - Database persistence
        using var scope = _factory.Services.CreateScope();
        var db = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
        var order = await db.Orders.FindAsync(created.OrderId);
        order.Should().NotBeNull();
        order!.UserId.Should().Be(TestAuthHandler.TestUserId);
    }

    [Fact]
    public async Task GetOrder_ExistingOrder_ReturnsWithItems()
    {
        // Arrange - Seed data
        using (var scope = _factory.Services.CreateScope())
        {
            var db = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
            var order = Order.Create(TestAuthHandler.TestUserId, 
                Address.Create("123 St", "City", "State", "12345", "VN"));
            order.AddItem(Guid.NewGuid(), 2, 10.00m);
            db.Orders.Add(order);
            await db.SaveChangesAsync();
        }

        // Act
        var response = await _client.GetAsync("/api/v1/orders");

        // Assert
        response.StatusCode.Should().Be(HttpStatusCode.OK);
        var orders = await response.Content.ReadFromJsonAsync<List<OrderDto>>();
        orders.Should().NotBeEmpty();
        orders!.First().Items.Should().NotBeEmpty();
    }
}
```

## 5. External Service Integration

### 5.1 Testing with WireMock

```csharp
/// <summary>
/// EN: Tests using WireMock for external API simulation.
/// VI: Tests dùng WireMock để giả lập external API.
/// </summary>
public class PaymentServiceIntegrationTests : IDisposable
{
    private readonly WireMockServer _wireMock;
    private readonly HttpClient _httpClient;

    public PaymentServiceIntegrationTests()
    {
        _wireMock = WireMockServer.Start();
        _httpClient = new HttpClient { BaseAddress = new Uri(_wireMock.Urls[0]) };
    }

    [Fact]
    public async Task ProcessPayment_SuccessResponse_ReturnsPaymentId()
    {
        // Arrange - Setup WireMock
        _wireMock.Given(
            Request.Create()
                .WithPath("/payments")
                .UsingPost())
            .RespondWith(
                Response.Create()
                    .WithStatusCode(201)
                    .WithBodyAsJson(new { paymentId = "pay-123", status = "completed" }));

        var service = new PaymentService(_httpClient);

        // Act
        var result = await service.ProcessPaymentAsync(new PaymentRequest
        {
            Amount = 100.00m,
            Currency = "VND"
        });

        // Assert
        result.PaymentId.Should().Be("pay-123");
        result.Status.Should().Be("completed");
    }

    [Fact]
    public async Task ProcessPayment_Timeout_ThrowsPaymentException()
    {
        // Arrange - Simulate timeout
        _wireMock.Given(
            Request.Create()
                .WithPath("/payments")
                .UsingPost())
            .RespondWith(
                Response.Create()
                    .WithDelay(TimeSpan.FromSeconds(30)));

        var httpClient = new HttpClient(new HttpClientHandler())
        {
            BaseAddress = new Uri(_wireMock.Urls[0]),
            Timeout = TimeSpan.FromSeconds(1)
        };
        var service = new PaymentService(httpClient);

        // Act & Assert
        var act = async () => await service.ProcessPaymentAsync(new PaymentRequest());
        await act.Should().ThrowAsync<PaymentException>()
            .WithMessage("*timeout*");
    }

    public void Dispose() => _wireMock.Stop();
}
```

## 6. Best Practices / Thực Hành Tốt

### Collection Fixtures

Dùng Collection Fixtures để chia sẻ database container giữa nhiều test classes:

```csharp
// Define collection
[CollectionDefinition("Database")]
public class DatabaseCollection : ICollectionFixture<PostgresTestFixture>
{
}

// Use in test classes
[Collection("Database")]
public class OrderRepositoryTests { }

[Collection("Database")]  
public class UserRepositoryTests { }
```

### Test Isolation

```csharp
/// <summary>
/// EN: Ensure each test has fresh data.
/// VI: Đảm bảo mỗi test có dữ liệu riêng.
/// </summary>
public class IsolatedTestBase : IAsyncLifetime
{
    protected ApplicationDbContext DbContext { get; private set; } = null!;
    
    public async Task InitializeAsync()
    {
        // Create fresh database per test
        var dbName = $"test_{Guid.NewGuid():N}";
        // ... setup code
    }

    public async Task DisposeAsync()
    {
        // Cleanup
        await DbContext.Database.EnsureDeletedAsync();
        await DbContext.DisposeAsync();
    }
}
```

### Parallel Test Execution

```csharp
// xunit.runner.json - Control parallelism
{
  "parallelizeTestCollections": true,
  "maxParallelThreads": 4
}
```

## 7. Common Pitfalls / Lỗi Thường Gặp

### ❌ Shared State Between Tests

```csharp
// ❌ BAD: Tests affect each other
private static readonly ApplicationDbContext _sharedContext = CreateContext();

// ✅ GOOD: Fresh context per test
private ApplicationDbContext CreateContext() => new(GetOptions());
```

### ❌ Not Clearing Tracker

```csharp
// ❌ BAD: Cached entities affect assertions
var saved = await repository.GetByIdAsync(id);
// Context still tracking original entity!

// ✅ GOOD: Clear tracker or use new context
context.ChangeTracker.Clear();
var saved = await repository.GetByIdAsync(id);
```

### ❌ Testing Against Production Database

```csharp
// ❌ BAD: Connection string from production
var conn = Environment.GetEnvironmentVariable("PROD_DB_CONN");

// ✅ GOOD: Always use test containers or in-memory
var conn = _testContainer.GetConnectionString();
```
