// Copyright (c) Umbraco. // See LICENSE for more details. using System; using System.Collections.Generic; using System.Data.Common; using System.Data.SqlClient; using System.IO; using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using Moq; using NUnit.Framework; using Serilog; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Composing; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Strings; using Umbraco.Cms.Core.Web; using Umbraco.Cms.Tests.Common.Builders; using Umbraco.Cms.Tests.Common.Testing; using Umbraco.Core.Persistence; using Umbraco.Core.Persistence.Mappers; using Umbraco.Core.Scoping; using Umbraco.Extensions; using Umbraco.Infrastructure.DependencyInjection; using Umbraco.Tests.Integration.DependencyInjection; using Umbraco.Tests.Integration.Extensions; using Umbraco.Tests.Integration.Implementations; using Constants = Umbraco.Cms.Core.Constants; namespace Umbraco.Tests.Integration.Testing { /// /// Abstract class for integration tests /// /// /// This will use a Host Builder to boot and install Umbraco ready for use /// [SingleThreaded] [NonParallelizable] public abstract class UmbracoIntegrationTest { private List _testTeardown = null; private readonly List _fixtureTeardown = new List(); public void OnTestTearDown(Action tearDown) { if (_testTeardown == null) { _testTeardown = new List(); } _testTeardown.Add(tearDown); } public void OnFixtureTearDown(Action tearDown) => _fixtureTeardown.Add(tearDown); [OneTimeTearDown] public void FixtureTearDown() { foreach (Action a in _fixtureTeardown) { a(); } } [TearDown] public async Task TearDownAsync() { if (_testTeardown != null) { foreach (Action a in _testTeardown) { a(); } } _testTeardown = null; FirstTestInFixture = false; FirstTestInSession = false; // Ensure CoreRuntime stopped (now it's a HostedService) IHost host = Services.GetRequiredService(); await host.StopAsync(); host.Dispose(); } [TearDown] public virtual void TearDown_Logging() => TestContext.Progress.Write($" {TestContext.CurrentContext.Result.Outcome.Status}"); [SetUp] public virtual void SetUp_Logging() => TestContext.Progress.Write($"Start test {TestCount++}: {TestContext.CurrentContext.Test.Name}"); [SetUp] public virtual void Setup() { InMemoryConfiguration[Constants.Configuration.ConfigGlobal + ":" + nameof(GlobalSettings.InstallUnattended)] = "true"; IHostBuilder hostBuilder = CreateHostBuilder(); IHost host = hostBuilder.Build(); BeforeHostStart(host); host.Start(); var app = new ApplicationBuilder(host.Services); Configure(app); } protected virtual void BeforeHostStart(IHost host) { Services = host.Services; UseTestDatabase(Services); } private ILoggerFactory CreateLoggerFactory() { try { switch (TestOptions.Logger) { case UmbracoTestOptions.Logger.Mock: return NullLoggerFactory.Instance; case UmbracoTestOptions.Logger.Serilog: return Microsoft.Extensions.Logging.LoggerFactory.Create(builder => { string path = Path.Combine(TestHelper.WorkingDirectory, "logs", "umbraco_integration_tests_.txt"); Log.Logger = new LoggerConfiguration() .WriteTo.File(path, rollingInterval: RollingInterval.Day) .CreateLogger(); builder.AddSerilog(Log.Logger); }); case UmbracoTestOptions.Logger.Console: return Microsoft.Extensions.Logging.LoggerFactory.Create(builder => builder.AddConsole()); } } catch { // ignored } return NullLoggerFactory.Instance; } /// /// Create the Generic Host and execute startup ConfigureServices/Configure calls /// public virtual IHostBuilder CreateHostBuilder() { IHostBuilder 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()); }) .ConfigureAppConfiguration((context, configBuilder) => { context.HostingEnvironment = TestHelper.GetWebHostEnvironment(); configBuilder.Sources.Clear(); configBuilder.AddInMemoryCollection(InMemoryConfiguration); Configuration = configBuilder.Build(); }) .ConfigureServices((hostContext, services) => { ConfigureServices(services); services.AddUnique(CreateLoggerFactory()); 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()); } }); return hostBuilder; } public virtual void ConfigureServices(IServiceCollection services) { services.AddSingleton(TestHelper.DbProviderFactoryCreator); services.AddTransient(); IWebHostEnvironment webHostEnvironment = TestHelper.GetWebHostEnvironment(); services.AddRequiredNetCoreServices(TestHelper, webHostEnvironment); // Add it! TypeLoader typeLoader = services.AddTypeLoader( GetType().Assembly, webHostEnvironment, TestHelper.GetHostingEnvironment(), TestHelper.ConsoleLoggerFactory, AppCaches.NoCache, Configuration, TestHelper.Profiler); var builder = new UmbracoBuilder(services, Configuration, typeLoader, TestHelper.ConsoleLoggerFactory); builder.Services.AddLogger(TestHelper.GetHostingEnvironment(), TestHelper.GetLoggingConfiguration(), Configuration); builder.AddConfiguration() .AddUmbracoCore() .AddWebComponents() .AddRuntimeMinifier() .AddBackOfficeAuthentication() .AddBackOfficeIdentity() .AddTestServices(TestHelper, GetAppCaches()); if (TestOptions.Mapper) { // TODO: Should these just be called from within AddUmbracoCore/AddWebComponents? builder .AddCoreMappingProfiles() .AddWebMappingProfiles(); } services.AddSignalR(); services.AddMvc(); CustomTestSetup(builder); builder.Build(); } protected virtual AppCaches GetAppCaches() => // Disable caches for integration tests AppCaches.NoCache; public virtual void Configure(IApplicationBuilder app) { if (TestOptions.Boot) { Services.GetRequiredService().EnsureBackOfficeSecurity(); Services.GetRequiredService().EnsureUmbracoContext(); } app.UseUmbracoCore(); // This no longer starts CoreRuntime, it's very fast } private static readonly object s_dbLocker = new object(); private static ITestDatabase s_dbInstance; private static TestDbMeta s_fixtureDbMeta; protected void UseTestDatabase(IServiceProvider serviceProvider) { IRuntimeState state = serviceProvider.GetRequiredService(); TestUmbracoDatabaseFactoryProvider testDatabaseFactoryProvider = serviceProvider.GetRequiredService(); IUmbracoDatabaseFactory databaseFactory = serviceProvider.GetRequiredService(); ILoggerFactory loggerFactory = serviceProvider.GetRequiredService(); // This will create a db, install the schema and ensure the app is configured to run SetupTestDatabase(testDatabaseFactoryProvider, databaseFactory, loggerFactory, state, TestHelper.WorkingDirectory); } /// /// Get or create an instance of /// /// /// There must only be ONE instance shared between all tests in a session /// private static ITestDatabase GetOrCreateDatabase(string filesPath, ILoggerFactory loggerFactory, TestUmbracoDatabaseFactoryProvider dbFactory) { lock (s_dbLocker) { if (s_dbInstance != null) { return s_dbInstance; } // TODO: pull from IConfiguration var settings = new TestDatabaseSettings { PrepareThreadCount = 4, EmptyDatabasesCount = 2, SchemaDatabaseCount = 4 }; s_dbInstance = TestDatabaseFactory.Create(settings, filesPath, loggerFactory, dbFactory); return s_dbInstance; } } /// /// Creates a LocalDb instance to use for the test /// private void SetupTestDatabase( TestUmbracoDatabaseFactoryProvider testUmbracoDatabaseFactoryProvider, IUmbracoDatabaseFactory databaseFactory, ILoggerFactory loggerFactory, IRuntimeState runtimeState, string workingDirectory) { if (TestOptions.Database == UmbracoTestOptions.Database.None) { return; } // need to manually register this factory DbProviderFactories.RegisterFactory(Constants.DbProviderNames.SqlServer, SqlClientFactory.Instance); string dbFilePath = Path.Combine(workingDirectory, "LocalDb"); ITestDatabase db = GetOrCreateDatabase(dbFilePath, loggerFactory, testUmbracoDatabaseFactoryProvider); switch (TestOptions.Database) { case UmbracoTestOptions.Database.NewSchemaPerTest: // New DB + Schema TestDbMeta newSchemaDbMeta = db.AttachSchema(); // Add teardown callback OnTestTearDown(() => db.Detach(newSchemaDbMeta)); ConfigureTestDatabaseFactory(newSchemaDbMeta, databaseFactory, runtimeState); Assert.AreEqual(RuntimeLevel.Run, runtimeState.Level); break; case UmbracoTestOptions.Database.NewEmptyPerTest: TestDbMeta newEmptyDbMeta = db.AttachEmpty(); // Add teardown callback OnTestTearDown(() => db.Detach(newEmptyDbMeta)); ConfigureTestDatabaseFactory(newEmptyDbMeta, databaseFactory, runtimeState); Assert.AreEqual(RuntimeLevel.Install, runtimeState.Level); break; case UmbracoTestOptions.Database.NewSchemaPerFixture: // Only attach schema once per fixture // Doing it more than once will block the process since the old db hasn't been detached // and it would be the same as NewSchemaPerTest even if it didn't block if (FirstTestInFixture) { // New DB + Schema TestDbMeta newSchemaFixtureDbMeta = db.AttachSchema(); s_fixtureDbMeta = newSchemaFixtureDbMeta; // Add teardown callback OnFixtureTearDown(() => db.Detach(newSchemaFixtureDbMeta)); } ConfigureTestDatabaseFactory(s_fixtureDbMeta, databaseFactory, runtimeState); break; case UmbracoTestOptions.Database.NewEmptyPerFixture: // Only attach schema once per fixture // Doing it more than once will block the process since the old db hasn't been detached // and it would be the same as NewSchemaPerTest even if it didn't block if (FirstTestInFixture) { // New DB + Schema TestDbMeta newEmptyFixtureDbMeta = db.AttachEmpty(); s_fixtureDbMeta = newEmptyFixtureDbMeta; // Add teardown callback OnFixtureTearDown(() => db.Detach(newEmptyFixtureDbMeta)); } ConfigureTestDatabaseFactory(s_fixtureDbMeta, databaseFactory, runtimeState); break; default: throw new ArgumentOutOfRangeException(nameof(TestOptions), TestOptions, null); } } private void ConfigureTestDatabaseFactory(TestDbMeta meta, IUmbracoDatabaseFactory factory, IRuntimeState state) { ILogger log = Services.GetRequiredService>(); log.LogInformation($"ConfigureTestDatabaseFactory - Using test database: [{meta.Name}] - IsEmpty: [{meta.IsEmpty}]"); // It's just been pulled from container and wasn't used to create test database Assert.IsFalse(factory.Configured); factory.Configure(meta.ConnectionString, Constants.DatabaseProviders.SqlServer); state.DetermineRuntimeLevel(); log.LogInformation($"ConfigureTestDatabaseFactory - Determined RuntimeLevel: [{state.Level}]"); } protected UmbracoTestAttribute TestOptions => TestOptionAttributeBase.GetTestOptions(); protected virtual T GetRequiredService() => Services.GetRequiredService(); public Dictionary InMemoryConfiguration { get; } = new Dictionary(); public IConfiguration Configuration { get; protected set; } public TestHelper TestHelper { get; } = new TestHelper(); protected virtual void CustomTestSetup(IUmbracoBuilder builder) { } /// /// Gets or sets the DI container. /// protected IServiceProvider Services { get; set; } /// /// Gets the /// protected IScopeProvider ScopeProvider => Services.GetRequiredService(); /// /// Gets the /// protected IScopeAccessor ScopeAccessor => Services.GetRequiredService(); /// /// Gets the /// protected ILoggerFactory LoggerFactory => Services.GetRequiredService(); protected AppCaches AppCaches => Services.GetRequiredService(); protected IIOHelper IOHelper => Services.GetRequiredService(); protected IShortStringHelper ShortStringHelper => Services.GetRequiredService(); protected GlobalSettings GlobalSettings => Services.GetRequiredService>().Value; protected IMapperCollection Mappers => Services.GetRequiredService(); protected UserBuilder UserBuilderInstance = new UserBuilder(); protected UserGroupBuilder UserGroupBuilderInstance = new UserGroupBuilder(); protected static bool FirstTestInSession = true; protected bool FirstTestInFixture = true; protected static int TestCount = 1; } }