Files
Umbraco-CMS/src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs

457 lines
17 KiB
C#
Raw Normal View History

using System;
using System.Collections.Generic;
using System.Data.Common;
using System.Data.SqlClient;
using System.IO;
using Microsoft.AspNetCore.Builder;
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.Core;
2020-03-30 21:53:30 +11:00
using Umbraco.Core.Cache;
using Umbraco.Core.Composing;
using Umbraco.Core.Configuration.Models;
using Umbraco.Core.DependencyInjection;
2020-03-30 21:53:30 +11:00
using Umbraco.Core.IO;
using Umbraco.Core.Persistence;
2020-03-30 21:53:30 +11:00
using Umbraco.Core.Persistence.Mappers;
using Umbraco.Core.Runtime;
using Umbraco.Core.Scoping;
2020-03-30 21:53:30 +11:00
using Umbraco.Core.Strings;
using Umbraco.Extensions;
2020-03-30 21:53:30 +11:00
using Umbraco.Tests.Common.Builders;
using Umbraco.Tests.Integration.Extensions;
using Umbraco.Tests.Integration.Implementations;
using Umbraco.Tests.Testing;
using Umbraco.Web;
namespace Umbraco.Tests.Integration.Testing
{
/// <summary>
/// Abstract class for integration tests
/// </summary>
/// <remarks>
/// This will use a Host Builder to boot and install Umbraco ready for use
/// </remarks>
[SingleThreaded]
[NonParallelizable]
public abstract class UmbracoIntegrationTest
{
private List<Action> _testTeardown = null;
private List<Action> _fixtureTeardown = new List<Action>();
public void OnTestTearDown(Action tearDown)
{
if (_testTeardown == null)
_testTeardown = new List<Action>();
_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()
{
if (_testTeardown != null)
{
foreach (var a in _testTeardown)
a();
}
_testTeardown = null;
FirstTestInFixture = false;
FirstTestInSession = false;
}
[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.InstallEmptyDatabase)] = "true";
var hostBuilder = CreateHostBuilder();
var host = hostBuilder.Start();
Services = host.Services;
var app = new ApplicationBuilder(host.Services);
Configure(app);
OnFixtureTearDown(() => host.Dispose());
}
#region Generic Host Builder and Runtime
private ILoggerFactory CreateLoggerFactory()
{
try
{
var testOptions = TestOptionAttributeBase.GetTestOptions<UmbracoTestAttribute>();
switch (testOptions.Logger)
{
case UmbracoTestOptions.Logger.Mock:
return NullLoggerFactory.Instance;
case UmbracoTestOptions.Logger.Serilog:
return Microsoft.Extensions.Logging.LoggerFactory.Create(builder => { builder.AddSerilog(); });
case UmbracoTestOptions.Logger.Console:
return Microsoft.Extensions.Logging.LoggerFactory.Create(builder => { builder.AddConsole(); });
}
}
catch
{
// ignored
}
return NullLoggerFactory.Instance;
}
/// <summary>
/// Create the Generic Host and execute startup ConfigureServices/Configure calls
/// </summary>
/// <returns></returns>
public virtual IHostBuilder CreateHostBuilder()
{
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()); })
Netcore: Alternate approach for MSDI refactor (#9247) * Doesn't make much sense to have Concrete on IRegister, only on IFactory * Handle FilesTreeController requires IFileSystem of type PhysicalFileSystem * Handle registration of default MediaFileSystem without using RegisterUniqueFor * Remove RegisterFor / RegisterUniqueFor from IRegister * Switch over from LightInject to wrappers around MSDI * Made mapper dependencies more explicit * Remove registration for AngularJsonMediaTypeFormatter It's dependencies aren't registered so container validation fails * Resolve lifetime issue for EnsureValidSessionId by service locating else resolve scoped in singleton * Make registration more explicit for backoffice UserManager * Make install step registrations more explicit * Disable service provider validation so site can launch Maybe this is a problem maybe not, we build about 8000 service providers so maybe everything is fine later... * Further cleanup of IFactory interface * Further cleanup of IRegister interface * Revert "Make registration more explicit for backoffice UserManager" This reverts commit 7215fe836103c597cd0873c66737a79b91ed4c49. * Resolve issue where NewInstallStep would fail to reset password for "SuperUser" Before MSDI, somehow BackOfficeIdentityOptions would be configured with token provider map from IdentityBuilder.AddDefaultTokenProviders. After switchover those config actions are lost. Subclass IdentityBuilder to ensure BackOfficeIdentityOptions doesn't miss config setup upstream. * Initialize current. * Add todo to turn container validation back on. * Migrated ScopeFileSystemsTests to integration tests Signed-off-by: Bjarke Berg <mail@bergmania.dk> * Resolve issue where MediaFileSystem was skipping ShadowFileSystem * Attempt to fix ScopeFileSystemsTests on azure devops Signed-off-by: Bjarke Berg <mail@bergmania.dk> * Be interesting to know what the actual full path is in pipeline. * Clarify intent of CreateMediaTest Doesn't help resolve weird UnauthorizedAccessException but it cuts so much cognitive overhead for the future. * Use ILoggerfactory rather than mock for the manually constructed file PhysicalFileSystem * Maybe resolve failing test on azure pipeline. Co-authored-by: Bjarke Berg <mail@bergmania.dk>
2020-10-26 10:47:14 +00:00
.UseUmbraco()
.ConfigureAppConfiguration((context, configBuilder) =>
{
context.HostingEnvironment = TestHelper.GetWebHostEnvironment();
configBuilder.Sources.Clear();
configBuilder.AddInMemoryCollection(InMemoryConfiguration);
Configuration = configBuilder.Build();
})
.ConfigureServices((hostContext, services) =>
{
services.AddTransient(_ => CreateLoggerFactory());
ConfigureServices(services);
});
return hostBuilder;
}
#endregion
#region IStartup
public virtual void ConfigureServices(IServiceCollection services)
{
services.AddSingleton(TestHelper.DbProviderFactoryCreator);
var webHostEnvironment = TestHelper.GetWebHostEnvironment();
services.AddRequiredNetCoreServices(TestHelper, webHostEnvironment);
// Add it!
2020-11-19 09:06:04 +00:00
2020-11-24 09:22:38 +00:00
var typeLoader = services.AddTypeLoader(
GetType().Assembly,
webHostEnvironment,
TestHelper.GetHostingEnvironment(),
TestHelper.ConsoleLoggerFactory,
AppCaches.NoCache,
Configuration,
TestHelper.Profiler);
var builder = new UmbracoBuilder(services, Configuration, typeLoader, TestHelper.ConsoleLoggerFactory);
2020-11-20 12:24:16 +00:00
builder.Services.AddLogger(TestHelper.GetHostingEnvironment(), TestHelper.GetLoggingConfiguration(), Configuration);
2020-11-20 12:24:16 +00:00
2020-11-19 09:06:04 +00:00
builder.AddConfiguration()
2020-11-20 12:24:16 +00:00
.AddUmbracoCore();
builder.Services.AddUnique<AppCaches>(GetAppCaches());
builder.Services.AddUnique<IUmbracoBootPermissionChecker>(Mock.Of<IUmbracoBootPermissionChecker>());
builder.Services.AddUnique<IMainDom>(TestHelper.MainDom);
2020-09-30 14:27:18 +02:00
services.AddSignalR();
2020-11-19 09:06:04 +00:00
builder.AddWebComponents();
builder.AddRuntimeMinifier();
builder.AddBackOffice();
builder.AddBackOfficeIdentity();
services.AddMvc();
2020-11-19 09:06:04 +00:00
builder.Build();
CustomTestSetup(services);
}
protected virtual AppCaches GetAppCaches()
{
// Disable caches for integration tests
return AppCaches.NoCache;
}
public virtual void Configure(IApplicationBuilder app)
{
UseTestLocalDb(app.ApplicationServices);
//get the currently set options
var testOptions = TestOptionAttributeBase.GetTestOptions<UmbracoTestAttribute>();
if (testOptions.Boot)
{
Services.GetRequiredService<IBackOfficeSecurityFactory>().EnsureBackOfficeSecurity();
Services.GetRequiredService<IUmbracoContextFactory>().EnsureUmbracoContext();
app.UseUmbracoCore(); // Takes 200 ms
OnTestTearDown(TerminateCoreRuntime);
}
}
/// <remarks>
/// Some IComponents hook onto static events (e.g. Published in ContentService)
/// If these fire after the components host has been shutdown, errors can occur.
/// If CoreRuntime.Start() is called We also need to de-register the events.
/// </remarks>
protected void TerminateCoreRuntime()
{
Services.GetRequiredService<IRuntime>().Terminate();
StaticApplicationLogging.Initialize(null);
}
#endregion
#region LocalDb
private static readonly object _dbLocker = new object();
private static ITestDatabase _dbInstance;
protected void UseTestLocalDb(IServiceProvider serviceProvider)
{
var state = serviceProvider.GetRequiredService<IRuntimeState>();
var databaseFactory = serviceProvider.GetRequiredService<IUmbracoDatabaseFactory>();
// This will create a db, install the schema and ensure the app is configured to run
InstallTestLocalDb(databaseFactory, serviceProvider.GetRequiredService<ILoggerFactory>(), state, TestHelper.WorkingDirectory);
TestDBConnectionString = databaseFactory.ConnectionString;
InMemoryConfiguration["ConnectionStrings:" + Constants.System.UmbracoConnectionName] = TestDBConnectionString;
}
/// <summary>
/// Get or create an instance of <see cref="LocalDbTestDatabase"/>
/// </summary>
/// <param name="filesPath"></param>
/// <param name="logger"></param>
2020-09-17 12:52:25 +02:00
/// <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>
private static ITestDatabase GetOrCreateDatabase(string filesPath, ILoggerFactory loggerFactory, IUmbracoDatabaseFactory dbFactory)
{
lock (_dbLocker)
{
if (_dbInstance != null)
return _dbInstance;
_dbInstance = TestDatabaseFactory.Create(filesPath, loggerFactory, dbFactory);
return _dbInstance;
}
}
/// <summary>
/// Creates a LocalDb instance to use for the test
/// </summary>
2020-09-17 12:52:25 +02:00
/// <param name="runtimeState"></param>
/// <param name="workingDirectory"></param>
/// <param name="connectionString"></param>
2020-09-17 12:52:25 +02:00
/// <param name="databaseFactory"></param>
/// <param name="loggerFactory"></param>
/// <param name="globalSettings"></param>
/// <returns></returns>
private void InstallTestLocalDb(
2020-09-18 12:53:06 +02:00
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)
return;
// need to manually register this factory
DbProviderFactories.RegisterFactory(Constants.DbProviderNames.SqlServer, SqlClientFactory.Instance);
if (!Directory.Exists(dbFilePath))
Directory.CreateDirectory(dbFilePath);
2020-09-18 12:53:06 +02:00
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));
// 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.Install, runtimeState.Level);
break;
case UmbracoTestOptions.Database.NewSchemaPerFixture:
2020-10-06 14:16:29 +02:00
// 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
2020-10-06 14:16:29 +02:00
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:
// 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 newEmptyFixtureDbId = db.AttachEmpty();
// Add teardown callback
OnFixtureTearDown(() => db.Detach(newEmptyFixtureDbId));
}
// 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);
}
break;
default:
throw new ArgumentOutOfRangeException(nameof(testOptions), testOptions, null);
}
}
#endregion
2020-07-01 17:42:39 +02:00
2020-03-30 21:53:30 +11:00
#region Common services
protected virtual T GetRequiredService<T>() => Services.GetRequiredService<T>();
public Dictionary<string, string> InMemoryConfiguration { get; } = new Dictionary<string, string>();
public IConfiguration Configuration { get; protected set; }
public TestHelper TestHelper = new TestHelper();
2020-10-21 13:54:22 +02:00
protected virtual string TestDBConnectionString { get; private set; }
protected virtual Action<IServiceCollection> CustomTestSetup => services => { };
/// <summary>
/// Returns the DI container
/// </summary>
protected IServiceProvider Services { get; set; }
/// <summary>
/// Returns the <see cref="IScopeProvider"/>
/// </summary>
protected IScopeProvider ScopeProvider => Services.GetRequiredService<IScopeProvider>();
/// <summary>
/// Returns the <see cref="IScopeAccessor"/>
/// </summary>
protected IScopeAccessor ScopeAccessor => Services.GetRequiredService<IScopeAccessor>();
/// <summary>
/// Returns the <see cref="ILoggerFactory"/>
/// </summary>
protected ILoggerFactory LoggerFactory => Services.GetRequiredService<ILoggerFactory>();
2020-03-30 21:53:30 +11:00
protected AppCaches AppCaches => Services.GetRequiredService<AppCaches>();
protected IIOHelper IOHelper => Services.GetRequiredService<IIOHelper>();
protected IShortStringHelper ShortStringHelper => Services.GetRequiredService<IShortStringHelper>();
protected GlobalSettings GlobalSettings => Services.GetRequiredService<IOptions<GlobalSettings>>().Value;
2020-03-30 21:53:30 +11:00
protected IMapperCollection Mappers => Services.GetRequiredService<IMapperCollection>();
#endregion
#region Builders
protected UserBuilder UserBuilderInstance = new UserBuilder();
protected UserGroupBuilder UserGroupBuilderInstance = new UserGroupBuilder();
2020-03-30 21:53:30 +11:00
#endregion
protected static bool FirstTestInSession = true;
protected bool FirstTestInFixture = true;
protected static int TestCount = 1;
}
}