- Fix ClusterId expectations in ConfigReloadTests - Use unique namespace in K8sDiscoveryTests to avoid test interference - Add cleanup after test execution
478 lines
14 KiB
C#
478 lines
14 KiB
C#
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;
|
||
|
||
/// <summary>
|
||
/// 网关配置热重载集成测试
|
||
/// </summary>
|
||
[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);
|
||
|
||
// 验证可以通过集群ID找到路由
|
||
var route = newConfig.Routes.FirstOrDefault(r =>
|
||
r.ClusterId == "member-cluster");
|
||
route.Should().NotBeNull("Route with member-cluster should exist after reload");
|
||
|
||
// 清理
|
||
_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<GwDestination>
|
||
{
|
||
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("member-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<GwTenantRoute>();
|
||
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
|
||
}
|