stela ops usage fixes roles propagation and timoeut, one account to support multi tenants, migrations consolidation, search to support documentation, doctor and open api vector db search
This commit is contained in:
@@ -46,7 +46,7 @@ public sealed class AuthorizationMiddlewareTests
|
||||
public async Task InvokeAsync_NoClaims_CallsNext()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateHttpContextWithEndpoint();
|
||||
var context = CreateHttpContextWithEndpoint([new Claim("sub", "alice")]);
|
||||
_claimsStore
|
||||
.Setup(s => s.GetEffectiveClaims("test-service", "GET", "/api/test"))
|
||||
.Returns(Array.Empty<ClaimRequirement>());
|
||||
@@ -59,6 +59,27 @@ public sealed class AuthorizationMiddlewareTests
|
||||
context.Response.StatusCode.Should().NotBe(403);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InvokeAsync_LegacyMetadataWithoutAuthFlag_FailsClosedWith401()
|
||||
{
|
||||
var endpoint = new EndpointDescriptor
|
||||
{
|
||||
ServiceName = "test-service",
|
||||
Version = "1.0.0",
|
||||
Method = "GET",
|
||||
Path = "/api/test",
|
||||
AllowAnonymous = false,
|
||||
RequiresAuthentication = false
|
||||
};
|
||||
|
||||
var context = CreateHttpContextWithEndpoint(endpoint: endpoint);
|
||||
|
||||
await _middleware.InvokeAsync(context);
|
||||
|
||||
_next.Verify(n => n(It.IsAny<HttpContext>()), Times.Never);
|
||||
context.Response.StatusCode.Should().Be(401);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InvokeAsync_UserHasRequiredClaims_CallsNext()
|
||||
{
|
||||
@@ -215,7 +236,7 @@ public sealed class AuthorizationMiddlewareTests
|
||||
public async Task InvokeAsync_ForbiddenResponse_ContainsErrorDetails()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateHttpContextWithEndpoint();
|
||||
var context = CreateHttpContextWithEndpoint([new Claim("sub", "alice")]);
|
||||
context.Response.Body = new MemoryStream();
|
||||
|
||||
_claimsStore
|
||||
@@ -233,18 +254,39 @@ public sealed class AuthorizationMiddlewareTests
|
||||
context.Response.ContentType.Should().Contain("application/json");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InvokeAsync_AllowAnonymousEndpoint_CallsNextWithoutAuthentication()
|
||||
{
|
||||
var endpoint = new EndpointDescriptor
|
||||
{
|
||||
ServiceName = "test-service",
|
||||
Version = "1.0.0",
|
||||
Method = "GET",
|
||||
Path = "/api/test",
|
||||
AllowAnonymous = true
|
||||
};
|
||||
|
||||
var context = CreateHttpContextWithEndpoint(endpoint: endpoint);
|
||||
|
||||
await _middleware.InvokeAsync(context);
|
||||
|
||||
_next.Verify(n => n(context), Times.Once);
|
||||
}
|
||||
|
||||
private static HttpContext CreateHttpContext()
|
||||
{
|
||||
var context = new DefaultHttpContext();
|
||||
return context;
|
||||
}
|
||||
|
||||
private static HttpContext CreateHttpContextWithEndpoint(Claim[]? userClaims = null)
|
||||
private static HttpContext CreateHttpContextWithEndpoint(
|
||||
Claim[]? userClaims = null,
|
||||
EndpointDescriptor? endpoint = null)
|
||||
{
|
||||
var context = new DefaultHttpContext();
|
||||
|
||||
// Set resolved endpoint
|
||||
var endpoint = new EndpointDescriptor
|
||||
endpoint ??= new EndpointDescriptor
|
||||
{
|
||||
ServiceName = "test-service",
|
||||
Version = "1.0.0",
|
||||
|
||||
@@ -56,21 +56,21 @@ public sealed class MessagingTransportIntegrationTests
|
||||
var tlsServer = new TlsTransportServer(tlsOptions, NullLogger<TlsTransportServer>.Instance);
|
||||
|
||||
var transportClient = new GatewayTransportClient(
|
||||
NullLogger<GatewayTransportClient>.Instance,
|
||||
tcpServer,
|
||||
tlsServer,
|
||||
NullLogger<GatewayTransportClient>.Instance,
|
||||
messagingServer);
|
||||
|
||||
// Act & Assert - construction should succeed with messaging server
|
||||
var hostedService = new GatewayHostedService(
|
||||
tcpServer,
|
||||
tlsServer,
|
||||
routingState.Object,
|
||||
transportClient,
|
||||
claimsStore.Object,
|
||||
gatewayOptions,
|
||||
new GatewayServiceStatus(),
|
||||
NullLogger<GatewayHostedService>.Instance,
|
||||
tcpServer: tcpServer,
|
||||
tlsServer: tlsServer,
|
||||
openApiCache: null,
|
||||
messagingServer: messagingServer);
|
||||
|
||||
@@ -92,21 +92,21 @@ public sealed class MessagingTransportIntegrationTests
|
||||
var tlsServer = new TlsTransportServer(tlsOptions, NullLogger<TlsTransportServer>.Instance);
|
||||
|
||||
var transportClient = new GatewayTransportClient(
|
||||
NullLogger<GatewayTransportClient>.Instance,
|
||||
tcpServer,
|
||||
tlsServer,
|
||||
NullLogger<GatewayTransportClient>.Instance,
|
||||
messagingServer: null);
|
||||
|
||||
// Act & Assert - construction should succeed without messaging server
|
||||
var hostedService = new GatewayHostedService(
|
||||
tcpServer,
|
||||
tlsServer,
|
||||
routingState.Object,
|
||||
transportClient,
|
||||
claimsStore.Object,
|
||||
gatewayOptions,
|
||||
new GatewayServiceStatus(),
|
||||
NullLogger<GatewayHostedService>.Instance,
|
||||
tcpServer: tcpServer,
|
||||
tlsServer: tlsServer,
|
||||
openApiCache: null,
|
||||
messagingServer: null);
|
||||
|
||||
@@ -132,9 +132,9 @@ public sealed class MessagingTransportIntegrationTests
|
||||
|
||||
// Act
|
||||
var transportClient = new GatewayTransportClient(
|
||||
NullLogger<GatewayTransportClient>.Instance,
|
||||
tcpServer,
|
||||
tlsServer,
|
||||
NullLogger<GatewayTransportClient>.Instance,
|
||||
messagingServer);
|
||||
|
||||
// Assert
|
||||
@@ -152,9 +152,9 @@ public sealed class MessagingTransportIntegrationTests
|
||||
|
||||
// No messaging server provided
|
||||
var transportClient = new GatewayTransportClient(
|
||||
NullLogger<GatewayTransportClient>.Instance,
|
||||
tcpServer,
|
||||
tlsServer,
|
||||
NullLogger<GatewayTransportClient>.Instance,
|
||||
messagingServer: null);
|
||||
|
||||
var connection = new ConnectionState
|
||||
@@ -193,6 +193,7 @@ public sealed class MessagingTransportIntegrationTests
|
||||
Assert.Equal("localhost:6379", options.ConnectionString);
|
||||
Assert.Null(options.Database);
|
||||
Assert.Equal("router:requests:{service}", options.RequestQueueTemplate);
|
||||
Assert.Equal("gateway-control", options.GatewayControlQueueServiceName);
|
||||
Assert.Equal("router:responses", options.ResponseQueueName);
|
||||
Assert.Equal("router-gateway", options.ConsumerGroup);
|
||||
Assert.Equal("30s", options.RequestTimeout);
|
||||
|
||||
@@ -0,0 +1,293 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Moq;
|
||||
using StellaOps.Gateway.WebService.Middleware;
|
||||
using StellaOps.Gateway.WebService.Routing;
|
||||
using StellaOps.Router.Gateway;
|
||||
using StellaOps.Router.Gateway.Configuration;
|
||||
|
||||
namespace StellaOps.Gateway.WebService.Tests.Middleware;
|
||||
|
||||
[Trait("Category", "Unit")]
|
||||
public sealed class RouteDispatchMiddlewareMicroserviceTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task InvokeAsync_MicroserviceRouteWithTranslatesTo_SetsTranslatedPath()
|
||||
{
|
||||
// Arrange
|
||||
var resolver = new StellaOpsRouteResolver(
|
||||
[
|
||||
new StellaOpsRoute
|
||||
{
|
||||
Type = StellaOpsRouteType.Microservice,
|
||||
Path = "/api/v1/advisory-ai/adapters",
|
||||
TranslatesTo = "http://advisoryai.stella-ops.local/v1/advisory-ai/adapters"
|
||||
}
|
||||
]);
|
||||
|
||||
var httpClientFactory = new Mock<IHttpClientFactory>();
|
||||
httpClientFactory.Setup(factory => factory.CreateClient(It.IsAny<string>())).Returns(new HttpClient());
|
||||
|
||||
var nextCalled = false;
|
||||
var middleware = new RouteDispatchMiddleware(
|
||||
_ =>
|
||||
{
|
||||
nextCalled = true;
|
||||
return Task.CompletedTask;
|
||||
},
|
||||
resolver,
|
||||
httpClientFactory.Object,
|
||||
NullLogger<RouteDispatchMiddleware>.Instance);
|
||||
|
||||
var context = new DefaultHttpContext();
|
||||
context.Request.Path = "/api/v1/advisory-ai/adapters/openai/chat/completions";
|
||||
|
||||
// Act
|
||||
await middleware.InvokeAsync(context);
|
||||
|
||||
// Assert
|
||||
Assert.True(nextCalled);
|
||||
Assert.Equal(
|
||||
"/v1/advisory-ai/adapters/openai/chat/completions",
|
||||
context.Items[RouterHttpContextKeys.TranslatedRequestPath] as string);
|
||||
Assert.Equal(
|
||||
"advisoryai",
|
||||
context.Items[RouterHttpContextKeys.RouteTargetMicroservice] as string);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InvokeAsync_MicroserviceRouteWithoutTranslatesTo_DoesNotSetTranslatedPath()
|
||||
{
|
||||
// Arrange
|
||||
var resolver = new StellaOpsRouteResolver(
|
||||
[
|
||||
new StellaOpsRoute
|
||||
{
|
||||
Type = StellaOpsRouteType.Microservice,
|
||||
Path = "/api/v1/timeline"
|
||||
}
|
||||
]);
|
||||
|
||||
var httpClientFactory = new Mock<IHttpClientFactory>();
|
||||
httpClientFactory.Setup(factory => factory.CreateClient(It.IsAny<string>())).Returns(new HttpClient());
|
||||
|
||||
var middleware = new RouteDispatchMiddleware(
|
||||
_ => Task.CompletedTask,
|
||||
resolver,
|
||||
httpClientFactory.Object,
|
||||
NullLogger<RouteDispatchMiddleware>.Instance);
|
||||
|
||||
var context = new DefaultHttpContext();
|
||||
context.Request.Path = "/api/v1/timeline";
|
||||
|
||||
// Act
|
||||
await middleware.InvokeAsync(context);
|
||||
|
||||
// Assert
|
||||
Assert.False(context.Items.ContainsKey(RouterHttpContextKeys.TranslatedRequestPath));
|
||||
Assert.Equal(
|
||||
"timeline",
|
||||
context.Items[RouterHttpContextKeys.RouteTargetMicroservice] as string);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InvokeAsync_MicroserviceRouteWithGenericHost_PrefersPathHintForTargetService()
|
||||
{
|
||||
// Arrange
|
||||
var resolver = new StellaOpsRouteResolver(
|
||||
[
|
||||
new StellaOpsRoute
|
||||
{
|
||||
Type = StellaOpsRouteType.Microservice,
|
||||
Path = "/vulnexplorer",
|
||||
TranslatesTo = "http://api.stella-ops.local"
|
||||
}
|
||||
]);
|
||||
|
||||
var httpClientFactory = new Mock<IHttpClientFactory>();
|
||||
httpClientFactory.Setup(factory => factory.CreateClient(It.IsAny<string>())).Returns(new HttpClient());
|
||||
|
||||
var middleware = new RouteDispatchMiddleware(
|
||||
_ => Task.CompletedTask,
|
||||
resolver,
|
||||
httpClientFactory.Object,
|
||||
NullLogger<RouteDispatchMiddleware>.Instance);
|
||||
|
||||
var context = new DefaultHttpContext();
|
||||
context.Request.Path = "/vulnexplorer";
|
||||
|
||||
// Act
|
||||
await middleware.InvokeAsync(context);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(
|
||||
"vulnexplorer",
|
||||
context.Items[RouterHttpContextKeys.RouteTargetMicroservice] as string);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InvokeAsync_MicroserviceRouteWithDefaultTimeout_SetsRouteTimeoutContextItem()
|
||||
{
|
||||
// Arrange
|
||||
var resolver = new StellaOpsRouteResolver(
|
||||
[
|
||||
new StellaOpsRoute
|
||||
{
|
||||
Type = StellaOpsRouteType.Microservice,
|
||||
Path = "/api/v1/timeline",
|
||||
DefaultTimeout = "15s"
|
||||
}
|
||||
]);
|
||||
|
||||
var httpClientFactory = new Mock<IHttpClientFactory>();
|
||||
httpClientFactory.Setup(factory => factory.CreateClient(It.IsAny<string>())).Returns(new HttpClient());
|
||||
|
||||
var middleware = new RouteDispatchMiddleware(
|
||||
_ => Task.CompletedTask,
|
||||
resolver,
|
||||
httpClientFactory.Object,
|
||||
NullLogger<RouteDispatchMiddleware>.Instance);
|
||||
|
||||
var context = new DefaultHttpContext();
|
||||
context.Request.Path = "/api/v1/timeline/events";
|
||||
|
||||
// Act
|
||||
await middleware.InvokeAsync(context);
|
||||
|
||||
// Assert
|
||||
Assert.True(context.Items.TryGetValue(RouterHttpContextKeys.RouteDefaultTimeout, out var timeoutObj));
|
||||
Assert.IsType<TimeSpan>(timeoutObj);
|
||||
Assert.Equal(TimeSpan.FromSeconds(15), (TimeSpan)timeoutObj!);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InvokeAsync_MicroserviceRoute_BrowserNavigation_ServesSpaFallback()
|
||||
{
|
||||
var tempDir = Path.Combine(Path.GetTempPath(), $"stella-router-spa-{Guid.NewGuid():N}");
|
||||
Directory.CreateDirectory(tempDir);
|
||||
|
||||
try
|
||||
{
|
||||
await File.WriteAllTextAsync(
|
||||
Path.Combine(tempDir, "index.html"),
|
||||
"<!DOCTYPE html><html><body><h1>SPA Root</h1></body></html>");
|
||||
|
||||
var resolver = new StellaOpsRouteResolver(
|
||||
[
|
||||
new StellaOpsRoute
|
||||
{
|
||||
Type = StellaOpsRouteType.Microservice,
|
||||
Path = "/policy",
|
||||
TranslatesTo = "http://policy-gateway.stella-ops.local"
|
||||
},
|
||||
new StellaOpsRoute
|
||||
{
|
||||
Type = StellaOpsRouteType.StaticFiles,
|
||||
Path = "/",
|
||||
TranslatesTo = tempDir,
|
||||
Headers = new Dictionary<string, string> { ["x-spa-fallback"] = "true" }
|
||||
}
|
||||
]);
|
||||
|
||||
var httpClientFactory = new Mock<IHttpClientFactory>();
|
||||
httpClientFactory.Setup(factory => factory.CreateClient(It.IsAny<string>())).Returns(new HttpClient());
|
||||
|
||||
var nextCalled = false;
|
||||
var middleware = new RouteDispatchMiddleware(
|
||||
_ =>
|
||||
{
|
||||
nextCalled = true;
|
||||
return Task.CompletedTask;
|
||||
},
|
||||
resolver,
|
||||
httpClientFactory.Object,
|
||||
NullLogger<RouteDispatchMiddleware>.Instance);
|
||||
|
||||
var context = new DefaultHttpContext();
|
||||
context.Request.Method = HttpMethods.Get;
|
||||
context.Request.Path = "/policy";
|
||||
context.Request.Headers.Accept = "text/html,application/xhtml+xml";
|
||||
context.Response.Body = new MemoryStream();
|
||||
|
||||
await middleware.InvokeAsync(context);
|
||||
|
||||
Assert.False(nextCalled);
|
||||
Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode);
|
||||
context.Response.Body.Position = 0;
|
||||
var body = await new StreamReader(context.Response.Body).ReadToEndAsync();
|
||||
Assert.Contains("SPA Root", body, StringComparison.Ordinal);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (Directory.Exists(tempDir))
|
||||
{
|
||||
Directory.Delete(tempDir, recursive: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InvokeAsync_MicroserviceApiPath_DoesNotUseSpaFallback()
|
||||
{
|
||||
var tempDir = Path.Combine(Path.GetTempPath(), $"stella-router-spa-api-{Guid.NewGuid():N}");
|
||||
Directory.CreateDirectory(tempDir);
|
||||
|
||||
try
|
||||
{
|
||||
await File.WriteAllTextAsync(
|
||||
Path.Combine(tempDir, "index.html"),
|
||||
"<!DOCTYPE html><html><body><h1>SPA Root</h1></body></html>");
|
||||
|
||||
var resolver = new StellaOpsRouteResolver(
|
||||
[
|
||||
new StellaOpsRoute
|
||||
{
|
||||
Type = StellaOpsRouteType.Microservice,
|
||||
Path = "/api/v1/policy",
|
||||
TranslatesTo = "http://policy-gateway.stella-ops.local/api/v1/policy"
|
||||
},
|
||||
new StellaOpsRoute
|
||||
{
|
||||
Type = StellaOpsRouteType.StaticFiles,
|
||||
Path = "/",
|
||||
TranslatesTo = tempDir,
|
||||
Headers = new Dictionary<string, string> { ["x-spa-fallback"] = "true" }
|
||||
}
|
||||
]);
|
||||
|
||||
var httpClientFactory = new Mock<IHttpClientFactory>();
|
||||
httpClientFactory.Setup(factory => factory.CreateClient(It.IsAny<string>())).Returns(new HttpClient());
|
||||
|
||||
var nextCalled = false;
|
||||
var middleware = new RouteDispatchMiddleware(
|
||||
_ =>
|
||||
{
|
||||
nextCalled = true;
|
||||
return Task.CompletedTask;
|
||||
},
|
||||
resolver,
|
||||
httpClientFactory.Object,
|
||||
NullLogger<RouteDispatchMiddleware>.Instance);
|
||||
|
||||
var context = new DefaultHttpContext();
|
||||
context.Request.Method = HttpMethods.Get;
|
||||
context.Request.Path = "/api/v1/policy/check";
|
||||
context.Request.Headers.Accept = "text/html,application/xhtml+xml";
|
||||
context.Response.Body = new MemoryStream();
|
||||
|
||||
await middleware.InvokeAsync(context);
|
||||
|
||||
Assert.True(nextCalled);
|
||||
Assert.Equal(
|
||||
"policy-gateway",
|
||||
context.Items[RouterHttpContextKeys.RouteTargetMicroservice] as string);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (Directory.Exists(tempDir))
|
||||
{
|
||||
Directory.Delete(tempDir, recursive: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,3 +9,4 @@ Source of truth: `docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229
|
||||
| AUDIT-0349-T | DONE | Revalidated 2026-01-07; test coverage audit for Router Gateway WebService tests. |
|
||||
| AUDIT-0349-A | DONE | Waived (test project; revalidated 2026-01-07). |
|
||||
| REMED-06 | DONE | SOLID review notes captured for SPRINT_20260130_002. |
|
||||
| RGH-01-T | DONE | 2026-02-22: Added route-dispatch unit tests for microservice SPA fallback and API-prefix bypass behavior. |
|
||||
|
||||
Reference in New Issue
Block a user