using System; using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using NUnit.Framework; using Umbraco.Core.Cache; using Umbraco.Core.Composing; using Umbraco.Core.Composing.LightInject; using Umbraco.Core.Configuration; using Umbraco.Core.IO; using Umbraco.Core.Logging; using Umbraco.Core.Persistence; using Umbraco.Core.Persistence.Mappers; using Umbraco.Core.Scoping; using Umbraco.Core.Strings; using Umbraco.Tests.Common.Builders; using Umbraco.Tests.Integration.Extensions; using Umbraco.Tests.Integration.Implementations; using Umbraco.Extensions; using Umbraco.Tests.Testing; using Umbraco.Web; using Umbraco.Core.Runtime; using Umbraco.Core; using Moq; using System.Collections.Generic; using Microsoft.Extensions.Configuration; using System.Data.SqlClient; using System.Data.Common; using System.IO; using Umbraco.Core.Configuration.Models; using Microsoft.Extensions.Options; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Serilog; using Umbraco.Core.Logging.Serilog; using ConnectionStrings = Umbraco.Core.Configuration.Models.ConnectionStrings; using ILogger = Microsoft.Extensions.Logging.ILogger; 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 { public static LightInjectContainer CreateUmbracoContainer(out UmbracoServiceProviderFactory serviceProviderFactory) { var container = UmbracoServiceProviderFactory.CreateServiceContainer(); serviceProviderFactory = new UmbracoServiceProviderFactory(container, false); var umbracoContainer = serviceProviderFactory.GetContainer(); return umbracoContainer; } private List _testTeardown = null; private 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 (var a in _fixtureTeardown) a(); } [TearDown] public virtual void TearDown() { foreach (var a in _testTeardown) a(); _testTeardown = null; FirstTestInFixture = false; FirstTestInSession = false; } [SetUp] public virtual void Setup() { var hostBuilder = CreateHostBuilder(); var host = hostBuilder.StartAsync().GetAwaiter().GetResult(); Services = host.Services; var app = new ApplicationBuilder(host.Services); Configure(app); OnFixtureTearDown(() => host.Dispose()); } #region Generic Host Builder and Runtime private ILoggerFactory CreateLoggerFactory() { ILoggerFactory factory; var testOptions = TestOptionAttributeBase.GetTestOptions(); switch (testOptions.Logger) { case UmbracoTestOptions.Logger.Mock: factory = NullLoggerFactory.Instance; break; case UmbracoTestOptions.Logger.Serilog: factory = Microsoft.Extensions.Logging.LoggerFactory.Create(builder => { builder.AddSerilog(); }); break; case UmbracoTestOptions.Logger.Console: factory = Microsoft.Extensions.Logging.LoggerFactory.Create(builder => { builder.AddConsole(); }); break; default: throw new NotSupportedException($"Logger option {testOptions.Logger} is not supported."); } return factory; } /// /// Create the Generic Host and execute startup ConfigureServices/Configure calls /// /// public virtual IHostBuilder CreateHostBuilder() { UmbracoContainer = CreateUmbracoContainer(out var serviceProviderFactory); _serviceProviderFactory = serviceProviderFactory; 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(_serviceProviderFactory) .ConfigureAppConfiguration((context, configBuilder) => { context.HostingEnvironment = TestHelper.GetWebHostEnvironment(); Configuration = context.Configuration; configBuilder.AddInMemoryCollection(InMemoryConfiguration); }) .ConfigureServices((hostContext, services) => { services.AddTransient(_ => CreateLoggerFactory()); ConfigureServices(services); }); return hostBuilder; } /// /// Creates a instance for testing and registers an event handler for database install /// /// /// /// /// /// /// /// /// /// /// /// /// /// public CoreRuntime CreateTestRuntime( GlobalSettings globalSettings, ConnectionStrings connectionStrings, IUmbracoVersion umbracoVersion, IIOHelper ioHelper, ILoggerFactory loggerFactory, IProfiler profiler, Core.Hosting.IHostingEnvironment hostingEnvironment, IBackOfficeInfo backOfficeInfo, ITypeFinder typeFinder, AppCaches appCaches, IDbProviderFactoryCreator dbProviderFactoryCreator) { var runtime = CreateTestRuntime( globalSettings, connectionStrings, umbracoVersion, ioHelper, loggerFactory, profiler, hostingEnvironment, backOfficeInfo, typeFinder, appCaches, dbProviderFactoryCreator, TestHelper.MainDom, // SimpleMainDom UseTestLocalDb // DB Installation event handler ); return runtime; } /// /// Creates a instance for testing and registers an event handler for database install /// /// /// /// /// /// /// /// /// /// /// /// /// /// The event handler used for DB installation /// /// public static CoreRuntime CreateTestRuntime( GlobalSettings globalSettings, ConnectionStrings connectionStrings, IUmbracoVersion umbracoVersion, IIOHelper ioHelper, ILoggerFactory loggerFactory, IProfiler profiler, Core.Hosting.IHostingEnvironment hostingEnvironment, IBackOfficeInfo backOfficeInfo, ITypeFinder typeFinder, AppCaches appCaches, IDbProviderFactoryCreator dbProviderFactoryCreator, IMainDom mainDom, Action eventHandler) { var runtime = new CoreRuntime( globalSettings, connectionStrings, umbracoVersion, ioHelper, loggerFactory, profiler, Mock.Of(), hostingEnvironment, backOfficeInfo, dbProviderFactoryCreator, mainDom, typeFinder, appCaches); runtime.RuntimeEssentials += (sender, args) => eventHandler(sender, args); return runtime; } #endregion #region IStartup public virtual void ConfigureServices(IServiceCollection services) { services.AddSingleton(TestHelper.DbProviderFactoryCreator); var webHostEnvironment = TestHelper.GetWebHostEnvironment(); services.AddRequiredNetCoreServices(TestHelper, webHostEnvironment); // Add it! services.AddUmbracoConfiguration(Configuration); services.AddUmbracoCore( webHostEnvironment, UmbracoContainer, GetType().Assembly, AppCaches.NoCache, // Disable caches for integration tests TestHelper.GetLoggingConfiguration(), Configuration, CreateTestRuntime, out _); services.AddSignalR(); services.AddUmbracoWebComponents(); services.AddUmbracoRuntimeMinifier(Configuration); services.AddUmbracoBackOffice(); services.AddUmbracoBackOfficeIdentity(); services.AddMvc(); CustomTestSetup(services); } public virtual void Configure(IApplicationBuilder app) { Services.GetRequiredService().EnsureBackofficeSecurity(); Services.GetRequiredService().EnsureUmbracoContext(); // get the currently set options var testOptions = TestOptionAttributeBase.GetTestOptions(); if (testOptions.Boot) { app.UseUmbracoCore(); } } #endregion #region LocalDb private static readonly object _dbLocker = new object(); private static LocalDbTestDatabase _dbInstance; /// /// Event handler for the to install the database and register the to Terminate /// /// /// protected void UseTestLocalDb(CoreRuntime runtime, RuntimeEssentialsEventArgs args) { // MUST be terminated on teardown OnTestTearDown(() => runtime.Terminate()); // This will create a db, install the schema and ensure the app is configured to run InstallTestLocalDb(args.DatabaseFactory, runtime.RuntimeLoggerFactory, runtime.State, TestHelper.WorkingDirectory, out var connectionString); TestDBConnectionString = connectionString; InMemoryConfiguration["ConnectionStrings:" + Constants.System.UmbracoConnectionName] = TestDBConnectionString; } /// /// Get or create an instance of /// /// /// /// /// /// /// /// /// There must only be ONE instance shared between all tests in a session /// private static LocalDbTestDatabase GetOrCreateDatabase(string filesPath, ILoggerFactory loggerFactory, IUmbracoDatabaseFactory dbFactory) { lock (_dbLocker) { if (_dbInstance != null) return _dbInstance; var localDb = new LocalDb(); if (localDb.IsAvailable == false) throw new InvalidOperationException("LocalDB is not available."); _dbInstance = new LocalDbTestDatabase(loggerFactory, localDb, filesPath, dbFactory); return _dbInstance; } } /// /// Creates a LocalDb instance to use for the test /// /// /// /// /// /// /// /// private void InstallTestLocalDb( IUmbracoDatabaseFactory databaseFactory, ILoggerFactory loggerFactory, IRuntimeState runtimeState, string workingDirectory, out string connectionString) { connectionString = null; var dbFilePath = Path.Combine(workingDirectory, "LocalDb"); // get the currently set db options var testOptions = TestOptionAttributeBase.GetTestOptions(); 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 db = GetOrCreateDatabase(dbFilePath, loggerFactory, databaseFactory); switch (testOptions.Database) { case UmbracoTestOptions.Database.NewSchemaPerTest: // New DB + Schema var newSchemaDbId = db.AttachSchema(); // Add teardown callback OnTestTearDown(() => db.Detach(newSchemaDbId)); // We must re-configure our current factory since attaching a new LocalDb from the pool changes connection strings if (!databaseFactory.Configured) { databaseFactory.Configure(db.ConnectionString, Constants.DatabaseProviders.SqlServer); } // re-run the runtime level check runtimeState.DetermineRuntimeLevel(); Assert.AreEqual(RuntimeLevel.Run, runtimeState.Level); break; case UmbracoTestOptions.Database.NewEmptyPerTest: var newEmptyDbId = db.AttachEmpty(); // Add teardown callback OnTestTearDown(() => db.Detach(newEmptyDbId)); 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 var newSchemaFixtureDbId = db.AttachSchema(); // Add teardown callback OnFixtureTearDown(() => db.Detach(newSchemaFixtureDbId)); } // We must re-configure our current factory since attaching a new LocalDb from the pool changes connection strings if (!databaseFactory.Configured) { databaseFactory.Configure(db.ConnectionString, Constants.DatabaseProviders.SqlServer); } // re-run the runtime level check runtimeState.DetermineRuntimeLevel(); break; case UmbracoTestOptions.Database.NewEmptyPerFixture: throw new NotImplementedException(); //// Add teardown callback //integrationTest.OnFixtureTearDown(() => db.Detach()); break; default: throw new ArgumentOutOfRangeException(nameof(testOptions), testOptions, null); } connectionString = db.ConnectionString; } #endregion #region Common services protected LightInjectContainer UmbracoContainer { get; private set; } private UmbracoServiceProviderFactory _serviceProviderFactory; protected virtual T GetRequiredService() => Services.GetRequiredService(); public Dictionary InMemoryConfiguration { get; } = new Dictionary(); public IConfiguration Configuration { get; protected set; } public TestHelper TestHelper = new TestHelper(); protected string TestDBConnectionString { get; private set; } protected virtual Action CustomTestSetup => services => { }; /// /// Returns the DI container /// protected IServiceProvider Services { get; set; } /// /// Returns the /// protected IScopeProvider ScopeProvider => Services.GetRequiredService(); /// /// Returns the /// protected IScopeAccessor ScopeAccessor => Services.GetRequiredService(); /// /// Returns 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(); #endregion #region Builders protected UserBuilder UserBuilder = new UserBuilder(); protected UserGroupBuilder UserGroupBuilder = new UserGroupBuilder(); #endregion protected static bool FirstTestInSession = true; protected bool FirstTestInFixture = true; } }