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 }