- 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.
219 lines
7.9 KiB
C#
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();
|
|
}
|
|
}
|