// EN: Custom WebApplicationFactory for functional tests with full dependency mocking.
// VI: WebApplicationFactory tùy chỉnh cho functional tests với mock đầy đủ dependencies.
using System.Data;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.AspNetCore.SignalR;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using OrderService.API.Hubs;
using OrderService.API.Infrastructure.Tenant;
using OrderService.Domain.AggregatesModel.OrderAggregate;
using OrderService.Domain.Strategies;
using OrderService.Infrastructure;
using OrderService.Infrastructure.ExternalServices;
using Serilog;
namespace OrderService.FunctionalTests;
///
/// EN: Custom WebApplicationFactory for functional tests.
/// Replaces PostgreSQL with InMemory database, mocks external services,
/// and provides test authentication.
/// VI: WebApplicationFactory tùy chỉnh cho functional tests.
/// Thay thế PostgreSQL bằng InMemory database, mock external services,
/// và cung cấp test authentication.
///
public class CustomWebApplicationFactory : WebApplicationFactory
{
///
/// EN: Unique database name per factory instance to isolate test data.
/// VI: Tên database duy nhất cho mỗi factory instance để cách ly dữ liệu test.
///
private readonly string _databaseName = $"TestDatabase_{Guid.NewGuid()}";
public CustomWebApplicationFactory()
{
// EN: Reset Serilog static logger before host creation to prevent "logger is already frozen".
// Program.cs calls CreateBootstrapLogger() which creates a ReloadableLogger.
// UseSerilog() then freezes it. On subsequent factory instances in the same
// test process, Log.Logger is already frozen. Reset it to a fresh bootstrap logger.
// VI: Reset Serilog static logger trước khi tạo host để ngăn "logger is already frozen".
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Warning()
.WriteTo.Console()
.CreateBootstrapLogger();
}
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.UseEnvironment("Testing");
builder.ConfigureServices(services =>
{
// ============================================================
// EN: Replace DbContext with InMemory database.
// Must remove ALL EF Core / Npgsql service registrations
// to avoid "multiple database providers" error.
// VI: Thay thế DbContext bằng InMemory database.
// Phải xóa TẤT CẢ đăng ký EF Core / Npgsql services
// để tránh lỗi "multiple database providers".
// ============================================================
var descriptorsToRemove = services
.Where(d =>
d.ServiceType == typeof(DbContextOptions) ||
d.ServiceType == typeof(OrderContext) ||
d.ServiceType.FullName?.Contains("Npgsql") == true ||
d.ServiceType.FullName?.Contains("RelationalConnection") == true ||
(d.ServiceType.IsGenericType &&
d.ServiceType.GetGenericTypeDefinition().FullName?.Contains("DbContextOptions") == true))
.ToList();
foreach (var descriptor in descriptorsToRemove)
{
services.Remove(descriptor);
}
// EN: Also remove the generic DbContextOptions registration
// VI: Cũng xóa đăng ký DbContextOptions generic
services.RemoveAll>();
services.AddDbContext(options =>
{
options.UseInMemoryDatabase(_databaseName);
});
// ============================================================
// EN: Replace IDbConnection (Dapper) with a no-op to avoid PostgreSQL dependency.
// Queries that use Dapper with raw SQL won't work with InMemory,
// so we provide a mock connection that returns empty results.
// VI: Thay thế IDbConnection (Dapper) để tránh phụ thuộc PostgreSQL.
// Queries dùng Dapper với raw SQL không hoạt động với InMemory,
// nên cung cấp mock connection trả về kết quả rỗng.
// ============================================================
services.RemoveAll();
services.AddTransient(_ => new MockDbConnection());
// ============================================================
// EN: Mock external services
// VI: Mock external services
// ============================================================
// EN: Mock IWalletServiceClient — return success for all payment requests
// VI: Mock IWalletServiceClient — trả về thành công cho tất cả yêu cầu thanh toán
services.RemoveAll();
services.AddSingleton();
// EN: Mock IPosNotificationService — no-op (skip SignalR)
// VI: Mock IPosNotificationService — no-op (bỏ qua SignalR)
services.RemoveAll();
services.AddSingleton();
// EN: Mock ILineItemStrategy — always return valid for all product types
// VI: Mock ILineItemStrategy — luôn trả về hợp lệ cho tất cả loại sản phẩm
services.RemoveAll();
services.AddTransient(_ => new MockLineItemStrategy("Physical"));
services.AddTransient(_ => new MockLineItemStrategy("Service"));
services.AddTransient(_ => new MockLineItemStrategy("PreparedFood"));
// EN: Mock IOrderTenantProvider — bypass tenant filtering in tests
// VI: Mock IOrderTenantProvider — bỏ qua tenant filtering trong tests
services.RemoveAll();
services.AddSingleton();
services.RemoveAll();
services.AddSingleton();
// EN: Remove TransactionBehavior — InMemory database does not support transactions.
// The ExecutionStrategy from InMemory throws NotSupportedException.
// VI: Xóa TransactionBehavior — InMemory database không hỗ trợ transactions.
var transactionBehaviors = services
.Where(d => d.ServiceType.IsGenericType &&
d.ServiceType.GetGenericTypeDefinition() == typeof(MediatR.IPipelineBehavior<,>) &&
d.ImplementationType?.Name?.Contains("TransactionBehavior") == true)
.ToList();
foreach (var descriptor in transactionBehaviors)
{
services.Remove(descriptor);
}
// EN: Note: IUserIdProvider is registered by SignalR and needed at runtime.
// ClaimsUserIdProvider is already registered in Program.cs, so no action needed.
// VI: IUserIdProvider đã được đăng ký bởi SignalR và cần tại runtime.
// ClaimsUserIdProvider đã đăng ký trong Program.cs, không cần thao tác.
// ============================================================
// EN: Replace authentication with test scheme
// VI: Thay thế authentication bằng test scheme
// ============================================================
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = TestAuthHandler.SchemeName;
options.DefaultChallengeScheme = TestAuthHandler.SchemeName;
})
.AddScheme(
TestAuthHandler.SchemeName, _ => { });
});
}
///
/// EN: Seed database after host is created but before tests run.
/// VI: Seed database sau khi host được tạo nhưng trước khi tests chạy.
///
protected override IHost CreateHost(IHostBuilder builder)
{
var host = base.CreateHost(builder);
// EN: Seed the InMemory database with OrderStatus enum data
// VI: Seed InMemory database với OrderStatus enum data
using var scope = host.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService();
db.Database.EnsureCreated();
return host;
}
}
// ============================================================
// EN: Mock implementations
// VI: Mock implementations
// ============================================================
///
/// EN: Mock wallet service client that always succeeds.
/// VI: Mock wallet service client luôn thành công.
///
internal class MockWalletServiceClient : IWalletServiceClient
{
public Task CreatePaymentAsync(
Guid orderId, decimal amount, string gateway,
string returnUrl, string ipAddress,
CancellationToken cancellationToken = default)
{
return Task.FromResult(
new CreatePaymentResponse(
TransactionId: $"MOCK-{Guid.NewGuid():N}",
PaymentUrl: $"https://mock-gateway.test/pay?txn=MOCK-{orderId}",
Status: "Pending"));
}
}
///
/// EN: Mock POS notification service — no-op.
/// VI: Mock POS notification service — no-op.
///
internal class MockPosNotificationService : IPosNotificationService
{
public Task NotifyOrderCreatedAsync(Guid shopId, OrderNotificationDto order, CancellationToken cancellationToken = default)
=> Task.CompletedTask;
public Task NotifyOrderUpdatedAsync(Guid shopId, OrderNotificationDto order, CancellationToken cancellationToken = default)
=> Task.CompletedTask;
public Task NotifyOrderStatusChangedAsync(Guid shopId, Guid orderId, string oldStatus, string newStatus, CancellationToken cancellationToken = default)
=> Task.CompletedTask;
public Task NotifyKitchenTicketCreatedAsync(Guid shopId, KitchenTicketNotificationDto ticket, CancellationToken cancellationToken = default)
=> Task.CompletedTask;
public Task NotifyKitchenTicketUpdatedAsync(Guid shopId, KitchenTicketNotificationDto ticket, CancellationToken cancellationToken = default)
=> Task.CompletedTask;
public Task NotifyPaymentCompletedAsync(Guid shopId, PaymentNotificationDto payment, CancellationToken cancellationToken = default)
=> Task.CompletedTask;
public Task NotifyTableStatusChangedAsync(Guid shopId, TableStatusNotificationDto tableStatus, CancellationToken cancellationToken = default)
=> Task.CompletedTask;
}
///
/// EN: Mock line item strategy — always validates and executes successfully.
/// VI: Mock line item strategy — luôn validate và execute thành công.
///
internal class MockLineItemStrategy : ILineItemStrategy
{
public string SupportedType { get; }
public MockLineItemStrategy(string supportedType)
{
SupportedType = supportedType;
}
public Task ValidateAsync(OrderItem item, Guid shopId, CancellationToken cancellationToken = default)
=> Task.FromResult(true);
public Task ExecuteAsync(OrderItem item, Guid shopId, CancellationToken cancellationToken = default)
=> Task.CompletedTask;
}
///
/// EN: Mock tenant provider for Infrastructure layer — bypasses tenant filtering.
/// VI: Mock tenant provider cho lớp Infrastructure — bỏ qua tenant filtering.
///
internal class MockTenantProvider : IOrderTenantProvider
{
public Guid? GetCurrentShopId() => null;
public bool ShouldBypassTenantFilter() => true;
}
///
/// EN: Mock API tenant provider — returns test tenant IDs.
/// VI: Mock API tenant provider — trả về test tenant IDs.
///
internal class MockApiTenantProvider : ITenantProvider
{
public Guid? GetCurrentUserId() => TestAuthHandler.TestUserId;
public Guid? GetCurrentMerchantId() => TestAuthHandler.TestMerchantId;
public Guid? GetCurrentShopId() => TestAuthHandler.TestShopId;
public bool IsServiceCall() => false;
public bool IsAdmin() => true;
}
#pragma warning disable CS8767 // Nullability of reference types in interface implementations (mock objects)
///
/// EN: Mock IDbConnection for Dapper queries — returns empty results.
/// Since Dapper queries use raw SQL with PostgreSQL-specific joins,
/// they cannot work with InMemory database. This mock prevents
/// connection errors while allowing command-side tests to pass.
/// VI: Mock IDbConnection cho Dapper queries — trả về kết quả rỗng.
/// Vì Dapper queries dùng raw SQL với PostgreSQL-specific joins,
/// chúng không hoạt động với InMemory database. Mock này ngăn
/// connection errors trong khi cho phép command-side tests pass.
///
internal class MockDbConnection : IDbConnection
{
public string ConnectionString { get; set; } = string.Empty;
public int ConnectionTimeout => 30;
public string Database => "test";
public ConnectionState State => ConnectionState.Open;
public IDbTransaction BeginTransaction() => new MockDbTransaction(this);
public IDbTransaction BeginTransaction(IsolationLevel il) => new MockDbTransaction(this);
public void ChangeDatabase(string databaseName) { }
public void Close() { }
public IDbCommand CreateCommand() => new MockDbCommand();
public void Dispose() { }
public void Open() { }
}
internal class MockDbTransaction : IDbTransaction
{
public IDbConnection Connection { get; }
public IsolationLevel IsolationLevel => IsolationLevel.ReadCommitted;
public MockDbTransaction(IDbConnection connection)
{
Connection = connection;
}
public void Commit() { }
public void Rollback() { }
public void Dispose() { }
}
internal class MockDbCommand : IDbCommand
{
public string CommandText { get; set; } = string.Empty;
public int CommandTimeout { get; set; } = 30;
public CommandType CommandType { get; set; } = CommandType.Text;
public IDbConnection? Connection { get; set; }
public IDataParameterCollection Parameters => new MockParameterCollection();
public IDbTransaction? Transaction { get; set; }
public UpdateRowSource UpdatedRowSource { get; set; } = UpdateRowSource.None;
public void Cancel() { }
public IDbDataParameter CreateParameter() => new MockDbDataParameter();
public void Dispose() { }
public int ExecuteNonQuery() => 0;
public IDataReader ExecuteReader() => new MockDataReader();
public IDataReader ExecuteReader(CommandBehavior behavior) => new MockDataReader();
public object? ExecuteScalar() => 0;
public void Prepare() { }
}
internal class MockDbDataParameter : IDbDataParameter
{
public DbType DbType { get; set; }
public ParameterDirection Direction { get; set; }
public bool IsNullable => true;
public string ParameterName { get; set; } = string.Empty;
public string SourceColumn { get; set; } = string.Empty;
public DataRowVersion SourceVersion { get; set; }
public object? Value { get; set; }
public byte Precision { get; set; }
public byte Scale { get; set; }
public int Size { get; set; }
}
internal class MockParameterCollection : IDataParameterCollection
{
private readonly List