diff --git a/src/Host/Dev/Program.cs b/src/Host/Dev/Program.cs index 1697ead4..44861375 100644 --- a/src/Host/Dev/Program.cs +++ b/src/Host/Dev/Program.cs @@ -13,7 +13,11 @@ // // This host closes that gap and nothing more. It is deliberately the same handful of calls the // distribution host makes, because that is its second job: if someone breaks AddCalloraHost or the -// order these have to run in, this stops building rather than the next distribution finding out. +// order these have to run in, it shows up here rather than in the next distribution. +// +// "Shows up here" used to say "stops building", which was only true of compile errors. A missing +// registration compiles cleanly and fails at builder.Build() — and nothing in CI made that call. +// TheCompositionBuildsTests now does, with the same calls in the same order; keep them in step. // // It is not a second product. No installer, no first-run provisioning, no packaging — those belong // to a distribution, which owns its own configuration and lifecycle. diff --git a/tests/Callora.Core.Tests/Application/Startup/TheCompositionBuildsTests.cs b/tests/Callora.Core.Tests/Application/Startup/TheCompositionBuildsTests.cs new file mode 100644 index 00000000..47d4a423 --- /dev/null +++ b/tests/Callora.Core.Tests/Application/Startup/TheCompositionBuildsTests.cs @@ -0,0 +1,84 @@ +using Callora.Administration; +using Callora.Core.Application.Plugins; +using Callora.Core.Application.Security; +using Callora.Core.Infrastructure.DependencyInjection; +using Callora.Surface.Rendering; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Callora.Core.Tests.Application.Startup; + +/// +/// Die Zusammenstellung des Hosts lässt sich bauen — mit Lifetime-Validierung. +/// +/// +/// +/// Der Befund: src/Host/Dev/Program.cs trägt den Kommentar, es falle auf, „wenn jemand +/// AddCalloraHost oder die Reihenfolge der Modulaufrufe bricht". Das stimmte nur für +/// Compile-Fehler. Eine fehlende Registrierung übersetzt sauber und scheitert erst bei +/// builder.Build() — und den Aufruf machte in der CI niemand. Ein Dienst, der im Testhost von +/// Hand registriert wird und in der echten Komposition fehlt, kam damit grün durch die Suite und +/// hätte den Start des Dev-Stacks abgeräumt. +/// +/// +/// AddCalloraHost setzt ValidateScopes und ValidateOnBuild, deshalb sagt schon +/// der Bau etwas: Er prüft jede registrierte Abhängigkeit auf Auflösbarkeit und auf einen +/// von einem Singleton gefangenen Scoped-Dienst. +/// +/// +/// Und genau da hört er auf — nachgemessen, nicht angenommen: Wird +/// PluginSelfService aus der Zusammenstellung entfernt, baut der Host weiter. Ein Dienst, der +/// nirgends registriert ist, wird auch nicht validiert, und ein [FromServices]-Parameter +/// eines Controllers wird erst beim Aufruf aufgelöst. Deshalb der zweite Test, der die Dienste +/// beim Namen nennt: Was nur über [FromServices] konsumiert wird, muss hier stehen, sonst +/// deckt es niemand. +/// +/// +/// Es wird nichts verbunden: Die Verbindungszeichenfolge muss nur gültig aussehen, denn ein +/// DbContext öffnet beim Auflösen keine Verbindung. +/// +/// +public sealed class TheCompositionBuildsTests +{ + [Fact] + public void TheHostComposesWithoutAMissingOrCaptiveRegistration() + { + using var app = BuildHost(); + + Assert.NotNull(app.Services); + } + + /// + /// Die Dienste der Mandantenebene lösen aus der echten Zusammenstellung auf, nicht nur aus den + /// von Hand zusammengesetzten Testhosts. + /// + [Fact] + public void TheTenantLevelServicesResolveFromTheRealComposition() + { + using var app = BuildHost(); + using var scope = app.Services.CreateScope(); + + Assert.NotNull(scope.ServiceProvider.GetRequiredService()); + Assert.NotNull(scope.ServiceProvider.GetRequiredService()); + Assert.NotNull(scope.ServiceProvider.GetRequiredService()); + } + + // Dieselben Aufrufe in derselben Reihenfolge wie src/Host/Dev/Program.cs. Weicht der Test davon + // ab, prüft er eine Zusammenstellung, die niemand ausliefert. + private static WebApplication BuildHost() + { + var builder = WebApplication.CreateBuilder(); + + // Dieselben Schlüssel, die docker-compose.yml setzt — der Abschnitt heißt BackendHost. + builder.Configuration["BackendHost:DatabaseConnectionString"] = + "Host=localhost;Database=callora-composition-test;Username=u;Password=p"; + builder.Configuration["BackendHost:ApiKeys:0"] = "composition-test-key"; + + builder.AddCalloraHost(); + builder.AddCalloraAdministration(); + builder.Services.AddCalloraSurfaceRendering(); + + return builder.Build(); + } +}