- RouteCacheTests: 使用 Fengling.Platform.Domain - 更新 Id 类型为 string (Guid) - 更新 PathPattern 为 Match.Path - TenantRoutingMiddlewareTests: 更新 Id 类型为 string
314 lines
8.9 KiB
C#
314 lines
8.9 KiB
C#
using System.Security.Claims;
|
|
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
|
using Microsoft.Extensions.Logging;
|
|
using Moq;
|
|
using Xunit;
|
|
using FluentAssertions;
|
|
using YarpGateway.Middleware;
|
|
using YarpGateway.Services;
|
|
|
|
namespace YarpGateway.Tests.Unit.Middleware;
|
|
|
|
public class TenantRoutingMiddlewareTests
|
|
{
|
|
private readonly Mock<IRouteCache> _routeCacheMock;
|
|
private readonly Mock<ILogger<TenantRoutingMiddleware>> _loggerMock;
|
|
private readonly RequestDelegate _nextDelegate;
|
|
|
|
public TenantRoutingMiddlewareTests()
|
|
{
|
|
_routeCacheMock = new Mock<IRouteCache>();
|
|
_loggerMock = new Mock<ILogger<TenantRoutingMiddleware>>();
|
|
|
|
// Default: call next
|
|
_nextDelegate = _ => Task.CompletedTask;
|
|
}
|
|
|
|
private TenantRoutingMiddleware CreateMiddleware(
|
|
IRouteCache? routeCache = null,
|
|
RequestDelegate? next = null)
|
|
{
|
|
return new TenantRoutingMiddleware(
|
|
next: next ?? _nextDelegate,
|
|
routeCache: routeCache ?? _routeCacheMock.Object,
|
|
logger: _loggerMock.Object
|
|
);
|
|
}
|
|
|
|
private DefaultHttpContext CreateContext(string? tenantId = null, string path = "/api/user-service/users")
|
|
{
|
|
var context = new DefaultHttpContext
|
|
{
|
|
Request = { Path = path }
|
|
};
|
|
|
|
if (!string.IsNullOrEmpty(tenantId))
|
|
{
|
|
context.Request.Headers["X-Tenant-Id"] = tenantId;
|
|
}
|
|
|
|
return context;
|
|
}
|
|
|
|
private DefaultHttpContext CreateAuthenticatedContext(string tenantId, string headerTenantId)
|
|
{
|
|
var context = CreateContext(headerTenantId);
|
|
|
|
var claims = new List<Claim>
|
|
{
|
|
new Claim("tenant", tenantId),
|
|
new Claim(ClaimTypes.NameIdentifier, "user-1")
|
|
};
|
|
|
|
var identity = new ClaimsIdentity(claims, JwtBearerDefaults.AuthenticationScheme);
|
|
context.User = new ClaimsPrincipal(identity);
|
|
|
|
return context;
|
|
}
|
|
|
|
[Fact]
|
|
public async Task InvokeAsync_WithoutTenantHeader_ShouldCallNext()
|
|
{
|
|
// Arrange
|
|
var nextCalled = false;
|
|
var middleware = CreateMiddleware(next: _ => { nextCalled = true; return Task.CompletedTask; });
|
|
var context = CreateContext(tenantId: null);
|
|
|
|
// Act
|
|
await middleware.InvokeAsync(context);
|
|
|
|
// Assert
|
|
nextCalled.Should().BeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task InvokeAsync_WithValidTenantAndRoute_ShouldSetClusterId()
|
|
{
|
|
// Arrange
|
|
var routeInfo = new RouteInfo
|
|
{
|
|
Id = "1",
|
|
ClusterId = "cluster-user-service",
|
|
PathPattern = "/api/user-service/**",
|
|
Priority = 1,
|
|
IsGlobal = false
|
|
};
|
|
|
|
_routeCacheMock
|
|
.Setup(x => x.GetRoute("tenant-1", "user-service"))
|
|
.Returns(routeInfo);
|
|
|
|
var middleware = CreateMiddleware();
|
|
var context = CreateContext(tenantId: "tenant-1");
|
|
|
|
// Act
|
|
await middleware.InvokeAsync(context);
|
|
|
|
// Assert
|
|
context.Items["DynamicClusterId"].Should().Be("cluster-user-service");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task InvokeAsync_WhenRouteNotFound_ShouldCallNext()
|
|
{
|
|
// Arrange
|
|
_routeCacheMock
|
|
.Setup(x => x.GetRoute(It.IsAny<string>(), It.IsAny<string>()))
|
|
.Returns((RouteInfo?)null);
|
|
|
|
var middleware = CreateMiddleware();
|
|
var context = CreateContext(tenantId: "tenant-1");
|
|
|
|
// Act
|
|
await middleware.InvokeAsync(context);
|
|
|
|
// Assert - should not throw, just continue
|
|
context.Items.Should().NotContainKey("DynamicClusterId");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task InvokeAsync_WithTenantIdMismatch_ShouldReturn403()
|
|
{
|
|
// Arrange
|
|
// JWT has tenant-1, but header has tenant-2
|
|
var middleware = CreateMiddleware();
|
|
var context = CreateAuthenticatedContext("tenant-1", "tenant-2");
|
|
|
|
// Act
|
|
await middleware.InvokeAsync(context);
|
|
|
|
// Assert
|
|
context.Response.StatusCode.Should().Be(StatusCodes.Status403Forbidden);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task InvokeAsync_WithMatchingTenant_ShouldAllowRequest()
|
|
{
|
|
// Arrange
|
|
var routeInfo = new RouteInfo
|
|
{
|
|
Id = "1",
|
|
ClusterId = "cluster-1",
|
|
IsGlobal = false
|
|
};
|
|
|
|
_routeCacheMock
|
|
.Setup(x => x.GetRoute("tenant-1", "user-service"))
|
|
.Returns(routeInfo);
|
|
|
|
var middleware = CreateMiddleware();
|
|
var context = CreateAuthenticatedContext("tenant-1", "tenant-1");
|
|
|
|
// Act
|
|
await middleware.InvokeAsync(context);
|
|
|
|
// Assert
|
|
context.Response.StatusCode.Should().NotBe(StatusCodes.Status403Forbidden);
|
|
context.Items["DynamicClusterId"].Should().Be("cluster-1");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task InvokeAsync_WithoutAuthentication_ShouldAllowRequest()
|
|
{
|
|
// Arrange
|
|
var routeInfo = new RouteInfo
|
|
{
|
|
Id = "1",
|
|
ClusterId = "cluster-1",
|
|
IsGlobal = false
|
|
};
|
|
|
|
_routeCacheMock
|
|
.Setup(x => x.GetRoute("tenant-1", "user-service"))
|
|
.Returns(routeInfo);
|
|
|
|
var middleware = CreateMiddleware();
|
|
var context = CreateContext(tenantId: "tenant-1");
|
|
// No authentication set
|
|
|
|
// Act
|
|
await middleware.InvokeAsync(context);
|
|
|
|
// Assert - should proceed without 403
|
|
context.Response.StatusCode.Should().NotBe(StatusCodes.Status403Forbidden);
|
|
context.Items["DynamicClusterId"].Should().Be("cluster-1");
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("/api/user-service/users", "user-service")]
|
|
[InlineData("/api/order-service/orders", "order-service")]
|
|
[InlineData("/api/payment/", "payment")]
|
|
[InlineData("/api/auth", "auth")]
|
|
[InlineData("/other/path", "")]
|
|
public async Task InvokeAsync_ShouldExtractServiceNameFromPath(string path, string expectedServiceName)
|
|
{
|
|
// Arrange
|
|
var middleware = CreateMiddleware();
|
|
var context = CreateContext(tenantId: "tenant-1", path: path);
|
|
|
|
// Act
|
|
await middleware.InvokeAsync(context);
|
|
|
|
// Assert
|
|
if (!string.IsNullOrEmpty(expectedServiceName))
|
|
{
|
|
_routeCacheMock.Verify(
|
|
x => x.GetRoute("tenant-1", expectedServiceName),
|
|
Times.Once);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public async Task InvokeAsync_WithTenantRoute_ShouldLogAsTenantSpecific()
|
|
{
|
|
// Arrange
|
|
var routeInfo = new RouteInfo
|
|
{
|
|
Id = "1",
|
|
ClusterId = "cluster-1",
|
|
IsGlobal = false
|
|
};
|
|
|
|
_routeCacheMock
|
|
.Setup(x => x.GetRoute("tenant-1", "user-service"))
|
|
.Returns(routeInfo);
|
|
|
|
var middleware = CreateMiddleware();
|
|
var context = CreateContext(tenantId: "tenant-1");
|
|
|
|
// Act
|
|
await middleware.InvokeAsync(context);
|
|
|
|
// Assert - just verify it completes without error
|
|
context.Items["DynamicClusterId"].Should().Be("cluster-1");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task InvokeAsync_WithGlobalRoute_ShouldLogAsGlobal()
|
|
{
|
|
// Arrange
|
|
var routeInfo = new RouteInfo
|
|
{
|
|
Id = "1",
|
|
ClusterId = "global-cluster",
|
|
IsGlobal = true
|
|
};
|
|
|
|
_routeCacheMock
|
|
.Setup(x => x.GetRoute("tenant-1", "user-service"))
|
|
.Returns(routeInfo);
|
|
|
|
var middleware = CreateMiddleware();
|
|
var context = CreateContext(tenantId: "tenant-1");
|
|
|
|
// Act
|
|
await middleware.InvokeAsync(context);
|
|
|
|
// Assert
|
|
context.Items["DynamicClusterId"].Should().Be("global-cluster");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task InvokeAsync_WithEmptyPath_ShouldCallNext()
|
|
{
|
|
// Arrange
|
|
var middleware = CreateMiddleware();
|
|
var context = CreateContext(tenantId: "tenant-1", path: "");
|
|
|
|
// Act
|
|
await middleware.InvokeAsync(context);
|
|
|
|
// Assert - should not try to extract service name
|
|
_routeCacheMock.Verify(
|
|
x => x.GetRoute(It.IsAny<string>(), It.IsAny<string>()),
|
|
Times.Never);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task InvokeAsync_PrioritizesTenantRouteOverGlobal()
|
|
{
|
|
// Arrange - This test verifies the middleware calls GetRoute with tenant code
|
|
// The priority logic is in RouteCache, not in middleware
|
|
|
|
var tenantRoute = new RouteInfo
|
|
{
|
|
Id = "1",
|
|
ClusterId = "tenant-specific-cluster",
|
|
IsGlobal = false
|
|
};
|
|
|
|
_routeCacheMock
|
|
.Setup(x => x.GetRoute("tenant-1", "user-service"))
|
|
.Returns(tenantRoute);
|
|
|
|
var middleware = CreateMiddleware();
|
|
var context = CreateContext(tenantId: "tenant-1");
|
|
|
|
// Act
|
|
await middleware.InvokeAsync(context);
|
|
|
|
// Assert
|
|
context.Items["DynamicClusterId"].Should().Be("tenant-specific-cluster");
|
|
}
|
|
}
|