fengling-gateway/tests/YarpGateway.Tests/Unit/Services/RouteCacheTests.cs
movingsam cc11c7258f fix: 更新单元测试适配新模型
- RouteCacheTests: 使用 Fengling.Platform.Domain
- 更新 Id 类型为 string (Guid)
- 更新 PathPattern 为 Match.Path
- TenantRoutingMiddlewareTests: 更新 Id 类型为 string
2026-03-04 13:45:04 +08:00

434 lines
13 KiB
C#

using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Moq;
using Xunit;
using FluentAssertions;
using YarpGateway.Data;
using Fengling.Platform.Domain.AggregatesModel.GatewayAggregate;
using YarpGateway.Services;
namespace YarpGateway.Tests.Unit.Services;
public class RouteCacheTests
{
private readonly Mock<IDbContextFactory<GatewayDbContext>> _dbContextFactoryMock;
private readonly Mock<ILogger<RouteCache>> _loggerMock;
public RouteCacheTests()
{
_dbContextFactoryMock = new Mock<IDbContextFactory<GatewayDbContext>>();
_loggerMock = new Mock<ILogger<RouteCache>>();
}
private GatewayDbContext CreateInMemoryDbContext(List<GwTenantRoute> routes)
{
var options = new DbContextOptionsBuilder<GatewayDbContext>()
.UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
.Options;
var context = new GatewayDbContext(options);
context.TenantRoutes.AddRange(routes);
context.SaveChanges();
return context;
}
private RouteCache CreateRouteCache(GatewayDbContext context)
{
_dbContextFactoryMock
.Setup(x => x.CreateDbContext())
.Returns(context);
return new RouteCache(
dbContextFactory: _dbContextFactoryMock.Object,
logger: _loggerMock.Object
);
}
[Fact]
public async Task InitializeAsync_ShouldLoadGlobalRoutes()
{
// Arrange
var routes = new List<GwTenantRoute>
{
new GwTenantRoute
{
Id = Guid.CreateVersion7().ToString("N"),
TenantCode = "",
ServiceName = "user-service",
ClusterId = "cluster-user",
Match = new GwRouteMatch { Path = "/api/user/**" },
Priority = 1,
Status = 1,
IsGlobal = true,
IsDeleted = false
},
new GwTenantRoute
{
Id = Guid.CreateVersion7().ToString("N"),
TenantCode = "",
ServiceName = "order-service",
ClusterId = "cluster-order",
Match = new GwRouteMatch { Path = "/api/order/**" },
Priority = 1,
Status = 1,
IsGlobal = true,
IsDeleted = false
}
};
var context = CreateInMemoryDbContext(routes);
var routeCache = CreateRouteCache(context);
// Act
await routeCache.InitializeAsync();
// Assert
var result = routeCache.GetRoute("any-tenant", "user-service");
result.Should().NotBeNull();
result!.ClusterId.Should().Be("cluster-user");
}
[Fact]
public async Task InitializeAsync_ShouldLoadTenantRoutes()
{
// Arrange
var routes = new List<GwTenantRoute>
{
new GwTenantRoute
{
Id = Guid.CreateVersion7().ToString("N"),
TenantCode = "tenant-1",
ServiceName = "user-service",
ClusterId = "cluster-tenant-user",
Match = new GwRouteMatch { Path = "/api/user/**" },
Priority = 1,
Status = 1,
IsGlobal = false,
IsDeleted = false
}
};
var context = CreateInMemoryDbContext(routes);
var routeCache = CreateRouteCache(context);
// Act
await routeCache.InitializeAsync();
// Assert
var result = routeCache.GetRoute("tenant-1", "user-service");
result.Should().NotBeNull();
result!.ClusterId.Should().Be("cluster-tenant-user");
}
[Fact]
public async Task GetRoute_WithTenantRouteAvailable_ShouldReturnTenantRoute()
{
// Arrange
var routes = new List<GwTenantRoute>
{
new GwTenantRoute
{
Id = Guid.CreateVersion7().ToString("N"),
TenantCode = "tenant-1",
ServiceName = "user-service",
ClusterId = "tenant-cluster",
Match = new GwRouteMatch { Path = "/api/user/**" },
Priority = 1,
Status = 1,
IsGlobal = false,
IsDeleted = false
},
new GwTenantRoute
{
Id = Guid.CreateVersion7().ToString("N"),
TenantCode = "",
ServiceName = "user-service",
ClusterId = "global-cluster",
Match = new GwRouteMatch { Path = "/api/user/**" },
Priority = 1,
Status = 1,
IsGlobal = true,
IsDeleted = false
}
};
var context = CreateInMemoryDbContext(routes);
var routeCache = CreateRouteCache(context);
await routeCache.InitializeAsync();
// Act
var result = routeCache.GetRoute("tenant-1", "user-service");
// Assert - tenant route should be prioritized
result.Should().NotBeNull();
result!.ClusterId.Should().Be("tenant-cluster");
}
[Fact]
public async Task GetRoute_WithoutTenantRoute_ShouldFallbackToGlobal()
{
// Arrange
var routes = new List<GwTenantRoute>
{
new GwTenantRoute
{
Id = Guid.CreateVersion7().ToString("N"),
TenantCode = "",
ServiceName = "user-service",
ClusterId = "global-cluster",
Match = new GwRouteMatch { Path = "/api/user/**" },
Priority = 1,
Status = 1,
IsGlobal = true,
IsDeleted = false
}
};
var context = CreateInMemoryDbContext(routes);
var routeCache = CreateRouteCache(context);
await routeCache.InitializeAsync();
// Act
var result = routeCache.GetRoute("unknown-tenant", "user-service");
// Assert
result.Should().NotBeNull();
result!.ClusterId.Should().Be("global-cluster");
}
[Fact]
public async Task GetRoute_WithMissingRoute_ShouldReturnNull()
{
// Arrange
var routes = new List<GwTenantRoute>();
var context = CreateInMemoryDbContext(routes);
var routeCache = CreateRouteCache(context);
await routeCache.InitializeAsync();
// Act
var result = routeCache.GetRoute("tenant-1", "non-existent");
// Assert
result.Should().BeNull();
}
[Fact]
public async Task GetRouteByPath_WithValidPath_ShouldReturnRoute()
{
// Arrange
var routes = new List<GwTenantRoute>
{
new GwTenantRoute
{
Id = Guid.CreateVersion7().ToString("N"),
TenantCode = "",
ServiceName = "user-service",
ClusterId = "cluster-user",
Match = new GwRouteMatch { Path = "/api/user/**" },
Priority = 1,
Status = 1,
IsGlobal = true,
IsDeleted = false
}
};
var context = CreateInMemoryDbContext(routes);
var routeCache = CreateRouteCache(context);
await routeCache.InitializeAsync();
// Act
var result = routeCache.GetRouteByPath("/api/user/users");
// Assert
result.Should().NotBeNull();
result!.ClusterId.Should().Be("cluster-user");
}
[Fact]
public async Task GetRouteByPath_WithMissingPath_ShouldReturnNull()
{
// Arrange
var routes = new List<GwTenantRoute>();
var context = CreateInMemoryDbContext(routes);
var routeCache = CreateRouteCache(context);
await routeCache.InitializeAsync();
// Act
var result = routeCache.GetRouteByPath("/unknown/path");
// Assert
result.Should().BeNull();
}
[Fact]
public async Task ReloadAsync_ShouldClearOldRoutes()
{
// Arrange
var initialRoutes = new List<GwTenantRoute>
{
new GwTenantRoute
{
Id = Guid.CreateVersion7().ToString("N"),
TenantCode = "",
ServiceName = "old-service",
ClusterId = "old-cluster",
Match = new GwRouteMatch { Path = "/api/old/**" },
Priority = 1,
Status = 1,
IsGlobal = true,
IsDeleted = false
}
};
var context = CreateInMemoryDbContext(initialRoutes);
var routeCache = CreateRouteCache(context);
await routeCache.InitializeAsync();
// Verify initial state
routeCache.GetRoute("any", "old-service").Should().NotBeNull();
// Modify the database (replace routes)
context.TenantRoutes.RemoveRange(context.TenantRoutes);
context.TenantRoutes.Add(new GwTenantRoute
{
Id = Guid.CreateVersion7().ToString("N"),
TenantCode = "",
ServiceName = "new-service",
ClusterId = "new-cluster",
Match = new GwRouteMatch { Path = "/api/new/**" },
Priority = 1,
Status = 1,
IsGlobal = true,
IsDeleted = false
});
context.SaveChanges();
// Act
await routeCache.ReloadAsync();
// Assert - old route should be gone, new route should exist
routeCache.GetRoute("any", "old-service").Should().BeNull();
routeCache.GetRoute("any", "new-service").Should().NotBeNull();
}
[Fact]
public async Task InitializeAsync_ShouldExcludeDeletedRoutes()
{
// Arrange
var routes = new List<GwTenantRoute>
{
new GwTenantRoute
{
Id = Guid.CreateVersion7().ToString("N"),
TenantCode = "",
ServiceName = "active-service",
ClusterId = "cluster-1",
Match = new GwRouteMatch { Path = "/api/active/**" },
Priority = 1,
Status = 1,
IsGlobal = true,
IsDeleted = false
},
new GwTenantRoute
{
Id = Guid.CreateVersion7().ToString("N"),
TenantCode = "",
ServiceName = "deleted-service",
ClusterId = "cluster-2",
Match = new GwRouteMatch { Path = "/api/deleted/**" },
Priority = 1,
Status = 1,
IsGlobal = true,
IsDeleted = true
}
};
var context = CreateInMemoryDbContext(routes);
var routeCache = CreateRouteCache(context);
await routeCache.InitializeAsync();
// Assert
routeCache.GetRoute("any", "active-service").Should().NotBeNull();
routeCache.GetRoute("any", "deleted-service").Should().BeNull();
}
[Fact]
public async Task InitializeAsync_ShouldExcludeInactiveRoutes()
{
// Arrange
var routes = new List<GwTenantRoute>
{
new GwTenantRoute
{
Id = Guid.CreateVersion7().ToString("N"),
TenantCode = "",
ServiceName = "active-service",
ClusterId = "cluster-1",
Match = new GwRouteMatch { Path = "/api/active/**" },
Priority = 1,
Status = 1,
IsGlobal = true,
IsDeleted = false
},
new GwTenantRoute
{
Id = Guid.CreateVersion7().ToString("N"),
TenantCode = "",
ServiceName = "inactive-service",
ClusterId = "cluster-2",
Match = new GwRouteMatch { Path = "/api/inactive/**" },
Priority = 1,
Status = 0, // Inactive
IsGlobal = true,
IsDeleted = false
}
};
var context = CreateInMemoryDbContext(routes);
var routeCache = CreateRouteCache(context);
await routeCache.InitializeAsync();
// Assert
routeCache.GetRoute("any", "active-service").Should().NotBeNull();
routeCache.GetRoute("any", "inactive-service").Should().BeNull();
}
[Fact]
public async Task GetRoute_ConcurrentReads_ShouldBeThreadSafe()
{
// Arrange
var routes = new List<GwTenantRoute>
{
new GwTenantRoute
{
Id = Guid.CreateVersion7().ToString("N"),
TenantCode = "",
ServiceName = "user-service",
ClusterId = "cluster-user",
Match = new GwRouteMatch { Path = "/api/user/**" },
Priority = 1,
Status = 1,
IsGlobal = true,
IsDeleted = false
}
};
var context = CreateInMemoryDbContext(routes);
var routeCache = CreateRouteCache(context);
await routeCache.InitializeAsync();
// Act & Assert - multiple concurrent reads should not throw
var tasks = Enumerable.Range(0, 100)
.Select(_ => Task.Run(() => routeCache.GetRoute("any", "user-service")))
.ToList();
var results = await Task.WhenAll(tasks);
// All results should be consistent
results.Should().AllSatisfy(r => r.Should().NotBeNull());
}
}