Files
git.stella-ops.org/tests/StellaOps.Router.Gateway.Tests/MiddlewareErrorScenarioTests.cs
master 8779e9226f 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.
2025-12-19 18:11:59 +02:00

219 lines
7.9 KiB
C#

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