From 9ed925941f0a4100808f97b0a2250abd4ed4627a Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 30 Mar 2020 17:25:29 +1100 Subject: [PATCH 1/3] Gets DB installation test working with runtime level checking --- .../Models/GlobalSettings.cs | 2 +- .../Umbraco.Configuration.csproj | 6 + .../Configuration/UmbracoVersion.cs | 10 +- .../Install/DatabaseSchemaCreator.cs | 8 +- .../Persistence/LocalDb.cs | 5 + .../Persistence/UmbracoDatabase.cs | 43 +++ .../Persistence/UmbracoDatabaseFactory.cs | 32 +- .../Runtime/SqlMainDomLock.cs | 8 +- .../GlobalSetupTeardown.cs | 29 ++ .../ApplicationBuilderExtensions.cs | 91 +++++ .../Implementations/HostBuilderExtensions.cs | 42 --- src/Umbraco.Tests.Integration/RuntimeTests.cs | 29 +- .../Testing/LocalDbTestDatabase.cs | 332 ++++++++++++++++++ .../Testing/TestLocalDb.cs | 49 --- .../Persistence/DatabaseContextTests.cs | 2 +- .../FaultHandling/ConnectionRetryTest.cs | 4 +- src/Umbraco.Tests/TestHelpers/TestObjects.cs | 5 +- .../TestHelpers/TestWithDatabaseBase.cs | 2 +- src/Umbraco.Tests/Testing/UmbracoTestBase.cs | 6 +- 19 files changed, 552 insertions(+), 153 deletions(-) create mode 100644 src/Umbraco.Tests.Integration/GlobalSetupTeardown.cs create mode 100644 src/Umbraco.Tests.Integration/Implementations/ApplicationBuilderExtensions.cs delete mode 100644 src/Umbraco.Tests.Integration/Implementations/HostBuilderExtensions.cs create mode 100644 src/Umbraco.Tests.Integration/Testing/LocalDbTestDatabase.cs delete mode 100644 src/Umbraco.Tests.Integration/Testing/TestLocalDb.cs diff --git a/src/Umbraco.Configuration/Models/GlobalSettings.cs b/src/Umbraco.Configuration/Models/GlobalSettings.cs index 4dc764a974..ed1b4e58a2 100644 --- a/src/Umbraco.Configuration/Models/GlobalSettings.cs +++ b/src/Umbraco.Configuration/Models/GlobalSettings.cs @@ -12,7 +12,7 @@ namespace Umbraco.Configuration.Models /// internal class GlobalSettings : IGlobalSettings { - private const string Prefix = Constants.Configuration.ConfigPrefix + "Global:"; + public const string Prefix = Constants.Configuration.ConfigPrefix + "Global:"; internal const string StaticReservedPaths = "~/app_plugins/,~/install/,~/mini-profiler-resources/,"; //must end with a comma! diff --git a/src/Umbraco.Configuration/Umbraco.Configuration.csproj b/src/Umbraco.Configuration/Umbraco.Configuration.csproj index 88bb3639d0..2337ea24f8 100644 --- a/src/Umbraco.Configuration/Umbraco.Configuration.csproj +++ b/src/Umbraco.Configuration/Umbraco.Configuration.csproj @@ -32,4 +32,10 @@ + + + <_Parameter1>Umbraco.Tests.Integration + + + diff --git a/src/Umbraco.Core/Configuration/UmbracoVersion.cs b/src/Umbraco.Core/Configuration/UmbracoVersion.cs index dd96e6edd7..35154a9f50 100644 --- a/src/Umbraco.Core/Configuration/UmbracoVersion.cs +++ b/src/Umbraco.Core/Configuration/UmbracoVersion.cs @@ -13,7 +13,7 @@ namespace Umbraco.Core.Configuration private readonly IGlobalSettings _globalSettings; public UmbracoVersion(IGlobalSettings globalSettings) - :this() + : this() { _globalSettings = globalSettings; } @@ -55,7 +55,7 @@ namespace Umbraco.Core.Configuration /// Is the one that the CLR checks for compatibility. Therefore, it changes only on /// hard-breaking changes (for instance, on new major versions). /// - public Version AssemblyVersion {get; } + public Version AssemblyVersion { get; } /// /// Gets the assembly file version of the Umbraco code. @@ -83,11 +83,13 @@ namespace Umbraco.Core.Configuration /// and changes during an upgrade. The executing code version changes when new code is /// deployed. The site/files version changes during an upgrade. /// - public SemVersion LocalVersion { + public SemVersion LocalVersion + { get { var value = _globalSettings.ConfigurationStatus; return value.IsNullOrWhiteSpace() ? null : SemVersion.TryParse(value, out var semver) ? semver : null; - } } + } + } } } diff --git a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseSchemaCreator.cs b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseSchemaCreator.cs index 8a79cca403..921ba0b3d5 100644 --- a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseSchemaCreator.cs +++ b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseSchemaCreator.cs @@ -130,7 +130,7 @@ namespace Umbraco.Core.Migrations.Install if (e.Cancel == false) { - var dataCreation = new DatabaseDataCreator(_database, _logger,_umbracoVersion, _globalSettings); + var dataCreation = new DatabaseDataCreator(_database, _logger, _umbracoVersion, _globalSettings); foreach (var table in OrderedTables) CreateTable(false, table, dataCreation); } @@ -451,7 +451,7 @@ namespace Umbraco.Core.Migrations.Install //Execute the Create Table sql var created = _database.Execute(new Sql(createSql)); - _logger.Info("Create Table {TableName} ({Created}): \n {Sql}", tableName, created, createSql); + _logger.Info("Create Table {TableName} ({Created}): \n {Sql}", tableName, created, createSql); //If any statements exists for the primary key execute them here if (string.IsNullOrEmpty(createPrimaryKeySql) == false) @@ -487,11 +487,11 @@ namespace Umbraco.Core.Migrations.Install if (overwrite) { - _logger.Info("Table {TableName} was recreated", tableName); + _logger.Info("Table {TableName} was recreated", tableName); } else { - _logger.Info("New table {TableName} was created", tableName); + _logger.Info("New table {TableName} was created", tableName); } } diff --git a/src/Umbraco.Infrastructure/Persistence/LocalDb.cs b/src/Umbraco.Infrastructure/Persistence/LocalDb.cs index 55d6565344..4ec233e17f 100644 --- a/src/Umbraco.Infrastructure/Persistence/LocalDb.cs +++ b/src/Umbraco.Infrastructure/Persistence/LocalDb.cs @@ -253,6 +253,11 @@ namespace Umbraco.Core.Persistence _masterCstr = $@"Server=(localdb)\{instanceName};Integrated Security=True;"; } + public static string GetConnectionString(string instanceName, string databaseName) + { + return $@"Server=(localdb)\{instanceName};Integrated Security=True;Database={databaseName};"; + } + /// /// Gets a LocalDb connection string. /// diff --git a/src/Umbraco.Infrastructure/Persistence/UmbracoDatabase.cs b/src/Umbraco.Infrastructure/Persistence/UmbracoDatabase.cs index ea3d603f95..a4b4afbe25 100644 --- a/src/Umbraco.Infrastructure/Persistence/UmbracoDatabase.cs +++ b/src/Umbraco.Infrastructure/Persistence/UmbracoDatabase.cs @@ -28,6 +28,7 @@ namespace Umbraco.Core.Persistence private readonly RetryPolicy _connectionRetryPolicy; private readonly RetryPolicy _commandRetryPolicy; private readonly Guid _instanceGuid = Guid.NewGuid(); + private List _commands; #region Ctor @@ -162,6 +163,14 @@ namespace Umbraco.Core.Persistence /// public int SqlCount { get; private set; } + internal bool LogCommands + { + get => _commands != null; + set => _commands = value ? new List() : null; + } + + internal IEnumerable Commands => _commands; + public int BulkInsertRecords(IEnumerable records) { return _bulkSqlInsertProvider.BulkInsertRecords(this, records); @@ -267,9 +276,43 @@ namespace Umbraco.Core.Persistence if (_enableCount) SqlCount++; + _commands?.Add(new CommandInfo(cmd)); + base.OnExecutedCommand(cmd); } #endregion + + // used for tracking commands + public class CommandInfo + { + public CommandInfo(IDbCommand cmd) + { + Text = cmd.CommandText; + var parameters = new List(); + foreach (IDbDataParameter parameter in cmd.Parameters) parameters.Add(new ParameterInfo(parameter)); + Parameters = parameters.ToArray(); + } + + public string Text { get; } + public ParameterInfo[] Parameters { get; } + } + + // used for tracking commands + public class ParameterInfo + { + public ParameterInfo(IDbDataParameter parameter) + { + Name = parameter.ParameterName; + Value = parameter.Value; + DbType = parameter.DbType; + Size = parameter.Size; + } + + public string Name { get; } + public object Value { get; } + public DbType DbType { get; } + public int Size { get; } + } } } diff --git a/src/Umbraco.Infrastructure/Persistence/UmbracoDatabaseFactory.cs b/src/Umbraco.Infrastructure/Persistence/UmbracoDatabaseFactory.cs index a93c8db409..837fe6cba3 100644 --- a/src/Umbraco.Infrastructure/Persistence/UmbracoDatabaseFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/UmbracoDatabaseFactory.cs @@ -29,7 +29,6 @@ namespace Umbraco.Core.Persistence { private readonly IDbProviderFactoryCreator _dbProviderFactoryCreator; private readonly IGlobalSettings _globalSettings; - private readonly IConnectionStrings _connectionStrings; private readonly Lazy _mappers; private readonly ILogger _logger; @@ -37,7 +36,6 @@ namespace Umbraco.Core.Persistence private DatabaseFactory _npocoDatabaseFactory; private IPocoDataFactory _pocoDataFactory; - private string _connectionString; private string _providerName; private DatabaseType _databaseType; private ISqlSyntaxProvider _sqlSyntax; @@ -73,7 +71,7 @@ namespace Umbraco.Core.Persistence /// /// Used by core runtime. public UmbracoDatabaseFactory(ILogger logger, IGlobalSettings globalSettings, IConnectionStrings connectionStrings, Lazy mappers,IDbProviderFactoryCreator dbProviderFactoryCreator) - : this(Constants.System.UmbracoConnectionName, globalSettings, connectionStrings, logger, mappers, dbProviderFactoryCreator) + : this(logger, globalSettings, connectionStrings, Constants.System.UmbracoConnectionName, mappers, dbProviderFactoryCreator) { } @@ -82,13 +80,12 @@ namespace Umbraco.Core.Persistence /// Initializes a new instance of the . /// /// Used by the other ctor and in tests. - public UmbracoDatabaseFactory(string connectionStringName, IGlobalSettings globalSettings, IConnectionStrings connectionStrings, ILogger logger, Lazy mappers, IDbProviderFactoryCreator dbProviderFactoryCreator) + public UmbracoDatabaseFactory(ILogger logger, IGlobalSettings globalSettings, IConnectionStrings connectionStrings, string connectionStringName, Lazy mappers, IDbProviderFactoryCreator dbProviderFactoryCreator) { if (connectionStringName == null) throw new ArgumentNullException(nameof(connectionStringName)); if (string.IsNullOrWhiteSpace(connectionStringName)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(connectionStringName)); _globalSettings = globalSettings; - _connectionStrings = connectionStrings; _mappers = mappers ?? throw new ArgumentNullException(nameof(mappers)); _dbProviderFactoryCreator = dbProviderFactoryCreator ?? throw new ArgumentNullException(nameof(dbProviderFactoryCreator)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); @@ -118,7 +115,7 @@ namespace Umbraco.Core.Persistence /// Initializes a new instance of the . /// /// Used in tests. - public UmbracoDatabaseFactory(string connectionString, string providerName, ILogger logger, Lazy mappers, IDbProviderFactoryCreator dbProviderFactoryCreator) + public UmbracoDatabaseFactory(ILogger logger, string connectionString, string providerName, Lazy mappers, IDbProviderFactoryCreator dbProviderFactoryCreator) { _mappers = mappers ?? throw new ArgumentNullException(nameof(mappers)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); @@ -142,7 +139,7 @@ namespace Umbraco.Core.Persistence { lock (_lock) { - return !_connectionString.IsNullOrWhiteSpace() && !_providerName.IsNullOrWhiteSpace(); + return !ConnectionString.IsNullOrWhiteSpace() && !_providerName.IsNullOrWhiteSpace(); } } } @@ -151,13 +148,13 @@ namespace Umbraco.Core.Persistence public bool Initialized => Volatile.Read(ref _initialized); /// - public string ConnectionString => _connectionString; + public string ConnectionString { get; private set; } /// public bool CanConnect => // actually tries to connect to the database (regardless of configured/initialized) - !_connectionString.IsNullOrWhiteSpace() && !_providerName.IsNullOrWhiteSpace() && - DbConnectionExtensions.IsConnectionAvailable(_connectionString, DbProviderFactory); + !ConnectionString.IsNullOrWhiteSpace() && !_providerName.IsNullOrWhiteSpace() && + DbConnectionExtensions.IsConnectionAvailable(ConnectionString, DbProviderFactory); private void UpdateSqlServerDatabaseType() { @@ -169,7 +166,7 @@ namespace Umbraco.Core.Persistence if (setting.IsNullOrWhiteSpace() || !setting.StartsWith("SqlServer.") || !Enum.TryParse(setting.Substring("SqlServer.".Length), out var versionName, true)) { - versionName = ((SqlServerSyntaxProvider) _sqlSyntax).GetSetVersion(_connectionString, _providerName, _logger).ProductVersionName; + versionName = ((SqlServerSyntaxProvider) _sqlSyntax).GetSetVersion(ConnectionString, _providerName, _logger).ProductVersionName; } else { @@ -233,7 +230,7 @@ namespace Umbraco.Core.Persistence if (Volatile.Read(ref _initialized)) throw new InvalidOperationException("Already initialized."); - _connectionString = connectionString; + ConnectionString = connectionString; _providerName = providerName; } @@ -249,18 +246,19 @@ namespace Umbraco.Core.Persistence { _logger.Debug("Initializing."); - if (_connectionString.IsNullOrWhiteSpace()) throw new InvalidOperationException("The factory has not been configured with a proper connection string."); + if (ConnectionString.IsNullOrWhiteSpace()) throw new InvalidOperationException("The factory has not been configured with a proper connection string."); if (_providerName.IsNullOrWhiteSpace()) throw new InvalidOperationException("The factory has not been configured with a proper provider name."); if (DbProviderFactory == null) throw new Exception($"Can't find a provider factory for provider name \"{_providerName}\"."); // cannot initialize without being able to talk to the database - if (!DbConnectionExtensions.IsConnectionAvailable(_connectionString, DbProviderFactory)) + // TODO: Why not? + if (!DbConnectionExtensions.IsConnectionAvailable(ConnectionString, DbProviderFactory)) throw new Exception("Cannot connect to the database."); - _connectionRetryPolicy = RetryPolicyFactory.GetDefaultSqlConnectionRetryPolicyByConnectionString(_connectionString); - _commandRetryPolicy = RetryPolicyFactory.GetDefaultSqlCommandRetryPolicyByConnectionString(_connectionString); + _connectionRetryPolicy = RetryPolicyFactory.GetDefaultSqlConnectionRetryPolicyByConnectionString(ConnectionString); + _commandRetryPolicy = RetryPolicyFactory.GetDefaultSqlCommandRetryPolicyByConnectionString(ConnectionString); _databaseType = DatabaseType.Resolve(DbProviderFactory.GetType().Name, _providerName); @@ -313,7 +311,7 @@ namespace Umbraco.Core.Persistence // method used by NPoco's UmbracoDatabaseFactory to actually create the database instance private UmbracoDatabase CreateDatabaseInstance() { - return new UmbracoDatabase(_connectionString, SqlContext, DbProviderFactory, _logger, _bulkSqlInsertProvider, _connectionRetryPolicy, _commandRetryPolicy); + return new UmbracoDatabase(ConnectionString, SqlContext, DbProviderFactory, _logger, _bulkSqlInsertProvider, _connectionRetryPolicy, _commandRetryPolicy); } protected override void DisposeResources() diff --git a/src/Umbraco.Infrastructure/Runtime/SqlMainDomLock.cs b/src/Umbraco.Infrastructure/Runtime/SqlMainDomLock.cs index 6d73934387..99f38f0486 100644 --- a/src/Umbraco.Infrastructure/Runtime/SqlMainDomLock.cs +++ b/src/Umbraco.Infrastructure/Runtime/SqlMainDomLock.cs @@ -33,14 +33,12 @@ namespace Umbraco.Core.Runtime // unique id for our appdomain, this is more unique than the appdomain id which is just an INT counter to its safer _lockId = Guid.NewGuid().ToString(); _logger = logger; - _dbFactory = new UmbracoDatabaseFactory( - Constants.System.UmbracoConnectionName, + _dbFactory = new UmbracoDatabaseFactory(_logger, globalSettings, connectionStrings, - _logger, + Constants.System.UmbracoConnectionName, new Lazy(() => new MapperCollection(Enumerable.Empty())), - dbProviderFactoryCreator - ); + dbProviderFactoryCreator); } public async Task AcquireLockAsync(int millisecondsTimeout) diff --git a/src/Umbraco.Tests.Integration/GlobalSetupTeardown.cs b/src/Umbraco.Tests.Integration/GlobalSetupTeardown.cs new file mode 100644 index 0000000000..fe1d604dd9 --- /dev/null +++ b/src/Umbraco.Tests.Integration/GlobalSetupTeardown.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Text; +using NUnit.Framework; +using Umbraco.Tests.Integration.Testing; + +// this class has NO NAMESPACE +// it applies to the whole assembly + +[SetUpFixture] +// ReSharper disable once CheckNamespace +public class TestsSetup +{ + private Stopwatch _stopwatch; + + [OneTimeSetUp] + public void SetUp() + { + _stopwatch = Stopwatch.StartNew(); + } + + [OneTimeTearDown] + public void TearDown() + { + LocalDbTestDatabase.KillLocalDb(); + Console.WriteLine("TOTAL TESTS DURATION: {0}", _stopwatch.Elapsed); + } +} diff --git a/src/Umbraco.Tests.Integration/Implementations/ApplicationBuilderExtensions.cs b/src/Umbraco.Tests.Integration/Implementations/ApplicationBuilderExtensions.cs new file mode 100644 index 0000000000..abb22c0bd7 --- /dev/null +++ b/src/Umbraco.Tests.Integration/Implementations/ApplicationBuilderExtensions.cs @@ -0,0 +1,91 @@ +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 NUnit.Framework; +using Umbraco.Configuration.Models; +using Umbraco.Core; +using Umbraco.Core.Configuration; +using Umbraco.Core.Logging; +using Umbraco.Core.Persistence; +using Umbraco.Tests.Integration.Testing; + +namespace Umbraco.Tests.Integration.Implementations +{ + public static class ApplicationBuilderExtensions + { + /// + /// Creates a LocalDb instance to use for the test + /// + /// + /// + /// + /// Default is true - meaning a brand new database is created for this test. If this is false it will try to + /// re-use an existing database that was already created as part of this test fixture. + /// // TODO Implement the 'false' behavior + /// + /// + /// Default is true - meaning a database schema will be created for this test if it's a new database. If this is false + /// it will just create an empty database. + /// + /// + public static IApplicationBuilder UseTestLocalDb(this IApplicationBuilder app, + string dbFilePath, + bool createNewDb = true, + bool initializeSchema = true) + { + // need to manually register this factory + DbProviderFactories.RegisterFactory(Constants.DbProviderNames.SqlServer, SqlClientFactory.Instance); + + if (!Directory.Exists(dbFilePath)) + Directory.CreateDirectory(dbFilePath); + + var db = LocalDbTestDatabase.Get(dbFilePath, + app.ApplicationServices.GetRequiredService(), + app.ApplicationServices.GetRequiredService(), + app.ApplicationServices.GetRequiredService()); + + if (initializeSchema) + { + // New DB + Schema + db.AttachSchema(); + + // In the case that we've initialized the schema, it means that we are installed so we'll want to ensure that + // the runtime state is configured correctly so we'll force update the configuration flag and re-run the + // runtime state checker. + // TODO: This wouldn't be required if we don't store the Umbraco version in config + + // right now we are an an 'Install' state + var runtimeState = (RuntimeState)app.ApplicationServices.GetRequiredService(); + Assert.AreEqual(RuntimeLevel.Install, runtimeState.Level); + + // dynamically change the config status + var umbVersion = app.ApplicationServices.GetRequiredService(); + var config = app.ApplicationServices.GetRequiredService(); + config[GlobalSettings.Prefix + "ConfigurationStatus"] = umbVersion.SemanticVersion.ToString(); + + // re-run the runtime level check + var dbFactory = app.ApplicationServices.GetRequiredService(); + var profilingLogger = app.ApplicationServices.GetRequiredService(); + runtimeState.DetermineRuntimeLevel(dbFactory, profilingLogger); + + Assert.AreEqual(RuntimeLevel.Run, runtimeState.Level); + + } + else + { + db.AttachEmpty(); + } + + return app; + } + + } + + +} diff --git a/src/Umbraco.Tests.Integration/Implementations/HostBuilderExtensions.cs b/src/Umbraco.Tests.Integration/Implementations/HostBuilderExtensions.cs deleted file mode 100644 index 60cac26349..0000000000 --- a/src/Umbraco.Tests.Integration/Implementations/HostBuilderExtensions.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Data.Common; -using System.Data.SqlClient; -using System.IO; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Hosting; -using Umbraco.Core; -using Umbraco.Tests.Integration.Testing; - -namespace Umbraco.Tests.Integration.Implementations -{ - public static class HostBuilderExtensions - { - - public static IHostBuilder UseLocalDb(this IHostBuilder hostBuilder, string dbFilePath) - { - // Need to register SqlClient manually - // TODO: Move this to someplace central - DbProviderFactories.RegisterFactory(Constants.DbProviderNames.SqlServer, SqlClientFactory.Instance); - - hostBuilder.ConfigureAppConfiguration(x => - { - if (!Directory.Exists(dbFilePath)) - Directory.CreateDirectory(dbFilePath); - - var dbName = Guid.NewGuid().ToString("N"); - var instance = TestLocalDb.EnsureLocalDbInstanceAndDatabase(dbName, dbFilePath); - - x.AddInMemoryCollection(new[] - { - new KeyValuePair($"ConnectionStrings:{Constants.System.UmbracoConnectionName}", instance.GetConnectionString(dbName)) - }); - }); - return hostBuilder; - } - - - } - - -} diff --git a/src/Umbraco.Tests.Integration/RuntimeTests.cs b/src/Umbraco.Tests.Integration/RuntimeTests.cs index 0e11a29b95..814687eb7f 100644 --- a/src/Umbraco.Tests.Integration/RuntimeTests.cs +++ b/src/Umbraco.Tests.Integration/RuntimeTests.cs @@ -11,9 +11,11 @@ using System.Collections.Generic; using System.IO; using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; +using Umbraco.Configuration.Models; using Umbraco.Core; using Umbraco.Core.Composing; using Umbraco.Core.Composing.LightInject; +using Umbraco.Core.Configuration; using Umbraco.Core.Logging; using Umbraco.Core.Migrations.Install; using Umbraco.Core.Persistence; @@ -40,7 +42,7 @@ namespace Umbraco.Tests.Integration [OneTimeTearDown] public void FixtureTearDown() { - TestLocalDb.Cleanup(); + } /// @@ -174,8 +176,6 @@ namespace Umbraco.Tests.Integration var testHelper = new TestHelper(); var hostBuilder = new HostBuilder() - //TODO: Need to have a configured umb version for the runtime state - .UseLocalDb(Path.Combine(testHelper.CurrentAssemblyDirectory, "LocalDb")) .UseUmbraco(serviceProviderFactory) .ConfigureServices((hostContext, services) => { @@ -190,26 +190,13 @@ namespace Umbraco.Tests.Integration var host = await hostBuilder.StartAsync(); var app = new ApplicationBuilder(host.Services); + // This will create a db, install the schema and ensure the app is configured to run + app.UseTestLocalDb(Path.Combine(testHelper.CurrentAssemblyDirectory, "LocalDb")); + app.UseUmbracoCore(); - - var runtimeState = (RuntimeState)app.ApplicationServices.GetRequiredService(); - Assert.AreEqual(RuntimeLevel.Install, runtimeState.Level); - - var dbBuilder = app.ApplicationServices.GetRequiredService(); - Assert.IsNotNull(dbBuilder); - - var canConnect = dbBuilder.CanConnectToDatabase; - Assert.IsTrue(canConnect); - - var dbResult = dbBuilder.CreateSchemaAndData(); - Assert.IsTrue(dbResult.Success); - - // TODO: Get this to work ... but to do that we need to mock or pass in a current umbraco version - //var dbFactory = app.ApplicationServices.GetRequiredService(); - //var profilingLogger = app.ApplicationServices.GetRequiredService(); - //runtimeState.DetermineRuntimeLevel(dbFactory, profilingLogger); - //Assert.AreEqual(RuntimeLevel.Run, runtimeState.Level); + var runtimeState = app.ApplicationServices.GetRequiredService(); + Assert.AreEqual(RuntimeLevel.Run, runtimeState.Level); } internal static LightInjectContainer GetUmbracoContainer(out UmbracoServiceProviderFactory serviceProviderFactory) diff --git a/src/Umbraco.Tests.Integration/Testing/LocalDbTestDatabase.cs b/src/Umbraco.Tests.Integration/Testing/LocalDbTestDatabase.cs new file mode 100644 index 0000000000..63b336701c --- /dev/null +++ b/src/Umbraco.Tests.Integration/Testing/LocalDbTestDatabase.cs @@ -0,0 +1,332 @@ +using System; +using System.Collections.Concurrent; +using System.Configuration; +using System.Data; +using System.Data.Common; +using System.Data.SqlClient; +using System.IO; +using System.Linq; +using System.Threading; +using Umbraco.Core.Configuration; +using Umbraco.Core.Logging; +using Umbraco.Core.Migrations.Install; +using Umbraco.Core.Persistence; + +namespace Umbraco.Tests.Integration.Testing +{ + /// + /// Manages a pool of LocalDb databases for integration testing + /// + internal class LocalDbTestDatabase + { + public static LocalDbTestDatabase Get(string filesPath, ILogger logger, IGlobalSettings globalSettings, IUmbracoDatabaseFactory dbFactory) + { + var localDb = new LocalDb(); + if (localDb.IsAvailable == false) + throw new InvalidOperationException("LocalDB is not available."); + return new LocalDbTestDatabase(logger, globalSettings, localDb, filesPath, dbFactory); + } + + public const string InstanceName = "UmbracoTests"; + public const string DatabaseName = "UmbracoTests"; + + private readonly ILogger _logger; + private readonly IGlobalSettings _globalSettings; + private readonly LocalDb _localDb; + private readonly IUmbracoVersion _umbracoVersion; + private static LocalDb.Instance _instance; + private static string _filesPath; + private readonly IUmbracoDatabaseFactory _dbFactory; + private UmbracoDatabase.CommandInfo[] _dbCommands; + private string _currentCstr; + private static DatabasePool _emptyPool; + private static DatabasePool _schemaPool; + private DatabasePool _currentPool; + + public LocalDbTestDatabase(ILogger logger, IGlobalSettings globalSettings, LocalDb localDb, string filesPath, IUmbracoDatabaseFactory dbFactory) + { + _umbracoVersion = new UmbracoVersion(); + _logger = logger; + _globalSettings = globalSettings; + _localDb = localDb; + _filesPath = filesPath; + _dbFactory = dbFactory; + + _instance = _localDb.GetInstance(InstanceName); + if (_instance != null) return; + + if (_localDb.CreateInstance(InstanceName) == false) + throw new Exception("Failed to create a LocalDb instance."); + _instance = _localDb.GetInstance(InstanceName); + } + + public string ConnectionString => _currentCstr ?? _instance.GetAttachedConnectionString("XXXXXX", _filesPath); + + private void Create() + { + var tempName = Guid.NewGuid().ToString("N"); + _instance.CreateDatabase(tempName, _filesPath); + _instance.DetachDatabase(tempName); + + // there's probably a sweet spot to be found for size / parallel... + + var s = ConfigurationManager.AppSettings["Umbraco.Tests.LocalDbTestDatabase.EmptyPoolSize"]; + var emptySize = s == null ? 2 : int.Parse(s); + s = ConfigurationManager.AppSettings["Umbraco.Tests.LocalDbTestDatabase.EmptyPoolThreadCount"]; + var emptyParallel = s == null ? 1 : int.Parse(s); + s = ConfigurationManager.AppSettings["Umbraco.Tests.LocalDbTestDatabase.SchemaPoolSize"]; + var schemaSize = s == null ? 2 : int.Parse(s); + s = ConfigurationManager.AppSettings["Umbraco.Tests.LocalDbTestDatabase.SchemaPoolThreadCount"]; + var schemaParallel = s == null ? 1 : int.Parse(s); + + _emptyPool = new DatabasePool(_localDb, _instance, DatabaseName + "-Empty", tempName, _filesPath, emptySize, emptyParallel); + _schemaPool = new DatabasePool(_localDb, _instance, DatabaseName + "-Schema", tempName, _filesPath, schemaSize, schemaParallel, delete: true, prepare: RebuildSchema); + } + + public void AttachEmpty() + { + if (_emptyPool == null) + Create(); + + _currentCstr = _emptyPool.AttachDatabase(); + _currentPool = _emptyPool; + } + + public void AttachSchema() + { + if (_schemaPool == null) + Create(); + + _currentCstr = _schemaPool.AttachDatabase(); + _currentPool = _schemaPool; + } + + public void Detach() + { + _currentPool.DetachDatabase(); + } + + private void RebuildSchema(DbConnection conn, IDbCommand cmd) + { + + if (_dbCommands != null) + { + foreach (var dbCommand in _dbCommands) + { + + if (dbCommand.Text.StartsWith("SELECT ")) continue; + + cmd.CommandText = dbCommand.Text; + cmd.Parameters.Clear(); + foreach (var parameterInfo in dbCommand.Parameters) + AddParameter(cmd, parameterInfo); + cmd.ExecuteNonQuery(); + } + } + else + { + _dbFactory.Configure(conn.ConnectionString, Umbraco.Core.Constants.DatabaseProviders.SqlServer); + + using var database = (UmbracoDatabase)_dbFactory.CreateDatabase(); + // track each db command ran as part of creating the database so we can replay these + database.LogCommands = true; + + using var trans = database.GetTransaction(); + + var creator = new DatabaseSchemaCreator(database, _logger, _umbracoVersion, _globalSettings); + creator.InitializeDatabaseSchema(); + + trans.Complete(); // commit it + + _dbCommands = database.Commands.ToArray(); + } + + } + + private static void AddParameter(IDbCommand cmd, UmbracoDatabase.ParameterInfo parameterInfo) + { + var p = cmd.CreateParameter(); + p.ParameterName = parameterInfo.Name; + p.Value = parameterInfo.Value; + p.DbType = parameterInfo.DbType; + p.Size = parameterInfo.Size; + cmd.Parameters.Add(p); + } + + public void Clear() + { + var filename = Path.Combine(_filesPath, DatabaseName).ToUpper(); + + foreach (var database in _instance.GetDatabases()) + { + if (database.StartsWith(filename)) + _instance.DropDatabase(database); + } + + foreach (var file in Directory.EnumerateFiles(_filesPath)) + { + if (file.EndsWith(".mdf") == false && file.EndsWith(".ldf") == false) continue; + File.Delete(file); + } + } + + private static void ResetLocalDb(IDbCommand cmd) + { + // https://stackoverflow.com/questions/536350 + + cmd.CommandType = CommandType.Text; + cmd.CommandText = @" + declare @n char(1); + set @n = char(10); + declare @stmt nvarchar(max); + -- check constraints + select @stmt = isnull( @stmt + @n, '' ) + + 'alter table [' + schema_name(schema_id) + '].[' + object_name( parent_object_id ) + '] drop constraint [' + name + ']' + from sys.check_constraints; + -- foreign keys + select @stmt = isnull( @stmt + @n, '' ) + + 'alter table [' + schema_name(schema_id) + '].[' + object_name( parent_object_id ) + '] drop constraint [' + name + ']' + from sys.foreign_keys; + -- tables + select @stmt = isnull( @stmt + @n, '' ) + + 'drop table [' + schema_name(schema_id) + '].[' + name + ']' + from sys.tables; + exec sp_executesql @stmt; + "; + cmd.ExecuteNonQuery(); + } + + public static void KillLocalDb() + { + _emptyPool?.Stop(); + _schemaPool?.Stop(); + + if (_filesPath == null) + return; + + var filename = Path.Combine(_filesPath, DatabaseName).ToUpper(); + + foreach (var database in _instance.GetDatabases()) + { + if (database.StartsWith(filename)) + _instance.DropDatabase(database); + } + + foreach (var file in Directory.EnumerateFiles(_filesPath)) + { + if (file.EndsWith(".mdf") == false && file.EndsWith(".ldf") == false) continue; + try + { + File.Delete(file); + } + catch (IOException) + { + // ignore, must still be in use but nothing we can do + } + } + } + + private class DatabasePool + { + private readonly LocalDb _localDb; + private readonly LocalDb.Instance _instance; + private readonly string _filesPath; + private readonly string _name; + private readonly int _size; + private readonly string[] _cstrs; + private readonly BlockingCollection _prepareQueue, _readyQueue; + private readonly Action _prepare; + private int _current; + + public DatabasePool(LocalDb localDb, LocalDb.Instance instance, string name, string tempName, string filesPath, int size, int parallel = 1, Action prepare = null, bool delete = false) + { + _localDb = localDb; + _instance = instance; + _filesPath = filesPath; + _name = name; + _size = size; + _prepare = prepare; + _prepareQueue = new BlockingCollection(); + _readyQueue = new BlockingCollection(); + _cstrs = new string[_size]; + + for (var i = 0; i < size; i++) + localDb.CopyDatabaseFiles(tempName, filesPath, targetDatabaseName: name + "-" + i, overwrite: true, delete: delete && i == size - 1); + + if (prepare == null) + { + for (var i = 0; i < size; i++) + _readyQueue.Add(i); + } + else + { + for (var i = 0; i < size; i++) + _prepareQueue.Add(i); + } + + for (var i = 0; i < parallel; i++) + { + var thread = new Thread(PrepareThread); + thread.Start(); + } + } + + public string AttachDatabase() + { + try + { + _current = _readyQueue.Take(); + } + catch (InvalidOperationException) + { + _current = 0; + return null; + } + return ConnectionString(_current); + } + + public void DetachDatabase() + { + _prepareQueue.Add(_current); + } + + private string ConnectionString(int i) + { + return _cstrs[i] ?? (_cstrs[i] = _instance.GetAttachedConnectionString(_name + "-" + i, _filesPath)); + } + + private void PrepareThread() + { + while (_prepareQueue.IsCompleted == false) + { + int i; + try + { + i = _prepareQueue.Take(); + } + catch (InvalidOperationException) + { + continue; + } + using (var conn = new SqlConnection(ConnectionString(i))) + using (var cmd = conn.CreateCommand()) + { + conn.Open(); + ResetLocalDb(cmd); + _prepare?.Invoke(conn, cmd); + } + _readyQueue.Add(i); + } + } + + public void Stop() + { + int i; + _prepareQueue.CompleteAdding(); + while (_prepareQueue.TryTake(out i)) { } + _readyQueue.CompleteAdding(); + while (_readyQueue.TryTake(out i)) { } + } + } + } +} diff --git a/src/Umbraco.Tests.Integration/Testing/TestLocalDb.cs b/src/Umbraco.Tests.Integration/Testing/TestLocalDb.cs deleted file mode 100644 index 8ee326783b..0000000000 --- a/src/Umbraco.Tests.Integration/Testing/TestLocalDb.cs +++ /dev/null @@ -1,49 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; -using Umbraco.Core.Persistence; - -namespace Umbraco.Tests.Integration.Testing -{ - public static class TestLocalDb - { - private const string LocalDbInstanceName = "UmbTests"; - - private static LocalDb LocalDb { get; } = new LocalDb(); - - // TODO: We need to borrow logic from this old branch, this is the latest commit at the old branch where we had LocalDb - // working for tests. There's a lot of hoops to jump through to make it work 'fast'. Turns out it didn't actually run as - // fast as SqlCe due to the dropping/creating of DB instances since that is faster in SqlCe but this code was all heavily - // optimized to go as fast as possible. - // see https://github.com/umbraco/Umbraco-CMS/blob/3a8716ac7b1c48b51258724337086cd0712625a1/src/Umbraco.Tests/TestHelpers/LocalDbTestDatabase.cs - internal static LocalDb.Instance EnsureLocalDbInstanceAndDatabase(string dbName, string dbFilePath) - { - if (!LocalDb.InstanceExists(LocalDbInstanceName) && !LocalDb.CreateInstance(LocalDbInstanceName)) - { - throw new InvalidOperationException( - $"Failed to create LocalDb instance {LocalDbInstanceName}, assuming LocalDb is not really available."); - } - - var instance = LocalDb.GetInstance(LocalDbInstanceName); - - if (instance == null) - { - throw new InvalidOperationException( - $"Failed to get LocalDb instance {LocalDbInstanceName}, assuming LocalDb is not really available."); - } - - instance.CreateDatabase(dbName, dbFilePath); - - return instance; - } - - public static void Cleanup() - { - var instance = LocalDb.GetInstance(LocalDbInstanceName); - if (instance != null) - { - instance.DropDatabases(); - } - } - } -} diff --git a/src/Umbraco.Tests/Persistence/DatabaseContextTests.cs b/src/Umbraco.Tests/Persistence/DatabaseContextTests.cs index 52f2365cbc..d964ab972b 100644 --- a/src/Umbraco.Tests/Persistence/DatabaseContextTests.cs +++ b/src/Umbraco.Tests/Persistence/DatabaseContextTests.cs @@ -72,7 +72,7 @@ namespace Umbraco.Tests.Persistence } // re-create the database factory and database context with proper connection string - _databaseFactory = new UmbracoDatabaseFactory(connString, Constants.DbProviderNames.SqlCe, _logger, new Lazy(() => Mock.Of()), TestHelper.DbProviderFactoryCreator); + _databaseFactory = new UmbracoDatabaseFactory(_logger, connString, Constants.DbProviderNames.SqlCe, new Lazy(() => Mock.Of()), TestHelper.DbProviderFactoryCreator); // test get database type (requires an actual database) using (var database = _databaseFactory.CreateDatabase()) diff --git a/src/Umbraco.Tests/Persistence/FaultHandling/ConnectionRetryTest.cs b/src/Umbraco.Tests/Persistence/FaultHandling/ConnectionRetryTest.cs index 4a9e8e2b26..bab0617ec6 100644 --- a/src/Umbraco.Tests/Persistence/FaultHandling/ConnectionRetryTest.cs +++ b/src/Umbraco.Tests/Persistence/FaultHandling/ConnectionRetryTest.cs @@ -20,7 +20,7 @@ namespace Umbraco.Tests.Persistence.FaultHandling { const string connectionString = @"server=.\SQLEXPRESS;database=EmptyForTest;user id=x;password=umbraco"; const string providerName = Constants.DbProviderNames.SqlServer; - var factory = new UmbracoDatabaseFactory(connectionString, providerName, Mock.Of(), new Lazy(() => Mock.Of()), TestHelper.DbProviderFactoryCreator); + var factory = new UmbracoDatabaseFactory(Mock.Of(), connectionString, providerName, new Lazy(() => Mock.Of()), TestHelper.DbProviderFactoryCreator); using (var database = factory.CreateDatabase()) { @@ -34,7 +34,7 @@ namespace Umbraco.Tests.Persistence.FaultHandling { const string connectionString = @"server=.\SQLEXPRESS;database=EmptyForTest;user id=umbraco;password=umbraco"; const string providerName = Constants.DbProviderNames.SqlServer; - var factory = new UmbracoDatabaseFactory(connectionString, providerName, Mock.Of(), new Lazy(() => Mock.Of()), TestHelper.DbProviderFactoryCreator); + var factory = new UmbracoDatabaseFactory(Mock.Of(), connectionString, providerName, new Lazy(() => Mock.Of()), TestHelper.DbProviderFactoryCreator); using (var database = factory.CreateDatabase()) { diff --git a/src/Umbraco.Tests/TestHelpers/TestObjects.cs b/src/Umbraco.Tests/TestHelpers/TestObjects.cs index 33e477df2c..fa037fbb50 100644 --- a/src/Umbraco.Tests/TestHelpers/TestObjects.cs +++ b/src/Umbraco.Tests/TestHelpers/TestObjects.cs @@ -244,11 +244,10 @@ namespace Umbraco.Tests.TestHelpers // mappersBuilder.AddCore(); // var mappers = mappersBuilder.CreateCollection(); var mappers = Current.Factory.GetInstance(); - databaseFactory = new UmbracoDatabaseFactory( - Constants.System.UmbracoConnectionName, + databaseFactory = new UmbracoDatabaseFactory(logger, SettingsForTests.GetDefaultGlobalSettings(), new ConnectionStrings(), - logger, + Constants.System.UmbracoConnectionName, new Lazy(() => mappers), TestHelper.DbProviderFactoryCreator); } diff --git a/src/Umbraco.Tests/TestHelpers/TestWithDatabaseBase.cs b/src/Umbraco.Tests/TestHelpers/TestWithDatabaseBase.cs index fbfada118a..b4db4f551c 100644 --- a/src/Umbraco.Tests/TestHelpers/TestWithDatabaseBase.cs +++ b/src/Umbraco.Tests/TestHelpers/TestWithDatabaseBase.cs @@ -89,7 +89,7 @@ namespace Umbraco.Tests.TestHelpers return TestObjects.GetDatabaseFactoryMock(); var lazyMappers = new Lazy(f.GetInstance); - var factory = new UmbracoDatabaseFactory(GetDbConnectionString(), GetDbProviderName(), f.GetInstance(), lazyMappers, TestHelper.DbProviderFactoryCreator); + var factory = new UmbracoDatabaseFactory(f.GetInstance(), GetDbConnectionString(), GetDbProviderName(), lazyMappers, TestHelper.DbProviderFactoryCreator); factory.ResetForTests(); return factory; }); diff --git a/src/Umbraco.Tests/Testing/UmbracoTestBase.cs b/src/Umbraco.Tests/Testing/UmbracoTestBase.cs index 0345aab2da..c3505a23fa 100644 --- a/src/Umbraco.Tests/Testing/UmbracoTestBase.cs +++ b/src/Umbraco.Tests/Testing/UmbracoTestBase.cs @@ -462,13 +462,13 @@ namespace Umbraco.Tests.Testing var globalSettings = TestHelper.GetConfigs().Global(); var connectionStrings = TestHelper.GetConfigs().ConnectionStrings(); - Composition.RegisterUnique(f => new UmbracoDatabaseFactory( - Constants.System.UmbracoConnectionName, + Composition.RegisterUnique(f => new UmbracoDatabaseFactory(Logger, globalSettings, connectionStrings, - Logger, + Constants.System.UmbracoConnectionName, new Lazy(f.GetInstance), TestHelper.DbProviderFactoryCreator)); + Composition.RegisterUnique(f => f.TryGetInstance().SqlContext); Composition.WithCollectionBuilder(); // empty From 3757f722a0895390fe163052fe3e82f96ae42549 Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 30 Mar 2020 20:55:13 +1100 Subject: [PATCH 2/3] Gets first integration test moved, moves the test options to the common project, ensures that the LocalDb pool is updated on each test --- .../Testing/TestOptionAttributeBase.cs | 6 +- .../Testing/UmbracoTestAttribute.cs | 0 .../Testing/UmbracoTestOptions.cs | 61 +++++++ .../ContainerTests.cs | 3 +- .../ApplicationBuilderExtensions.cs | 117 ++++++++++++++ .../Extensions/ServiceCollectionExtensions.cs | 23 +++ .../ApplicationBuilderExtensions.cs | 91 ----------- .../Repositories/AuditRepositoryTest.cs | 54 +++---- src/Umbraco.Tests.Integration/RuntimeTests.cs | 78 ++------- .../Testing/LocalDbTestDatabase.cs | 14 +- .../Testing/UmbracoIntegrationTest.cs | 150 ++++++++++++++++++ src/Umbraco.Tests/TestHelpers/TestObjects.cs | 4 +- .../Testing/UmbracoTestOptions.cs | 39 ----- src/Umbraco.Tests/Umbraco.Tests.csproj | 4 - 14 files changed, 401 insertions(+), 243 deletions(-) rename src/{Umbraco.Tests => Umbraco.Tests.Common}/Testing/TestOptionAttributeBase.cs (88%) rename src/{Umbraco.Tests => Umbraco.Tests.Common}/Testing/UmbracoTestAttribute.cs (100%) create mode 100644 src/Umbraco.Tests.Common/Testing/UmbracoTestOptions.cs create mode 100644 src/Umbraco.Tests.Integration/Extensions/ApplicationBuilderExtensions.cs create mode 100644 src/Umbraco.Tests.Integration/Extensions/ServiceCollectionExtensions.cs delete mode 100644 src/Umbraco.Tests.Integration/Implementations/ApplicationBuilderExtensions.cs rename src/{Umbraco.Tests => Umbraco.Tests.Integration}/Persistence/Repositories/AuditRepositoryTest.cs (72%) create mode 100644 src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs delete mode 100644 src/Umbraco.Tests/Testing/UmbracoTestOptions.cs diff --git a/src/Umbraco.Tests/Testing/TestOptionAttributeBase.cs b/src/Umbraco.Tests.Common/Testing/TestOptionAttributeBase.cs similarity index 88% rename from src/Umbraco.Tests/Testing/TestOptionAttributeBase.cs rename to src/Umbraco.Tests.Common/Testing/TestOptionAttributeBase.cs index 2fb4660481..bbfa68778f 100644 --- a/src/Umbraco.Tests/Testing/TestOptionAttributeBase.cs +++ b/src/Umbraco.Tests.Common/Testing/TestOptionAttributeBase.cs @@ -31,10 +31,14 @@ namespace Umbraco.Tests.Testing var test = TestContext.CurrentContext.Test; var typeName = test.ClassName; var methodName = test.MethodName; + // this will only get types from whatever is already loaded in the app domain var type = Type.GetType(typeName, false); if (type == null) { - type = ScanAssemblies + // automatically add the executing and calling assemblies to the list to scan for this type + var scanAssemblies = ScanAssemblies.Union(new[] {Assembly.GetExecutingAssembly(), Assembly.GetCallingAssembly()}).ToList(); + + type = scanAssemblies .Select(assembly => assembly.GetType(typeName, false)) .FirstOrDefault(x => x != null); if (type == null) diff --git a/src/Umbraco.Tests/Testing/UmbracoTestAttribute.cs b/src/Umbraco.Tests.Common/Testing/UmbracoTestAttribute.cs similarity index 100% rename from src/Umbraco.Tests/Testing/UmbracoTestAttribute.cs rename to src/Umbraco.Tests.Common/Testing/UmbracoTestAttribute.cs diff --git a/src/Umbraco.Tests.Common/Testing/UmbracoTestOptions.cs b/src/Umbraco.Tests.Common/Testing/UmbracoTestOptions.cs new file mode 100644 index 0000000000..eef6e45d7e --- /dev/null +++ b/src/Umbraco.Tests.Common/Testing/UmbracoTestOptions.cs @@ -0,0 +1,61 @@ +namespace Umbraco.Tests.Testing +{ + public static class UmbracoTestOptions + { + public enum Logger + { + /// + /// pure mocks + /// + Mock, + /// + /// Serilog for tests + /// + Serilog, + /// + /// console logger + /// + Console + } + + public enum Database + { + /// + /// no database + /// + None, + /// + /// new empty database file for the entire fixture + /// + NewEmptyPerFixture, + /// + /// new empty database file per test + /// + NewEmptyPerTest, + /// + /// new database file with schema for the entire fixture + /// + NewSchemaPerFixture, + /// + /// new database file with schema per test + /// + NewSchemaPerTest + } + + public enum TypeLoader + { + /// + /// the default, global type loader for tests + /// + Default, + /// + /// create one type loader for the feature + /// + PerFixture, + /// + /// create one type loader for each test + /// + PerTest + } + } +} diff --git a/src/Umbraco.Tests.Integration/ContainerTests.cs b/src/Umbraco.Tests.Integration/ContainerTests.cs index 945eeda2f0..34a32a38a8 100644 --- a/src/Umbraco.Tests.Integration/ContainerTests.cs +++ b/src/Umbraco.Tests.Integration/ContainerTests.cs @@ -13,6 +13,7 @@ using Umbraco.Core.Configuration; using Umbraco.Core.Persistence; using Umbraco.Tests.Common; using Umbraco.Tests.Integration.Implementations; +using Umbraco.Tests.Integration.Testing; namespace Umbraco.Tests.Integration { @@ -82,7 +83,7 @@ namespace Umbraco.Tests.Integration // it means the container won't be disposed, and maybe other services? not sure. // In cases where we use it can we use IConfigureOptions? https://andrewlock.net/access-services-inside-options-and-startup-using-configureoptions/ - var umbracoContainer = RuntimeTests.GetUmbracoContainer(out var serviceProviderFactory); + var umbracoContainer = UmbracoIntegrationTest.GetUmbracoContainer(out var serviceProviderFactory); IHostApplicationLifetime lifetime1 = null; diff --git a/src/Umbraco.Tests.Integration/Extensions/ApplicationBuilderExtensions.cs b/src/Umbraco.Tests.Integration/Extensions/ApplicationBuilderExtensions.cs new file mode 100644 index 0000000000..553aa4fe85 --- /dev/null +++ b/src/Umbraco.Tests.Integration/Extensions/ApplicationBuilderExtensions.cs @@ -0,0 +1,117 @@ +using System; +using System.Data.Common; +using System.Data.SqlClient; +using System.IO; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using NUnit.Framework; +using Umbraco.Configuration.Models; +using Umbraco.Core; +using Umbraco.Core.Configuration; +using Umbraco.Core.Logging; +using Umbraco.Core.Persistence; +using Umbraco.Tests.Integration.Testing; +using Umbraco.Tests.Testing; + +namespace Umbraco.Tests.Integration.Extensions +{ + public static class ApplicationBuilderExtensions + { + /// + /// Creates a LocalDb instance to use for the test + /// + /// + /// + /// + /// + public static IApplicationBuilder UseTestLocalDb(this IApplicationBuilder app, + string dbFilePath, + UmbracoIntegrationTest integrationTest) + { + // get the currently set db options + var testOptions = TestOptionAttributeBase.GetTestOptions(); + + if (testOptions.Database == UmbracoTestOptions.Database.None) + return app; + + // need to manually register this factory + DbProviderFactories.RegisterFactory(Constants.DbProviderNames.SqlServer, SqlClientFactory.Instance); + + if (!Directory.Exists(dbFilePath)) + Directory.CreateDirectory(dbFilePath); + + var db = UmbracoIntegrationTest.GetOrCreate(dbFilePath, + app.ApplicationServices.GetRequiredService(), + app.ApplicationServices.GetRequiredService(), + app.ApplicationServices.GetRequiredService()); + + switch (testOptions.Database) + { + case UmbracoTestOptions.Database.NewSchemaPerTest: + + // Add teardown callback + integrationTest.OnTestTearDown(() => db.Detach()); + + // New DB + Schema + db.AttachSchema(); + + // We must re-configure our current factory since attaching a new LocalDb from the pool changes connection strings + var dbFactory = app.ApplicationServices.GetRequiredService(); + if (!dbFactory.Configured) + { + dbFactory.Configure(db.ConnectionString, Umbraco.Core.Constants.DatabaseProviders.SqlServer); + } + + // In the case that we've initialized the schema, it means that we are installed so we'll want to ensure that + // the runtime state is configured correctly so we'll force update the configuration flag and re-run the + // runtime state checker. + // TODO: This wouldn't be required if we don't store the Umbraco version in config + + // right now we are an an 'Install' state + var runtimeState = (RuntimeState)app.ApplicationServices.GetRequiredService(); + Assert.AreEqual(RuntimeLevel.Install, runtimeState.Level); + + // dynamically change the config status + var umbVersion = app.ApplicationServices.GetRequiredService(); + var config = app.ApplicationServices.GetRequiredService(); + config[GlobalSettings.Prefix + "ConfigurationStatus"] = umbVersion.SemanticVersion.ToString(); + + // re-run the runtime level check + var profilingLogger = app.ApplicationServices.GetRequiredService(); + runtimeState.DetermineRuntimeLevel(dbFactory, profilingLogger); + + Assert.AreEqual(RuntimeLevel.Run, runtimeState.Level); + + break; + case UmbracoTestOptions.Database.NewEmptyPerTest: + + // Add teardown callback + integrationTest.OnTestTearDown(() => db.Detach()); + + db.AttachEmpty(); + + break; + case UmbracoTestOptions.Database.NewSchemaPerFixture: + + // Add teardown callback + integrationTest.OnFixtureTearDown(() => db.Detach()); + + break; + case UmbracoTestOptions.Database.NewEmptyPerFixture: + + // Add teardown callback + integrationTest.OnFixtureTearDown(() => db.Detach()); + + break; + default: + throw new ArgumentOutOfRangeException(nameof(testOptions), testOptions, null); + } + + return app; + } + + } + + +} diff --git a/src/Umbraco.Tests.Integration/Extensions/ServiceCollectionExtensions.cs b/src/Umbraco.Tests.Integration/Extensions/ServiceCollectionExtensions.cs new file mode 100644 index 0000000000..9dde20036b --- /dev/null +++ b/src/Umbraco.Tests.Integration/Extensions/ServiceCollectionExtensions.cs @@ -0,0 +1,23 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Umbraco.Tests.Integration.Implementations; + +namespace Umbraco.Tests.Integration.Extensions +{ + public static class ServiceCollectionExtensions + { + /// + /// These services need to be manually added because they do not get added by the generic host + /// + /// + /// + /// + public static void AddRequiredNetCoreServices(this IServiceCollection services, TestHelper testHelper, IWebHostEnvironment webHostEnvironment) + { + services.AddSingleton(x => testHelper.GetHttpContextAccessor()); + // the generic host does add IHostEnvironment but not this one because we are not actually in a web context + services.AddSingleton(x => webHostEnvironment); + } + } +} diff --git a/src/Umbraco.Tests.Integration/Implementations/ApplicationBuilderExtensions.cs b/src/Umbraco.Tests.Integration/Implementations/ApplicationBuilderExtensions.cs deleted file mode 100644 index abb22c0bd7..0000000000 --- a/src/Umbraco.Tests.Integration/Implementations/ApplicationBuilderExtensions.cs +++ /dev/null @@ -1,91 +0,0 @@ -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 NUnit.Framework; -using Umbraco.Configuration.Models; -using Umbraco.Core; -using Umbraco.Core.Configuration; -using Umbraco.Core.Logging; -using Umbraco.Core.Persistence; -using Umbraco.Tests.Integration.Testing; - -namespace Umbraco.Tests.Integration.Implementations -{ - public static class ApplicationBuilderExtensions - { - /// - /// Creates a LocalDb instance to use for the test - /// - /// - /// - /// - /// Default is true - meaning a brand new database is created for this test. If this is false it will try to - /// re-use an existing database that was already created as part of this test fixture. - /// // TODO Implement the 'false' behavior - /// - /// - /// Default is true - meaning a database schema will be created for this test if it's a new database. If this is false - /// it will just create an empty database. - /// - /// - public static IApplicationBuilder UseTestLocalDb(this IApplicationBuilder app, - string dbFilePath, - bool createNewDb = true, - bool initializeSchema = true) - { - // need to manually register this factory - DbProviderFactories.RegisterFactory(Constants.DbProviderNames.SqlServer, SqlClientFactory.Instance); - - if (!Directory.Exists(dbFilePath)) - Directory.CreateDirectory(dbFilePath); - - var db = LocalDbTestDatabase.Get(dbFilePath, - app.ApplicationServices.GetRequiredService(), - app.ApplicationServices.GetRequiredService(), - app.ApplicationServices.GetRequiredService()); - - if (initializeSchema) - { - // New DB + Schema - db.AttachSchema(); - - // In the case that we've initialized the schema, it means that we are installed so we'll want to ensure that - // the runtime state is configured correctly so we'll force update the configuration flag and re-run the - // runtime state checker. - // TODO: This wouldn't be required if we don't store the Umbraco version in config - - // right now we are an an 'Install' state - var runtimeState = (RuntimeState)app.ApplicationServices.GetRequiredService(); - Assert.AreEqual(RuntimeLevel.Install, runtimeState.Level); - - // dynamically change the config status - var umbVersion = app.ApplicationServices.GetRequiredService(); - var config = app.ApplicationServices.GetRequiredService(); - config[GlobalSettings.Prefix + "ConfigurationStatus"] = umbVersion.SemanticVersion.ToString(); - - // re-run the runtime level check - var dbFactory = app.ApplicationServices.GetRequiredService(); - var profilingLogger = app.ApplicationServices.GetRequiredService(); - runtimeState.DetermineRuntimeLevel(dbFactory, profilingLogger); - - Assert.AreEqual(RuntimeLevel.Run, runtimeState.Level); - - } - else - { - db.AttachEmpty(); - } - - return app; - } - - } - - -} diff --git a/src/Umbraco.Tests/Persistence/Repositories/AuditRepositoryTest.cs b/src/Umbraco.Tests.Integration/Persistence/Repositories/AuditRepositoryTest.cs similarity index 72% rename from src/Umbraco.Tests/Persistence/Repositories/AuditRepositoryTest.cs rename to src/Umbraco.Tests.Integration/Persistence/Repositories/AuditRepositoryTest.cs index 49e48e0a2f..e92653fb98 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/AuditRepositoryTest.cs +++ b/src/Umbraco.Tests.Integration/Persistence/Repositories/AuditRepositoryTest.cs @@ -1,29 +1,29 @@ using System.Linq; using NUnit.Framework; +using Umbraco.Core; using Umbraco.Core.Models; using Umbraco.Core.Persistence; using Umbraco.Core.Persistence.Dtos; using Umbraco.Core.Persistence.Repositories.Implement; using Umbraco.Core.Scoping; -using Umbraco.Tests.TestHelpers; +using Umbraco.Tests.Integration.Testing; using Umbraco.Tests.Testing; -using Umbraco.Core; -namespace Umbraco.Tests.Persistence.Repositories +namespace Umbraco.Tests.Integration.Persistence.Repositories { [TestFixture] [UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest, Logger = UmbracoTestOptions.Logger.Console)] - public class AuditRepositoryTest : TestWithDatabaseBase + public class AuditRepositoryTest : UmbracoIntegrationTest { [Test] public void Can_Add_Audit_Entry() { - var sp = TestObjects.GetScopeProvider(Logger); - using (var scope = sp.CreateScope()) + var sp = ScopeProvider; + using (var scope = ScopeProvider.CreateScope()) { - var repo = new AuditRepository((IScopeAccessor) sp, Logger); - repo.Save(new AuditItem(-1, AuditType.System, -1, ObjectTypes.GetName(UmbracoObjectTypes.Document), "This is a System audit trail")); + var repo = new AuditRepository((IScopeAccessor)sp, Logger); + repo.Save(new AuditItem(-1, AuditType.System, -1, UmbracoObjectTypes.Document.GetName(), "This is a System audit trail")); var dtos = scope.Database.Fetch("WHERE id > -1"); @@ -35,15 +35,15 @@ namespace Umbraco.Tests.Persistence.Repositories [Test] public void Get_Paged_Items() { - var sp = TestObjects.GetScopeProvider(Logger); + var sp = ScopeProvider; using (var scope = sp.CreateScope()) { - var repo = new AuditRepository((IScopeAccessor) sp, Logger); + var repo = new AuditRepository((IScopeAccessor)sp, Logger); for (var i = 0; i < 100; i++) { - repo.Save(new AuditItem(i, AuditType.New, -1, ObjectTypes.GetName(UmbracoObjectTypes.Document), $"Content {i} created")); - repo.Save(new AuditItem(i, AuditType.Publish, -1, ObjectTypes.GetName(UmbracoObjectTypes.Document), $"Content {i} published")); + repo.Save(new AuditItem(i, AuditType.New, -1, UmbracoObjectTypes.Document.GetName(), $"Content {i} created")); + repo.Save(new AuditItem(i, AuditType.Publish, -1, UmbracoObjectTypes.Document.GetName(), $"Content {i} published")); } scope.Complete(); @@ -51,7 +51,7 @@ namespace Umbraco.Tests.Persistence.Repositories using (var scope = sp.CreateScope()) { - var repo = new AuditRepository((IScopeAccessor) sp, Logger); + var repo = new AuditRepository((IScopeAccessor)sp, Logger); var page = repo.GetPagedResultsByQuery(sp.SqlContext.Query(), 0, 10, out var total, Direction.Descending, null, null); @@ -63,15 +63,15 @@ namespace Umbraco.Tests.Persistence.Repositories [Test] public void Get_Paged_Items_By_User_Id_With_Query_And_Filter() { - var sp = TestObjects.GetScopeProvider(Logger); + var sp = ScopeProvider; using (var scope = sp.CreateScope()) { var repo = new AuditRepository((IScopeAccessor)sp, Logger); for (var i = 0; i < 100; i++) { - repo.Save(new AuditItem(i, AuditType.New, -1, ObjectTypes.GetName(UmbracoObjectTypes.Document), $"Content {i} created")); - repo.Save(new AuditItem(i, AuditType.Publish, -1, ObjectTypes.GetName(UmbracoObjectTypes.Document), $"Content {i} published")); + repo.Save(new AuditItem(i, AuditType.New, -1, UmbracoObjectTypes.Document.GetName(), $"Content {i} created")); + repo.Save(new AuditItem(i, AuditType.Publish, -1, UmbracoObjectTypes.Document.GetName(), $"Content {i} published")); } scope.Complete(); @@ -106,15 +106,15 @@ namespace Umbraco.Tests.Persistence.Repositories [Test] public void Get_Paged_Items_With_AuditType_Filter() { - var sp = TestObjects.GetScopeProvider(Logger); + var sp = ScopeProvider; using (var scope = sp.CreateScope()) { - var repo = new AuditRepository((IScopeAccessor) sp, Logger); + var repo = new AuditRepository((IScopeAccessor)sp, Logger); for (var i = 0; i < 100; i++) { - repo.Save(new AuditItem(i, AuditType.New, -1, ObjectTypes.GetName(UmbracoObjectTypes.Document), $"Content {i} created")); - repo.Save(new AuditItem(i, AuditType.Publish, -1, ObjectTypes.GetName(UmbracoObjectTypes.Document), $"Content {i} published")); + repo.Save(new AuditItem(i, AuditType.New, -1, UmbracoObjectTypes.Document.GetName(), $"Content {i} created")); + repo.Save(new AuditItem(i, AuditType.Publish, -1, UmbracoObjectTypes.Document.GetName(), $"Content {i} published")); } scope.Complete(); @@ -122,10 +122,10 @@ namespace Umbraco.Tests.Persistence.Repositories using (var scope = sp.CreateScope()) { - var repo = new AuditRepository((IScopeAccessor) sp, Logger); + var repo = new AuditRepository((IScopeAccessor)sp, Logger); var page = repo.GetPagedResultsByQuery(sp.SqlContext.Query(), 0, 9, out var total, Direction.Descending, - new[] {AuditType.Publish}, null) + new[] { AuditType.Publish }, null) .ToArray(); Assert.AreEqual(9, page.Length); @@ -137,15 +137,15 @@ namespace Umbraco.Tests.Persistence.Repositories [Test] public void Get_Paged_Items_With_Custom_Filter() { - var sp = TestObjects.GetScopeProvider(Logger); + var sp = ScopeProvider; using (var scope = sp.CreateScope()) { - var repo = new AuditRepository((IScopeAccessor) sp, Logger); + var repo = new AuditRepository((IScopeAccessor)sp, Logger); for (var i = 0; i < 100; i++) { - repo.Save(new AuditItem(i, AuditType.New, -1, ObjectTypes.GetName(UmbracoObjectTypes.Document), "Content created")); - repo.Save(new AuditItem(i, AuditType.Publish, -1, ObjectTypes.GetName(UmbracoObjectTypes.Document), "Content published")); + repo.Save(new AuditItem(i, AuditType.New, -1, UmbracoObjectTypes.Document.GetName(), "Content created")); + repo.Save(new AuditItem(i, AuditType.Publish, -1, UmbracoObjectTypes.Document.GetName(), "Content published")); } scope.Complete(); @@ -153,7 +153,7 @@ namespace Umbraco.Tests.Persistence.Repositories using (var scope = sp.CreateScope()) { - var repo = new AuditRepository((IScopeAccessor) sp, Logger); + var repo = new AuditRepository((IScopeAccessor)sp, Logger); var page = repo.GetPagedResultsByQuery(sp.SqlContext.Query(), 0, 8, out var total, Direction.Descending, null, sp.SqlContext.Query().Where(item => item.Comment == "Content created")) diff --git a/src/Umbraco.Tests.Integration/RuntimeTests.cs b/src/Umbraco.Tests.Integration/RuntimeTests.cs index 814687eb7f..210f767de1 100644 --- a/src/Umbraco.Tests.Integration/RuntimeTests.cs +++ b/src/Umbraco.Tests.Integration/RuntimeTests.cs @@ -1,30 +1,21 @@ -using LightInject; -using LightInject.Microsoft.DependencyInjection; -using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Moq; using NUnit.Framework; -using System.Collections.Generic; using System.IO; using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; -using Umbraco.Configuration.Models; using Umbraco.Core; using Umbraco.Core.Composing; -using Umbraco.Core.Composing.LightInject; -using Umbraco.Core.Configuration; using Umbraco.Core.Logging; -using Umbraco.Core.Migrations.Install; -using Umbraco.Core.Persistence; using Umbraco.Core.Runtime; using Umbraco.Tests.Common; +using Umbraco.Tests.Integration.Extensions; using Umbraco.Tests.Integration.Implementations; using Umbraco.Tests.Integration.Testing; using Umbraco.Web.BackOffice.AspNetCore; -using static Umbraco.Core.Migrations.Install.DatabaseBuilder; namespace Umbraco.Tests.Integration { @@ -39,10 +30,11 @@ namespace Umbraco.Tests.Integration MyComposer.Reset(); } - [OneTimeTearDown] - public void FixtureTearDown() + [SetUp] + public void Setup() { - + MyComponent.Reset(); + MyComposer.Reset(); } /// @@ -95,7 +87,7 @@ namespace Umbraco.Tests.Integration [Test] public async Task AddUmbracoCore() { - var umbracoContainer = GetUmbracoContainer(out var serviceProviderFactory); + var umbracoContainer = UmbracoIntegrationTest.GetUmbracoContainer(out var serviceProviderFactory); var testHelper = new TestHelper(); var hostBuilder = new HostBuilder() @@ -103,7 +95,7 @@ namespace Umbraco.Tests.Integration .ConfigureServices((hostContext, services) => { var webHostEnvironment = testHelper.GetWebHostEnvironment(); - AddRequiredNetCoreServices(services, testHelper, webHostEnvironment); + services.AddRequiredNetCoreServices(testHelper, webHostEnvironment); // Add it! services.AddUmbracoConfiguration(hostContext.Configuration); @@ -134,7 +126,7 @@ namespace Umbraco.Tests.Integration [Test] public async Task UseUmbracoCore() { - var umbracoContainer = GetUmbracoContainer(out var serviceProviderFactory); + var umbracoContainer = UmbracoIntegrationTest.GetUmbracoContainer(out var serviceProviderFactory); var testHelper = new TestHelper(); var hostBuilder = new HostBuilder() @@ -142,7 +134,7 @@ namespace Umbraco.Tests.Integration .ConfigureServices((hostContext, services) => { var webHostEnvironment = testHelper.GetWebHostEnvironment(); - AddRequiredNetCoreServices(services, testHelper, webHostEnvironment); + services.AddRequiredNetCoreServices(testHelper, webHostEnvironment); // Add it! services.AddUmbracoConfiguration(hostContext.Configuration); @@ -169,56 +161,6 @@ namespace Umbraco.Tests.Integration Assert.IsTrue(MyComponent.IsTerminated); } - [Test] - public async Task Install_Database() - { - var umbracoContainer = GetUmbracoContainer(out var serviceProviderFactory); - var testHelper = new TestHelper(); - - var hostBuilder = new HostBuilder() - .UseUmbraco(serviceProviderFactory) - .ConfigureServices((hostContext, services) => - { - var webHostEnvironment = testHelper.GetWebHostEnvironment(); - AddRequiredNetCoreServices(services, testHelper, webHostEnvironment); - - // Add it! - services.AddUmbracoConfiguration(hostContext.Configuration); - services.AddUmbracoCore(webHostEnvironment, umbracoContainer, GetType().Assembly); - }); - - var host = await hostBuilder.StartAsync(); - var app = new ApplicationBuilder(host.Services); - - // This will create a db, install the schema and ensure the app is configured to run - app.UseTestLocalDb(Path.Combine(testHelper.CurrentAssemblyDirectory, "LocalDb")); - - app.UseUmbracoCore(); - - var runtimeState = app.ApplicationServices.GetRequiredService(); - Assert.AreEqual(RuntimeLevel.Run, runtimeState.Level); - } - - internal static LightInjectContainer GetUmbracoContainer(out UmbracoServiceProviderFactory serviceProviderFactory) - { - var container = UmbracoServiceProviderFactory.CreateServiceContainer(); - serviceProviderFactory = new UmbracoServiceProviderFactory(container); - var umbracoContainer = serviceProviderFactory.GetContainer(); - return umbracoContainer; - } - - /// - /// These services need to be manually added because they do not get added by the generic host - /// - /// - /// - /// - private void AddRequiredNetCoreServices(IServiceCollection services, TestHelper testHelper, IWebHostEnvironment webHostEnvironment) - { - services.AddSingleton(x => testHelper.GetHttpContextAccessor()); - // the generic host does add IHostEnvironment but not this one because we are not actually in a web context - services.AddSingleton(x => webHostEnvironment); - } [RuntimeLevel(MinLevel = RuntimeLevel.Install)] public class MyComposer : IUserComposer diff --git a/src/Umbraco.Tests.Integration/Testing/LocalDbTestDatabase.cs b/src/Umbraco.Tests.Integration/Testing/LocalDbTestDatabase.cs index 63b336701c..da2b83bc39 100644 --- a/src/Umbraco.Tests.Integration/Testing/LocalDbTestDatabase.cs +++ b/src/Umbraco.Tests.Integration/Testing/LocalDbTestDatabase.cs @@ -17,16 +17,8 @@ namespace Umbraco.Tests.Integration.Testing /// /// Manages a pool of LocalDb databases for integration testing /// - internal class LocalDbTestDatabase + public class LocalDbTestDatabase { - public static LocalDbTestDatabase Get(string filesPath, ILogger logger, IGlobalSettings globalSettings, IUmbracoDatabaseFactory dbFactory) - { - var localDb = new LocalDb(); - if (localDb.IsAvailable == false) - throw new InvalidOperationException("LocalDB is not available."); - return new LocalDbTestDatabase(logger, globalSettings, localDb, filesPath, dbFactory); - } - public const string InstanceName = "UmbracoTests"; public const string DatabaseName = "UmbracoTests"; @@ -43,7 +35,8 @@ namespace Umbraco.Tests.Integration.Testing private static DatabasePool _schemaPool; private DatabasePool _currentPool; - public LocalDbTestDatabase(ILogger logger, IGlobalSettings globalSettings, LocalDb localDb, string filesPath, IUmbracoDatabaseFactory dbFactory) + //It's internal because `Umbraco.Core.Persistence.LocalDb` is internal + internal LocalDbTestDatabase(ILogger logger, IGlobalSettings globalSettings, LocalDb localDb, string filesPath, IUmbracoDatabaseFactory dbFactory) { _umbracoVersion = new UmbracoVersion(); _logger = logger; @@ -328,5 +321,6 @@ namespace Umbraco.Tests.Integration.Testing while (_readyQueue.TryTake(out i)) { } } } + } } diff --git a/src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs b/src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs new file mode 100644 index 0000000000..90a6e7bdf8 --- /dev/null +++ b/src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs @@ -0,0 +1,150 @@ +using System; +using System.Collections.Generic; +using System.Data.Common; +using System.IO; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using NUnit.Framework; +using Umbraco.Core.Composing; +using Umbraco.Core.Composing.LightInject; +using Umbraco.Core.Configuration; +using Umbraco.Core.Logging; +using Umbraco.Core.Persistence; +using Umbraco.Core.Scoping; +using Umbraco.Tests.Integration.Extensions; +using Umbraco.Tests.Integration.Implementations; +using Umbraco.Tests.Testing; +using Umbraco.Web.BackOffice.AspNetCore; + +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 GetUmbracoContainer(out UmbracoServiceProviderFactory serviceProviderFactory) + { + var container = UmbracoServiceProviderFactory.CreateServiceContainer(); + serviceProviderFactory = new UmbracoServiceProviderFactory(container); + var umbracoContainer = serviceProviderFactory.GetContainer(); + return umbracoContainer; + } + + /// + /// Get or create an instance of + /// + /// + /// + /// + /// + /// + /// + /// There must only be ONE instance shared between all tests in a session + /// + public static LocalDbTestDatabase GetOrCreate(string filesPath, ILogger logger, IGlobalSettings globalSettings, 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, globalSettings, localDb, filesPath, dbFactory); + return _dbInstance; + } + } + + private static readonly object _dbLocker = new object(); + private static LocalDbTestDatabase _dbInstance; + + private readonly List _testTeardown = new List(); + private readonly List _fixtureTeardown = new List(); + + public void OnTestTearDown(Action tearDown) + { + _testTeardown.Add(tearDown); + } + + public void OnFixtureTearDown(Action tearDown) + { + _fixtureTeardown.Add(tearDown); + } + + [OneTimeTearDown] + public void FixtureTearDown() + { + // call all registered callbacks + foreach (var action in _fixtureTeardown) + { + action(); + } + } + + [TearDown] + public void TearDown() + { + // call all registered callbacks + foreach (var action in _testTeardown) + { + action(); + } + } + + [SetUp] + public async Task Setup() + { + var umbracoContainer = GetUmbracoContainer(out var serviceProviderFactory); + var testHelper = new TestHelper(); + + var hostBuilder = new HostBuilder() + .UseUmbraco(serviceProviderFactory) + .ConfigureServices((hostContext, services) => + { + var webHostEnvironment = testHelper.GetWebHostEnvironment(); + services.AddRequiredNetCoreServices(testHelper, webHostEnvironment); + + // Add it! + services.AddUmbracoConfiguration(hostContext.Configuration); + services.AddUmbracoCore(webHostEnvironment, umbracoContainer, GetType().Assembly); + }); + + var host = await hostBuilder.StartAsync(); + var app = new ApplicationBuilder(host.Services); + Services = app.ApplicationServices; + + // This will create a db, install the schema and ensure the app is configured to run + app.UseTestLocalDb(Path.Combine(testHelper.CurrentAssemblyDirectory, "LocalDb"), this); + + app.UseUmbracoCore(); + } + + /// + /// Returns the DI container + /// + protected IServiceProvider Services { get; private set; } + + /// + /// Returns the + /// + protected IScopeProvider ScopeProvider => Services.GetRequiredService(); + + /// + /// Returns the + /// + protected IScopeAccessor ScopeAccessor => Services.GetRequiredService(); + + /// + /// Returns the + /// + protected ILogger Logger => Services.GetRequiredService(); + } +} diff --git a/src/Umbraco.Tests/TestHelpers/TestObjects.cs b/src/Umbraco.Tests/TestHelpers/TestObjects.cs index fa037fbb50..4609d8a59c 100644 --- a/src/Umbraco.Tests/TestHelpers/TestObjects.cs +++ b/src/Umbraco.Tests/TestHelpers/TestObjects.cs @@ -252,8 +252,8 @@ namespace Umbraco.Tests.TestHelpers TestHelper.DbProviderFactoryCreator); } - typeFinder = typeFinder ?? new TypeFinder(logger, new DefaultUmbracoAssemblyProvider(GetType().Assembly)); - fileSystems = fileSystems ?? new FileSystems(Current.Factory, logger, TestHelper.IOHelper, SettingsForTests.GenerateMockGlobalSettings()); + typeFinder ??= new TypeFinder(logger, new DefaultUmbracoAssemblyProvider(GetType().Assembly)); + fileSystems ??= new FileSystems(Current.Factory, logger, TestHelper.IOHelper, SettingsForTests.GenerateMockGlobalSettings()); var coreDebug = TestHelper.CoreDebugSettings; var mediaFileSystem = Mock.Of(); var scopeProvider = new ScopeProvider(databaseFactory, fileSystems, coreDebug, mediaFileSystem, logger, typeFinder, NoAppCache.Instance); diff --git a/src/Umbraco.Tests/Testing/UmbracoTestOptions.cs b/src/Umbraco.Tests/Testing/UmbracoTestOptions.cs deleted file mode 100644 index da3ffccc55..0000000000 --- a/src/Umbraco.Tests/Testing/UmbracoTestOptions.cs +++ /dev/null @@ -1,39 +0,0 @@ -namespace Umbraco.Tests.Testing -{ - public static class UmbracoTestOptions - { - public enum Logger - { - // pure mocks - Mock, - // Serilog for tests - Serilog, - // console logger - Console - } - - public enum Database - { - // no database - None, - // new empty database file for the entire feature - NewEmptyPerFixture, - // new empty database file per test - NewEmptyPerTest, - // new database file with schema for the entire feature - NewSchemaPerFixture, - // new database file with schema per test - NewSchemaPerTest - } - - public enum TypeLoader - { - // the default, global type loader for tests - Default, - // create one type loader for the feature - PerFixture, - // create one type loader for each test - PerTest - } - } -} diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index 06f6a98573..f667b11aa3 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -231,11 +231,8 @@ - - - @@ -267,7 +264,6 @@ - From 7617027c499518778ae5040e1737e5dae2ab77e5 Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 30 Mar 2020 21:53:30 +1100 Subject: [PATCH 3/3] Moves most of the UserRepositoryTest --- .../Implement/UserGroupRepository.cs | 2 +- .../Builders/UserBuilder.cs | 106 +++++ .../Builders/UserGroupBuilder.cs | 65 +++ .../Repositories/UserRepositoryTest.cs | 393 ++++++++++++++++++ .../Testing/UmbracoIntegrationTest.cs | 23 + .../Repositories/UserRepositoryTest.cs | 343 +-------------- .../TestHelpers/TestObjects-Mocks.cs | 5 - src/Umbraco.Tests/Umbraco.Tests.csproj | 2 +- 8 files changed, 591 insertions(+), 348 deletions(-) create mode 100644 src/Umbraco.Tests.Common/Builders/UserBuilder.cs create mode 100644 src/Umbraco.Tests.Common/Builders/UserGroupBuilder.cs create mode 100644 src/Umbraco.Tests.Integration/Persistence/Repositories/UserRepositoryTest.cs diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserGroupRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserGroupRepository.cs index 07cfbb05a8..863f3dc455 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserGroupRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserGroupRepository.cs @@ -319,7 +319,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement private void PersistAllowedSections(IUserGroup entity) { - var userGroup = (UserGroup) entity; + var userGroup = entity; // First delete all Database.Delete("WHERE UserGroupId = @UserGroupId", new { UserGroupId = userGroup.Id }); diff --git a/src/Umbraco.Tests.Common/Builders/UserBuilder.cs b/src/Umbraco.Tests.Common/Builders/UserBuilder.cs new file mode 100644 index 0000000000..07ab8ef3f7 --- /dev/null +++ b/src/Umbraco.Tests.Common/Builders/UserBuilder.cs @@ -0,0 +1,106 @@ +using System; +using System.Linq; +using Moq; +using Umbraco.Core; +using Umbraco.Core.Configuration; +using Umbraco.Core.Models.Membership; +using Umbraco.Tests.Common.Builders.Interfaces; + +namespace Umbraco.Tests.Common.Builders +{ + public class UserBuilder + : BuilderBase, + IWithIdBuilder + { + private int? _id; + private string _language; + private bool? _approved; + private string _name; + private string _rawPassword; + private bool? _isLockedOut; + private string _email; + private string _username; + private string _defaultLang; + private string _suffix = string.Empty; + + public UserBuilder WithDefaultUILanguage(string defaultLang) + { + _defaultLang = defaultLang; + return this; + } + + public UserBuilder WithLanguage(string language) + { + _language = language; + return this; + } + + public UserBuilder WithApproved(bool approved) + { + _approved = approved; + return this; + } + + public UserBuilder WithRawPassword(string rawPassword) + { + _rawPassword = rawPassword; + return this; + } + + public UserBuilder WithEmail(string email) + { + _email = email; + return this; + } + + public UserBuilder WithUsername(string username) + { + _username = username; + return this; + } + + public UserBuilder WithLockedOut(bool isLockedOut) + { + _isLockedOut = isLockedOut; + return this; + } + + public UserBuilder WithName(string name) + { + _name = name; + return this; + } + + /// + /// Will suffix the name, email and username for testing + /// + /// + /// + public UserBuilder WithSuffix(string suffix) + { + _suffix = suffix; + return this; + } + + public override User Build() + { + var globalSettings = Mock.Of(x => x.DefaultUILanguage == (_defaultLang ?? "en-US")); + return new User(globalSettings, + _name ?? "TestUser" + _suffix, + _email ?? "test" + _suffix + "@test.com", + _username ?? "TestUser" + _suffix, + _rawPassword ?? "abcdefghijklmnopqrstuvwxyz") + { + Language = _language ?? _defaultLang ?? "en-US", + IsLockedOut = _isLockedOut ?? false, + IsApproved = _approved ?? true + }; + } + + int? IWithIdBuilder.Id + { + get => _id; + set => _id = value; + } + } +} diff --git a/src/Umbraco.Tests.Common/Builders/UserGroupBuilder.cs b/src/Umbraco.Tests.Common/Builders/UserGroupBuilder.cs new file mode 100644 index 0000000000..d3ce5e71a8 --- /dev/null +++ b/src/Umbraco.Tests.Common/Builders/UserGroupBuilder.cs @@ -0,0 +1,65 @@ +using System.Collections.Generic; +using System.Linq; +using Moq; +using Umbraco.Core.Models.Membership; +using Umbraco.Tests.Common.Builders.Interfaces; + +namespace Umbraco.Tests.Common.Builders +{ + public class UserGroupBuilder + : BuilderBase, + IWithIdBuilder + { + private int? _startContentId; + private int? _startMediaId; + private string _alias; + private string _icon; + private string _name; + private IEnumerable _permissions = Enumerable.Empty(); + private IEnumerable _sectionCollection = Enumerable.Empty(); + private string _suffix; + private int? _id; + + /// + /// Will suffix the name and alias for testing + /// + /// + /// + public UserGroupBuilder WithSuffix(string suffix) + { + _suffix = suffix; + return this; + } + + public IReadOnlyUserGroup BuildReadOnly(IUserGroup userGroup) + { + return Mock.Of(x => + x.Permissions == userGroup.Permissions && + x.Alias == userGroup.Alias && + x.Icon == userGroup.Icon && + x.Name == userGroup.Name && + x.StartContentId == userGroup.StartContentId && + x.StartMediaId == userGroup.StartMediaId && + x.AllowedSections == userGroup.AllowedSections && + x.Id == userGroup.Id); + } + + public override IUserGroup Build() + { + return Mock.Of(x => + x.StartContentId == _startContentId && + x.StartMediaId == _startMediaId && + x.Name == (_name ?? ("TestUserGroup" + _suffix)) && + x.Alias == (_alias ?? ("testUserGroup" + _suffix)) && + x.Icon == _icon && + x.Permissions == _permissions && + x.AllowedSections == _sectionCollection); + } + + int? IWithIdBuilder.Id + { + get => _id; + set => _id = value; + } + } +} diff --git a/src/Umbraco.Tests.Integration/Persistence/Repositories/UserRepositoryTest.cs b/src/Umbraco.Tests.Integration/Persistence/Repositories/UserRepositoryTest.cs new file mode 100644 index 0000000000..7feb69f0c6 --- /dev/null +++ b/src/Umbraco.Tests.Integration/Persistence/Repositories/UserRepositoryTest.cs @@ -0,0 +1,393 @@ +using System.Linq; +using Moq; +using NUnit.Framework; +using Umbraco.Core; +using Umbraco.Core.Cache; +using Umbraco.Core.Logging; +using Umbraco.Core.Models.Membership; +using Umbraco.Core.Persistence.Mappers; +using Umbraco.Core.Persistence.Repositories; +using Umbraco.Core.Persistence.Repositories.Implement; +using Umbraco.Core.Scoping; +using Umbraco.Tests.Testing; +using Umbraco.Core.Persistence; +using Umbraco.Core.PropertyEditors; +using System; +using Umbraco.Core.Configuration; +using Umbraco.Core.Services.Implement; +using Umbraco.Tests.Integration.Testing; + +namespace Umbraco.Tests.Persistence.Repositories +{ + [TestFixture] + [UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest, WithApplication = true, Logger = UmbracoTestOptions.Logger.Console)] + public class UserRepositoryTest : UmbracoIntegrationTest + { + private UserRepository CreateRepository(IScopeProvider provider) + { + var accessor = (IScopeAccessor) provider; + var repository = new UserRepository(accessor, AppCaches.Disabled, Logger, Mappers, GlobalSettings, Mock.Of()); + return repository; + } + + private UserGroupRepository CreateUserGroupRepository(IScopeProvider provider) + { + var accessor = (IScopeAccessor) provider; + return new UserGroupRepository(accessor, AppCaches.Disabled, Logger, ShortStringHelper); + } + + [Test] + public void Can_Perform_Add_On_UserRepository() + { + // Arrange + var provider = ScopeProvider; + using (var scope = provider.CreateScope()) + { + var repository = CreateRepository(provider); + + var user = UserBuilder.Build(); + + // Act + repository.Save(user); + + + // Assert + Assert.That(user.HasIdentity, Is.True); + } + } + + [Test] + public void Can_Perform_Multiple_Adds_On_UserRepository() + { + // Arrange + var provider = ScopeProvider; + using (var scope = provider.CreateScope()) + { + var repository = CreateRepository(provider); + + var user1 = UserBuilder.WithSuffix("1").Build(); + var use2 = UserBuilder.WithSuffix("2").Build(); + + // Act + repository.Save(user1); + + repository.Save(use2); + + + // Assert + Assert.That(user1.HasIdentity, Is.True); + Assert.That(use2.HasIdentity, Is.True); + } + } + + [Test] + public void Can_Verify_Fresh_Entity_Is_Not_Dirty() + { + // Arrange + var provider = ScopeProvider; + using (var scope = provider.CreateScope()) + { + var repository = CreateRepository(provider); + + var user = UserBuilder.Build(); + repository.Save(user); + + + // Act + var resolved = repository.Get((int)user.Id); + bool dirty = ((User)resolved).IsDirty(); + + // Assert + Assert.That(dirty, Is.False); + } + } + + [Test] + public void Can_Perform_Delete_On_UserRepository() + { + // Arrange + var provider = ScopeProvider; + using (var scope = provider.CreateScope()) + { + var repository = CreateRepository(provider); + + var user = UserBuilder.Build(); + + // Act + repository.Save(user); + + var id = user.Id; + + var repository2 = new UserRepository((IScopeAccessor) provider, AppCaches.Disabled, Logger, Mock.Of(),GlobalSettings, Mock.Of()); + + repository2.Delete(user); + + + var resolved = repository2.Get((int) id); + + // Assert + Assert.That(resolved, Is.Null); + } + } + + [Test] + public void Can_Perform_Get_On_UserRepository() + { + // Arrange + var provider = ScopeProvider; + using (var scope = provider.CreateScope()) + { + var repository = CreateRepository(provider); + var userGroupRepository = CreateUserGroupRepository(provider); + + var user = CreateAndCommitUserWithGroup(repository, userGroupRepository); + + // Act + var updatedItem = repository.Get(user.Id); + + // FIXME: this test cannot work, user has 2 sections but the way it's created, + // they don't show, so the comparison with updatedItem fails - fix! + + // Assert + AssertPropertyValues(updatedItem, user); + } + } + + [Test] + public void Can_Perform_GetByQuery_On_UserRepository() + { + // Arrange + var provider = ScopeProvider; + using (var scope = provider.CreateScope()) + { + var repository = CreateRepository(provider); + + CreateAndCommitMultipleUsers(repository); + + // Act + var query = scope.SqlContext.Query().Where(x => x.Username == "TestUser1"); + var result = repository.Get(query); + + // Assert + Assert.That(result.Count(), Is.GreaterThanOrEqualTo(1)); + } + } + + [Test] + public void Can_Perform_GetAll_By_Param_Ids_On_UserRepository() + { + // Arrange + var provider = ScopeProvider; + using (var scope = provider.CreateScope()) + { + var repository = CreateRepository(provider); + + var users = CreateAndCommitMultipleUsers(repository); + + // Act + var result = repository.GetMany((int) users[0].Id, (int) users[1].Id); + + // Assert + Assert.That(result, Is.Not.Null); + Assert.That(result.Any(), Is.True); + Assert.That(result.Count(), Is.EqualTo(2)); + } + } + + [Test] + public void Can_Perform_GetAll_On_UserRepository() + { + // Arrange + var provider = ScopeProvider; + using (var scope = provider.CreateScope()) + { + var repository = CreateRepository(provider); + + CreateAndCommitMultipleUsers(repository); + + // Act + var result = repository.GetMany(); + + // Assert + Assert.That(result, Is.Not.Null); + Assert.That(result.Any(), Is.True); + Assert.That(result.Count(), Is.GreaterThanOrEqualTo(3)); + } + } + + [Test] + public void Can_Perform_Exists_On_UserRepository() + { + // Arrange + var provider = ScopeProvider; + using (var scope = provider.CreateScope()) + { + var repository = CreateRepository(provider); + + var users = CreateAndCommitMultipleUsers(repository); + + // Act + var exists = repository.Exists(users[0].Id); + + // Assert + Assert.That(exists, Is.True); + } + } + + [Test] + public void Can_Perform_Count_On_UserRepository() + { + // Arrange + var provider = ScopeProvider; + using (var scope = provider.CreateScope()) + { + var repository = CreateRepository(provider); + + var users = CreateAndCommitMultipleUsers(repository); + + // Act + var query = scope.SqlContext.Query().Where(x => x.Username == "TestUser1" || x.Username == "TestUser2"); + var result = repository.Count(query); + + // Assert + Assert.AreEqual(2, result); + } + } + + [Test] + public void Can_Get_Paged_Results_By_Query_And_Filter_And_Groups() + { + var provider = ScopeProvider; + using (var scope = provider.CreateScope()) + { + var repository = CreateRepository(provider); + + var users = CreateAndCommitMultipleUsers(repository); + var query = provider.SqlContext.Query().Where(x => x.Username == "TestUser1" || x.Username == "TestUser2"); + + try + { + scope.Database.AsUmbracoDatabase().EnableSqlTrace = true; + scope.Database.AsUmbracoDatabase().EnableSqlCount = true; + + // Act + var result = repository.GetPagedResultsByQuery(query, 0, 10, out var totalRecs, user => user.Id, Direction.Ascending, + excludeUserGroups: new[] { Constants.Security.TranslatorGroupAlias }, + filter: provider.SqlContext.Query().Where(x => x.Id > -1)); + + // Assert + Assert.AreEqual(2, totalRecs); + } + finally + { + scope.Database.AsUmbracoDatabase().EnableSqlTrace = false; + scope.Database.AsUmbracoDatabase().EnableSqlCount = false; + } + } + + } + + [Test] + public void Can_Get_Paged_Results_With_Filter_And_Groups() + { + var provider = ScopeProvider; + using (var scope = provider.CreateScope()) + { + var repository = CreateRepository(provider); + + var users = CreateAndCommitMultipleUsers(repository); + + try + { + scope.Database.AsUmbracoDatabase().EnableSqlTrace = true; + scope.Database.AsUmbracoDatabase().EnableSqlCount = true; + + // Act + var result = repository.GetPagedResultsByQuery(null, 0, 10, out var totalRecs, user => user.Id, Direction.Ascending, + includeUserGroups: new[] { Constants.Security.AdminGroupAlias, Constants.Security.SensitiveDataGroupAlias }, + excludeUserGroups: new[] { Constants.Security.TranslatorGroupAlias }, + filter: provider.SqlContext.Query().Where(x => x.Id == -1)); + + // Assert + Assert.AreEqual(1, totalRecs); + } + finally + { + scope.Database.AsUmbracoDatabase().EnableSqlTrace = false; + scope.Database.AsUmbracoDatabase().EnableSqlCount = false; + } + } + } + + [Test] + public void Can_Invalidate_SecurityStamp_On_Username_Change() + { + // Arrange + var provider = ScopeProvider; + using (var scope = provider.CreateScope()) + { + var repository = CreateRepository(provider); + var userGroupRepository = CreateUserGroupRepository(provider); + + var user = CreateAndCommitUserWithGroup(repository, userGroupRepository); + var originalSecurityStamp = user.SecurityStamp; + + // Ensure when user generated a security stamp is present + Assert.That(user.SecurityStamp, Is.Not.Null); + Assert.That(user.SecurityStamp, Is.Not.Empty); + + // Update username + user.Username = user.Username + "UPDATED"; + repository.Save(user); + + // Get the user + var updatedUser = repository.Get(user.Id); + + // Ensure the Security Stamp is invalidated & no longer the same + Assert.AreNotEqual(originalSecurityStamp, updatedUser.SecurityStamp); + } + } + + private void AssertPropertyValues(IUser updatedItem, IUser originalUser) + { + Assert.That(updatedItem.Id, Is.EqualTo(originalUser.Id)); + Assert.That(updatedItem.Name, Is.EqualTo(originalUser.Name)); + Assert.That(updatedItem.Language, Is.EqualTo(originalUser.Language)); + Assert.That(updatedItem.IsApproved, Is.EqualTo(originalUser.IsApproved)); + Assert.That(updatedItem.RawPasswordValue, Is.EqualTo(originalUser.RawPasswordValue)); + Assert.That(updatedItem.IsLockedOut, Is.EqualTo(originalUser.IsLockedOut)); + Assert.IsTrue(updatedItem.StartContentIds.UnsortedSequenceEqual(originalUser.StartContentIds)); + Assert.IsTrue(updatedItem.StartMediaIds.UnsortedSequenceEqual(originalUser.StartMediaIds)); + Assert.That(updatedItem.Email, Is.EqualTo(originalUser.Email)); + Assert.That(updatedItem.Username, Is.EqualTo(originalUser.Username)); + Assert.That(updatedItem.AllowedSections.Count(), Is.EqualTo(originalUser.AllowedSections.Count())); + foreach (var allowedSection in originalUser.AllowedSections) + Assert.IsTrue(updatedItem.AllowedSections.Contains(allowedSection)); + } + + private User CreateAndCommitUserWithGroup(IUserRepository repository, IUserGroupRepository userGroupRepository) + { + var user = UserBuilder.Build(); + repository.Save(user); + + + var group = UserGroupBuilder.Build(); + userGroupRepository.AddOrUpdateGroupWithUsers(@group, new[] { user.Id }); + + user.AddGroup(UserGroupBuilder.BuildReadOnly(group)); + + return user; + } + + private IUser[] CreateAndCommitMultipleUsers(IUserRepository repository) + { + var user1 = UserBuilder.WithSuffix("1").Build(); + var user2 = UserBuilder.WithSuffix("2").Build(); + var user3 = UserBuilder.WithSuffix("3").Build(); + repository.Save(user1); + repository.Save(user2); + repository.Save(user3); + return new IUser[] { user1, user2, user3 }; + } + } +} diff --git a/src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs b/src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs index 90a6e7bdf8..3d94e52860 100644 --- a/src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs +++ b/src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs @@ -7,12 +7,17 @@ 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.Tests.Testing; @@ -127,6 +132,8 @@ namespace Umbraco.Tests.Integration.Testing app.UseUmbracoCore(); } + #region Common services + /// /// Returns the DI container /// @@ -146,5 +153,21 @@ namespace Umbraco.Tests.Integration.Testing /// Returns the /// protected ILogger Logger => Services.GetRequiredService(); + + protected AppCaches AppCaches => Services.GetRequiredService(); + protected IIOHelper IOHelper => Services.GetRequiredService(); + protected IShortStringHelper ShortStringHelper => Services.GetRequiredService(); + protected IGlobalSettings GlobalSettings => Services.GetRequiredService(); + protected IMapperCollection Mappers => Services.GetRequiredService(); + + #endregion + + #region Builders + + protected GlobalSettingsBuilder GlobalSettingsBuilder = new GlobalSettingsBuilder(); + protected UserBuilder UserBuilder = new UserBuilder(); + protected UserGroupBuilder UserGroupBuilder = new UserGroupBuilder(); + + #endregion } } diff --git a/src/Umbraco.Tests/Persistence/Repositories/UserRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/UserRepositoryTest.cs index 201b84f29a..8938a69579 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/UserRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/UserRepositoryTest.cs @@ -5,21 +5,20 @@ using Umbraco.Core; using Umbraco.Core.Cache; using Umbraco.Core.Logging; using Umbraco.Core.Models.Membership; -using Umbraco.Core.Persistence.Mappers; -using Umbraco.Core.Persistence.DatabaseModelDefinitions; using Umbraco.Core.Persistence.Repositories; using Umbraco.Core.Persistence.Repositories.Implement; using Umbraco.Core.Scoping; using Umbraco.Tests.TestHelpers; using Umbraco.Tests.TestHelpers.Entities; using Umbraco.Tests.Testing; -using Umbraco.Core.Persistence; using Umbraco.Core.PropertyEditors; using System; using Umbraco.Core.Configuration; namespace Umbraco.Tests.Persistence.Repositories { + // TODO: Move the remaining parts to Integration tests + [TestFixture] [UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest, WithApplication = true, Logger = UmbracoTestOptions.Logger.Console)] public class UserRepositoryTest : TestWithDatabaseBase @@ -78,72 +77,6 @@ namespace Umbraco.Tests.Persistence.Repositories return new UserGroupRepository(accessor, AppCaches.Disabled, Logger, ShortStringHelper); } - [Test] - public void Can_Perform_Add_On_UserRepository() - { - // Arrange - var provider = TestObjects.GetScopeProvider(Logger); - using (var scope = provider.CreateScope()) - { - var repository = CreateRepository(provider); - - var user = MockedUser.CreateUser(); - - // Act - repository.Save(user); - - - // Assert - Assert.That(user.HasIdentity, Is.True); - } - } - - [Test] - public void Can_Perform_Multiple_Adds_On_UserRepository() - { - // Arrange - var provider = TestObjects.GetScopeProvider(Logger); - using (var scope = provider.CreateScope()) - { - var repository = CreateRepository(provider); - - var user1 = MockedUser.CreateUser("1"); - var use2 = MockedUser.CreateUser("2"); - - // Act - repository.Save(user1); - - repository.Save(use2); - - - // Assert - Assert.That(user1.HasIdentity, Is.True); - Assert.That(use2.HasIdentity, Is.True); - } - } - - [Test] - public void Can_Verify_Fresh_Entity_Is_Not_Dirty() - { - // Arrange - var provider = TestObjects.GetScopeProvider(Logger); - using (var scope = provider.CreateScope()) - { - var repository = CreateRepository(provider); - - var user = MockedUser.CreateUser(); - repository.Save(user); - - - // Act - var resolved = repository.Get((int)user.Id); - bool dirty = ((User)resolved).IsDirty(); - - // Assert - Assert.That(dirty, Is.False); - } - } - [Test] public void Can_Perform_Update_On_UserRepository() { @@ -206,268 +139,6 @@ namespace Umbraco.Tests.Persistence.Repositories } } - [Test] - public void Can_Perform_Delete_On_UserRepository() - { - // Arrange - var provider = TestObjects.GetScopeProvider(Logger); - using (var scope = provider.CreateScope()) - { - var repository = CreateRepository(provider); - - var user = MockedUser.CreateUser(); - - // Act - repository.Save(user); - - var id = user.Id; - - var repository2 = new UserRepository((IScopeAccessor) provider, AppCaches.Disabled, Logger, Mock.Of(),TestObjects.GetGlobalSettings(), Mock.Of()); - - repository2.Delete(user); - - - var resolved = repository2.Get((int) id); - - // Assert - Assert.That(resolved, Is.Null); - } - } - - [Test] - public void Can_Perform_Get_On_UserRepository() - { - // Arrange - var provider = TestObjects.GetScopeProvider(Logger); - using (var scope = provider.CreateScope()) - { - var repository = CreateRepository(provider); - var userGroupRepository = CreateUserGroupRepository(provider); - - var user = CreateAndCommitUserWithGroup(repository, userGroupRepository); - - // Act - var updatedItem = repository.Get(user.Id); - - // FIXME: this test cannot work, user has 2 sections but the way it's created, - // they don't show, so the comparison with updatedItem fails - fix! - - // Assert - AssertPropertyValues(updatedItem, user); - } - } - - [Test] - public void Can_Perform_GetByQuery_On_UserRepository() - { - // Arrange - var provider = TestObjects.GetScopeProvider(Logger); - using (var scope = provider.CreateScope()) - { - var repository = CreateRepository(provider); - - CreateAndCommitMultipleUsers(repository); - - // Act - var query = scope.SqlContext.Query().Where(x => x.Username == "TestUser1"); - var result = repository.Get(query); - - // Assert - Assert.That(result.Count(), Is.GreaterThanOrEqualTo(1)); - } - } - - [Test] - public void Can_Perform_GetAll_By_Param_Ids_On_UserRepository() - { - // Arrange - var provider = TestObjects.GetScopeProvider(Logger); - using (var scope = provider.CreateScope()) - { - var repository = CreateRepository(provider); - - var users = CreateAndCommitMultipleUsers(repository); - - // Act - var result = repository.GetMany((int) users[0].Id, (int) users[1].Id); - - // Assert - Assert.That(result, Is.Not.Null); - Assert.That(result.Any(), Is.True); - Assert.That(result.Count(), Is.EqualTo(2)); - } - } - - [Test] - public void Can_Perform_GetAll_On_UserRepository() - { - // Arrange - var provider = TestObjects.GetScopeProvider(Logger); - using (var scope = provider.CreateScope()) - { - var repository = CreateRepository(provider); - - CreateAndCommitMultipleUsers(repository); - - // Act - var result = repository.GetMany(); - - // Assert - Assert.That(result, Is.Not.Null); - Assert.That(result.Any(), Is.True); - Assert.That(result.Count(), Is.GreaterThanOrEqualTo(3)); - } - } - - [Test] - public void Can_Perform_Exists_On_UserRepository() - { - // Arrange - var provider = TestObjects.GetScopeProvider(Logger); - using (var scope = provider.CreateScope()) - { - var repository = CreateRepository(provider); - - var users = CreateAndCommitMultipleUsers(repository); - - // Act - var exists = repository.Exists(users[0].Id); - - // Assert - Assert.That(exists, Is.True); - } - } - - [Test] - public void Can_Perform_Count_On_UserRepository() - { - // Arrange - var provider = TestObjects.GetScopeProvider(Logger); - using (var scope = provider.CreateScope()) - { - var repository = CreateRepository(provider); - - var users = CreateAndCommitMultipleUsers(repository); - - // Act - var query = scope.SqlContext.Query().Where(x => x.Username == "TestUser1" || x.Username == "TestUser2"); - var result = repository.Count(query); - - // Assert - Assert.AreEqual(2, result); - } - } - - [Test] - public void Can_Get_Paged_Results_By_Query_And_Filter_And_Groups() - { - var provider = TestObjects.GetScopeProvider(Logger); - using (var scope = provider.CreateScope()) - { - var repository = CreateRepository(provider); - - var users = CreateAndCommitMultipleUsers(repository); - var query = provider.SqlContext.Query().Where(x => x.Username == "TestUser1" || x.Username == "TestUser2"); - - try - { - scope.Database.AsUmbracoDatabase().EnableSqlTrace = true; - scope.Database.AsUmbracoDatabase().EnableSqlCount = true; - - // Act - var result = repository.GetPagedResultsByQuery(query, 0, 10, out var totalRecs, user => user.Id, Direction.Ascending, - excludeUserGroups: new[] { Constants.Security.TranslatorGroupAlias }, - filter: provider.SqlContext.Query().Where(x => x.Id > -1)); - - // Assert - Assert.AreEqual(2, totalRecs); - } - finally - { - scope.Database.AsUmbracoDatabase().EnableSqlTrace = false; - scope.Database.AsUmbracoDatabase().EnableSqlCount = false; - } - } - - } - - [Test] - public void Can_Get_Paged_Results_With_Filter_And_Groups() - { - var provider = TestObjects.GetScopeProvider(Logger); - using (var scope = provider.CreateScope()) - { - var repository = CreateRepository(provider); - - var users = CreateAndCommitMultipleUsers(repository); - - try - { - scope.Database.AsUmbracoDatabase().EnableSqlTrace = true; - scope.Database.AsUmbracoDatabase().EnableSqlCount = true; - - // Act - var result = repository.GetPagedResultsByQuery(null, 0, 10, out var totalRecs, user => user.Id, Direction.Ascending, - includeUserGroups: new[] { Constants.Security.AdminGroupAlias, Constants.Security.SensitiveDataGroupAlias }, - excludeUserGroups: new[] { Constants.Security.TranslatorGroupAlias }, - filter: provider.SqlContext.Query().Where(x => x.Id == -1)); - - // Assert - Assert.AreEqual(1, totalRecs); - } - finally - { - scope.Database.AsUmbracoDatabase().EnableSqlTrace = false; - scope.Database.AsUmbracoDatabase().EnableSqlCount = false; - } - } - } - - [Test] - public void Can_Invalidate_SecurityStamp_On_Username_Change() - { - // Arrange - var provider = TestObjects.GetScopeProvider(Logger); - using (var scope = provider.CreateScope()) - { - var repository = CreateRepository(provider); - var userGroupRepository = CreateUserGroupRepository(provider); - - var user = CreateAndCommitUserWithGroup(repository, userGroupRepository); - var originalSecurityStamp = user.SecurityStamp; - - // Ensure when user generated a security stamp is present - Assert.That(user.SecurityStamp, Is.Not.Null); - Assert.That(user.SecurityStamp, Is.Not.Empty); - - // Update username - user.Username = user.Username + "UPDATED"; - repository.Save(user); - - // Get the user - var updatedUser = repository.Get(user.Id); - - // Ensure the Security Stamp is invalidated & no longer the same - Assert.AreNotEqual(originalSecurityStamp, updatedUser.SecurityStamp); - } - } - - private void AssertPropertyValues(IUser updatedItem, IUser originalUser) - { - Assert.That(updatedItem.Id, Is.EqualTo(originalUser.Id)); - Assert.That(updatedItem.Name, Is.EqualTo(originalUser.Name)); - Assert.That(updatedItem.Language, Is.EqualTo(originalUser.Language)); - Assert.That(updatedItem.IsApproved, Is.EqualTo(originalUser.IsApproved)); - Assert.That(updatedItem.RawPasswordValue, Is.EqualTo(originalUser.RawPasswordValue)); - Assert.That(updatedItem.IsLockedOut, Is.EqualTo(originalUser.IsLockedOut)); - Assert.IsTrue(updatedItem.StartContentIds.UnsortedSequenceEqual(originalUser.StartContentIds)); - Assert.IsTrue(updatedItem.StartMediaIds.UnsortedSequenceEqual(originalUser.StartMediaIds)); - Assert.That(updatedItem.Email, Is.EqualTo(originalUser.Email)); - Assert.That(updatedItem.Username, Is.EqualTo(originalUser.Username)); - Assert.That(updatedItem.AllowedSections.Count(), Is.EqualTo(originalUser.AllowedSections.Count())); - foreach (var allowedSection in originalUser.AllowedSections) - Assert.IsTrue(updatedItem.AllowedSections.Contains(allowedSection)); - } private static User CreateAndCommitUserWithGroup(IUserRepository repository, IUserGroupRepository userGroupRepository) { @@ -483,15 +154,5 @@ namespace Umbraco.Tests.Persistence.Repositories return user; } - private IUser[] CreateAndCommitMultipleUsers(IUserRepository repository) - { - var user1 = MockedUser.CreateUser("1"); - var user2 = MockedUser.CreateUser("2"); - var user3 = MockedUser.CreateUser("3"); - repository.Save(user1); - repository.Save(user2); - repository.Save(user3); - return new IUser[] { user1, user2, user3 }; - } } } diff --git a/src/Umbraco.Tests/TestHelpers/TestObjects-Mocks.cs b/src/Umbraco.Tests/TestHelpers/TestObjects-Mocks.cs index a07be868a5..49444f8b68 100644 --- a/src/Umbraco.Tests/TestHelpers/TestObjects-Mocks.cs +++ b/src/Umbraco.Tests/TestHelpers/TestObjects-Mocks.cs @@ -4,24 +4,19 @@ using System.Data; using System.Data.Common; using System.Linq; using System.Linq.Expressions; -using System.Web; using Moq; using Umbraco.Core; using Umbraco.Core.Composing; using Umbraco.Core.Configuration; -using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.IO; using Umbraco.Core.Logging; using Umbraco.Core.Models; -using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.Persistence; using Umbraco.Core.Persistence.SqlSyntax; using Umbraco.Core.Services; using Umbraco.Tests.Common; using Umbraco.Web; using Umbraco.Web.PublishedCache; -using Umbraco.Web.Routing; -using Umbraco.Web.Security; namespace Umbraco.Tests.TestHelpers { diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index f667b11aa3..a1d086f94d 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -148,6 +148,7 @@ + @@ -340,7 +341,6 @@ -