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;
///
/// K8s Service Label 发现流程集成测试
///
[Collection("Integration Tests")]
public class K8sDiscoveryTests : IDisposable
{
private readonly TestFixture _fixture;
private readonly GatewayDbContext _dbContext;
public K8sDiscoveryTests(TestFixture fixture)
{
_fixture = fixture;
_dbContext = _fixture.CreateDbContext();
}
public void Dispose()
{
_dbContext.Dispose();
}
#region K8s Service 发现测试
[Fact]
public async Task WhenK8sServiceCreated_WithValidLabels_ShouldGeneratePendingConfig()
{
// Arrange: 模拟 K8s Service 创建事件
var service = TestData.CreateRoutedK8sService(
serviceName: "member",
prefix: "/member",
clusterName: "member",
destination: "default",
@namespace: "test-namespace"
);
// Act: 将服务添加到数据库(模拟 Watch 事件处理)
_dbContext.PendingServiceDiscoveries.Add(service);
await _dbContext.SaveChangesAsync();
// Assert: 验证待确认配置已生成
var pendingConfig = await _dbContext.PendingServiceDiscoveries
.FirstOrDefaultAsync(s => s.K8sServiceName == "member" &&
s.K8sNamespace == "test-namespace");
pendingConfig.Should().NotBeNull();
pendingConfig!.K8sServiceName.Should().Be("member");
pendingConfig.Status.Should().Be(0); // Pending status
}
[Fact]
public async Task WhenK8sServiceCreated_WithRouterLabels_ShouldParseLabelsCorrectly()
{
// Arrange
var labels = new Dictionary
{
["app-router-name"] = "inventory-service",
["app-router-prefix"] = "/api/inventory",
["app-cluster-name"] = "inventory-cluster",
["app-cluster-destination"] = "v1",
["custom-label"] = "custom-value"
};
var service = TestData.CreateK8sService(
serviceName: "inventory",
@namespace: "services",
labels: labels,
clusterIp: "10.96.100.10",
podCount: 3
);
// Act
_dbContext.PendingServiceDiscoveries.Add(service);
await _dbContext.SaveChangesAsync();
// Assert
var saved = await _dbContext.PendingServiceDiscoveries
.FirstOrDefaultAsync(s => s.K8sServiceName == "inventory");
saved.Should().NotBeNull();
var parsedLabels = JsonSerializer.Deserialize>(saved!.Labels);
parsedLabels.Should().ContainKey("app-router-name").WhoseValue.Should().Be("inventory-service");
parsedLabels.Should().ContainKey("app-router-prefix").WhoseValue.Should().Be("/api/inventory");
parsedLabels.Should().ContainKey("app-cluster-name").WhoseValue.Should().Be("inventory-cluster");
parsedLabels.Should().ContainKey("custom-label").WhoseValue.Should().Be("custom-value");
}
[Fact]
public async Task WhenK8sServiceCreated_WithoutRouterLabels_ShouldNotCreatePendingConfig()
{
// Arrange: 没有路由标签的服务
var service = TestData.CreateK8sService(
serviceName: "background-worker",
@namespace: "workers",
labels: new Dictionary
{
["app"] = "worker",
["tier"] = "backend"
}
);
// Act: 在真实场景中,没有路由标签的服务应该被过滤掉
// 这里我们验证服务可以被添加,但状态可能不同
_dbContext.PendingServiceDiscoveries.Add(service);
await _dbContext.SaveChangesAsync();
// Assert
var saved = await _dbContext.PendingServiceDiscoveries
.FirstOrDefaultAsync(s => s.K8sServiceName == "background-worker");
saved.Should().NotBeNull();
// 验证标签不包含路由信息
var parsedLabels = JsonSerializer.Deserialize>(saved!.Labels);
parsedLabels.Should().NotContainKey("app-router-name");
parsedLabels.Should().NotContainKey("app-router-prefix");
}
[Fact]
public async Task WhenK8sServiceUpdated_ShouldUpdatePendingConfig()
{
// Arrange: 创建初始服务
var service = TestData.CreateRoutedK8sService(
serviceName: "payment-service",
prefix: "/api/payment",
clusterName: "payment",
@namespace: "test-namespace"
);
service.PodCount = 2;
_dbContext.PendingServiceDiscoveries.Add(service);
await _dbContext.SaveChangesAsync();
var originalId = service.Id;
// Act: 模拟 Pod 数量变化
service.PodCount = 5;
service.Version = 2;
_dbContext.PendingServiceDiscoveries.Update(service);
await _dbContext.SaveChangesAsync();
// Assert
var updated = await _dbContext.PendingServiceDiscoveries
.FirstOrDefaultAsync(s => s.Id == originalId);
updated.Should().NotBeNull();
updated!.PodCount.Should().Be(5);
updated.Version.Should().Be(2);
}
[Fact]
public async Task WhenK8sServiceDeleted_ShouldMarkAsDeleted()
{
// Arrange
var service = TestData.CreateRoutedK8sService(
serviceName: "temp-service",
prefix: "/api/temp",
clusterName: "temp",
@namespace: "test-namespace"
);
_dbContext.PendingServiceDiscoveries.Add(service);
await _dbContext.SaveChangesAsync();
var serviceId = service.Id;
// Act
service.IsDeleted = true;
_dbContext.PendingServiceDiscoveries.Update(service);
await _dbContext.SaveChangesAsync();
// Assert
var deleted = await _dbContext.PendingServiceDiscoveries
.FirstOrDefaultAsync(s => s.Id == serviceId);
deleted.Should().NotBeNull();
deleted!.IsDeleted.Should().BeTrue();
}
[Fact]
public async Task WhenMultipleServicesDiscovered_ShouldCreateMultiplePendingConfigs()
{
// Arrange - 使用唯一的命名空间避免与其他测试冲突
var uniqueNs = $"test-ns-{Guid.NewGuid():N}";
var services = new[]
{
TestData.CreateRoutedK8sService("service-a", "/api/a", "cluster-a", @namespace: uniqueNs),
TestData.CreateRoutedK8sService("service-b", "/api/b", "cluster-b", @namespace: uniqueNs),
TestData.CreateRoutedK8sService("service-c", "/api/c", "cluster-c", @namespace: uniqueNs)
};
// Act
foreach (var service in services)
{
_dbContext.PendingServiceDiscoveries.Add(service);
}
await _dbContext.SaveChangesAsync();
// Assert
var pendingConfigs = await _dbContext.PendingServiceDiscoveries
.Where(s => s.K8sNamespace == uniqueNs)
.ToListAsync();
pendingConfigs.Should().HaveCount(3);
pendingConfigs.Select(s => s.K8sServiceName).Should().Contain("service-a", "service-b", "service-c");
// 清理
_dbContext.PendingServiceDiscoveries.RemoveRange(pendingConfigs);
await _dbContext.SaveChangesAsync();
}
[Fact]
public async Task WhenDuplicateServiceDiscovered_ShouldHandleGracefully()
{
// Arrange
var service1 = TestData.CreateRoutedK8sService(
"duplicate-service",
"/api/duplicate",
"duplicate",
@namespace: "test-ns"
);
_dbContext.PendingServiceDiscoveries.Add(service1);
await _dbContext.SaveChangesAsync();
// Act: 尝试添加相同名称和命名空间的服务
var service2 = TestData.CreateRoutedK8sService(
"duplicate-service",
"/api/duplicate",
"duplicate",
@namespace: "test-ns"
);
// 在真实场景中,这里应该更新而不是创建新记录
// 或者根据业务逻辑处理冲突
var existing = await _dbContext.PendingServiceDiscoveries
.FirstOrDefaultAsync(s => s.K8sServiceName == "duplicate-service" &&
s.K8sNamespace == "test-ns" &&
!s.IsDeleted);
if (existing != null)
{
// 更新现有记录
existing.PodCount = service2.PodCount;
existing.Version++;
_dbContext.PendingServiceDiscoveries.Update(existing);
}
else
{
_dbContext.PendingServiceDiscoveries.Add(service2);
}
await _dbContext.SaveChangesAsync();
// Assert
var configs = await _dbContext.PendingServiceDiscoveries
.Where(s => s.K8sServiceName == "duplicate-service" &&
s.K8sNamespace == "test-ns" &&
!s.IsDeleted)
.ToListAsync();
configs.Should().HaveCount(1);
}
#endregion
#region 健康检查相关测试
[Fact]
public async Task WhenServiceHasMultiplePorts_ShouldStoreAllPorts()
{
// Arrange
var service = TestData.CreateK8sService(
serviceName: "multi-port-service",
@namespace: "test-ns",
labels: new Dictionary { ["app-router-name"] = "multi" }
);
service.DiscoveredPorts = "[8080, 8443, 9090]";
// Act
_dbContext.PendingServiceDiscoveries.Add(service);
await _dbContext.SaveChangesAsync();
// Assert
var saved = await _dbContext.PendingServiceDiscoveries
.FirstOrDefaultAsync(s => s.K8sServiceName == "multi-port-service");
saved.Should().NotBeNull();
saved!.DiscoveredPorts.Should().Be("[8080, 8443, 9090]");
}
[Fact]
public async Task WhenServiceHasNoClusterIP_ShouldHandleGracefully()
{
// Arrange: Headless service (没有 ClusterIP)
var service = TestData.CreateK8sService(
serviceName: "headless-service",
@namespace: "test-ns",
labels: new Dictionary { ["app-router-name"] = "headless" },
clusterIp: null!
);
// Act
_dbContext.PendingServiceDiscoveries.Add(service);
await _dbContext.SaveChangesAsync();
// Assert
var saved = await _dbContext.PendingServiceDiscoveries
.FirstOrDefaultAsync(s => s.K8sServiceName == "headless-service");
saved.Should().NotBeNull();
saved!.K8sClusterIP.Should().BeNull();
}
#endregion
}