diff --git a/.gitattributes b/.gitattributes index c8987ade67..3241b6511c 100644 --- a/.gitattributes +++ b/.gitattributes @@ -43,6 +43,7 @@ *.hs text=auto *.json text=auto *.xml text=auto +*.resx text=auto *.csproj text=auto merge=union *.vbproj text=auto merge=union diff --git a/src/Umbraco.Infrastructure/Persistence/LocalDb.cs b/src/Umbraco.Infrastructure/Persistence/LocalDb.cs index 4ec233e17f..89fce803b2 100644 --- a/src/Umbraco.Infrastructure/Persistence/LocalDb.cs +++ b/src/Umbraco.Infrastructure/Persistence/LocalDb.cs @@ -937,7 +937,7 @@ namespace Umbraco.Core.Persistence /// This is a C# implementation of T-SQL QUOTEDNAME. /// is optional, it can be '[' (default), ']', '\'' or '"'. /// - private static string QuotedName(string name, char quote = '[') + internal static string QuotedName(string name, char quote = '[') { switch (quote) { diff --git a/src/Umbraco.Tests.Integration/Testing/ITestDatabase.cs b/src/Umbraco.Tests.Integration/Testing/ITestDatabase.cs new file mode 100644 index 0000000000..250e062ee4 --- /dev/null +++ b/src/Umbraco.Tests.Integration/Testing/ITestDatabase.cs @@ -0,0 +1,10 @@ +namespace Umbraco.Tests.Integration.Testing +{ + public interface ITestDatabase + { + string ConnectionString { get; } + int AttachEmpty(); + int AttachSchema(); + void Detach(int id); + } +} diff --git a/src/Umbraco.Tests.Integration/Testing/IntegrationTestComposer.cs b/src/Umbraco.Tests.Integration/Testing/IntegrationTestComposer.cs index dacfd950e0..39d74f8869 100644 --- a/src/Umbraco.Tests.Integration/Testing/IntegrationTestComposer.cs +++ b/src/Umbraco.Tests.Integration/Testing/IntegrationTestComposer.cs @@ -49,8 +49,10 @@ namespace Umbraco.Tests.Integration.Testing // we don't want persisted nucache files in tests builder.Services.AddTransient(factory => new PublishedSnapshotServiceOptions { IgnoreLocalDb = true }); + #if IS_WINDOWS // ensure all lucene indexes are using RAM directory (no file system) builder.Services.AddUnique(); + #endif // replace this service so that it can lookup the correct file locations builder.Services.AddUnique(GetLocalizedTextService); diff --git a/src/Umbraco.Tests.Integration/Testing/LocalDbTestDatabase.cs b/src/Umbraco.Tests.Integration/Testing/LocalDbTestDatabase.cs index 39f9ca5592..bb83914b63 100644 --- a/src/Umbraco.Tests.Integration/Testing/LocalDbTestDatabase.cs +++ b/src/Umbraco.Tests.Integration/Testing/LocalDbTestDatabase.cs @@ -20,7 +20,7 @@ namespace Umbraco.Tests.Integration.Testing /// /// Manages a pool of LocalDb databases for integration testing /// - public class LocalDbTestDatabase + public class LocalDbTestDatabase : ITestDatabase { public const string InstanceName = "UmbracoTests"; public const string DatabaseName = "UmbracoTests"; @@ -139,7 +139,7 @@ namespace Umbraco.Tests.Integration.Testing } - private static void AddParameter(IDbCommand cmd, UmbracoDatabase.ParameterInfo parameterInfo) + internal static void AddParameter(IDbCommand cmd, UmbracoDatabase.ParameterInfo parameterInfo) { var p = cmd.CreateParameter(); p.ParameterName = parameterInfo.Name; @@ -149,22 +149,6 @@ namespace Umbraco.Tests.Integration.Testing 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) { diff --git a/src/Umbraco.Tests.Integration/Testing/SqlDeveloperTestDatabase.cs b/src/Umbraco.Tests.Integration/Testing/SqlDeveloperTestDatabase.cs new file mode 100644 index 0000000000..f5ae1661b8 --- /dev/null +++ b/src/Umbraco.Tests.Integration/Testing/SqlDeveloperTestDatabase.cs @@ -0,0 +1,163 @@ +using System; +using System.Data; +using System.Data.SqlClient; +using System.Linq; +using System.Text.RegularExpressions; +using Microsoft.Extensions.Logging; +using Umbraco.Core; +using Umbraco.Core.Configuration; +using Umbraco.Core.Migrations.Install; +using Umbraco.Core.Persistence; + +// ReSharper disable ConvertToUsingDeclaration + +namespace Umbraco.Tests.Integration.Testing +{ + /// + /// It's not meant to be pretty, rushed port of LocalDb.cs + LocalDbTestDatabase.cs + /// + public class SqlDeveloperTestDatabase : ITestDatabase + { + private readonly string _masterConnectionString; + private readonly string _databaseName; + private readonly ILoggerFactory _loggerFactory; + private readonly ILogger _log; + private readonly IUmbracoDatabaseFactory _databaseFactory; + private UmbracoDatabase.CommandInfo[] _cachedDatabaseInitCommands; + + public SqlDeveloperTestDatabase(ILoggerFactory loggerFactory, IUmbracoDatabaseFactory databaseFactory, string masterConnectionString) + { + _loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory)); + _databaseFactory = databaseFactory ?? throw new ArgumentNullException(nameof(databaseFactory)); + _masterConnectionString = masterConnectionString; + _databaseName = $"Umbraco_Integration_{Guid.NewGuid()}".Replace("-", string.Empty); + _log = loggerFactory.CreateLogger(); + } + + public string ConnectionString { get; private set; } + + public int AttachEmpty() + { + CreateDatabase(); + return -1; + } + + public int AttachSchema() + { + CreateDatabase(); + + _log.LogInformation($"Attaching schema {_databaseName}"); + + using (var connection = new SqlConnection(ConnectionString)) + { + connection.Open(); + using (var command = connection.CreateCommand()) + { + RebuildSchema(command); + } + } + + return -1; + } + + public void Detach(int id) + { + _log.LogInformation($"Dropping database {_databaseName}"); + using (var connection = new SqlConnection(_masterConnectionString)) + { + connection.Open(); + using (var command = connection.CreateCommand()) + { + SetCommand(command, $@" + ALTER DATABASE{LocalDb.QuotedName(_databaseName)} + SET SINGLE_USER + WITH ROLLBACK IMMEDIATE + "); + command.ExecuteNonQuery(); + + SetCommand(command, $@"DROP DATABASE {LocalDb.QuotedName(_databaseName)}"); + command.ExecuteNonQuery(); + } + } + } + + private void CreateDatabase() + { + _log.LogInformation($"Creating database {_databaseName}"); + using (var connection = new SqlConnection(_masterConnectionString)) + { + connection.Open(); + using (var command = connection.CreateCommand()) + { + SetCommand(command, $@"CREATE DATABASE {LocalDb.QuotedName(_databaseName)}"); + var unused = command.ExecuteNonQuery(); + } + } + + ConnectionString = ConstructConnectionString(_masterConnectionString, _databaseName); + } + + private static string ConstructConnectionString(string masterConnectionString, string databaseName) + { + var prefix = Regex.Replace(masterConnectionString, "Database=.+?;", string.Empty); + var connectionString = $"{prefix};Database={databaseName};"; + return connectionString.Replace(";;", ";"); + } + + private static void SetCommand(SqlCommand command, string sql, params object[] args) + { + command.CommandType = CommandType.Text; + command.CommandText = sql; + command.Parameters.Clear(); + + for (var i = 0; i < args.Length; i++) + { + command.Parameters.AddWithValue("@" + i, args[i]); + } + } + + private void RebuildSchema(IDbCommand command) + { + if (_cachedDatabaseInitCommands != null) + { + foreach (var dbCommand in _cachedDatabaseInitCommands) + { + + if (dbCommand.Text.StartsWith("SELECT ")) + { + continue; + } + + command.CommandText = dbCommand.Text; + command.Parameters.Clear(); + + foreach (var parameterInfo in dbCommand.Parameters) + { + LocalDbTestDatabase.AddParameter(command, parameterInfo); + } + + command.ExecuteNonQuery(); + } + } + else + { + _databaseFactory.Configure(ConnectionString, Constants.DatabaseProviders.SqlServer); + + using (var database = (UmbracoDatabase)_databaseFactory.CreateDatabase()) + { + database.LogCommands = true; + + using (var transaction = database.GetTransaction()) + { + var schemaCreator = new DatabaseSchemaCreator(database, _loggerFactory.CreateLogger(), _loggerFactory, new UmbracoVersion()); + schemaCreator.InitializeDatabaseSchema(); + + transaction.Complete(); + + _cachedDatabaseInitCommands = database.Commands.ToArray(); + } + } + } + } + } +} diff --git a/src/Umbraco.Tests.Integration/Testing/TestDatabaseFactory.cs b/src/Umbraco.Tests.Integration/Testing/TestDatabaseFactory.cs new file mode 100644 index 0000000000..0f403818b7 --- /dev/null +++ b/src/Umbraco.Tests.Integration/Testing/TestDatabaseFactory.cs @@ -0,0 +1,44 @@ +using System; +using System.Runtime.InteropServices; +using Microsoft.Extensions.Logging; +using Umbraco.Core.Persistence; + +namespace Umbraco.Tests.Integration.Testing +{ + public class TestDatabaseFactory + { + public static ITestDatabase Create(string filesPath, ILoggerFactory loggerFactory, IUmbracoDatabaseFactory dbFactory) + { + return RuntimeInformation.IsOSPlatform(OSPlatform.Windows) + ? CreateLocalDb(filesPath, loggerFactory, dbFactory) + : CreateSqlDeveloper(loggerFactory, dbFactory); + } + + private static ITestDatabase CreateLocalDb(string filesPath, ILoggerFactory loggerFactory, IUmbracoDatabaseFactory dbFactory) + { + var localDb = new LocalDb(); + + if (!localDb.IsAvailable) + { + throw new InvalidOperationException("LocalDB is not available."); + } + + return new LocalDbTestDatabase(loggerFactory, localDb, filesPath, dbFactory); + } + + private static ITestDatabase CreateSqlDeveloper(ILoggerFactory loggerFactory, IUmbracoDatabaseFactory dbFactory) + { + // $ export SA_PASSWORD=Foobar123! + // $ export UmbracoIntegrationTestConnectionString="Server=localhost,1433;User Id=sa;Password=$SA_PASSWORD;" + // $ docker run -e 'ACCEPT_EULA=Y' -e "SA_PASSWORD=$SA_PASSWORD" -e 'MSSQL_PID=Developer' -p 1433:1433 -d mcr.microsoft.com/mssql/server:2017-latest-ubuntu + var connectionString = Environment.GetEnvironmentVariable("UmbracoIntegrationTestConnectionString"); + + if (string.IsNullOrEmpty(connectionString)) + { + throw new InvalidOperationException("ENV: UmbracoIntegrationTestConnectionString is not set"); + } + + return new SqlDeveloperTestDatabase(loggerFactory, dbFactory, connectionString); + } + } +} diff --git a/src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs b/src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs index a8875de286..9627fdf42a 100644 --- a/src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs +++ b/src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs @@ -242,7 +242,7 @@ namespace Umbraco.Tests.Integration.Testing #region LocalDb private static readonly object _dbLocker = new object(); - private static LocalDbTestDatabase _dbInstance; + private static ITestDatabase _dbInstance; protected void UseTestLocalDb(IServiceProvider serviceProvider) { @@ -267,17 +267,14 @@ namespace Umbraco.Tests.Integration.Testing /// /// There must only be ONE instance shared between all tests in a session /// - private static LocalDbTestDatabase GetOrCreateDatabase(string filesPath, ILoggerFactory loggerFactory, IUmbracoDatabaseFactory dbFactory) + private static ITestDatabase GetOrCreateDatabase(string filesPath, ILoggerFactory loggerFactory, IUmbracoDatabaseFactory dbFactory) { lock (_dbLocker) { if (_dbInstance != null) return _dbInstance; - var localDb = new LocalDb(); - if (localDb.IsAvailable == false) - throw new InvalidOperationException("LocalDB is not available."); - _dbInstance = new LocalDbTestDatabase(loggerFactory, localDb, filesPath, dbFactory); + _dbInstance = TestDatabaseFactory.Create(filesPath, loggerFactory, dbFactory); return _dbInstance; } } diff --git a/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/Importing/ImportResources.resx b/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/Importing/ImportResources.resx index 5823fa1245..fdf7880297 100644 --- a/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/Importing/ImportResources.resx +++ b/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/Importing/ImportResources.resx @@ -119,6 +119,6 @@ - dictionary-package.xml;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-8 + Dictionary-Package.xml;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-8 - \ No newline at end of file + diff --git a/src/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj b/src/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj index c5b42f9848..b996205712 100644 --- a/src/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj +++ b/src/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj @@ -7,6 +7,10 @@ 8 + + IS_WINDOWS + +