Run a test's application on Kestrel with [KestrelHost] - #275
Merged
Merged
Conversation
The host a test's application runs on is now an attribute on the test, and everything the test holds follows it: ITestWebApp, the HttpClient the harness hands out, every typed client parameter, and LastResponse. Hardened.Web.Testing gets the seam - ITestHost, TestHostAttribute, the pipeline host that was always there, and SocketHost, the client half every socket host shares - and Hardened.Web.Kestrel.Testing ships [KestrelHost] over KestrelServerRunner, on a loopback port the kernel picks, over the test's own container. KestrelServerRunner runs the startup services through the guarded ApplicationLogic.Start. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
ipjohnson
force-pushed
the
test-hosts
branch
from
September 5, 2026 13:15
2486510 to
7979adc
Compare
CI's coverage gate found the one branch nothing took: the subject header on the request a socket host's handler applies the credential to. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
[KestrelHost]on a[HardenedTest], a test class or the assembly runs the test's application on Kestrel, over the test's own container, on a loopback port the kernel picks. Everything the test holds follows it:ITestWebAppsends to the socket, theHttpClientthe harness hands out and every Kiota, Refit or hand-written client parameter send to the socket,LastResponseis what came back over the wire, and[Mock],[Grants],[Subject],[Anonymous]and the environment attributes work as they do in-process, because the server resolves handlers from the same container the test does.[PipelineHost]on a method opts one test back to the pipeline inside a socket class.The seam is in
Hardened.Web.Testing:ITestHost,TestHostAttribute,PipelineHost(today's behaviour, moved behind the seam) andSocketHost(the client half every socket host shares).Hardened.Web.Kestrel.Testingis its own package, one attribute and one internal host overKestrelServerRunner, because hosting needs the ASP.NET Core shared framework and a test project on the pipeline must not be forced onto it.Why
The pipeline harness cannot see the host: Kestrel's framing and headers, a body arriving compressed, a cached response on a socket, a connection cut by a timeout. The 0.19 and 0.20 trials found defects only over a socket, and the guide already tells every suite to budget a smoke test against one; today that is a hand-built harness per test class, available only in this repository, that composes a
ServiceCollectionby hand and so has no[Mock], no credential attributes and no entry point.TEST-HOST-PLAN.mdhas the design.Teardown and ports
SocketHost.DisposeAsyncdisposes its one sharedSocketsHttpHandlerfirst, so no request is in flight, then stops the server with a token bounded bySocketHost.StopBound, then disposes it. Kestrel closes what is idle at once and aborts what is left when the bound fires;KestrelServerRunner.DisposeAsyncalone stops withCancellationToken.Noneand would wait on a hung handler for ever, which a test holds it to.IServerAddressesFeatureafter the bind rather than computed, so parallel tests, classes, processes and CI jobs cannot collide. The client is given the literal127.0.0.1:port, neverlocalhost.KestrelServerRunner.StartAsyncruns the startup services through the guardedApplicationLogic.Start, so a test whose entry point already ran them does not run them again;KestrelHostingTestsstill holds the runner to one run.Held by
Hardened.Web.Kestrel.Testing.Tests: the bound address is a loopback port the kernel picked and is not available before start; the request goes in with its method, path, query, headers and body bytes and the answer comes out with its status, Kestrel's headers and the body; an unmatched path is a 404 from the real not-found handler; the credential is applied where a request carries neither test header and left alone where it does; a client built over the host records what it received; an event stream is handed on without being buffered; disposing the container closes the port; and a hung handler cannot hold disposal past the bound.KestrelHostTestsin the web SUT's test project,[KestrelHost]on the class:ITestWebAppwith the headers Kestrel wrote, a compressed body arriving encoded and reading decoded, a[Mock]behind a route, a Kiota client and a Refit interface read byReturns<T>(),LastResponse, three parameters with three credentials, a 404, the handler's exception not crossing the wire, and[PipelineHost]on one method getting it back.KestrelHostUnderNUnitTestsin the NUnit project: the same shapes on the other runner.TestClientContext.BaseAddress, which is now the bound address.Notes
LastResponseon a socket is recorded client-side on the runner seam.Hardened.Web.Testingand the new assembly; the new assembly is not incoverage-baseline.json, which the gate reads as unmeasured.docs/client-testing.mdgains an "Over a socket" section. The Docs site's guide is item D of the plan.🤖 Generated with Claude Code