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:
master
2026-02-22 19:27:54 +02:00
parent a29f438f53
commit bd8fee6ed8
373 changed files with 832097 additions and 3369 deletions

View File

@@ -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",

View File

@@ -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);

View File

@@ -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);
}
}
}
}

View File

@@ -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. |