Fix build and code structure improvements. New but essential UI functionality. CI improvements. Documentation improvements. AI module improvements.
This commit is contained in:
@@ -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";
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user