Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
434 lines
13 KiB
C#
434 lines
13 KiB
C#
using Microsoft.EntityFrameworkCore;
|
|
using Microsoft.Extensions.Logging;
|
|
using Moq;
|
|
using Xunit;
|
|
using FluentAssertions;
|
|
using YarpGateway.Data;
|
|
using YarpGateway.Models;
|
|
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 = 1,
|
|
TenantCode = "",
|
|
ServiceName = "user-service",
|
|
ClusterId = "cluster-user",
|
|
PathPattern = "/api/user/**",
|
|
Priority = 1,
|
|
Status = 1,
|
|
IsGlobal = true,
|
|
IsDeleted = false
|
|
},
|
|
new GwTenantRoute
|
|
{
|
|
Id = 2,
|
|
TenantCode = "",
|
|
ServiceName = "order-service",
|
|
ClusterId = "cluster-order",
|
|
PathPattern = "/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 = 1,
|
|
TenantCode = "tenant-1",
|
|
ServiceName = "user-service",
|
|
ClusterId = "cluster-tenant-user",
|
|
PathPattern = "/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 = 1,
|
|
TenantCode = "tenant-1",
|
|
ServiceName = "user-service",
|
|
ClusterId = "tenant-cluster",
|
|
PathPattern = "/api/user/**",
|
|
Priority = 1,
|
|
Status = 1,
|
|
IsGlobal = false,
|
|
IsDeleted = false
|
|
},
|
|
new GwTenantRoute
|
|
{
|
|
Id = 2,
|
|
TenantCode = "",
|
|
ServiceName = "user-service",
|
|
ClusterId = "global-cluster",
|
|
PathPattern = "/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 = 1,
|
|
TenantCode = "",
|
|
ServiceName = "user-service",
|
|
ClusterId = "global-cluster",
|
|
PathPattern = "/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 = 1,
|
|
TenantCode = "",
|
|
ServiceName = "user-service",
|
|
ClusterId = "cluster-user",
|
|
PathPattern = "/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 = 1,
|
|
TenantCode = "",
|
|
ServiceName = "old-service",
|
|
ClusterId = "old-cluster",
|
|
PathPattern = "/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 = 2,
|
|
TenantCode = "",
|
|
ServiceName = "new-service",
|
|
ClusterId = "new-cluster",
|
|
PathPattern = "/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 = 1,
|
|
TenantCode = "",
|
|
ServiceName = "active-service",
|
|
ClusterId = "cluster-1",
|
|
PathPattern = "/api/active/**",
|
|
Priority = 1,
|
|
Status = 1,
|
|
IsGlobal = true,
|
|
IsDeleted = false
|
|
},
|
|
new GwTenantRoute
|
|
{
|
|
Id = 2,
|
|
TenantCode = "",
|
|
ServiceName = "deleted-service",
|
|
ClusterId = "cluster-2",
|
|
PathPattern = "/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 = 1,
|
|
TenantCode = "",
|
|
ServiceName = "active-service",
|
|
ClusterId = "cluster-1",
|
|
PathPattern = "/api/active/**",
|
|
Priority = 1,
|
|
Status = 1,
|
|
IsGlobal = true,
|
|
IsDeleted = false
|
|
},
|
|
new GwTenantRoute
|
|
{
|
|
Id = 2,
|
|
TenantCode = "",
|
|
ServiceName = "inactive-service",
|
|
ClusterId = "cluster-2",
|
|
PathPattern = "/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 = 1,
|
|
TenantCode = "",
|
|
ServiceName = "user-service",
|
|
ClusterId = "cluster-user",
|
|
PathPattern = "/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());
|
|
}
|
|
}
|