Fix build and code structure improvements. New but essential UI functionality. CI improvements. Documentation improvements. AI module improvements.

This commit is contained in:
StellaOps Bot
2025-12-26 21:54:17 +02:00
parent 335ff7da16
commit c2b9cd8d1f
3717 changed files with 264714 additions and 48202 deletions

View File

@@ -0,0 +1,184 @@
using System.Net;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using StellaOps.Router.Common.Abstractions;
using StellaOps.Router.Common.Enums;
using StellaOps.Router.Common.Models;
using StellaOps.Router.Gateway.Configuration;
namespace StellaOps.Gateway.WebService.Tests.Integration;
public sealed class GatewayIntegrationTests : IClassFixture<GatewayWebApplicationFactory>
{
private readonly GatewayWebApplicationFactory _factory;
public GatewayIntegrationTests(GatewayWebApplicationFactory factory)
{
_factory = factory;
}
[Fact]
public async Task HealthEndpoint_ReturnsHealthy()
{
var client = _factory.CreateClient();
var response = await client.GetAsync("/health");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}
[Fact]
public async Task HealthLive_ReturnsOk()
{
var client = _factory.CreateClient();
var response = await client.GetAsync("/health/live");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}
[Fact]
public async Task HealthReady_ReturnsOk()
{
var client = _factory.CreateClient();
var response = await client.GetAsync("/health/ready");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}
[Fact]
public async Task OpenApiJson_ReturnsValidOpenApiDocument()
{
var client = _factory.CreateClient();
var response = await client.GetAsync("/openapi.json");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal("application/json; charset=utf-8", response.Content.Headers.ContentType?.ToString());
var content = await response.Content.ReadAsStringAsync();
Assert.Contains("\"openapi\"", content);
Assert.Contains("\"3.1.0\"", content);
}
[Fact]
public async Task OpenApiYaml_ReturnsValidYaml()
{
var client = _factory.CreateClient();
var response = await client.GetAsync("/openapi.yaml");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal("application/yaml; charset=utf-8", response.Content.Headers.ContentType?.ToString());
var content = await response.Content.ReadAsStringAsync();
Assert.Contains("openapi:", content);
}
[Fact]
public async Task OpenApiDiscovery_ReturnsWellKnownEndpoints()
{
var client = _factory.CreateClient();
var response = await client.GetAsync("/.well-known/openapi");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var content = await response.Content.ReadAsStringAsync();
Assert.Contains("openapi_json", content);
Assert.Contains("openapi_yaml", content);
}
[Fact]
public async Task OpenApiJson_WithETag_ReturnsNotModified()
{
var client = _factory.CreateClient();
// First request to get ETag
var response1 = await client.GetAsync("/openapi.json");
Assert.Equal(HttpStatusCode.OK, response1.StatusCode);
var etag = response1.Headers.ETag?.Tag;
Assert.NotNull(etag);
// Second request with If-None-Match
var request2 = new HttpRequestMessage(HttpMethod.Get, "/openapi.json");
request2.Headers.TryAddWithoutValidation("If-None-Match", etag);
var response2 = await client.SendAsync(request2);
Assert.Equal(HttpStatusCode.NotModified, response2.StatusCode);
}
[Fact]
public async Task Metrics_ReturnsPrometheusFormat()
{
var client = _factory.CreateClient();
var response = await client.GetAsync("/metrics");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}
[Fact]
public async Task UnknownRoute_WithNoRegisteredMicroservices_Returns404()
{
var client = _factory.CreateClient();
var response = await client.GetAsync("/api/v1/unknown");
// Without registered microservices, unmatched routes should return 404
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
[Fact]
public async Task CorrelationId_IsReturnedInResponse()
{
var client = _factory.CreateClient();
var response = await client.GetAsync("/health");
Assert.True(response.Headers.Contains("X-Correlation-Id"));
var correlationId = response.Headers.GetValues("X-Correlation-Id").FirstOrDefault();
Assert.False(string.IsNullOrEmpty(correlationId));
}
[Fact]
public async Task CorrelationId_ProvidedInRequest_IsEchoed()
{
var client = _factory.CreateClient();
var requestCorrelationId = Guid.NewGuid().ToString("N");
var request = new HttpRequestMessage(HttpMethod.Get, "/health");
request.Headers.TryAddWithoutValidation("X-Correlation-Id", requestCorrelationId);
var response = await client.SendAsync(request);
Assert.True(response.Headers.Contains("X-Correlation-Id"));
var responseCorrelationId = response.Headers.GetValues("X-Correlation-Id").FirstOrDefault();
Assert.Equal(requestCorrelationId, responseCorrelationId);
}
}
/// <summary>
/// Custom WebApplicationFactory for Gateway integration tests.
/// </summary>
public sealed class GatewayWebApplicationFactory : WebApplicationFactory<Program>
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.UseEnvironment("Development");
builder.ConfigureTestServices(services =>
{
// Override configuration for testing
services.Configure<RouterNodeConfig>(config =>
{
config.Region = "test";
config.NodeId = "test-gateway-01";
config.Environment = "test";
});
});
}
}

