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
+
+