2026-04-20 00:35:51 +01:00
2026-04-19 16:02:50 +01:00
using IdentityServer8.EntityFramework.Entities ;
using IdentityServer8.Models ;
2025-07-14 18:58:04 +01:00
using Microsoft.AspNetCore.Builder ;
2026-04-19 16:18:37 +01:00
using Microsoft.AspNetCore.Hosting ;
2025-07-13 18:13:04 +01:00
using Microsoft.AspNetCore.Hosting.Server ;
using Microsoft.AspNetCore.Hosting.Server.Features ;
using Microsoft.AspNetCore.Identity ;
2026-04-20 00:35:51 +01:00
using Microsoft.Data.SqlClient ;
2026-04-19 16:02:50 +01:00
using Microsoft.EntityFrameworkCore ;
2025-07-13 18:13:04 +01:00
using Microsoft.Extensions.Configuration ;
using Microsoft.Extensions.DependencyInjection ;
using Microsoft.Extensions.Logging ;
using Microsoft.Extensions.Options ;
2025-07-30 15:52:01 +01:00
using Serilog ;
using Serilog.Events ;
using Serilog.Sinks.SystemConsole.Themes ;
2026-04-19 16:02:50 +01:00
using System.Net ;
using System.Security.Cryptography ;
using System.Security.Cryptography.X509Certificates ;
using Yavsc ;
using Yavsc.Extensions ;
using Yavsc.Models ;
2026-03-09 02:07:09 +00:00
using Client = IdentityServer8 . EntityFramework . Entities . Client ;
2025-07-13 18:13:04 +01:00
namespace isnd.tests
{
[CollectionDefinition("Web server collection")]
public class WebServerFixture : IDisposable
{
2026-04-19 16:18:37 +01:00
private static readonly Lazy < X509Certificate2 > _selfSignedCertificate = new Lazy < X509Certificate2 > ( CreateSelfSignedCertificate ) ;
2026-04-19 17:15:10 +01:00
private static readonly object _sync = new object ( ) ;
2026-04-19 16:18:37 +01:00
private static WebApplication ? _app ;
private static bool _isInitialized = false ;
private static int _instanceCount = 0 ;
2026-04-19 17:15:10 +01:00
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 ;
2026-04-19 16:18:37 +01:00
2025-07-13 18:13:04 +01:00
public List < string > Addresses { get ; private set ; } = new List < string > ( ) ;
2026-04-19 14:43:37 +01:00
public Microsoft . Extensions . Logging . ILogger ? Logger { get ; internal set ; }
2025-07-13 18:13:04 +01:00
2026-04-19 14:43:37 +01:00
private SiteSettings ? siteSettings ;
2025-07-13 18:13:04 +01:00
2026-04-19 14:43:37 +01:00
public IConfiguration ? Configuration { get ; private set ; }
2025-07-14 18:58:04 +01:00
2026-04-19 14:43:37 +01:00
public string? TestClientId { get ; private set ; }
2025-07-13 18:13:04 +01:00
2026-04-19 14:43:37 +01:00
public IServiceProvider ? Services { get ; private set ; }
public string? TestingUserName { get ; private set ; }
public string? TestingUserPassword { get ; private set ; }
2026-03-09 02:07:09 +00:00
2026-04-19 14:43:37 +01:00
public string? ProtectedTestingApiKey { get ; internal set ; }
public ApplicationUser ? TestingUser { get ; private set ; }
2025-07-13 18:13:04 +01:00
public bool DbCreated { get ; internal set ; }
2026-04-19 14:43:37 +01:00
public SiteSettings ? SiteSettings { get = > siteSettings ; set = > siteSettings = value ; }
public string? TestClientSecret { get ; set ; }
2026-04-19 17:15:10 +01:00
public string? TestingUserEmail { get ; set ; }
2025-07-13 18:13:04 +01:00
public WebServerFixture ( )
{
2026-04-19 17:15:10 +01:00
lock ( _sync )
2026-04-19 16:18:37 +01:00
{
_instanceCount + + ;
if ( ! _isInitialized )
{
SetupHost ( ) . Wait ( ) ;
_isInitialized = true ;
}
2026-04-19 17:15:10 +01:00
CopySharedState ( ) ;
2026-04-19 16:18:37 +01:00
}
2025-07-13 18:13:04 +01:00
}
public void Dispose ( )
{
2026-04-19 17:15:10 +01:00
lock ( _sync )
2026-04-19 16:18:37 +01:00
{
_instanceCount - - ;
if ( _instanceCount = = 0 & & _app ! = null )
{
_app . StopAsync ( ) . Wait ( ) ;
_app = null ;
_isInitialized = false ;
2026-04-19 17:15:10 +01:00
_sharedAddresses . Clear ( ) ;
_sharedServices = null ;
_sharedConfiguration = null ;
_sharedSiteSettings = null ;
_sharedLogger = null ;
_sharedTestClientId = null ;
_sharedTestClientSecret = null ;
_sharedTestingUserName = null ;
_sharedTestingUserPassword = null ;
_sharedTestingUserEmail = null ;
2026-04-19 16:18:37 +01:00
}
}
2025-07-13 18:13:04 +01:00
}
2026-04-19 17:15:10 +01:00
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 ;
}
2025-07-30 15:52:01 +01:00
void ConfigureLogger ( ) = > Log . Logger = new LoggerConfiguration ( )
. MinimumLevel . Debug ( )
. MinimumLevel . Override ( "Microsoft" , LogEventLevel . Warning )
. MinimumLevel . Override ( "Microsoft.Hosting.Lifetime" , LogEventLevel . Information )
. MinimumLevel . Override ( "System" , LogEventLevel . Warning )
. MinimumLevel . Override ( "Microsoft.AspNetCore.Authentication" , LogEventLevel . Information )
. Enrich . FromLogContext ( )
// uncomment to write to Azure diagnostics stream
//.WriteTo.File(
// @"D:\home\LogFiles\Application\identityserver.txt",
// fileSizeLimitBytes: 1_000_000,
// rollOnFileSizeLimit: true,
// shared: true,
// flushToDiskInterval: TimeSpan.FromSeconds(1))
. WriteTo . Console ( outputTemplate : "[{Timestamp:HH:mm:ss} {Level}] {SourceContext}{NewLine}{Message:lj}{NewLine}{Exception}{NewLine}" , theme : AnsiConsoleTheme . Code )
. CreateLogger ( ) ;
2025-07-14 18:58:04 +01:00
public async Task SetupHost ( )
2025-07-13 18:13:04 +01:00
{
2025-07-15 19:43:41 +01:00
2025-07-30 15:52:01 +01:00
var builder = WebApplication . CreateBuilder ( ) ;
2026-04-19 14:40:40 +01:00
builder . Environment . EnvironmentName = "Development" ;
2026-04-19 19:14:37 +01:00
// Set ContentRoot to the Yavsc.Org project directory so WebRootPath resolves correctly
var testAssemblyLocation = AppDomain . CurrentDomain . BaseDirectory ;
var yavscOrgPath = Path . GetFullPath ( Path . Combine ( testAssemblyLocation , "../../src/Yavsc.Org" ) ) ;
builder . Environment . ContentRootPath = yavscOrgPath ;
2025-07-30 15:52:01 +01:00
ConfigureLogger ( ) ;
2026-04-20 00:35:51 +01:00
var config = builder . Configuration
2026-04-19 14:40:40 +01:00
. AddJsonFile ( "appsettings.json" , optional : false , reloadOnChange : false )
2026-04-20 00:35:51 +01:00
. AddJsonFile ( $"appsettings.{builder.Environment.EnvironmentName}.json" , optional : false , reloadOnChange : false )
. AddEnvironmentVariables ( ) . Build ( ) ;
2026-04-19 16:02:50 +01:00
2026-04-19 17:15:10 +01:00
// Configure Kestrel for HTTPS with self-signed certificate on a dynamic port
2026-04-19 16:18:37 +01:00
builder . WebHost . ConfigureKestrel ( options = >
{
2026-04-19 17:15:10 +01:00
options . Listen ( IPAddress . Loopback , 0 , listenOptions = >
2026-04-19 16:18:37 +01:00
{
listenOptions . UseHttps ( _selfSignedCertificate . Value ) ;
} ) ;
} ) ;
2026-04-19 14:40:40 +01:00
Configuration = builder . Configuration ;
2025-07-14 18:58:04 +01:00
2026-04-19 16:18:37 +01:00
_app = builder . ConfigureWebAppServices ( ) ;
Services = _app . Services ;
SiteSettings = _app . Services . GetRequiredService < IOptions < SiteSettings > > ( ) . Value ;
2026-04-20 00:35:51 +01:00
String cxStr = config . GetConnectionString ( Constants . YavscConnectionStringName ) ? ? throw new InvalidOperationException ( "DefaultConnection string is not configured." ) ;
2026-04-19 17:15:10 +01:00
2026-04-19 16:18:37 +01:00
using ( var migrationScope = _app . Services . CreateScope ( ) )
2025-07-13 18:13:04 +01:00
{
2025-07-14 18:58:04 +01:00
var db = migrationScope . ServiceProvider . GetRequiredService < ApplicationDbContext > ( ) ;
2025-09-14 17:46:48 +01:00
db . Database . EnsureDeleted ( ) ;
db . Database . EnsureCreated ( ) ;
TestingUserName = "Tester" ;
2026-04-20 00:35:51 +01:00
TestingUserPassword = "Test123!" ;
2025-09-14 17:46:48 +01:00
TestClientId = "testClientId" ;
2026-04-19 17:15:10 +01:00
TestingUserEmail = "test@no-reply.com" ;
TestingUser = null ;
2026-03-09 02:07:09 +00:00
TestClientSecret = Guid . CreateVersion7 ( ) . ToString ( ) ;
2026-04-19 17:15:10 +01:00
EnsureUser ( TestingUserName , TestingUserPassword , TestingUserEmail , migrationScope ) ;
AddAuthorizedClient ( migrationScope , TestClientId , TestClientSecret ) ;
2026-03-09 02:07:09 +00:00
TestingUser = await db . Users . FirstOrDefaultAsync ( u = > u . UserName = = TestingUserName ) ;
2025-07-14 18:58:04 +01:00
}
2026-04-19 18:10:00 +01:00
// Seed IdentityServer ConfigurationDbContext with API resources and scopes
using ( var configScope = _app . Services . CreateScope ( ) )
{
try
{
var configDbContext = configScope . ServiceProvider . GetService < IdentityServer8 . EntityFramework . DbContexts . ConfigurationDbContext > ( ) ;
if ( configDbContext ! = null )
{
configDbContext . Database . EnsureCreated ( ) ;
// Add test API scope if it doesn't exist
var testScope = configDbContext . ApiScopes . FirstOrDefault ( s = > s . Name = = "test" ) ;
if ( testScope = = null )
{
configDbContext . ApiScopes . Add ( new IdentityServer8 . EntityFramework . Entities . ApiScope
{
Name = "test" ,
Enabled = true ,
2026-04-19 20:36:03 +01:00
DisplayName = "Test API Scope" ,
Description = "Scope for testing purposes" ,
UserClaims = new List < IdentityServer8 . EntityFramework . Entities . ApiScopeClaim >
{
new IdentityServer8 . EntityFramework . Entities . ApiScopeClaim { Type = "role" } ,
new IdentityServer8 . EntityFramework . Entities . ApiScopeClaim { Type = "email" }
}
2026-04-19 18:10:00 +01:00
} ) ;
// Add a basic API resource for the test scope
var apiResource = new IdentityServer8 . EntityFramework . Entities . ApiResource
{
Name = "testapi" ,
DisplayName = "Test API" ,
Enabled = true ,
Scopes = new List < IdentityServer8 . EntityFramework . Entities . ApiResourceScope >
{
new IdentityServer8 . EntityFramework . Entities . ApiResourceScope
{
Scope = "test"
}
}
} ;
configDbContext . ApiResources . Add ( apiResource ) ;
configDbContext . SaveChanges ( ) ;
}
}
}
catch ( Exception ex )
{
_sharedLogger ? . LogWarning ( $"Failed to seed ConfigurationDbContext: {ex.Message}" ) ;
// Don't fail the fixture if seeding fails
}
}
2026-04-19 16:18:37 +01:00
await _app ! . ConfigurePipeline ( ) ;
_app . UseSession ( ) ;
await _app . StartAsync ( ) ;
2025-07-15 19:43:41 +01:00
2026-04-19 17:15:10 +01:00
_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 ;
2025-07-15 19:43:41 +01:00
2026-04-19 17:15:10 +01:00
var server = _app . Services . GetRequiredService < IServer > ( ) ;
2025-07-13 18:13:04 +01:00
var addressFeatures = server . Features . Get < IServerAddressesFeature > ( ) ;
2026-04-19 14:43:37 +01:00
if ( addressFeatures ? . Addresses ! = null )
2025-07-13 18:13:04 +01:00
{
2026-04-19 17:15:10 +01:00
_sharedAddresses . Clear ( ) ;
2026-04-19 14:43:37 +01:00
foreach ( var address in addressFeatures . Addresses )
{
2026-04-19 17:15:10 +01:00
_sharedAddresses . Add ( address ) ;
2026-04-19 14:43:37 +01:00
Addresses . Add ( address ) ;
}
2025-07-13 18:13:04 +01:00
}
2026-03-09 02:07:09 +00:00
}
2026-04-19 17:15:10 +01:00
private void AddAuthorizedClient ( IServiceScope scope , string testClientId , string testClientSecret )
2026-03-09 02:07:09 +00:00
{
2026-04-19 20:36:03 +01:00
var configDb = scope . ServiceProvider . GetRequiredService < IdentityServer8 . EntityFramework . DbContexts . ConfigurationDbContext > ( ) ;
2026-04-19 19:14:37 +01:00
if ( configDb = = null )
throw new InvalidOperationException ( "ConfigurationDbContext is not available for IdentityServer client seeding." ) ;
2026-04-19 17:15:10 +01:00
Client testingClient = new Client
2026-03-09 02:07:09 +00:00
{
2026-04-19 17:15:10 +01:00
ClientId = testClientId ,
AccessTokenLifetime = 3600000 ,
AccessTokenType = 1 ,
ClientName = "Testing client" ,
2026-04-19 19:14:37 +01:00
Enabled = true ,
RequireClientSecret = true
2026-04-19 17:15:10 +01:00
} ;
2026-04-19 20:36:03 +01:00
configDb . Set < Client > ( ) . Add ( testingClient ) ;
2026-04-19 19:14:37 +01:00
configDb . SaveChanges ( ) ;
2026-04-19 20:36:03 +01:00
var apiScope = new IdentityServer8 . EntityFramework . Entities . ApiScope
{
Name = "test" ,
DisplayName = "Test Scope" ,
Description = "Scope for testing" ,
Enabled = true ,
Required = false ,
ShowInDiscoveryDocument = true ,
Emphasize = false
} ;
configDb . Set < IdentityServer8 . EntityFramework . Entities . ApiScope > ( ) . Add ( apiScope ) ;
2026-04-19 17:15:10 +01:00
ClientSecret secret = new ClientSecret
{
Value = testClientSecret . Sha256 ( ) ,
2026-04-19 19:14:37 +01:00
Type = IdentityServer8 . IdentityServerConstants . SecretTypes . SharedSecret ,
2026-04-19 17:15:10 +01:00
ClientId = testingClient . Id
} ;
2026-04-19 19:14:37 +01:00
configDb . Set < ClientSecret > ( ) . Add ( secret ) ;
2026-03-09 02:07:09 +00:00
2026-04-19 19:14:37 +01:00
configDb . Set < ClientGrantType > ( ) . Add ( new ClientGrantType
2026-04-19 17:15:10 +01:00
{
ClientId = testingClient . Id ,
GrantType = "client_credentials"
} ) ;
2026-04-19 19:14:37 +01:00
configDb . Set < ClientGrantType > ( ) . Add ( new ClientGrantType
2026-04-19 17:15:10 +01:00
{
ClientId = testingClient . Id ,
GrantType = "password"
} ) ;
2026-04-19 19:14:37 +01:00
configDb . Set < ClientScope > ( ) . Add ( new ClientScope
2026-04-19 17:15:10 +01:00
{
ClientId = testingClient . Id ,
Scope = "test"
} ) ;
2026-03-09 02:07:09 +00:00
2026-04-19 19:14:37 +01:00
configDb . SaveChanges ( ) ;
2025-07-13 18:13:04 +01:00
}
2026-04-19 17:15:10 +01:00
public void EnsureUser ( string testingUserName , string password , string email , IServiceScope scope )
2025-07-13 18:13:04 +01:00
{
if ( TestingUser = = null )
{
2026-03-09 02:07:09 +00:00
var userManager =
2025-07-13 18:13:04 +01:00
scope . ServiceProvider . GetRequiredService < UserManager < ApplicationUser > > ( ) ;
TestingUser = new ApplicationUser
{
2025-07-15 15:42:30 +01:00
UserName = testingUserName ,
Email = testingUserName + "@example.com" ,
EmailConfirmed = true
2025-07-13 18:13:04 +01:00
} ;
2026-03-09 02:07:09 +00:00
var result = userManager . CreateAsync ( TestingUser , password ) . Result ;
2025-07-13 18:13:04 +01:00
Assert . True ( result . Succeeded ) ;
ApplicationDbContext dbContext =
scope . ServiceProvider . GetRequiredService < ApplicationDbContext > ( ) ;
TestingUser = dbContext . Users . FirstOrDefault ( u = > u . UserName = = testingUserName ) ;
}
}
2026-04-19 16:18:37 +01:00
private static X509Certificate2 CreateSelfSignedCertificate ( )
{
var rsa = RSA . Create ( 2048 ) ;
var certRequest = new CertificateRequest ( "CN=localhost" , rsa , HashAlgorithmName . SHA256 , RSASignaturePadding . Pkcs1 ) ;
certRequest . CertificateExtensions . Add (
new X509KeyUsageExtension ( X509KeyUsageFlags . DataEncipherment | X509KeyUsageFlags . KeyEncipherment | X509KeyUsageFlags . DigitalSignature , false ) ) ;
certRequest . CertificateExtensions . Add (
new X509EnhancedKeyUsageExtension (
new OidCollection { new Oid ( "1.3.6.1.5.5.7.3.1" ) } , false ) ) ;
var certificate = certRequest . CreateSelfSigned ( new DateTimeOffset ( DateTime . UtcNow . AddDays ( - 1 ) ) , new DateTimeOffset ( DateTime . UtcNow . AddDays ( 3650 ) ) ) ;
return certificate ;
}
2025-07-13 18:13:04 +01:00
}
}