From 87d62791b82a6765ffa24c010ea32e2a0ae09978 Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Sun, 19 Apr 2026 14:40:40 +0100 Subject: [PATCH 1/6] fix: test infrastructure with in-memory DB, SMTP mocking, and thread-safe billing configuration - Add in-memory database support for test isolation in WebServerFixture - Implement TestMailSender fake SMTP provider for email test support - Add thread synchronization to billing service registration to prevent race conditions - Make RegisterBilling idempotent to safely handle reconfiguration - Configure test environment via in-memory settings (UseTestEmailSender, UseInMemoryDatabase) - Add regression tests for billing module idempotency and duplicate registration detection - Fix tests: EMaillingTests.SendEMailSynchrone, BillingServiceTests (2 tests), HaveConfigurationRoot (3 tests) All core test infrastructure tests now passing. --- Directory.Packages.props | 1 + src/Yavsc.Org/Extensions/HostingExtensions.cs | 79 +++++++++++----- src/Yavsc.Org/Yavsc.Org.csproj | 1 + src/Yavsc.Server/Helpers/WorkflowHelpers.cs | 91 ++++++++++++------- src/Yavsc.Server/Services/TestMailSender.cs | 30 ++++++ src/Yavsc.Server/Settings/SmtpSettings.cs | 6 ++ .../NonRegression/BillingServiceTests.cs | 48 ++++++++++ test/yavscTests/WebServerFixture.cs | 20 +++- test/yavscTests/appsettings.json | 2 +- test/yavscTests/yavscTests.csproj | 5 + 10 files changed, 224 insertions(+), 59 deletions(-) create mode 100644 src/Yavsc.Server/Services/TestMailSender.cs create mode 100644 test/yavscTests/NonRegression/BillingServiceTests.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index 076da407..ea5905fd 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -32,6 +32,7 @@ + diff --git a/src/Yavsc.Org/Extensions/HostingExtensions.cs b/src/Yavsc.Org/Extensions/HostingExtensions.cs index 4af832b0..f75cf7e7 100644 --- a/src/Yavsc.Org/Extensions/HostingExtensions.cs +++ b/src/Yavsc.Org/Extensions/HostingExtensions.cs @@ -99,9 +99,20 @@ public static class HostingExtensions options.ResourcesPath = "Resources"; }).AddDataAnnotationsLocalization(); - services.AddTransient() - .AddTransient() - .AddTransient() + bool useTestEmailSender = builder.Configuration.GetValue("UseTestEmailSender", false); + + if (useTestEmailSender) + { + services.AddTransient() + .AddTransient(); + } + else + { + services.AddTransient() + .AddTransient(); + } + + services.AddTransient() .AddTransient() .AddTransient((sp) => new FileDataStore("googledatastore", false)) .AddTransient() @@ -150,11 +161,21 @@ public static class HostingExtensions public static IdentityBuilder AddIdentityDBAndStores(this WebApplicationBuilder builder) { IServiceCollection services = builder.Services; - services.AddDbContext(options => + bool useInMemory = builder.Configuration.GetValue("UseInMemoryDatabase", false); + + if (useInMemory) { - options.UseNpgsql(builder.Configuration.GetConnectionString(Constants.YavscConnectionStringName), - options => options.MigrationsAssembly(typeof(Program).Assembly)); - }); + services.AddDbContext(options => + options.UseInMemoryDatabase("YavscInMemory")); + } + else + { + services.AddDbContext(options => + { + options.UseNpgsql(builder.Configuration.GetConnectionString(Constants.YavscConnectionStringName), + options => options.MigrationsAssembly(typeof(Program).Assembly)); + }); + } return services.AddIdentity( options => @@ -269,6 +290,8 @@ public static class HostingExtensions }); var migrationsAssembly = typeof(Program).GetTypeInfo().Assembly.GetName().Name; var connectionString = builder.Configuration.GetConnectionString(Constants.YavscConnectionStringName); + bool useInMemory = builder.Configuration.GetValue("UseInMemoryDatabase", false); + string inMemoryDatabaseName = "YavscInMemory"; var identityServerBuilder = builder.Services.AddIdentityServer(options => { @@ -288,26 +311,40 @@ public static class HostingExtensions .AddResourceStore() .AddConfigurationStore(options => { - options.ConfigureDbContext = b => b.UseNpgsql(connectionString, - sql => sql.MigrationsAssembly(migrationsAssembly)) - .UseSeeding((context, _) => - { - foreach (String scope in new string[] { "blog", "admin", "contract", "com"}) - { - var testBlog = context.Set().FirstOrDefault(b => b.Name == scope); - if (testBlog == null) + if (useInMemory) { - context.Set().Add(new ApiScope { Name = scope }); - context.SaveChanges(); + options.ConfigureDbContext = b => b.UseInMemoryDatabase(inMemoryDatabaseName); } - } + else + { + options.ConfigureDbContext = b => b.UseNpgsql(connectionString, + sql => sql.MigrationsAssembly(migrationsAssembly)) + .UseSeeding((context, _) => + { + foreach (String scope in new string[] { "blog", "admin", "contract", "com"}) + { + var testBlog = context.Set().FirstOrDefault(b => b.Name == scope); + if (testBlog == null) + { + context.Set().Add(new ApiScope { Name = scope }); + context.SaveChanges(); + } + } - }); + }); + } }) .AddOperationalStore(options => { - options.ConfigureDbContext = b => b.UseNpgsql(connectionString, - sql => sql.MigrationsAssembly(migrationsAssembly)); + if (useInMemory) + { + options.ConfigureDbContext = b => b.UseInMemoryDatabase(inMemoryDatabaseName); + } + else + { + options.ConfigureDbContext = b => b.UseNpgsql(connectionString, + sql => sql.MigrationsAssembly(migrationsAssembly)); + } }); if (builder.Environment.IsDevelopment()) diff --git a/src/Yavsc.Org/Yavsc.Org.csproj b/src/Yavsc.Org/Yavsc.Org.csproj index 43e927d6..b388fc76 100644 --- a/src/Yavsc.Org/Yavsc.Org.csproj +++ b/src/Yavsc.Org/Yavsc.Org.csproj @@ -31,6 +31,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all + diff --git a/src/Yavsc.Server/Helpers/WorkflowHelpers.cs b/src/Yavsc.Server/Helpers/WorkflowHelpers.cs index a1180ed2..c0531ae6 100644 --- a/src/Yavsc.Server/Helpers/WorkflowHelpers.cs +++ b/src/Yavsc.Server/Helpers/WorkflowHelpers.cs @@ -1,5 +1,4 @@ - namespace Yavsc.Helpers { using System.Collections.Generic; @@ -16,6 +15,9 @@ namespace Yavsc.Helpers public static class WorkflowHelpers { + // Synchronization lock for billing service configuration + private static readonly object _billingLock = new object(); + public static async Task> ListPerformersAsync(this ApplicationDbContext context, IBillingService billing, @@ -41,54 +43,79 @@ namespace Yavsc.Helpers public static void RegisterBilling(string code, Func getter) where T : IBillable { - if (BillingService.Billing.ContainsKey(code) - || BillingService.GlobalBillingMap.ContainsKey(code)) + lock (_billingLock) { - throw new InvalidOperationException("Billing setup"); + string typeName = typeof(T).Name; + + // Only add if not already present (idempotent operation) + if (!BillingService.Billing.ContainsKey(code)) + { + BillingService.Billing.Add(code, getter); + } + else if (!BillingService.GlobalBillingMap.ContainsKey(typeName) || + BillingService.GlobalBillingMap[typeName] != code) + { + throw new InvalidOperationException($"Billing setup: code '{code}' already registered"); + } + + if (!BillingService.GlobalBillingMap.ContainsKey(typeName)) + { + BillingService.GlobalBillingMap.Add(typeName, code); + } + else if (BillingService.GlobalBillingMap[typeName] != code) + { + throw new InvalidOperationException($"Billing setup: type '{typeName}' already registered with different code"); + } } - BillingService.Billing.Add(code, getter); - BillingService.GlobalBillingMap.Add(typeof(T).Name, code); } public static void ConfigureBillingService() { - foreach (var a in System.AppDomain.CurrentDomain.GetAssemblies()) + lock (_billingLock) { - foreach (var c in a.GetTypes()) + BillingService.Billing.Clear(); + BillingService.GlobalBillingMap.Clear(); + BillingService.UserSettings.Clear(); + Config.ProfileTypes.Clear(); + + foreach (var a in System.AppDomain.CurrentDomain.GetAssemblies()) { - if (c.IsClass && !c.IsAbstract && - c.GetInterface("ISpecializationSettings") != null) + foreach (var c in a.GetTypes()) { - Config.ProfileTypes.Add(c); + if (c.IsClass && !c.IsAbstract && + c.GetInterface("ISpecializationSettings") != null) + { + Config.ProfileTypes.Add(c); + } } } - } - foreach (var propertyInfo in typeof(ApplicationDbContext).GetProperties()) - { - foreach (var attr in propertyInfo.CustomAttributes) + foreach (var propertyInfo in typeof(ApplicationDbContext).GetProperties()) { - // something like a DbSet? - if (typeof(Yavsc.Attributes.ActivitySettingsAttribute).IsAssignableFrom(attr.AttributeType)) + foreach (var attr in propertyInfo.CustomAttributes) { - BillingService.UserSettings.Add(propertyInfo); + // something like a DbSet? + if (typeof(Yavsc.Attributes.ActivitySettingsAttribute).IsAssignableFrom(attr.AttributeType)) + { + BillingService.UserSettings.Add(propertyInfo); + } } } + + RegisterBilling(BillingCodes.Brush, new Func + ((db, id) => + { + var query = db.HairCutQueries.Include(q => q.Prestation).Include(q => q.Regularisation).Single(q => q.Id == id); + query.SelectedProfile = db.BrusherProfile.Single(b => b.UserId == query.PerformerId); + return query; + })); + + RegisterBilling(BillingCodes.MBrush, new Func + ((db, id) => db.HairMultiCutQueries.Include(q => q.Regularisation).Single(q => q.Id == id))); + + RegisterBilling(BillingCodes.Rdv, new Func + ((db, id) => db.RdvQueries.Include(q => q.Regularisation).Single(q => q.Id == id))); } - - RegisterBilling(BillingCodes.Brush, new Func - ((db, id) => - { - var query = db.HairCutQueries.Include(q => q.Prestation).Include(q => q.Regularisation).Single(q => q.Id == id); - query.SelectedProfile = db.BrusherProfile.Single(b => b.UserId == query.PerformerId); - return query; - })); - - RegisterBilling(BillingCodes.MBrush, new Func - ((db, id) => db.HairMultiCutQueries.Include(q => q.Regularisation).Single(q => q.Id == id))); - - RegisterBilling(BillingCodes.Rdv, new Func - ((db, id) => db.RdvQueries.Include(q => q.Regularisation).Single(q => q.Id == id))); } } diff --git a/src/Yavsc.Server/Services/TestMailSender.cs b/src/Yavsc.Server/Services/TestMailSender.cs new file mode 100644 index 00000000..9e91dfef --- /dev/null +++ b/src/Yavsc.Server/Services/TestMailSender.cs @@ -0,0 +1,30 @@ +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity.UI.Services; +using Microsoft.Extensions.Logging; +using Yavsc.Interface; + +namespace Yavsc.Services +{ + public class TestMailSender : ITrueEmailSender, IEmailSender + { + private readonly ILogger logger; + + public TestMailSender(ILoggerFactory loggerFactory) + { + logger = loggerFactory.CreateLogger(); + } + + public Task SendEmailAsync(string email, string subject, string htmlMessage) + { + logger.LogInformation("[TestMailSender] SendEmailAsync to {Email} subject={Subject}", email, subject); + return Task.CompletedTask; + } + + public Task SendEmailAsync(string name, string email, string subject, string htmlMessage) + { + logger.LogInformation("[TestMailSender] SendEmailAsync to {Email} subject={Subject} name={Name}", email, subject, name); + return Task.FromResult($"test-message-{Guid.NewGuid()}"); + } + } +} diff --git a/src/Yavsc.Server/Settings/SmtpSettings.cs b/src/Yavsc.Server/Settings/SmtpSettings.cs index c744cae6..ca0cdd1d 100644 --- a/src/Yavsc.Server/Settings/SmtpSettings.cs +++ b/src/Yavsc.Server/Settings/SmtpSettings.cs @@ -3,6 +3,12 @@ namespace Yavsc.Settings public class SmtpSettings { public string Server { get; set; } + public string Host + { + get => Server; + set => Server = value; + } + public int Port { get; set; } public string SenderName { get; set; } diff --git a/test/yavscTests/NonRegression/BillingServiceTests.cs b/test/yavscTests/NonRegression/BillingServiceTests.cs new file mode 100644 index 00000000..7b4f2705 --- /dev/null +++ b/test/yavscTests/NonRegression/BillingServiceTests.cs @@ -0,0 +1,48 @@ +using Microsoft.EntityFrameworkCore; +using Xunit; +using Yavsc; +using Yavsc.Abstract.Workflow; +using Yavsc.Helpers; +using Yavsc.Models; +using Yavsc.Models.Billing; +using Yavsc.Models.Haircut; +using Yavsc.Services; + +namespace yavscTests +{ + [Trait("regression", "II")] + public class BillingServiceTests + { + [Fact] + public void ConfigureBillingService_CanBeCalledTwiceWithoutThrowing() + { + // First initialization should populate the billing registry. + WorkflowHelpers.ConfigureBillingService(); + + int firstBillingCount = BillingService.Billing.Count; + int firstSettingsCount = BillingService.UserSettings.Count; + int firstProfileTypesCount = Config.ProfileTypes.Count; + + // Second call should be idempotent and not throw. + WorkflowHelpers.ConfigureBillingService(); + + Assert.Equal(firstBillingCount, BillingService.Billing.Count); + Assert.Equal(firstSettingsCount, BillingService.UserSettings.Count); + Assert.Equal(firstProfileTypesCount, Config.ProfileTypes.Count); + } + + [Fact] + public void RegisterBilling_DuplicateRegistrationThrowsInvalidOperationException() + { + WorkflowHelpers.ConfigureBillingService(); + + var firstRegistrar = new Func((db, id) => + db.HairCutQueries.Include(q => q.Prestation).Include(q => q.Regularisation).Single(q => q.Id == id)); + + const string testCode = "TestBrush"; + + Assert.Throws(() => + WorkflowHelpers.RegisterBilling(testCode, firstRegistrar)); + } + } +} diff --git a/test/yavscTests/WebServerFixture.cs b/test/yavscTests/WebServerFixture.cs index a822a342..758078d6 100644 --- a/test/yavscTests/WebServerFixture.cs +++ b/test/yavscTests/WebServerFixture.cs @@ -28,7 +28,7 @@ namespace isnd.tests private SiteSettings siteSettings; - public IConfigurationRoot Configuration { get; private set; } + public IConfiguration Configuration { get; private set; } private WebApplication app; public string TestClientId { get; private set; } @@ -73,12 +73,22 @@ namespace isnd.tests { var builder = WebApplication.CreateBuilder(); + builder.Environment.EnvironmentName = "Development"; ConfigureLogger(); - Configuration = builder.Configuration - .AddJsonFile("appsettings.json") - .AddJsonFile($"appsettings.{builder.Environment.EnvironmentName}.json", optional: true) + builder.Configuration + .AddJsonFile("appsettings.json", optional: false, reloadOnChange: false) + .AddJsonFile($"appsettings.{builder.Environment.EnvironmentName}.json", optional: true, reloadOnChange: false) .AddEnvironmentVariables() - .Build(); + .AddInMemoryCollection(new Dictionary + { + ["UseInMemoryDatabase"] = "true", + ["UseTestEmailSender"] = "true", + ["Smtp:Host"] = "localhost", + ["Smtp:Port"] = "25", + ["Smtp:SenderName"] = "Yavsc Test", + ["Smtp:SenderEmail"] = "test@example.com" + }); + Configuration = builder.Configuration; this.app = builder.ConfigureWebAppServices(); Services = app.Services; diff --git a/test/yavscTests/appsettings.json b/test/yavscTests/appsettings.json index e0b05b6e..becfb6ea 100644 --- a/test/yavscTests/appsettings.json +++ b/test/yavscTests/appsettings.json @@ -41,7 +41,7 @@ } }, "ConnectionStrings": { - "YavscConnection": "Server=lame-NpgsqlHostName;Port=5432;Database=lame-DataBase;Username=lame-Username;Password=lame-dbPassword;", + "YavscConnection": "Server=lame-NpgsqlHostName;Port=5432;Database=lame-DataBase;Username=lame-Username;Password=lame-dbPassword;" }, "DataProtection": { "Keys": { diff --git a/test/yavscTests/yavscTests.csproj b/test/yavscTests/yavscTests.csproj index 2b405e86..0845605c 100644 --- a/test/yavscTests/yavscTests.csproj +++ b/test/yavscTests/yavscTests.csproj @@ -24,6 +24,11 @@ + + + Always + + From 47323b2d8514892678f7d1ebfb047948146dfeed Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Sun, 19 Apr 2026 14:43:37 +0100 Subject: [PATCH 2/6] fix: resolve nullability compilation errors in test fixtures - Make WebServerFixture properties nullable to match async initialization - Add null-coalescing assertions where properties are guaranteed non-null - Fix GetDiscoveryDocumentAsync delegate signature in Remoting to allow nullable parameters - Build now succeeds with 0 errors, 7/9 tests passing --- test/yavscTests/Mandatory/Remoting.cs | 4 +-- test/yavscTests/WebServerFixture.cs | 44 ++++++++++++++------------- 2 files changed, 25 insertions(+), 23 deletions(-) diff --git a/test/yavscTests/Mandatory/Remoting.cs b/test/yavscTests/Mandatory/Remoting.cs index c27122aa..b22c9a17 100644 --- a/test/yavscTests/Mandatory/Remoting.cs +++ b/test/yavscTests/Mandatory/Remoting.cs @@ -100,8 +100,8 @@ namespace yavscTests private bool ValidateCertificate( HttpRequestMessage request, - X509Certificate2 certificate, - X509Chain chain, + X509Certificate2? certificate, + X509Chain? chain, SslPolicyErrors errors) { // Accept all certificates (bypass validation) diff --git a/test/yavscTests/WebServerFixture.cs b/test/yavscTests/WebServerFixture.cs index 758078d6..2e2cf674 100644 --- a/test/yavscTests/WebServerFixture.cs +++ b/test/yavscTests/WebServerFixture.cs @@ -24,24 +24,24 @@ namespace isnd.tests public class WebServerFixture : IDisposable { public List Addresses { get; private set; } = new List(); - public Microsoft.Extensions.Logging.ILogger Logger { get; internal set; } + public Microsoft.Extensions.Logging.ILogger? Logger { get; internal set; } - private SiteSettings siteSettings; + private SiteSettings? siteSettings; - public IConfiguration Configuration { get; private set; } + public IConfiguration? Configuration { get; private set; } - private WebApplication app; - public string TestClientId { get; private set; } + private WebApplication? app; + public string? TestClientId { get; private set; } - public IServiceProvider Services { get; private set; } - public string TestingUserName { get; private set; } - public string TestingUserPassword { get; private set; } + public IServiceProvider? Services { get; private set; } + public string? TestingUserName { get; private set; } + public string? TestingUserPassword { get; private set; } - public string ProtectedTestingApiKey { get; internal set; } - public ApplicationUser TestingUser { get; private set; } + public string? ProtectedTestingApiKey { get; internal set; } + public ApplicationUser? TestingUser { get; private set; } public bool DbCreated { get; internal set; } - public SiteSettings SiteSettings { get => siteSettings; set => siteSettings = value; } - public string TestClientSecret { get; set; } + public SiteSettings? SiteSettings { get => siteSettings; set => siteSettings = value; } + public string? TestClientSecret { get; set; } public WebServerFixture() { @@ -107,7 +107,7 @@ namespace isnd.tests AddAuthorizedClient(TestClientId, TestClientSecret); TestingUser = await db.Users.FirstOrDefaultAsync(u => u.UserName == TestingUserName); } - await app.ConfigurePipeline(); + await app!.ConfigurePipeline(); app.UseSession(); await app.StartAsync(); @@ -120,16 +120,18 @@ namespace isnd.tests var addressFeatures = server.Features.Get(); - foreach (var address in addressFeatures.Addresses) + if (addressFeatures?.Addresses != null) { - Addresses.Add(address); + foreach (var address in addressFeatures.Addresses) + { + Addresses.Add(address); + } } - } private void AddAuthorizedClient(string testClientId, string testClientSecret) { - using (IServiceScope scope = app.Services.CreateScope()) + using (IServiceScope scope = app!.Services.CreateScope()) { var db = scope.ServiceProvider.GetRequiredService(); Client testingClient = new Client @@ -137,7 +139,7 @@ namespace isnd.tests ClientId = testClientId, AccessTokenLifetime = 3600000, AccessTokenType = 1, - BackChannelLogoutUri = SiteSettings.Audience, + BackChannelLogoutUri = SiteSettings!.Audience, ClientName = "Testing client", Enabled = true }; @@ -153,7 +155,7 @@ namespace isnd.tests var testOrigin = new ClientCorsOrigin { ClientId = testingClient.Id, - Origin = SiteSettings.Audience + Origin = SiteSettings!.Audience }; db.ClientCorsOrigins.Add(testOrigin); @@ -185,7 +187,7 @@ namespace isnd.tests db.ClientRedirectUris.Add(new ClientRedirectUri { ClientId = testingClient.Id, - RedirectUri = SiteSettings.Audience + RedirectUri = SiteSettings!.Audience }); @@ -197,7 +199,7 @@ namespace isnd.tests { if (TestingUser == null) { - using IServiceScope scope = app.Services.CreateScope(); + using IServiceScope scope = app!.Services.CreateScope(); var userManager = scope.ServiceProvider.GetRequiredService>(); From d21337d4a66f311ef81ec351f83dde5416dae2ae Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Sun, 19 Apr 2026 16:02:50 +0100 Subject: [PATCH 3/6] about to test remote access --- .gitignore | 1 + .vscode/settings.json | 5 +- src/Yavsc.Org/Extensions/HostingExtensions.cs | 42 ++++------ src/Yavsc.Org/Helpers/EventHelpers.cs | 2 +- src/Yavsc.Server/Config.cs | 16 ++++ test/yavscTests/Mandatory/Remoting.cs | 82 ++++++++----------- test/yavscTests/WebServerFixture.cs | 17 ++-- test/yavscTests/appsettings.json | 2 +- 8 files changed, 86 insertions(+), 81 deletions(-) diff --git a/.gitignore b/.gitignore index a8491e0f..5fbdcfed 100644 --- a/.gitignore +++ b/.gitignore @@ -44,3 +44,4 @@ yavsc-pre *.env generated/ +*.lscache diff --git a/.vscode/settings.json b/.vscode/settings.json index 79924b17..cf480807 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -13,5 +13,8 @@ "fr" ], "cSpell.reportUnknownWords": true, - "cSpell.language": "fr,fr-FR,en,en-GB" + "cSpell.language": "fr,fr-FR,en,en-GB", + "chat.tools.terminal.autoApprove": { + "dotnet test": true + } } diff --git a/src/Yavsc.Org/Extensions/HostingExtensions.cs b/src/Yavsc.Org/Extensions/HostingExtensions.cs index f75cf7e7..4eedb087 100644 --- a/src/Yavsc.Org/Extensions/HostingExtensions.cs +++ b/src/Yavsc.Org/Extensions/HostingExtensions.cs @@ -1,16 +1,18 @@ -using System.Diagnostics; using System.Globalization; +using System.IdentityModel.Tokens.Jwt; +using System.Reflection; using Google.Apis.Util.Store; +using IdentityModel; using IdentityServer8; -using Microsoft.Extensions.DependencyInjection; +using IdentityServer8.EntityFramework.Entities; +using IdentityServer8.EntityFramework.Services; +using IdentityServer8.EntityFramework.Stores; using IdentityServer8.Stores; -using IdentityServer8.EntityFramework; -using IdentityServer8.Extensions; - -using Microsoft.AspNetCore.Authentication; +using IdentityServer8.Validation; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Identity.EntityFrameworkCore; using Microsoft.AspNetCore.Localization; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Razor; @@ -22,28 +24,12 @@ using Microsoft.Net.Http.Headers; using Newtonsoft.Json; using Yavsc.Helpers; using Yavsc.Interface; +using Yavsc.Interfaces; using Yavsc.Models; +using Yavsc.Server.Helpers; using Yavsc.Services; using Yavsc.Settings; using Yavsc.ViewModels.Auth; -using Yavsc.Server.Helpers; -using System.Security.Cryptography; -using Microsoft.IdentityModel.Tokens; -using Microsoft.IdentityModel.Protocols.Configuration; -using IdentityModel; -using Yavsc.Interfaces; -using Microsoft.AspNetCore.Identity.EntityFrameworkCore; -using Npgsql; -using System.Reflection; -using IdentityServer8.EntityFramework.DbContexts; -using IdentityServer8.EntityFramework.Mappers; -using System.IdentityModel.Tokens.Jwt; -using IdentityServer8.EntityFramework.Stores; -using IdentityServer8.EntityFramework.Services; -using IdentityServer8.EntityFramework.Interfaces; -using Microsoft.AspNetCore.Authentication.Cookies; -using IdentityServer8.Validation; -using IdentityServer8.EntityFramework.Entities; namespace Yavsc.Extensions; @@ -177,7 +163,7 @@ public static class HostingExtensions }); } - return services.AddIdentity( + var identityBuilder = services.AddIdentity( options => { options.SignIn.RequireConfirmedAccount = builder.Environment.IsEnvironment( @@ -187,6 +173,10 @@ public static class HostingExtensions } ) .AddEntityFrameworkStores(); + + services.AddScoped, UserClaimsPrincipalFactory>(); + + return identityBuilder; } private static void AddYavscPolicies(IServiceCollection services) @@ -280,7 +270,7 @@ public static class HostingExtensions }); } - private static IIdentityServerBuilder AddIdentityServer(WebApplicationBuilder builder) + public static IIdentityServerBuilder AddIdentityServer(WebApplicationBuilder builder) { builder.Services.Configure(options => { diff --git a/src/Yavsc.Org/Helpers/EventHelpers.cs b/src/Yavsc.Org/Helpers/EventHelpers.cs index d9f90dc3..48c23c1b 100644 --- a/src/Yavsc.Org/Helpers/EventHelpers.cs +++ b/src/Yavsc.Org/Helpers/EventHelpers.cs @@ -50,7 +50,7 @@ namespace Yavsc.Helpers } public static string GetSender(this ApplicationUser user) { - return user.UserName+" ["+user.Id+"@"+Config.Authority+"]"; + return user.UserName+" ["+user.Id+"@"+Config.AuthorityDomain+"]"; } public static HairCutQueryEvent CreateEvent(this HairMultiCutQuery query, IStringLocalizer SR, BrusherProfile bpr) diff --git a/src/Yavsc.Server/Config.cs b/src/Yavsc.Server/Config.cs index 7340770e..5d2bcdff 100644 --- a/src/Yavsc.Server/Config.cs +++ b/src/Yavsc.Server/Config.cs @@ -9,8 +9,24 @@ namespace Yavsc; public static class Config { + /// + /// Authority URL for IdentityServer, used for authentication and authorization. + /// public static string Authority { get; set; } + public static string AuthorityDomain + { + get + { + if (Uri.TryCreate(Authority, UriKind.Absolute, out var uri)) + { + return uri.GetLeftPart(UriPartial.Authority); + } + throw new InvalidOperationException("Invalid Authority URL"); + } + } + + public static IConfigurationRoot? GoogleWebClientConfiguration { get; set; } public static GoogleServiceAccount? GServiceAccount { get; set; } diff --git a/test/yavscTests/Mandatory/Remoting.cs b/test/yavscTests/Mandatory/Remoting.cs index b22c9a17..9d3aae6b 100644 --- a/test/yavscTests/Mandatory/Remoting.cs +++ b/test/yavscTests/Mandatory/Remoting.cs @@ -1,9 +1,8 @@ -using isnd.tests; +using System.Security.Cryptography.X509Certificates; +using System.Net.Security; +using isnd.tests; using Xunit.Abstractions; using IdentityModel.Client; -using System.Net; -using System.Security.Cryptography.X509Certificates; -using System.Net.Security; namespace yavscTests { @@ -20,64 +19,55 @@ namespace yavscTests [Fact] public async Task ObtainServiceToken() - { - var serverUrl = _serverFixture.Addresses.FirstOrDefault( - u => u.StartsWith("https:") - ); + { + 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) throw new Exception(disco.Error); - String authority = _serverFixture.SiteSettings.Authority; - HttpClient client = NewHttpClient(); - var disco = await client.GetDiscoveryDocumentAsync(authority); - if (disco.IsError) throw new Exception(disco.Error); - - var response = await client.RequestClientCredentialsTokenAsync(new ClientCredentialsTokenRequest - { - Address = disco.TokenEndpoint, - ClientId = _serverFixture.TestClientId, - ClientSecret = _serverFixture.TestClientSecret, - Scope = "test", - GrantType = "client_credentials" - }); - /*"mvc"; - options.ClientSecret = "49C1A7E1-0C79-4A89-A3D6-A37998FB86B0";*/ - if (response.IsError) throw new Exception(response.Error); - - } + 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] + [Fact] public async Task ObtainResourceOwnerPasswordToken() { - var serverUrl = _serverFixture.Addresses.FirstOrDefault( - u => u.StartsWith("https:") - ); - - String authority = _serverFixture.SiteSettings.Authority; + 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(authority); + 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 = + { + 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); diff --git a/test/yavscTests/WebServerFixture.cs b/test/yavscTests/WebServerFixture.cs index 2e2cf674..e07c350e 100644 --- a/test/yavscTests/WebServerFixture.cs +++ b/test/yavscTests/WebServerFixture.cs @@ -1,20 +1,24 @@ +using IdentityServer8.EntityFramework.Entities; +using IdentityServer8.Models; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting.Server; using Microsoft.AspNetCore.Hosting.Server.Features; using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Server.Kestrel.Core; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using Yavsc; -using Yavsc.Models; -using Yavsc.Extensions; -using Microsoft.EntityFrameworkCore; using Serilog; using Serilog.Events; using Serilog.Sinks.SystemConsole.Themes; -using IdentityServer8.EntityFramework.Entities; -using IdentityServer8.Models; +using System.Net; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; +using Yavsc; +using Yavsc.Extensions; +using Yavsc.Models; using Client = IdentityServer8.EntityFramework.Entities.Client; namespace isnd.tests @@ -88,6 +92,7 @@ namespace isnd.tests ["Smtp:SenderName"] = "Yavsc Test", ["Smtp:SenderEmail"] = "test@example.com" }); + Configuration = builder.Configuration; this.app = builder.ConfigureWebAppServices(); diff --git a/test/yavscTests/appsettings.json b/test/yavscTests/appsettings.json index becfb6ea..28b36a57 100644 --- a/test/yavscTests/appsettings.json +++ b/test/yavscTests/appsettings.json @@ -1,6 +1,6 @@ { "Site": { - "Authority": "localhost", + "Authority": "https://localhost", "Title": "Yavsc dev", "Slogan": "Yavsc : WIP.", "Banner": "/images/yavsc.png", From b1a7a47f699789a59355f0ba29e1807df75c3d78 Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Sun, 19 Apr 2026 16:18:37 +0100 Subject: [PATCH 4/6] generate a cert for tests --- test/yavscTests/WebServerFixture.cs | 80 +++++++++++++++++++++++++---- 1 file changed, 70 insertions(+), 10 deletions(-) diff --git a/test/yavscTests/WebServerFixture.cs b/test/yavscTests/WebServerFixture.cs index e07c350e..ab5b8446 100644 --- a/test/yavscTests/WebServerFixture.cs +++ b/test/yavscTests/WebServerFixture.cs @@ -1,6 +1,7 @@ using IdentityServer8.EntityFramework.Entities; using IdentityServer8.Models; using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting.Server; using Microsoft.AspNetCore.Hosting.Server.Features; using Microsoft.AspNetCore.Identity; @@ -27,6 +28,11 @@ namespace isnd.tests [CollectionDefinition("Web server collection")] public class WebServerFixture : IDisposable { + private static readonly Lazy _selfSignedCertificate = new Lazy(CreateSelfSignedCertificate); + private static WebApplication? _app; + private static bool _isInitialized = false; + private static int _instanceCount = 0; + public List Addresses { get; private set; } = new List(); public Microsoft.Extensions.Logging.ILogger? Logger { get; internal set; } @@ -49,13 +55,42 @@ namespace isnd.tests public WebServerFixture() { - SetupHost().Wait(); + lock (this) + { + _instanceCount++; + if (!_isInitialized) + { + SetupHost().Wait(); + _isInitialized = true; + } + else + { + // Get addresses from existing app + var server = _app!.Services.GetRequiredService(); + var addressFeatures = server.Features.Get(); + if (addressFeatures?.Addresses != null) + { + foreach (var address in addressFeatures.Addresses) + { + Addresses.Add(address); + } + } + } + } } public void Dispose() { - if (app != null) - app.StopAsync().Wait(); + lock (this) + { + _instanceCount--; + if (_instanceCount == 0 && _app != null) + { + _app.StopAsync().Wait(); + _app = null; + _isInitialized = false; + } + } } void ConfigureLogger() => Log.Logger = new LoggerConfiguration() .MinimumLevel.Debug() @@ -93,13 +128,22 @@ namespace isnd.tests ["Smtp:SenderEmail"] = "test@example.com" }); + // Configure Kestrel for HTTPS with self-signed certificate + builder.WebHost.ConfigureKestrel(options => + { + options.Listen(IPAddress.Loopback, 5001, listenOptions => + { + listenOptions.UseHttps(_selfSignedCertificate.Value); + }); + }); + Configuration = builder.Configuration; - this.app = builder.ConfigureWebAppServices(); - Services = app.Services; - SiteSettings = app.Services.GetRequiredService>().Value; + _app = builder.ConfigureWebAppServices(); + Services = _app.Services; + SiteSettings = _app.Services.GetRequiredService>().Value; - using (var migrationScope = app.Services.CreateScope()) + using (var migrationScope = _app.Services.CreateScope()) { var db = migrationScope.ServiceProvider.GetRequiredService(); db.Database.EnsureDeleted(); @@ -112,9 +156,9 @@ namespace isnd.tests AddAuthorizedClient(TestClientId, TestClientSecret); TestingUser = await db.Users.FirstOrDefaultAsync(u => u.UserName == TestingUserName); } - await app!.ConfigurePipeline(); - app.UseSession(); - await app.StartAsync(); + await _app!.ConfigurePipeline(); + _app.UseSession(); + await _app.StartAsync(); @@ -225,5 +269,21 @@ namespace isnd.tests TestingUser = dbContext.Users.FirstOrDefault(u => u.UserName == testingUserName); } } + + 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; + } } } From 5b6caa72b58f7ac9ed9fa47fb0e4b2ab5e40df70 Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Sun, 19 Apr 2026 17:15:10 +0100 Subject: [PATCH 5/6] a web server fixture shared state --- test/yavscTests/Mandatory/Remoting.cs | 44 +++--- test/yavscTests/WebServerFixture.cs | 205 +++++++++++++++----------- 2 files changed, 139 insertions(+), 110 deletions(-) diff --git a/test/yavscTests/Mandatory/Remoting.cs b/test/yavscTests/Mandatory/Remoting.cs index 9d3aae6b..3b53eb50 100644 --- a/test/yavscTests/Mandatory/Remoting.cs +++ b/test/yavscTests/Mandatory/Remoting.cs @@ -23,7 +23,7 @@ namespace yavscTests 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) throw new Exception(disco.Error); @@ -39,10 +39,10 @@ namespace yavscTests if (response.IsError) throw new Exception(response.Error); } - private static HttpClient NewHttpClient() - { - return new HttpClient(new BypassSslValidationHandler()); - } + private static HttpClient NewHttpClient() + { + return new HttpClient(new BypassSslValidationHandler()); + } [Fact] public async Task ObtainResourceOwnerPasswordToken() @@ -50,7 +50,7 @@ namespace yavscTests 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); @@ -80,22 +80,22 @@ namespace yavscTests } - internal class BypassSslValidationHandler : HttpClientHandler -{ - public BypassSslValidationHandler() + internal class BypassSslValidationHandler : HttpClientHandler { - // 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; + 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; + } } } -} diff --git a/test/yavscTests/WebServerFixture.cs b/test/yavscTests/WebServerFixture.cs index ab5b8446..da9c8076 100644 --- a/test/yavscTests/WebServerFixture.cs +++ b/test/yavscTests/WebServerFixture.cs @@ -29,9 +29,20 @@ namespace isnd.tests public class WebServerFixture : IDisposable { private static readonly Lazy _selfSignedCertificate = new Lazy(CreateSelfSignedCertificate); + private static readonly object _sync = new object(); private static WebApplication? _app; private static bool _isInitialized = false; private static int _instanceCount = 0; + private static readonly List _sharedAddresses = new List(); + 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 Addresses { get; private set; } = new List(); public Microsoft.Extensions.Logging.ILogger? Logger { get; internal set; } @@ -40,7 +51,6 @@ namespace isnd.tests public IConfiguration? Configuration { get; private set; } - private WebApplication? app; public string? TestClientId { get; private set; } public IServiceProvider? Services { get; private set; } @@ -52,10 +62,10 @@ namespace isnd.tests public bool DbCreated { get; internal set; } public SiteSettings? SiteSettings { get => siteSettings; set => siteSettings = value; } public string? TestClientSecret { get; set; } - + public string? TestingUserEmail { get; set; } public WebServerFixture() { - lock (this) + lock (_sync) { _instanceCount++; if (!_isInitialized) @@ -63,25 +73,14 @@ namespace isnd.tests SetupHost().Wait(); _isInitialized = true; } - else - { - // Get addresses from existing app - var server = _app!.Services.GetRequiredService(); - var addressFeatures = server.Features.Get(); - if (addressFeatures?.Addresses != null) - { - foreach (var address in addressFeatures.Addresses) - { - Addresses.Add(address); - } - } - } + + CopySharedState(); } } public void Dispose() { - lock (this) + lock (_sync) { _instanceCount--; if (_instanceCount == 0 && _app != null) @@ -89,9 +88,33 @@ namespace isnd.tests _app.StopAsync().Wait(); _app = null; _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(_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() .MinimumLevel.Debug() .MinimumLevel.Override("Microsoft", LogEventLevel.Warning) @@ -128,10 +151,10 @@ namespace isnd.tests ["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 => { - options.Listen(IPAddress.Loopback, 5001, listenOptions => + options.Listen(IPAddress.Loopback, 0, listenOptions => { listenOptions.UseHttps(_selfSignedCertificate.Value); }); @@ -142,7 +165,7 @@ namespace isnd.tests _app = builder.ConfigureWebAppServices(); Services = _app.Services; SiteSettings = _app.Services.GetRequiredService>().Value; - + using (var migrationScope = _app.Services.CreateScope()) { var db = migrationScope.ServiceProvider.GetRequiredService(); @@ -151,104 +174,110 @@ namespace isnd.tests TestingUserName = "Tester"; TestingUserPassword = "tesT456+*"; TestClientId = "testClientId"; + TestingUserEmail = "test@no-reply.com"; + TestingUser = null; TestClientSecret = Guid.CreateVersion7().ToString(); - EnsureUser(TestingUserName, TestingUserPassword); - AddAuthorizedClient(TestClientId, TestClientSecret); + EnsureUser(TestingUserName, TestingUserPassword, TestingUserEmail, migrationScope); + AddAuthorizedClient(migrationScope, TestClientId, TestClientSecret); TestingUser = await db.Users.FirstOrDefaultAsync(u => u.UserName == TestingUserName); } await _app!.ConfigurePipeline(); _app.UseSession(); await _app.StartAsync(); + _sharedServices = _app.Services; + _sharedConfiguration = Configuration; + _sharedSiteSettings = SiteSettings; + _sharedTestClientId = TestClientId; + _sharedTestClientSecret = TestClientSecret; + _sharedTestingUserName = TestingUserName; + _sharedTestingUserPassword = TestingUserPassword; + _sharedTestingUserEmail = TestingUserEmail; + _sharedLogger = _app.Services.GetRequiredService().CreateLogger(); + Logger = _sharedLogger; - - var logFactory = app.Services.GetRequiredService(); - Logger = logFactory.CreateLogger(); - - var server = app.Services.GetRequiredService(); - + var server = _app.Services.GetRequiredService(); var addressFeatures = server.Features.Get(); if (addressFeatures?.Addresses != null) { + _sharedAddresses.Clear(); foreach (var address in addressFeatures.Addresses) { + _sharedAddresses.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(); + Client testingClient = new Client { - var db = scope.ServiceProvider.GetRequiredService(); - Client testingClient = new Client - { - ClientId = testClientId, - AccessTokenLifetime = 3600000, - AccessTokenType = 1, - BackChannelLogoutUri = SiteSettings!.Audience, - ClientName = "Testing client", - Enabled = true - }; - db.Clients.Add(testingClient); - db.SaveChanges(); - ClientSecret secret = new ClientSecret - { - Value = testClientSecret.Sha256(), - ClientId = testingClient.Id - }; - db.ClientSecrets.Add(secret); + ClientId = testClientId, + AccessTokenLifetime = 3600000, + AccessTokenType = 1, + BackChannelLogoutUri = SiteSettings!.Audience, + ClientName = "Testing client", + Enabled = true + }; + db.Clients.Add(testingClient); + db.SaveChanges(); + ClientSecret secret = new ClientSecret + { + Value = testClientSecret.Sha256(), + ClientId = testingClient.Id + }; + db.ClientSecrets.Add(secret); - var testOrigin = new ClientCorsOrigin - { - ClientId = testingClient.Id, - Origin = SiteSettings!.Audience + var testOrigin = new ClientCorsOrigin + { + ClientId = testingClient.Id, + Origin = SiteSettings!.Audience - }; - db.ClientCorsOrigins.Add(testOrigin); - db.ClientGrantTypes.Add(new ClientGrantType - { - ClientId = testingClient.Id, - GrantType = "client_credentials" - }); - db.ClientGrantTypes.Add(new ClientGrantType - { - ClientId = testingClient.Id, - GrantType = "password" - }); - db.ClientGrantTypes.Add(new ClientGrantType - { - ClientId = testingClient.Id, - GrantType = "code" - }); - db.ClientScopes.Add(new ClientScope - { - ClientId = testingClient.Id, - Scope = "test" - }); - db.ApiScopes.Add(new IdentityServer8.EntityFramework.Entities.ApiScope - { - Name = "test", - Enabled = true - }); - db.ClientRedirectUris.Add(new ClientRedirectUri - { - ClientId = testingClient.Id, - RedirectUri = SiteSettings!.Audience + }; + db.ClientCorsOrigins.Add(testOrigin); + db.ClientGrantTypes.Add(new ClientGrantType + { + ClientId = testingClient.Id, + GrantType = "client_credentials" + }); + db.ClientGrantTypes.Add(new ClientGrantType + { + ClientId = testingClient.Id, + GrantType = "password" + }); + db.ClientGrantTypes.Add(new ClientGrantType + { + ClientId = testingClient.Id, + GrantType = "code" + }); + db.ClientScopes.Add(new ClientScope + { + ClientId = testingClient.Id, + Scope = "test" + }); + db.ApiScopes.Add(new IdentityServer8.EntityFramework.Entities.ApiScope + { + Name = "test", + Enabled = true + }); + db.ClientRedirectUris.Add(new ClientRedirectUri + { + ClientId = testingClient.Id, + 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) { - using IServiceScope scope = app!.Services.CreateScope(); var userManager = scope.ServiceProvider.GetRequiredService>(); From e1ad89915845512b97b0899f854d2c8becba1d19 Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Sun, 19 Apr 2026 17:23:18 +0100 Subject: [PATCH 6/6] Modernization --- Directory.Packages.props | 56 ++++++++++++++-------------- src/Api/Api.csproj | 2 +- src/Yavsc.Blogs/Yavsc.Blogs.csproj | 2 +- src/Yavsc.Org/Yavsc.Org.csproj | 2 +- src/Yavsc.Server/Yavsc.Server.csproj | 2 +- src/Yavsc.Web/Yavsc.Web.csproj | 2 +- src/cli/cli.csproj | 2 +- test/yavscTests/yavscTests.csproj | 2 +- 8 files changed, 35 insertions(+), 35 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index ea5905fd..3b2164c8 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -5,10 +5,10 @@ - - - - + + + + @@ -17,47 +17,47 @@ - - + + - + - + - - + + - - + + - - - - - - - + + + + + + + - - + + - - - - - - + + + + + + - + \ No newline at end of file diff --git a/src/Api/Api.csproj b/src/Api/Api.csproj index 4edbeb59..e8d8bf71 100644 --- a/src/Api/Api.csproj +++ b/src/Api/Api.csproj @@ -1,6 +1,6 @@ - net9.0 + net10.0 enable 1c73094f-959f-4211-b1a1-6a69b236c283 Yavsc.Api diff --git a/src/Yavsc.Blogs/Yavsc.Blogs.csproj b/src/Yavsc.Blogs/Yavsc.Blogs.csproj index 4edbeb59..e8d8bf71 100644 --- a/src/Yavsc.Blogs/Yavsc.Blogs.csproj +++ b/src/Yavsc.Blogs/Yavsc.Blogs.csproj @@ -1,6 +1,6 @@ - net9.0 + net10.0 enable 1c73094f-959f-4211-b1a1-6a69b236c283 Yavsc.Api diff --git a/src/Yavsc.Org/Yavsc.Org.csproj b/src/Yavsc.Org/Yavsc.Org.csproj index b388fc76..790838ec 100644 --- a/src/Yavsc.Org/Yavsc.Org.csproj +++ b/src/Yavsc.Org/Yavsc.Org.csproj @@ -1,6 +1,6 @@ - net9.0 + net10.0 enable WTFPL 76e56fc2-1619-40d8-8393-365258b7a21d diff --git a/src/Yavsc.Server/Yavsc.Server.csproj b/src/Yavsc.Server/Yavsc.Server.csproj index b38a36a4..68524f68 100644 --- a/src/Yavsc.Server/Yavsc.Server.csproj +++ b/src/Yavsc.Server/Yavsc.Server.csproj @@ -1,6 +1,6 @@ - net9.0 + net10.0 enable 53bd70e8-ff81-497a-847f-a15fd8ea7a09 Yavsc.Server diff --git a/src/Yavsc.Web/Yavsc.Web.csproj b/src/Yavsc.Web/Yavsc.Web.csproj index 19035032..86f61a85 100644 --- a/src/Yavsc.Web/Yavsc.Web.csproj +++ b/src/Yavsc.Web/Yavsc.Web.csproj @@ -1,6 +1,6 @@ - net9.0 + net10.0 enable enable diff --git a/src/cli/cli.csproj b/src/cli/cli.csproj index 71f0e93e..c35b5e1e 100644 --- a/src/cli/cli.csproj +++ b/src/cli/cli.csproj @@ -1,7 +1,7 @@ Exe - net9.0 + net10.0 enable Yavsc.cli diff --git a/test/yavscTests/yavscTests.csproj b/test/yavscTests/yavscTests.csproj index 0845605c..df1ff580 100644 --- a/test/yavscTests/yavscTests.csproj +++ b/test/yavscTests/yavscTests.csproj @@ -1,6 +1,6 @@ - net9.0 + net10.0 enable enable false