Compare commits
No commits in common. "09604092c5b599a0ce183377f679396e78af4ab8" and "402bcc1db8e789e2ba9b675b2c8b698c74ca330c" have entirely different histories.
09604092c5
...
402bcc1db8
3 changed files with 78 additions and 17 deletions
|
|
@ -1,6 +1,7 @@
|
||||||
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
|
||||||
{
|
{
|
||||||
|
|
@ -24,7 +25,43 @@ 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) throw new Exception(disco.Error);
|
if (disco.IsError)
|
||||||
|
{
|
||||||
|
// 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
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@ using IdentityServer8.EntityFramework.Entities;
|
||||||
using IdentityServer8.Models;
|
using IdentityServer8.Models;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Builder;
|
using Microsoft.AspNetCore.Builder;
|
||||||
|
using Microsoft.AspNetCore.Hosting.Server;
|
||||||
|
using Microsoft.AspNetCore.Hosting.Server.Features;
|
||||||
using Microsoft.AspNetCore.Identity;
|
using Microsoft.AspNetCore.Identity;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
|
|
@ -73,6 +75,19 @@ 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
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,5 @@
|
||||||
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;
|
||||||
|
|
@ -16,14 +14,17 @@ 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
|
||||||
/// on a dynamically-allocated port (no port collisions between
|
/// bound to the URL declared in configuration under
|
||||||
/// parallel xUnit test classes).</description></item>
|
/// <c>Site:Authority</c> (port fixed by the specialisation — no
|
||||||
|
/// 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 discovery via
|
/// <item><description>Address list sourced from
|
||||||
/// <see cref="IServerAddressesFeature"/>.</description></item>
|
/// <c>Site:Authority</c> so the listen URL and the OIDC
|
||||||
|
/// 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
|
||||||
|
|
@ -108,9 +109,20 @@ 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, 0, listenOptions =>
|
options.Listen(IPAddress.Loopback, authorityUri.Port, listenOptions =>
|
||||||
{
|
{
|
||||||
listenOptions.UseHttps(_selfSignedCertificate.Value);
|
listenOptions.UseHttps(_selfSignedCertificate.Value);
|
||||||
});
|
});
|
||||||
|
|
@ -123,16 +135,13 @@ public abstract class WebHostFixture : IDisposable
|
||||||
_app = app;
|
_app = app;
|
||||||
_sharedServices = app.Services;
|
_sharedServices = app.Services;
|
||||||
|
|
||||||
var server = app.Services.GetRequiredService<IServer>();
|
// Source of truth for the listen URL is the configuration
|
||||||
var addressFeatures = server.Features.Get<IServerAddressesFeature>();
|
// (Site:Authority) — not the IServerAddressesFeature, which
|
||||||
|
// 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();
|
||||||
if (addressFeatures?.Addresses is not null)
|
_sharedAddresses.Add(authority.TrimEnd('/') + "/");
|
||||||
{
|
|
||||||
foreach (var address in addressFeatures.Addresses)
|
|
||||||
{
|
|
||||||
_sharedAddresses.Add(address);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Addresses = _sharedAddresses.ToArray();
|
Addresses = _sharedAddresses.ToArray();
|
||||||
IsInitialized = true;
|
IsInitialized = true;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue