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 }