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 ILogger = Umbraco.Core.Logging.ILogger;
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 ConnectionStrings = Umbraco.Core.Configuration.Models.ConnectionStrings;
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;
}
[SetUp]
public virtual async Task Setup()
{
var hostBuilder = CreateHostBuilder();
var host = await hostBuilder.StartAsync();
Services = host.Services;
var app = new ApplicationBuilder(host.Services);
Configure(app);
}
#region Generic Host Builder and Runtime
///
/// 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) =>
{
ConfigureServices(services);
});
return hostBuilder;
}
///
/// Creates a instance for testing and registers an event handler for database install
///
///
///
///
///
///
///
///
///
///
///
///
///
///
public CoreRuntime CreateTestRuntime(
Configs configs,
GlobalSettings globalSettings,
ConnectionStrings connectionStrings,
IUmbracoVersion umbracoVersion, IIOHelper ioHelper,
ILogger logger, IProfiler profiler, Core.Hosting.IHostingEnvironment hostingEnvironment, IBackOfficeInfo backOfficeInfo,
ITypeFinder typeFinder, AppCaches appCaches, IDbProviderFactoryCreator dbProviderFactoryCreator)
{
var runtime = CreateTestRuntime(configs,
globalSettings,
connectionStrings,
umbracoVersion,
ioHelper,
logger,
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(
Configs configs,
GlobalSettings globalSettings,
ConnectionStrings connectionStrings,
IUmbracoVersion umbracoVersion, IIOHelper ioHelper,
ILogger logger, IProfiler profiler, Core.Hosting.IHostingEnvironment hostingEnvironment, IBackOfficeInfo backOfficeInfo,
ITypeFinder typeFinder, AppCaches appCaches, IDbProviderFactoryCreator dbProviderFactoryCreator,
IMainDom mainDom, Action eventHandler)
{
var runtime = new CoreRuntime(
configs,
globalSettings,
connectionStrings,
umbracoVersion,
ioHelper,
logger,
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(),
CreateTestRuntime,
out _);
services.AddUmbracoWebComponents();
services.AddUmbracoRuntimeMinifier(Configuration);
services.AddUmbracoBackOffice();
services.AddUmbracoBackOfficeIdentity();
services.AddMvc();
services.AddSingleton(new ConsoleLogger(new MessageTemplates()));
CustomTestSetup(services);
}
public virtual void Configure(IApplicationBuilder app)
{
Services.GetRequiredService().EnsureUmbracoContext();
// get the currently set ptions
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.ProfilingLogger, 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, ILogger logger, 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(logger, localDb, filesPath, dbFactory);
return _dbInstance;
}
}
///
/// Creates a LocalDb instance to use for the test
///
///
///
///
///
///
private void InstallTestLocalDb(
IUmbracoDatabaseFactory databaseFactory, IProfilingLogger logger,
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, logger, 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:
// New DB + Schema
var newSchemaFixtureDbId = db.AttachSchema();
// Add teardown callback
OnFixtureTearDown(() => db.Detach(newSchemaFixtureDbId));
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 ILogger Logger => 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 GlobalSettingsBuilder GlobalSettingsBuilder = new GlobalSettingsBuilder();
protected UserBuilder UserBuilder = new UserBuilder();
protected UserGroupBuilder UserGroupBuilder = new UserGroupBuilder();
#endregion
}
}