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;
///
/// 待确认配置确认流程集成测试
///
[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 辅助方法
///
/// 确认待配置服务
///
private async Task 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>(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(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()
};
_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;
}
///
/// 拒绝待配置服务
///
private async Task 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
}