using System.Collections.Concurrent; using FluentAssertions; using Microsoft.EntityFrameworkCore; using Xunit; using YarpGateway.Data; using YarpGateway.DynamicProxy; using YarpGateway.Services; using Fengling.Platform.Domain.AggregatesModel.GatewayAggregate; namespace YarpGateway.Tests.Integration; /// /// 网关配置热重载集成测试 /// [Collection("Integration Tests")] public class ConfigReloadTests : IDisposable { private readonly TestFixture _fixture; private readonly GatewayDbContext _dbContext; public ConfigReloadTests(TestFixture fixture) { _fixture = fixture; _dbContext = _fixture.CreateDbContext(); } public void Dispose() { _dbContext.Dispose(); } #region 配置重载测试 [Fact] public async Task WhenConfigChanged_ReloadAsync_ShouldUpdateRouteCache() { // Arrange: 获取初始路由数量 var routeCache = _fixture.GetRouteCache(); var initialRoute = routeCache.GetRoute("test-tenant", "reload-test"); initialRoute.Should().BeNull(); // 初始时不存在 // 添加新路由 var newRoute = new GwTenantRoute { Id = Guid.CreateVersion7().ToString("N"), TenantCode = "test-tenant", ServiceName = "reload-test", ClusterId = "member-cluster", Match = new GwRouteMatch { Path = "/api/reload-test/**" }, Priority = 1, Status = 1, IsGlobal = false, CreatedTime = DateTime.UtcNow }; _dbContext.GwTenantRoutes.Add(newRoute); await _dbContext.SaveChangesAsync(); // Act: 重新加载配置 await routeCache.ReloadAsync(); // Assert: 应该能找到新路由 var reloadedRoute = routeCache.GetRoute("test-tenant", "reload-test"); reloadedRoute.Should().NotBeNull(); reloadedRoute!.ClusterId.Should().Be("member-cluster"); // 清理 _dbContext.GwTenantRoutes.Remove(newRoute); await _dbContext.SaveChangesAsync(); } [Fact] public async Task WhenConfigChanged_ReloadAsync_ShouldUpdateProxyConfig() { // Arrange var configProvider = _fixture.GetConfigProvider(); var initialConfig = configProvider.GetConfig(); var initialRouteCount = initialConfig.Routes.Count; // 添加新路由 var newRoute = new GwTenantRoute { Id = Guid.CreateVersion7().ToString("N"), TenantCode = "", ServiceName = "proxy-reload-test", ClusterId = "member-cluster", Match = new GwRouteMatch { Path = "/api/proxy-reload/**" }, Priority = 1, Status = 1, IsGlobal = true, CreatedTime = DateTime.UtcNow }; _dbContext.GwTenantRoutes.Add(newRoute); await _dbContext.SaveChangesAsync(); // 重新加载 RouteCache var routeCache = _fixture.GetRouteCache(); await routeCache.ReloadAsync(); // Act: 更新代理配置 configProvider.UpdateConfig(); // Assert var newConfig = configProvider.GetConfig(); newConfig.Routes.Count.Should().BeGreaterThanOrEqualTo(initialRouteCount); // 验证可以通过服务名找到路由 var route = newConfig.Routes.FirstOrDefault(r => r.Metadata?.ContainsKey("ServiceName") == true && r.Metadata["ServiceName"] == "proxy-reload-test"); route.Should().NotBeNull(); // 清理 _dbContext.GwTenantRoutes.Remove(newRoute); await _dbContext.SaveChangesAsync(); } [Fact] public async Task WhenRouteDisabled_Reload_ShouldRemoveFromCache() { // Arrange: 添加并确认路由存在 var routeId = Guid.CreateVersion7().ToString("N"); var newRoute = new GwTenantRoute { Id = routeId, TenantCode = "disable-test", ServiceName = "disable-test-service", ClusterId = "member-cluster", Match = new GwRouteMatch { Path = "/api/disable/**" }, Priority = 1, Status = 1, IsGlobal = false, CreatedTime = DateTime.UtcNow }; _dbContext.GwTenantRoutes.Add(newRoute); await _dbContext.SaveChangesAsync(); var routeCache = _fixture.GetRouteCache(); await routeCache.ReloadAsync(); // 确认路由存在 var routeBefore = routeCache.GetRoute("disable-test", "disable-test-service"); routeBefore.Should().NotBeNull(); // Act: 禁用路由 newRoute.Status = 0; _dbContext.GwTenantRoutes.Update(newRoute); await _dbContext.SaveChangesAsync(); await routeCache.ReloadAsync(); // Assert: 路由应该从缓存中移除 var routeAfter = routeCache.GetRoute("disable-test", "disable-test-service"); routeAfter.Should().BeNull(); // 清理 _dbContext.GwTenantRoutes.Remove(newRoute); await _dbContext.SaveChangesAsync(); } [Fact] public async Task WhenRouteDeleted_Reload_ShouldRemoveFromCache() { // Arrange var routeId = Guid.CreateVersion7().ToString("N"); var newRoute = new GwTenantRoute { Id = routeId, TenantCode = "delete-test", ServiceName = "delete-test-service", ClusterId = "member-cluster", Match = new GwRouteMatch { Path = "/api/delete/**" }, Priority = 1, Status = 1, IsGlobal = false, IsDeleted = false, CreatedTime = DateTime.UtcNow }; _dbContext.GwTenantRoutes.Add(newRoute); await _dbContext.SaveChangesAsync(); var routeCache = _fixture.GetRouteCache(); await routeCache.ReloadAsync(); // 确认路由存在 var routeBefore = routeCache.GetRoute("delete-test", "delete-test-service"); routeBefore.Should().NotBeNull(); // Act: 软删除路由 newRoute.IsDeleted = true; _dbContext.GwTenantRoutes.Update(newRoute); await _dbContext.SaveChangesAsync(); await routeCache.ReloadAsync(); // Assert: 路由应该从缓存中移除 var routeAfter = routeCache.GetRoute("delete-test", "delete-test-service"); routeAfter.Should().BeNull(); // 清理 _dbContext.GwTenantRoutes.Remove(newRoute); await _dbContext.SaveChangesAsync(); } [Fact] public async Task WhenClusterChanged_Reload_ShouldUpdateClusterConfig() { // Arrange var configProvider = _fixture.GetConfigProvider(); // 添加新集群 var newCluster = new GwCluster { Id = Guid.CreateVersion7().ToString("N"), ClusterId = "reload-test-cluster", Name = "Reload Test Cluster", Status = 1, CreatedTime = DateTime.UtcNow, LoadBalancingPolicy = GwLoadBalancingPolicy.RoundRobin, Destinations = new List { new() { DestinationId = "dest-1", Address = "http://reload-test:8080", Weight = 1, Status = 1 } } }; _dbContext.GwClusters.Add(newCluster); await _dbContext.SaveChangesAsync(); // Act await _fixture.ReloadConfigurationAsync(); // Assert var config = configProvider.GetConfig(); var cluster = config.Clusters.FirstOrDefault(c => c.ClusterId == "reload-test-cluster"); cluster.Should().NotBeNull(); cluster!.Destinations.Should().ContainKey("dest-1"); // 清理 _dbContext.GwClusters.Remove(newCluster); await _dbContext.SaveChangesAsync(); } [Fact] public async Task WhenDestinationAdded_Reload_ShouldIncludeNewDestination() { // Arrange: 获取现有集群 var cluster = await _dbContext.GwClusters .Include(c => c.Destinations) .FirstOrDefaultAsync(c => c.ClusterId == "member-cluster"); cluster.Should().NotBeNull(); var initialDestCount = cluster!.Destinations.Count; // 添加新目标 var newDest = new GwDestination { DestinationId = "new-dest", Address = "http://new-destination:8080", Weight = 1, Status = 1 }; cluster.Destinations.Add(newDest); await _dbContext.SaveChangesAsync(); // Act await _fixture.ReloadConfigurationAsync(); // Assert var configProvider = _fixture.GetConfigProvider(); var config = configProvider.GetConfig(); var updatedCluster = config.Clusters.FirstOrDefault(c => c.ClusterId == "member-cluster"); updatedCluster.Should().NotBeNull(); updatedCluster!.Destinations.Should().ContainKey("new-dest"); // 清理 cluster.Destinations.Remove(newDest); await _dbContext.SaveChangesAsync(); } #endregion #region 并发重载测试 [Fact] public async Task ConcurrentReload_ShouldBeThreadSafe() { // Arrange var routeCache = _fixture.GetRouteCache(); var configProvider = _fixture.GetConfigProvider(); // Act: 并发执行多次重载 var tasks = Enumerable.Range(0, 10) .Select(_ => Task.Run(async () => { await routeCache.ReloadAsync(); configProvider.UpdateConfig(); })) .ToList(); // 不应该抛出异常 await Task.WhenAll(tasks); // Assert: 配置应该保持一致 var config = configProvider.GetConfig(); config.Should().NotBeNull(); config.Routes.Should().NotBeNull(); config.Clusters.Should().NotBeNull(); } #endregion #region 配置变更通知测试(模拟) [Fact] public async Task SimulateNotify_ShouldTriggerReload() { // Arrange var routeCache = _fixture.GetRouteCache(); var configProvider = _fixture.GetConfigProvider(); // 添加新路由 var newRoute = new GwTenantRoute { Id = Guid.CreateVersion7().ToString("N"), TenantCode = "", ServiceName = "notify-test", ClusterId = "member-cluster", Match = new GwRouteMatch { Path = "/api/notify/**" }, Priority = 1, Status = 1, IsGlobal = true, CreatedTime = DateTime.UtcNow }; _dbContext.GwTenantRoutes.Add(newRoute); await _dbContext.SaveChangesAsync(); // Act: 模拟接收到 NOTIFY 事件后手动触发重载 // 在真实环境中,这是由 PgSqlConfigChangeListener 自动处理的 await routeCache.ReloadAsync(); configProvider.UpdateConfig(); // Assert var route = routeCache.GetRoute("any-tenant", "notify-test"); route.Should().NotBeNull(); route!.ClusterId.Should().Be("test-cluster"); // 清理 _dbContext.GwTenantRoutes.Remove(newRoute); await _dbContext.SaveChangesAsync(); } [Fact] public async Task ConfigChangeToken_ShouldSignalChange() { // Arrange var configProvider = _fixture.GetConfigProvider(); var config = configProvider.GetConfig(); // 获取变更令牌 var changeToken = config.ChangeToken; var changeDetected = false; // 注册变更回调 using var registration = changeToken.RegisterChangeCallback(_ => { changeDetected = true; }, null); // Act: 添加新路由并更新配置 var newRoute = new GwTenantRoute { Id = Guid.CreateVersion7().ToString("N"), TenantCode = "", ServiceName = "token-test", ClusterId = "member-cluster", Match = new GwRouteMatch { Path = "/api/token/**" }, Priority = 1, Status = 1, IsGlobal = true, CreatedTime = DateTime.UtcNow }; _dbContext.GwTenantRoutes.Add(newRoute); await _dbContext.SaveChangesAsync(); await _fixture.ReloadConfigurationAsync(); // 获取新配置 var newConfig = configProvider.GetConfig(); // Assert: 新配置的 ChangeToken 应该与老配置不同 newConfig.ChangeToken.Should().NotBeSameAs(changeToken); // 清理 _dbContext.GwTenantRoutes.Remove(newRoute); await _dbContext.SaveChangesAsync(); } #endregion #region 性能测试 [Fact] public async Task ReloadPerformance_ShouldBeFast() { // Arrange var routeCache = _fixture.GetRouteCache(); var stopwatch = System.Diagnostics.Stopwatch.StartNew(); // Act: 执行多次重载 for (int i = 0; i < 10; i++) { await routeCache.ReloadAsync(); } stopwatch.Stop(); // Assert: 10 次重载应该在合理时间内完成 stopwatch.ElapsedMilliseconds.Should().BeLessThan(5000); } [Fact] public async Task LargeConfigReload_ShouldHandleGracefully() { // Arrange: 批量添加路由 var routes = new List(); for (int i = 0; i < 100; i++) { routes.Add(new GwTenantRoute { Id = Guid.CreateVersion7().ToString("N"), TenantCode = $"perf-tenant-{i % 10}", ServiceName = $"perf-service-{i}", ClusterId = "member-cluster", Match = new GwRouteMatch { Path = $"/api/perf-{i}/**" }, Priority = 1, Status = 1, IsGlobal = i % 10 == 0, CreatedTime = DateTime.UtcNow }); } _dbContext.GwTenantRoutes.AddRange(routes); await _dbContext.SaveChangesAsync(); var routeCache = _fixture.GetRouteCache(); var stopwatch = System.Diagnostics.Stopwatch.StartNew(); // Act await routeCache.ReloadAsync(); stopwatch.Stop(); // Assert: 100 条路由的重载应该在合理时间内完成 stopwatch.ElapsedMilliseconds.Should().BeLessThan(5000); // 验证路由已加载 var route = routeCache.GetRoute("perf-tenant-0", "perf-service-0"); route.Should().NotBeNull(); // 清理 _dbContext.GwTenantRoutes.RemoveRange(routes); await _dbContext.SaveChangesAsync(); } #endregion }