yavsc/src/Yavsc.Org.Tests/Mandatory/Remoting.cs
Paul Schneider d58fad552a Test host: bind Kestrel to Site:Authority instead of dynamic port
The Yavsc.Org integration tests were failing 'Internal Server Error' on
the OIDC discovery document when run as part of the full test suite.

Root cause: WebHostFixture bound Kestrel to IPAddress.Loopback on a
dynamically-allocated port and exposed it via IServerAddressesFeature.
But the OIDC issuer URLs (and the issuer claim) come from Site:Authority,
which was left at the production value (mercure.pschneider.fr). So
IdentityServer8's discovery document advertised URLs unreachable from
the test process, and the discovery call returned a 500.

Fix:
  - WebServerFixture now overrides Site:Authority and Site:ExternalUrl
    in AddInMemoryCollection to 'https://localhost:44300' (the ASP.NET
    Core dev HTTPS convention).
  - WebHostFixture reads Site:Authority from configuration and binds
    Kestrel to that fixed URL. The exposed Addresses list is sourced
    from the same configuration value instead of the
    IServerAddressesFeature, so the listen URL and the OIDC issuer
    URLs always match.

Remoting.cs (Mandatory/Remoting.cs): add 'using
Microsoft.Extensions.DependencyInjection;' so the existing OIDC/DB
diagnostic block (capture raw HTTP response + dump OIDC-related DB
state on discovery failure) compiles. The diagnostic itself is left
in place — it's what surfaced the 500 in the first place.
2026-07-12 03:43:22 +01:00

136 lines
5.5 KiB
C#

using System.Security.Cryptography.X509Certificates;
using System.Net.Security;
using IdentityModel.Client;
using Microsoft.Extensions.DependencyInjection;
namespace Yavsc.Org.Tests
{
[Collection("Yavsc Server")]
[Trait("regression", "oui")]
public class Remoting : BaseTestContext, IClassFixture<WebServerFixture>
{
public Remoting(WebServerFixture serverFixture, ITestOutputHelper output)
: base(output, serverFixture)
{
}
[Fact]
public async Task ObtainServiceToken()
{
var serverUrl = _serverFixture.Addresses.FirstOrDefault(u => u.StartsWith("https:"));
if (string.IsNullOrEmpty(serverUrl))
throw new InvalidOperationException("No HTTPS server address found");
HttpClient client = NewHttpClient();
var disco = await client.GetDiscoveryDocumentAsync(serverUrl);
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
{
Address = disco.TokenEndpoint,
ClientId = _serverFixture.TestClientId,
ClientSecret = _serverFixture.TestClientSecret,
Scope = "test",
GrantType = "client_credentials"
});
if (response.IsError) throw new Exception(response.Error);
}
private static HttpClient NewHttpClient()
{
return new HttpClient(new BypassSslValidationHandler());
}
[Fact]
public async Task ObtainResourceOwnerPasswordToken()
{
var serverUrl = _serverFixture.Addresses.FirstOrDefault(u => u.StartsWith("https:"));
if (string.IsNullOrEmpty(serverUrl))
throw new InvalidOperationException("No HTTPS server address found");
var client = NewHttpClient();
var disco = await client.GetDiscoveryDocumentAsync(serverUrl);
if (disco.IsError) throw new Exception(disco.Error);
var response = await client.RequestPasswordTokenAsync(new PasswordTokenRequest
{
Address = disco.TokenEndpoint,
ClientId = _serverFixture.TestClientId,
ClientSecret = _serverFixture.TestClientSecret,
UserName = _serverFixture.TestingUserName,
Password = _serverFixture.TestingUserPassword,
Scope = "test",
Parameters =
{
{ "acr_values", "tenant:custom_account_store1 foo bar quux" }
}
});
if (response.IsError) throw new Exception(response.Error);
}
public static IEnumerable<object[]> GetLoginIntentData()
{
return new object[][] { new object[] { "testuser", "test" } };
}
}
internal class BypassSslValidationHandler : HttpClientHandler
{
public BypassSslValidationHandler()
{
// Override validation for this handler only
ServerCertificateCustomValidationCallback = ValidateCertificate;
}
private bool ValidateCertificate(
HttpRequestMessage request,
X509Certificate2? certificate,
X509Chain? chain,
SslPolicyErrors errors)
{
// Accept all certificates (bypass validation)
return true;
}
}
}