- TestFixture: Base test infrastructure with WebApplicationFactory - K8sDiscoveryTests: K8s Service Label discovery flow tests - ConfigConfirmationTests: Pending config confirmation flow tests - MultiTenantRoutingTests: Tenant-specific vs default destination routing tests - ConfigReloadTests: Gateway hot-reload via NOTIFY mechanism tests - TestData: Mock data for K8s services, JWT tokens, database seeding Tests cover: 1. K8s Service discovery with valid labels 2. Config confirmation -> DB write -> NOTIFY 3. Multi-tenant routing (dedicated vs default destination) 4. Gateway config hot-reload without restart
359 lines
12 KiB
C#
359 lines
12 KiB
C#
using System.Net;
|
||
using System.Net.Http.Json;
|
||
using System.Text.Json;
|
||
using FluentAssertions;
|
||
using Microsoft.EntityFrameworkCore;
|
||
using Xunit;
|
||
using YarpGateway.Data;
|
||
using YarpGateway.Models;
|
||
using Fengling.Platform.Domain.AggregatesModel.GatewayAggregate;
|
||
|
||
namespace YarpGateway.Tests.Integration;
|
||
|
||
/// <summary>
|
||
/// 待确认配置确认流程集成测试
|
||
/// </summary>
|
||
[Collection("Integration Tests")]
|
||
public class ConfigConfirmationTests : IDisposable
|
||
{
|
||
private readonly TestFixture _fixture;
|
||
private readonly GatewayDbContext _dbContext;
|
||
|
||
public ConfigConfirmationTests(TestFixture fixture)
|
||
{
|
||
_fixture = fixture;
|
||
_dbContext = _fixture.CreateDbContext();
|
||
}
|
||
|
||
public void Dispose()
|
||
{
|
||
_dbContext.Dispose();
|
||
}
|
||
|
||
#region 配置确认流程测试
|
||
|
||
[Fact]
|
||
public async Task WhenPendingConfigConfirmed_ShouldCreateRouteAndCluster()
|
||
{
|
||
// Arrange: 创建待确认配置
|
||
var pendingConfig = TestData.CreateRoutedK8sService(
|
||
serviceName: "new-service",
|
||
prefix: "/api/new",
|
||
clusterName: "new-cluster",
|
||
destination: "default",
|
||
@namespace: "test-ns"
|
||
);
|
||
pendingConfig.K8sClusterIP = "10.96.50.50";
|
||
pendingConfig.DiscoveredPorts = "[8080]";
|
||
|
||
_dbContext.PendingServiceDiscoveries.Add(pendingConfig);
|
||
await _dbContext.SaveChangesAsync();
|
||
|
||
// Act: 确认配置(模拟 Console API 调用)
|
||
var confirmResult = await ConfirmPendingConfigAsync(pendingConfig.Id, "admin-user");
|
||
|
||
// Assert: 验证待确认配置状态已更新
|
||
confirmResult.Should().BeTrue();
|
||
|
||
var updatedPending = await _dbContext.PendingServiceDiscoveries
|
||
.FirstOrDefaultAsync(s => s.Id == pendingConfig.Id);
|
||
updatedPending.Should().NotBeNull();
|
||
updatedPending!.Status.Should().Be(1); // Confirmed
|
||
updatedPending.AssignedBy.Should().Be("admin-user");
|
||
updatedPending.AssignedAt.Should().NotBeNull();
|
||
updatedPending.AssignedClusterId.Should().Be("new-cluster");
|
||
|
||
// Assert: 验证集群已创建
|
||
var cluster = await _dbContext.GwClusters
|
||
.FirstOrDefaultAsync(c => c.ClusterId == "new-cluster");
|
||
cluster.Should().NotBeNull();
|
||
cluster!.Name.Should().Be("new-cluster");
|
||
cluster.Status.Should().Be(1);
|
||
|
||
// Assert: 验证路由已创建
|
||
var route = await _dbContext.GwTenantRoutes
|
||
.FirstOrDefaultAsync(r => r.ClusterId == "new-cluster" && r.ServiceName == "new-service");
|
||
route.Should().NotBeNull();
|
||
route!.ServiceName.Should().Be("new-service");
|
||
route.IsGlobal.Should().BeTrue();
|
||
route.Status.Should().Be(1);
|
||
}
|
||
|
||
[Fact]
|
||
public async Task WhenPendingConfigConfirmed_WithTenantSpecificDestination_ShouldCreateTenantRoute()
|
||
{
|
||
// Arrange
|
||
var pendingConfig = TestData.CreateRoutedK8sService(
|
||
serviceName: "tenant-specific-service",
|
||
prefix: "/api/tenant-svc",
|
||
clusterName: "tenant-cluster",
|
||
destination: "tenant1",
|
||
@namespace: "test-ns"
|
||
);
|
||
pendingConfig.K8sClusterIP = "10.96.60.60";
|
||
|
||
_dbContext.PendingServiceDiscoveries.Add(pendingConfig);
|
||
await _dbContext.SaveChangesAsync();
|
||
|
||
// Act
|
||
var confirmResult = await ConfirmPendingConfigAsync(
|
||
pendingConfig.Id,
|
||
"admin-user",
|
||
tenantCode: "tenant1");
|
||
|
||
// Assert
|
||
confirmResult.Should().BeTrue();
|
||
|
||
// 验证创建了租户专属路由
|
||
var route = await _dbContext.GwTenantRoutes
|
||
.FirstOrDefaultAsync(r => r.ServiceName == "tenant-specific-service" &&
|
||
r.TenantCode == "tenant1");
|
||
route.Should().NotBeNull();
|
||
route!.IsGlobal.Should().BeFalse();
|
||
route.TenantCode.Should().Be("tenant1");
|
||
}
|
||
|
||
[Fact]
|
||
public async Task WhenPendingConfigRejected_ShouldNotCreateRouteOrCluster()
|
||
{
|
||
// Arrange
|
||
var pendingConfig = TestData.CreateRoutedK8sService(
|
||
serviceName: "rejected-service",
|
||
prefix: "/api/rejected",
|
||
clusterName: "rejected-cluster",
|
||
@namespace: "test-ns"
|
||
);
|
||
|
||
_dbContext.PendingServiceDiscoveries.Add(pendingConfig);
|
||
await _dbContext.SaveChangesAsync();
|
||
|
||
// Act: 拒绝配置
|
||
var rejectResult = await RejectPendingConfigAsync(pendingConfig.Id, "admin-user", "不符合安全规范");
|
||
|
||
// Assert
|
||
rejectResult.Should().BeTrue();
|
||
|
||
var rejected = await _dbContext.PendingServiceDiscoveries
|
||
.FirstOrDefaultAsync(s => s.Id == pendingConfig.Id);
|
||
rejected.Should().NotBeNull();
|
||
rejected!.Status.Should().Be(2); // Rejected
|
||
|
||
// 验证没有创建集群
|
||
var cluster = await _dbContext.GwClusters
|
||
.FirstOrDefaultAsync(c => c.ClusterId == "rejected-cluster");
|
||
cluster.Should().BeNull();
|
||
|
||
// 验证没有创建路由
|
||
var route = await _dbContext.GwTenantRoutes
|
||
.FirstOrDefaultAsync(r => r.ServiceName == "rejected-service");
|
||
route.Should().BeNull();
|
||
}
|
||
|
||
[Fact]
|
||
public async Task WhenConfirmNonExistentPendingConfig_ShouldReturnFalse()
|
||
{
|
||
// Act
|
||
var result = await ConfirmPendingConfigAsync(99999, "admin-user");
|
||
|
||
// Assert
|
||
result.Should().BeFalse();
|
||
}
|
||
|
||
[Fact]
|
||
public async Task WhenPendingConfigAlreadyConfirmed_ShouldNotDuplicate()
|
||
{
|
||
// Arrange
|
||
var pendingConfig = TestData.CreateRoutedK8sService(
|
||
serviceName: "duplicate-confirm-service",
|
||
prefix: "/api/dup",
|
||
clusterName: "dup-cluster",
|
||
@namespace: "test-ns"
|
||
);
|
||
|
||
_dbContext.PendingServiceDiscoveries.Add(pendingConfig);
|
||
await _dbContext.SaveChangesAsync();
|
||
|
||
// 第一次确认
|
||
await ConfirmPendingConfigAsync(pendingConfig.Id, "admin-user");
|
||
|
||
// Act: 第二次确认(应该幂等处理)
|
||
var secondConfirm = await ConfirmPendingConfigAsync(pendingConfig.Id, "admin-user");
|
||
|
||
// Assert
|
||
// 根据业务逻辑,可能返回 false(已处理)或 true(幂等成功)
|
||
// 但不应该创建重复的集群和路由
|
||
var clusters = await _dbContext.GwClusters
|
||
.Where(c => c.ClusterId == "dup-cluster")
|
||
.ToListAsync();
|
||
clusters.Should().HaveCount(1);
|
||
|
||
var routes = await _dbContext.GwTenantRoutes
|
||
.Where(r => r.ServiceName == "duplicate-confirm-service")
|
||
.ToListAsync();
|
||
routes.Should().HaveCount(1);
|
||
}
|
||
|
||
[Fact]
|
||
public async Task WhenConfigConfirmed_ShouldTriggerConfigReload()
|
||
{
|
||
// Arrange
|
||
var pendingConfig = TestData.CreateRoutedK8sService(
|
||
serviceName: "reload-test-service",
|
||
prefix: "/api/reload",
|
||
clusterName: "reload-cluster",
|
||
@namespace: "test-ns"
|
||
);
|
||
|
||
_dbContext.PendingServiceDiscoveries.Add(pendingConfig);
|
||
await _dbContext.SaveChangesAsync();
|
||
|
||
var initialConfig = _fixture.GetConfigProvider().GetConfig();
|
||
var initialRouteCount = initialConfig.Routes.Count;
|
||
|
||
// Act: 确认配置并重新加载
|
||
await ConfirmPendingConfigAsync(pendingConfig.Id, "admin-user");
|
||
await _fixture.ReloadConfigurationAsync();
|
||
|
||
// Assert: 验证配置已更新
|
||
var newConfig = _fixture.GetConfigProvider().GetConfig();
|
||
newConfig.Routes.Count.Should().BeGreaterThanOrEqualTo(initialRouteCount);
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region 辅助方法
|
||
|
||
/// <summary>
|
||
/// 确认待配置服务
|
||
/// </summary>
|
||
private async Task<bool> ConfirmPendingConfigAsync(
|
||
long pendingConfigId,
|
||
string confirmedBy,
|
||
string? tenantCode = null)
|
||
{
|
||
var pendingConfig = await _dbContext.PendingServiceDiscoveries
|
||
.FirstOrDefaultAsync(s => s.Id == pendingConfigId);
|
||
|
||
if (pendingConfig == null || pendingConfig.Status != 0)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
// 解析标签
|
||
var labels = JsonSerializer.Deserialize<Dictionary<string, string>>(pendingConfig.Labels) ?? new();
|
||
var serviceName = labels.GetValueOrDefault("app-router-name") ?? pendingConfig.K8sServiceName;
|
||
var prefix = labels.GetValueOrDefault("app-router-prefix") ?? $"/api/{serviceName}";
|
||
var clusterName = labels.GetValueOrDefault("app-cluster-name") ?? serviceName;
|
||
var destinationId = labels.GetValueOrDefault("app-cluster-destination") ?? "default";
|
||
|
||
// 构建地址
|
||
var ports = JsonSerializer.Deserialize<int[]>(pendingConfig.DiscoveredPorts) ?? new[] { 8080 };
|
||
var port = ports.First();
|
||
var clusterIp = pendingConfig.K8sClusterIP ?? $"{serviceName}.{pendingConfig.K8sNamespace}";
|
||
var address = clusterIp.StartsWith("http")
|
||
? clusterIp
|
||
: $"http://{clusterIp}:{port}";
|
||
|
||
// 创建或更新集群
|
||
var cluster = await _dbContext.GwClusters
|
||
.FirstOrDefaultAsync(c => c.ClusterId == clusterName);
|
||
|
||
if (cluster == null)
|
||
{
|
||
cluster = new GwCluster
|
||
{
|
||
Id = Guid.CreateVersion7().ToString("N"),
|
||
ClusterId = clusterName,
|
||
Name = clusterName,
|
||
Status = 1,
|
||
CreatedTime = DateTime.UtcNow,
|
||
LoadBalancingPolicy = GwLoadBalancingPolicy.RoundRobin,
|
||
Destinations = new List<GwDestination>()
|
||
};
|
||
_dbContext.GwClusters.Add(cluster);
|
||
}
|
||
|
||
// 添加目标
|
||
var destination = cluster.Destinations.FirstOrDefault(d => d.DestinationId == destinationId);
|
||
if (destination == null)
|
||
{
|
||
destination = new GwDestination
|
||
{
|
||
DestinationId = destinationId,
|
||
Address = address,
|
||
Weight = 1,
|
||
Status = 1,
|
||
TenantCode = tenantCode
|
||
};
|
||
cluster.Destinations.Add(destination);
|
||
}
|
||
else
|
||
{
|
||
destination.Address = address;
|
||
destination.TenantCode = tenantCode ?? destination.TenantCode;
|
||
}
|
||
|
||
// 创建路由
|
||
var routeTenantCode = tenantCode ?? "";
|
||
var isGlobal = string.IsNullOrEmpty(tenantCode);
|
||
var routeId = Guid.CreateVersion7().ToString("N");
|
||
|
||
var existingRoute = await _dbContext.GwTenantRoutes
|
||
.FirstOrDefaultAsync(r => r.ServiceName == serviceName &&
|
||
r.TenantCode == routeTenantCode &&
|
||
r.ClusterId == clusterName);
|
||
|
||
if (existingRoute == null)
|
||
{
|
||
var route = new GwTenantRoute
|
||
{
|
||
Id = routeId,
|
||
TenantCode = routeTenantCode,
|
||
ServiceName = serviceName,
|
||
ClusterId = clusterName,
|
||
Match = new GwRouteMatch { Path = $"{prefix}/**" },
|
||
Priority = isGlobal ? 1 : 0,
|
||
Status = 1,
|
||
IsGlobal = isGlobal,
|
||
CreatedTime = DateTime.UtcNow
|
||
};
|
||
_dbContext.GwTenantRoutes.Add(route);
|
||
}
|
||
|
||
// 更新待确认配置状态
|
||
pendingConfig.Status = 1; // Confirmed
|
||
pendingConfig.AssignedBy = confirmedBy;
|
||
pendingConfig.AssignedAt = DateTime.UtcNow;
|
||
pendingConfig.AssignedClusterId = clusterName;
|
||
|
||
await _dbContext.SaveChangesAsync();
|
||
return true;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 拒绝待配置服务
|
||
/// </summary>
|
||
private async Task<bool> RejectPendingConfigAsync(
|
||
long pendingConfigId,
|
||
string rejectedBy,
|
||
string reason)
|
||
{
|
||
var pendingConfig = await _dbContext.PendingServiceDiscoveries
|
||
.FirstOrDefaultAsync(s => s.Id == pendingConfigId);
|
||
|
||
if (pendingConfig == null || pendingConfig.Status != 0)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
pendingConfig.Status = 2; // Rejected
|
||
pendingConfig.AssignedBy = rejectedBy;
|
||
pendingConfig.AssignedAt = DateTime.UtcNow;
|
||
|
||
await _dbContext.SaveChangesAsync();
|
||
return true;
|
||
}
|
||
|
||
#endregion
|
||
}
|