feat: add stella-callgraph-node for JavaScript/TypeScript call graph extraction
- Implemented a new tool `stella-callgraph-node` that extracts call graphs from JavaScript/TypeScript projects using Babel AST. - Added command-line interface with options for JSON output and help. - Included functionality to analyze project structure, detect functions, and build call graphs. - Created a package.json file for dependency management. feat: introduce stella-callgraph-python for Python call graph extraction - Developed `stella-callgraph-python` to extract call graphs from Python projects using AST analysis. - Implemented command-line interface with options for JSON output and verbose logging. - Added framework detection to identify popular web frameworks and their entry points. - Created an AST analyzer to traverse Python code and extract function definitions and calls. - Included requirements.txt for project dependencies. chore: add framework detection for Python projects - Implemented framework detection logic to identify frameworks like Flask, FastAPI, Django, and others based on project files and import patterns. - Enhanced the AST analyzer to recognize entry points based on decorators and function definitions.
This commit is contained in:
@@ -0,0 +1,259 @@
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Router.Common.Enums;
|
||||
using StellaOps.Router.Common.Models;
|
||||
using StellaOps.Router.Gateway.Configuration;
|
||||
using StellaOps.Router.Gateway.Routing;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Router.Gateway.Tests;
|
||||
|
||||
public sealed class DefaultRoutingPluginTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task ChooseInstanceAsync_WhenNoCandidates_ReturnsNull()
|
||||
{
|
||||
var plugin = CreatePlugin(gatewayRegion: "eu1");
|
||||
|
||||
var decision = await plugin.ChooseInstanceAsync(
|
||||
new RoutingContext
|
||||
{
|
||||
Method = "GET",
|
||||
Path = "/items",
|
||||
Headers = new Dictionary<string, string>(),
|
||||
Endpoint = new EndpointDescriptor
|
||||
{
|
||||
ServiceName = "inventory",
|
||||
Version = "1.0.0",
|
||||
Method = "GET",
|
||||
Path = "/items"
|
||||
},
|
||||
AvailableConnections = [],
|
||||
GatewayRegion = "eu1",
|
||||
RequestedVersion = null,
|
||||
CancellationToken = CancellationToken.None
|
||||
},
|
||||
CancellationToken.None);
|
||||
|
||||
decision.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ChooseInstanceAsync_WhenRequestedVersionDoesNotMatch_ReturnsNull()
|
||||
{
|
||||
var plugin = CreatePlugin(
|
||||
gatewayRegion: "eu1",
|
||||
options: new RoutingOptions
|
||||
{
|
||||
StrictVersionMatching = true,
|
||||
DefaultVersion = null
|
||||
});
|
||||
|
||||
var candidates = new List<ConnectionState>
|
||||
{
|
||||
CreateConnection("inv-1", "inventory", "1.0.0", region: "eu1", status: InstanceHealthStatus.Healthy)
|
||||
};
|
||||
|
||||
var decision = await plugin.ChooseInstanceAsync(
|
||||
CreateContext(
|
||||
requestedVersion: "2.0.0",
|
||||
candidates: candidates),
|
||||
CancellationToken.None);
|
||||
|
||||
decision.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ChooseInstanceAsync_PrefersHealthyOverDegraded()
|
||||
{
|
||||
var plugin = CreatePlugin(
|
||||
gatewayRegion: "eu1",
|
||||
options: new RoutingOptions
|
||||
{
|
||||
StrictVersionMatching = true,
|
||||
DefaultVersion = null,
|
||||
PreferLocalRegion = true,
|
||||
AllowDegradedInstances = true,
|
||||
TieBreaker = TieBreakerMode.RoundRobin
|
||||
});
|
||||
|
||||
var degraded = CreateConnection("inv-degraded", "inventory", "1.0.0", region: "eu1", status: InstanceHealthStatus.Degraded);
|
||||
degraded.AveragePingMs = 1;
|
||||
|
||||
var healthy = CreateConnection("inv-healthy", "inventory", "1.0.0", region: "eu1", status: InstanceHealthStatus.Healthy);
|
||||
healthy.AveragePingMs = 50;
|
||||
|
||||
var decision = await plugin.ChooseInstanceAsync(
|
||||
CreateContext(
|
||||
requestedVersion: "1.0.0",
|
||||
candidates: [degraded, healthy]),
|
||||
CancellationToken.None);
|
||||
|
||||
decision.Should().NotBeNull();
|
||||
decision!.Connection.ConnectionId.Should().Be("inv-healthy");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ChooseInstanceAsync_PrefersLocalRegionOverRemote()
|
||||
{
|
||||
var plugin = CreatePlugin(
|
||||
gatewayRegion: "eu1",
|
||||
options: new RoutingOptions
|
||||
{
|
||||
StrictVersionMatching = true,
|
||||
DefaultVersion = null,
|
||||
PreferLocalRegion = true,
|
||||
AllowDegradedInstances = true,
|
||||
TieBreaker = TieBreakerMode.RoundRobin
|
||||
});
|
||||
|
||||
var remote = CreateConnection("inv-us1", "inventory", "1.0.0", region: "us1", status: InstanceHealthStatus.Healthy);
|
||||
var local = CreateConnection("inv-eu1", "inventory", "1.0.0", region: "eu1", status: InstanceHealthStatus.Healthy);
|
||||
|
||||
var decision = await plugin.ChooseInstanceAsync(
|
||||
CreateContext(
|
||||
requestedVersion: "1.0.0",
|
||||
candidates: [remote, local]),
|
||||
CancellationToken.None);
|
||||
|
||||
decision.Should().NotBeNull();
|
||||
decision!.Connection.ConnectionId.Should().Be("inv-eu1");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ChooseInstanceAsync_WhenNoLocal_UsesNeighborRegion()
|
||||
{
|
||||
var plugin = CreatePlugin(
|
||||
gatewayRegion: "eu1",
|
||||
gatewayNeighbors: ["eu2"],
|
||||
options: new RoutingOptions
|
||||
{
|
||||
StrictVersionMatching = true,
|
||||
DefaultVersion = null,
|
||||
PreferLocalRegion = true,
|
||||
AllowDegradedInstances = true,
|
||||
TieBreaker = TieBreakerMode.RoundRobin
|
||||
});
|
||||
|
||||
var neighbor = CreateConnection("inv-eu2", "inventory", "1.0.0", region: "eu2", status: InstanceHealthStatus.Healthy);
|
||||
var remote = CreateConnection("inv-us1", "inventory", "1.0.0", region: "us1", status: InstanceHealthStatus.Healthy);
|
||||
|
||||
var decision = await plugin.ChooseInstanceAsync(
|
||||
CreateContext(
|
||||
requestedVersion: "1.0.0",
|
||||
candidates: [remote, neighbor]),
|
||||
CancellationToken.None);
|
||||
|
||||
decision.Should().NotBeNull();
|
||||
decision!.Connection.ConnectionId.Should().Be("inv-eu2");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ChooseInstanceAsync_WhenTied_UsesRoundRobin()
|
||||
{
|
||||
var plugin = CreatePlugin(
|
||||
gatewayRegion: "eu1",
|
||||
options: new RoutingOptions
|
||||
{
|
||||
StrictVersionMatching = true,
|
||||
DefaultVersion = null,
|
||||
PreferLocalRegion = false,
|
||||
AllowDegradedInstances = true,
|
||||
TieBreaker = TieBreakerMode.RoundRobin,
|
||||
PingToleranceMs = 1_000
|
||||
});
|
||||
|
||||
var heartbeat = DateTime.UtcNow;
|
||||
|
||||
var a = CreateConnection("inv-a", "inventory", "1.0.0", region: "us1", status: InstanceHealthStatus.Healthy);
|
||||
a.AveragePingMs = 10;
|
||||
a.LastHeartbeatUtc = heartbeat;
|
||||
|
||||
var b = CreateConnection("inv-b", "inventory", "1.0.0", region: "us1", status: InstanceHealthStatus.Healthy);
|
||||
b.AveragePingMs = 10;
|
||||
b.LastHeartbeatUtc = heartbeat;
|
||||
|
||||
var ctx = CreateContext(requestedVersion: "1.0.0", candidates: [a, b]);
|
||||
|
||||
var decision1 = await plugin.ChooseInstanceAsync(ctx, CancellationToken.None);
|
||||
var decision2 = await plugin.ChooseInstanceAsync(ctx, CancellationToken.None);
|
||||
|
||||
decision1.Should().NotBeNull();
|
||||
decision2.Should().NotBeNull();
|
||||
|
||||
decision1!.Connection.ConnectionId.Should().Be("inv-a");
|
||||
decision2!.Connection.ConnectionId.Should().Be("inv-b");
|
||||
}
|
||||
|
||||
private static DefaultRoutingPlugin CreatePlugin(
|
||||
string gatewayRegion,
|
||||
RoutingOptions? options = null,
|
||||
IReadOnlyList<string>? gatewayNeighbors = null)
|
||||
{
|
||||
options ??= new RoutingOptions
|
||||
{
|
||||
StrictVersionMatching = true,
|
||||
DefaultVersion = null,
|
||||
PreferLocalRegion = true,
|
||||
AllowDegradedInstances = true,
|
||||
TieBreaker = TieBreakerMode.RoundRobin
|
||||
};
|
||||
|
||||
var node = new RouterNodeConfig
|
||||
{
|
||||
Region = gatewayRegion,
|
||||
NeighborRegions = gatewayNeighbors?.ToList() ?? []
|
||||
};
|
||||
|
||||
return new DefaultRoutingPlugin(
|
||||
Options.Create(options),
|
||||
Options.Create(node));
|
||||
}
|
||||
|
||||
private static RoutingContext CreateContext(
|
||||
string? requestedVersion,
|
||||
IReadOnlyList<ConnectionState> candidates)
|
||||
{
|
||||
return new RoutingContext
|
||||
{
|
||||
Method = "GET",
|
||||
Path = "/items",
|
||||
Headers = new Dictionary<string, string>(),
|
||||
Endpoint = new EndpointDescriptor
|
||||
{
|
||||
ServiceName = "inventory",
|
||||
Version = "1.0.0",
|
||||
Method = "GET",
|
||||
Path = "/items"
|
||||
},
|
||||
AvailableConnections = candidates,
|
||||
GatewayRegion = "eu1",
|
||||
RequestedVersion = requestedVersion,
|
||||
CancellationToken = CancellationToken.None
|
||||
};
|
||||
}
|
||||
|
||||
private static ConnectionState CreateConnection(
|
||||
string connectionId,
|
||||
string serviceName,
|
||||
string version,
|
||||
string region,
|
||||
InstanceHealthStatus status)
|
||||
{
|
||||
return new ConnectionState
|
||||
{
|
||||
ConnectionId = connectionId,
|
||||
Instance = new InstanceDescriptor
|
||||
{
|
||||
InstanceId = connectionId,
|
||||
ServiceName = serviceName,
|
||||
Version = version,
|
||||
Region = region
|
||||
},
|
||||
Status = status,
|
||||
LastHeartbeatUtc = DateTime.UtcNow,
|
||||
TransportType = TransportType.InMemory
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
using FluentAssertions;
|
||||
using StellaOps.Router.Common.Enums;
|
||||
using StellaOps.Router.Common.Models;
|
||||
using StellaOps.Router.Gateway.State;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Router.Gateway.Tests;
|
||||
|
||||
public sealed class InMemoryRoutingStateTests
|
||||
{
|
||||
[Fact]
|
||||
public void ResolveEndpoint_WhenExactMatch_ReturnsEndpointDescriptor()
|
||||
{
|
||||
var routingState = new InMemoryRoutingState();
|
||||
|
||||
var connection = CreateConnection("conn-1", "inventory", "1.0.0", region: "test");
|
||||
connection.Endpoints[("GET", "/items")] = new EndpointDescriptor
|
||||
{
|
||||
ServiceName = "inventory",
|
||||
Version = "1.0.0",
|
||||
Method = "GET",
|
||||
Path = "/items"
|
||||
};
|
||||
|
||||
routingState.AddConnection(connection);
|
||||
|
||||
var resolved = routingState.ResolveEndpoint("GET", "/items");
|
||||
|
||||
resolved.Should().NotBeNull();
|
||||
resolved!.ServiceName.Should().Be("inventory");
|
||||
resolved.Version.Should().Be("1.0.0");
|
||||
resolved.Method.Should().Be("GET");
|
||||
resolved.Path.Should().Be("/items");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResolveEndpoint_WhenTemplateMatch_ReturnsEndpointDescriptor()
|
||||
{
|
||||
var routingState = new InMemoryRoutingState();
|
||||
|
||||
var connection = CreateConnection("conn-1", "inventory", "1.0.0", region: "test");
|
||||
connection.Endpoints[("GET", "/items/{sku}")] = new EndpointDescriptor
|
||||
{
|
||||
ServiceName = "inventory",
|
||||
Version = "1.0.0",
|
||||
Method = "GET",
|
||||
Path = "/items/{sku}"
|
||||
};
|
||||
|
||||
routingState.AddConnection(connection);
|
||||
|
||||
var resolved = routingState.ResolveEndpoint("GET", "/items/SKU-001");
|
||||
|
||||
resolved.Should().NotBeNull();
|
||||
resolved!.Path.Should().Be("/items/{sku}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RemoveConnection_RemovesEndpointsFromIndex()
|
||||
{
|
||||
var routingState = new InMemoryRoutingState();
|
||||
|
||||
var connection = CreateConnection("conn-1", "inventory", "1.0.0", region: "test");
|
||||
connection.Endpoints[("GET", "/items")] = new EndpointDescriptor
|
||||
{
|
||||
ServiceName = "inventory",
|
||||
Version = "1.0.0",
|
||||
Method = "GET",
|
||||
Path = "/items"
|
||||
};
|
||||
|
||||
routingState.AddConnection(connection);
|
||||
routingState.ResolveEndpoint("GET", "/items").Should().NotBeNull();
|
||||
|
||||
routingState.RemoveConnection("conn-1");
|
||||
|
||||
routingState.ResolveEndpoint("GET", "/items").Should().BeNull();
|
||||
routingState.GetAllConnections().Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetConnectionsFor_FiltersByServiceAndVersion()
|
||||
{
|
||||
var routingState = new InMemoryRoutingState();
|
||||
|
||||
var inventoryV1 = CreateConnection("inv-v1", "inventory", "1.0.0", region: "test");
|
||||
inventoryV1.Endpoints[("GET", "/items")] = new EndpointDescriptor
|
||||
{
|
||||
ServiceName = "inventory",
|
||||
Version = "1.0.0",
|
||||
Method = "GET",
|
||||
Path = "/items"
|
||||
};
|
||||
|
||||
var inventoryV2 = CreateConnection("inv-v2", "inventory", "2.0.0", region: "test");
|
||||
inventoryV2.Endpoints[("GET", "/items")] = new EndpointDescriptor
|
||||
{
|
||||
ServiceName = "inventory",
|
||||
Version = "2.0.0",
|
||||
Method = "GET",
|
||||
Path = "/items"
|
||||
};
|
||||
|
||||
routingState.AddConnection(inventoryV1);
|
||||
routingState.AddConnection(inventoryV2);
|
||||
|
||||
var connections = routingState.GetConnectionsFor(
|
||||
serviceName: "inventory",
|
||||
version: "1.0.0",
|
||||
method: "GET",
|
||||
path: "/items");
|
||||
|
||||
connections.Should().HaveCount(1);
|
||||
connections[0].ConnectionId.Should().Be("inv-v1");
|
||||
}
|
||||
|
||||
private static ConnectionState CreateConnection(
|
||||
string connectionId,
|
||||
string serviceName,
|
||||
string version,
|
||||
string region)
|
||||
{
|
||||
return new ConnectionState
|
||||
{
|
||||
ConnectionId = connectionId,
|
||||
Instance = new InstanceDescriptor
|
||||
{
|
||||
InstanceId = connectionId,
|
||||
ServiceName = serviceName,
|
||||
Version = version,
|
||||
Region = region
|
||||
},
|
||||
Status = InstanceHealthStatus.Healthy,
|
||||
LastHeartbeatUtc = DateTime.UtcNow,
|
||||
TransportType = TransportType.InMemory
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,218 @@
|
||||
using System.Security.Claims;
|
||||
using System.Text.Json;
|
||||
using FluentAssertions;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.FileProviders;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Router.Common.Abstractions;
|
||||
using StellaOps.Router.Common.Models;
|
||||
using StellaOps.Router.Gateway.Authorization;
|
||||
using StellaOps.Router.Gateway.Configuration;
|
||||
using StellaOps.Router.Gateway.Middleware;
|
||||
using StellaOps.Router.Gateway.State;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Router.Gateway.Tests;
|
||||
|
||||
public sealed class MiddlewareErrorScenarioTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task EndpointResolutionMiddleware_WhenNoEndpoint_Returns404StructuredError()
|
||||
{
|
||||
var context = CreateContext(method: "GET", path: "/missing");
|
||||
var routingState = new InMemoryRoutingState();
|
||||
|
||||
var nextCalled = false;
|
||||
var middleware = new EndpointResolutionMiddleware(_ =>
|
||||
{
|
||||
nextCalled = true;
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
|
||||
await middleware.Invoke(context, routingState);
|
||||
|
||||
nextCalled.Should().BeFalse();
|
||||
context.Response.StatusCode.Should().Be(StatusCodes.Status404NotFound);
|
||||
|
||||
var body = ReadJson(context);
|
||||
body.GetProperty("error").GetString().Should().Be("Endpoint not found");
|
||||
body.GetProperty("status").GetInt32().Should().Be(404);
|
||||
body.GetProperty("method").GetString().Should().Be("GET");
|
||||
body.GetProperty("path").GetString().Should().Be("/missing");
|
||||
body.GetProperty("traceId").GetString().Should().Be("trace-1");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RoutingDecisionMiddleware_WhenNoInstances_Returns503StructuredError()
|
||||
{
|
||||
var context = CreateContext(method: "GET", path: "/items");
|
||||
context.Items[RouterHttpContextKeys.EndpointDescriptor] = new EndpointDescriptor
|
||||
{
|
||||
ServiceName = "inventory",
|
||||
Version = "1.0.0",
|
||||
Method = "GET",
|
||||
Path = "/items"
|
||||
};
|
||||
|
||||
var routingState = new InMemoryRoutingState();
|
||||
var plugin = new NullRoutingPlugin();
|
||||
|
||||
var nextCalled = false;
|
||||
var middleware = new RoutingDecisionMiddleware(_ =>
|
||||
{
|
||||
nextCalled = true;
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
|
||||
await middleware.Invoke(
|
||||
context,
|
||||
plugin,
|
||||
routingState,
|
||||
Options.Create(new RouterNodeConfig { Region = "eu1", NodeId = "gw-eu1-01" }),
|
||||
Options.Create(new RoutingOptions { DefaultVersion = null }));
|
||||
|
||||
nextCalled.Should().BeFalse();
|
||||
context.Response.StatusCode.Should().Be(StatusCodes.Status503ServiceUnavailable);
|
||||
|
||||
var body = ReadJson(context);
|
||||
body.GetProperty("error").GetString().Should().Be("No instances available");
|
||||
body.GetProperty("status").GetInt32().Should().Be(503);
|
||||
body.GetProperty("service").GetString().Should().Be("inventory");
|
||||
body.GetProperty("version").GetString().Should().Be("1.0.0");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AuthorizationMiddleware_WhenMissingClaim_Returns403StructuredError()
|
||||
{
|
||||
var context = CreateContext(method: "GET", path: "/items");
|
||||
context.User = new ClaimsPrincipal(new ClaimsIdentity(
|
||||
[new Claim("scope", "user")],
|
||||
authenticationType: "test"));
|
||||
|
||||
context.Items[RouterHttpContextKeys.EndpointDescriptor] = new EndpointDescriptor
|
||||
{
|
||||
ServiceName = "inventory",
|
||||
Version = "1.0.0",
|
||||
Method = "GET",
|
||||
Path = "/items",
|
||||
RequiringClaims = []
|
||||
};
|
||||
|
||||
var claimsStore = new StaticClaimsStore(
|
||||
[new ClaimRequirement { Type = "scope", Value = "admin" }]);
|
||||
|
||||
var nextCalled = false;
|
||||
var middleware = new AuthorizationMiddleware(
|
||||
_ =>
|
||||
{
|
||||
nextCalled = true;
|
||||
return Task.CompletedTask;
|
||||
},
|
||||
claimsStore,
|
||||
NullLogger<AuthorizationMiddleware>.Instance);
|
||||
|
||||
await middleware.InvokeAsync(context);
|
||||
|
||||
nextCalled.Should().BeFalse();
|
||||
context.Response.StatusCode.Should().Be(StatusCodes.Status403Forbidden);
|
||||
|
||||
var body = ReadJson(context);
|
||||
body.GetProperty("error").GetString().Should().Be("Forbidden");
|
||||
body.GetProperty("status").GetInt32().Should().Be(403);
|
||||
body.GetProperty("service").GetString().Should().Be("inventory");
|
||||
body.GetProperty("version").GetString().Should().Be("1.0.0");
|
||||
body.GetProperty("details").GetProperty("requiredClaimType").GetString().Should().Be("scope");
|
||||
body.GetProperty("details").GetProperty("requiredClaimValue").GetString().Should().Be("admin");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GlobalErrorHandlerMiddleware_WhenUnhandledException_Returns500StructuredError()
|
||||
{
|
||||
var context = CreateContext(method: "GET", path: "/boom");
|
||||
var environment = new TestHostEnvironment { EnvironmentName = Environments.Development };
|
||||
|
||||
var middleware = new GlobalErrorHandlerMiddleware(
|
||||
_ => throw new InvalidOperationException("boom"),
|
||||
NullLogger<GlobalErrorHandlerMiddleware>.Instance,
|
||||
environment);
|
||||
|
||||
await middleware.Invoke(context);
|
||||
|
||||
context.Response.StatusCode.Should().Be(StatusCodes.Status500InternalServerError);
|
||||
|
||||
var body = ReadJson(context);
|
||||
body.GetProperty("error").GetString().Should().Be("Internal Server Error");
|
||||
body.GetProperty("status").GetInt32().Should().Be(500);
|
||||
body.GetProperty("message").GetString().Should().Be("boom");
|
||||
}
|
||||
|
||||
private static DefaultHttpContext CreateContext(string method, string path, string? queryString = null)
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
services.AddLogging();
|
||||
services.AddOptions();
|
||||
|
||||
var context = new DefaultHttpContext
|
||||
{
|
||||
RequestServices = services.BuildServiceProvider()
|
||||
};
|
||||
|
||||
context.TraceIdentifier = "trace-1";
|
||||
context.Request.Method = method;
|
||||
context.Request.Path = path;
|
||||
context.Request.QueryString = string.IsNullOrWhiteSpace(queryString) ? QueryString.Empty : new QueryString(queryString);
|
||||
context.Response.Body = new MemoryStream();
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
private static JsonElement ReadJson(DefaultHttpContext context)
|
||||
{
|
||||
context.Response.Body.Position = 0;
|
||||
using var doc = JsonDocument.Parse(context.Response.Body);
|
||||
return doc.RootElement.Clone();
|
||||
}
|
||||
|
||||
private sealed class NullRoutingPlugin : IRoutingPlugin
|
||||
{
|
||||
public Task<RoutingDecision?> ChooseInstanceAsync(RoutingContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.FromResult<RoutingDecision?>(null);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class StaticClaimsStore : IEffectiveClaimsStore
|
||||
{
|
||||
private readonly IReadOnlyList<ClaimRequirement> _claims;
|
||||
|
||||
public StaticClaimsStore(IReadOnlyList<ClaimRequirement> claims)
|
||||
{
|
||||
_claims = claims;
|
||||
}
|
||||
|
||||
public IReadOnlyList<ClaimRequirement> GetEffectiveClaims(string serviceName, string method, string path) => _claims;
|
||||
|
||||
public void UpdateFromMicroservice(string serviceName, IReadOnlyList<EndpointDescriptor> endpoints)
|
||||
{
|
||||
}
|
||||
|
||||
public void UpdateFromAuthority(IReadOnlyDictionary<EndpointKey, IReadOnlyList<ClaimRequirement>> overrides)
|
||||
{
|
||||
}
|
||||
|
||||
public void RemoveService(string serviceName)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class TestHostEnvironment : IHostEnvironment
|
||||
{
|
||||
public string EnvironmentName { get; set; } = Environments.Production;
|
||||
public string ApplicationName { get; set; } = "StellaOps.Router.Gateway.Tests";
|
||||
public string ContentRootPath { get; set; } = Environment.CurrentDirectory;
|
||||
public IFileProvider ContentRootFileProvider { get; set; } = new NullFileProvider();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Router.Gateway.Configuration;
|
||||
using StellaOps.Router.Gateway.DependencyInjection;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Router.Gateway.Tests;
|
||||
|
||||
public sealed class RouterNodeConfigValidationTests
|
||||
{
|
||||
[Fact]
|
||||
public void RouterNodeConfig_WhenRegionMissing_ThrowsOptionsValidationException()
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
services.AddRouterGatewayCore();
|
||||
|
||||
using var provider = services.BuildServiceProvider();
|
||||
|
||||
var act = () => provider.GetRequiredService<IOptions<RouterNodeConfig>>().Value;
|
||||
|
||||
act.Should().Throw<OptionsValidationException>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RouterNodeConfig_WhenRegionProvided_GeneratesNodeIdIfMissing()
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
services.AddRouterGatewayCore();
|
||||
services.Configure<RouterNodeConfig>(c => c.Region = "test");
|
||||
|
||||
using var provider = services.BuildServiceProvider();
|
||||
|
||||
var config = provider.GetRequiredService<IOptions<RouterNodeConfig>>().Value;
|
||||
|
||||
config.Region.Should().Be("test");
|
||||
config.NodeId.Should().StartWith("gw-test-");
|
||||
config.NodeId.Should().HaveLength("gw-test-".Length + 8);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user