Skip to content

test(darling): live HTTP proof of the web host's gates (#4128a) - #4132

Merged
erikdarlingdata merged 2 commits into
devfrom
test/4128-web-host-gates
Sep 24, 2026
Merged

erikdarlingdata merged 2 commits into
devfrom
test/4128-web-host-gates

Conversation

@erikdarlingdata

@erikdarlingdata erikdarlingdata commented Sep 24, 2026 •

Copy link
Copy Markdown
Owner

Part of #4128 (the web host; the MCP host and /core follow after #4121).

What changed

  1. Package. Added Microsoft.AspNetCore.TestHost to Directory.Packages.props at 10.0.12 (matching the
    Microsoft.Extensions.* pins already at that version for the net10.0 TFM), and a <PackageReference> in
    Darling/Darling.Tests/Darling.Tests.csproj.

  2. Extracted the pipeline seam. DarlingWebHostService.TryStartServerAsync used to build the whole
    middleware pipeline inline after builder.Build(). That block (the Host-allowlist/DNS-rebinding guard,
    the network-mode auth middleware, DarlingWebEndpoints.MapAll → UseDefaultFiles → UseStaticFiles) is
    now internal void ConfigurePipeline(WebApplication app, NpgsqlDataSource postgres, bool networkMode, IPAddress? networkListenIp, IPNetwork allowedCidr, string accessToken, DarlingWebOidcClient? oidcClient).
    TryStartServerAsync calls it right after _app = builder.Build();, passing exactly the same locals it
    used inline, in the same order — I diffed the extracted body against the original inline block (git diff
    on this PR shows only the extraction, no logic reordering) to confirm the call sequence is unchanged.
    It stayed an instance method (not static) because the gates call back into HandleAuthFlowAsync and
    ReportSignInRefusal, and read _logger/_collectorState/_baselineCache — exactly as before, just
    with the test's own DarlingWebHostService instance as the receiver instead of production's.

  3. New live test class Darling/Darling.Tests/DarlingWebHostGateLiveTests.cs: builds a WebApplication
    with UseTestServer(), constructs a throwaway DarlingWebHostService, calls the REAL ConfigurePipeline
    on it, starts it, and drives it through TestServer.SendAsync (not CreateClient, since the gates read
    RemoteIpAddress, which only SendAsync lets a test set). Six cases:

    • network mode, no token → 200 login page (not 401 — WebRequestAction.ShowLogin by design)
    • network mode, right token + in-CIDR remote → 302 + Set-Cookie (proof it passed every gate)
    • network mode, right token + out-of-CIDR remote → 403 (CIDR is outermost)
    • network mode, foreign Host → 400 (DNS-rebinding guard)
    • loopback mode, foreign Host → 400 (the Darling custom views/notebooks (#1563) #1576 fix: the guard runs in loopback too)
    • loopback mode, no token → reaches past the gates (no 400/403 — loopback registers no auth middleware)
  4. Updated HostHeaderGuardTests.EachHost_RunsTheGuardInBothModes_NotOnlyWhenNetworkExposed for
    DarlingWebHostService.cs: the extraction moved TryStartServerAsync's own unrelated post-start
    if (networkMode) logging branch ahead of ConfigurePipeline in the file text, so the FIRST
    if (networkMode) after builder.Build() is no longer necessarily the auth-gating one the test means to
    pin. Now searches from the guard's own index instead of from Build(), which finds the conditional that
    actually follows the guard in the same method — the same claim (guard registered before the network-only
    auth block), evaluated correctly under the new file shape. ReadHostSource's anchor changed from
    "_app.Use(" to "app.Use(" (a substring of the former) so it matches both the MCP host's still-direct
    _app.Use and the web host's ConfigurePipeline, which now calls app.Use on its parameter.

What I ran

  • dotnet build on the service project and Darling.Tests (-p:EnableWindowsTargeting=true): both green.
  • Darling.Tests.DarlingWebHostGateLiveTests alone: 6/6 pass.
  • Darling.Tests.HostHeaderGuardTests alone: 42/42 pass (after the fix above).
  • DarlingWebAuthTests, DarlingWebSupervisorTests, DarlingWebOidcTests, DarlingWebTlsTests,
    DarlingWebEditorAttributionTests (every other class touching DarlingWebHostService): 254/254 pass.
  • Full Darling.Tests suite once: Total: 13403, Errors: 0, Failed: 218, Skipped: 715. All 218 failures are
    outside anything this PR touches (checked by name — none mention DarlingWebHostGateLiveTests,
    HostHeaderGuardTests, or DarlingWebHostService); this Mac's known baseline is ~214 Windows-only
    failures (WPF/registry/etc.), and the 218 here is in that same neighborhood.

What I did NOT verify

Coordinator verification (branch head e6a88a8)

  • Behaviour-preserving, checked mechanically. Every line removed from TryStartServerAsync reappears in ConfigurePipeline, with the receiver _app becoming the app parameter. The only other additions are the method signature, its doc comment, and the one call that replaces the inline block. The gate order is unchanged: the Host guard, then the network-mode auth, then MapAll, UseDefaultFiles and UseStaticFiles.

  • HostHeaderGuardTests' changed source pin still claims that the guard is the first app.Use after Build(), with the network-mode block after it. The six live TestServer cases prove the same thing in behaviour.

  • The full Darling suite on macOS, compared with dev on the same rig: 4 new failures, all mechanical and fixed in e6a88a8:

    • two source pins spelled the old MapAll(_app, ...) (FleetSweepWebFeedTests, SharedBaselineCacheTests);
    • a cref to TestServer.CreateClient, which resolves to nothing the repo spells (DocCommentHygieneTests ×2).

    Every affected class passes.

  • No separate security review round: the gate code is provably unchanged (line-identical), and this PR adds tests of it.

CHANGELOG entry

  • Added a live-HTTP test of the web dashboard host's Host-allowlist, network-mode token, and CIDR gates
    through a real Kestrel pipeline, and extracted the gate-wiring code into a testable ConfigurePipeline
    method with production behavior unchanged.

Extracts everything after builder.Build() through UseStaticFiles into an
internal ConfigurePipeline(WebApplication app, ...) method that
TryStartServerAsync calls with the same values in the same order, then
adds DarlingWebHostGateLiveTests: six TestServer-driven cases covering
network-mode no-token (login page), right-token+in-CIDR (passes),
right-token+out-of-CIDR (403), foreign Host in both modes (400), and
loopback no-token (reaches past the gates).

HostHeaderGuardTests' EachHost_RunsTheGuardInBothModes_NotOnlyWhenNetworkExposed
is updated to search for the auth-gating 'if (networkMode)' starting from
the guard's own position, since the extraction moved
TryStartServerAsync's unrelated post-start 'if (networkMode)' branch
ahead of ConfigurePipeline in the file.
…pins, and make TestServer.CreateClient prose, not a cref
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant