Revert "Test host: bind Kestrel to Site:Authority instead of dynamic port"
Some checks failed
Dotnet build and test / log-the-inputs (push) Has been cancelled
Dotnet build and test / build (push) Has been cancelled

This reverts commit 402bcc1db8.
This commit is contained in:
Paul Schneider 2026-07-12 03:48:49 +01:00
commit 09604092c5
3 changed files with 17 additions and 76 deletions

View file

@ -1,7 +1,6 @@
using System.Security.Cryptography.X509Certificates; using System.Security.Cryptography.X509Certificates;
using System.Net.Security; using System.Net.Security;
using IdentityModel.Client; using IdentityModel.Client;
using Microsoft.Extensions.DependencyInjection;
namespace Yavsc.Org.Tests namespace Yavsc.Org.Tests
{ {
@ -25,43 +24,7 @@ namespace Yavsc.Org.Tests
HttpClient client = NewHttpClient(); HttpClient client = NewHttpClient();
var disco = await client.GetDiscoveryDocumentAsync(serverUrl); var disco = await client.GetDiscoveryDocumentAsync(serverUrl);
if (disco.IsError) if (disco.IsError) throw new Exception(disco.Error);
{
// Diagnostic 2026-07-12 : capture the raw HTTP response
// AND dump the OIDC-related DB state so we can pinpoint
// which state is corrupt when the discovery is broken.
var rawResp = await client.GetAsync(serverUrl + "/.well-known/openid-configuration");
var body = await rawResp.Content.ReadAsStringAsync();
string dbState = "no logger";
try
{
using var scope = _serverFixture.Services.CreateScope();
var cfg = scope.ServiceProvider
.GetRequiredService<IdentityServer8.EntityFramework.DbContexts.ConfigurationDbContext>();
var clients = cfg.Clients.Select(c => new {
c.Id, c.ClientId, c.Enabled, c.RequireClientSecret
}).ToList();
var apiScopes = cfg.ApiScopes.Select(s => new { s.Name, s.Enabled }).ToList();
var apiResources = cfg.ApiResources.Select(r => new { r.Name, r.Enabled }).ToList();
var identityResources = cfg.IdentityResources.Select(r => new { r.Name, r.Enabled }).ToList();
dbState = $"clients={System.Text.Json.JsonSerializer.Serialize(clients)}\n" +
$"apiScopes={System.Text.Json.JsonSerializer.Serialize(apiScopes)}\n" +
$"apiResources={System.Text.Json.JsonSerializer.Serialize(apiResources)}\n" +
$"identityResources={System.Text.Json.JsonSerializer.Serialize(identityResources)}";
}
catch (Exception dumpEx)
{
dbState = $"dump failed: {dumpEx.Message}";
}
throw new Exception(
$"disco.Error={disco.Error}\n" +
$"HTTP status={(int)rawResp.StatusCode}\n" +
$"Body[0..2000]:\n{body.Substring(0, Math.Min(2000, body.Length))}\n" +
$"---\n" +
$"OIDC DB state at failure:\n{dbState}");
}
var response = await client.RequestClientCredentialsTokenAsync(new ClientCredentialsTokenRequest var response = await client.RequestClientCredentialsTokenAsync(new ClientCredentialsTokenRequest
{ {

View file

@ -73,19 +73,6 @@ public sealed class WebServerFixture : WebHostFixture
// that plus the in-memory overrides below. // that plus the in-memory overrides below.
builder.AddConfiguration(null).AddInMemoryCollection(new Dictionary<string, string?> builder.AddConfiguration(null).AddInMemoryCollection(new Dictionary<string, string?>
{ {
// Test host: fixed authority + external URL. Matches
// the Kestrel bind in WebHostFixture.InitializeAsync
// (https://localhost:44300) so IdentityServer8's
// discovery document and the test client agree on the
// same base URL. IdentityServer8 reads Site:Authority
// to populate the `issuer` claim, the discovery
// document's `issuer` and endpoint URLs — leaving it
// pointed at the production host (e.g.
// mercure.pschneider.fr) made /.well-known/openid-configuration
// return URLs unreachable from the test, hence
// "Internal Server Error" in the discovery call.
["Site:Authority"] = "https://localhost:44300",
["Site:ExternalUrl"] = "https://localhost:44300",
[$"ConnectionStrings:{YavscConstants.YavscConnectionStringName}"] = "InMemory", [$"ConnectionStrings:{YavscConstants.YavscConnectionStringName}"] = "InMemory",
// SMTP test config: UserName non-null so MailSender // SMTP test config: UserName non-null so MailSender
// exercises the Authenticate branch — the // exercises the Authenticate branch — the

View file

@ -1,5 +1,7 @@
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Hosting.Server;
using Microsoft.AspNetCore.Hosting.Server.Features;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using System.Net; using System.Net;
using System.Security.Cryptography; using System.Security.Cryptography;
@ -14,17 +16,14 @@ namespace Yavsc.Tests.Shared;
/// ///
/// <list type="bullet"> /// <list type="bullet">
/// <item><description>Kestrel with a self-signed HTTPS certificate /// <item><description>Kestrel with a self-signed HTTPS certificate
/// bound to the URL declared in configuration under /// on a dynamically-allocated port (no port collisions between
/// <c>Site:Authority</c> (port fixed by the specialisation — no /// parallel xUnit test classes).</description></item>
/// port collisions since all Yavsc.Org tests share a single
/// collection).</description></item>
/// <item><description>A per-process single-instance host initialised /// <item><description>A per-process single-instance host initialised
/// on first construction and torn down when the last fixture is /// on first construction and torn down when the last fixture is
/// disposed — same lazy + lock + count pattern as the original Org /// disposed — same lazy + lock + count pattern as the original Org
/// fixture, lifted out of the specialisation.</description></item> /// fixture, lifted out of the specialisation.</description></item>
/// <item><description>Address list sourced from /// <item><description>Address discovery via
/// <c>Site:Authority</c> so the listen URL and the OIDC /// <see cref="IServerAddressesFeature"/>.</description></item>
/// discovery / <c>issuer</c> URLs always match.</description></item>
/// </list> /// </list>
/// ///
/// The actual service registration, middleware pipeline and route /// The actual service registration, middleware pipeline and route
@ -109,20 +108,9 @@ public abstract class WebHostFixture : IDisposable
{ {
var builder = WebApplication.CreateBuilder(); var builder = WebApplication.CreateBuilder();
// Bind Kestrel to the URL the specialisation declared in
// Site:Authority (the same value IdentityServer8 reads to
// build its discovery document). Reading it from
// configuration makes the server URL and the issuer URLs
// refer to the same base — tests can just take
// _sharedAddresses[0] and trust it.
var authority = builder.Configuration["Site:Authority"]
?? throw new InvalidOperationException(
"WebHostFixture: Site:Authority must be configured before InitializeAsync runs.");
var authorityUri = new Uri(authority);
builder.WebHost.ConfigureKestrel(options => builder.WebHost.ConfigureKestrel(options =>
{ {
options.Listen(IPAddress.Loopback, authorityUri.Port, listenOptions => options.Listen(IPAddress.Loopback, 0, listenOptions =>
{ {
listenOptions.UseHttps(_selfSignedCertificate.Value); listenOptions.UseHttps(_selfSignedCertificate.Value);
}); });
@ -135,13 +123,16 @@ public abstract class WebHostFixture : IDisposable
_app = app; _app = app;
_sharedServices = app.Services; _sharedServices = app.Services;
// Source of truth for the listen URL is the configuration var server = app.Services.GetRequiredService<IServer>();
// (Site:Authority) — not the IServerAddressesFeature, which var addressFeatures = server.Features.Get<IServerAddressesFeature>();
// can be a different representation (e.g. 127.0.0.1 vs
// localhost) and causes discovery / issuer mismatches when
// tests contact the host.
_sharedAddresses.Clear(); _sharedAddresses.Clear();
_sharedAddresses.Add(authority.TrimEnd('/') + "/"); if (addressFeatures?.Addresses is not null)
{
foreach (var address in addressFeatures.Addresses)
{
_sharedAddresses.Add(address);
}
}
Addresses = _sharedAddresses.ToArray(); Addresses = _sharedAddresses.ToArray();
IsInitialized = true; IsInitialized = true;
} }