fengling-gateway/tests/YarpGateway.Tests/Integration/TestData.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

405 lines
12 KiB
C#

using System.Security.Claims;
using System.Text;
using System.Text.Json;
using Fengling.Platform.Domain.AggregatesModel.GatewayAggregate;
using Fengling.Platform.Domain.AggregatesModel.TenantAggregate;
using Microsoft.IdentityModel.Tokens;
using YarpGateway.Data;
using YarpGateway.Models;
namespace YarpGateway.Tests.Integration;
/// <summary>
/// 集成测试数据准备
/// </summary>
public static class TestData
{
#region
public static async Task SeedTenantsAsync(GatewayDbContext dbContext)
{
var tenants = new List<Tenant>
{
new()
{
TenantCode = "tenant1",
Name = "Tenant One",
Status = TenantStatus.Active,
CreatedAt = DateTime.UtcNow
},
new()
{
TenantCode = "tenant2",
Name = "Tenant Two",
Status = TenantStatus.Active,
CreatedAt = DateTime.UtcNow
},
new()
{
TenantCode = "default",
Name = "Default Tenant",
Status = TenantStatus.Active,
CreatedAt = DateTime.UtcNow
}
};
foreach (var tenant in tenants)
{
if (!dbContext.Tenants.Any(t => t.TenantCode == tenant.TenantCode))
{
dbContext.Tenants.Add(tenant);
}
}
await dbContext.SaveChangesAsync();
}
#endregion
#region
public static async Task SeedRoutesAsync(GatewayDbContext dbContext)
{
var routes = new List<GwTenantRoute>
{
// 全局路由 - member 服务
new()
{
Id = Guid.CreateVersion7().ToString("N"),
TenantCode = "",
ServiceName = "member",
ClusterId = "member-cluster",
Match = new GwRouteMatch { Path = "/api/member/**" },
Priority = 1,
Status = 1,
IsGlobal = true,
IsDeleted = false,
CreatedTime = DateTime.UtcNow
},
// 全局路由 - order 服务
new()
{
Id = Guid.CreateVersion7().ToString("N"),
TenantCode = "",
ServiceName = "order",
ClusterId = "order-cluster",
Match = new GwRouteMatch { Path = "/api/order/**" },
Priority = 1,
Status = 1,
IsGlobal = true,
IsDeleted = false,
CreatedTime = DateTime.UtcNow
},
// 租户专属路由 - tenant1 的 member 服务
new()
{
Id = Guid.CreateVersion7().ToString("N"),
TenantCode = "tenant1",
ServiceName = "member",
ClusterId = "tenant1-member-cluster",
Match = new GwRouteMatch { Path = "/api/member/**" },
Priority = 0, // 更高优先级
Status = 1,
IsGlobal = false,
IsDeleted = false,
CreatedTime = DateTime.UtcNow
}
};
foreach (var route in routes)
{
if (!dbContext.GwTenantRoutes.Any(r => r.Id == route.Id))
{
dbContext.GwTenantRoutes.Add(route);
}
}
await dbContext.SaveChangesAsync();
}
#endregion
#region
public static async Task SeedClustersAsync(GatewayDbContext dbContext)
{
var clusters = new List<GwCluster>
{
// member-cluster - 默认集群
new()
{
Id = Guid.CreateVersion7().ToString("N"),
ClusterId = "member-cluster",
Name = "Member Service Cluster",
LoadBalancingPolicy = GwLoadBalancingPolicy.RoundRobin,
Status = 1,
CreatedTime = DateTime.UtcNow,
Destinations = new List<GwDestination>
{
new()
{
DestinationId = "default",
Address = "http://default-member:8080",
Weight = 1,
Status = 1,
TenantCode = null // 默认目标
}
}
},
// tenant1-member-cluster - tenant1 专属集群
new()
{
Id = Guid.CreateVersion7().ToString("N"),
ClusterId = "tenant1-member-cluster",
Name = "Tenant1 Member Service Cluster",
LoadBalancingPolicy = GwLoadBalancingPolicy.RoundRobin,
Status = 1,
CreatedTime = DateTime.UtcNow,
Destinations = new List<GwDestination>
{
new()
{
DestinationId = "tenant1-dest",
Address = "http://tenant1-member:8080",
Weight = 1,
Status = 1,
TenantCode = "tenant1" // 租户专属目标
}
}
},
// order-cluster - 默认集群
new()
{
Id = Guid.CreateVersion7().ToString("N"),
ClusterId = "order-cluster",
Name = "Order Service Cluster",
LoadBalancingPolicy = GwLoadBalancingPolicy.RoundRobin,
Status = 1,
CreatedTime = DateTime.UtcNow,
Destinations = new List<GwDestination>
{
new()
{
DestinationId = "default",
Address = "http://default-order:8080",
Weight = 1,
Status = 1,
TenantCode = null // 默认目标
}
}
}
};
foreach (var cluster in clusters)
{
if (!dbContext.GwClusters.Any(c => c.ClusterId == cluster.ClusterId))
{
dbContext.GwClusters.Add(cluster);
}
}
await dbContext.SaveChangesAsync();
}
#endregion
#region K8s Service
/// <summary>
/// 创建模拟的 K8s Service 发现数据
/// </summary>
public static GwPendingServiceDiscovery CreateK8sService(
string serviceName,
string @namespace,
Dictionary<string, string> labels,
string clusterIp = "10.96.123.45",
int podCount = 1)
{
return new GwPendingServiceDiscovery
{
K8sServiceName = serviceName,
K8sNamespace = @namespace,
K8sClusterIP = clusterIp,
Labels = JsonSerializer.Serialize(labels),
DiscoveredPorts = "[8080]",
PodCount = podCount,
Status = (int)PendingConfigStatus.Pending,
DiscoveredAt = DateTime.UtcNow,
Version = 1,
IsDeleted = false
};
}
/// <summary>
/// 创建带路由标签的 K8s Service
/// </summary>
public static GwPendingServiceDiscovery CreateRoutedK8sService(
string serviceName,
string prefix,
string clusterName,
string destination = "default",
string @namespace = "default")
{
return CreateK8sService(
serviceName,
@namespace,
new Dictionary<string, string>
{
["app-router-name"] = serviceName,
["app-router-prefix"] = prefix,
["app-cluster-name"] = clusterName,
["app-cluster-destination"] = destination
});
}
#endregion
#region JWT Token
private static readonly SymmetricSecurityKey TestSigningKey = new(
Encoding.UTF8.GetBytes("test-signing-key-that-is-long-enough-for-hs256-algorithm"));
/// <summary>
/// 生成测试用 JWT Token
/// </summary>
public static string GenerateJwtToken(
string userId,
string tenantCode,
string[]? roles = null,
DateTime? expires = null)
{
var claims = new List<Claim>
{
new(ClaimTypes.NameIdentifier, userId),
new("sub", userId),
new("tenant", tenantCode),
new("tenant_id", tenantCode),
new(ClaimTypes.Name, $"test-user-{userId}"),
new("name", $"Test User {userId}")
};
if (roles != null)
{
foreach (var role in roles)
{
claims.Add(new Claim(ClaimTypes.Role, role));
claims.Add(new Claim("role", role));
}
}
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(claims),
Expires = expires ?? DateTime.UtcNow.AddHours(1),
Issuer = "test-issuer",
Audience = "test-audience",
SigningCredentials = new SigningCredentials(TestSigningKey, SecurityAlgorithms.HmacSha256)
};
var tokenHandler = new System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler();
var token = tokenHandler.CreateToken(tokenDescriptor);
return tokenHandler.WriteToken(token);
}
/// <summary>
/// 获取 tenant1 的测试 Token
/// </summary>
public static string GetTenant1Token(string userId = "user1")
{
return GenerateJwtToken(userId, "tenant1", new[] { "user" });
}
/// <summary>
/// 获取 tenant2 的测试 Token
/// </summary>
public static string GetTenant2Token(string userId = "user2")
{
return GenerateJwtToken(userId, "tenant2", new[] { "user" });
}
/// <summary>
/// 获取默认租户的测试 Token
/// </summary>
public static string GetDefaultToken(string userId = "default-user")
{
return GenerateJwtToken(userId, "default", new[] { "user" });
}
#endregion
}
#region
/// <summary>
/// 待确认配置状态
/// </summary>
public enum PendingConfigStatus
{
Pending = 0,
Confirmed = 1,
Rejected = 2
}
/// <summary>
/// PendingServiceDiscovery 扩展方法
/// </summary>
public static class PendingServiceDiscoveryExtensions
{
/// <summary>
/// 获取解析后的标签
/// </summary>
public static Dictionary<string, string> GetParsedLabels(this GwPendingServiceDiscovery discovery)
{
return JsonSerializer.Deserialize<Dictionary<string, string>>(discovery.Labels) ?? new();
}
/// <summary>
/// 获取路由名称
/// </summary>
public static string GetRouteName(this GwPendingServiceDiscovery discovery)
{
var labels = GetParsedLabels(discovery);
return labels.GetValueOrDefault("app-router-name", discovery.K8sServiceName);
}
/// <summary>
/// 获取路由前缀
/// </summary>
public static string GetRoutePrefix(this GwPendingServiceDiscovery discovery)
{
var labels = GetParsedLabels(discovery);
return labels.GetValueOrDefault("app-router-prefix", $"/api/{discovery.K8sServiceName}");
}
/// <summary>
/// 获取集群名称
/// </summary>
public static string GetClusterName(this GwPendingServiceDiscovery discovery)
{
var labels = GetParsedLabels(discovery);
return labels.GetValueOrDefault("app-cluster-name", discovery.K8sServiceName);
}
/// <summary>
/// 获取目标 ID
/// </summary>
public static string GetDestinationId(this GwPendingServiceDiscovery discovery)
{
var labels = GetParsedLabels(discovery);
return labels.GetValueOrDefault("app-cluster-destination", "default");
}
/// <summary>
/// 构建服务地址
/// </summary>
public static string BuildAddress(this GwPendingServiceDiscovery discovery)
{
var host = discovery.K8sClusterIP ?? $"{discovery.K8sServiceName}.{discovery.K8sNamespace}";
var ports = JsonSerializer.Deserialize<int[]>(discovery.DiscoveredPorts) ?? new[] { 8080 };
var port = ports.FirstOrDefault(8080);
return $"http://{host}:{port}";
}
}
#endregion