V10: fix build warnings in test projects (#12509)
* Run code cleanup * Dotnet format benchmarks project * Fix up Test.Common * Run dotnet format + manual cleanup * Run code cleanup for unit tests * Run dotnet format * Fix up errors * Manual cleanup of Unit test project * Update tests/Umbraco.Tests.Benchmarks/HexStringBenchmarks.cs Co-authored-by: Mole <nikolajlauridsen@protonmail.ch> * Update tests/Umbraco.Tests.Integration/Testing/TestDbMeta.cs Co-authored-by: Mole <nikolajlauridsen@protonmail.ch> * Update tests/Umbraco.Tests.Benchmarks/TypeFinderBenchmarks.cs Co-authored-by: Mole <nikolajlauridsen@protonmail.ch> * Update tests/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs Co-authored-by: Mole <nikolajlauridsen@protonmail.ch> * Update tests/Umbraco.Tests.Integration/Umbraco.Core/Events/EventAggregatorTests.cs Co-authored-by: Mole <nikolajlauridsen@protonmail.ch> * Fix according to review * Fix after merge * Fix errors Co-authored-by: Nikolaj Geisle <niko737@edu.ucl.dk> Co-authored-by: Mole <nikolajlauridsen@protonmail.ch> Co-authored-by: Zeegaan <nge@umbraco.dk>
This commit is contained in:
@@ -9,139 +9,130 @@ using System.Data.Common;
|
||||
using System.Diagnostics;
|
||||
using System.Threading;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Moq;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Configuration;
|
||||
using Umbraco.Cms.Core.Events;
|
||||
using Umbraco.Cms.Infrastructure.Migrations.Install;
|
||||
using Umbraco.Cms.Infrastructure.Persistence;
|
||||
|
||||
namespace Umbraco.Cms.Tests.Integration.Testing
|
||||
namespace Umbraco.Cms.Tests.Integration.Testing;
|
||||
|
||||
public abstract class BaseTestDatabase
|
||||
{
|
||||
public abstract class BaseTestDatabase
|
||||
protected IUmbracoDatabaseFactory _databaseFactory;
|
||||
|
||||
protected ILoggerFactory _loggerFactory;
|
||||
|
||||
protected BlockingCollection<TestDbMeta> _prepareQueue;
|
||||
protected BlockingCollection<TestDbMeta> _readyEmptyQueue;
|
||||
protected BlockingCollection<TestDbMeta> _readySchemaQueue;
|
||||
protected IList<TestDbMeta> _testDatabases;
|
||||
|
||||
public BaseTestDatabase() => Instance = this;
|
||||
public static BaseTestDatabase Instance { get; private set; }
|
||||
public static bool IsSqlite() => Instance is SqliteTestDatabase;
|
||||
public static bool IsSqlServer() => Instance is SqlServerBaseTestDatabase;
|
||||
|
||||
protected abstract void Initialize();
|
||||
|
||||
public virtual TestDbMeta AttachEmpty()
|
||||
{
|
||||
public static bool IsSqlite() => BaseTestDatabase.Instance is SqliteTestDatabase;
|
||||
public static bool IsSqlServer() => BaseTestDatabase.Instance is SqlServerBaseTestDatabase;
|
||||
|
||||
protected ILoggerFactory _loggerFactory;
|
||||
protected IUmbracoDatabaseFactory _databaseFactory;
|
||||
protected IList<TestDbMeta> _testDatabases;
|
||||
|
||||
protected BlockingCollection<TestDbMeta> _prepareQueue;
|
||||
protected BlockingCollection<TestDbMeta> _readySchemaQueue;
|
||||
protected BlockingCollection<TestDbMeta> _readyEmptyQueue;
|
||||
public static BaseTestDatabase Instance { get; private set; }
|
||||
|
||||
public BaseTestDatabase() => Instance = this;
|
||||
|
||||
protected abstract void Initialize();
|
||||
|
||||
public virtual TestDbMeta AttachEmpty()
|
||||
if (_prepareQueue == null)
|
||||
{
|
||||
if (_prepareQueue == null)
|
||||
{
|
||||
Initialize();
|
||||
}
|
||||
|
||||
return _readyEmptyQueue.Take();
|
||||
Initialize();
|
||||
}
|
||||
|
||||
public virtual TestDbMeta AttachSchema()
|
||||
{
|
||||
if (_prepareQueue == null)
|
||||
{
|
||||
Initialize();
|
||||
}
|
||||
return _readyEmptyQueue.Take();
|
||||
}
|
||||
|
||||
return _readySchemaQueue.Take();
|
||||
public virtual TestDbMeta AttachSchema()
|
||||
{
|
||||
if (_prepareQueue == null)
|
||||
{
|
||||
Initialize();
|
||||
}
|
||||
|
||||
public virtual void Detach(TestDbMeta meta)
|
||||
{
|
||||
_prepareQueue.TryAdd(meta);
|
||||
}
|
||||
return _readySchemaQueue.Take();
|
||||
}
|
||||
|
||||
protected virtual void PrepareDatabase() =>
|
||||
Retry(10, () =>
|
||||
{
|
||||
while (_prepareQueue.IsCompleted == false)
|
||||
{
|
||||
TestDbMeta meta;
|
||||
try
|
||||
{
|
||||
meta = _prepareQueue.Take();
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
ResetTestDatabase(meta);
|
||||
|
||||
if (!meta.IsEmpty)
|
||||
{
|
||||
using (var conn = GetConnection(meta))
|
||||
{
|
||||
conn.Open();
|
||||
using (var cmd = conn.CreateCommand())
|
||||
{
|
||||
RebuildSchema(cmd, meta);
|
||||
}
|
||||
}
|
||||
|
||||
_readySchemaQueue.TryAdd(meta);
|
||||
}
|
||||
else
|
||||
{
|
||||
_readyEmptyQueue.TryAdd(meta);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
protected static void AddParameter(IDbCommand cmd, UmbracoDatabase.ParameterInfo parameterInfo)
|
||||
{
|
||||
IDbDataParameter p = cmd.CreateParameter();
|
||||
p.ParameterName = parameterInfo.Name;
|
||||
p.Value = parameterInfo.Value;
|
||||
p.DbType = parameterInfo.DbType;
|
||||
p.Size = parameterInfo.Size;
|
||||
cmd.Parameters.Add(p);
|
||||
}
|
||||
|
||||
protected abstract DbConnection GetConnection(TestDbMeta meta);
|
||||
|
||||
protected abstract void RebuildSchema(IDbCommand command, TestDbMeta meta);
|
||||
|
||||
protected abstract void ResetTestDatabase(TestDbMeta meta);
|
||||
|
||||
protected static void Retry(int maxIterations, Action action)
|
||||
{
|
||||
for (int i = 0; i < maxIterations; i++)
|
||||
public virtual void Detach(TestDbMeta meta) => _prepareQueue.TryAdd(meta);
|
||||
|
||||
protected virtual void PrepareDatabase() =>
|
||||
Retry(10, () =>
|
||||
{
|
||||
while (_prepareQueue.IsCompleted == false)
|
||||
{
|
||||
TestDbMeta meta;
|
||||
try
|
||||
{
|
||||
action();
|
||||
return;
|
||||
}
|
||||
catch (DbException ex)
|
||||
{
|
||||
// Console.Error.WriteLine($"SqlException occured, but we try again {i+1}/{maxIterations}.\n{e}");
|
||||
// This can occur when there's a transaction deadlock which means (i think) that the database is still in use and hasn't been closed properly yet
|
||||
// so we need to just wait a little bit
|
||||
Thread.Sleep(100 * i);
|
||||
if (i == maxIterations - 1)
|
||||
{
|
||||
Debugger.Launch();
|
||||
throw;
|
||||
}
|
||||
meta = _prepareQueue.Take();
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
// Ignore
|
||||
continue;
|
||||
}
|
||||
|
||||
ResetTestDatabase(meta);
|
||||
|
||||
if (!meta.IsEmpty)
|
||||
{
|
||||
using (var conn = GetConnection(meta))
|
||||
{
|
||||
conn.Open();
|
||||
using (var cmd = conn.CreateCommand())
|
||||
{
|
||||
RebuildSchema(cmd, meta);
|
||||
}
|
||||
}
|
||||
|
||||
_readySchemaQueue.TryAdd(meta);
|
||||
}
|
||||
else
|
||||
{
|
||||
_readyEmptyQueue.TryAdd(meta);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
public abstract void TearDown();
|
||||
protected 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);
|
||||
}
|
||||
|
||||
protected abstract DbConnection GetConnection(TestDbMeta meta);
|
||||
|
||||
protected abstract void RebuildSchema(IDbCommand command, TestDbMeta meta);
|
||||
|
||||
protected abstract void ResetTestDatabase(TestDbMeta meta);
|
||||
|
||||
protected static void Retry(int maxIterations, Action action)
|
||||
{
|
||||
for (var i = 0; i < maxIterations; i++)
|
||||
{
|
||||
try
|
||||
{
|
||||
action();
|
||||
return;
|
||||
}
|
||||
catch (DbException ex)
|
||||
{
|
||||
// Console.Error.WriteLine($"SqlException occured, but we try again {i+1}/{maxIterations}.\n{e}");
|
||||
// This can occur when there's a transaction deadlock which means (i think) that the database is still in use and hasn't been closed properly yet
|
||||
// so we need to just wait a little bit
|
||||
Thread.Sleep(100 * i);
|
||||
if (i == maxIterations - 1)
|
||||
{
|
||||
Debugger.Launch();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public abstract void TearDown();
|
||||
}
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
// Copyright (c) Umbraco.
|
||||
// See LICENSE for more details.
|
||||
|
||||
namespace Umbraco.Cms.Tests.Integration.Testing
|
||||
namespace Umbraco.Cms.Tests.Integration.Testing;
|
||||
|
||||
public interface ITestDatabase
|
||||
{
|
||||
public interface ITestDatabase
|
||||
{
|
||||
TestDbMeta AttachEmpty();
|
||||
TestDbMeta AttachEmpty();
|
||||
|
||||
TestDbMeta AttachSchema();
|
||||
TestDbMeta AttachSchema();
|
||||
|
||||
void Detach(TestDbMeta id);
|
||||
}
|
||||
void Detach(TestDbMeta id);
|
||||
}
|
||||
|
||||
@@ -5,34 +5,33 @@ using Examine;
|
||||
using Examine.Lucene.Providers;
|
||||
using Umbraco.Cms.Core.Composing;
|
||||
|
||||
namespace Umbraco.Cms.Tests.Integration.Testing
|
||||
namespace Umbraco.Cms.Tests.Integration.Testing;
|
||||
|
||||
/// <summary>
|
||||
/// A component to customize some services to work nicely with integration tests
|
||||
/// </summary>
|
||||
public class IntegrationTestComponent : IComponent
|
||||
{
|
||||
/// <summary>
|
||||
/// A component to customize some services to work nicely with integration tests
|
||||
/// </summary>
|
||||
public class IntegrationTestComponent : IComponent
|
||||
private readonly IExamineManager _examineManager;
|
||||
|
||||
public IntegrationTestComponent(IExamineManager examineManager) => _examineManager = examineManager;
|
||||
|
||||
public void Initialize() => ConfigureExamineIndexes();
|
||||
|
||||
public void Terminate()
|
||||
{
|
||||
private readonly IExamineManager _examineManager;
|
||||
}
|
||||
|
||||
public IntegrationTestComponent(IExamineManager examineManager) => _examineManager = examineManager;
|
||||
|
||||
public void Initialize() => ConfigureExamineIndexes();
|
||||
|
||||
public void Terminate()
|
||||
/// <summary>
|
||||
/// Configure all indexes to run sync (non-backbround threads) and to use RAMDirectory
|
||||
/// </summary>
|
||||
private void ConfigureExamineIndexes()
|
||||
{
|
||||
foreach (var index in _examineManager.Indexes)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configure all indexes to run sync (non-backbround threads) and to use RAMDirectory
|
||||
/// </summary>
|
||||
private void ConfigureExamineIndexes()
|
||||
{
|
||||
foreach (IIndex index in _examineManager.Indexes)
|
||||
if (index is LuceneIndex luceneIndex)
|
||||
{
|
||||
if (index is LuceneIndex luceneIndex)
|
||||
{
|
||||
luceneIndex.WithThreadingMode(IndexThreadingMode.Synchronous);
|
||||
}
|
||||
luceneIndex.WithThreadingMode(IndexThreadingMode.Synchronous);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,136 +10,135 @@ using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Umbraco.Cms.Infrastructure.Persistence;
|
||||
|
||||
namespace Umbraco.Cms.Tests.Integration.Testing
|
||||
namespace Umbraco.Cms.Tests.Integration.Testing;
|
||||
|
||||
/// <summary>
|
||||
/// Manages a pool of LocalDb databases for integration testing
|
||||
/// </summary>
|
||||
public class LocalDbTestDatabase : SqlServerBaseTestDatabase, ITestDatabase
|
||||
{
|
||||
/// <summary>
|
||||
/// Manages a pool of LocalDb databases for integration testing
|
||||
/// </summary>
|
||||
public class LocalDbTestDatabase : SqlServerBaseTestDatabase, ITestDatabase
|
||||
public const string InstanceName = "UmbracoTests";
|
||||
public const string DatabaseName = "UmbracoTests";
|
||||
private static LocalDb.Instance s_localDbInstance;
|
||||
private static string s_filesPath;
|
||||
private readonly LocalDb _localDb;
|
||||
|
||||
private readonly TestDatabaseSettings _settings;
|
||||
|
||||
// It's internal because `Umbraco.Core.Persistence.LocalDb` is internal
|
||||
internal LocalDbTestDatabase(TestDatabaseSettings settings, ILoggerFactory loggerFactory, LocalDb localDb, IUmbracoDatabaseFactory dbFactory)
|
||||
{
|
||||
public const string InstanceName = "UmbracoTests";
|
||||
public const string DatabaseName = "UmbracoTests";
|
||||
_loggerFactory = loggerFactory;
|
||||
_databaseFactory = dbFactory;
|
||||
|
||||
private readonly TestDatabaseSettings _settings;
|
||||
private readonly LocalDb _localDb;
|
||||
private static LocalDb.Instance s_localDbInstance;
|
||||
private static string s_filesPath;
|
||||
_settings = settings;
|
||||
_localDb = localDb;
|
||||
s_filesPath = settings.FilesPath;
|
||||
|
||||
// It's internal because `Umbraco.Core.Persistence.LocalDb` is internal
|
||||
internal LocalDbTestDatabase(TestDatabaseSettings settings, ILoggerFactory loggerFactory, LocalDb localDb, IUmbracoDatabaseFactory dbFactory)
|
||||
var counter = 0;
|
||||
|
||||
var schema = Enumerable.Range(0, _settings.SchemaDatabaseCount)
|
||||
.Select(x => TestDbMeta.CreateWithoutConnectionString($"{DatabaseName}-{++counter}", false));
|
||||
|
||||
var empty = Enumerable.Range(0, _settings.EmptyDatabasesCount)
|
||||
.Select(x => TestDbMeta.CreateWithoutConnectionString($"{DatabaseName}-{++counter}", true));
|
||||
|
||||
_testDatabases = schema.Concat(empty).ToList();
|
||||
|
||||
s_localDbInstance = _localDb.GetInstance(InstanceName);
|
||||
if (s_localDbInstance != null)
|
||||
{
|
||||
_loggerFactory = loggerFactory;
|
||||
_databaseFactory = dbFactory;
|
||||
|
||||
_settings = settings;
|
||||
_localDb = localDb;
|
||||
s_filesPath = settings.FilesPath;
|
||||
|
||||
var counter = 0;
|
||||
|
||||
var schema = Enumerable.Range(0, _settings.SchemaDatabaseCount)
|
||||
.Select(x => TestDbMeta.CreateWithoutConnectionString($"{DatabaseName}-{++counter}", false));
|
||||
|
||||
var empty = Enumerable.Range(0, _settings.EmptyDatabasesCount)
|
||||
.Select(x => TestDbMeta.CreateWithoutConnectionString($"{DatabaseName}-{++counter}", true));
|
||||
|
||||
_testDatabases = schema.Concat(empty).ToList();
|
||||
|
||||
s_localDbInstance = _localDb.GetInstance(InstanceName);
|
||||
if (s_localDbInstance != null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_localDb.CreateInstance(InstanceName) == false)
|
||||
{
|
||||
throw new Exception("Failed to create a LocalDb instance.");
|
||||
}
|
||||
|
||||
s_localDbInstance = _localDb.GetInstance(InstanceName);
|
||||
return;
|
||||
}
|
||||
|
||||
protected override void Initialize()
|
||||
if (_localDb.CreateInstance(InstanceName) == false)
|
||||
{
|
||||
string tempName = Guid.NewGuid().ToString("N");
|
||||
s_localDbInstance.CreateDatabase(tempName, s_filesPath);
|
||||
s_localDbInstance.DetachDatabase(tempName);
|
||||
|
||||
_prepareQueue = new BlockingCollection<TestDbMeta>();
|
||||
_readySchemaQueue = new BlockingCollection<TestDbMeta>();
|
||||
_readyEmptyQueue = new BlockingCollection<TestDbMeta>();
|
||||
|
||||
for (int i = 0; i < _testDatabases.Count; i++)
|
||||
{
|
||||
TestDbMeta meta = _testDatabases[i];
|
||||
bool isLast = i == _testDatabases.Count - 1;
|
||||
|
||||
_localDb.CopyDatabaseFiles(tempName, s_filesPath, targetDatabaseName: meta.Name, overwrite: true, delete: isLast);
|
||||
meta.ConnectionString = s_localDbInstance.GetAttachedConnectionString(meta.Name, s_filesPath);
|
||||
_prepareQueue.Add(meta);
|
||||
}
|
||||
|
||||
for (int i = 0; i < _settings.PrepareThreadCount; i++)
|
||||
{
|
||||
var thread = new Thread(PrepareDatabase);
|
||||
thread.Start();
|
||||
}
|
||||
throw new Exception("Failed to create a LocalDb instance.");
|
||||
}
|
||||
|
||||
public override void TearDown()
|
||||
s_localDbInstance = _localDb.GetInstance(InstanceName);
|
||||
}
|
||||
|
||||
protected override void Initialize()
|
||||
{
|
||||
var tempName = Guid.NewGuid().ToString("N");
|
||||
s_localDbInstance.CreateDatabase(tempName, s_filesPath);
|
||||
s_localDbInstance.DetachDatabase(tempName);
|
||||
|
||||
_prepareQueue = new BlockingCollection<TestDbMeta>();
|
||||
_readySchemaQueue = new BlockingCollection<TestDbMeta>();
|
||||
_readyEmptyQueue = new BlockingCollection<TestDbMeta>();
|
||||
|
||||
for (var i = 0; i < _testDatabases.Count; i++)
|
||||
{
|
||||
if (_prepareQueue == null)
|
||||
var meta = _testDatabases[i];
|
||||
var isLast = i == _testDatabases.Count - 1;
|
||||
|
||||
_localDb.CopyDatabaseFiles(tempName, s_filesPath, meta.Name, overwrite: true, delete: isLast);
|
||||
meta.ConnectionString = s_localDbInstance.GetAttachedConnectionString(meta.Name, s_filesPath);
|
||||
_prepareQueue.Add(meta);
|
||||
}
|
||||
|
||||
for (var i = 0; i < _settings.PrepareThreadCount; i++)
|
||||
{
|
||||
var thread = new Thread(PrepareDatabase);
|
||||
thread.Start();
|
||||
}
|
||||
}
|
||||
|
||||
public override void TearDown()
|
||||
{
|
||||
if (_prepareQueue == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_prepareQueue.CompleteAdding();
|
||||
while (_prepareQueue.TryTake(out _))
|
||||
{
|
||||
}
|
||||
|
||||
_readyEmptyQueue.CompleteAdding();
|
||||
while (_readyEmptyQueue.TryTake(out _))
|
||||
{
|
||||
}
|
||||
|
||||
_readySchemaQueue.CompleteAdding();
|
||||
while (_readySchemaQueue.TryTake(out _))
|
||||
{
|
||||
}
|
||||
|
||||
if (s_filesPath == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var filename = Path.Combine(s_filesPath, DatabaseName).ToUpper();
|
||||
|
||||
Parallel.ForEach(s_localDbInstance.GetDatabases(), instance =>
|
||||
{
|
||||
if (instance.StartsWith(filename))
|
||||
{
|
||||
return;
|
||||
s_localDbInstance.DropDatabase(instance);
|
||||
}
|
||||
});
|
||||
|
||||
_localDb.StopInstance(InstanceName);
|
||||
|
||||
foreach (var file in Directory.EnumerateFiles(s_filesPath))
|
||||
{
|
||||
if (file.EndsWith(".mdf") == false && file.EndsWith(".ldf") == false)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
_prepareQueue.CompleteAdding();
|
||||
while (_prepareQueue.TryTake(out _))
|
||||
try
|
||||
{
|
||||
File.Delete(file);
|
||||
}
|
||||
|
||||
_readyEmptyQueue.CompleteAdding();
|
||||
while (_readyEmptyQueue.TryTake(out _))
|
||||
catch (IOException)
|
||||
{
|
||||
}
|
||||
|
||||
_readySchemaQueue.CompleteAdding();
|
||||
while (_readySchemaQueue.TryTake(out _))
|
||||
{
|
||||
}
|
||||
|
||||
if (s_filesPath == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
string filename = Path.Combine(s_filesPath, DatabaseName).ToUpper();
|
||||
|
||||
Parallel.ForEach(s_localDbInstance.GetDatabases(), instance =>
|
||||
{
|
||||
if (instance.StartsWith(filename))
|
||||
{
|
||||
s_localDbInstance.DropDatabase(instance);
|
||||
}
|
||||
});
|
||||
|
||||
_localDb.StopInstance(InstanceName);
|
||||
|
||||
foreach (string file in Directory.EnumerateFiles(s_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
|
||||
}
|
||||
// ignore, must still be in use but nothing we can do
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@ namespace Umbraco.Cms.Tests.Integration.Testing;
|
||||
|
||||
public abstract class SqlServerBaseTestDatabase : BaseTestDatabase
|
||||
{
|
||||
|
||||
protected UmbracoDatabase.CommandInfo[] _cachedDatabaseInitCommands = new UmbracoDatabase.CommandInfo[0];
|
||||
|
||||
protected override void ResetTestDatabase(TestDbMeta meta)
|
||||
@@ -58,14 +57,13 @@ public abstract class SqlServerBaseTestDatabase : BaseTestDatabase
|
||||
command.CommandText = sql;
|
||||
command.Parameters.Clear();
|
||||
|
||||
for (int i = 0; i < args.Length; i++)
|
||||
for (var i = 0; i < args.Length; i++)
|
||||
{
|
||||
command.Parameters.AddWithValue("@" + i, args[i]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
protected override DbConnection GetConnection(TestDbMeta meta) => new SqlConnection(meta.ConnectionString);
|
||||
|
||||
protected override void RebuildSchema(IDbCommand command, TestDbMeta meta)
|
||||
@@ -79,12 +77,12 @@ public abstract class SqlServerBaseTestDatabase : BaseTestDatabase
|
||||
}
|
||||
}
|
||||
|
||||
foreach (UmbracoDatabase.CommandInfo dbCommand in _cachedDatabaseInitCommands)
|
||||
foreach (var dbCommand in _cachedDatabaseInitCommands)
|
||||
{
|
||||
command.CommandText = dbCommand.Text;
|
||||
command.Parameters.Clear();
|
||||
|
||||
foreach (UmbracoDatabase.ParameterInfo parameterInfo in dbCommand.Parameters)
|
||||
foreach (var parameterInfo in dbCommand.Parameters)
|
||||
{
|
||||
AddParameter(command, parameterInfo);
|
||||
}
|
||||
@@ -101,13 +99,16 @@ public abstract class SqlServerBaseTestDatabase : BaseTestDatabase
|
||||
{
|
||||
database.LogCommands = true;
|
||||
|
||||
using (NPoco.ITransaction transaction = database.GetTransaction())
|
||||
using (var transaction = database.GetTransaction())
|
||||
{
|
||||
var options = new TestOptionsMonitor<InstallDefaultDataSettings>(new InstallDefaultDataSettings { InstallData = InstallDefaultDataOption.All });
|
||||
var options =
|
||||
new TestOptionsMonitor<InstallDefaultDataSettings>(
|
||||
new InstallDefaultDataSettings { InstallData = InstallDefaultDataOption.All });
|
||||
|
||||
var schemaCreator = new DatabaseSchemaCreator(
|
||||
database,
|
||||
_loggerFactory.CreateLogger<DatabaseSchemaCreator>(), _loggerFactory,
|
||||
_loggerFactory.CreateLogger<DatabaseSchemaCreator>(),
|
||||
_loggerFactory,
|
||||
new UmbracoVersion(),
|
||||
Mock.Of<IEventAggregator>(),
|
||||
options);
|
||||
|
||||
@@ -11,121 +11,117 @@ using Microsoft.Extensions.Logging;
|
||||
using Umbraco.Cms.Infrastructure.Persistence;
|
||||
|
||||
// ReSharper disable ConvertToUsingDeclaration
|
||||
namespace Umbraco.Cms.Tests.Integration.Testing
|
||||
namespace Umbraco.Cms.Tests.Integration.Testing;
|
||||
|
||||
/// <remarks>
|
||||
/// It's not meant to be pretty, rushed port of LocalDb.cs + LocalDbTestDatabase.cs
|
||||
/// </remarks>
|
||||
public class SqlServerTestDatabase : SqlServerBaseTestDatabase, ITestDatabase
|
||||
{
|
||||
/// <remarks>
|
||||
/// It's not meant to be pretty, rushed port of LocalDb.cs + LocalDbTestDatabase.cs
|
||||
/// </remarks>
|
||||
public class SqlServerTestDatabase : SqlServerBaseTestDatabase, ITestDatabase
|
||||
public const string DatabaseName = "UmbracoTests";
|
||||
private readonly TestDatabaseSettings _settings;
|
||||
|
||||
public SqlServerTestDatabase(TestDatabaseSettings settings, ILoggerFactory loggerFactory, IUmbracoDatabaseFactory databaseFactory)
|
||||
{
|
||||
private readonly TestDatabaseSettings _settings;
|
||||
public const string DatabaseName = "UmbracoTests";
|
||||
_loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory));
|
||||
_databaseFactory = databaseFactory ?? throw new ArgumentNullException(nameof(databaseFactory));
|
||||
|
||||
public SqlServerTestDatabase(TestDatabaseSettings settings, ILoggerFactory loggerFactory,
|
||||
IUmbracoDatabaseFactory databaseFactory)
|
||||
_settings = settings;
|
||||
|
||||
var counter = 0;
|
||||
|
||||
var schema = Enumerable.Range(0, _settings.SchemaDatabaseCount)
|
||||
.Select(x => TestDbMeta.CreateWithMasterConnectionString($"{DatabaseName}-{++counter}", false, _settings.SQLServerMasterConnectionString));
|
||||
|
||||
var empty = Enumerable.Range(0, _settings.EmptyDatabasesCount)
|
||||
.Select(x => TestDbMeta.CreateWithMasterConnectionString($"{DatabaseName}-{++counter}", true, _settings.SQLServerMasterConnectionString));
|
||||
|
||||
_testDatabases = schema.Concat(empty).ToList();
|
||||
}
|
||||
|
||||
protected override void Initialize()
|
||||
{
|
||||
_prepareQueue = new BlockingCollection<TestDbMeta>();
|
||||
_readySchemaQueue = new BlockingCollection<TestDbMeta>();
|
||||
_readyEmptyQueue = new BlockingCollection<TestDbMeta>();
|
||||
|
||||
foreach (var meta in _testDatabases)
|
||||
{
|
||||
_loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory));
|
||||
_databaseFactory = databaseFactory ?? throw new ArgumentNullException(nameof(databaseFactory));
|
||||
|
||||
_settings = settings;
|
||||
|
||||
var counter = 0;
|
||||
|
||||
var schema = Enumerable.Range(0, _settings.SchemaDatabaseCount)
|
||||
.Select(x => TestDbMeta.CreateWithMasterConnectionString($"{DatabaseName}-{++counter}", false,
|
||||
_settings.SQLServerMasterConnectionString));
|
||||
|
||||
var empty = Enumerable.Range(0, _settings.EmptyDatabasesCount)
|
||||
.Select(x => TestDbMeta.CreateWithMasterConnectionString($"{DatabaseName}-{++counter}", true,
|
||||
_settings.SQLServerMasterConnectionString));
|
||||
|
||||
_testDatabases = schema.Concat(empty).ToList();
|
||||
CreateDatabase(meta);
|
||||
_prepareQueue.Add(meta);
|
||||
}
|
||||
|
||||
protected override void Initialize()
|
||||
for (var i = 0; i < _settings.PrepareThreadCount; i++)
|
||||
{
|
||||
_prepareQueue = new BlockingCollection<TestDbMeta>();
|
||||
_readySchemaQueue = new BlockingCollection<TestDbMeta>();
|
||||
_readyEmptyQueue = new BlockingCollection<TestDbMeta>();
|
||||
var thread = new Thread(PrepareDatabase);
|
||||
thread.Start();
|
||||
}
|
||||
}
|
||||
|
||||
foreach (TestDbMeta meta in _testDatabases)
|
||||
{
|
||||
CreateDatabase(meta);
|
||||
_prepareQueue.Add(meta);
|
||||
}
|
||||
private void CreateDatabase(TestDbMeta meta)
|
||||
{
|
||||
Drop(meta);
|
||||
|
||||
for (int i = 0; i < _settings.PrepareThreadCount; i++)
|
||||
using (var connection = new SqlConnection(_settings.SQLServerMasterConnectionString))
|
||||
{
|
||||
connection.Open();
|
||||
using (var command = connection.CreateCommand())
|
||||
{
|
||||
var thread = new Thread(PrepareDatabase);
|
||||
thread.Start();
|
||||
SetCommand(command, $@"CREATE DATABASE {LocalDb.QuotedName(meta.Name)}");
|
||||
command.ExecuteNonQuery();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void CreateDatabase(TestDbMeta meta)
|
||||
private void Drop(TestDbMeta meta)
|
||||
{
|
||||
using (var connection = new SqlConnection(_settings.SQLServerMasterConnectionString))
|
||||
{
|
||||
Drop(meta);
|
||||
|
||||
using (var connection = new SqlConnection(_settings.SQLServerMasterConnectionString))
|
||||
connection.Open();
|
||||
using (var command = connection.CreateCommand())
|
||||
{
|
||||
connection.Open();
|
||||
using (SqlCommand command = connection.CreateCommand())
|
||||
SetCommand(command, "select count(1) from sys.databases where name = @0", meta.Name);
|
||||
var records = (int)command.ExecuteScalar();
|
||||
if (records == 0)
|
||||
{
|
||||
SetCommand(command, $@"CREATE DATABASE {LocalDb.QuotedName(meta.Name)}");
|
||||
command.ExecuteNonQuery();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void Drop(TestDbMeta meta)
|
||||
{
|
||||
using (var connection = new SqlConnection(_settings.SQLServerMasterConnectionString))
|
||||
{
|
||||
connection.Open();
|
||||
using (SqlCommand command = connection.CreateCommand())
|
||||
{
|
||||
SetCommand(command, "select count(1) from sys.databases where name = @0", meta.Name);
|
||||
var records = (int)command.ExecuteScalar();
|
||||
if (records == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
string sql = $@"
|
||||
var sql = $@"
|
||||
ALTER DATABASE {LocalDb.QuotedName(meta.Name)}
|
||||
SET SINGLE_USER
|
||||
WITH ROLLBACK IMMEDIATE";
|
||||
SetCommand(command, sql);
|
||||
command.ExecuteNonQuery();
|
||||
SetCommand(command, sql);
|
||||
command.ExecuteNonQuery();
|
||||
|
||||
SetCommand(command, $@"DROP DATABASE {LocalDb.QuotedName(meta.Name)}");
|
||||
command.ExecuteNonQuery();
|
||||
}
|
||||
SetCommand(command, $@"DROP DATABASE {LocalDb.QuotedName(meta.Name)}");
|
||||
command.ExecuteNonQuery();
|
||||
}
|
||||
}
|
||||
|
||||
public override void TearDown()
|
||||
{
|
||||
if (_prepareQueue == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_prepareQueue.CompleteAdding();
|
||||
while (_prepareQueue.TryTake(out _))
|
||||
{
|
||||
}
|
||||
|
||||
_readyEmptyQueue.CompleteAdding();
|
||||
while (_readyEmptyQueue.TryTake(out _))
|
||||
{
|
||||
}
|
||||
|
||||
_readySchemaQueue.CompleteAdding();
|
||||
while (_readySchemaQueue.TryTake(out _))
|
||||
{
|
||||
}
|
||||
|
||||
Parallel.ForEach(_testDatabases, Drop);
|
||||
}
|
||||
}
|
||||
|
||||
public override void TearDown()
|
||||
{
|
||||
if (_prepareQueue == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_prepareQueue.CompleteAdding();
|
||||
while (_prepareQueue.TryTake(out _))
|
||||
{
|
||||
}
|
||||
|
||||
_readyEmptyQueue.CompleteAdding();
|
||||
while (_readyEmptyQueue.TryTake(out _))
|
||||
{
|
||||
}
|
||||
|
||||
_readySchemaQueue.CompleteAdding();
|
||||
while (_readySchemaQueue.TryTake(out _))
|
||||
{
|
||||
}
|
||||
|
||||
Parallel.ForEach(_testDatabases, Drop);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,10 +2,8 @@ using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Data;
|
||||
using System.Data.Common;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Data.Sqlite;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Moq;
|
||||
@@ -16,22 +14,21 @@ using Umbraco.Cms.Core.Events;
|
||||
using Umbraco.Cms.Infrastructure.Migrations.Install;
|
||||
using Umbraco.Cms.Infrastructure.Persistence;
|
||||
using Umbraco.Cms.Infrastructure.Persistence.Mappers;
|
||||
using Umbraco.Cms.Persistence.Sqlite;
|
||||
using Umbraco.Cms.Persistence.Sqlite.Mappers;
|
||||
using Umbraco.Cms.Persistence.Sqlite.Services;
|
||||
using Umbraco.Cms.Tests.Common;
|
||||
|
||||
namespace Umbraco.Cms.Tests.Integration.Testing;
|
||||
|
||||
public class SqliteTestDatabase : BaseTestDatabase, ITestDatabase
|
||||
{
|
||||
private readonly TestDatabaseSettings _settings;
|
||||
private readonly TestUmbracoDatabaseFactoryProvider _dbFactoryProvider;
|
||||
public const string DatabaseName = "UmbracoTests";
|
||||
private readonly TestUmbracoDatabaseFactoryProvider _dbFactoryProvider;
|
||||
private readonly TestDatabaseSettings _settings;
|
||||
|
||||
protected UmbracoDatabase.CommandInfo[] _cachedDatabaseInitCommands = new UmbracoDatabase.CommandInfo[0];
|
||||
|
||||
public SqliteTestDatabase(TestDatabaseSettings settings, TestUmbracoDatabaseFactoryProvider dbFactoryProvider,
|
||||
ILoggerFactory loggerFactory)
|
||||
public SqliteTestDatabase(TestDatabaseSettings settings, TestUmbracoDatabaseFactoryProvider dbFactoryProvider, ILoggerFactory loggerFactory)
|
||||
{
|
||||
_settings = settings ?? throw new ArgumentNullException(nameof(settings));
|
||||
_dbFactoryProvider = dbFactoryProvider;
|
||||
@@ -47,13 +44,19 @@ public class SqliteTestDatabase : BaseTestDatabase, ITestDatabase
|
||||
_testDatabases = schema.Concat(empty).ToList();
|
||||
}
|
||||
|
||||
public override void Detach(TestDbMeta meta)
|
||||
{
|
||||
meta.Connection.Close();
|
||||
_prepareQueue.TryAdd(CreateSqLiteMeta(meta.IsEmpty));
|
||||
}
|
||||
|
||||
protected override void Initialize()
|
||||
{
|
||||
_prepareQueue = new BlockingCollection<TestDbMeta>();
|
||||
_readySchemaQueue = new BlockingCollection<TestDbMeta>();
|
||||
_readyEmptyQueue = new BlockingCollection<TestDbMeta>();
|
||||
|
||||
foreach (TestDbMeta meta in _testDatabases)
|
||||
foreach (var meta in _testDatabases)
|
||||
{
|
||||
_prepareQueue.Add(meta);
|
||||
}
|
||||
@@ -72,12 +75,6 @@ public class SqliteTestDatabase : BaseTestDatabase, ITestDatabase
|
||||
meta.Connection.Open();
|
||||
}
|
||||
|
||||
public override void Detach(TestDbMeta meta)
|
||||
{
|
||||
meta.Connection.Close();
|
||||
_prepareQueue.TryAdd(CreateSqLiteMeta(meta.IsEmpty));
|
||||
}
|
||||
|
||||
protected override DbConnection GetConnection(TestDbMeta meta) => new SqliteConnection(meta.ConnectionString);
|
||||
|
||||
protected override void RebuildSchema(IDbCommand command, TestDbMeta meta)
|
||||
@@ -101,7 +98,7 @@ public class SqliteTestDatabase : BaseTestDatabase, ITestDatabase
|
||||
database.Mappers.Add(new NullableDateMapper());
|
||||
database.Mappers.Add(new SqlitePocoGuidMapper());
|
||||
|
||||
foreach (UmbracoDatabase.CommandInfo dbCommand in _cachedDatabaseInitCommands)
|
||||
foreach (var dbCommand in _cachedDatabaseInitCommands)
|
||||
{
|
||||
database.Execute(dbCommand.Text, dbCommand.Parameters.Select(x => x.Value).ToArray());
|
||||
}
|
||||
@@ -117,9 +114,11 @@ public class SqliteTestDatabase : BaseTestDatabase, ITestDatabase
|
||||
using var database = (UmbracoDatabase)dbFactory.CreateDatabase();
|
||||
database.LogCommands = true;
|
||||
|
||||
using NPoco.ITransaction transaction = database.GetTransaction();
|
||||
using var transaction = database.GetTransaction();
|
||||
|
||||
var options = new TestOptionsMonitor<InstallDefaultDataSettings>(new InstallDefaultDataSettings { InstallData = InstallDefaultDataOption.All });
|
||||
var options =
|
||||
new TestOptionsMonitor<InstallDefaultDataSettings>(
|
||||
new InstallDefaultDataSettings { InstallData = InstallDefaultDataOption.All });
|
||||
|
||||
var schemaCreator = new DatabaseSchemaCreator(
|
||||
database,
|
||||
@@ -145,26 +144,29 @@ public class SqliteTestDatabase : BaseTestDatabase, ITestDatabase
|
||||
}
|
||||
|
||||
_prepareQueue.CompleteAdding();
|
||||
while (_prepareQueue.TryTake(out _)) { }
|
||||
while (_prepareQueue.TryTake(out _))
|
||||
{ }
|
||||
|
||||
_readyEmptyQueue.CompleteAdding();
|
||||
while (_readyEmptyQueue.TryTake(out _)) { }
|
||||
while (_readyEmptyQueue.TryTake(out _))
|
||||
{ }
|
||||
|
||||
_readySchemaQueue.CompleteAdding();
|
||||
while (_readySchemaQueue.TryTake(out _)) { }
|
||||
while (_readySchemaQueue.TryTake(out _))
|
||||
{ }
|
||||
}
|
||||
|
||||
private TestDbMeta CreateSqLiteMeta(bool empty)
|
||||
{
|
||||
var builder = new SqliteConnectionStringBuilder()
|
||||
var builder = new SqliteConnectionStringBuilder
|
||||
{
|
||||
DataSource = $"{Guid.NewGuid()}",
|
||||
Mode = SqliteOpenMode.Memory,
|
||||
ForeignKeys = true,
|
||||
Pooling = false, // When pooling true, files kept open after connections closed, bad for cleanup.
|
||||
Cache = SqliteCacheMode.Shared,
|
||||
Cache = SqliteCacheMode.Shared
|
||||
};
|
||||
|
||||
return new TestDbMeta(builder.DataSource, empty, builder.ConnectionString, Persistence.Sqlite.Constants.ProviderName, "InMemory");
|
||||
return new TestDbMeta(builder.DataSource, empty, builder.ConnectionString, Constants.ProviderName, "InMemory");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
using System;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
|
||||
namespace Umbraco.Cms.Tests.Integration.Testing
|
||||
namespace Umbraco.Cms.Tests.Integration.Testing;
|
||||
|
||||
public class TestConflictingRouteService : IConflictingRouteService
|
||||
{
|
||||
public class TestConflictingRouteService : IConflictingRouteService
|
||||
public bool HasConflictingRoutes(out string controllername)
|
||||
{
|
||||
public bool HasConflictingRoutes(out string controllername)
|
||||
{
|
||||
controllername = string.Empty;
|
||||
return false;
|
||||
}
|
||||
controllername = string.Empty;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,57 +2,53 @@
|
||||
// See LICENSE for more details.
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Umbraco.Cms.Infrastructure.Persistence;
|
||||
|
||||
namespace Umbraco.Cms.Tests.Integration.Testing
|
||||
namespace Umbraco.Cms.Tests.Integration.Testing;
|
||||
|
||||
public static class TestDatabaseFactory
|
||||
{
|
||||
public static class TestDatabaseFactory
|
||||
/// <summary>
|
||||
/// Creates a TestDatabase instance
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// SQL Server setup requires configured master connection string & privileges to create database.
|
||||
/// </remarks>
|
||||
/// <example>
|
||||
/// <code>
|
||||
/// # SQL Server Environment variable setup
|
||||
/// $ export Tests__Database__DatabaseType="SqlServer"
|
||||
/// $ export Tests__Database__SQLServerMasterConnectionString="Server=localhost,1433; User Id=sa; Password=MySuperSecretPassword123!;"
|
||||
/// </code>
|
||||
/// </example>
|
||||
/// <example>
|
||||
/// <code>
|
||||
/// # Docker cheat sheet
|
||||
/// $ docker run -e 'ACCEPT_EULA=Y' -e "SA_PASSWORD=MySuperSecretPassword123!" -e 'MSSQL_PID=Developer' -p 1433:1433 -d mcr.microsoft.com/mssql/server:2017-latest-ubuntu
|
||||
/// </code>
|
||||
/// </example>
|
||||
public static ITestDatabase Create(TestDatabaseSettings settings, TestUmbracoDatabaseFactoryProvider dbFactory, ILoggerFactory loggerFactory) =>
|
||||
settings.DatabaseType switch
|
||||
{
|
||||
TestDatabaseSettings.TestDatabaseType.Sqlite => new SqliteTestDatabase(settings, dbFactory, loggerFactory),
|
||||
TestDatabaseSettings.TestDatabaseType.SqlServer => CreateSqlServer(settings, loggerFactory, dbFactory),
|
||||
TestDatabaseSettings.TestDatabaseType.LocalDb => CreateLocalDb(settings, loggerFactory, dbFactory),
|
||||
_ => throw new ApplicationException("Unsupported test database provider")
|
||||
};
|
||||
|
||||
private static ITestDatabase CreateLocalDb(TestDatabaseSettings settings, ILoggerFactory loggerFactory, TestUmbracoDatabaseFactoryProvider dbFactory)
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a TestDatabase instance
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// SQL Server setup requires configured master connection string & privileges to create database.
|
||||
/// </remarks>
|
||||
/// <example>
|
||||
/// <code>
|
||||
/// # SQL Server Environment variable setup
|
||||
/// $ export Tests__Database__DatabaseType="SqlServer"
|
||||
/// $ export Tests__Database__SQLServerMasterConnectionString="Server=localhost,1433; User Id=sa; Password=MySuperSecretPassword123!;"
|
||||
/// </code>
|
||||
/// </example>
|
||||
/// <example>
|
||||
/// <code>
|
||||
/// # Docker cheat sheet
|
||||
/// $ docker run -e 'ACCEPT_EULA=Y' -e "SA_PASSWORD=MySuperSecretPassword123!" -e 'MSSQL_PID=Developer' -p 1433:1433 -d mcr.microsoft.com/mssql/server:2017-latest-ubuntu
|
||||
/// </code>
|
||||
/// </example>
|
||||
public static ITestDatabase Create(TestDatabaseSettings settings, TestUmbracoDatabaseFactoryProvider dbFactory, ILoggerFactory loggerFactory) =>
|
||||
settings.DatabaseType switch
|
||||
{
|
||||
TestDatabaseSettings.TestDatabaseType.Sqlite=> new SqliteTestDatabase(settings, dbFactory, loggerFactory),
|
||||
TestDatabaseSettings.TestDatabaseType.SqlServer => CreateSqlServer(settings, loggerFactory, dbFactory),
|
||||
TestDatabaseSettings.TestDatabaseType.LocalDb => CreateLocalDb(settings, loggerFactory, dbFactory),
|
||||
_ => throw new ApplicationException("Unsupported test database provider")
|
||||
};
|
||||
var localDb = new LocalDb();
|
||||
|
||||
private static ITestDatabase CreateLocalDb(TestDatabaseSettings settings, ILoggerFactory loggerFactory, TestUmbracoDatabaseFactoryProvider dbFactory)
|
||||
if (!localDb.IsAvailable)
|
||||
{
|
||||
var localDb = new LocalDb();
|
||||
|
||||
if (!localDb.IsAvailable)
|
||||
{
|
||||
throw new InvalidOperationException("LocalDB is not available.");
|
||||
}
|
||||
|
||||
return new LocalDbTestDatabase(settings, loggerFactory, localDb, dbFactory.Create());
|
||||
throw new InvalidOperationException("LocalDB is not available.");
|
||||
}
|
||||
|
||||
private static ITestDatabase CreateSqlServer(TestDatabaseSettings settings, ILoggerFactory loggerFactory, TestUmbracoDatabaseFactoryProvider dbFactory)
|
||||
{
|
||||
return new SqlServerTestDatabase(settings, loggerFactory, dbFactory.Create());
|
||||
}
|
||||
return new LocalDbTestDatabase(settings, loggerFactory, localDb, dbFactory.Create());
|
||||
}
|
||||
|
||||
private static ITestDatabase CreateSqlServer(TestDatabaseSettings settings, ILoggerFactory loggerFactory, TestUmbracoDatabaseFactoryProvider dbFactory) =>
|
||||
new SqlServerTestDatabase(settings, loggerFactory, dbFactory.Create());
|
||||
}
|
||||
|
||||
@@ -1,29 +1,27 @@
|
||||
namespace Umbraco.Cms.Tests.Integration.Testing;
|
||||
|
||||
namespace Umbraco.Cms.Tests.Integration.Testing
|
||||
public class TestDatabaseSettings
|
||||
{
|
||||
public class TestDatabaseSettings
|
||||
public enum TestDatabaseType
|
||||
{
|
||||
public TestDatabaseType DatabaseType { get; set; }
|
||||
|
||||
public int PrepareThreadCount { get; set; }
|
||||
|
||||
public int SchemaDatabaseCount { get; set; }
|
||||
|
||||
public int EmptyDatabasesCount { get; set; }
|
||||
|
||||
public string FilesPath { get; set; }
|
||||
|
||||
/// <remarks>
|
||||
/// Only used for SQL Server e.g. on Linux/MacOS (not required for localdb).
|
||||
/// </remarks>
|
||||
public string SQLServerMasterConnectionString { get; set; }
|
||||
|
||||
public enum TestDatabaseType
|
||||
{
|
||||
Unknown,
|
||||
Sqlite,
|
||||
SqlServer,
|
||||
LocalDb
|
||||
}
|
||||
Unknown,
|
||||
Sqlite,
|
||||
SqlServer,
|
||||
LocalDb
|
||||
}
|
||||
|
||||
public TestDatabaseType DatabaseType { get; set; }
|
||||
|
||||
public int PrepareThreadCount { get; set; }
|
||||
|
||||
public int SchemaDatabaseCount { get; set; }
|
||||
|
||||
public int EmptyDatabasesCount { get; set; }
|
||||
|
||||
public string FilesPath { get; set; }
|
||||
|
||||
/// <remarks>
|
||||
/// Only used for SQL Server e.g. on Linux/MacOS (not required for localdb).
|
||||
/// </remarks>
|
||||
public string SQLServerMasterConnectionString { get; set; }
|
||||
}
|
||||
|
||||
@@ -4,47 +4,42 @@
|
||||
using System.Data.Common;
|
||||
using System.Text.RegularExpressions;
|
||||
using Umbraco.Cms.Core.Configuration.Models;
|
||||
using Umbraco.Cms.Persistence.SqlServer;
|
||||
|
||||
namespace Umbraco.Cms.Tests.Integration.Testing
|
||||
namespace Umbraco.Cms.Tests.Integration.Testing;
|
||||
|
||||
public class TestDbMeta
|
||||
{
|
||||
public class TestDbMeta
|
||||
public TestDbMeta(string name, bool isEmpty, string connectionString, string providerName, string path)
|
||||
{
|
||||
public string Name { get; }
|
||||
public bool IsEmpty { get; }
|
||||
public string ConnectionString { get; set; }
|
||||
public string Provider { get; set; }
|
||||
public string Path { get; set; } // Null if not embedded.
|
||||
public DbConnection Connection { get; set; } // for SQLite in memory, can move to subclass later.
|
||||
IsEmpty = isEmpty;
|
||||
Name = name;
|
||||
ConnectionString = connectionString;
|
||||
Provider = providerName;
|
||||
Path = path;
|
||||
}
|
||||
|
||||
public TestDbMeta(string name, bool isEmpty, string connectionString, string providerName, string path)
|
||||
{
|
||||
IsEmpty = isEmpty;
|
||||
Name = name;
|
||||
ConnectionString = connectionString;
|
||||
Provider = providerName;
|
||||
Path = path;
|
||||
}
|
||||
public string Name { get; }
|
||||
public bool IsEmpty { get; }
|
||||
public string ConnectionString { get; set; }
|
||||
public string Provider { get; set; }
|
||||
public string Path { get; set; } // Null if not embedded.
|
||||
public DbConnection Connection { get; set; } // for SQLite in memory, can move to subclass later.
|
||||
|
||||
private static string ConstructConnectionString(string masterConnectionString, string databaseName)
|
||||
{
|
||||
string prefix = Regex.Replace(masterConnectionString, "Database=.+?;", string.Empty);
|
||||
string connectionString = $"{prefix};Database={databaseName};";
|
||||
return connectionString.Replace(";;", ";");
|
||||
}
|
||||
private static string ConstructConnectionString(string masterConnectionString, string databaseName)
|
||||
{
|
||||
var prefix = Regex.Replace(masterConnectionString, "Database=.+?;", string.Empty);
|
||||
var connectionString = $"{prefix};Database={databaseName};";
|
||||
return connectionString.Replace(";;", ";");
|
||||
}
|
||||
|
||||
public static TestDbMeta CreateWithMasterConnectionString(string name, bool isEmpty, string masterConnectionString) =>
|
||||
new TestDbMeta(name, isEmpty, ConstructConnectionString(masterConnectionString, name), Persistence.SqlServer.Constants.ProviderName, null);
|
||||
|
||||
// LocalDb mdf funtimes
|
||||
public static TestDbMeta CreateWithoutConnectionString(string name, bool isEmpty) =>
|
||||
new TestDbMeta(name, isEmpty, null, Persistence.SqlServer.Constants.ProviderName, null);
|
||||
// LocalDb mdf funtimes
|
||||
public static TestDbMeta CreateWithoutConnectionString(string name, bool isEmpty) =>
|
||||
new(name, isEmpty, null, Constants.ProviderName, null);
|
||||
|
||||
public ConnectionStrings ToStronglyTypedConnectionString() =>
|
||||
new ConnectionStrings
|
||||
{
|
||||
Name = Name,
|
||||
ConnectionString = ConnectionString,
|
||||
ProviderName = Provider
|
||||
};
|
||||
}
|
||||
public ConnectionStrings ToStronglyTypedConnectionString() =>
|
||||
new() { Name = Name, ConnectionString = ConnectionString, ProviderName = Provider };
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// Copyright (c) Umbraco.
|
||||
// See LICENSE for more details.
|
||||
|
||||
using System;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Umbraco.Cms.Core.Configuration.Models;
|
||||
@@ -9,49 +8,48 @@ using Umbraco.Cms.Infrastructure.Migrations.Install;
|
||||
using Umbraco.Cms.Infrastructure.Persistence;
|
||||
using Umbraco.Cms.Infrastructure.Persistence.Mappers;
|
||||
|
||||
namespace Umbraco.Cms.Tests.Integration.Testing
|
||||
namespace Umbraco.Cms.Tests.Integration.Testing;
|
||||
|
||||
/// <summary>
|
||||
/// I want to be able to create a database for integration testsing without setting the connection string on the
|
||||
/// singleton database factory forever.
|
||||
/// </summary>
|
||||
public class TestUmbracoDatabaseFactoryProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// I want to be able to create a database for integration testsing without setting the connection string on the
|
||||
/// singleton database factory forever.
|
||||
/// </summary>
|
||||
public class TestUmbracoDatabaseFactoryProvider
|
||||
private readonly IOptionsMonitor<ConnectionStrings> _connectionStrings;
|
||||
private readonly DatabaseSchemaCreatorFactory _databaseSchemaCreatorFactory;
|
||||
private readonly IDbProviderFactoryCreator _dbProviderFactoryCreator;
|
||||
private readonly IOptions<GlobalSettings> _globalSettings;
|
||||
private readonly ILoggerFactory _loggerFactory;
|
||||
private readonly IMapperCollection _mappers;
|
||||
private readonly NPocoMapperCollection _npocoMappers;
|
||||
|
||||
public TestUmbracoDatabaseFactoryProvider(
|
||||
ILoggerFactory loggerFactory,
|
||||
IOptions<GlobalSettings> globalSettings,
|
||||
IOptionsMonitor<ConnectionStrings> connectionStrings,
|
||||
IMapperCollection mappers,
|
||||
IDbProviderFactoryCreator dbProviderFactoryCreator,
|
||||
DatabaseSchemaCreatorFactory databaseSchemaCreatorFactory,
|
||||
NPocoMapperCollection npocoMappers)
|
||||
{
|
||||
private readonly ILoggerFactory _loggerFactory;
|
||||
private readonly IOptions<GlobalSettings> _globalSettings;
|
||||
private readonly IOptionsMonitor<ConnectionStrings> _connectionStrings;
|
||||
private readonly IMapperCollection _mappers;
|
||||
private readonly IDbProviderFactoryCreator _dbProviderFactoryCreator;
|
||||
private readonly DatabaseSchemaCreatorFactory _databaseSchemaCreatorFactory;
|
||||
private readonly NPocoMapperCollection _npocoMappers;
|
||||
|
||||
public TestUmbracoDatabaseFactoryProvider(
|
||||
ILoggerFactory loggerFactory,
|
||||
IOptions<GlobalSettings> globalSettings,
|
||||
IOptionsMonitor<ConnectionStrings> connectionStrings,
|
||||
IMapperCollection mappers,
|
||||
IDbProviderFactoryCreator dbProviderFactoryCreator,
|
||||
DatabaseSchemaCreatorFactory databaseSchemaCreatorFactory,
|
||||
NPocoMapperCollection npocoMappers)
|
||||
{
|
||||
_loggerFactory = loggerFactory;
|
||||
_globalSettings = globalSettings;
|
||||
_connectionStrings = connectionStrings;
|
||||
_mappers = mappers;
|
||||
_dbProviderFactoryCreator = dbProviderFactoryCreator;
|
||||
_databaseSchemaCreatorFactory = databaseSchemaCreatorFactory;
|
||||
_npocoMappers = npocoMappers;
|
||||
}
|
||||
|
||||
public IUmbracoDatabaseFactory Create()
|
||||
=> new UmbracoDatabaseFactory(
|
||||
_loggerFactory.CreateLogger<UmbracoDatabaseFactory>(),
|
||||
_loggerFactory,
|
||||
_globalSettings,
|
||||
_connectionStrings,
|
||||
_mappers,
|
||||
_dbProviderFactoryCreator,
|
||||
_databaseSchemaCreatorFactory,
|
||||
_npocoMappers);
|
||||
_loggerFactory = loggerFactory;
|
||||
_globalSettings = globalSettings;
|
||||
_connectionStrings = connectionStrings;
|
||||
_mappers = mappers;
|
||||
_dbProviderFactoryCreator = dbProviderFactoryCreator;
|
||||
_databaseSchemaCreatorFactory = databaseSchemaCreatorFactory;
|
||||
_npocoMappers = npocoMappers;
|
||||
}
|
||||
|
||||
public IUmbracoDatabaseFactory Create()
|
||||
=> new UmbracoDatabaseFactory(
|
||||
_loggerFactory.CreateLogger<UmbracoDatabaseFactory>(),
|
||||
_loggerFactory,
|
||||
_globalSettings,
|
||||
_connectionStrings,
|
||||
_mappers,
|
||||
_dbProviderFactoryCreator,
|
||||
_databaseSchemaCreatorFactory,
|
||||
_npocoMappers);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
@@ -7,13 +6,10 @@ using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Cache;
|
||||
using Umbraco.Cms.Core.Composing;
|
||||
using Umbraco.Cms.Core.Configuration.Models;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
using Umbraco.Cms.Core.IO;
|
||||
using Umbraco.Cms.Core.Scoping;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.Strings;
|
||||
using Umbraco.Cms.Core.Web;
|
||||
@@ -27,175 +23,174 @@ using Umbraco.Cms.Tests.Integration.DependencyInjection;
|
||||
using Umbraco.Cms.Tests.Integration.Extensions;
|
||||
using Umbraco.Cms.Web.Common.Hosting;
|
||||
using Umbraco.Extensions;
|
||||
using IScopeProvider = Umbraco.Cms.Infrastructure.Scoping.IScopeProvider;
|
||||
using Constants = Umbraco.Cms.Core.Constants;
|
||||
|
||||
namespace Umbraco.Cms.Tests.Integration.Testing
|
||||
namespace Umbraco.Cms.Tests.Integration.Testing;
|
||||
|
||||
/// <summary>
|
||||
/// Abstract class for integration tests
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This will use a Host Builder to boot and install Umbraco ready for use
|
||||
/// </remarks>
|
||||
public abstract class UmbracoIntegrationTest : UmbracoIntegrationTestBase
|
||||
{
|
||||
private IHost _host;
|
||||
|
||||
protected IServiceProvider Services => _host.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Abstract class for integration tests
|
||||
/// Gets the <see cref="IScopeProvider" />
|
||||
/// </summary>
|
||||
protected IScopeProvider ScopeProvider => Services.GetRequiredService<IScopeProvider>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="IScopeAccessor" />
|
||||
/// </summary>
|
||||
protected IScopeAccessor ScopeAccessor => Services.GetRequiredService<IScopeAccessor>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="ILoggerFactory" />
|
||||
/// </summary>
|
||||
protected ILoggerFactory LoggerFactory => Services.GetRequiredService<ILoggerFactory>();
|
||||
|
||||
protected AppCaches AppCaches => Services.GetRequiredService<AppCaches>();
|
||||
|
||||
protected IIOHelper IOHelper => Services.GetRequiredService<IIOHelper>();
|
||||
|
||||
protected IShortStringHelper ShortStringHelper => Services.GetRequiredService<IShortStringHelper>();
|
||||
|
||||
protected GlobalSettings GlobalSettings => Services.GetRequiredService<IOptions<GlobalSettings>>().Value;
|
||||
|
||||
protected IMapperCollection Mappers => Services.GetRequiredService<IMapperCollection>();
|
||||
|
||||
protected UserBuilder UserBuilderInstance { get; } = new();
|
||||
|
||||
protected UserGroupBuilder UserGroupBuilderInstance { get; } = new();
|
||||
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
InMemoryConfiguration[Constants.Configuration.ConfigUnattended + ":" + nameof(UnattendedSettings.InstallUnattended)] = "true";
|
||||
var hostBuilder = CreateHostBuilder();
|
||||
|
||||
_host = hostBuilder.Build();
|
||||
UseTestDatabase(_host.Services);
|
||||
_host.Start();
|
||||
|
||||
if (TestOptions.Boot)
|
||||
{
|
||||
Services.GetRequiredService<IUmbracoContextFactory>().EnsureUmbracoContext();
|
||||
}
|
||||
}
|
||||
|
||||
[TearDown]
|
||||
public void TearDownAsync() => _host.StopAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Create the Generic Host and execute startup ConfigureServices/Configure calls
|
||||
/// </summary>
|
||||
private IHostBuilder CreateHostBuilder()
|
||||
{
|
||||
var hostBuilder = Host.CreateDefaultBuilder()
|
||||
.ConfigureUmbracoDefaults()
|
||||
|
||||
// IMPORTANT: We Cannot use UseStartup, there's all sorts of threads about this with testing. Although this can work
|
||||
// if you want to setup your tests this way, it is a bit annoying to do that as the WebApplicationFactory will
|
||||
// create separate Host instances. So instead of UseStartup, we just call ConfigureServices/Configure ourselves,
|
||||
// and in the case of the UmbracoTestServerTestBase it will use the ConfigureWebHost to Configure the IApplicationBuilder directly.
|
||||
.ConfigureAppConfiguration((context, configBuilder) =>
|
||||
{
|
||||
context.HostingEnvironment = TestHelper.GetWebHostEnvironment();
|
||||
configBuilder.Sources.Clear();
|
||||
configBuilder.AddInMemoryCollection(InMemoryConfiguration);
|
||||
configBuilder.AddConfiguration(GlobalSetupTeardown.TestConfiguration);
|
||||
|
||||
Configuration = configBuilder.Build();
|
||||
})
|
||||
.ConfigureServices((_, services) =>
|
||||
{
|
||||
ConfigureServices(services);
|
||||
ConfigureTestServices(services);
|
||||
services.AddUnique(CreateLoggerFactory());
|
||||
|
||||
if (!TestOptions.Boot)
|
||||
{
|
||||
// If boot is false, we don't want the CoreRuntime hosted service to start
|
||||
// So we replace it with a Mock
|
||||
services.AddUnique(Mock.Of<IRuntime>());
|
||||
}
|
||||
});
|
||||
|
||||
return hostBuilder;
|
||||
}
|
||||
|
||||
protected void ConfigureServices(IServiceCollection services)
|
||||
{
|
||||
services.AddTransient<TestUmbracoDatabaseFactoryProvider>();
|
||||
var webHostEnvironment = TestHelper.GetWebHostEnvironment();
|
||||
services.AddRequiredNetCoreServices(TestHelper, webHostEnvironment);
|
||||
|
||||
// We register this service because we need it for IRuntimeState, if we don't this breaks 900 tests
|
||||
services.AddSingleton<IConflictingRouteService, TestConflictingRouteService>();
|
||||
|
||||
services.AddLogger(webHostEnvironment, Configuration);
|
||||
|
||||
// Add it!
|
||||
var hostingEnvironment = TestHelper.GetHostingEnvironment();
|
||||
var typeLoader = services.AddTypeLoader(
|
||||
GetType().Assembly,
|
||||
hostingEnvironment,
|
||||
TestHelper.ConsoleLoggerFactory,
|
||||
AppCaches.NoCache,
|
||||
Configuration,
|
||||
TestHelper.Profiler);
|
||||
var builder = new UmbracoBuilder(services, Configuration, typeLoader, TestHelper.ConsoleLoggerFactory, TestHelper.Profiler, AppCaches.NoCache, hostingEnvironment);
|
||||
|
||||
builder.AddConfiguration()
|
||||
.AddUmbracoCore()
|
||||
.AddWebComponents()
|
||||
.AddRuntimeMinifier()
|
||||
.AddBackOfficeAuthentication()
|
||||
.AddBackOfficeIdentity()
|
||||
.AddMembersIdentity()
|
||||
.AddExamine()
|
||||
.AddUmbracoSqlServerSupport()
|
||||
.AddUmbracoSqliteSupport()
|
||||
.AddTestServices(TestHelper);
|
||||
|
||||
if (TestOptions.Mapper)
|
||||
{
|
||||
// TODO: Should these just be called from within AddUmbracoCore/AddWebComponents?
|
||||
builder
|
||||
.AddCoreMappingProfiles()
|
||||
.AddWebMappingProfiles();
|
||||
}
|
||||
|
||||
services.AddSignalR();
|
||||
services.AddMvc();
|
||||
|
||||
CustomTestSetup(builder);
|
||||
|
||||
builder.Build();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Hook for altering UmbracoBuilder setup
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This will use a Host Builder to boot and install Umbraco ready for use
|
||||
/// Can also be used for registering test doubles.
|
||||
/// </remarks>
|
||||
public abstract class UmbracoIntegrationTest : UmbracoIntegrationTestBase
|
||||
protected virtual void CustomTestSetup(IUmbracoBuilder builder)
|
||||
{
|
||||
private IHost _host;
|
||||
|
||||
protected IServiceProvider Services => _host.Services;
|
||||
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
InMemoryConfiguration[Core.Constants.Configuration.ConfigUnattended + ":" + nameof(UnattendedSettings.InstallUnattended)] = "true";
|
||||
IHostBuilder hostBuilder = CreateHostBuilder();
|
||||
|
||||
_host = hostBuilder.Build();
|
||||
UseTestDatabase(_host.Services);
|
||||
_host.Start();
|
||||
|
||||
if (TestOptions.Boot)
|
||||
{
|
||||
Services.GetRequiredService<IUmbracoContextFactory>().EnsureUmbracoContext();
|
||||
}
|
||||
}
|
||||
|
||||
[TearDown]
|
||||
public void TearDownAsync() => _host.StopAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Create the Generic Host and execute startup ConfigureServices/Configure calls
|
||||
/// </summary>
|
||||
private IHostBuilder CreateHostBuilder()
|
||||
{
|
||||
IHostBuilder hostBuilder = Host.CreateDefaultBuilder()
|
||||
.ConfigureUmbracoDefaults()
|
||||
|
||||
// IMPORTANT: We Cannot use UseStartup, there's all sorts of threads about this with testing. Although this can work
|
||||
// if you want to setup your tests this way, it is a bit annoying to do that as the WebApplicationFactory will
|
||||
// create separate Host instances. So instead of UseStartup, we just call ConfigureServices/Configure ourselves,
|
||||
// and in the case of the UmbracoTestServerTestBase it will use the ConfigureWebHost to Configure the IApplicationBuilder directly.
|
||||
.ConfigureAppConfiguration((context, configBuilder) =>
|
||||
{
|
||||
context.HostingEnvironment = TestHelper.GetWebHostEnvironment();
|
||||
configBuilder.Sources.Clear();
|
||||
configBuilder.AddInMemoryCollection(InMemoryConfiguration);
|
||||
configBuilder.AddConfiguration(GlobalSetupTeardown.TestConfiguration);
|
||||
|
||||
Configuration = configBuilder.Build();
|
||||
})
|
||||
.ConfigureServices((_, services) =>
|
||||
{
|
||||
ConfigureServices(services);
|
||||
ConfigureTestServices(services);
|
||||
services.AddUnique(CreateLoggerFactory());
|
||||
|
||||
if (!TestOptions.Boot)
|
||||
{
|
||||
// If boot is false, we don't want the CoreRuntime hosted service to start
|
||||
// So we replace it with a Mock
|
||||
services.AddUnique(Mock.Of<IRuntime>());
|
||||
}
|
||||
});
|
||||
|
||||
return hostBuilder;
|
||||
}
|
||||
|
||||
protected void ConfigureServices(IServiceCollection services)
|
||||
{
|
||||
services.AddTransient<TestUmbracoDatabaseFactoryProvider>();
|
||||
IWebHostEnvironment webHostEnvironment = TestHelper.GetWebHostEnvironment();
|
||||
services.AddRequiredNetCoreServices(TestHelper, webHostEnvironment);
|
||||
|
||||
// We register this service because we need it for IRuntimeState, if we don't this breaks 900 tests
|
||||
services.AddSingleton<IConflictingRouteService, TestConflictingRouteService>();
|
||||
|
||||
services.AddLogger(webHostEnvironment, Configuration);
|
||||
|
||||
// Add it!
|
||||
Core.Hosting.IHostingEnvironment hostingEnvironment = TestHelper.GetHostingEnvironment();
|
||||
TypeLoader typeLoader = services.AddTypeLoader(
|
||||
GetType().Assembly,
|
||||
hostingEnvironment,
|
||||
TestHelper.ConsoleLoggerFactory,
|
||||
AppCaches.NoCache,
|
||||
Configuration,
|
||||
TestHelper.Profiler);
|
||||
var builder = new UmbracoBuilder(services, Configuration, typeLoader, TestHelper.ConsoleLoggerFactory, TestHelper.Profiler, AppCaches.NoCache, hostingEnvironment);
|
||||
|
||||
builder.AddConfiguration()
|
||||
.AddUmbracoCore()
|
||||
.AddWebComponents()
|
||||
.AddRuntimeMinifier()
|
||||
.AddBackOfficeAuthentication()
|
||||
.AddBackOfficeIdentity()
|
||||
.AddMembersIdentity()
|
||||
.AddExamine()
|
||||
.AddUmbracoSqlServerSupport()
|
||||
.AddUmbracoSqliteSupport()
|
||||
.AddTestServices(TestHelper);
|
||||
|
||||
if (TestOptions.Mapper)
|
||||
{
|
||||
// TODO: Should these just be called from within AddUmbracoCore/AddWebComponents?
|
||||
builder
|
||||
.AddCoreMappingProfiles()
|
||||
.AddWebMappingProfiles();
|
||||
}
|
||||
|
||||
services.AddSignalR();
|
||||
services.AddMvc();
|
||||
|
||||
CustomTestSetup(builder);
|
||||
|
||||
builder.Build();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Hook for altering UmbracoBuilder setup
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Can also be used for registering test doubles.
|
||||
/// </remarks>
|
||||
protected virtual void CustomTestSetup(IUmbracoBuilder builder)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Hook for registering test doubles.
|
||||
/// </summary>
|
||||
protected virtual void ConfigureTestServices(IServiceCollection services)
|
||||
{
|
||||
}
|
||||
|
||||
protected virtual T GetRequiredService<T>() => Services.GetRequiredService<T>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="IScopeProvider"/>
|
||||
/// </summary>
|
||||
protected IScopeProvider ScopeProvider => Services.GetRequiredService<IScopeProvider>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="IScopeAccessor"/>
|
||||
/// </summary>
|
||||
protected IScopeAccessor ScopeAccessor => Services.GetRequiredService<IScopeAccessor>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="ILoggerFactory"/>
|
||||
/// </summary>
|
||||
protected ILoggerFactory LoggerFactory => Services.GetRequiredService<ILoggerFactory>();
|
||||
|
||||
protected AppCaches AppCaches => Services.GetRequiredService<AppCaches>();
|
||||
|
||||
protected IIOHelper IOHelper => Services.GetRequiredService<IIOHelper>();
|
||||
|
||||
protected IShortStringHelper ShortStringHelper => Services.GetRequiredService<IShortStringHelper>();
|
||||
|
||||
protected GlobalSettings GlobalSettings => Services.GetRequiredService<IOptions<GlobalSettings>>().Value;
|
||||
|
||||
protected IMapperCollection Mappers => Services.GetRequiredService<IMapperCollection>();
|
||||
|
||||
protected UserBuilder UserBuilderInstance { get; } = new ();
|
||||
|
||||
protected UserGroupBuilder UserGroupBuilderInstance { get; } = new ();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Hook for registering test doubles.
|
||||
/// </summary>
|
||||
protected virtual void ConfigureTestServices(IServiceCollection services)
|
||||
{
|
||||
}
|
||||
|
||||
protected virtual T GetRequiredService<T>() => Services.GetRequiredService<T>();
|
||||
}
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Data.Common;
|
||||
using System.IO;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.Data.SqlClient;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Logging.Console;
|
||||
using Microsoft.Extensions.Options;
|
||||
using NUnit.Framework;
|
||||
using Serilog;
|
||||
@@ -22,29 +19,29 @@ using Umbraco.Cms.Tests.Integration.Implementations;
|
||||
namespace Umbraco.Cms.Tests.Integration.Testing;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for all UmbracoIntegrationTests
|
||||
/// Base class for all UmbracoIntegrationTests
|
||||
/// </summary>
|
||||
[SingleThreaded]
|
||||
[NonParallelizable]
|
||||
public abstract class UmbracoIntegrationTestBase
|
||||
{
|
||||
private static readonly object s_dbLocker = new ();
|
||||
private static readonly object s_dbLocker = new();
|
||||
private static ITestDatabase s_dbInstance;
|
||||
private static TestDbMeta s_fixtureDbMeta;
|
||||
private static int s_testCount = 1;
|
||||
private readonly List<Action> _fixtureTeardown = new();
|
||||
private readonly Queue<Action> _testTeardown = new();
|
||||
|
||||
private bool _firstTestInFixture = true;
|
||||
private readonly Queue<Action> _testTeardown = new ();
|
||||
private readonly List<Action> _fixtureTeardown = new ();
|
||||
|
||||
protected Dictionary<string, string> InMemoryConfiguration { get; } = new ();
|
||||
protected Dictionary<string, string> InMemoryConfiguration { get; } = new();
|
||||
|
||||
protected IConfiguration Configuration { get; set; }
|
||||
|
||||
protected UmbracoTestAttribute TestOptions =>
|
||||
TestOptionAttributeBase.GetTestOptions<UmbracoTestAttribute>();
|
||||
|
||||
protected TestHelper TestHelper { get; } = new ();
|
||||
protected TestHelper TestHelper { get; } = new();
|
||||
|
||||
private void AddOnTestTearDown(Action tearDown) => _testTeardown.Enqueue(tearDown);
|
||||
|
||||
@@ -61,7 +58,7 @@ public abstract class UmbracoIntegrationTestBase
|
||||
[OneTimeTearDown]
|
||||
public void FixtureTearDown()
|
||||
{
|
||||
foreach (Action a in _fixtureTeardown)
|
||||
foreach (var a in _fixtureTeardown)
|
||||
{
|
||||
a();
|
||||
}
|
||||
@@ -72,7 +69,7 @@ public abstract class UmbracoIntegrationTestBase
|
||||
{
|
||||
_firstTestInFixture = false;
|
||||
|
||||
while (_testTeardown.TryDequeue(out Action a))
|
||||
while (_testTeardown.TryDequeue(out var a))
|
||||
{
|
||||
a();
|
||||
}
|
||||
@@ -120,11 +117,11 @@ public abstract class UmbracoIntegrationTestBase
|
||||
|
||||
protected void UseTestDatabase(IServiceProvider serviceProvider)
|
||||
{
|
||||
IRuntimeState state = serviceProvider.GetRequiredService<IRuntimeState>();
|
||||
TestUmbracoDatabaseFactoryProvider testDatabaseFactoryProvider = serviceProvider.GetRequiredService<TestUmbracoDatabaseFactoryProvider>();
|
||||
IUmbracoDatabaseFactory databaseFactory = serviceProvider.GetRequiredService<IUmbracoDatabaseFactory>();
|
||||
ILoggerFactory loggerFactory = serviceProvider.GetRequiredService<ILoggerFactory>();
|
||||
IOptionsMonitor<ConnectionStrings> connectionStrings = serviceProvider.GetRequiredService<IOptionsMonitor<ConnectionStrings>>();
|
||||
var state = serviceProvider.GetRequiredService<IRuntimeState>();
|
||||
var testDatabaseFactoryProvider = serviceProvider.GetRequiredService<TestUmbracoDatabaseFactoryProvider>();
|
||||
var databaseFactory = serviceProvider.GetRequiredService<IUmbracoDatabaseFactory>();
|
||||
var loggerFactory = serviceProvider.GetRequiredService<ILoggerFactory>();
|
||||
var connectionStrings = serviceProvider.GetRequiredService<IOptionsMonitor<ConnectionStrings>>();
|
||||
|
||||
// This will create a db, install the schema and ensure the app is configured to run
|
||||
SetupTestDatabase(testDatabaseFactoryProvider, connectionStrings, databaseFactory, loggerFactory, state);
|
||||
@@ -146,87 +143,86 @@ public abstract class UmbracoIntegrationTestBase
|
||||
}
|
||||
|
||||
private void SetupTestDatabase(
|
||||
TestUmbracoDatabaseFactoryProvider testUmbracoDatabaseFactoryProvider,
|
||||
IOptionsMonitor<ConnectionStrings> connectionStrings,
|
||||
IUmbracoDatabaseFactory databaseFactory,
|
||||
ILoggerFactory loggerFactory,
|
||||
IRuntimeState runtimeState)
|
||||
TestUmbracoDatabaseFactoryProvider testUmbracoDatabaseFactoryProvider,
|
||||
IOptionsMonitor<ConnectionStrings> connectionStrings,
|
||||
IUmbracoDatabaseFactory databaseFactory,
|
||||
ILoggerFactory loggerFactory,
|
||||
IRuntimeState runtimeState)
|
||||
{
|
||||
if (TestOptions.Database == UmbracoTestOptions.Database.None)
|
||||
{
|
||||
if (TestOptions.Database == UmbracoTestOptions.Database.None)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ITestDatabase db = GetOrCreateDatabase(loggerFactory, testUmbracoDatabaseFactoryProvider);
|
||||
|
||||
switch (TestOptions.Database)
|
||||
{
|
||||
case UmbracoTestOptions.Database.NewSchemaPerTest:
|
||||
|
||||
// New DB + Schema
|
||||
TestDbMeta newSchemaDbMeta = db.AttachSchema();
|
||||
|
||||
// Add teardown callback
|
||||
AddOnTestTearDown(() => db.Detach(newSchemaDbMeta));
|
||||
|
||||
ConfigureTestDatabaseFactory(newSchemaDbMeta, databaseFactory, runtimeState, connectionStrings);
|
||||
|
||||
Assert.AreEqual(RuntimeLevel.Run, runtimeState.Level);
|
||||
|
||||
break;
|
||||
case UmbracoTestOptions.Database.NewEmptyPerTest:
|
||||
TestDbMeta newEmptyDbMeta = db.AttachEmpty();
|
||||
|
||||
// Add teardown callback
|
||||
AddOnTestTearDown(() => db.Detach(newEmptyDbMeta));
|
||||
|
||||
ConfigureTestDatabaseFactory(newEmptyDbMeta, databaseFactory, runtimeState, connectionStrings);
|
||||
|
||||
Assert.AreEqual(RuntimeLevel.Install, runtimeState.Level);
|
||||
|
||||
break;
|
||||
case UmbracoTestOptions.Database.NewSchemaPerFixture:
|
||||
// Only attach schema once per fixture
|
||||
// Doing it more than once will block the process since the old db hasn't been detached
|
||||
// and it would be the same as NewSchemaPerTest even if it didn't block
|
||||
if (_firstTestInFixture)
|
||||
{
|
||||
// New DB + Schema
|
||||
TestDbMeta newSchemaFixtureDbMeta = db.AttachSchema();
|
||||
s_fixtureDbMeta = newSchemaFixtureDbMeta;
|
||||
|
||||
// Add teardown callback
|
||||
AddOnFixtureTearDown(() => db.Detach(newSchemaFixtureDbMeta));
|
||||
}
|
||||
|
||||
ConfigureTestDatabaseFactory(s_fixtureDbMeta, databaseFactory, runtimeState, connectionStrings);
|
||||
|
||||
break;
|
||||
case UmbracoTestOptions.Database.NewEmptyPerFixture:
|
||||
// Only attach schema once per fixture
|
||||
// Doing it more than once will block the process since the old db hasn't been detached
|
||||
// and it would be the same as NewSchemaPerTest even if it didn't block
|
||||
if (_firstTestInFixture)
|
||||
{
|
||||
// New DB + Schema
|
||||
TestDbMeta newEmptyFixtureDbMeta = db.AttachEmpty();
|
||||
s_fixtureDbMeta = newEmptyFixtureDbMeta;
|
||||
|
||||
// Add teardown callback
|
||||
AddOnFixtureTearDown(() => db.Detach(newEmptyFixtureDbMeta));
|
||||
}
|
||||
|
||||
ConfigureTestDatabaseFactory(s_fixtureDbMeta, databaseFactory, runtimeState, connectionStrings);
|
||||
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(TestOptions), TestOptions, null);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
var db = GetOrCreateDatabase(loggerFactory, testUmbracoDatabaseFactoryProvider);
|
||||
|
||||
private ITestDatabase GetOrCreateDatabase(ILoggerFactory loggerFactory,
|
||||
TestUmbracoDatabaseFactoryProvider dbFactory)
|
||||
switch (TestOptions.Database)
|
||||
{
|
||||
case UmbracoTestOptions.Database.NewSchemaPerTest:
|
||||
|
||||
// New DB + Schema
|
||||
var newSchemaDbMeta = db.AttachSchema();
|
||||
|
||||
// Add teardown callback
|
||||
AddOnTestTearDown(() => db.Detach(newSchemaDbMeta));
|
||||
|
||||
ConfigureTestDatabaseFactory(newSchemaDbMeta, databaseFactory, runtimeState, connectionStrings);
|
||||
|
||||
Assert.AreEqual(RuntimeLevel.Run, runtimeState.Level);
|
||||
|
||||
break;
|
||||
case UmbracoTestOptions.Database.NewEmptyPerTest:
|
||||
var newEmptyDbMeta = db.AttachEmpty();
|
||||
|
||||
// Add teardown callback
|
||||
AddOnTestTearDown(() => db.Detach(newEmptyDbMeta));
|
||||
|
||||
ConfigureTestDatabaseFactory(newEmptyDbMeta, databaseFactory, runtimeState, connectionStrings);
|
||||
|
||||
Assert.AreEqual(RuntimeLevel.Install, runtimeState.Level);
|
||||
|
||||
break;
|
||||
case UmbracoTestOptions.Database.NewSchemaPerFixture:
|
||||
// Only attach schema once per fixture
|
||||
// Doing it more than once will block the process since the old db hasn't been detached
|
||||
// and it would be the same as NewSchemaPerTest even if it didn't block
|
||||
if (_firstTestInFixture)
|
||||
{
|
||||
// New DB + Schema
|
||||
var newSchemaFixtureDbMeta = db.AttachSchema();
|
||||
s_fixtureDbMeta = newSchemaFixtureDbMeta;
|
||||
|
||||
// Add teardown callback
|
||||
AddOnFixtureTearDown(() => db.Detach(newSchemaFixtureDbMeta));
|
||||
}
|
||||
|
||||
ConfigureTestDatabaseFactory(s_fixtureDbMeta, databaseFactory, runtimeState, connectionStrings);
|
||||
|
||||
break;
|
||||
case UmbracoTestOptions.Database.NewEmptyPerFixture:
|
||||
// Only attach schema once per fixture
|
||||
// Doing it more than once will block the process since the old db hasn't been detached
|
||||
// and it would be the same as NewSchemaPerTest even if it didn't block
|
||||
if (_firstTestInFixture)
|
||||
{
|
||||
// New DB + Schema
|
||||
var newEmptyFixtureDbMeta = db.AttachEmpty();
|
||||
s_fixtureDbMeta = newEmptyFixtureDbMeta;
|
||||
|
||||
// Add teardown callback
|
||||
AddOnFixtureTearDown(() => db.Detach(newEmptyFixtureDbMeta));
|
||||
}
|
||||
|
||||
ConfigureTestDatabaseFactory(s_fixtureDbMeta, databaseFactory, runtimeState, connectionStrings);
|
||||
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(TestOptions), TestOptions, null);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private ITestDatabase GetOrCreateDatabase(ILoggerFactory loggerFactory, TestUmbracoDatabaseFactoryProvider dbFactory)
|
||||
{
|
||||
lock (s_dbLocker)
|
||||
{
|
||||
@@ -238,11 +234,13 @@ public abstract class UmbracoIntegrationTestBase
|
||||
var settings = new TestDatabaseSettings
|
||||
{
|
||||
FilesPath = Path.Combine(TestHelper.WorkingDirectory, "databases"),
|
||||
DatabaseType = Configuration.GetValue<TestDatabaseSettings.TestDatabaseType>("Tests:Database:DatabaseType"),
|
||||
DatabaseType =
|
||||
Configuration.GetValue<TestDatabaseSettings.TestDatabaseType>("Tests:Database:DatabaseType"),
|
||||
PrepareThreadCount = Configuration.GetValue<int>("Tests:Database:PrepareThreadCount"),
|
||||
EmptyDatabasesCount = Configuration.GetValue<int>("Tests:Database:EmptyDatabasesCount"),
|
||||
SchemaDatabaseCount = Configuration.GetValue<int>("Tests:Database:SchemaDatabaseCount"),
|
||||
SQLServerMasterConnectionString = Configuration.GetValue<string>("Tests:Database:SQLServerMasterConnectionString"),
|
||||
SQLServerMasterConnectionString =
|
||||
Configuration.GetValue<string>("Tests:Database:SQLServerMasterConnectionString")
|
||||
};
|
||||
|
||||
Directory.CreateDirectory(settings.FilesPath);
|
||||
|
||||
@@ -5,66 +5,65 @@ using System;
|
||||
using NUnit.Framework;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.Services.Implement;
|
||||
using Umbraco.Cms.Tests.Common.Builders;
|
||||
|
||||
namespace Umbraco.Cms.Tests.Integration.Testing
|
||||
namespace Umbraco.Cms.Tests.Integration.Testing;
|
||||
|
||||
public abstract class UmbracoIntegrationTestWithContent : UmbracoIntegrationTest
|
||||
{
|
||||
public abstract class UmbracoIntegrationTestWithContent : UmbracoIntegrationTest
|
||||
protected IContentTypeService ContentTypeService => GetRequiredService<IContentTypeService>();
|
||||
|
||||
protected IFileService FileService => GetRequiredService<IFileService>();
|
||||
|
||||
protected ContentService ContentService => (ContentService)GetRequiredService<IContentService>();
|
||||
|
||||
protected Content Trashed { get; private set; }
|
||||
|
||||
protected Content Subpage2 { get; private set; }
|
||||
protected Content Subpage3 { get; private set; }
|
||||
|
||||
protected Content Subpage { get; private set; }
|
||||
|
||||
protected Content Textpage { get; private set; }
|
||||
|
||||
protected ContentType ContentType { get; private set; }
|
||||
|
||||
[SetUp]
|
||||
public void Setup() => CreateTestData();
|
||||
|
||||
public virtual void CreateTestData()
|
||||
{
|
||||
protected IContentTypeService ContentTypeService => GetRequiredService<IContentTypeService>();
|
||||
// NOTE Maybe not the best way to create/save test data as we are using the services, which are being tested.
|
||||
var template = TemplateBuilder.CreateTextPageTemplate();
|
||||
FileService.SaveTemplate(template);
|
||||
|
||||
protected IFileService FileService => GetRequiredService<IFileService>();
|
||||
// Create and Save ContentType "umbTextpage" -> 1051 (template), 1052 (content type)
|
||||
ContentType =
|
||||
ContentTypeBuilder.CreateSimpleContentType("umbTextpage", "Textpage", defaultTemplateId: template.Id);
|
||||
ContentType.Key = new Guid("1D3A8E6E-2EA9-4CC1-B229-1AEE19821522");
|
||||
ContentTypeService.Save(ContentType);
|
||||
|
||||
protected ContentService ContentService => (ContentService)GetRequiredService<IContentService>();
|
||||
// Create and Save Content "Homepage" based on "umbTextpage" -> 1053
|
||||
Textpage = ContentBuilder.CreateSimpleContent(ContentType);
|
||||
Textpage.Key = new Guid("B58B3AD4-62C2-4E27-B1BE-837BD7C533E0");
|
||||
ContentService.Save(Textpage, 0);
|
||||
|
||||
[SetUp]
|
||||
public void Setup() => CreateTestData();
|
||||
// Create and Save Content "Text Page 1" based on "umbTextpage" -> 1054
|
||||
Subpage = ContentBuilder.CreateSimpleContent(ContentType, "Text Page 1", Textpage.Id);
|
||||
var contentSchedule = ContentScheduleCollection.CreateWithEntry(DateTime.Now.AddMinutes(-5), null);
|
||||
ContentService.Save(Subpage, 0, contentSchedule);
|
||||
|
||||
public virtual void CreateTestData()
|
||||
{
|
||||
// NOTE Maybe not the best way to create/save test data as we are using the services, which are being tested.
|
||||
Template template = TemplateBuilder.CreateTextPageTemplate();
|
||||
FileService.SaveTemplate(template);
|
||||
|
||||
// Create and Save ContentType "umbTextpage" -> 1051 (template), 1052 (content type)
|
||||
ContentType = ContentTypeBuilder.CreateSimpleContentType("umbTextpage", "Textpage", defaultTemplateId: template.Id);
|
||||
ContentType.Key = new Guid("1D3A8E6E-2EA9-4CC1-B229-1AEE19821522");
|
||||
ContentTypeService.Save(ContentType);
|
||||
|
||||
// Create and Save Content "Homepage" based on "umbTextpage" -> 1053
|
||||
Textpage = ContentBuilder.CreateSimpleContent(ContentType);
|
||||
Textpage.Key = new Guid("B58B3AD4-62C2-4E27-B1BE-837BD7C533E0");
|
||||
ContentService.Save(Textpage, 0);
|
||||
|
||||
// Create and Save Content "Text Page 1" based on "umbTextpage" -> 1054
|
||||
Subpage = ContentBuilder.CreateSimpleContent(ContentType, "Text Page 1", Textpage.Id);
|
||||
var contentSchedule = ContentScheduleCollection.CreateWithEntry(DateTime.Now.AddMinutes(-5), null);
|
||||
ContentService.Save(Subpage, 0, contentSchedule);
|
||||
|
||||
// Create and Save Content "Text Page 1" based on "umbTextpage" -> 1055
|
||||
Subpage2 = ContentBuilder.CreateSimpleContent(ContentType, "Text Page 2", Textpage.Id);
|
||||
ContentService.Save(Subpage2, 0);
|
||||
// Create and Save Content "Text Page 1" based on "umbTextpage" -> 1055
|
||||
Subpage2 = ContentBuilder.CreateSimpleContent(ContentType, "Text Page 2", Textpage.Id);
|
||||
ContentService.Save(Subpage2, 0);
|
||||
|
||||
|
||||
Subpage3 = ContentBuilder.CreateSimpleContent(ContentType, "Text Page 3", Textpage.Id);
|
||||
ContentService.Save(Subpage3, 0);
|
||||
Subpage3 = ContentBuilder.CreateSimpleContent(ContentType, "Text Page 3", Textpage.Id);
|
||||
ContentService.Save(Subpage3, 0);
|
||||
|
||||
// Create and Save Content "Text Page Deleted" based on "umbTextpage" -> 1056
|
||||
Trashed = ContentBuilder.CreateSimpleContent(ContentType, "Text Page Deleted", -20);
|
||||
Trashed.Trashed = true;
|
||||
ContentService.Save(Trashed, 0);
|
||||
}
|
||||
|
||||
protected Content Trashed { get; private set; }
|
||||
|
||||
protected Content Subpage2 { get; private set; }
|
||||
protected Content Subpage3 { get; private set; }
|
||||
|
||||
protected Content Subpage { get; private set; }
|
||||
|
||||
protected Content Textpage { get; private set; }
|
||||
|
||||
protected ContentType ContentType { get; private set; }
|
||||
// Create and Save Content "Text Page Deleted" based on "umbTextpage" -> 1056
|
||||
Trashed = ContentBuilder.CreateSimpleContent(ContentType, "Text Page Deleted", -20);
|
||||
Trashed.Trashed = true;
|
||||
ContentService.Save(Trashed, 0);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user