a web server fixture shared state

This commit is contained in:
Paul Schneider 2026-04-19 17:15:10 +01:00
commit 5b6caa72b5
2 changed files with 140 additions and 111 deletions

View file

@ -23,7 +23,7 @@ namespace yavscTests
var serverUrl = _serverFixture.Addresses.FirstOrDefault(u => u.StartsWith("https:")); var serverUrl = _serverFixture.Addresses.FirstOrDefault(u => u.StartsWith("https:"));
if (string.IsNullOrEmpty(serverUrl)) if (string.IsNullOrEmpty(serverUrl))
throw new InvalidOperationException("No HTTPS server address found"); throw new InvalidOperationException("No HTTPS server address found");
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) throw new Exception(disco.Error);
@ -39,10 +39,10 @@ namespace yavscTests
if (response.IsError) throw new Exception(response.Error); if (response.IsError) throw new Exception(response.Error);
} }
private static HttpClient NewHttpClient() private static HttpClient NewHttpClient()
{ {
return new HttpClient(new BypassSslValidationHandler()); return new HttpClient(new BypassSslValidationHandler());
} }
[Fact] [Fact]
public async Task ObtainResourceOwnerPasswordToken() public async Task ObtainResourceOwnerPasswordToken()
@ -50,7 +50,7 @@ namespace yavscTests
var serverUrl = _serverFixture.Addresses.FirstOrDefault(u => u.StartsWith("https:")); var serverUrl = _serverFixture.Addresses.FirstOrDefault(u => u.StartsWith("https:"));
if (string.IsNullOrEmpty(serverUrl)) if (string.IsNullOrEmpty(serverUrl))
throw new InvalidOperationException("No HTTPS server address found"); throw new InvalidOperationException("No HTTPS server address found");
var client = NewHttpClient(); var 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) throw new Exception(disco.Error);
@ -80,22 +80,22 @@ namespace yavscTests
} }
internal class BypassSslValidationHandler : HttpClientHandler internal class BypassSslValidationHandler : HttpClientHandler
{
public BypassSslValidationHandler()
{ {
// Override validation for this handler only public BypassSslValidationHandler()
ServerCertificateCustomValidationCallback = ValidateCertificate; {
} // Override validation for this handler only
ServerCertificateCustomValidationCallback = ValidateCertificate;
private bool ValidateCertificate( }
HttpRequestMessage request,
X509Certificate2? certificate, private bool ValidateCertificate(
X509Chain? chain, HttpRequestMessage request,
SslPolicyErrors errors) X509Certificate2? certificate,
{ X509Chain? chain,
// Accept all certificates (bypass validation) SslPolicyErrors errors)
return true; {
// Accept all certificates (bypass validation)
return true;
}
} }
} }
}

View file

