Resolve all 46 low-severity architecture review findings
This commit is contained in:
@@ -0,0 +1,24 @@
|
||||
using System.Reflection;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using ZB.MOM.WW.ScadaBridge.ClusterInfrastructure;
|
||||
using ZB.MOM.WW.ScadaBridge.Host;
|
||||
using ZB.MOM.WW.ScadaBridge.Host.Actors;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.Host.Tests;
|
||||
|
||||
public class BootstrapCancellationTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task CancelledBootstrap_DoesNotResolveActorSystemOrJoin()
|
||||
{
|
||||
var accessed = 0;
|
||||
var coordinator = new ClusterBootstrapCoordinator(
|
||||
() => { accessed++; throw new InvalidOperationException("must not resolve during shutdown"); },
|
||||
Options.Create(new ClusterOptions { SeedNodes = ["akka.tcp://scadabridge@127.0.0.1:2551"] }),
|
||||
Options.Create(new NodeOptions { NodeHostname = "127.0.0.1", RemotingPort = 2551 }), NullLogger<ClusterBootstrapCoordinator>.Instance);
|
||||
var run = typeof(ClusterBootstrapCoordinator).GetMethod("RunAsync", BindingFlags.Instance | BindingFlags.NonPublic)!;
|
||||
await (Task)run.Invoke(coordinator, [new CancellationToken(true)])!;
|
||||
Assert.Equal(0, accessed);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
using System.Reflection;
|
||||
using Grpc.AspNetCore.Server;
|
||||
using Grpc.Core;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
using ZB.MOM.WW.LocalDb.Replication;
|
||||
using ZB.MOM.WW.Secrets.Replicator.Grpc;
|
||||
using ZB.MOM.WW.Secrets.Replicator.Grpc.DependencyInjection;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.Host.Tests;
|
||||
|
||||
/// <summary>Every actually mapped generated gRPC method must have an authentication gate.</summary>
|
||||
public class MappedGrpcAuthenticationCoverageTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task EveryMappedGrpcService_IsCoveredIncludingExternalMappings()
|
||||
{
|
||||
var builder = WebApplication.CreateBuilder();
|
||||
builder.Configuration.AddInMemoryCollection(new Dictionary<string, string?>
|
||||
{
|
||||
["Secrets:Replication:Enabled"] = "true",
|
||||
["Secrets:Replication:Mode"] = "Grpc",
|
||||
["Secrets:GrpcHub:BearerToken"] = "coverage-test-token",
|
||||
});
|
||||
builder.Services.AddGrpc();
|
||||
builder.Services.AddZbSecretsGrpcHub(builder.Configuration, "Secrets:GrpcHub");
|
||||
await using var app = builder.Build();
|
||||
|
||||
// Discover the production map calls, not a hand-maintained three-service type list.
|
||||
var root = FindRepositoryRoot();
|
||||
var hostSources = Directory.EnumerateFiles(Path.Combine(root, "src", "ZB.MOM.WW.ScadaBridge.Host"), "*.cs", SearchOption.AllDirectories)
|
||||
.Where(p => !p.Contains($"{Path.DirectorySeparatorChar}obj{Path.DirectorySeparatorChar}")
|
||||
&& !p.Contains($"{Path.DirectorySeparatorChar}bin{Path.DirectorySeparatorChar}"))
|
||||
.Select(p => CSharpSyntaxTree.ParseText(File.ReadAllText(p)).GetRoot()).ToList();
|
||||
var typeNames = hostSources.SelectMany(s => s.DescendantNodes().OfType<GenericNameSyntax>())
|
||||
.Where(n => n.Identifier.ValueText == "MapGrpcService")
|
||||
.Select(n => n.TypeArgumentList.Arguments.Single().ToString()).Distinct().ToList();
|
||||
Assert.NotEmpty(typeNames);
|
||||
var assemblies = typeof(SecretsRegistration).Assembly.GetReferencedAssemblies().Select(Assembly.Load)
|
||||
.Append(typeof(SecretsRegistration).Assembly).ToList();
|
||||
var map = typeof(GrpcEndpointRouteBuilderExtensions).GetMethods()
|
||||
.Single(m => m.Name == "MapGrpcService" && m.IsGenericMethodDefinition);
|
||||
foreach (var name in typeNames)
|
||||
{
|
||||
var type = assemblies.Select(a => a.GetType(name)).Single(t => t is not null)!;
|
||||
map.MakeGenericMethod(type).Invoke(null, [app]);
|
||||
}
|
||||
|
||||
// Exercise the production extension mappings too; these packages hide their MapGrpcService calls.
|
||||
Assert.Contains(hostSources.SelectMany(s => s.DescendantNodes().OfType<InvocationExpressionSyntax>()),
|
||||
n => n.Expression.ToString().EndsWith(".MapZbLocalDbSync", StringComparison.Ordinal));
|
||||
app.MapZbLocalDbSync();
|
||||
app.MapScadaBridgeSecretsHub(builder.Configuration);
|
||||
var metadata = ((IEndpointRouteBuilder)app).DataSources.SelectMany(s => s.Endpoints)
|
||||
.Select(e => e.Metadata.GetMetadata<GrpcMethodMetadata>()).Where(m => m is not null).ToList();
|
||||
Assert.NotEmpty(metadata);
|
||||
var serviceTypes = metadata.Select(m => m!.ServiceType).Distinct().ToList();
|
||||
Assert.True(serviceTypes.Count >= typeNames.Count + 2, "External LocalDb and Secrets mappings must both be exercised.");
|
||||
|
||||
var prefixes = ControlPlaneAuthInterceptor.DefaultGatedPrefixes
|
||||
.Concat(CentralControlAuthInterceptor.DefaultGatedPrefixes)
|
||||
.Append((string)typeof(LocalDbSyncAuthInterceptor).GetField("ServicePrefix", BindingFlags.Static | BindingFlags.NonPublic)!.GetRawConstantValue()!)
|
||||
.ToArray();
|
||||
foreach (var type in serviceTypes)
|
||||
{
|
||||
var binder = new MethodCollector();
|
||||
var attribute = type.GetCustomAttribute<BindServiceMethodAttribute>(inherit: true);
|
||||
Assert.NotNull(attribute);
|
||||
var bind = attribute.BindType.GetMethods(BindingFlags.Public | BindingFlags.Static)
|
||||
.Single(m => m.Name == attribute.BindMethodName && m.GetParameters().Length == 2
|
||||
&& m.GetParameters()[0].ParameterType == typeof(ServiceBinderBase));
|
||||
bind.Invoke(null, [binder, null]);
|
||||
Assert.NotEmpty(binder.Methods);
|
||||
Assert.Equal(metadata.Where(m => m!.ServiceType == type).Select(m => m!.Method.FullName).Order(), binder.Methods.Order());
|
||||
|
||||
// Secrets installs an all-method interceptor on its own service options. Inspect actual DI options.
|
||||
var optionsType = typeof(IOptions<>).MakeGenericType(typeof(GrpcServiceOptions<>).MakeGenericType(type));
|
||||
var options = app.Services.GetRequiredService(optionsType);
|
||||
var serviceOptions = (GrpcServiceOptions)optionsType.GetProperty("Value")!.GetValue(options)!;
|
||||
var hasSecretsGate = serviceOptions.Interceptors.Any(i => i.Type == typeof(SecretsHubAuthInterceptor));
|
||||
Assert.All(binder.Methods, method => Assert.True(
|
||||
hasSecretsGate || prefixes.Any(prefix => method.StartsWith(prefix, StringComparison.Ordinal)),
|
||||
$"Mapped service {type.FullName} method {method} has no authentication interceptor gate."));
|
||||
if (!hasSecretsGate)
|
||||
{
|
||||
// Negative control uses the real service's methods: removing its gate must be detected.
|
||||
var removed = prefixes.First(p => binder.Methods.Any(m => m.StartsWith(p, StringComparison.Ordinal)));
|
||||
var remaining = prefixes.Where(p => p != removed).ToArray();
|
||||
Assert.Contains(binder.Methods, method => !remaining.Any(p => method.StartsWith(p, StringComparison.Ordinal)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static string FindRepositoryRoot()
|
||||
{
|
||||
for (var directory = new DirectoryInfo(AppContext.BaseDirectory); directory is not null; directory = directory.Parent)
|
||||
if (File.Exists(Path.Combine(directory.FullName, "ZB.MOM.WW.ScadaBridge.slnx"))) return directory.FullName;
|
||||
throw new InvalidOperationException("Repository root not found.");
|
||||
}
|
||||
|
||||
private sealed class MethodCollector : ServiceBinderBase
|
||||
{
|
||||
public List<string> Methods { get; } = [];
|
||||
public override void AddMethod<TRequest, TResponse>(Method<TRequest, TResponse> method, UnaryServerMethod<TRequest, TResponse>? handler)
|
||||
=> Methods.Add(method.FullName);
|
||||
public override void AddMethod<TRequest, TResponse>(Method<TRequest, TResponse> method, ClientStreamingServerMethod<TRequest, TResponse>? handler)
|
||||
=> Methods.Add(method.FullName);
|
||||
public override void AddMethod<TRequest, TResponse>(Method<TRequest, TResponse> method, ServerStreamingServerMethod<TRequest, TResponse>? handler)
|
||||
=> Methods.Add(method.FullName);
|
||||
public override void AddMethod<TRequest, TResponse>(Method<TRequest, TResponse> method, DuplexStreamingServerMethod<TRequest, TResponse>? handler)
|
||||
=> Methods.Add(method.FullName);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user