diff --git a/tests/Umbraco.Tests.Integration/DependencyInjection/UmbracoBuilderExtensions.cs b/tests/Umbraco.Tests.Integration/DependencyInjection/UmbracoBuilderExtensions.cs index 7bf435485f..b5c917a337 100644 --- a/tests/Umbraco.Tests.Integration/DependencyInjection/UmbracoBuilderExtensions.cs +++ b/tests/Umbraco.Tests.Integration/DependencyInjection/UmbracoBuilderExtensions.cs @@ -39,9 +39,9 @@ namespace Umbraco.Cms.Tests.Integration.DependencyInjection /// /// Uses/Replaces services with testing services /// - public static IUmbracoBuilder AddTestServices(this IUmbracoBuilder builder, TestHelper testHelper, AppCaches appCaches = null) + public static IUmbracoBuilder AddTestServices(this IUmbracoBuilder builder, TestHelper testHelper) { - builder.Services.AddUnique(appCaches ?? AppCaches.NoCache); + builder.Services.AddUnique(AppCaches.NoCache); builder.Services.AddUnique(Mock.Of()); builder.Services.AddUnique(testHelper.MainDom); diff --git a/tests/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs b/tests/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs index 45fb305de9..6d7835b889 100644 --- a/tests/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs +++ b/tests/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs @@ -1,6 +1,3 @@ -// Copyright (c) Umbraco. -// See LICENSE for more details. - using System; using System.Linq.Expressions; using System.Net.Http; @@ -17,7 +14,6 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Moq; using NUnit.Framework; -using Umbraco.Cms.Core; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Composing; using Umbraco.Cms.Core.DependencyInjection; @@ -35,14 +31,17 @@ namespace Umbraco.Cms.Tests.Integration.TestServerTest { [TestFixture] [UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest, Logger = UmbracoTestOptions.Logger.Console, Boot = true)] - public abstract class UmbracoTestServerTestBase : UmbracoIntegrationTest + public abstract class UmbracoTestServerTestBase : UmbracoIntegrationTestBase { - [SetUp] - public override void Setup() - { - InMemoryConfiguration["ConnectionStrings:" + Constants.System.UmbracoConnectionName] = null; - InMemoryConfiguration["Umbraco:CMS:Hosting:Debug"] = "true"; + protected HttpClient Client { get; private set; } + protected LinkGenerator LinkGenerator { get; private set; } + + protected WebApplicationFactory Factory { get; private set; } + + [SetUp] + public void Setup() + { /* * It's worth noting that our usage of WebApplicationFactory is non-standard, * the intent is that your Startup.ConfigureServices is called just like @@ -53,9 +52,12 @@ namespace Umbraco.Cms.Tests.Integration.TestServerTest * This is currently a pain to refactor towards due to UmbracoBuilder+TypeFinder+TypeLoader setup but * we should get there one day. * + * However we need to separate the testing framework we provide for downstream projects from our own tests. + * We cannot use the Umbraco.Web.UI startup yet as that is not available downstream. + * * See https://docs.microsoft.com/en-us/aspnet/core/test/integration-tests */ - var factory = new UmbracoWebApplicationFactory(CreateHostBuilder, BeforeHostStart); + var factory = new UmbracoWebApplicationFactory(CreateHostBuilder); // additional host configuration for web server integration tests Factory = factory.WithWebHostBuilder(builder => @@ -71,7 +73,6 @@ namespace Umbraco.Cms.Tests.Integration.TestServerTest .AddScheme(TestAuthHandler.TestAuthenticationScheme, options => { })); }); - Client = Factory.CreateClient(new WebApplicationFactoryClientOptions { AllowAutoRedirect = false @@ -80,57 +81,6 @@ namespace Umbraco.Cms.Tests.Integration.TestServerTest LinkGenerator = Factory.Services.GetRequiredService(); } - protected override IHostBuilder CreateHostBuilder() - { - /* It is important that ConfigureWebHost is called before ConfigureServices, this is consistent with the host setup - * found in Program.cs and avoids nasty surprises. - * - * e.g. the registration for RefreshingRazorViewEngine requires that IWebHostEnvironment is registered - * at the point in time that the service collection is snapshotted. - */ - IHostBuilder hostBuilder = Host.CreateDefaultBuilder() - .ConfigureAppConfiguration((context, configBuilder) => - { - context.HostingEnvironment = TestHelper.GetWebHostEnvironment(); - configBuilder.Sources.Clear(); - configBuilder.AddInMemoryCollection(InMemoryConfiguration); - - Configuration = configBuilder.Build(); - }) - .ConfigureWebHost(builder => - { - // need to configure the IWebHostEnvironment too - builder.ConfigureServices((c, s) => c.HostingEnvironment = TestHelper.GetWebHostEnvironment()); - - // call startup - builder.Configure(app => Configure(app)); - }) - .ConfigureServices((_, services) => - { - ConfigureServices(services); - ConfigureTestSpecificServices(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()); - } - }) - .UseDefaultServiceProvider(cfg => - { - // These default to true *if* WebHostEnvironment.EnvironmentName == Development - // When running tests, EnvironmentName used to be null on the mock that we register into services. - // Enable opt in for tests so that validation occurs regardless of environment name. - // Would be nice to have this on for UmbracoIntegrationTest also but requires a lot more effort to resolve issues. - cfg.ValidateOnBuild = true; - cfg.ValidateScopes = true; - }); - - return hostBuilder; - } - /// /// Prepare a url before using . /// This returns the url but also sets the HttpContext.request into to use this url. @@ -139,7 +89,7 @@ namespace Umbraco.Cms.Tests.Integration.TestServerTest protected string PrepareApiControllerUrl(Expression> methodSelector) where T : UmbracoApiController { - string url = LinkGenerator.GetUmbracoApiService(methodSelector); + var url = LinkGenerator.GetUmbracoApiService(methodSelector); return PrepareUrl(url); } @@ -151,7 +101,7 @@ namespace Umbraco.Cms.Tests.Integration.TestServerTest protected string PrepareSurfaceControllerUrl(Expression> methodSelector) where T : SurfaceController { - string url = LinkGenerator.GetUmbracoSurfaceUrl(methodSelector); + var url = LinkGenerator.GetUmbracoSurfaceUrl(methodSelector); return PrepareUrl(url); } @@ -181,17 +131,67 @@ namespace Umbraco.Cms.Tests.Integration.TestServerTest return url; } - protected HttpClient Client { get; private set; } + private IHostBuilder CreateHostBuilder() + { + IHostBuilder hostBuilder = Host.CreateDefaultBuilder() + .ConfigureAppConfiguration((context, configBuilder) => + { + context.HostingEnvironment = TestHelper.GetWebHostEnvironment(); + configBuilder.Sources.Clear(); + configBuilder.AddInMemoryCollection(InMemoryConfiguration); - protected LinkGenerator LinkGenerator { get; private set; } + Configuration = configBuilder.Build(); + }) + /* It is important that ConfigureWebHost is called before ConfigureServices, this is consistent with the host setup + * found in Program.cs and avoids nasty surprises. + * + * e.g. the registration for RefreshingRazorViewEngine requires that IWebHostEnvironment is registered + * at the point in time that the service collection is snapshotted. + */ + .ConfigureWebHost(builder => + { + // need to configure the IWebHostEnvironment too + builder.ConfigureServices((c, s) => c.HostingEnvironment = TestHelper.GetWebHostEnvironment()); - protected WebApplicationFactory Factory { get; private set; } + // call startup + builder.Configure(Configure); + }) + .ConfigureServices((_, services) => + { + ConfigureServices(services); + ConfigureTestSpecificServices(services); + + 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()); + } + }) + .UseDefaultServiceProvider(cfg => + { + // These default to true *if* WebHostEnvironment.EnvironmentName == Development + // When running tests, EnvironmentName used to be null on the mock that we register into services. + // Enable opt in for tests so that validation occurs regardless of environment name. + // Would be nice to have this on for UmbracoIntegrationTest also but requires a lot more effort to resolve issues. + cfg.ValidateOnBuild = true; + cfg.ValidateScopes = true; + }); + + return hostBuilder; + } + + protected virtual IServiceProvider Services => Factory.Services; + + protected virtual T GetRequiredService() => Factory.Services.GetRequiredService(); private void ConfigureServices(IServiceCollection services) { + services.AddUnique(CreateLoggerFactory()); services.AddTransient(); Core.Hosting.IHostingEnvironment hostingEnvironment = TestHelper.GetHostingEnvironment(); + TypeLoader typeLoader = services.AddTypeLoader( GetType().Assembly, hostingEnvironment, @@ -234,8 +234,10 @@ namespace Umbraco.Cms.Tests.Integration.TestServerTest .Build(); } - public override void Configure(IApplicationBuilder app) + private void Configure(IApplicationBuilder app) { + UseTestDatabase(app); + app.UseUmbraco() .WithMiddleware(u => { diff --git a/tests/Umbraco.Tests.Integration/TestServerTest/UmbracoWebApplicationFactory.cs b/tests/Umbraco.Tests.Integration/TestServerTest/UmbracoWebApplicationFactory.cs index 380603ae5c..62d84a19a1 100644 --- a/tests/Umbraco.Tests.Integration/TestServerTest/UmbracoWebApplicationFactory.cs +++ b/tests/Umbraco.Tests.Integration/TestServerTest/UmbracoWebApplicationFactory.cs @@ -18,11 +18,7 @@ namespace Umbraco.Cms.Tests.Integration.TestServerTest /// /// Method to create the IHostBuilder /// Method to perform an action before IHost starts - public UmbracoWebApplicationFactory(Func createHostBuilder, Action beforeStart = null) - { - _createHostBuilder = createHostBuilder; - _beforeStart = beforeStart; - } + public UmbracoWebApplicationFactory(Func createHostBuilder) => _createHostBuilder = createHostBuilder; protected override IHostBuilder CreateHostBuilder() => _createHostBuilder(); diff --git a/tests/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs b/tests/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs index 915d2d7629..1e9ae97474 100644 --- a/tests/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs +++ b/tests/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs @@ -1,23 +1,12 @@ -// Copyright (c) Umbraco. -// See LICENSE for more details. - using System; -using System.Collections.Generic; -using System.Data.Common; -using System.IO; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; -using Microsoft.Data.SqlClient; 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; @@ -29,14 +18,11 @@ using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Strings; using Umbraco.Cms.Core.Web; using Umbraco.Cms.Infrastructure.DependencyInjection; -using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Infrastructure.Persistence.Mappers; using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Cms.Tests.Common.Builders; -using Umbraco.Cms.Tests.Common.Testing; using Umbraco.Cms.Tests.Integration.DependencyInjection; using Umbraco.Cms.Tests.Integration.Extensions; -using Umbraco.Cms.Tests.Integration.Implementations; using Umbraco.Extensions; namespace Umbraco.Cms.Tests.Integration.Testing @@ -47,123 +33,35 @@ namespace Umbraco.Cms.Tests.Integration.Testing /// /// This will use a Host Builder to boot and install Umbraco ready for use /// - [SingleThreaded] - [NonParallelizable] - public abstract class UmbracoIntegrationTest + public abstract class UmbracoIntegrationTest : UmbracoIntegrationTestBase { - private List _testTeardown = null; - private readonly List _fixtureTeardown = new List(); + private IHost _host; - 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; - s_firstTestInSession = false; - - // Ensure CoreRuntime stopped (now it's a HostedService) - IHost host = Services?.GetService(); - if (host is not null) - { - await host.StopAsync(); - host.Dispose(); - } - - } - - [TearDown] - public virtual void TearDown_Logging() => - TestContext.Progress.Write($" {TestContext.CurrentContext.Result.Outcome.Status}"); + protected IServiceProvider Services => _host.Services; [SetUp] - public virtual void SetUp_Logging() => - TestContext.Progress.Write($"Start test {s_testCount++}: {TestContext.CurrentContext.Test.Name}"); - - [SetUp] - public virtual void Setup() + public void Setup() { InMemoryConfiguration[Constants.Configuration.ConfigUnattended + ":" + nameof(UnattendedSettings.InstallUnattended)] = "true"; IHostBuilder hostBuilder = CreateHostBuilder(); - IHost host = hostBuilder.Build(); - BeforeHostStart(host); - host.Start(); + _host = hostBuilder.Build(); + UseTestDatabase(_host.Services); + _host.Start(); - var app = new ApplicationBuilder(host.Services); - Configure(app); - } - - protected virtual void BeforeHostStart(IHost host) - { - Services = host.Services; - UseTestDatabase(Services); - } - - protected ILoggerFactory CreateLoggerFactory() - { - try + if (TestOptions.Boot) { - 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) - .MinimumLevel.Debug() - .CreateLogger(); - - builder.AddSerilog(Log.Logger); - }); - case UmbracoTestOptions.Logger.Console: - return Microsoft.Extensions.Logging.LoggerFactory.Create(builder => builder.AddConsole().SetMinimumLevel(LogLevel.Debug)); - } + Services.GetRequiredService().EnsureUmbracoContext(); } - catch - { - // ignored - } - - return NullLoggerFactory.Instance; } + [TearDown] + public void TearDownAsync() => _host.StopAsync(); + /// /// Create the Generic Host and execute startup ConfigureServices/Configure calls /// - protected virtual IHostBuilder CreateHostBuilder() + private IHostBuilder CreateHostBuilder() { IHostBuilder hostBuilder = Host.CreateDefaultBuilder() @@ -171,7 +69,6 @@ namespace Umbraco.Cms.Tests.Integration.Testing // 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(); @@ -184,7 +81,6 @@ namespace Umbraco.Cms.Tests.Integration.Testing { ConfigureServices(services); ConfigureTestSpecificServices(services); - services.AddUnique(CreateLoggerFactory()); if (!TestOptions.Boot) { @@ -197,12 +93,9 @@ namespace Umbraco.Cms.Tests.Integration.Testing return hostBuilder; } - protected virtual void ConfigureTestSpecificServices(IServiceCollection services) - { - } - private void ConfigureServices(IServiceCollection services) { + services.AddUnique(CreateLoggerFactory()); services.AddSingleton(TestHelper.DbProviderFactoryCreator); services.AddTransient(); IWebHostEnvironment webHostEnvironment = TestHelper.GetWebHostEnvironment(); @@ -232,7 +125,7 @@ namespace Umbraco.Cms.Tests.Integration.Testing .AddBackOfficeIdentity() .AddMembersIdentity() .AddExamine() - .AddTestServices(TestHelper, GetAppCaches()); + .AddTestServices(TestHelper); if (TestOptions.Mapper) { @@ -250,182 +143,12 @@ namespace Umbraco.Cms.Tests.Integration.Testing builder.Build(); } - protected virtual AppCaches GetAppCaches() => - - // Disable caches for integration tests - AppCaches.NoCache; - - public virtual void Configure(IApplicationBuilder app) + protected virtual void CustomTestSetup(IUmbracoBuilder builder) { - if (TestOptions.Boot) - { - 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 /// @@ -451,11 +174,8 @@ namespace Umbraco.Cms.Tests.Integration.Testing protected IMapperCollection Mappers => Services.GetRequiredService(); - protected UserBuilder UserBuilderInstance { get; } = new UserBuilder(); - protected UserGroupBuilder UserGroupBuilderInstance { get; } = new UserGroupBuilder(); + protected UserBuilder UserBuilderInstance { get; } = new (); - private static bool s_firstTestInSession = true; - private bool _firstTestInFixture = true; - private static int s_testCount = 1; + protected UserGroupBuilder UserGroupBuilderInstance { get; } = new (); } } diff --git a/tests/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTestBase.cs b/tests/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTestBase.cs new file mode 100644 index 0000000000..439d169c61 --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTestBase.cs @@ -0,0 +1,243 @@ +using System; +using System.Collections.Generic; +using System.Data.Common; +using System.IO; +using Microsoft.AspNetCore.Builder; +using Microsoft.Data.SqlClient; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using NUnit.Framework; +using Serilog; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Infrastructure.Persistence; +using Umbraco.Cms.Tests.Common.Testing; +using Umbraco.Cms.Tests.Integration.Implementations; + +namespace Umbraco.Cms.Tests.Integration.Testing; + +/// +/// Base class for all UmbracoIntegrationTests +/// +[SingleThreaded] +[NonParallelizable] +public abstract class UmbracoIntegrationTestBase +{ + private static readonly object s_dbLocker = new (); + private static ITestDatabase s_dbInstance; + private static TestDbMeta s_fixtureDbMeta; + private static int s_testCount = 1; + + private bool _firstTestInFixture = true; + private readonly Queue _testTeardown = new (); + private readonly List _fixtureTeardown = new (); + + protected Dictionary InMemoryConfiguration { get; } = new (); + + protected IConfiguration Configuration { get; set; } + + protected UmbracoTestAttribute TestOptions => + TestOptionAttributeBase.GetTestOptions(); + + protected TestHelper TestHelper { get; } = new (); + + private void AddOnTestTearDown(Action tearDown) => _testTeardown.Enqueue(tearDown); + + private void AddOnFixtureTearDown(Action tearDown) => _fixtureTeardown.Add(tearDown); + + [SetUp] + public void SetUp_Logging() => + TestContext.Progress.Write($"Start test {s_testCount++}: {TestContext.CurrentContext.Test.Name}"); + + [TearDown] + public void TearDown_Logging() => + TestContext.Progress.Write($" {TestContext.CurrentContext.Result.Outcome.Status}"); + + [OneTimeTearDown] + public void FixtureTearDown() + { + foreach (Action a in _fixtureTeardown) + { + a(); + } + } + + [TearDown] + public void TearDown() + { + _firstTestInFixture = false; + + while (_testTeardown.TryDequeue(out Action a)) + { + a(); + } + } + + protected ILoggerFactory CreateLoggerFactory() + { + try + { + switch (TestOptions.Logger) + { + case UmbracoTestOptions.Logger.Mock: + return NullLoggerFactory.Instance; + case UmbracoTestOptions.Logger.Serilog: + return LoggerFactory.Create(builder => + { + var path = Path.Combine(TestHelper.WorkingDirectory, "logs", "umbraco_integration_tests_.txt"); + + Log.Logger = new LoggerConfiguration() + .WriteTo.File(path, rollingInterval: RollingInterval.Day) + .MinimumLevel.Debug() + .CreateLogger(); + + builder.AddSerilog(Log.Logger); + }); + case UmbracoTestOptions.Logger.Console: + return LoggerFactory.Create(builder => builder.AddConsole().SetMinimumLevel(LogLevel.Debug)); + } + } + catch + { + // ignored + } + + return NullLoggerFactory.Instance; + } + + protected virtual void ConfigureTestSpecificServices(IServiceCollection services) + { + } + + protected void UseTestDatabase(IApplicationBuilder app) + => UseTestDatabase(app.ApplicationServices); + + 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); + } + + private void ConfigureTestDatabaseFactory(TestDbMeta meta, IUmbracoDatabaseFactory factory, IRuntimeState state) + { + // 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(); + } + + 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); + + var 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 + AddOnTestTearDown(() => 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 + AddOnTestTearDown(() => 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 + AddOnFixtureTearDown(() => 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 + AddOnFixtureTearDown(() => db.Detach(newEmptyFixtureDbMeta)); + } + + ConfigureTestDatabaseFactory(s_fixtureDbMeta, databaseFactory, runtimeState); + + break; + default: + throw new ArgumentOutOfRangeException(nameof(TestOptions), TestOptions, null); + } + } + + 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; + } + } +} diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/RuntimeStateTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/RuntimeStateTests.cs index 52fade8dc2..d0d90c5726 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/RuntimeStateTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/RuntimeStateTests.cs @@ -24,14 +24,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Core [UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest)] public class RuntimeStateTests : UmbracoIntegrationTest { - private protected IRuntimeState RuntimeState { get; private set; } - - public override void Configure(IApplicationBuilder app) - { - base.Configure(app); - - RuntimeState = Services.GetRequiredService(); - } + private IRuntimeState RuntimeState => Services.GetRequiredService(); protected override void CustomTestSetup(IUmbracoBuilder builder) { diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Scoping/ScopeTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Scoping/ScopeTests.cs index bbb9ea8766..1fcb14f0f5 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Scoping/ScopeTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Scoping/ScopeTests.cs @@ -7,6 +7,7 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Castle.Core.Logging; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Moq; using NUnit.Framework; @@ -16,6 +17,7 @@ using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Tests.Common; using Umbraco.Cms.Tests.Common.Testing; using Umbraco.Cms.Tests.Integration.Testing; +using Umbraco.Extensions; namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Scoping { @@ -28,16 +30,19 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Scoping [SetUp] public void SetUp() => Assert.IsNull(ScopeProvider.AmbientScope); // gone - protected override AppCaches GetAppCaches() + + protected override void ConfigureTestSpecificServices(IServiceCollection services) { // Need to have a mockable request cache for tests var appCaches = new AppCaches( NoAppCache.Instance, Mock.Of(x => x.IsAvailable == false), new IsolatedCaches(_ => NoAppCache.Instance)); - return appCaches; + + services.AddUnique(appCaches); } + [Test] public void GivenUncompletedScopeOnChildThread_WhenTheParentCompletes_TheTransactionIsRolledBack() { diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Scoping/ScopedRepositoryTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Scoping/ScopedRepositoryTests.cs index 0c0508e523..f9a844817e 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Scoping/ScopedRepositoryTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Scoping/ScopedRepositoryTests.cs @@ -31,15 +31,15 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Scoping private ILocalizationService LocalizationService => GetRequiredService(); - protected override AppCaches GetAppCaches() + protected override void ConfigureTestSpecificServices(IServiceCollection services) { // this is what's created core web runtime - var result = new AppCaches( + var appCaches = new AppCaches( new DeepCloneAppCache(new ObjectCacheAppCache()), NoAppCache.Instance, new IsolatedCaches(type => new DeepCloneAppCache(new ObjectCacheAppCache()))); - return result; + services.AddUnique(appCaches); } protected override void CustomTestSetup(IUmbracoBuilder builder) diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/Controllers/EntityControllerTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/Controllers/EntityControllerTests.cs index 4e4ce29e9a..ecbd8d52aa 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/Controllers/EntityControllerTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/Controllers/EntityControllerTests.cs @@ -8,6 +8,7 @@ using Microsoft.Extensions.DependencyInjection; using NUnit.Framework; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Tests.Common.Builders; using Umbraco.Cms.Tests.Common.Builders.Extensions; @@ -21,6 +22,8 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Web.BackOffice.Controllers [TestFixture] public class EntityControllerTests : UmbracoTestServerTestBase { + private IScopeProvider ScopeProvider => GetRequiredService(); + [Test] public async Task GetUrlsByIds_MediaWithIntegerIds_ReturnsValidMap() {