From 3bc50a08982f92c0b0ca6d708a05ad97eb0f3fd9 Mon Sep 17 00:00:00 2001 From: David Bechstein Date: Thu, 3 Sep 2026 23:14:50 +0200 Subject: [PATCH] test(startup): the host composition is built in CI, not only by whoever runs it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit src/Host/Dev/Program.cs carries the claim that it notices "if someone breaks AddCalloraHost or the order these have to run in". That held for compile errors only. A missing registration compiles cleanly and fails at builder.Build() — and nothing in CI ever made that call. A service registered by hand in a test host and absent from the real composition therefore came through the suite green and would have taken down the dev stack at startup. AddCalloraHost sets ValidateScopes and ValidateOnBuild, so the build itself says something: every registered dependency is checked for resolvability and for a scoped service captured by a singleton. And that is where it stops — measured, not assumed. Remove PluginSelfService from the composition and the host still builds: a service registered nowhere is not validated either, and a controller's [FromServices] parameter resolves only when it is called. Hence the second test naming the tenant-level services explicitly. Whatever is consumed only through [FromServices] has to be listed there or nothing covers it. The comment in the test says so, so the next person does not read more into the first one than it delivers. The Program.cs comment is corrected in the same move, and points at the test. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_011iSX3geDv7PNRmiusRr1xd --- src/Host/Dev/Program.cs | 6 +- .../Startup/TheCompositionBuildsTests.cs | 84 +++++++++++++++++++ 2 files changed, 89 insertions(+), 1 deletion(-) create mode 100644 tests/Callora.Core.Tests/Application/Startup/TheCompositionBuildsTests.cs 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(); + } +}