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> _dbContextFactoryMock; private readonly Mock> _loggerMock; public RouteCacheTests() { _dbContextFactoryMock = new Mock>(); _loggerMock = new Mock>(); } private GatewayDbContext CreateInMemoryDbContext(List routes) { var options = new DbContextOptionsBuilder() .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 { 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 { 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 { 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 { 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(); 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 { 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(); 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 { 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 { 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 { 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 { 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()); } }