diff --git a/.gitignore b/.gitignore index 022acb5db7..ea2ddfbb68 100644 --- a/.gitignore +++ b/.gitignore @@ -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/ diff --git a/src/Umbraco.Infrastructure/Composing/HostBuilderExtensions.cs b/src/Umbraco.Infrastructure/Composing/HostBuilderExtensions.cs index 7c17a8a338..e2be3569d6 100644 --- a/src/Umbraco.Infrastructure/Composing/HostBuilderExtensions.cs +++ b/src/Umbraco.Infrastructure/Composing/HostBuilderExtensions.cs @@ -1,19 +1,29 @@ -using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; namespace Umbraco.Core.Composing { /// - /// Extends the to enable Umbraco to be used as the service container. + /// Extends the to add CoreRuntime as a HostedService /// public static class HostBuilderExtensions { /// - /// Assigns a custom service provider factory to use Umbraco's container + /// Adds CoreRuntime as HostedService /// - /// - /// + /// + /// When running the site should be called before ConfigureWebDefaults. + /// + /// + /// When testing should be called after ConfigureWebDefaults to ensure UseTestDatabase is called before CoreRuntime + /// starts or we initialize components with incorrect run level. + /// + /// public static IHostBuilder UseUmbraco(this IHostBuilder builder) { + _ = builder.ConfigureServices((context, services) => + services.AddSingleton(factory => factory.GetRequiredService())); + return builder; } } diff --git a/src/Umbraco.PublishedCache.NuCache/PublishedSnapshotService.cs b/src/Umbraco.PublishedCache.NuCache/PublishedSnapshotService.cs index 97e3df16a6..82c2fa8dd1 100644 --- a/src/Umbraco.PublishedCache.NuCache/PublishedSnapshotService.cs +++ b/src/Umbraco.PublishedCache.NuCache/PublishedSnapshotService.cs @@ -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 diff --git a/src/Umbraco.Tests.Integration/RuntimeTests.cs b/src/Umbraco.Tests.Integration/RuntimeTests.cs index 1e3317262a..5d721ad176 100644 --- a/src/Umbraco.Tests.Integration/RuntimeTests.cs +++ b/src/Umbraco.Tests.Integration/RuntimeTests.cs @@ -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.NoCache); builder.AddConfiguration() - .AddUmbracoCore() - .Build(); + .AddUmbracoCore() + .Build(); services.AddRouting(); // LinkGenerator - }); + }) + .UseUmbraco(); var host = await hostBuilder.StartAsync(); var app = new ApplicationBuilder(host.Services); diff --git a/src/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs b/src/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs index c3c01ed977..87ac4505e5 100644 --- a/src/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs +++ b/src/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs @@ -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; } diff --git a/src/Umbraco.Tests.Integration/Testing/TestDatabaseFactory.cs b/src/Umbraco.Tests.Integration/Testing/TestDatabaseFactory.cs index 9bcbfa4d3a..60c767e17e 100644 --- a/src/Umbraco.Tests.Integration/Testing/TestDatabaseFactory.cs +++ b/src/Umbraco.Tests.Integration/Testing/TestDatabaseFactory.cs @@ -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); } } } diff --git a/src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs b/src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs index f72139576e..5fb3c50cc8 100644 --- a/src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs +++ b/src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs @@ -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(); - switch (testOptions.Logger) + switch (TestOptions.Logger) { case UmbracoTestOptions.Logger.Mock: return NullLoggerFactory.Instance; @@ -138,15 +137,13 @@ namespace Umbraco.Tests.Integration.Testing /// public virtual IHostBuilder CreateHostBuilder() { - - var testOptions = TestOptionAttributeBase.GetTestOptions(); 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()); } }); @@ -224,11 +222,15 @@ namespace Umbraco.Tests.Integration.Testing public virtual void Configure(IApplicationBuilder app) { - UseTestLocalDb(app.ApplicationServices); + UseTestDatabase(app.ApplicationServices); - Services.GetRequiredService().EnsureBackOfficeSecurity(); - Services.GetRequiredService().EnsureUmbracoContext(); - app.UseUmbracoCore(); + if (TestOptions.Boot) + { + Services.GetRequiredService().EnsureBackOfficeSecurity(); + Services.GetRequiredService().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(); var testDatabaseFactoryProvider = serviceProvider.GetRequiredService(); var databaseFactory = serviceProvider.GetRequiredService(); + var loggerFactory = serviceProvider.GetRequiredService(); // This will create a db, install the schema and ensure the app is configured to run - InstallTestLocalDb(testDatabaseFactoryProvider, databaseFactory, serviceProvider.GetRequiredService(), state, TestHelper.WorkingDirectory); + SetupTestDatabase(testDatabaseFactoryProvider, databaseFactory, loggerFactory, state, TestHelper.WorkingDirectory); } /// - /// Get or create an instance of + /// Get or create an instance of /// - /// - /// - /// - /// - /// - /// /// /// There must only be ONE instance shared between all tests in a session /// @@ -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 /// /// Creates a LocalDb instance to use for the test /// - 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(); - - 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(); + protected virtual T GetRequiredService() => Services.GetRequiredService(); public Dictionary InMemoryConfiguration { get; } = new Dictionary(); diff --git a/src/Umbraco.Web.Common/Builder/UmbracoBuilderExtensions.cs b/src/Umbraco.Web.Common/Builder/UmbracoBuilderExtensions.cs index 9ecda715ee..b8dccfbe33 100644 --- a/src/Umbraco.Web.Common/Builder/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Web.Common/Builder/UmbracoBuilderExtensions.cs @@ -146,9 +146,7 @@ namespace Umbraco.Core.DependencyInjection builder.Services.AddUnique(); builder.Services.AddUnique(); builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddHostedService(factory => factory.GetRequiredService()); builder.AddCoreInitialServices(); builder.AddComposers(); diff --git a/src/Umbraco.Web.UI.NetCore/Program.cs b/src/Umbraco.Web.UI.NetCore/Program.cs index 4a7722597d..95322cb1b0 100644 --- a/src/Umbraco.Web.UI.NetCore/Program.cs +++ b/src/Umbraco.Web.UI.NetCore/Program.cs @@ -20,7 +20,7 @@ namespace Umbraco.Web.UI.NetCore { x.ClearProviders(); }) - .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup(); }) - .UseUmbraco(); + .UseUmbraco() + .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup(); }); } }