View File

@@ -0,0 +1,215 @@
using System.Text.Json;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using StellaOps.Gateway.WebService.Configuration;
using GatewayClaimsStore = StellaOps.Gateway.WebService.Authorization.IEffectiveClaimsStore;
using StellaOps.Gateway.WebService.Services;
using StellaOps.Messaging;
using StellaOps.Messaging.Abstractions;
using StellaOps.Router.Common.Abstractions;
using StellaOps.Router.Common.Enums;
using StellaOps.Router.Common.Models;
using StellaOps.Router.Transport.Messaging;
using StellaOps.Router.Transport.Messaging.Options;
using StellaOps.Router.Transport.Tcp;
using StellaOps.Router.Transport.Tls;
namespace StellaOps.Gateway.WebService.Tests.Integration;
/// <summary>
/// Unit tests for the messaging transport integration in GatewayHostedService and GatewayTransportClient.
/// These tests verify the wiring and event handling without requiring a real Valkey instance.
/// </summary>
public sealed class MessagingTransportIntegrationTests
{
private readonly JsonSerializerOptions _jsonOptions;
public MessagingTransportIntegrationTests()
{
_jsonOptions = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
WriteIndented = false
};
}
[Fact]
public void GatewayHostedService_CanAcceptMessagingServer()
{
// Arrange
var mockQueueFactory = new Mock<IMessageQueueFactory>();
var messagingOptions = Options.Create(new MessagingTransportOptions());
var messagingServer = new MessagingTransportServer(
mockQueueFactory.Object,
messagingOptions,
NullLogger<MessagingTransportServer>.Instance);
var gatewayOptions = Options.Create(new GatewayOptions());
var routingState = new Mock<IGlobalRoutingState>();
var claimsStore = new Mock<GatewayClaimsStore>();
var tcpOptions = Options.Create(new TcpTransportOptions { Port = 29100 });
var tlsOptions = Options.Create(new TlsTransportOptions { Port = 29443 });
var tcpServer = new TcpTransportServer(tcpOptions, NullLogger<TcpTransportServer>.Instance);
var tlsServer = new TlsTransportServer(tlsOptions, NullLogger<TlsTransportServer>.Instance);
var transportClient = new GatewayTransportClient(
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,
openApiCache: null,
messagingServer: messagingServer);
Assert.NotNull(hostedService);
}
[Fact]
public void GatewayHostedService_CanAcceptNullMessagingServer()
{
// Arrange
var gatewayOptions = Options.Create(new GatewayOptions());
var routingState = new Mock<IGlobalRoutingState>();
var claimsStore = new Mock<GatewayClaimsStore>();
var tcpOptions = Options.Create(new TcpTransportOptions { Port = 29101 });
var tlsOptions = Options.Create(new TlsTransportOptions { Port = 29444 });
var tcpServer = new TcpTransportServer(tcpOptions, NullLogger<TcpTransportServer>.Instance);
var tlsServer = new TlsTransportServer(tlsOptions, NullLogger<TlsTransportServer>.Instance);
var transportClient = new GatewayTransportClient(
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,
openApiCache: null,
messagingServer: null);
Assert.NotNull(hostedService);
}
[Fact]
public void GatewayTransportClient_WithMessagingServer_CanBeConstructed()
{
// Arrange
var mockQueueFactory = new Mock<IMessageQueueFactory>();
var messagingOptions = Options.Create(new MessagingTransportOptions());
var messagingServer = new MessagingTransportServer(
mockQueueFactory.Object,
messagingOptions,
NullLogger<MessagingTransportServer>.Instance);
var tcpOptions = Options.Create(new TcpTransportOptions { Port = 29102 });
var tlsOptions = Options.Create(new TlsTransportOptions { Port = 29445 });
var tcpServer = new TcpTransportServer(tcpOptions, NullLogger<TcpTransportServer>.Instance);
var tlsServer = new TlsTransportServer(tlsOptions, NullLogger<TlsTransportServer>.Instance);
// Act
var transportClient = new GatewayTransportClient(
tcpServer,
tlsServer,
NullLogger<GatewayTransportClient>.Instance,
messagingServer);
// Assert
Assert.NotNull(transportClient);
}
[Fact]
public async Task GatewayTransportClient_SendToMessagingConnection_ThrowsWhenServerNull()
{
// Arrange
var tcpOptions = Options.Create(new TcpTransportOptions { Port = 29103 });
var tlsOptions = Options.Create(new TlsTransportOptions { Port = 29446 });
var tcpServer = new TcpTransportServer(tcpOptions, NullLogger<TcpTransportServer>.Instance);
var tlsServer = new TlsTransportServer(tlsOptions, NullLogger<TlsTransportServer>.Instance);
// No messaging server provided
var transportClient = new GatewayTransportClient(
tcpServer,
tlsServer,
NullLogger<GatewayTransportClient>.Instance,
messagingServer: null);
var connection = new ConnectionState
{
ConnectionId = "msg-conn-001",
Instance = new InstanceDescriptor
{
InstanceId = "test-001",
ServiceName = "test-service",
Version = "1.0.0",
Region = "test"
},
TransportType = TransportType.Messaging
};
var frame = new Frame
{
Type = FrameType.Request,
CorrelationId = Guid.NewGuid().ToString("N"),
Payload = new byte[] { 1, 2, 3 }
};
// Act & Assert
await Assert.ThrowsAsync<InvalidOperationException>(async () =>
await transportClient.SendRequestAsync(connection, frame, TimeSpan.FromSeconds(5), CancellationToken.None));
}
[Fact]
public void GatewayOptions_MessagingTransport_HasCorrectDefaults()
{
// Arrange & Act
var options = new GatewayMessagingTransportOptions();
// Assert
Assert.False(options.Enabled);
Assert.Equal("localhost:6379", options.ConnectionString);
Assert.Null(options.Database);
Assert.Equal("router:requests:{service}", options.RequestQueueTemplate);
Assert.Equal("router:responses", options.ResponseQueueName);
Assert.Equal("router-gateway", options.ConsumerGroup);
Assert.Equal("30s", options.RequestTimeout);
Assert.Equal("5m", options.LeaseDuration);
Assert.Equal(10, options.BatchSize);
Assert.Equal("10s", options.HeartbeatInterval);
}
[Fact]
public void GatewayTransportOptions_IncludesMessaging()
{
// Arrange & Act
var options = new GatewayTransportOptions();
// Assert
Assert.NotNull(options.Tcp);
Assert.NotNull(options.Tls);
Assert.NotNull(options.Messaging);
}
}