Adds SlpModularCms.Api.Slave for local master/slave dev testing (Unit 1)
Relocates ModuleOrchestrator, ServiceCollectionExtensions, and ApiPrefixConvention from SlpModularCms.Api into SlpModularCms.Core.Hosting so a new Master-less SlpModularCms.Api.Slave host project (ports 5285/7222) can share the same bootstrap code without duplicating it. This lets a developer run a master instance and a slave instance side by side locally to test the master/slave connection, without touching the existing master/slave protocol itself. Relocates the two orchestrator/convention test files from Modules.Identity.Tests to Core.Tests, dropping an incidental ProjectReference to SlpModularCms.Api that existed only for those tests. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
using System.Reflection;
|
||||
using FluentAssertions;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.ApplicationModels;
|
||||
using SlpModularCms.Core.Hosting;
|
||||
|
||||
namespace SlpModularCms.Core.Tests.Hosting;
|
||||
|
||||
public class ApiPrefixConventionTests
|
||||
{
|
||||
[Fact]
|
||||
public void Apply_PrependsPrefixToExistingAttributeRoute()
|
||||
{
|
||||
var convention = new ApiPrefixConvention("api/v1");
|
||||
var application = BuildApplicationModel("[controller]");
|
||||
|
||||
convention.Apply(application);
|
||||
|
||||
var template = application.Controllers[0].Selectors[0].AttributeRouteModel!.Template;
|
||||
template.Should().Be("api/v1/[controller]");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Apply_SetsPrefixAsRoute_WhenNoExistingAttributeRoute()
|
||||
{
|
||||
var convention = new ApiPrefixConvention("api/v1");
|
||||
var application = BuildApplicationModelWithoutRoute();
|
||||
|
||||
convention.Apply(application);
|
||||
|
||||
var template = application.Controllers[0].Selectors[0].AttributeRouteModel!.Template;
|
||||
template.Should().Be("api/v1");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Apply_HandlesMulitpleControllers()
|
||||
{
|
||||
var convention = new ApiPrefixConvention("api/v1");
|
||||
var application = new ApplicationModel();
|
||||
application.Controllers.Add(BuildController("[controller]"));
|
||||
application.Controllers.Add(BuildController("other"));
|
||||
|
||||
convention.Apply(application);
|
||||
|
||||
application.Controllers[0].Selectors[0].AttributeRouteModel!.Template.Should().Be("api/v1/[controller]");
|
||||
application.Controllers[1].Selectors[0].AttributeRouteModel!.Template.Should().Be("api/v1/other");
|
||||
}
|
||||
|
||||
private static ApplicationModel BuildApplicationModel(string routeTemplate)
|
||||
{
|
||||
var app = new ApplicationModel();
|
||||
app.Controllers.Add(BuildController(routeTemplate));
|
||||
return app;
|
||||
}
|
||||
|
||||
private static ApplicationModel BuildApplicationModelWithoutRoute()
|
||||
{
|
||||
var app = new ApplicationModel();
|
||||
var controller = new ControllerModel(typeof(object).GetConstructors()[0].DeclaringType!.GetTypeInfo(), []);
|
||||
var selector = new SelectorModel { AttributeRouteModel = null };
|
||||
controller.Selectors.Add(selector);
|
||||
app.Controllers.Add(controller);
|
||||
return app;
|
||||
}
|
||||
|
||||
private static ControllerModel BuildController(string routeTemplate)
|
||||
{
|
||||
var controller = new ControllerModel(typeof(object).GetConstructors()[0].DeclaringType!.GetTypeInfo(), []);
|
||||
var selector = new SelectorModel
|
||||
{
|
||||
AttributeRouteModel = new AttributeRouteModel(new RouteAttribute(routeTemplate))
|
||||
};
|
||||
controller.Selectors.Add(selector);
|
||||
return controller;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
using FluentAssertions;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using NSubstitute;
|
||||
using SlpModularCms.Core.Hosting;
|
||||
using SlpModularCms.Core.Modules;
|
||||
|
||||
namespace SlpModularCms.Core.Tests.Hosting;
|
||||
|
||||
public class ModuleOrchestratorTests
|
||||
{
|
||||
private readonly ModuleOrchestrator _orchestrator =
|
||||
new(NullLogger<ModuleOrchestrator>.Instance);
|
||||
|
||||
[Fact]
|
||||
public void RegisterModuleServices_CallsRegisterServicesOnDiscoveredModules()
|
||||
{
|
||||
var module = Substitute.For<IModule>();
|
||||
var services = new ServiceCollection();
|
||||
|
||||
InjectModules(_orchestrator, [module]);
|
||||
_orchestrator.RegisterModuleServices(services);
|
||||
|
||||
module.Received(1).RegisterServices(services);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UseModules_CallsUseModuleOnDiscoveredModules()
|
||||
{
|
||||
var module = Substitute.For<IModule>();
|
||||
var app = Substitute.For<IApplicationBuilder>();
|
||||
app.ApplicationServices.Returns(new ServiceCollection().BuildServiceProvider());
|
||||
|
||||
InjectModules(_orchestrator, [module]);
|
||||
_orchestrator.UseModules(app);
|
||||
|
||||
module.Received(1).UseModule(app);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RegisterModuleServices_DoesNotThrow_WhenModuleListIsEmpty()
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
var act = () => _orchestrator.RegisterModuleServices(services);
|
||||
|
||||
act.Should().NotThrow();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UseModules_DoesNotThrow_WhenModuleListIsEmpty()
|
||||
{
|
||||
var app = Substitute.For<IApplicationBuilder>();
|
||||
app.ApplicationServices.Returns(new ServiceCollection().BuildServiceProvider());
|
||||
|
||||
var act = () => _orchestrator.UseModules(app);
|
||||
|
||||
act.Should().NotThrow();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DiscoverModules_RunsWithoutThrowing_InTestEnvironment()
|
||||
{
|
||||
var act = () => _orchestrator.DiscoverModules();
|
||||
act.Should().NotThrow();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RegisterModuleServices_ContinuesOnModuleThatThrows()
|
||||
{
|
||||
var badModule = Substitute.For<IModule>();
|
||||
badModule.When(m => m.RegisterServices(Arg.Any<IServiceCollection>()))
|
||||
.Do(_ => throw new InvalidOperationException("boom"));
|
||||
var goodModule = Substitute.For<IModule>();
|
||||
var services = new ServiceCollection();
|
||||
|
||||
InjectModules(_orchestrator, [badModule, goodModule]);
|
||||
var act = () => _orchestrator.RegisterModuleServices(services);
|
||||
|
||||
act.Should().NotThrow();
|
||||
goodModule.Received(1).RegisterServices(services);
|
||||
}
|
||||
|
||||
// Inject modules via reflection — ModuleOrchestrator stores them in a private List<IModule>
|
||||
private static void InjectModules(ModuleOrchestrator orchestrator, IModule[] modules)
|
||||
{
|
||||
var field = typeof(ModuleOrchestrator)
|
||||
.GetField("_modules", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)!;
|
||||
var list = (List<IModule>)field.GetValue(orchestrator)!;
|
||||
list.AddRange(modules);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user