using System.Net;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
namespace Elsa.Hosts.SmokeTests;
///
/// Boots a host the way its own entry point does and asserts it comes up serving gated routes.
///
///
/// This repo runs two parallel feature systems -- the classic Features/ path and the CShells
/// ShellFeatures/ path -- and every module must register in both. Nothing exercised either until
/// now: the unit and integration suites construct services directly, so a service missing from one path,
/// or a feature registered in one and not the other, passes every test and fails only when a host starts.
/// Three such bugs in #7980 were found by running these two hosts by hand.
///
/// Each host is booted through , which runs the real
/// Program with its full feature registration. Assembling a service collection here instead would
/// reproduce exactly the blind spot these tests exist to close.
///
///
/// The assertions go through HTTP rather than by resolving services out of the container. The two hosts
/// have genuinely different container topologies -- the classic host is flat, while CShells gives each
/// shell its own provider, so the module services are simply not in the root one -- and a test that
/// reached into either would have to encode that difference and would break whenever CShells changed its
/// internals. Behaviour at the edge is both host-agnostic and the thing actually worth pinning: whichever
/// feature system a host uses, the observable result has to be the same.
///
///
public abstract class HostSmokeTests(HostFixture host) : IClassFixture>
where TEntryPoint : class
{
///
/// Routes this host is expected to serve behind a permission. Each names a different module, so the
/// set doubles as an inventory of what this host's feature system is supposed to have registered.
///
protected abstract IReadOnlyCollection GatedRoutes { get; }
[Fact]
public void HostStarts()
{
// Touching Services forces the host to be built, which is where a feature that fails to register or
// an option that fails validation throws.
Assert.NotNull(host.Services);
}
[Fact]
public async Task EveryGatedRouteChallengesInsteadOfFailing()
{
using var client = host.CreateClient();
var problems = new List();
foreach (var route in GatedRoutes)
{
var status = (int)(await client.GetAsync(route)).StatusCode;
// Each way this can go wrong is a distinct bug, so they are named rather than collapsed into one
// "expected 401" message that leaves the reader to work out which failure they are looking at.
var problem = status switch
{
404 => "404: this host never registered the module serving it",
>= 500 => $"{status}: the endpoint was found but could not be activated, so a dependency is missing",
200 => "200: reachable without credentials, so no permission gate ran",
401 => null,
_ => $"{status}: expected 401"
};
if (problem is not null)
problems.Add($"{route} -> {problem}");
}
// Every route is reported at once: when a feature system stops registering a group of modules, one
// failure per run turns a single cause into a queue of identical-looking investigations.
Assert.True(problems.Count == 0, $"{problems.Count} of {GatedRoutes.Count} gated route(s) did not challenge:{Environment.NewLine}{string.Join(Environment.NewLine, problems)}");
}
}
/// Boots a host once per test class.
public class HostFixture : WebApplicationFactory where TEntryPoint : class
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
// Both hosts refuse to start outside Development while the signing key is a known default. That is
// the guard working as intended, so the test satisfies it rather than configuring around it.
builder.UseEnvironment("Development");
}
}