fengling-gateway/tests/YarpGateway.Tests/Integration/ConfigConfirmationTests.cs
movingsam 9b77169b80 test: add end-to-end integration tests (IMPL-12)
- 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
2026-03-08 10:53:19 +08:00

359 lines
12 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
}