Fixup CoreRuntime so it starts after Startup.Configure

Makes integration tests play nice with Components as RuntimeLevel will be correct
This commit is contained in:
Paul Johnson
2020-12-16 01:54:49 +00:00
parent 91e2f58822
commit 431403e372
9 changed files with 82 additions and 68 deletions

1
.gitignore vendored
View File

@@ -191,6 +191,7 @@ src/Umbraco.Web.UI/Umbraco/telemetrics-id.umb
/src/Umbraco.Web.UI.NetCore/App_Data/Smidge/Cache/*
/src/Umbraco.Web.UI.NetCore/umbraco/logs
src/Umbraco.Tests.Integration/umbraco/Data/
src/Umbraco.Tests.Integration/umbraco/logs/
src/Umbraco.Tests.Integration/Views/

View File

@@ -1,19 +1,29 @@
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace Umbraco.Core.Composing
{
/// <summary>
/// Extends the <see cref="IHostBuilder"/> to enable Umbraco to be used as the service container.
/// Extends the <see cref="IHostBuilder"/> to add CoreRuntime as a HostedService
/// </summary>
public static class HostBuilderExtensions
{
/// <summary>
/// Assigns a custom service provider factory to use Umbraco's container
/// Adds CoreRuntime as HostedService
/// </summary>
/// <param name="builder"></param>
/// <returns></returns>
/// <remarks>
/// <para>When running the site should be called before ConfigureWebDefaults.</para>
///
/// <para>
/// When testing should be called after ConfigureWebDefaults to ensure UseTestDatabase is called before CoreRuntime
/// starts or we initialize components with incorrect run level.
/// </para>
/// </remarks>
public static IHostBuilder UseUmbraco(this IHostBuilder builder)
{
_ = builder.ConfigureServices((context, services) =>
services.AddSingleton<IHostedService>(factory => factory.GetRequiredService<IRuntime>()));
return builder;
}
}

View File

@@ -135,16 +135,16 @@ namespace Umbraco.Web.PublishedCache.NuCache
_entitySerializer = entitySerializer;
_publishedModelFactory = publishedModelFactory;
// we always want to handle repository events, configured or not
// assuming no repository event will trigger before the whole db is ready
// (ideally we'd have Upgrading.App vs Upgrading.Data application states...)
InitializeRepositoryEvents();
_lifeTime.ApplicationInit += OnApplicationInit;
}
internal void OnApplicationInit(object sender, EventArgs e)
{
// we always want to handle repository events, configured or not
// assuming no repository event will trigger before the whole db is ready
// (ideally we'd have Upgrading.App vs Upgrading.Data application states...)
InitializeRepositoryEvents();
// however, the cache is NOT available until we are configured, because loading
// content (and content types) from database cannot be consistent (see notes in "Handle
// Notifications" region), so

View File

@@ -43,7 +43,6 @@ namespace Umbraco.Tests.Integration
var testHelper = new TestHelper();
var hostBuilder = new HostBuilder()
.UseUmbraco()
.ConfigureServices((hostContext, services) =>
{
var webHostEnvironment = testHelper.GetWebHostEnvironment();
@@ -61,14 +60,16 @@ namespace Umbraco.Tests.Integration
hostContext.Configuration,
testHelper.Profiler);
var builder = new UmbracoBuilder(services, hostContext.Configuration, typeLoader, testHelper.ConsoleLoggerFactory);
var builder = new UmbracoBuilder(services, hostContext.Configuration, typeLoader,
testHelper.ConsoleLoggerFactory);
builder.Services.AddUnique<AppCaches>(AppCaches.NoCache);
builder.AddConfiguration()
.AddUmbracoCore()
.Build();
.AddUmbracoCore()
.Build();
services.AddRouting(); // LinkGenerator
});
})
.UseUmbraco();
var host = await hostBuilder.StartAsync();
var app = new ApplicationBuilder(host.Services);

View File

@@ -1,8 +1,6 @@
using System;
using System.Linq.Expressions;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
@@ -11,7 +9,6 @@ using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using NUnit.Framework;
using Umbraco.Core;
using Umbraco.Extensions;
@@ -22,8 +19,7 @@ using Umbraco.Core.DependencyInjection;
using Umbraco.Web.Common.Controllers;
using Microsoft.Extensions.Hosting;
using Umbraco.Core.Cache;
using Umbraco.Core.Persistence;
using Umbraco.Core.Runtime;
using Umbraco.Core.Composing;
using Umbraco.Web.BackOffice.Controllers;
namespace Umbraco.Tests.Integration.TestServerTest
@@ -75,11 +71,14 @@ namespace Umbraco.Tests.Integration.TestServerTest
// call startup
builder.Configure(app =>
{
UseTestLocalDb(app.ApplicationServices);
UseTestDatabase(app.ApplicationServices);
Services = app.ApplicationServices;
Configure(app);
});
}).UseEnvironment(Environments.Development);
}).UseEnvironment(Environments.Development);
builder.UseUmbraco(); // Ensures CoreRuntime.StartAsync is called, must be after ConfigureWebHost
return builder;
}

View File

@@ -1,4 +1,5 @@
using System;
using System.IO;
using Microsoft.Extensions.Logging;
using Umbraco.Core.Persistence;
@@ -8,13 +9,20 @@ namespace Umbraco.Tests.Integration.Testing
{
public static ITestDatabase Create(string filesPath, ILoggerFactory loggerFactory, TestUmbracoDatabaseFactoryProvider dbFactory)
{
return string.IsNullOrEmpty(Environment.GetEnvironmentVariable("UmbracoIntegrationTestConnectionString"))
? CreateLocalDb(filesPath, loggerFactory, dbFactory.Create())
: CreateSqlDeveloper(loggerFactory, dbFactory.Create());
var connectionString = Environment.GetEnvironmentVariable("UmbracoIntegrationTestConnectionString");
return string.IsNullOrEmpty(connectionString)
? CreateLocalDb(filesPath, loggerFactory, dbFactory)
: CreateSqlDeveloper(loggerFactory, dbFactory, connectionString);
}
private static ITestDatabase CreateLocalDb(string filesPath, ILoggerFactory loggerFactory, IUmbracoDatabaseFactory dbFactory)
private static ITestDatabase CreateLocalDb(string filesPath, ILoggerFactory loggerFactory, TestUmbracoDatabaseFactoryProvider dbFactory)
{
if (!Directory.Exists(filesPath))
{
Directory.CreateDirectory(filesPath);
}
var localDb = new LocalDb();
if (!localDb.IsAvailable)
@@ -22,22 +30,21 @@ namespace Umbraco.Tests.Integration.Testing
throw new InvalidOperationException("LocalDB is not available.");
}
return new LocalDbTestDatabase(loggerFactory, localDb, filesPath, dbFactory);
return new LocalDbTestDatabase(loggerFactory, localDb, filesPath, dbFactory.Create());
}
private static ITestDatabase CreateSqlDeveloper(ILoggerFactory loggerFactory, IUmbracoDatabaseFactory dbFactory)
private static ITestDatabase CreateSqlDeveloper(ILoggerFactory loggerFactory, TestUmbracoDatabaseFactoryProvider dbFactory, string connectionString)
{
// NOTE: Example setup for Linux box.
// $ export SA_PASSWORD=Foobar123!
// $ export UmbracoIntegrationTestConnectionString="Server=localhost,1433;User Id=sa;Password=$SA_PASSWORD;"
// $ docker run -e 'ACCEPT_EULA=Y' -e "SA_PASSWORD=$SA_PASSWORD" -e 'MSSQL_PID=Developer' -p 1433:1433 -d mcr.microsoft.com/mssql/server:2017-latest-ubuntu
var connectionString = Environment.GetEnvironmentVariable("UmbracoIntegrationTestConnectionString");
if (string.IsNullOrEmpty(connectionString))
{
throw new InvalidOperationException("ENV: UmbracoIntegrationTestConnectionString is not set");
}
return new SqlDeveloperTestDatabase(loggerFactory, dbFactory, connectionString);
return new SqlDeveloperTestDatabase(loggerFactory, dbFactory.Create(), connectionString);
}
}
}

View File

@@ -97,10 +97,10 @@ namespace Umbraco.Tests.Integration.Testing
public virtual void Setup()
{
InMemoryConfiguration[Constants.Configuration.ConfigGlobal + ":" + nameof(GlobalSettings.InstallEmptyDatabase)] = "true";
var hostBuilder = CreateHostBuilder();
var hostBuilder = CreateHostBuilder()
.UseUmbraco(); // This ensures CoreRuntime.StartAsync will be called (however it's a mock if boot = false)
var host = hostBuilder.Start();
Services = host.Services;
var app = new ApplicationBuilder(host.Services);
@@ -113,8 +113,7 @@ namespace Umbraco.Tests.Integration.Testing
{
try
{
var testOptions = TestOptionAttributeBase.GetTestOptions<UmbracoTestAttribute>();
switch (testOptions.Logger)
switch (TestOptions.Logger)
{
case UmbracoTestOptions.Logger.Mock:
return NullLoggerFactory.Instance;
@@ -138,15 +137,13 @@ namespace Umbraco.Tests.Integration.Testing
/// <returns></returns>
public virtual IHostBuilder CreateHostBuilder()
{
var testOptions = TestOptionAttributeBase.GetTestOptions<UmbracoTestAttribute>();
var hostBuilder = Host.CreateDefaultBuilder()
// IMPORTANT: We Cannot use UseStartup, there's all sorts of threads about this with testing. Although this can work
// if you want to setup your tests this way, it is a bit annoying to do that as the WebApplicationFactory will
// create separate Host instances. So instead of UseStartup, we just call ConfigureServices/Configure ourselves,
// and in the case of the UmbracoTestServerTestBase it will use the ConfigureWebHost to Configure the IApplicationBuilder directly.
//.ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup(GetType()); })
.UseUmbraco()
.ConfigureAppConfiguration((context, configBuilder) =>
{
context.HostingEnvironment = TestHelper.GetWebHostEnvironment();
@@ -157,12 +154,13 @@ namespace Umbraco.Tests.Integration.Testing
})
.ConfigureServices((hostContext, services) =>
{
services.AddTransient(_ => CreateLoggerFactory());
ConfigureServices(services);
services.AddUnique(CreateLoggerFactory());
if (!testOptions.Boot)
if (!TestOptions.Boot)
{
// If boot is false, we don't want the CoreRuntime hosted service to start
// So we replace it with a Mock
services.AddUnique(Mock.Of<IRuntime>());
}
});
@@ -224,11 +222,15 @@ namespace Umbraco.Tests.Integration.Testing
public virtual void Configure(IApplicationBuilder app)
{
UseTestLocalDb(app.ApplicationServices);
UseTestDatabase(app.ApplicationServices);
Services.GetRequiredService<IBackOfficeSecurityFactory>().EnsureBackOfficeSecurity();
Services.GetRequiredService<IUmbracoContextFactory>().EnsureUmbracoContext();
app.UseUmbracoCore();
if (TestOptions.Boot)
{
Services.GetRequiredService<IBackOfficeSecurityFactory>().EnsureBackOfficeSecurity();
Services.GetRequiredService<IUmbracoContextFactory>().EnsureUmbracoContext();
}
app.UseUmbracoCore(); // This no longer starts CoreRuntime, it's very fast
}
#endregion
@@ -239,25 +241,20 @@ namespace Umbraco.Tests.Integration.Testing
private static ITestDatabase _dbInstance;
private static TestDbMeta _fixtureDbMeta;
protected void UseTestLocalDb(IServiceProvider serviceProvider)
protected void UseTestDatabase(IServiceProvider serviceProvider)
{
var state = serviceProvider.GetRequiredService<IRuntimeState>();
var testDatabaseFactoryProvider = serviceProvider.GetRequiredService<TestUmbracoDatabaseFactoryProvider>();
var databaseFactory = serviceProvider.GetRequiredService<IUmbracoDatabaseFactory>();
var loggerFactory = serviceProvider.GetRequiredService<ILoggerFactory>();
// This will create a db, install the schema and ensure the app is configured to run
InstallTestLocalDb(testDatabaseFactoryProvider, databaseFactory, serviceProvider.GetRequiredService<ILoggerFactory>(), state, TestHelper.WorkingDirectory);
SetupTestDatabase(testDatabaseFactoryProvider, databaseFactory, loggerFactory, state, TestHelper.WorkingDirectory);
}
/// <summary>
/// Get or create an instance of <see cref="LocalDbTestDatabase"/>
/// Get or create an instance of <see cref="ITestDatabase"/>
/// </summary>
/// <param name="filesPath"></param>
/// <param name="logger"></param>
/// <param name="loggerFactory"></param>
/// <param name="globalSettings"></param>
/// <param name="dbFactory"></param>
/// <returns></returns>
/// <remarks>
/// There must only be ONE instance shared between all tests in a session
/// </remarks>
@@ -266,9 +263,12 @@ namespace Umbraco.Tests.Integration.Testing
lock (_dbLocker)
{
if (_dbInstance != null)
{
return _dbInstance;
}
_dbInstance = TestDatabaseFactory.Create(filesPath, loggerFactory, dbFactory);
return _dbInstance;
}
}
@@ -276,30 +276,26 @@ namespace Umbraco.Tests.Integration.Testing
/// <summary>
/// Creates a LocalDb instance to use for the test
/// </summary>
private void InstallTestLocalDb(
private void SetupTestDatabase(
TestUmbracoDatabaseFactoryProvider testUmbracoDatabaseFactoryProvider,
IUmbracoDatabaseFactory databaseFactory,
ILoggerFactory loggerFactory,
IRuntimeState runtimeState,
string workingDirectory)
{
var dbFilePath = Path.Combine(workingDirectory, "LocalDb");
// get the currently set db options
var testOptions = TestOptionAttributeBase.GetTestOptions<UmbracoTestAttribute>();
if (testOptions.Database == UmbracoTestOptions.Database.None)
if (TestOptions.Database == UmbracoTestOptions.Database.None)
{
return;
}
// need to manually register this factory
DbProviderFactories.RegisterFactory(Constants.DbProviderNames.SqlServer, SqlClientFactory.Instance);
if (!Directory.Exists(dbFilePath))
Directory.CreateDirectory(dbFilePath);
var dbFilePath = Path.Combine(workingDirectory, "LocalDb");
var db = GetOrCreateDatabase(dbFilePath, loggerFactory, testUmbracoDatabaseFactoryProvider);
switch (testOptions.Database)
switch (TestOptions.Database)
{
case UmbracoTestOptions.Database.NewSchemaPerTest:
@@ -385,7 +381,7 @@ namespace Umbraco.Tests.Integration.Testing
break;
default:
throw new ArgumentOutOfRangeException(nameof(testOptions), testOptions, null);
throw new ArgumentOutOfRangeException(nameof(TestOptions), TestOptions, null);
}
}
@@ -393,6 +389,8 @@ namespace Umbraco.Tests.Integration.Testing
#region Common services
protected UmbracoTestAttribute TestOptions => TestOptionAttributeBase.GetTestOptions<UmbracoTestAttribute>();
protected virtual T GetRequiredService<T>() => Services.GetRequiredService<T>();
public Dictionary<string, string> InMemoryConfiguration { get; } = new Dictionary<string, string>();

View File

@@ -146,9 +146,7 @@ namespace Umbraco.Core.DependencyInjection
builder.Services.AddUnique<IRuntimeState, RuntimeState>();
builder.Services.AddUnique<IHostingEnvironment, AspNetCoreHostingEnvironment>();
builder.Services.AddUnique<IMainDom, MainDom>();
builder.Services.AddUnique<IRuntime, CoreRuntime>();
builder.Services.AddHostedService(factory => factory.GetRequiredService<IRuntime>());
builder.AddCoreInitialServices();
builder.AddComposers();

View File

@@ -20,7 +20,7 @@ namespace Umbraco.Web.UI.NetCore
{
x.ClearProviders();
})
.ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup<Startup>(); })
.UseUmbraco();
.UseUmbraco()
.ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup<Startup>(); });
}
}