@ -29,9 +29,20 @@ namespace isnd.tests
public class WebServerFixture : IDisposable public class WebServerFixture : IDisposable
{ {
private static readonly Lazy<X509Certificate2> _selfSignedCertificate = new Lazy<X509Certificate2>(CreateSelfSignedCertificate); private static readonly Lazy<X509Certificate2> _selfSignedCertificate = new Lazy<X509Certificate2>(CreateSelfSignedCertificate);
private static readonly object _sync = new object();
private static WebApplication? _app; private static WebApplication? _app;
private static bool _isInitialized = false; private static bool _isInitialized = false;
private static int _instanceCount = 0; private static int _instanceCount = 0;
private static readonly List<string> _sharedAddresses = new List<string>();
private static string? _sharedTestClientId;
private static string? _sharedTestClientSecret;
private static string? _sharedTestingUserName;
private static string? _sharedTestingUserPassword;
private static string? _sharedTestingUserEmail;
private static IServiceProvider? _sharedServices;
private static IConfiguration? _sharedConfiguration;
private static SiteSettings? _sharedSiteSettings;
private static Microsoft.Extensions.Logging.ILogger? _sharedLogger;
public List<string> Addresses { get; private set; } = new List<string>(); public List<string> Addresses { get; private set; } = new List<string>();
public Microsoft.Extensions.Logging.ILogger? Logger { get; internal set; } public Microsoft.Extensions.Logging.ILogger? Logger { get; internal set; }
@ -40,7 +51,6 @@ namespace isnd.tests
public IConfiguration? Configuration { get; private set; } public IConfiguration? Configuration { get; private set; }
private WebApplication? app;
public string? TestClientId { get; private set; } public string? TestClientId { get; private set; }
public IServiceProvider? Services { get; private set; } public IServiceProvider? Services { get; private set; }
@ -52,10 +62,10 @@ namespace isnd.tests
public bool DbCreated { get; internal set; } public bool DbCreated { get; internal set; }
public SiteSettings? SiteSettings { get => siteSettings; set => siteSettings = value; } public SiteSettings? SiteSettings { get => siteSettings; set => siteSettings = value; }
public string? TestClientSecret { get; set; } public string? TestClientSecret { get; set; }
public string? TestingUserEmail { get; set; }
public WebServerFixture() public WebServerFixture()
{ {
lock (this) lock (_sync)
{ {
_instanceCount++; _instanceCount++;
if (!_isInitialized) if (!_isInitialized)
@ -63,25 +73,14 @@ namespace isnd.tests
SetupHost().Wait(); SetupHost().Wait();
_isInitialized = true; _isInitialized = true;
} }
else
{ CopySharedState();
// Get addresses from existing app
var server = _app!.Services.GetRequiredService<IServer>();
var addressFeatures = server.Features.Get<IServerAddressesFeature>();
if (addressFeatures?.Addresses != null)
{
foreach (var address in addressFeatures.Addresses)
{
Addresses.Add(address);
}
}
}
} }
} }
public void Dispose() public void Dispose()
{ {
lock (this) lock (_sync)
{ {
_instanceCount--; _instanceCount--;
if (_instanceCount == 0 && _app != null) if (_instanceCount == 0 && _app != null)
@ -89,9 +88,33 @@ namespace isnd.tests
_app.StopAsync().Wait(); _app.StopAsync().Wait();
_app = null; _app = null;
_isInitialized = false; _isInitialized = false;
_sharedAddresses.Clear();
_sharedServices = null;
_sharedConfiguration = null;
_sharedSiteSettings = null;
_sharedLogger = null;
_sharedTestClientId = null;
_sharedTestClientSecret = null;
_sharedTestingUserName = null;
_sharedTestingUserPassword = null;
_sharedTestingUserEmail = null;
} }
} }
} }
private void CopySharedState()
{
Addresses = new List<string>(_sharedAddresses);
Logger = _sharedLogger;
Configuration = _sharedConfiguration;
Services = _sharedServices;
SiteSettings = _sharedSiteSettings;
TestClientId = _sharedTestClientId;
TestClientSecret = _sharedTestClientSecret;
TestingUserName = _sharedTestingUserName;
TestingUserPassword = _sharedTestingUserPassword;
TestingUserEmail = _sharedTestingUserEmail;
}
void ConfigureLogger() => Log.Logger = new LoggerConfiguration() void ConfigureLogger() => Log.Logger = new LoggerConfiguration()
.MinimumLevel.Debug() .MinimumLevel.Debug()
.MinimumLevel.Override("Microsoft", LogEventLevel.Warning) .MinimumLevel.Override("Microsoft", LogEventLevel.Warning)
@ -128,10 +151,10 @@ namespace isnd.tests
["Smtp:SenderEmail"] = "test@example.com" ["Smtp:SenderEmail"] = "test@example.com"
}); });
// Configure Kestrel for HTTPS with self-signed certificate // Configure Kestrel for HTTPS with self-signed certificate on a dynamic port
builder.WebHost.ConfigureKestrel(options => builder.WebHost.ConfigureKestrel(options =>
{ {
options.Listen(IPAddress.Loopback, 5001, listenOptions => options.Listen(IPAddress.Loopback, 0, listenOptions =>
{ {
listenOptions.UseHttps(_selfSignedCertificate.Value); listenOptions.UseHttps(_selfSignedCertificate.Value);
}); });
@ -142,7 +165,7 @@ namespace isnd.tests
_app = builder.ConfigureWebAppServices(); _app = builder.ConfigureWebAppServices();
Services = _app.Services; Services = _app.Services;
SiteSettings = _app.Services.GetRequiredService<IOptions<SiteSettings>>().Value; SiteSettings = _app.Services.GetRequiredService<IOptions<SiteSettings>>().Value;
using (var migrationScope = _app.Services.CreateScope()) using (var migrationScope = _app.Services.CreateScope())
{ {
var db = migrationScope.ServiceProvider.GetRequiredService<ApplicationDbContext>(); var db = migrationScope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
@ -151,104 +174,110 @@ namespace isnd.tests
TestingUserName = "Tester"; TestingUserName = "Tester";
TestingUserPassword = "tesT456+*"; TestingUserPassword = "tesT456+*";
TestClientId = "testClientId"; TestClientId = "testClientId";
TestingUserEmail = "test@no-reply.com";
TestingUser = null;
TestClientSecret = Guid.CreateVersion7().ToString(); TestClientSecret = Guid.CreateVersion7().ToString();
EnsureUser(TestingUserName, TestingUserPassword); EnsureUser(TestingUserName, TestingUserPassword, TestingUserEmail, migrationScope);
AddAuthorizedClient(TestClientId, TestClientSecret); AddAuthorizedClient(migrationScope, TestClientId, TestClientSecret);
TestingUser = await db.Users.FirstOrDefaultAsync(u => u.UserName == TestingUserName); TestingUser = await db.Users.FirstOrDefaultAsync(u => u.UserName == TestingUserName);
} }
await _app!.ConfigurePipeline(); await _app!.ConfigurePipeline();
_app.UseSession(); _app.UseSession();
await _app.StartAsync(); await _app.StartAsync();
_sharedServices = _app.Services;
_sharedConfiguration = Configuration;
_sharedSiteSettings = SiteSettings;
_sharedTestClientId = TestClientId;
_sharedTestClientSecret = TestClientSecret;
_sharedTestingUserName = TestingUserName;
_sharedTestingUserPassword = TestingUserPassword;
_sharedTestingUserEmail = TestingUserEmail;
_sharedLogger = _app.Services.GetRequiredService<ILoggerFactory>().CreateLogger<WebServerFixture>();
Logger = _sharedLogger;
var server = _app.Services.GetRequiredService<IServer>();
var logFactory = app.Services.GetRequiredService<ILoggerFactory>();
Logger = logFactory.CreateLogger<WebServerFixture>();
var server = app.Services.GetRequiredService<IServer>();
var addressFeatures = server.Features.Get<IServerAddressesFeature>(); var addressFeatures = server.Features.Get<IServerAddressesFeature>();
if (addressFeatures?.Addresses != null) if (addressFeatures?.Addresses != null)
{ {
_sharedAddresses.Clear();
foreach (var address in addressFeatures.Addresses) foreach (var address in addressFeatures.Addresses)
{ {
_sharedAddresses.Add(address);
Addresses.Add(address); Addresses.Add(address);
} }
} }
} }
private void AddAuthorizedClient(string testClientId, string testClientSecret) private void AddAuthorizedClient(IServiceScope scope, string testClientId, string testClientSecret)
{ {
using (IServiceScope scope = app!.Services.CreateScope())
var db = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
Client testingClient = new Client
{ {
var db = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>(); ClientId = testClientId,
Client testingClient = new Client AccessTokenLifetime = 3600000,
{ AccessTokenType = 1,
ClientId = testClientId, BackChannelLogoutUri = SiteSettings!.Audience,
AccessTokenLifetime = 3600000, ClientName = "Testing client",
AccessTokenType = 1, Enabled = true
BackChannelLogoutUri = SiteSettings!.Audience, };
ClientName = "Testing client", db.Clients.Add(testingClient);
Enabled = true db.SaveChanges();
}; ClientSecret secret = new ClientSecret
db.Clients.Add(testingClient); {
db.SaveChanges(); Value = testClientSecret.Sha256(),
ClientSecret secret = new ClientSecret ClientId = testingClient.Id
{ };
Value = testClientSecret.Sha256(), db.ClientSecrets.Add(secret);
ClientId = testingClient.Id
};
db.ClientSecrets.Add(secret);
var testOrigin = new ClientCorsOrigin var testOrigin = new ClientCorsOrigin
{ {
ClientId = testingClient.Id, ClientId = testingClient.Id,
Origin = SiteSettings!.Audience Origin = SiteSettings!.Audience
}; };
db.ClientCorsOrigins.Add(testOrigin); db.ClientCorsOrigins.Add(testOrigin);
db.ClientGrantTypes.Add(new ClientGrantType db.ClientGrantTypes.Add(new ClientGrantType
{ {
ClientId = testingClient.Id, ClientId = testingClient.Id,
GrantType = "client_credentials" GrantType = "client_credentials"
}); });
db.ClientGrantTypes.Add(new ClientGrantType db.ClientGrantTypes.Add(new ClientGrantType
{ {
ClientId = testingClient.Id, ClientId = testingClient.Id,
GrantType = "password" GrantType = "password"
}); });
db.ClientGrantTypes.Add(new ClientGrantType db.ClientGrantTypes.Add(new ClientGrantType
{ {
ClientId = testingClient.Id, ClientId = testingClient.Id,
GrantType = "code" GrantType = "code"
}); });
db.ClientScopes.Add(new ClientScope db.ClientScopes.Add(new ClientScope
{ {
ClientId = testingClient.Id, ClientId = testingClient.Id,
Scope = "test" Scope = "test"
}); });
db.ApiScopes.Add(new IdentityServer8.EntityFramework.Entities.ApiScope db.ApiScopes.Add(new IdentityServer8.EntityFramework.Entities.ApiScope
{ {
Name = "test", Name = "test",
Enabled = true Enabled = true
}); });
db.ClientRedirectUris.Add(new ClientRedirectUri db.ClientRedirectUris.Add(new ClientRedirectUri
{ {
ClientId = testingClient.Id, ClientId = testingClient.Id,
RedirectUri = SiteSettings!.Audience RedirectUri = SiteSettings!.Audience
}); });
db.SaveChanges(); db.SaveChanges();
}
} }
public void EnsureUser(string testingUserName, string password) public void EnsureUser(string testingUserName, string password, string email, IServiceScope scope)
{ {
if (TestingUser == null) if (TestingUser == null)
{ {
using IServiceScope scope = app!.Services.CreateScope();
var userManager = var userManager =
scope.ServiceProvider.GetRequiredService<UserManager<ApplicationUser>>(); scope.ServiceProvider.GetRequiredService<UserManager<ApplicationUser>>();