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
+
+