From b665099657bd2fa9f6ec6da9328fb540ca35fc09 Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Thu, 23 Mar 2023 08:59:28 +0100 Subject: [PATCH 01/15] Updated reference for JSON schema to latest release of Forms 10. (#13996) --- src/JsonSchema/JsonSchema.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/JsonSchema/JsonSchema.csproj b/src/JsonSchema/JsonSchema.csproj index 901f89e8b2..3d31dd265d 100644 --- a/src/JsonSchema/JsonSchema.csproj +++ b/src/JsonSchema/JsonSchema.csproj @@ -13,6 +13,6 @@ - + From 5598cc2491f32ca6329d2c1e7641b55cef09eb8f Mon Sep 17 00:00:00 2001 From: Nikolaj Date: Thu, 23 Mar 2023 09:20:20 +0100 Subject: [PATCH 02/15] Convert path to absolute --- .../PropertyEditors/RichTextEditorPastedImages.cs | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Infrastructure/PropertyEditors/RichTextEditorPastedImages.cs b/src/Umbraco.Infrastructure/PropertyEditors/RichTextEditorPastedImages.cs index 569f38139d..5044a5b13e 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/RichTextEditorPastedImages.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/RichTextEditorPastedImages.cs @@ -29,6 +29,7 @@ public sealed class RichTextEditorPastedImages private readonly IPublishedUrlProvider _publishedUrlProvider; private readonly IShortStringHelper _shortStringHelper; private readonly IUmbracoContextAccessor _umbracoContextAccessor; + private readonly string _tempFolderAbsolutePath; public RichTextEditorPastedImages( IUmbracoContextAccessor umbracoContextAccessor, @@ -52,6 +53,9 @@ public sealed class RichTextEditorPastedImages _mediaUrlGenerators = mediaUrlGenerators; _shortStringHelper = shortStringHelper; _publishedUrlProvider = publishedUrlProvider; + + _tempFolderAbsolutePath = _hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.TempImageUploads); + } /// @@ -85,12 +89,14 @@ public sealed class RichTextEditorPastedImages continue; } - if (IsValidPath(tmpImgPath) == false) + + var absoluteTempImagePath = Path.GetFullPath(_hostingEnvironment.MapPathContentRoot(tmpImgPath)); + + if (IsValidPath(absoluteTempImagePath) == false) { continue; } - var absoluteTempImagePath = _hostingEnvironment.MapPathContentRoot(tmpImgPath); var fileName = Path.GetFileName(absoluteTempImagePath); var safeFileName = fileName.ToSafeFileName(_shortStringHelper); @@ -191,5 +197,8 @@ public sealed class RichTextEditorPastedImages return htmlDoc.DocumentNode.OuterHtml; } - private bool IsValidPath(string imagePath) => imagePath.StartsWith(Constants.SystemDirectories.TempImageUploads); + private bool IsValidPath(string imagePath) + { + return imagePath.StartsWith(_tempFolderAbsolutePath); + } } From 1ece9627f73e5ba8e5b4a562a19183e94f2c594b Mon Sep 17 00:00:00 2001 From: Nikolaj Date: Thu, 23 Mar 2023 09:20:20 +0100 Subject: [PATCH 03/15] Convert path to absolute --- .../PropertyEditors/RichTextEditorPastedImages.cs | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Infrastructure/PropertyEditors/RichTextEditorPastedImages.cs b/src/Umbraco.Infrastructure/PropertyEditors/RichTextEditorPastedImages.cs index 569f38139d..5044a5b13e 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/RichTextEditorPastedImages.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/RichTextEditorPastedImages.cs @@ -29,6 +29,7 @@ public sealed class RichTextEditorPastedImages private readonly IPublishedUrlProvider _publishedUrlProvider; private readonly IShortStringHelper _shortStringHelper; private readonly IUmbracoContextAccessor _umbracoContextAccessor; + private readonly string _tempFolderAbsolutePath; public RichTextEditorPastedImages( IUmbracoContextAccessor umbracoContextAccessor, @@ -52,6 +53,9 @@ public sealed class RichTextEditorPastedImages _mediaUrlGenerators = mediaUrlGenerators; _shortStringHelper = shortStringHelper; _publishedUrlProvider = publishedUrlProvider; + + _tempFolderAbsolutePath = _hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.TempImageUploads); + } /// @@ -85,12 +89,14 @@ public sealed class RichTextEditorPastedImages continue; } - if (IsValidPath(tmpImgPath) == false) + + var absoluteTempImagePath = Path.GetFullPath(_hostingEnvironment.MapPathContentRoot(tmpImgPath)); + + if (IsValidPath(absoluteTempImagePath) == false) { continue; } - var absoluteTempImagePath = _hostingEnvironment.MapPathContentRoot(tmpImgPath); var fileName = Path.GetFileName(absoluteTempImagePath); var safeFileName = fileName.ToSafeFileName(_shortStringHelper); @@ -191,5 +197,8 @@ public sealed class RichTextEditorPastedImages return htmlDoc.DocumentNode.OuterHtml; } - private bool IsValidPath(string imagePath) => imagePath.StartsWith(Constants.SystemDirectories.TempImageUploads); + private bool IsValidPath(string imagePath) + { + return imagePath.StartsWith(_tempFolderAbsolutePath); + } } From b8899459dc7fd50f167ac1516c7a05553eb1ea45 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Thu, 30 Mar 2023 10:19:35 +0200 Subject: [PATCH 04/15] Determine correct if we can create a database with current connectionstring (#14030) * Fix CanForceCreateDatabase method and add some unit tests * Fixed an old copy/paste error * A little nitpicking over wording and formatting --------- Co-authored-by: kjac --- .../Constants.cs | 4 +- .../SqlAzureDatabaseProviderMetadata.cs | 20 ++++++++++ .../SqlLocalDbDatabaseProviderMetadata.cs | 21 +++++++++++ .../SqlServerDatabaseProviderMetadata.cs | 23 ++++++++++++ .../SqliteDatabaseProviderMetadata.cs | 20 ++++++++++ .../Install/InstallHelper.cs | 2 +- .../DatabaseProviderMetadataExtensions.cs | 27 +++++++++++++- .../Persistence/IDatabaseProviderMetadata.cs | 6 +++ .../Runtime/RuntimeState.cs | 2 +- .../SqAzureDatabaseProviderMetadataTests.cs | 37 +++++++++++++++++++ ...SqlLocalDbDatabaseProviderMetadataTests.cs | 37 +++++++++++++++++++ .../SqlServerDatabaseProviderMetadataTests.cs | 37 +++++++++++++++++++ .../SqliteDatabaseProviderMetadataTests.cs | 37 +++++++++++++++++++ .../Factories/DatabaseSettingsFactoryTests.cs | 2 + .../Umbraco.Tests.UnitTests.csproj | 1 + 15 files changed, 271 insertions(+), 5 deletions(-) create mode 100644 tests/Umbraco.Tests.UnitTests/Umbraco.Cms.Persistence.SqlServer/SqAzureDatabaseProviderMetadataTests.cs create mode 100644 tests/Umbraco.Tests.UnitTests/Umbraco.Cms.Persistence.SqlServer/SqlLocalDbDatabaseProviderMetadataTests.cs create mode 100644 tests/Umbraco.Tests.UnitTests/Umbraco.Cms.Persistence.SqlServer/SqlServerDatabaseProviderMetadataTests.cs create mode 100644 tests/Umbraco.Tests.UnitTests/Umbraco.Cms.Persistence.Sqlite/SqliteDatabaseProviderMetadataTests.cs diff --git a/src/Umbraco.Cms.Persistence.SqlServer/Constants.cs b/src/Umbraco.Cms.Persistence.SqlServer/Constants.cs index ae16a9735f..6b5099b314 100644 --- a/src/Umbraco.Cms.Persistence.SqlServer/Constants.cs +++ b/src/Umbraco.Cms.Persistence.SqlServer/Constants.cs @@ -1,12 +1,12 @@ namespace Umbraco.Cms.Persistence.SqlServer; /// -/// Constants related to SQLite. +/// Constants related to SQL Server. /// public static class Constants { /// - /// SQLite provider name. + /// SQL Server provider name. /// public const string ProviderName = "Microsoft.Data.SqlClient"; } diff --git a/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlAzureDatabaseProviderMetadata.cs b/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlAzureDatabaseProviderMetadata.cs index 112d556712..95dda5fe7b 100644 --- a/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlAzureDatabaseProviderMetadata.cs +++ b/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlAzureDatabaseProviderMetadata.cs @@ -1,4 +1,5 @@ using System.Runtime.Serialization; +using Microsoft.Data.SqlClient; using Umbraco.Cms.Core.Install.Models; using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Extensions; @@ -50,6 +51,25 @@ public class SqlAzureDatabaseProviderMetadata : IDatabaseProviderMetadata /// public bool ForceCreateDatabase => false; + /// + public bool CanRecognizeConnectionString(string? connectionString) + { + if (connectionString is null) + { + return false; + } + + try + { + var builder = new SqlConnectionStringBuilder(connectionString); + + return string.IsNullOrEmpty(builder.AttachDBFilename) && builder.DataSource.Contains("database.windows.net"); + } + catch (ArgumentException) + { + return false; + } + } /// public string GenerateConnectionString(DatabaseModel databaseModel) { diff --git a/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlLocalDbDatabaseProviderMetadata.cs b/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlLocalDbDatabaseProviderMetadata.cs index 30a503e5f9..589ef5a623 100644 --- a/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlLocalDbDatabaseProviderMetadata.cs +++ b/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlLocalDbDatabaseProviderMetadata.cs @@ -51,6 +51,27 @@ public class SqlLocalDbDatabaseProviderMetadata : IDatabaseProviderMetadata /// public bool ForceCreateDatabase => true; + /// + public bool CanRecognizeConnectionString(string? connectionString) + { + if (connectionString is null) + { + return false; + } + + try + { + var builder = new SqlConnectionStringBuilder(connectionString); + + return !string.IsNullOrEmpty(builder.AttachDBFilename); + + } + catch (ArgumentException) + { + return false; + } + } + /// public string GenerateConnectionString(DatabaseModel databaseModel) { diff --git a/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlServerDatabaseProviderMetadata.cs b/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlServerDatabaseProviderMetadata.cs index 8b36736804..edb44700d8 100644 --- a/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlServerDatabaseProviderMetadata.cs +++ b/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlServerDatabaseProviderMetadata.cs @@ -1,6 +1,9 @@ +using System.Data.Common; using System.Runtime.Serialization; +using Microsoft.Data.SqlClient; using Umbraco.Cms.Core.Install.Models; using Umbraco.Cms.Infrastructure.Persistence; +using Umbraco.Extensions; namespace Umbraco.Cms.Persistence.SqlServer.Services; @@ -49,6 +52,26 @@ public class SqlServerDatabaseProviderMetadata : IDatabaseProviderMetadata /// public bool ForceCreateDatabase => false; + /// + public bool CanRecognizeConnectionString(string? connectionString) + { + if (connectionString is null) + { + return false; + } + + try + { + var builder = new SqlConnectionStringBuilder(connectionString); + + return string.IsNullOrEmpty(builder.AttachDBFilename); + } + catch (ArgumentException) + { + return false; + } + } + /// public string GenerateConnectionString(DatabaseModel databaseModel) => databaseModel.IntegratedAuth diff --git a/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteDatabaseProviderMetadata.cs b/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteDatabaseProviderMetadata.cs index 54a7a5cb1d..d087acc8cb 100644 --- a/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteDatabaseProviderMetadata.cs +++ b/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteDatabaseProviderMetadata.cs @@ -56,6 +56,26 @@ public class SqliteDatabaseProviderMetadata : IDatabaseProviderMetadata /// public bool ForceCreateDatabase => true; + /// + public bool CanRecognizeConnectionString(string? connectionString) + { + if (connectionString is null) + { + return false; + } + + try + { + var builder = new SqliteConnectionStringBuilder(connectionString); + + return !string.IsNullOrEmpty(builder.DataSource); + + } + catch (ArgumentException) + { + return false; + } + } /// public string GenerateConnectionString(DatabaseModel databaseModel) { diff --git a/src/Umbraco.Infrastructure/Install/InstallHelper.cs b/src/Umbraco.Infrastructure/Install/InstallHelper.cs index 4686625895..82d4905f12 100644 --- a/src/Umbraco.Infrastructure/Install/InstallHelper.cs +++ b/src/Umbraco.Infrastructure/Install/InstallHelper.cs @@ -164,7 +164,7 @@ namespace Umbraco.Cms.Infrastructure.Install private bool IsBrandNewInstall => _connectionStrings.CurrentValue.IsConnectionStringConfigured() == false || _databaseBuilder.IsDatabaseConfigured == false || - (_databaseBuilder.CanConnectToDatabase == false && _databaseProviderMetadata.CanForceCreateDatabase(_umbracoDatabaseFactory.SqlContext.SqlSyntax.DbProvider)) || + (_databaseBuilder.CanConnectToDatabase == false && _databaseProviderMetadata.CanForceCreateDatabase(_umbracoDatabaseFactory)) || _databaseBuilder.IsUmbracoInstalled() == false; } } diff --git a/src/Umbraco.Infrastructure/Persistence/DatabaseProviderMetadataExtensions.cs b/src/Umbraco.Infrastructure/Persistence/DatabaseProviderMetadataExtensions.cs index 1ea941932e..09c0a121dc 100644 --- a/src/Umbraco.Infrastructure/Persistence/DatabaseProviderMetadataExtensions.cs +++ b/src/Umbraco.Infrastructure/Persistence/DatabaseProviderMetadataExtensions.cs @@ -1,4 +1,5 @@ using Umbraco.Cms.Core.Install.Models; +using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; namespace Umbraco.Cms.Infrastructure.Persistence; @@ -26,8 +27,32 @@ public static class DatabaseProviderMetadataExtensions /// /// true if a database can be created for the specified provider name; otherwise, false. /// + [Obsolete("Use CanForceCreateDatabase that takes an IUmbracoDatabaseFactory. Scheduled for removal in Umbraco 13.")] public static bool CanForceCreateDatabase(this IEnumerable databaseProviderMetadata, string? providerName) - => databaseProviderMetadata.FirstOrDefault(x => string.Equals(x.ProviderName, providerName, StringComparison.InvariantCultureIgnoreCase))?.ForceCreateDatabase == true; + { + return databaseProviderMetadata + .FirstOrDefault(x => + string.Equals(x.ProviderName, providerName, StringComparison.InvariantCultureIgnoreCase)) + ?.ForceCreateDatabase == true; + } + + /// + /// Determines whether a database can be created for the specified provider name while ignoring the value of . + /// + /// The database provider metadata. + /// The database factory. + /// + /// true if a database can be created for the specified database; otherwise, false. + /// + public static bool CanForceCreateDatabase(this IEnumerable databaseProviderMetadata, IUmbracoDatabaseFactory umbracoDatabaseFactory) + { + // In case more metadata providers can recognize the connection string, we need to check if any can force create. + // E.g. Both SqlServer and SqlAzure will recognize an azure connection string, but luckily none of those can force create. + return databaseProviderMetadata + .Where(x => + string.Equals(x.ProviderName, umbracoDatabaseFactory.SqlContext.SqlSyntax.ProviderName, StringComparison.InvariantCultureIgnoreCase) + && x.CanRecognizeConnectionString(umbracoDatabaseFactory.ConnectionString) && x.IsAvailable).Any(x => x.ForceCreateDatabase == true); + } /// /// Generates the connection string. diff --git a/src/Umbraco.Infrastructure/Persistence/IDatabaseProviderMetadata.cs b/src/Umbraco.Infrastructure/Persistence/IDatabaseProviderMetadata.cs index 1c06dd089f..55a7c3a686 100644 --- a/src/Umbraco.Infrastructure/Persistence/IDatabaseProviderMetadata.cs +++ b/src/Umbraco.Infrastructure/Persistence/IDatabaseProviderMetadata.cs @@ -82,6 +82,12 @@ public interface IDatabaseProviderMetadata /// public bool ForceCreateDatabase { get; } + + /// + /// Gets a value indicating whether this connections could have been build using . + /// + public bool CanRecognizeConnectionString(string? connectionString) => false; + /// /// Creates a connection string for this provider. /// diff --git a/src/Umbraco.Infrastructure/Runtime/RuntimeState.cs b/src/Umbraco.Infrastructure/Runtime/RuntimeState.cs index f0679aa470..6597fadf61 100644 --- a/src/Umbraco.Infrastructure/Runtime/RuntimeState.cs +++ b/src/Umbraco.Infrastructure/Runtime/RuntimeState.cs @@ -209,7 +209,7 @@ public class RuntimeState : IRuntimeState // cannot connect to configured database, this is bad, fail _logger.LogDebug("Could not connect to database."); - if (_globalSettings.Value.InstallMissingDatabase || _databaseProviderMetadata.CanForceCreateDatabase(_databaseFactory.ProviderName)) + if (_globalSettings.Value.InstallMissingDatabase || _databaseProviderMetadata.CanForceCreateDatabase(_databaseFactory)) { // ok to install on a configured but missing database Level = RuntimeLevel.Install; diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Cms.Persistence.SqlServer/SqAzureDatabaseProviderMetadataTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Cms.Persistence.SqlServer/SqAzureDatabaseProviderMetadataTests.cs new file mode 100644 index 0000000000..e945eeae07 --- /dev/null +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Cms.Persistence.SqlServer/SqAzureDatabaseProviderMetadataTests.cs @@ -0,0 +1,37 @@ +using NUnit.Framework; +using Umbraco.Cms.Core.Install.Models; +using Umbraco.Cms.Persistence.SqlServer.Services; + +namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Cms.Persistence.SqlServer; + +[TestFixture] +public class SqlAzureDatabaseProviderMetadataTests +{ + [Test] + [TestCase("myServer", "myDatabase", "myLogin", "myPassword", true /*ignored*/, ExpectedResult = "Server=tcp:myServer.database.windows.net,1433;Database=myDatabase;User ID=myLogin@myServer;Password=myPassword")] + [TestCase("myServer", "myDatabase", "myLogin", "myPassword", false, ExpectedResult = "Server=tcp:myServer.database.windows.net,1433;Database=myDatabase;User ID=myLogin@myServer;Password=myPassword")] + public string GenerateConnectionString(string server, string databaseName, string login, string password, bool integratedAuth) + { + var sut = new SqlAzureDatabaseProviderMetadata(); + return sut.GenerateConnectionString(new DatabaseModel() + { + DatabaseName = databaseName, + Login = login, + Password = password, + Server = server, + IntegratedAuth = integratedAuth + }); + } + + [Test] + [TestCase("Server=myServer;Database=myDatabase;Integrated Security=true", ExpectedResult = false)] // SqlServer + [TestCase("Server=myServer;Database=myDatabase;User Id=myLogin;Password=myPassword", ExpectedResult = false)] // SqlServer + [TestCase("Server=tcp:cmstest27032000.database.windows.net,1433;Database=test_27032000;User ID=asdasdas@cmstest27032000;Password=123456879", ExpectedResult = true)] // Azure + [TestCase("Data Source=|DataDirectory|/Umbraco.sqlite.db;Cache=Shared;Foreign Keys=True;Pooling=True", ExpectedResult = false)] // Sqlite + [TestCase("Data Source=(LocalDb)\\MSSQLLocalDB;Initial Catalog=aspnet-MvcMovie;Integrated Security=SSPI;AttachDBFilename=|DataDirectory|\\umbraco.mdf", ExpectedResult = false)] // localDB + public bool CanRecognizeConnectionString(string connectionString) + { + var sut = new SqlAzureDatabaseProviderMetadata(); + return sut.CanRecognizeConnectionString(connectionString); + } +} diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Cms.Persistence.SqlServer/SqlLocalDbDatabaseProviderMetadataTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Cms.Persistence.SqlServer/SqlLocalDbDatabaseProviderMetadataTests.cs new file mode 100644 index 0000000000..24728a6c3e --- /dev/null +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Cms.Persistence.SqlServer/SqlLocalDbDatabaseProviderMetadataTests.cs @@ -0,0 +1,37 @@ +using NUnit.Framework; +using Umbraco.Cms.Core.Install.Models; +using Umbraco.Cms.Persistence.SqlServer.Services; + +namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Cms.Persistence.SqlServer; + +[TestFixture] +public class SqlLocalDbDatabaseProviderMetadataTests +{ + [Test] + [TestCase("ignored", "myDatabase", "ignored", "ignored", true, ExpectedResult = "Data Source=(localdb)\\MSSQLLocalDB;AttachDbFilename=|DataDirectory|\\myDatabase.mdf;Integrated Security=True")] + [TestCase("ignored", "myDatabase2", "ignored", "ignored", false /*ignored*/, ExpectedResult = "Data Source=(localdb)\\MSSQLLocalDB;AttachDbFilename=|DataDirectory|\\myDatabase2.mdf;Integrated Security=True")] + public string GenerateConnectionString(string server, string databaseName, string login, string password, bool integratedAuth) + { + var sut = new SqlLocalDbDatabaseProviderMetadata(); + return sut.GenerateConnectionString(new DatabaseModel() + { + DatabaseName = databaseName, + Login = login, + Password = password, + Server = server, + IntegratedAuth = integratedAuth, + }); + } + + [Test] + [TestCase("Server=myServer;Database=myDatabase;Integrated Security=true", ExpectedResult = false)] // SqlServer + [TestCase("Server=myServer;Database=myDatabase;User Id=myLogin;Password=myPassword", ExpectedResult = false)] // SqlServer + [TestCase("Server=tcp:cmstest27032000.database.windows.net,1433;Database=test_27032000;User ID=asdasdas@cmstest27032000;Password=123456879", ExpectedResult = false)] // Azure + [TestCase("Data Source=|DataDirectory|/Umbraco.sqlite.db;Cache=Shared;Foreign Keys=True;Pooling=True", ExpectedResult = false)] // Sqlite + [TestCase("Data Source=(LocalDb)\\MSSQLLocalDB;Initial Catalog=aspnet-MvcMovie;Integrated Security=SSPI;AttachDBFilename=|DataDirectory|\\umbraco.mdf", ExpectedResult = true)] // localDB + public bool CanRecognizeConnectionString(string connectionString) + { + var sut = new SqlLocalDbDatabaseProviderMetadata(); + return sut.CanRecognizeConnectionString(connectionString); + } +} diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Cms.Persistence.SqlServer/SqlServerDatabaseProviderMetadataTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Cms.Persistence.SqlServer/SqlServerDatabaseProviderMetadataTests.cs new file mode 100644 index 0000000000..4e96c375e1 --- /dev/null +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Cms.Persistence.SqlServer/SqlServerDatabaseProviderMetadataTests.cs @@ -0,0 +1,37 @@ +using NUnit.Framework; +using Umbraco.Cms.Core.Install.Models; +using Umbraco.Cms.Persistence.SqlServer.Services; + +namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Cms.Persistence.SqlServer; + +[TestFixture] +public class SqlServerDatabaseProviderMetadataTests +{ + [Test] + [TestCase("myServer", "myDatabase", "myLogin", "myPassword", true, ExpectedResult = "Server=myServer;Database=myDatabase;Integrated Security=true")] + [TestCase("myServer", "myDatabase", "myLogin", "myPassword", false, ExpectedResult = "Server=myServer;Database=myDatabase;User Id=myLogin;Password=myPassword")] + public string GenerateConnectionString(string server, string databaseName, string login, string password, bool integratedAuth) + { + var sut = new SqlServerDatabaseProviderMetadata(); + return sut.GenerateConnectionString(new DatabaseModel() + { + DatabaseName = databaseName, + Login = login, + Password = password, + Server = server, + IntegratedAuth = integratedAuth + }); + } + + [Test] + [TestCase("Server=myServer;Database=myDatabase;Integrated Security=true", ExpectedResult = true)] // SqlServer + [TestCase("Server=myServer;Database=myDatabase;User Id=myLogin;Password=myPassword", ExpectedResult = true)] // SqlServer + [TestCase("Server=tcp:cmstest27032000.database.windows.net,1433;Database=test_27032000;User ID=asdasdas@cmstest27032000;Password=123456879", ExpectedResult = true)] // Azure + [TestCase("Data Source=|DataDirectory|/Umbraco.sqlite.db;Cache=Shared;Foreign Keys=True;Pooling=True", ExpectedResult = false)] // Sqlite + [TestCase("Data Source=(LocalDb)\\MSSQLLocalDB;Initial Catalog=aspnet-MvcMovie;Integrated Security=SSPI;AttachDBFilename=|DataDirectory|\\umbraco.mdf", ExpectedResult = false)] // localDB + public bool CanRecognizeConnectionString(string connectionString) + { + var sut = new SqlServerDatabaseProviderMetadata(); + return sut.CanRecognizeConnectionString(connectionString); + } +} diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Cms.Persistence.Sqlite/SqliteDatabaseProviderMetadataTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Cms.Persistence.Sqlite/SqliteDatabaseProviderMetadataTests.cs new file mode 100644 index 0000000000..63eb6c86fd --- /dev/null +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Cms.Persistence.Sqlite/SqliteDatabaseProviderMetadataTests.cs @@ -0,0 +1,37 @@ +using NUnit.Framework; +using Umbraco.Cms.Core.Install.Models; +using Umbraco.Cms.Persistence.Sqlite.Services; + +namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Cms.Persistence.Sqlite; + +[TestFixture] +public class SqliteDatabaseProviderMetadataTests +{ + [Test] + [TestCase("ignored", "myDatabase", "ignored", "ignored", true /*ignored*/, ExpectedResult = "Data Source=|DataDirectory|/myDatabase.sqlite.db;Cache=Shared;Foreign Keys=True;Pooling=True")] + [TestCase("ignored", "myDatabase2", "ignored", "ignored", false /*ignored*/, ExpectedResult = "Data Source=|DataDirectory|/myDatabase2.sqlite.db;Cache=Shared;Foreign Keys=True;Pooling=True")] + public string GenerateConnectionString(string server, string databaseName, string login, string password, bool integratedAuth) + { + var sut = new SqliteDatabaseProviderMetadata(); + return sut.GenerateConnectionString(new DatabaseModel() + { + DatabaseName = databaseName, + Login = login, + Password = password, + Server = server, + IntegratedAuth = integratedAuth + }); + } + + [Test] + [TestCase("Server=myServer;Database=myDatabase;Integrated Security=true", ExpectedResult = false)] // SqlServer + [TestCase("Server=myServer;Database=myDatabase;User Id=myLogin;Password=myPassword", ExpectedResult = false)] // SqlServer + [TestCase("Server=tcp:cmstest27032000.database.windows.net,1433;Database=test_27032000;User ID=asdasdas@cmstest27032000;Password=123456879", ExpectedResult = false)] // Azure + [TestCase("Data Source=|DataDirectory|/Umbraco.sqlite.db;Cache=Shared;Foreign Keys=True;Pooling=True", ExpectedResult = true)] // Sqlite + [TestCase("Data Source=(LocalDb)\\MSSQLLocalDB;Initial Catalog=aspnet-MvcMovie;Integrated Security=SSPI;AttachDBFilename=|DataDirectory|\\umbraco.mdf", ExpectedResult = false)] // localDB + public bool CanRecognizeConnectionString(string connectionString) + { + var sut = new SqliteDatabaseProviderMetadata(); + return sut.CanRecognizeConnectionString(connectionString); + } +} diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.New.Cms.Infrastructure/Factories/DatabaseSettingsFactoryTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.New.Cms.Infrastructure/Factories/DatabaseSettingsFactoryTests.cs index fd2c7315b0..82beaeeab9 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.New.Cms.Infrastructure/Factories/DatabaseSettingsFactoryTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.New.Cms.Infrastructure/Factories/DatabaseSettingsFactoryTests.cs @@ -177,6 +177,8 @@ public class DatabaseSettingsFactoryTests public Func GenerateConnectionStringDelegate { get; set; } = _ => "ConnectionString"; + public bool CanRecognizeConnectionString(string? connectionString) => false; + public string? GenerateConnectionString(DatabaseModel databaseModel) => GenerateConnectionStringDelegate(databaseModel); } } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Tests.UnitTests.csproj b/tests/Umbraco.Tests.UnitTests/Umbraco.Tests.UnitTests.csproj index 8b59b8193f..8a79dd6d19 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Tests.UnitTests.csproj +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Tests.UnitTests.csproj @@ -15,6 +15,7 @@ + From ce47281c04f20e476b99c1a178818d4aece80eab Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Thu, 30 Mar 2023 10:19:35 +0200 Subject: [PATCH 05/15] Determine correct if we can create a database with current connectionstring (#14030) * Fix CanForceCreateDatabase method and add some unit tests * Fixed an old copy/paste error * A little nitpicking over wording and formatting --------- Co-authored-by: kjac --- .../Constants.cs | 4 +- .../SqlAzureDatabaseProviderMetadata.cs | 20 ++++++++++ .../SqlLocalDbDatabaseProviderMetadata.cs | 21 +++++++++++ .../SqlServerDatabaseProviderMetadata.cs | 23 ++++++++++++ .../SqliteDatabaseProviderMetadata.cs | 20 ++++++++++ .../Install/InstallHelper.cs | 2 +- .../DatabaseProviderMetadataExtensions.cs | 27 +++++++++++++- .../Persistence/IDatabaseProviderMetadata.cs | 6 +++ .../Runtime/RuntimeState.cs | 2 +- .../SqAzureDatabaseProviderMetadataTests.cs | 37 +++++++++++++++++++ ...SqlLocalDbDatabaseProviderMetadataTests.cs | 37 +++++++++++++++++++ .../SqlServerDatabaseProviderMetadataTests.cs | 37 +++++++++++++++++++ .../SqliteDatabaseProviderMetadataTests.cs | 37 +++++++++++++++++++ .../Factories/DatabaseSettingsFactoryTests.cs | 2 + .../Umbraco.Tests.UnitTests.csproj | 1 + 15 files changed, 271 insertions(+), 5 deletions(-) create mode 100644 tests/Umbraco.Tests.UnitTests/Umbraco.Cms.Persistence.SqlServer/SqAzureDatabaseProviderMetadataTests.cs create mode 100644 tests/Umbraco.Tests.UnitTests/Umbraco.Cms.Persistence.SqlServer/SqlLocalDbDatabaseProviderMetadataTests.cs create mode 100644 tests/Umbraco.Tests.UnitTests/Umbraco.Cms.Persistence.SqlServer/SqlServerDatabaseProviderMetadataTests.cs create mode 100644 tests/Umbraco.Tests.UnitTests/Umbraco.Cms.Persistence.Sqlite/SqliteDatabaseProviderMetadataTests.cs diff --git a/src/Umbraco.Cms.Persistence.SqlServer/Constants.cs b/src/Umbraco.Cms.Persistence.SqlServer/Constants.cs index ae16a9735f..6b5099b314 100644 --- a/src/Umbraco.Cms.Persistence.SqlServer/Constants.cs +++ b/src/Umbraco.Cms.Persistence.SqlServer/Constants.cs @@ -1,12 +1,12 @@ namespace Umbraco.Cms.Persistence.SqlServer; /// -/// Constants related to SQLite. +/// Constants related to SQL Server. /// public static class Constants { /// - /// SQLite provider name. + /// SQL Server provider name. /// public const string ProviderName = "Microsoft.Data.SqlClient"; } diff --git a/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlAzureDatabaseProviderMetadata.cs b/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlAzureDatabaseProviderMetadata.cs index 112d556712..95dda5fe7b 100644 --- a/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlAzureDatabaseProviderMetadata.cs +++ b/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlAzureDatabaseProviderMetadata.cs @@ -1,4 +1,5 @@ using System.Runtime.Serialization; +using Microsoft.Data.SqlClient; using Umbraco.Cms.Core.Install.Models; using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Extensions; @@ -50,6 +51,25 @@ public class SqlAzureDatabaseProviderMetadata : IDatabaseProviderMetadata /// public bool ForceCreateDatabase => false; + /// + public bool CanRecognizeConnectionString(string? connectionString) + { + if (connectionString is null) + { + return false; + } + + try + { + var builder = new SqlConnectionStringBuilder(connectionString); + + return string.IsNullOrEmpty(builder.AttachDBFilename) && builder.DataSource.Contains("database.windows.net"); + } + catch (ArgumentException) + { + return false; + } + } /// public string GenerateConnectionString(DatabaseModel databaseModel) { diff --git a/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlLocalDbDatabaseProviderMetadata.cs b/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlLocalDbDatabaseProviderMetadata.cs index 30a503e5f9..589ef5a623 100644 --- a/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlLocalDbDatabaseProviderMetadata.cs +++ b/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlLocalDbDatabaseProviderMetadata.cs @@ -51,6 +51,27 @@ public class SqlLocalDbDatabaseProviderMetadata : IDatabaseProviderMetadata /// public bool ForceCreateDatabase => true; + /// + public bool CanRecognizeConnectionString(string? connectionString) + { + if (connectionString is null) + { + return false; + } + + try + { + var builder = new SqlConnectionStringBuilder(connectionString); + + return !string.IsNullOrEmpty(builder.AttachDBFilename); + + } + catch (ArgumentException) + { + return false; + } + } + /// public string GenerateConnectionString(DatabaseModel databaseModel) { diff --git a/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlServerDatabaseProviderMetadata.cs b/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlServerDatabaseProviderMetadata.cs index 8b36736804..edb44700d8 100644 --- a/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlServerDatabaseProviderMetadata.cs +++ b/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlServerDatabaseProviderMetadata.cs @@ -1,6 +1,9 @@ +using System.Data.Common; using System.Runtime.Serialization; +using Microsoft.Data.SqlClient; using Umbraco.Cms.Core.Install.Models; using Umbraco.Cms.Infrastructure.Persistence; +using Umbraco.Extensions; namespace Umbraco.Cms.Persistence.SqlServer.Services; @@ -49,6 +52,26 @@ public class SqlServerDatabaseProviderMetadata : IDatabaseProviderMetadata /// public bool ForceCreateDatabase => false; + /// + public bool CanRecognizeConnectionString(string? connectionString) + { + if (connectionString is null) + { + return false; + } + + try + { + var builder = new SqlConnectionStringBuilder(connectionString); + + return string.IsNullOrEmpty(builder.AttachDBFilename); + } + catch (ArgumentException) + { + return false; + } + } + /// public string GenerateConnectionString(DatabaseModel databaseModel) => databaseModel.IntegratedAuth diff --git a/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteDatabaseProviderMetadata.cs b/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteDatabaseProviderMetadata.cs index 54a7a5cb1d..d087acc8cb 100644 --- a/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteDatabaseProviderMetadata.cs +++ b/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteDatabaseProviderMetadata.cs @@ -56,6 +56,26 @@ public class SqliteDatabaseProviderMetadata : IDatabaseProviderMetadata /// public bool ForceCreateDatabase => true; + /// + public bool CanRecognizeConnectionString(string? connectionString) + { + if (connectionString is null) + { + return false; + } + + try + { + var builder = new SqliteConnectionStringBuilder(connectionString); + + return !string.IsNullOrEmpty(builder.DataSource); + + } + catch (ArgumentException) + { + return false; + } + } /// public string GenerateConnectionString(DatabaseModel databaseModel) { diff --git a/src/Umbraco.Infrastructure/Install/InstallHelper.cs b/src/Umbraco.Infrastructure/Install/InstallHelper.cs index 9305c4444e..0ea4a1a84c 100644 --- a/src/Umbraco.Infrastructure/Install/InstallHelper.cs +++ b/src/Umbraco.Infrastructure/Install/InstallHelper.cs @@ -164,7 +164,7 @@ namespace Umbraco.Cms.Infrastructure.Install private bool IsBrandNewInstall => _connectionStrings.CurrentValue.IsConnectionStringConfigured() == false || _databaseBuilder.IsDatabaseConfigured == false || - (_databaseBuilder.CanConnectToDatabase == false && _databaseProviderMetadata.CanForceCreateDatabase(_umbracoDatabaseFactory.SqlContext.SqlSyntax.DbProvider)) || + (_databaseBuilder.CanConnectToDatabase == false && _databaseProviderMetadata.CanForceCreateDatabase(_umbracoDatabaseFactory)) || _databaseBuilder.IsUmbracoInstalled() == false; } } diff --git a/src/Umbraco.Infrastructure/Persistence/DatabaseProviderMetadataExtensions.cs b/src/Umbraco.Infrastructure/Persistence/DatabaseProviderMetadataExtensions.cs index 1ea941932e..09c0a121dc 100644 --- a/src/Umbraco.Infrastructure/Persistence/DatabaseProviderMetadataExtensions.cs +++ b/src/Umbraco.Infrastructure/Persistence/DatabaseProviderMetadataExtensions.cs @@ -1,4 +1,5 @@ using Umbraco.Cms.Core.Install.Models; +using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; namespace Umbraco.Cms.Infrastructure.Persistence; @@ -26,8 +27,32 @@ public static class DatabaseProviderMetadataExtensions /// /// true if a database can be created for the specified provider name; otherwise, false. /// + [Obsolete("Use CanForceCreateDatabase that takes an IUmbracoDatabaseFactory. Scheduled for removal in Umbraco 13.")] public static bool CanForceCreateDatabase(this IEnumerable databaseProviderMetadata, string? providerName) - => databaseProviderMetadata.FirstOrDefault(x => string.Equals(x.ProviderName, providerName, StringComparison.InvariantCultureIgnoreCase))?.ForceCreateDatabase == true; + { + return databaseProviderMetadata + .FirstOrDefault(x => + string.Equals(x.ProviderName, providerName, StringComparison.InvariantCultureIgnoreCase)) + ?.ForceCreateDatabase == true; + } + + /// + /// Determines whether a database can be created for the specified provider name while ignoring the value of . + /// + /// The database provider metadata. + /// The database factory. + /// + /// true if a database can be created for the specified database; otherwise, false. + /// + public static bool CanForceCreateDatabase(this IEnumerable databaseProviderMetadata, IUmbracoDatabaseFactory umbracoDatabaseFactory) + { + // In case more metadata providers can recognize the connection string, we need to check if any can force create. + // E.g. Both SqlServer and SqlAzure will recognize an azure connection string, but luckily none of those can force create. + return databaseProviderMetadata + .Where(x => + string.Equals(x.ProviderName, umbracoDatabaseFactory.SqlContext.SqlSyntax.ProviderName, StringComparison.InvariantCultureIgnoreCase) + && x.CanRecognizeConnectionString(umbracoDatabaseFactory.ConnectionString) && x.IsAvailable).Any(x => x.ForceCreateDatabase == true); + } /// /// Generates the connection string. diff --git a/src/Umbraco.Infrastructure/Persistence/IDatabaseProviderMetadata.cs b/src/Umbraco.Infrastructure/Persistence/IDatabaseProviderMetadata.cs index 1c06dd089f..55a7c3a686 100644 --- a/src/Umbraco.Infrastructure/Persistence/IDatabaseProviderMetadata.cs +++ b/src/Umbraco.Infrastructure/Persistence/IDatabaseProviderMetadata.cs @@ -82,6 +82,12 @@ public interface IDatabaseProviderMetadata /// public bool ForceCreateDatabase { get; } + + /// + /// Gets a value indicating whether this connections could have been build using . + /// + public bool CanRecognizeConnectionString(string? connectionString) => false; + /// /// Creates a connection string for this provider. /// diff --git a/src/Umbraco.Infrastructure/Runtime/RuntimeState.cs b/src/Umbraco.Infrastructure/Runtime/RuntimeState.cs index 74b00d3644..ec1cfc5d6e 100644 --- a/src/Umbraco.Infrastructure/Runtime/RuntimeState.cs +++ b/src/Umbraco.Infrastructure/Runtime/RuntimeState.cs @@ -210,7 +210,7 @@ public class RuntimeState : IRuntimeState // cannot connect to configured database, this is bad, fail _logger.LogDebug("Could not connect to database."); - if (_globalSettings.Value.InstallMissingDatabase || _databaseProviderMetadata.CanForceCreateDatabase(_databaseFactory.ProviderName)) + if (_globalSettings.Value.InstallMissingDatabase || _databaseProviderMetadata.CanForceCreateDatabase(_databaseFactory)) { // ok to install on a configured but missing database Level = RuntimeLevel.Install; diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Cms.Persistence.SqlServer/SqAzureDatabaseProviderMetadataTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Cms.Persistence.SqlServer/SqAzureDatabaseProviderMetadataTests.cs new file mode 100644 index 0000000000..e945eeae07 --- /dev/null +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Cms.Persistence.SqlServer/SqAzureDatabaseProviderMetadataTests.cs @@ -0,0 +1,37 @@ +using NUnit.Framework; +using Umbraco.Cms.Core.Install.Models; +using Umbraco.Cms.Persistence.SqlServer.Services; + +namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Cms.Persistence.SqlServer; + +[TestFixture] +public class SqlAzureDatabaseProviderMetadataTests +{ + [Test] + [TestCase("myServer", "myDatabase", "myLogin", "myPassword", true /*ignored*/, ExpectedResult = "Server=tcp:myServer.database.windows.net,1433;Database=myDatabase;User ID=myLogin@myServer;Password=myPassword")] + [TestCase("myServer", "myDatabase", "myLogin", "myPassword", false, ExpectedResult = "Server=tcp:myServer.database.windows.net,1433;Database=myDatabase;User ID=myLogin@myServer;Password=myPassword")] + public string GenerateConnectionString(string server, string databaseName, string login, string password, bool integratedAuth) + { + var sut = new SqlAzureDatabaseProviderMetadata(); + return sut.GenerateConnectionString(new DatabaseModel() + { + DatabaseName = databaseName, + Login = login, + Password = password, + Server = server, + IntegratedAuth = integratedAuth + }); + } + + [Test] + [TestCase("Server=myServer;Database=myDatabase;Integrated Security=true", ExpectedResult = false)] // SqlServer + [TestCase("Server=myServer;Database=myDatabase;User Id=myLogin;Password=myPassword", ExpectedResult = false)] // SqlServer + [TestCase("Server=tcp:cmstest27032000.database.windows.net,1433;Database=test_27032000;User ID=asdasdas@cmstest27032000;Password=123456879", ExpectedResult = true)] // Azure + [TestCase("Data Source=|DataDirectory|/Umbraco.sqlite.db;Cache=Shared;Foreign Keys=True;Pooling=True", ExpectedResult = false)] // Sqlite + [TestCase("Data Source=(LocalDb)\\MSSQLLocalDB;Initial Catalog=aspnet-MvcMovie;Integrated Security=SSPI;AttachDBFilename=|DataDirectory|\\umbraco.mdf", ExpectedResult = false)] // localDB + public bool CanRecognizeConnectionString(string connectionString) + { + var sut = new SqlAzureDatabaseProviderMetadata(); + return sut.CanRecognizeConnectionString(connectionString); + } +} diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Cms.Persistence.SqlServer/SqlLocalDbDatabaseProviderMetadataTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Cms.Persistence.SqlServer/SqlLocalDbDatabaseProviderMetadataTests.cs new file mode 100644 index 0000000000..24728a6c3e --- /dev/null +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Cms.Persistence.SqlServer/SqlLocalDbDatabaseProviderMetadataTests.cs @@ -0,0 +1,37 @@ +using NUnit.Framework; +using Umbraco.Cms.Core.Install.Models; +using Umbraco.Cms.Persistence.SqlServer.Services; + +namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Cms.Persistence.SqlServer; + +[TestFixture] +public class SqlLocalDbDatabaseProviderMetadataTests +{ + [Test] + [TestCase("ignored", "myDatabase", "ignored", "ignored", true, ExpectedResult = "Data Source=(localdb)\\MSSQLLocalDB;AttachDbFilename=|DataDirectory|\\myDatabase.mdf;Integrated Security=True")] + [TestCase("ignored", "myDatabase2", "ignored", "ignored", false /*ignored*/, ExpectedResult = "Data Source=(localdb)\\MSSQLLocalDB;AttachDbFilename=|DataDirectory|\\myDatabase2.mdf;Integrated Security=True")] + public string GenerateConnectionString(string server, string databaseName, string login, string password, bool integratedAuth) + { + var sut = new SqlLocalDbDatabaseProviderMetadata(); + return sut.GenerateConnectionString(new DatabaseModel() + { + DatabaseName = databaseName, + Login = login, + Password = password, + Server = server, + IntegratedAuth = integratedAuth, + }); + } + + [Test] + [TestCase("Server=myServer;Database=myDatabase;Integrated Security=true", ExpectedResult = false)] // SqlServer + [TestCase("Server=myServer;Database=myDatabase;User Id=myLogin;Password=myPassword", ExpectedResult = false)] // SqlServer + [TestCase("Server=tcp:cmstest27032000.database.windows.net,1433;Database=test_27032000;User ID=asdasdas@cmstest27032000;Password=123456879", ExpectedResult = false)] // Azure + [TestCase("Data Source=|DataDirectory|/Umbraco.sqlite.db;Cache=Shared;Foreign Keys=True;Pooling=True", ExpectedResult = false)] // Sqlite + [TestCase("Data Source=(LocalDb)\\MSSQLLocalDB;Initial Catalog=aspnet-MvcMovie;Integrated Security=SSPI;AttachDBFilename=|DataDirectory|\\umbraco.mdf", ExpectedResult = true)] // localDB + public bool CanRecognizeConnectionString(string connectionString) + { + var sut = new SqlLocalDbDatabaseProviderMetadata(); + return sut.CanRecognizeConnectionString(connectionString); + } +} diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Cms.Persistence.SqlServer/SqlServerDatabaseProviderMetadataTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Cms.Persistence.SqlServer/SqlServerDatabaseProviderMetadataTests.cs new file mode 100644 index 0000000000..4e96c375e1 --- /dev/null +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Cms.Persistence.SqlServer/SqlServerDatabaseProviderMetadataTests.cs @@ -0,0 +1,37 @@ +using NUnit.Framework; +using Umbraco.Cms.Core.Install.Models; +using Umbraco.Cms.Persistence.SqlServer.Services; + +namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Cms.Persistence.SqlServer; + +[TestFixture] +public class SqlServerDatabaseProviderMetadataTests +{ + [Test] + [TestCase("myServer", "myDatabase", "myLogin", "myPassword", true, ExpectedResult = "Server=myServer;Database=myDatabase;Integrated Security=true")] + [TestCase("myServer", "myDatabase", "myLogin", "myPassword", false, ExpectedResult = "Server=myServer;Database=myDatabase;User Id=myLogin;Password=myPassword")] + public string GenerateConnectionString(string server, string databaseName, string login, string password, bool integratedAuth) + { + var sut = new SqlServerDatabaseProviderMetadata(); + return sut.GenerateConnectionString(new DatabaseModel() + { + DatabaseName = databaseName, + Login = login, + Password = password, + Server = server, + IntegratedAuth = integratedAuth + }); + } + + [Test] + [TestCase("Server=myServer;Database=myDatabase;Integrated Security=true", ExpectedResult = true)] // SqlServer + [TestCase("Server=myServer;Database=myDatabase;User Id=myLogin;Password=myPassword", ExpectedResult = true)] // SqlServer + [TestCase("Server=tcp:cmstest27032000.database.windows.net,1433;Database=test_27032000;User ID=asdasdas@cmstest27032000;Password=123456879", ExpectedResult = true)] // Azure + [TestCase("Data Source=|DataDirectory|/Umbraco.sqlite.db;Cache=Shared;Foreign Keys=True;Pooling=True", ExpectedResult = false)] // Sqlite + [TestCase("Data Source=(LocalDb)\\MSSQLLocalDB;Initial Catalog=aspnet-MvcMovie;Integrated Security=SSPI;AttachDBFilename=|DataDirectory|\\umbraco.mdf", ExpectedResult = false)] // localDB + public bool CanRecognizeConnectionString(string connectionString) + { + var sut = new SqlServerDatabaseProviderMetadata(); + return sut.CanRecognizeConnectionString(connectionString); + } +} diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Cms.Persistence.Sqlite/SqliteDatabaseProviderMetadataTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Cms.Persistence.Sqlite/SqliteDatabaseProviderMetadataTests.cs new file mode 100644 index 0000000000..63eb6c86fd --- /dev/null +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Cms.Persistence.Sqlite/SqliteDatabaseProviderMetadataTests.cs @@ -0,0 +1,37 @@ +using NUnit.Framework; +using Umbraco.Cms.Core.Install.Models; +using Umbraco.Cms.Persistence.Sqlite.Services; + +namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Cms.Persistence.Sqlite; + +[TestFixture] +public class SqliteDatabaseProviderMetadataTests +{ + [Test] + [TestCase("ignored", "myDatabase", "ignored", "ignored", true /*ignored*/, ExpectedResult = "Data Source=|DataDirectory|/myDatabase.sqlite.db;Cache=Shared;Foreign Keys=True;Pooling=True")] + [TestCase("ignored", "myDatabase2", "ignored", "ignored", false /*ignored*/, ExpectedResult = "Data Source=|DataDirectory|/myDatabase2.sqlite.db;Cache=Shared;Foreign Keys=True;Pooling=True")] + public string GenerateConnectionString(string server, string databaseName, string login, string password, bool integratedAuth) + { + var sut = new SqliteDatabaseProviderMetadata(); + return sut.GenerateConnectionString(new DatabaseModel() + { + DatabaseName = databaseName, + Login = login, + Password = password, + Server = server, + IntegratedAuth = integratedAuth + }); + } + + [Test] + [TestCase("Server=myServer;Database=myDatabase;Integrated Security=true", ExpectedResult = false)] // SqlServer + [TestCase("Server=myServer;Database=myDatabase;User Id=myLogin;Password=myPassword", ExpectedResult = false)] // SqlServer + [TestCase("Server=tcp:cmstest27032000.database.windows.net,1433;Database=test_27032000;User ID=asdasdas@cmstest27032000;Password=123456879", ExpectedResult = false)] // Azure + [TestCase("Data Source=|DataDirectory|/Umbraco.sqlite.db;Cache=Shared;Foreign Keys=True;Pooling=True", ExpectedResult = true)] // Sqlite + [TestCase("Data Source=(LocalDb)\\MSSQLLocalDB;Initial Catalog=aspnet-MvcMovie;Integrated Security=SSPI;AttachDBFilename=|DataDirectory|\\umbraco.mdf", ExpectedResult = false)] // localDB + public bool CanRecognizeConnectionString(string connectionString) + { + var sut = new SqliteDatabaseProviderMetadata(); + return sut.CanRecognizeConnectionString(connectionString); + } +} diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.New.Cms.Infrastructure/Factories/DatabaseSettingsFactoryTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.New.Cms.Infrastructure/Factories/DatabaseSettingsFactoryTests.cs index fd2c7315b0..82beaeeab9 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.New.Cms.Infrastructure/Factories/DatabaseSettingsFactoryTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.New.Cms.Infrastructure/Factories/DatabaseSettingsFactoryTests.cs @@ -177,6 +177,8 @@ public class DatabaseSettingsFactoryTests public Func GenerateConnectionStringDelegate { get; set; } = _ => "ConnectionString"; + public bool CanRecognizeConnectionString(string? connectionString) => false; + public string? GenerateConnectionString(DatabaseModel databaseModel) => GenerateConnectionStringDelegate(databaseModel); } } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Tests.UnitTests.csproj b/tests/Umbraco.Tests.UnitTests/Umbraco.Tests.UnitTests.csproj index fcbb4d6908..8b6085bf6d 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Tests.UnitTests.csproj +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Tests.UnitTests.csproj @@ -16,6 +16,7 @@ + From 92d92feb1541b0a76aaef5b8e744ef3e5e514ea9 Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Wed, 5 Apr 2023 09:32:48 +0200 Subject: [PATCH 06/15] Add new 'Umbraco Package RCL' project template (#13776) * Add new 'Umbraco Package RCL' project template * Add --no-restore, hide --framework and remove conflicting -v parameter in Umbraco Package project template * Hide --framework and --PackageTestSiteName, remove conflicting -v parameter in Umbraco project template and persist selected parameters * Revert changes to Framework and UmbracoVersion parameter names --- templates/Umbraco.Templates.csproj | 1 + .../.template.config/dotnetcli.host.json | 7 +- .../.template.config/template.json | 23 +++++- .../.template.config/dotnetcli.host.json | 22 +++++ .../.template.config/ide.host.json | 20 +++++ .../.template.config/template.json | 82 +++++++++++++++++++ .../UmbracoPackageRcl/UmbracoPackage.csproj | 27 ++++++ .../wwwroot/package.manifest | 5 ++ .../.template.config/dotnetcli.host.json | 16 ++-- .../.template.config/ide.host.json | 20 ++--- .../.template.config/template.json | 2 +- 11 files changed, 203 insertions(+), 22 deletions(-) create mode 100644 templates/UmbracoPackageRcl/.template.config/dotnetcli.host.json create mode 100644 templates/UmbracoPackageRcl/.template.config/ide.host.json create mode 100644 templates/UmbracoPackageRcl/.template.config/template.json create mode 100644 templates/UmbracoPackageRcl/UmbracoPackage.csproj create mode 100644 templates/UmbracoPackageRcl/wwwroot/package.manifest diff --git a/templates/Umbraco.Templates.csproj b/templates/Umbraco.Templates.csproj index 10bbd666d1..3f9e1f0ce4 100644 --- a/templates/Umbraco.Templates.csproj +++ b/templates/Umbraco.Templates.csproj @@ -12,6 +12,7 @@ + UmbracoProject\Program.cs diff --git a/templates/UmbracoPackage/.template.config/dotnetcli.host.json b/templates/UmbracoPackage/.template.config/dotnetcli.host.json index 6bd79b7fd9..6473c5c643 100644 --- a/templates/UmbracoPackage/.template.config/dotnetcli.host.json +++ b/templates/UmbracoPackage/.template.config/dotnetcli.host.json @@ -3,11 +3,16 @@ "symbolInfo": { "Framework": { "longName": "Framework", - "shortName": "F" + "shortName": "F", + "isHidden": true }, "UmbracoVersion": { "longName": "version", "shortName": "v" + }, + "SkipRestore": { + "longName": "no-restore", + "shortName": "" } } } diff --git a/templates/UmbracoPackage/.template.config/template.json b/templates/UmbracoPackage/.template.config/template.json index 36c93da093..b1b4ef9425 100644 --- a/templates/UmbracoPackage/.template.config/template.json +++ b/templates/UmbracoPackage/.template.config/template.json @@ -41,9 +41,16 @@ "description": "The version of Umbraco.Cms to add as PackageReference.", "type": "parameter", "datatype": "string", - "defaultValue": "10.0.0-rc1", + "defaultValue": "11.0.0", "replaces": "UMBRACO_VERSION_FROM_TEMPLATE" }, + "SkipRestore": { + "displayName": "Skip restore", + "description": "If specified, skips the automatic restore of the project on create.", + "type": "parameter", + "datatype": "bool", + "defaultValue": "false" + }, "Namespace": { "type": "derived", "valueSource": "name", @@ -83,5 +90,19 @@ { "path": "UmbracoPackage.csproj" } + ], + "postActions": [ + { + "id": "restore", + "condition": "(!SkipRestore)", + "description": "Restore NuGet packages required by this project.", + "manualInstructions": [ + { + "text": "Run 'dotnet restore'" + } + ], + "actionId": "210D431B-A78B-4D2F-B762-4ED3E3EA9025", + "continueOnError": true + } ] } diff --git a/templates/UmbracoPackageRcl/.template.config/dotnetcli.host.json b/templates/UmbracoPackageRcl/.template.config/dotnetcli.host.json new file mode 100644 index 0000000000..9a960b348e --- /dev/null +++ b/templates/UmbracoPackageRcl/.template.config/dotnetcli.host.json @@ -0,0 +1,22 @@ +{ + "$schema": "https://json.schemastore.org/dotnetcli.host.json", + "symbolInfo": { + "Framework": { + "longName": "Framework", + "shortName": "F", + "isHidden": true + }, + "UmbracoVersion": { + "longName": "version", + "shortName": "" + }, + "SkipRestore": { + "longName": "no-restore", + "shortName": "" + }, + "SupportPagesAndViews": { + "longName": "support-pages-and-views", + "shortName": "s" + } + } +} diff --git a/templates/UmbracoPackageRcl/.template.config/ide.host.json b/templates/UmbracoPackageRcl/.template.config/ide.host.json new file mode 100644 index 0000000000..8e630f1e99 --- /dev/null +++ b/templates/UmbracoPackageRcl/.template.config/ide.host.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://json.schemastore.org/ide.host.json", + "order": 0, + "icon": "../../icon.png", + "description": { + "id": "UmbracoPackageRcl", + "text": "Umbraco Package RCL - An empty Umbraco package/plugin (Razor Class Library)." + }, + "symbolInfo": [ + { + "id": "UmbracoVersion", + "isVisible": true + }, + { + "id": "SupportPagesAndViews", + "isVisible": true, + "persistenceScope": "templateGroup" + } + ] +} diff --git a/templates/UmbracoPackageRcl/.template.config/template.json b/templates/UmbracoPackageRcl/.template.config/template.json new file mode 100644 index 0000000000..c61289f6c2 --- /dev/null +++ b/templates/UmbracoPackageRcl/.template.config/template.json @@ -0,0 +1,82 @@ +{ + "$schema": "https://json.schemastore.org/template.json", + "author": "Umbraco HQ", + "classifications": [ + "Web", + "CMS", + "Umbraco", + "Package", + "Plugin", + "Razor Class Library" + ], + "name": "Umbraco Package RCL", + "description": "An empty Umbraco package/plugin (Razor Class Library).", + "groupIdentity": "Umbraco.Templates.UmbracoPackageRcl", + "identity": "Umbraco.Templates.UmbracoPackageRcl.CSharp", + "shortName": "umbracopackage-rcl", + "tags": { + "language": "C#", + "type": "project" + }, + "sourceName": "UmbracoPackage", + "defaultName": "UmbracoPackage1", + "preferNameDirectory": true, + "symbols": { + "Framework": { + "displayName": "Framework", + "description": "The target framework for the project.", + "type": "parameter", + "datatype": "choice", + "choices": [ + { + "displayName": ".NET 7.0", + "description": "Target net7.0", + "choice": "net7.0" + } + ], + "defaultValue": "net7.0", + "replaces": "net7.0" + }, + "UmbracoVersion": { + "displayName": "Umbraco version", + "description": "The version of Umbraco.Cms to add as PackageReference.", + "type": "parameter", + "datatype": "string", + "defaultValue": "11.0.0", + "replaces": "UMBRACO_VERSION_FROM_TEMPLATE" + }, + "SkipRestore": { + "displayName": "Skip restore", + "description": "If specified, skips the automatic restore of the project on create.", + "type": "parameter", + "datatype": "bool", + "defaultValue": "false" + }, + "SupportPagesAndViews": { + "type": "parameter", + "datatype": "bool", + "defaultValue": "false", + "displayName": "Support pages and views", + "description": "Whether to support adding traditional Razor pages and Views to this library." + } + }, + "primaryOutputs": [ + { + "path": "UmbracoPackage.csproj" + } + ], + "postActions": [ + { + "id": "restore", + "condition": "(!SkipRestore)", + "description": "Restore NuGet packages required by this project.", + "manualInstructions": [ + { + "text": "Run 'dotnet restore'" + } + ], + "actionId": "210D431B-A78B-4D2F-B762-4ED3E3EA9025", + "continueOnError": true + } + ] +} diff --git a/templates/UmbracoPackageRcl/UmbracoPackage.csproj b/templates/UmbracoPackageRcl/UmbracoPackage.csproj new file mode 100644 index 0000000000..5c980684ce --- /dev/null +++ b/templates/UmbracoPackageRcl/UmbracoPackage.csproj @@ -0,0 +1,27 @@ + + + net7.0 + enable + enable + true + UmbracoPackage + App_Plugins/UmbracoPackage + + + + UmbracoPackage + UmbracoPackage + UmbracoPackage + ... + umbraco plugin package + + + + + + + + + + + diff --git a/templates/UmbracoPackageRcl/wwwroot/package.manifest b/templates/UmbracoPackageRcl/wwwroot/package.manifest new file mode 100644 index 0000000000..6aadd0cee6 --- /dev/null +++ b/templates/UmbracoPackageRcl/wwwroot/package.manifest @@ -0,0 +1,5 @@ +{ + "name": "UmbracoPackage", + "version": "", + "allowPackageTelemetry": true +} diff --git a/templates/UmbracoProject/.template.config/dotnetcli.host.json b/templates/UmbracoProject/.template.config/dotnetcli.host.json index 7f5479eab0..dfd6f80184 100644 --- a/templates/UmbracoProject/.template.config/dotnetcli.host.json +++ b/templates/UmbracoProject/.template.config/dotnetcli.host.json @@ -3,7 +3,8 @@ "symbolInfo": { "Framework": { "longName": "Framework", - "shortName": "F" + "shortName": "F", + "isHidden": true }, "UmbracoVersion": { "longName": "version", @@ -55,14 +56,15 @@ }, "PackageProjectName": { "longName": "PackageTestSiteName", - "shortName": "p" + "shortName": "p", + "isHidden": true } }, "usageExamples": [ - "dotnet new umbraco -n MyNewProject", - "dotnet new umbraco -n MyNewProject --no-restore", - "dotnet new umbraco -n MyNewProject --development-database-type SQLite", - "dotnet new umbraco -n MyNewProject --development-database-type LocalDB", - "dotnet new umbraco -n MyNewProject --friendly-name \"Friendly Admin User\" --email admin@example.com --password password1234 --connection-string \"Server=ConnectionStringHere\"" + "dotnet new umbraco --name MyNewProject", + "dotnet new umbraco --name MyNewProject --no-restore", + "dotnet new umbraco --name MyNewProject --development-database-type SQLite", + "dotnet new umbraco --name MyNewProject --development-database-type LocalDB", + "dotnet new umbraco --name MyNewProject --friendly-name \"Administrator\" --email admin@example.com --password 1234567890 --connection-string \"Server=(local);Database=MyNewProject;Trusted_Connection=True;\"" ] } diff --git a/templates/UmbracoProject/.template.config/ide.host.json b/templates/UmbracoProject/.template.config/ide.host.json index 617f924f97..1a302779cc 100644 --- a/templates/UmbracoProject/.template.config/ide.host.json +++ b/templates/UmbracoProject/.template.config/ide.host.json @@ -13,19 +13,18 @@ }, { "id": "UseHttpsRedirect", - "isVisible": true - }, - { - "id": "SkipRestore", - "isVisible": true + "isVisible": true, + "persistenceScope": "templateGroup" }, { "id": "ExcludeGitignore", - "isVisible": true + "isVisible": true, + "persistenceScope": "templateGroup" }, { "id": "MinimalGitignore", - "isVisible": true + "isVisible": true, + "persistenceScope": "templateGroup" }, { "id": "ConnectionString", @@ -37,7 +36,8 @@ }, { "id": "DevelopmentDatabaseType", - "isVisible": true + "isVisible": true, + "persistenceScope": "templateGroup" }, { "id": "UnattendedUserName", @@ -54,10 +54,6 @@ { "id": "NoNodesViewPath", "isVisible": true - }, - { - "id": "PackageProjectName", - "isVisible": true } ] } diff --git a/templates/UmbracoProject/.template.config/template.json b/templates/UmbracoProject/.template.config/template.json index befe6d7e5a..049db86bf1 100644 --- a/templates/UmbracoProject/.template.config/template.json +++ b/templates/UmbracoProject/.template.config/template.json @@ -51,7 +51,7 @@ "description": "The version of Umbraco.Cms to add as PackageReference.", "type": "parameter", "datatype": "string", - "defaultValue": "10.0.0-rc1", + "defaultValue": "11.0.0", "replaces": "UMBRACO_VERSION_FROM_TEMPLATE" }, "UseHttpsRedirect": { From 4cb46f06ae300cbe9792f6c9bce95b9cab04bd60 Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Wed, 5 Apr 2023 11:16:43 +0200 Subject: [PATCH 07/15] Update default UmbracoVersion template value using MSBuild target (#13481) * Update default UmbracoVersion template value using MSBuild target * Fix target order when using pack --no-build * Use JsonPathUpdateValue MSBuild task to update default UmbracoVersion template value * Update UmbracoVersion in Umbraco Package RCL --- build/azure-pipelines.yml | 19 ++-------------- templates/Umbraco.Templates.csproj | 22 +++++++++++++++++++ .../.template.config/template.json | 2 +- .../.template.config/template.json | 2 +- .../.template.config/template.json | 2 +- 5 files changed, 27 insertions(+), 20 deletions(-) diff --git a/build/azure-pipelines.yml b/build/azure-pipelines.yml index 97e0a10f9c..310477fa4c 100644 --- a/build/azure-pipelines.yml +++ b/build/azure-pipelines.yml @@ -94,23 +94,8 @@ stages: echo "##vso[task.setvariable variable=majorVersion;isOutput=true]$major" displayName: Set major version name: determineMajorVersion - - task: PowerShell@2 - displayName: Prepare nupkg - inputs: - targetType: inline - script: | - $umbracoVersion = "$(Build.BuildNumber)" -replace "\+",".g" - $templatePaths = Get-ChildItem 'templates/**/.template.config/template.json' - - foreach ($templatePath in $templatePaths) { - $a = Get-Content $templatePath -Raw | ConvertFrom-Json - if ($a.symbols -and $a.symbols.UmbracoVersion) { - $a.symbols.UmbracoVersion.defaultValue = $umbracoVersion - $a | ConvertTo-Json -Depth 32 | Set-Content $templatePath - } - } - - dotnet pack $(solution) --configuration $(buildConfiguration) --no-build --property:PackageOutputPath=$(Build.ArtifactStagingDirectory)/nupkg + - script: dotnet pack $(solution) --configuration $(buildConfiguration) --no-build --property:PackageOutputPath=$(Build.ArtifactStagingDirectory)/nupkg + displayName: Run dotnet pack - script: | sha="$(Build.SourceVersion)" sha=${sha:0:7} diff --git a/templates/Umbraco.Templates.csproj b/templates/Umbraco.Templates.csproj index 3f9e1f0ce4..38848d398a 100644 --- a/templates/Umbraco.Templates.csproj +++ b/templates/Umbraco.Templates.csproj @@ -8,6 +8,7 @@ true true . + NU5128 @@ -43,4 +44,25 @@ UmbracoProject\wwwroot + + + + + + + + <_TemplateJsonFiles Include="**\.template.config\template.json" Exclude="bin\**;obj\**" /> + <_TemplateJsonFiles> + $(IntermediateOutputPath)%(RelativeDir)%(Filename)%(Extension) + + + + + + <_PackageFiles Remove="@(_TemplateJsonFiles)" /> + <_PackageFiles Include="%(_TemplateJsonFiles.DestinationFile)"> + %(RelativeDir) + + + diff --git a/templates/UmbracoPackage/.template.config/template.json b/templates/UmbracoPackage/.template.config/template.json index b1b4ef9425..768a7a4bee 100644 --- a/templates/UmbracoPackage/.template.config/template.json +++ b/templates/UmbracoPackage/.template.config/template.json @@ -41,7 +41,7 @@ "description": "The version of Umbraco.Cms to add as PackageReference.", "type": "parameter", "datatype": "string", - "defaultValue": "11.0.0", + "defaultValue": "*", "replaces": "UMBRACO_VERSION_FROM_TEMPLATE" }, "SkipRestore": { diff --git a/templates/UmbracoPackageRcl/.template.config/template.json b/templates/UmbracoPackageRcl/.template.config/template.json index c61289f6c2..be7b1c04e8 100644 --- a/templates/UmbracoPackageRcl/.template.config/template.json +++ b/templates/UmbracoPackageRcl/.template.config/template.json @@ -42,7 +42,7 @@ "description": "The version of Umbraco.Cms to add as PackageReference.", "type": "parameter", "datatype": "string", - "defaultValue": "11.0.0", + "defaultValue": "*", "replaces": "UMBRACO_VERSION_FROM_TEMPLATE" }, "SkipRestore": { diff --git a/templates/UmbracoProject/.template.config/template.json b/templates/UmbracoProject/.template.config/template.json index 049db86bf1..d88b23c07d 100644 --- a/templates/UmbracoProject/.template.config/template.json +++ b/templates/UmbracoProject/.template.config/template.json @@ -51,7 +51,7 @@ "description": "The version of Umbraco.Cms to add as PackageReference.", "type": "parameter", "datatype": "string", - "defaultValue": "11.0.0", + "defaultValue": "*", "replaces": "UMBRACO_VERSION_FROM_TEMPLATE" }, "UseHttpsRedirect": { From 9223b5737ea5c7e4e4f0918b4484502d9b8b340c Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Wed, 5 Apr 2023 12:59:01 +0200 Subject: [PATCH 08/15] Update target to execute during packaging (#13521) Co-authored-by: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com> --- src/Umbraco.Cms.Targets/Umbraco.Cms.Targets.csproj | 4 ++-- src/Umbraco.Cms/Umbraco.Cms.csproj | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Cms.Targets/Umbraco.Cms.Targets.csproj b/src/Umbraco.Cms.Targets/Umbraco.Cms.Targets.csproj index d537b116c0..18f9beea96 100644 --- a/src/Umbraco.Cms.Targets/Umbraco.Cms.Targets.csproj +++ b/src/Umbraco.Cms.Targets/Umbraco.Cms.Targets.csproj @@ -50,10 +50,10 @@ - + - + <_PackageFiles Include="$(IntermediateOutputPath)_._" PackagePath="lib\$(TargetFramework)" /> diff --git a/src/Umbraco.Cms/Umbraco.Cms.csproj b/src/Umbraco.Cms/Umbraco.Cms.csproj index 1ff6e848ad..da6be4c30c 100644 --- a/src/Umbraco.Cms/Umbraco.Cms.csproj +++ b/src/Umbraco.Cms/Umbraco.Cms.csproj @@ -14,10 +14,10 @@ - + - + <_PackageFiles Include="$(IntermediateOutputPath)_._" PackagePath="lib\$(TargetFramework)" /> From 22328598dbbb22b36fb395e52507eeba9c3c3000 Mon Sep 17 00:00:00 2001 From: Elitsa Marinovska <21998037+elit0451@users.noreply.github.com> Date: Tue, 11 Apr 2023 15:41:55 +0200 Subject: [PATCH 09/15] Adding dedicated Forbidden and Unauthorized handling for members (#14036) --- .../Filters/UmbracoMemberAuthorizeFilter.cs | 15 ++++++++++++--- .../Security/ConfigureMemberCookieOptions.cs | 7 +++++++ 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Web.Common/Filters/UmbracoMemberAuthorizeFilter.cs b/src/Umbraco.Web.Common/Filters/UmbracoMemberAuthorizeFilter.cs index 351ea6e1bf..95c4ae5cec 100644 --- a/src/Umbraco.Web.Common/Filters/UmbracoMemberAuthorizeFilter.cs +++ b/src/Umbraco.Web.Common/Filters/UmbracoMemberAuthorizeFilter.cs @@ -54,11 +54,20 @@ public class UmbracoMemberAuthorizeFilter : IAsyncAuthorizationFilter IMemberManager memberManager = context.HttpContext.RequestServices.GetRequiredService(); - if (!await IsAuthorizedAsync(memberManager)) + if (memberManager.IsLoggedIn()) + { + if (!await IsAuthorizedAsync(memberManager)) + { + context.HttpContext.SetReasonPhrase( + "Resource restricted: the member is not of a permitted type or group."); + context.Result = new ForbidResult(); + } + } + else { context.HttpContext.SetReasonPhrase( - "Resource restricted: either member is not logged on or is not of a permitted type or group."); - context.Result = new ForbidResult(); + "Resource restricted: the member is not logged in."); + context.Result = new UnauthorizedResult(); } } diff --git a/src/Umbraco.Web.Common/Security/ConfigureMemberCookieOptions.cs b/src/Umbraco.Web.Common/Security/ConfigureMemberCookieOptions.cs index 1e0960fbc7..9aa073483a 100644 --- a/src/Umbraco.Web.Common/Security/ConfigureMemberCookieOptions.cs +++ b/src/Umbraco.Web.Common/Security/ConfigureMemberCookieOptions.cs @@ -1,4 +1,5 @@ using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Routing; @@ -44,6 +45,12 @@ public sealed class ConfigureMemberCookieOptions : IConfigureNamedOptions + { + ctx.Response.StatusCode = StatusCodes.Status403Forbidden; + return Task.CompletedTask; }, }; From 8e32ac3e20312c7f8317d1b026b7367366685470 Mon Sep 17 00:00:00 2001 From: Nikolaj Date: Wed, 12 Apr 2023 09:35:23 +0200 Subject: [PATCH 10/15] Bump to non-rc --- version.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.json b/version.json index 8fc9e69f4e..4c30184f1a 100644 --- a/version.json +++ b/version.json @@ -1,6 +1,6 @@ { "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json", - "version": "10.5.0-rc", + "version": "10.5.0", "assemblyVersion": { "precision": "build" }, From a1d6f65cff68d35e99dcc123771218f6d80ad984 Mon Sep 17 00:00:00 2001 From: Nikolaj Date: Wed, 12 Apr 2023 09:36:49 +0200 Subject: [PATCH 11/15] Bump to non-rc --- version.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.json b/version.json index 06b868d719..90dd35cd59 100644 --- a/version.json +++ b/version.json @@ -1,6 +1,6 @@ { "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json", - "version": "11.3.0-rc", + "version": "11.3.0", "assemblyVersion": { "precision": "build" }, From c636c7a7d285af0d96a75f0692a28141bc30d721 Mon Sep 17 00:00:00 2001 From: Nikolaj Date: Thu, 13 Apr 2023 08:52:50 +0200 Subject: [PATCH 12/15] Bump version --- version.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.json b/version.json index 90dd35cd59..4ea57b6f8c 100644 --- a/version.json +++ b/version.json @@ -1,6 +1,6 @@ { "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json", - "version": "11.3.0", + "version": "11.4.0-rc", "assemblyVersion": { "precision": "build" }, From 5d3546e1b59fe57eb32e9d086f5cc7c02426d1ac Mon Sep 17 00:00:00 2001 From: Nikolaj Date: Thu, 13 Apr 2023 08:53:47 +0200 Subject: [PATCH 13/15] Bump version --- version.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.json b/version.json index 4c30184f1a..0c594e5b7e 100644 --- a/version.json +++ b/version.json @@ -1,6 +1,6 @@ { "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json", - "version": "10.5.0", + "version": "10.6.0-rc", "assemblyVersion": { "precision": "build" }, From fe11f3c8e13e9cc49adb79e62d62aa35badfc0ce Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Thu, 13 Apr 2023 11:01:28 +0200 Subject: [PATCH 14/15] Bumped version to 12.0.0-rc (#14087) * Bumped version to 12.0.0-alpha1 * Update version.json --------- Co-authored-by: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com> --- version.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.json b/version.json index 06b868d719..1af6ab23c5 100644 --- a/version.json +++ b/version.json @@ -1,6 +1,6 @@ { "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json", - "version": "11.3.0-rc", + "version": "12.0.0-rc", "assemblyVersion": { "precision": "build" }, From eb31889be9f9f2daac7fd4b43e916bcf00e4bd8a Mon Sep 17 00:00:00 2001 From: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com> Date: Thu, 13 Apr 2023 12:23:44 +0200 Subject: [PATCH 15/15] V12: Cherry pick unscoped migrations (#14086) * New Backoffice: Refactor migrations to allow for unscoped migrations (#13654) * Remove PostMigrations These should be replaced with Notification usage * Remove outer scope from Upgrader * Remove unececary null check * Add marker base class for migrations * Enable scopeless migrations * Remove unnecessary state check The final state of the migration is no longer necessarily the final state of the plan. * Extend ExecutedMigrationPlan * Ensure that MigrationPlanExecutor.Execute always returns a result. * Always save final state, regardless of errors * Remove obsolete Execute * Add Umbraco specific migration notification * Publish notification after umbraco migration * Throw the exception that failed a migration after publishing notification * Handle notification publishing in DatabaseBuilder * Fix tests * Remember to complete scope * Clean up MigrationPlanExecutor * Run each package migration in a separate scope * Add PartialMigrationsTests * Add unhappy path test * Fix bug shown by test * Move PartialMigrationsTests into the correct folder * Comment out refresh cache in data type migration Need to add this back again as a notification handler or something. * Start working on a notification test * Allow migrations to request a cache rebuild * Set RebuildCache from MigrateDataTypeConfigurations * Clean MigrationPlanExecutor * Add comment explaining the need to partial migration success * Fix tests * Allow overriding DefinePlan of UmbracoPlan This is needed to test the DatabaseBuilder * Fix notification test * Don't throw exception to be immediately re-caught * Assert that scopes notification are always published * Ensure that scopes are created when requested * Make test classes internal. It doesn't really matter, but this way it doesn't show up in intellisense * Add notification handler for clearing cookies * Add CompatibilitySuppressions * Rename Execute to ExecutePlan We have to do this to be able to obsolete :( * Update CompatibilitySuppressions * Update src/Umbraco.Infrastructure/Migrations/MigrationPlanExecutor.cs Co-authored-by: Bjarke Berg Co-authored-by: Bjarke Berg * generate compatability suppresion file --------- Co-authored-by: Mole Co-authored-by: Bjarke Berg Co-authored-by: Zeegaan --- .../CompatibilitySuppressions.xml | 73 +++++ .../UmbracoBuilder.Installer.cs | 4 + .../InstallSteps/DatabaseUpgradeStep.cs | 1 - .../Install/PackageMigrationRunner.cs | 37 +-- .../Migrations/ExecutedMigrationPlan.cs | 48 ++- .../Migrations/IMigrationContext.cs | 7 - .../Migrations/IMigrationPlanExecutor.cs | 9 +- .../Migrations/Install/DatabaseBuilder.cs | 18 +- .../Migrations/MigrationBase.cs | 5 + .../Migrations/MigrationContext.cs | 15 - .../Migrations/MigrationPlan.cs | 19 -- .../Migrations/MigrationPlanExecutor.cs | 210 ++++++++---- .../MigrationPlansExecutedNotification.cs | 10 +- .../UmbracoPlanExecutedNotification.cs | 19 ++ .../PostMigrations/ClearCsrfCookieHandler.cs | 28 ++ .../PostMigrations/ClearCsrfCookies.cs | 22 -- .../DeleteLogViewerQueryFile.cs | 32 -- .../RebuildPublishedSnapshot.cs | 20 -- .../Migrations/UnscopedMigrationBase.cs | 14 + .../Migrations/Upgrade/UmbracoPlan.cs | 5 +- .../Migrations/Upgrade/Upgrader.cs | 80 ++--- .../DropDownPropertyEditorsMigration.cs | 5 +- ...adioAndCheckboxPropertyEditorsMigration.cs | 5 +- .../V_8_0_1/ChangeNuCacheJsonFormat.cs | 9 +- .../MigrateLogViewerQueriesFromFileToDb.cs | 2 - .../Installer/Steps/DatabaseUpgradeStep.cs | 2 +- .../Migrations/PartialMigrationsTests.cs | 310 ++++++++++++++++++ .../Migrations/MigrationPlanTests.cs | 24 +- .../Migrations/PostMigrationTests.cs | 172 ---------- 29 files changed, 756 insertions(+), 449 deletions(-) create mode 100644 src/Umbraco.Infrastructure/CompatibilitySuppressions.xml create mode 100644 src/Umbraco.Infrastructure/Migrations/Notifications/UmbracoPlanExecutedNotification.cs create mode 100644 src/Umbraco.Infrastructure/Migrations/PostMigrations/ClearCsrfCookieHandler.cs delete mode 100644 src/Umbraco.Infrastructure/Migrations/PostMigrations/ClearCsrfCookies.cs delete mode 100644 src/Umbraco.Infrastructure/Migrations/PostMigrations/DeleteLogViewerQueryFile.cs delete mode 100644 src/Umbraco.Infrastructure/Migrations/PostMigrations/RebuildPublishedSnapshot.cs create mode 100644 src/Umbraco.Infrastructure/Migrations/UnscopedMigrationBase.cs create mode 100644 tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Migrations/PartialMigrationsTests.cs delete mode 100644 tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Migrations/PostMigrationTests.cs diff --git a/src/Umbraco.Infrastructure/CompatibilitySuppressions.xml b/src/Umbraco.Infrastructure/CompatibilitySuppressions.xml new file mode 100644 index 0000000000..feff36d669 --- /dev/null +++ b/src/Umbraco.Infrastructure/CompatibilitySuppressions.xml @@ -0,0 +1,73 @@ + + + + CP0001 + T:Umbraco.Cms.Infrastructure.Migrations.PostMigrations.ClearCsrfCookies + lib/net7.0/Umbraco.Infrastructure.dll + lib/net7.0/Umbraco.Infrastructure.dll + true + + + CP0001 + T:Umbraco.Cms.Infrastructure.Migrations.PostMigrations.DeleteLogViewerQueryFile + lib/net7.0/Umbraco.Infrastructure.dll + lib/net7.0/Umbraco.Infrastructure.dll + true + + + CP0001 + T:Umbraco.Cms.Infrastructure.Migrations.PostMigrations.RebuildPublishedSnapshot + lib/net7.0/Umbraco.Infrastructure.dll + lib/net7.0/Umbraco.Infrastructure.dll + true + + + CP0002 + M:Umbraco.Cms.Core.Migrations.IMigrationPlanExecutor.Execute(Umbraco.Cms.Infrastructure.Migrations.MigrationPlan,System.String) + lib/net7.0/Umbraco.Infrastructure.dll + lib/net7.0/Umbraco.Infrastructure.dll + true + + + CP0002 + M:Umbraco.Cms.Infrastructure.Migrations.IMigrationContext.AddPostMigration``1 + lib/net7.0/Umbraco.Infrastructure.dll + lib/net7.0/Umbraco.Infrastructure.dll + true + + + CP0002 + M:Umbraco.Cms.Infrastructure.Migrations.MigrationPlan.AddPostMigration``1 + lib/net7.0/Umbraco.Infrastructure.dll + lib/net7.0/Umbraco.Infrastructure.dll + true + + + CP0002 + M:Umbraco.Cms.Infrastructure.Migrations.MigrationPlan.get_PostMigrationTypes + lib/net7.0/Umbraco.Infrastructure.dll + lib/net7.0/Umbraco.Infrastructure.dll + true + + + CP0002 + M:Umbraco.Cms.Infrastructure.Migrations.MigrationPlanExecutor.Execute(Umbraco.Cms.Infrastructure.Migrations.MigrationPlan,System.String) + lib/net7.0/Umbraco.Infrastructure.dll + lib/net7.0/Umbraco.Infrastructure.dll + true + + + CP0002 + M:Umbraco.Cms.Infrastructure.Migrations.Upgrade.Upgrader.Execute(Umbraco.Cms.Core.Migrations.IMigrationPlanExecutor,Umbraco.Cms.Core.Scoping.IScopeProvider,Umbraco.Cms.Core.Services.IKeyValueService) + lib/net7.0/Umbraco.Infrastructure.dll + lib/net7.0/Umbraco.Infrastructure.dll + true + + + CP0006 + M:Umbraco.Cms.Core.Migrations.IMigrationPlanExecutor.ExecutePlan(Umbraco.Cms.Infrastructure.Migrations.MigrationPlan,System.String) + lib/net7.0/Umbraco.Infrastructure.dll + lib/net7.0/Umbraco.Infrastructure.dll + true + + \ No newline at end of file diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Installer.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Installer.cs index c3aa291fb7..c63f76b6d8 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Installer.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Installer.cs @@ -7,6 +7,8 @@ using Umbraco.Cms.Core.Install.Models; using Umbraco.Cms.Core.Telemetry; using Umbraco.Cms.Infrastructure.Install; using Umbraco.Cms.Infrastructure.Install.InstallSteps; +using Umbraco.Cms.Infrastructure.Migrations.Notifications; +using Umbraco.Cms.Infrastructure.Migrations.PostMigrations; namespace Umbraco.Cms.Infrastructure.DependencyInjection; @@ -38,6 +40,8 @@ public static partial class UmbracoBuilderExtensions builder.Services.AddTransient(); + // Add post migration notification handlers + builder.AddNotificationHandler(); return builder; } } diff --git a/src/Umbraco.Infrastructure/Install/InstallSteps/DatabaseUpgradeStep.cs b/src/Umbraco.Infrastructure/Install/InstallSteps/DatabaseUpgradeStep.cs index 4039533fa1..38ad9e178d 100644 --- a/src/Umbraco.Infrastructure/Install/InstallSteps/DatabaseUpgradeStep.cs +++ b/src/Umbraco.Infrastructure/Install/InstallSteps/DatabaseUpgradeStep.cs @@ -48,7 +48,6 @@ namespace Umbraco.Cms.Infrastructure.Install.InstallSteps _logger.LogInformation("Running 'Upgrade' service"); var plan = new UmbracoPlan(_umbracoVersion); - plan.AddPostMigration(); // needed when running installer (back-office) DatabaseBuilder.Result? result = _databaseBuilder.UpgradeSchemaAndData(plan); diff --git a/src/Umbraco.Infrastructure/Install/PackageMigrationRunner.cs b/src/Umbraco.Infrastructure/Install/PackageMigrationRunner.cs index 348496a03c..f82c32e75a 100644 --- a/src/Umbraco.Infrastructure/Install/PackageMigrationRunner.cs +++ b/src/Umbraco.Infrastructure/Install/PackageMigrationRunner.cs @@ -21,6 +21,7 @@ namespace Umbraco.Cms.Infrastructure.Install; public class PackageMigrationRunner { private readonly IEventAggregator _eventAggregator; + private readonly ILogger _logger; private readonly IKeyValueService _keyValueService; private readonly IMigrationPlanExecutor _migrationPlanExecutor; private readonly Dictionary _packageMigrationPlans; @@ -44,6 +45,7 @@ public class PackageMigrationRunner _migrationPlanExecutor = migrationPlanExecutor; _keyValueService = keyValueService; _eventAggregator = eventAggregator; + _logger = logger; _packageMigrationPlans = packageMigrationPlans.ToDictionary(x => x.Name); } @@ -96,30 +98,27 @@ public class PackageMigrationRunner /// If any plan fails it will throw an exception. public IEnumerable RunPackagePlans(IEnumerable plansToRun) { - var results = new List(); + List results = new(); - // Create an explicit scope around all package migrations so they are - // all executed in a single transaction. If one package migration fails, - // none of them will be committed. This is intended behavior so we can - // ensure when we publish the success notification that is is done when they all succeed. - using (ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true)) + // We don't create an explicit scope, the Upgrader will handle that depending on the migration type. + // We also run ALL the package migration to completion, even if one fails, my package should not fail if someone else's package does + foreach (var migrationName in plansToRun) { - foreach (var migrationName in plansToRun) + if (_packageMigrationPlans.TryGetValue(migrationName, out PackageMigrationPlan? plan) is false) { - if (!_packageMigrationPlans.TryGetValue(migrationName, out PackageMigrationPlan? plan)) - { - throw new InvalidOperationException("Cannot find package migration plan " + migrationName); - } + // If we can't find the migration plan for a package we'll just log a message and continue. + _logger.LogError("Package migration failed for {migrationName}, was unable to find the migration plan", migrationName); + continue; + } - using (_profilingLogger.TraceDuration( - "Starting unattended package migration for " + migrationName, - "Unattended upgrade completed for " + migrationName)) - { - var upgrader = new Upgrader(plan); + using (_profilingLogger.TraceDuration( + "Starting unattended package migration for " + migrationName, + "Unattended upgrade completed for " + migrationName)) + { + Upgrader upgrader = new(plan); - // This may throw, if so the transaction will be rolled back - results.Add(upgrader.Execute(_migrationPlanExecutor, _scopeProvider, _keyValueService)); - } + // This may throw, if so the transaction will be rolled back + results.Add(upgrader.Execute(_migrationPlanExecutor, _scopeProvider, _keyValueService)); } } diff --git a/src/Umbraco.Infrastructure/Migrations/ExecutedMigrationPlan.cs b/src/Umbraco.Infrastructure/Migrations/ExecutedMigrationPlan.cs index 2e99770874..0298939b8f 100644 --- a/src/Umbraco.Infrastructure/Migrations/ExecutedMigrationPlan.cs +++ b/src/Umbraco.Infrastructure/Migrations/ExecutedMigrationPlan.cs @@ -9,9 +9,51 @@ public class ExecutedMigrationPlan FinalState = finalState ?? throw new ArgumentNullException(nameof(finalState)); } - public MigrationPlan Plan { get; } + public ExecutedMigrationPlan( + MigrationPlan plan, + string initialState, + string finalState, + bool successful, + IReadOnlyList completedTransitions) + { + Plan = plan; + InitialState = initialState ?? throw new ArgumentNullException(nameof(initialState)); + FinalState = finalState ?? throw new ArgumentNullException(nameof(finalState)); + Successful = successful; + CompletedTransitions = completedTransitions; + } - public string InitialState { get; } + public ExecutedMigrationPlan() + { + } - public string FinalState { get; } + /// + /// The Migration plan itself. + /// + public required MigrationPlan Plan { get; init; } + + /// + /// The initial state the plan started from, is null if the plan started from the beginning. + /// + public required string InitialState { get; init; } + + /// + /// The final state after the migrations has ran. + /// + public required string FinalState { get; init; } + + /// + /// Determines if the migration plan was a success, that is that all migrations ran successfully. + /// + public required bool Successful { get; init; } + + /// + /// The exception that caused the plan to fail. + /// + public Exception? Exception { get; init; } + + /// + /// A collection of all the succeeded transition. + /// + public required IReadOnlyList CompletedTransitions { get; init; } } diff --git a/src/Umbraco.Infrastructure/Migrations/IMigrationContext.cs b/src/Umbraco.Infrastructure/Migrations/IMigrationContext.cs index 5e0766755a..0001b3381d 100644 --- a/src/Umbraco.Infrastructure/Migrations/IMigrationContext.cs +++ b/src/Umbraco.Infrastructure/Migrations/IMigrationContext.cs @@ -37,11 +37,4 @@ public interface IMigrationContext /// Gets or sets a value indicating whether an expression is being built. /// bool BuildingExpression { get; set; } - - /// - /// Adds a post-migration. - /// - [Obsolete("This will be removed in the V13, and replaced with a RebuildCache flag on the MigrationBase")] - void AddPostMigration() - where TMigration : MigrationBase; } diff --git a/src/Umbraco.Infrastructure/Migrations/IMigrationPlanExecutor.cs b/src/Umbraco.Infrastructure/Migrations/IMigrationPlanExecutor.cs index 78ae07ccf5..f9f87f9e00 100644 --- a/src/Umbraco.Infrastructure/Migrations/IMigrationPlanExecutor.cs +++ b/src/Umbraco.Infrastructure/Migrations/IMigrationPlanExecutor.cs @@ -4,12 +4,5 @@ namespace Umbraco.Cms.Core.Migrations; public interface IMigrationPlanExecutor { - [Obsolete("Use ExecutePlan instead.")] - string Execute(MigrationPlan plan, string fromState); - - ExecutedMigrationPlan ExecutePlan(MigrationPlan plan, string fromState) - { - var state = Execute(plan, fromState); - return new ExecutedMigrationPlan(plan, fromState, state); - } + ExecutedMigrationPlan ExecutePlan(MigrationPlan plan, string fromState); } diff --git a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseBuilder.cs index 3abd2216ec..cd9266d45e 100644 --- a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseBuilder.cs @@ -12,6 +12,7 @@ using Umbraco.Cms.Core.Install.Models; using Umbraco.Cms.Core.Migrations; using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Infrastructure.Migrations.Notifications; using Umbraco.Cms.Infrastructure.Migrations.Upgrade; using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Infrastructure.Persistence.Dtos; @@ -38,6 +39,7 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Install private readonly IMigrationPlanExecutor _migrationPlanExecutor; private readonly DatabaseSchemaCreatorFactory _databaseSchemaCreatorFactory; private readonly IEnumerable _databaseProviderMetadata; + private readonly IEventAggregator _aggregator; private DatabaseSchemaResult? _databaseSchemaValidationResult; @@ -58,7 +60,7 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Install IMigrationPlanExecutor migrationPlanExecutor, DatabaseSchemaCreatorFactory databaseSchemaCreatorFactory, IEnumerable databaseProviderMetadata, - IEventAggregator eventAggregator) + IEventAggregator aggregator) { _scopeProvider = scopeProvider; _scopeAccessor = scopeAccessor; @@ -73,6 +75,7 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Install _migrationPlanExecutor = migrationPlanExecutor; _databaseSchemaCreatorFactory = databaseSchemaCreatorFactory; _databaseProviderMetadata = databaseProviderMetadata; + _aggregator = aggregator; } [Obsolete("Use constructor that takes IEventAggregator, this will be removed in V13.")] @@ -367,12 +370,17 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Install // upgrade var upgrader = new Upgrader(plan); - upgrader.Execute(_migrationPlanExecutor, _scopeProvider, _keyValueService); + ExecutedMigrationPlan result = upgrader.Execute(_migrationPlanExecutor, _scopeProvider, _keyValueService); + + _aggregator.Publish(new UmbracoPlanExecutedNotification { ExecutedPlan = result }); + + // The migration may have failed, it this is the case, we throw the exception now that we've taken care of business. + if (result.Successful is false && result.Exception is not null) + { + return HandleInstallException(result.Exception); + } var message = "

Upgrade completed!

"; - - //now that everything is done, we need to determine the version of SQL server that is executing - _logger.LogInformation("Database configuration status: {DbConfigStatus}", message); return new Result { Message = message, Success = true, Percentage = "100" }; diff --git a/src/Umbraco.Infrastructure/Migrations/MigrationBase.cs b/src/Umbraco.Infrastructure/Migrations/MigrationBase.cs index a4c4c0c99a..7b9bb1551c 100644 --- a/src/Umbraco.Infrastructure/Migrations/MigrationBase.cs +++ b/src/Umbraco.Infrastructure/Migrations/MigrationBase.cs @@ -85,6 +85,11 @@ public abstract partial class MigrationBase : IDiscoverable ///
public IUpdateBuilder Update => BeginBuild(new UpdateBuilder(Context)); + /// + /// If this is set to true, the published cache will be rebuild upon successful completion of the migration. + /// + public bool RebuildCache { get; set; } + /// /// Runs the migration. /// diff --git a/src/Umbraco.Infrastructure/Migrations/MigrationContext.cs b/src/Umbraco.Infrastructure/Migrations/MigrationContext.cs index f400a71420..4b1900c27c 100644 --- a/src/Umbraco.Infrastructure/Migrations/MigrationContext.cs +++ b/src/Umbraco.Infrastructure/Migrations/MigrationContext.cs @@ -8,8 +8,6 @@ namespace Umbraco.Cms.Infrastructure.Migrations; /// internal class MigrationContext : IMigrationContext { - private readonly List _postMigrations = new(); - /// /// Initializes a new instance of the class. /// @@ -18,13 +16,8 @@ internal class MigrationContext : IMigrationContext Plan = plan; Database = database ?? throw new ArgumentNullException(nameof(database)); Logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _postMigrations.AddRange(plan.PostMigrationTypes); } - // this is only internally exposed - [Obsolete("This will be removed in the V13, and replaced with a RebuildCache flag on the MigrationBase")] - public IReadOnlyList PostMigrations => _postMigrations; - /// public ILogger Logger { get; } @@ -41,12 +34,4 @@ internal class MigrationContext : IMigrationContext /// public bool BuildingExpression { get; set; } - - /// - [Obsolete("This will be removed in the V13, and replaced with a RebuildCache flag on the MigrationBase, and a UmbracoPlanExecutedNotification.")] - public void AddPostMigration() - where TMigration : MigrationBase => - - // just adding - will be de-duplicated when executing - _postMigrations.Add(typeof(TMigration)); } diff --git a/src/Umbraco.Infrastructure/Migrations/MigrationPlan.cs b/src/Umbraco.Infrastructure/Migrations/MigrationPlan.cs index 0ce07475d7..3be0a01a4f 100644 --- a/src/Umbraco.Infrastructure/Migrations/MigrationPlan.cs +++ b/src/Umbraco.Infrastructure/Migrations/MigrationPlan.cs @@ -7,7 +7,6 @@ namespace Umbraco.Cms.Infrastructure.Migrations; /// public class MigrationPlan { - private readonly List _postMigrationTypes = new(); private readonly Dictionary _transitions = new(StringComparer.InvariantCultureIgnoreCase); private string? _finalState; @@ -45,9 +44,6 @@ public class MigrationPlan /// public IReadOnlyDictionary Transitions => _transitions; - [Obsolete("This will be removed in the V13, and replaced with a RebuildCache flag on the MigrationBase")] - public IReadOnlyList PostMigrationTypes => _postMigrationTypes; - /// /// Gets the name of the plan. /// @@ -294,21 +290,6 @@ public class MigrationPlan return this; } - /// - /// Adds a post-migration to the plan. - /// - [Obsolete("This will be removed in the V13, and replaced with a RebuildCache flag on the MigrationBase")] - public virtual MigrationPlan AddPostMigration() - where TMigration : MigrationBase - { - // TODO: Post migrations are obsolete/irrelevant. Notifications should be used instead. - // The only place we use this is to clear cookies in the installer which could be done - // via notification. Then we can clean up all the code related to post migrations which is - // not insignificant. - _postMigrationTypes.Add(typeof(TMigration)); - return this; - } - /// /// Creates a random, unique state. /// diff --git a/src/Umbraco.Infrastructure/Migrations/MigrationPlanExecutor.cs b/src/Umbraco.Infrastructure/Migrations/MigrationPlanExecutor.cs index 69e69ee85b..8c7e1c2f9a 100644 --- a/src/Umbraco.Infrastructure/Migrations/MigrationPlanExecutor.cs +++ b/src/Umbraco.Infrastructure/Migrations/MigrationPlanExecutor.cs @@ -7,16 +7,42 @@ using Umbraco.Cms.Core.PublishedCache; using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Infrastructure.Scoping; +using Umbraco.Extensions; namespace Umbraco.Cms.Infrastructure.Migrations; +/* + * This is what runs our migration plans. + * It's important to note that this was altered to allow for partial migration completions. + * The need for this became apparent when we added support for SQLite. + * The main issue being how SQLites handles altering the schema. + * Long story short, SQLite doesn't support altering columns, + * or adding non-nullable columns with non-trivial types, for instance GUIDs. + * + * The recommended workaround for this is to add a new table, migrate the data, and delete the old one, + * however this causes issues with foreign keys. The recommended approach for this is to disable foreign keys entirely + * when doing the migration. However, foreign keys MUST be disabled outside a transaction. + * This was impossible with our previous migration system since it ALWAYS ran all migrations in a single transaction + * meaning that the transaction will always have been started when your migration is run, so you can't disable FKs. + * + * To now support this the UnscopedMigrationBase was added, which allows you to create a migration with no transaction. + * But this requires each migration to run in its own scope, + * meaning we can't roll back the entire plan, but only a single migration. + * + * Hence the need for partial migration completions. + */ + public class MigrationPlanExecutor : IMigrationPlanExecutor { private readonly ILogger _logger; private readonly ILoggerFactory _loggerFactory; private readonly IMigrationBuilder _migrationBuilder; + private readonly IUmbracoDatabaseFactory _databaseFactory; + private readonly IPublishedSnapshotService _publishedSnapshotService; + private readonly DistributedCache _distributedCache; private readonly IScopeAccessor _scopeAccessor; private readonly ICoreScopeProvider _scopeProvider; + private bool _rebuildCache; public MigrationPlanExecutor( ICoreScopeProvider scopeProvider, @@ -31,6 +57,9 @@ public class MigrationPlanExecutor : IMigrationPlanExecutor _scopeAccessor = scopeAccessor; _loggerFactory = loggerFactory; _migrationBuilder = migrationBuilder; + _databaseFactory = databaseFactory; + _publishedSnapshotService = publishedSnapshotService; + _distributedCache = distributedCache; _logger = _loggerFactory.CreateLogger(); } @@ -54,91 +83,152 @@ public class MigrationPlanExecutor : IMigrationPlanExecutor /// /// Executes the plan. /// - /// A scope. + /// The migration plan to be executes. /// The state to start execution at. - /// A migration builder. - /// A logger. - /// - /// The final state. - /// The plan executes within the scope, which must then be completed. + /// ExecutedMigrationPlan containing information about the plan execution, such as completion state and the steps that ran. + /// + /// Each migration in the plan, may or may not run in a scope depending on the type of plan. + /// A plan can complete partially, the changes of each completed migration will be saved. + /// [Obsolete("This will return an ExecutedMigrationPlan in V13")] - public string Execute(MigrationPlan plan, string fromState) + public ExecutedMigrationPlan ExecutePlan(MigrationPlan plan, string fromState) { plan.Validate(); - _logger.LogInformation("Starting '{MigrationName}'...", plan.Name); + ExecutedMigrationPlan result = RunMigrationPlan(plan, fromState); - fromState ??= string.Empty; + // If any completed migration requires us to rebuild cache we'll do that. + if (_rebuildCache) + { + RebuildCache(); + } + + return result; + } + + private ExecutedMigrationPlan RunMigrationPlan(MigrationPlan plan, string fromState) + { + _logger.LogInformation("Starting '{MigrationName}'...", plan.Name); var nextState = fromState; _logger.LogInformation("At {OrigState}", string.IsNullOrWhiteSpace(nextState) ? "origin" : nextState); - if (!plan.Transitions.TryGetValue(nextState, out MigrationPlan.Transition? transition)) + if (plan.Transitions.TryGetValue(nextState, out MigrationPlan.Transition? transition) is false) { plan.ThrowOnUnknownInitialState(nextState); } - using (ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true)) + List completedTransitions = new(); + + while (transition is not null) { - // We want to suppress scope (service, etc...) notifications during a migration plan - // execution. This is because if a package that doesn't have their migration plan - // executed is listening to service notifications to perform some persistence logic, - // that packages notification handlers may explode because that package isn't fully installed yet. - using (scope.Notifications.Suppress()) + _logger.LogInformation("Execute {MigrationType}", transition.MigrationType.Name); + + try { - var context = new MigrationContext(plan, _scopeAccessor.AmbientScope?.Database, - _loggerFactory.CreateLogger()); - - while (transition != null) + if (transition.MigrationType.IsAssignableTo(typeof(UnscopedMigrationBase))) { - _logger.LogInformation("Execute {MigrationType}", transition.MigrationType.Name); - - MigrationBase migration = _migrationBuilder.Build(transition.MigrationType, context); - migration.Run(); - - nextState = transition.TargetState; - - _logger.LogInformation("At {OrigState}", nextState); - - // throw a raw exception here: this should never happen as the plan has - // been validated - this is just a paranoid safety test - if (!plan.Transitions.TryGetValue(nextState, out transition)) - { - throw new InvalidOperationException($"Unknown state \"{nextState}\"."); - } + RunUnscopedMigration(transition.MigrationType, plan); } - - // prepare and de-duplicate post-migrations, only keeping the 1st occurence - var temp = new HashSet(); - IEnumerable postMigrationTypes = context.PostMigrations - .Where(x => !temp.Contains(x)) - .Select(x => - { - temp.Add(x); - return x; - }); - - // run post-migrations - foreach (Type postMigrationType in postMigrationTypes) + else { - _logger.LogInformation($"PostMigration: {postMigrationType.FullName}."); - MigrationBase postMigration = _migrationBuilder.Build(postMigrationType, context); - postMigration.Run(); + RunScopedMigration(transition.MigrationType, plan); } } + catch (Exception exception) + { + _logger.LogError("Plan failed at step {TargetState}", transition.TargetState); + // We have to always return something, so whatever running this has a chance to save the state we got to. + return new ExecutedMigrationPlan + { + Successful = false, + Exception = exception, + InitialState = fromState, + FinalState = transition.SourceState, + CompletedTransitions = completedTransitions, + Plan = plan, + }; + } + + // The plan migration (transition), completed, so we'll add this to our list so we can return this at some point. + completedTransitions.Add(transition); + nextState = transition.TargetState; + + _logger.LogInformation("At {OrigState}", nextState); + + // this should never happen as the plan has been validated - this is just a paranoid safety test + // If it does something is wrong and we'll fail the execution and return an error. + if (plan.Transitions.TryGetValue(nextState, out transition) is false) + { + return new ExecutedMigrationPlan + { + Successful = false, + Exception = new InvalidOperationException($"Unknown state \"{nextState}\"."), + InitialState = fromState, + // We were unable to get the next transition, and we never executed it, so final state is source state. + FinalState = completedTransitions.Last().TargetState, + CompletedTransitions = completedTransitions, + Plan = plan, + }; + } } - _logger.LogInformation("Done (pending scope completion)."); + _logger.LogInformation("Done"); - // safety check - again, this should never happen as the plan has been validated, - // and this is just a paranoid safety test - var finalState = plan.FinalState; - if (nextState != finalState) + return new ExecutedMigrationPlan { - throw new InvalidOperationException( - $"Internal error, reached state {nextState} which is not final state {finalState}"); - } + Successful = true, + InitialState = fromState, + FinalState = transition?.TargetState ?? completedTransitions.Last().TargetState, + CompletedTransitions = completedTransitions, + Plan = plan, + }; + } - return nextState; + private void RunUnscopedMigration(Type migrationType, MigrationPlan plan) + { + using IUmbracoDatabase database = _databaseFactory.CreateDatabase(); + var context = new MigrationContext(plan, database, _loggerFactory.CreateLogger()); + + RunMigration(migrationType, context); + } + + private void RunScopedMigration(Type migrationType, MigrationPlan plan) + { + // We want to suppress scope (service, etc...) notifications during a migration plan + // execution. This is because if a package that doesn't have their migration plan + // executed is listening to service notifications to perform some persistence logic, + // that packages notification handlers may explode because that package isn't fully installed yet. + using ICoreScope scope = _scopeProvider.CreateCoreScope(); + using (scope.Notifications.Suppress()) + { + var context = new MigrationContext( + plan, + _scopeAccessor.AmbientScope?.Database, + _loggerFactory.CreateLogger()); + + RunMigration(migrationType, context); + + scope.Complete(); + } + } + + private void RunMigration(Type migrationType, MigrationContext context) + { + MigrationBase migration = _migrationBuilder.Build(migrationType, context); + migration.Run(); + + // If the migration requires clearing the cache set the flag, this will automatically only happen if it succeeds + // Otherwise it'll error out before and return. + if (migration.RebuildCache) + { + _rebuildCache = true; + } + } + + private void RebuildCache() + { + _publishedSnapshotService.RebuildAll(); + _distributedCache.RefreshAllPublishedSnapshot(); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Notifications/MigrationPlansExecutedNotification.cs b/src/Umbraco.Infrastructure/Migrations/Notifications/MigrationPlansExecutedNotification.cs index 22c7e0710d..a227318405 100644 --- a/src/Umbraco.Infrastructure/Migrations/Notifications/MigrationPlansExecutedNotification.cs +++ b/src/Umbraco.Infrastructure/Migrations/Notifications/MigrationPlansExecutedNotification.cs @@ -3,8 +3,16 @@ using Umbraco.Cms.Core.Notifications; namespace Umbraco.Cms.Infrastructure.Migrations.Notifications; /// -/// Published when one or more migration plans have been successfully executed. +/// Published when one or more migration plans has been executed. /// +/// +/// +/// Each migration plan may or may not have succeeded, signaled by the Successful property. +/// +/// +/// A failed migration plan may have partially completed, in which case the successful transition are located in the CompletedTransitions collection. +/// +/// public class MigrationPlansExecutedNotification : INotification { public MigrationPlansExecutedNotification(IReadOnlyList executedPlans) diff --git a/src/Umbraco.Infrastructure/Migrations/Notifications/UmbracoPlanExecutedNotification.cs b/src/Umbraco.Infrastructure/Migrations/Notifications/UmbracoPlanExecutedNotification.cs new file mode 100644 index 0000000000..3410af134b --- /dev/null +++ b/src/Umbraco.Infrastructure/Migrations/Notifications/UmbracoPlanExecutedNotification.cs @@ -0,0 +1,19 @@ +using Umbraco.Cms.Core.Notifications; + +namespace Umbraco.Cms.Infrastructure.Migrations.Notifications; + +/// +/// Published the umbraco migration plan has been executed. +/// +/// +/// +/// The migration plan may or may not have succeeded, signaled by the Successful property. +/// +/// +/// A failed migration plan may have partially completed, in which case the successful transition are located in the CompletedTransitions collection. +/// +/// +public class UmbracoPlanExecutedNotification : INotification +{ + public required ExecutedMigrationPlan ExecutedPlan { get; init; } +} diff --git a/src/Umbraco.Infrastructure/Migrations/PostMigrations/ClearCsrfCookieHandler.cs b/src/Umbraco.Infrastructure/Migrations/PostMigrations/ClearCsrfCookieHandler.cs new file mode 100644 index 0000000000..fc796d66b8 --- /dev/null +++ b/src/Umbraco.Infrastructure/Migrations/PostMigrations/ClearCsrfCookieHandler.cs @@ -0,0 +1,28 @@ +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Core.Web; +using Umbraco.Cms.Infrastructure.Migrations.Notifications; + +namespace Umbraco.Cms.Infrastructure.Migrations.PostMigrations; + +public class ClearCsrfCookieHandler : INotificationHandler +{ + private readonly ICookieManager _cookieManager; + + public ClearCsrfCookieHandler(ICookieManager cookieManager) + { + _cookieManager = cookieManager; + } + + public void Handle(UmbracoPlanExecutedNotification notification) + { + // We'll only clear the cookie if the migration actually succeeded. + if (notification.ExecutedPlan.Successful is false) + { + return; + } + + _cookieManager.ExpireCookie(Constants.Web.AngularCookieName); + _cookieManager.ExpireCookie(Constants.Web.CsrfValidationCookieName); + } +} diff --git a/src/Umbraco.Infrastructure/Migrations/PostMigrations/ClearCsrfCookies.cs b/src/Umbraco.Infrastructure/Migrations/PostMigrations/ClearCsrfCookies.cs deleted file mode 100644 index a343635666..0000000000 --- a/src/Umbraco.Infrastructure/Migrations/PostMigrations/ClearCsrfCookies.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Umbraco.Cms.Core; -using Umbraco.Cms.Core.Web; - -namespace Umbraco.Cms.Infrastructure.Migrations.PostMigrations; - -/// -/// Clears Csrf tokens. -/// -[Obsolete("Removed in the V13, and replaced with a notification handler")] -public class ClearCsrfCookies : MigrationBase -{ - private readonly ICookieManager _cookieManager; - - public ClearCsrfCookies(IMigrationContext context, ICookieManager cookieManager) - : base(context) => _cookieManager = cookieManager; - - protected override void Migrate() - { - _cookieManager.ExpireCookie(Constants.Web.AngularCookieName); - _cookieManager.ExpireCookie(Constants.Web.CsrfValidationCookieName); - } -} diff --git a/src/Umbraco.Infrastructure/Migrations/PostMigrations/DeleteLogViewerQueryFile.cs b/src/Umbraco.Infrastructure/Migrations/PostMigrations/DeleteLogViewerQueryFile.cs deleted file mode 100644 index ac7260bd74..0000000000 --- a/src/Umbraco.Infrastructure/Migrations/PostMigrations/DeleteLogViewerQueryFile.cs +++ /dev/null @@ -1,32 +0,0 @@ -using Umbraco.Cms.Core.Hosting; - -// using Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_0_0; -namespace Umbraco.Cms.Infrastructure.Migrations.PostMigrations; - -/// -/// Deletes the old file that saved log queries -/// -[Obsolete("This will be removed in the V13")] -public class DeleteLogViewerQueryFile : MigrationBase -{ - private readonly IHostingEnvironment _hostingEnvironment; - - /// - /// Initializes a new instance of the class. - /// - public DeleteLogViewerQueryFile(IMigrationContext context, IHostingEnvironment hostingEnvironment) - : base(context) => - _hostingEnvironment = hostingEnvironment; - - /// - protected override void Migrate() - { - // var logViewerQueryFile = MigrateLogViewerQueriesFromFileToDb.GetLogViewerQueryFile(_hostingEnvironment); - // - // if(File.Exists(logViewerQueryFile)) - // { - // File.Delete(logViewerQueryFile); - // } - // }Rebuild - } -} diff --git a/src/Umbraco.Infrastructure/Migrations/PostMigrations/RebuildPublishedSnapshot.cs b/src/Umbraco.Infrastructure/Migrations/PostMigrations/RebuildPublishedSnapshot.cs deleted file mode 100644 index c0cc2c2993..0000000000 --- a/src/Umbraco.Infrastructure/Migrations/PostMigrations/RebuildPublishedSnapshot.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace Umbraco.Cms.Infrastructure.Migrations.PostMigrations; - -/// -/// Rebuilds the published snapshot. -/// -[Obsolete("This will be removed in the V13, and replaced with a RebuildCache flag on the MigrationBase")] -public class RebuildPublishedSnapshot : MigrationBase -{ - private readonly IPublishedSnapshotRebuilder _rebuilder; - - /// - /// Initializes a new instance of the class. - /// - public RebuildPublishedSnapshot(IMigrationContext context, IPublishedSnapshotRebuilder rebuilder) - : base(context) - => _rebuilder = rebuilder; - - /// - protected override void Migrate() => _rebuilder.Rebuild(); -} diff --git a/src/Umbraco.Infrastructure/Migrations/UnscopedMigrationBase.cs b/src/Umbraco.Infrastructure/Migrations/UnscopedMigrationBase.cs new file mode 100644 index 0000000000..79a7f2e8ac --- /dev/null +++ b/src/Umbraco.Infrastructure/Migrations/UnscopedMigrationBase.cs @@ -0,0 +1,14 @@ +namespace Umbraco.Cms.Infrastructure.Migrations; + +/// +/// Base class for creating a migration that does not have a scope provided for it +/// +/// +/// This is just a marker class, and has all the same functionality as the underlying MigrationBase +/// +public abstract class UnscopedMigrationBase : MigrationBase +{ + protected UnscopedMigrationBase(IMigrationContext context) : base(context) + { + } +} diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs index fe7568a680..9600bbee00 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs @@ -30,7 +30,10 @@ public class UmbracoPlan : MigrationPlan /// /// Defines the plan. /// - protected void DefinePlan() + /// + /// This is virtual for testing purposes. + /// + protected virtual void DefinePlan() { // Please take great care when modifying the plan! // diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/Upgrader.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/Upgrader.cs index e5b8c7d055..05ddb12e39 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/Upgrader.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/Upgrader.cs @@ -31,20 +31,15 @@ public class Upgrader public virtual string StateValueKey => Constants.Conventions.Migrations.KeyValuePrefix + Name; /// - /// Executes. - /// - /// A scope provider. - /// A key-value service. - [Obsolete("Please use the Execute method that accepts an Umbraco.Cms.Core.Scoping.ICoreScopeProvider instead.")] - public ExecutedMigrationPlan Execute(IMigrationPlanExecutor migrationPlanExecutor, IScopeProvider scopeProvider, IKeyValueService keyValueService) - => Execute(migrationPlanExecutor, (ICoreScopeProvider)scopeProvider, keyValueService); - - /// /// Executes. /// + /// /// A scope provider. /// A key-value service. - public ExecutedMigrationPlan Execute(IMigrationPlanExecutor migrationPlanExecutor, ICoreScopeProvider scopeProvider, + /// + public ExecutedMigrationPlan Execute( + IMigrationPlanExecutor migrationPlanExecutor, + ICoreScopeProvider scopeProvider, IKeyValueService keyValueService) { if (scopeProvider == null) @@ -57,38 +52,49 @@ public class Upgrader throw new ArgumentNullException(nameof(keyValueService)); } - using (ICoreScope scope = scopeProvider.CreateCoreScope()) + var initialState = GetInitialState(scopeProvider, keyValueService); + + ExecutedMigrationPlan result = migrationPlanExecutor.ExecutePlan(Plan, initialState); + + if (string.IsNullOrWhiteSpace(result.FinalState) || result.FinalState == result.InitialState) { - // read current state - var currentState = keyValueService.GetValue(StateValueKey); - var forceState = false; - - if (currentState == null || Plan.IgnoreCurrentState) - { - currentState = Plan.InitialState; - forceState = true; - } - - // execute plan - var state = migrationPlanExecutor.ExecutePlan(Plan, currentState).FinalState; - if (string.IsNullOrWhiteSpace(state)) + // This should never happen, if the final state comes back as null or equal to the initial state + // it means that no transitions was successful, which means it cannot be a successful migration + if (result.Successful) { throw new InvalidOperationException("Plan execution returned an invalid null or empty state."); } - // save new state - if (forceState) - { - keyValueService.SetValue(StateValueKey, state); - } - else if (currentState != state) - { - keyValueService.SetValue(StateValueKey, currentState, state); - } - - scope.Complete(); - - return new ExecutedMigrationPlan(Plan, currentState, state); + // Otherwise it just means that our migration failed on the first step, which is fine. + // We will skip saving the state since we it's still the same + return result; } + + // We always save the final state of the migration plan, this is because a partial success is possible + // So we still want to save the place we got to in the database- + SetState(result.FinalState, scopeProvider, keyValueService); + + return result; + } + + private string GetInitialState(ICoreScopeProvider scopeProvider, IKeyValueService keyValueService) + { + using ICoreScope scope = scopeProvider.CreateCoreScope(); + var currentState = keyValueService.GetValue(StateValueKey); + scope.Complete(); + + if (currentState is null || Plan.IgnoreCurrentState) + { + return Plan.InitialState; + } + + return currentState; + } + + private void SetState(string state, ICoreScopeProvider scopeProvider, IKeyValueService keyValueService) + { + using ICoreScope scope = scopeProvider.CreateCoreScope(); + keyValueService.SetValue(StateValueKey, state); + scope.Complete(); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DropDownPropertyEditorsMigration.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DropDownPropertyEditorsMigration.cs index 5f4c024b42..43ee3e9953 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DropDownPropertyEditorsMigration.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DropDownPropertyEditorsMigration.cs @@ -45,10 +45,7 @@ public class DropDownPropertyEditorsMigration : PropertyEditorsMigrationBase // if some data types have been updated directly in the database (editing DataTypeDto and/or PropertyDataDto), // bypassing the services, then we need to rebuild the cache entirely, including the umbracoContentNu table - if (refreshCache) - { - Context.AddPostMigration(); - } + // This has been removed since this migration should be deleted. } private bool Migrate(IEnumerable dataTypes) diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/RadioAndCheckboxPropertyEditorsMigration.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/RadioAndCheckboxPropertyEditorsMigration.cs index fbb0b49bb0..bbfa10f432 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/RadioAndCheckboxPropertyEditorsMigration.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/RadioAndCheckboxPropertyEditorsMigration.cs @@ -50,10 +50,7 @@ public class RadioAndCheckboxPropertyEditorsMigration : PropertyEditorsMigration // if some data types have been updated directly in the database (editing DataTypeDto and/or PropertyDataDto), // bypassing the services, then we need to rebuild the cache entirely, including the umbracoContentNu table - if (refreshCache) - { - Context.AddPostMigration(); - } + // This has been removed since this migration should be deleted. } private bool Migrate(IEnumerable dataTypes, bool isMultiple) diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_1/ChangeNuCacheJsonFormat.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_1/ChangeNuCacheJsonFormat.cs index 83d97d9331..b9dcdfa395 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_1/ChangeNuCacheJsonFormat.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_1/ChangeNuCacheJsonFormat.cs @@ -1,3 +1,4 @@ +using HtmlAgilityPack; using Umbraco.Cms.Infrastructure.Migrations.PostMigrations; namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_1; @@ -10,8 +11,8 @@ public class ChangeNuCacheJsonFormat : MigrationBase { } - protected override void Migrate() => - - // nothing - just adding the post-migration - Context.AddPostMigration(); + protected override void Migrate() + { + // This has been removed since post migrations are no longer a thing, and this migration should be deleted. + } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/MigrateLogViewerQueriesFromFileToDb.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/MigrateLogViewerQueriesFromFileToDb.cs index b9e6123c15..68b34ae8fb 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/MigrateLogViewerQueriesFromFileToDb.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/MigrateLogViewerQueriesFromFileToDb.cs @@ -101,7 +101,5 @@ public class MigrateLogViewerQueriesFromFileToDb : MigrationBase } Database.InsertBulk(logQueriesInFile!); - - Context.AddPostMigration(); } } diff --git a/src/Umbraco.New.Cms.Infrastructure/Installer/Steps/DatabaseUpgradeStep.cs b/src/Umbraco.New.Cms.Infrastructure/Installer/Steps/DatabaseUpgradeStep.cs index 83cae8d80b..d0763f58dd 100644 --- a/src/Umbraco.New.Cms.Infrastructure/Installer/Steps/DatabaseUpgradeStep.cs +++ b/src/Umbraco.New.Cms.Infrastructure/Installer/Steps/DatabaseUpgradeStep.cs @@ -42,7 +42,7 @@ public class DatabaseUpgradeStep : IInstallStep, IUpgradeStep _logger.LogInformation("Running 'Upgrade' service"); var plan = new UmbracoPlan(_umbracoVersion); - plan.AddPostMigration(); // needed when running installer (back-office) + // TODO: Clear CSRF cookies with notification. DatabaseBuilder.Result? result = _databaseBuilder.UpgradeSchemaAndData(plan); diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Migrations/PartialMigrationsTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Migrations/PartialMigrationsTests.cs new file mode 100644 index 0000000000..6fadae55d0 --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Migrations/PartialMigrationsTests.cs @@ -0,0 +1,310 @@ +using Microsoft.Extensions.DependencyInjection; +using NPoco; +using NUnit.Framework; +using Umbraco.Cms.Core.Configuration; +using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Core.Exceptions; +using Umbraco.Cms.Core.Migrations; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Infrastructure.Migrations; +using Umbraco.Cms.Infrastructure.Migrations.Install; +using Umbraco.Cms.Infrastructure.Migrations.Notifications; +using Umbraco.Cms.Infrastructure.Migrations.Upgrade; +using Umbraco.Cms.Infrastructure.Persistence; +using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; +using Umbraco.Cms.Infrastructure.Scoping; +using Umbraco.Cms.Tests.Common.Testing; +using Umbraco.Cms.Tests.Integration.Testing; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Migrations; + +// These tests depend on the key-value table, so we need a schema to run these tests. +[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest)] +[TestFixture] +public class PartialMigrationsTests : UmbracoIntegrationTest +{ + public const string TableName = "testTable"; + public const string ColumnName = "testColumn"; + + private IMigrationPlanExecutor MigrationPlanExecutor => GetRequiredService(); + + private IKeyValueService KeyValueService => GetRequiredService(); + + [TearDown] + public void ResetMigration() + { + ErrorMigration.ShouldExplode = true; + UmbracoPlanExecutedTestNotificationHandler.HandleNotification = null; + } + + protected override void ConfigureTestServices(IServiceCollection services) + => services.AddNotificationHandler(); + + [Test] + public void CanRerunPartiallyCompletedMigration() + { + var plan = new MigrationPlan("test") + .From(string.Empty) + .To("a") + .To("b") + .To("c"); + + var upgrader = new Upgrader(plan); + + var result = upgrader.Execute(MigrationPlanExecutor, ScopeProvider, KeyValueService); + + Assert.Multiple(() => + { + Assert.IsFalse(result.Successful); + Assert.AreEqual(string.Empty, result.InitialState); + Assert.AreEqual("a", result.FinalState); + Assert.AreEqual(1, result.CompletedTransitions.Count); + Assert.IsNotNull(result.Exception); + + // Ensure that the partial success is saved in the keyvalue service so next plan execution starts correctly. + using var scope = ScopeProvider.CreateScope(autoComplete: true); + Assert.AreEqual("a", KeyValueService.GetValue(upgrader.StateValueKey)); + // Ensure that the changes from the first migration is persisted + Assert.IsTrue(scope.Database.HasTable(TableName)); + // But that the final migration wasn't run + Assert.IsFalse(ColumnExists(TableName, ColumnName, scope)); + }); + + // Now let's simulate that someone came along and fixed the broken migration and we'll now try and rerun + ErrorMigration.ShouldExplode = false; + upgrader = new Upgrader(plan); + result = upgrader.Execute(MigrationPlanExecutor, ScopeProvider, KeyValueService); + + Assert.Multiple(() => + { + Assert.AreEqual("a", result.InitialState); + Assert.IsTrue(result.Successful); + Assert.IsNull(result.Exception); + Assert.AreEqual(2, result.CompletedTransitions.Count); + Assert.AreEqual("c", result.FinalState); + + // Ensure that everything got updated in the database. + using var scope = ScopeProvider.CreateScope(autoComplete: true); + Assert.AreEqual("c", KeyValueService.GetValue(upgrader.StateValueKey)); + Assert.IsTrue(scope.Database.HasTable(TableName)); + Assert.IsTrue(ColumnExists(TableName, ColumnName, scope)); + }); + } + + [Test] + public void StateIsOnlySavedIfAMigrationSucceeds() + { + var plan = new MigrationPlan("test") + .From(string.Empty) + .To("a") + .To("b"); + + var upgrader = new Upgrader(plan); + var result = upgrader.Execute(MigrationPlanExecutor, ScopeProvider, KeyValueService); + + Assert.Multiple(() => + { + Assert.IsFalse(result.Successful); + Assert.IsNotNull(result.Exception); + Assert.IsInstanceOf(result.Exception); + Assert.IsEmpty(result.CompletedTransitions); + Assert.AreEqual(string.Empty, result.InitialState); + Assert.AreEqual(string.Empty, result.FinalState); + + using var scope = ScopeProvider.CreateCoreScope(); + Assert.IsNull(KeyValueService.GetValue(upgrader.StateValueKey)); + }); + } + + [Test] + public void ScopesAreCreatedIfNecessary() + { + // The migrations have assert to esnure scopes + var plan = new MigrationPlan("test") + .From(string.Empty) + .To("a") + .To("b"); + + var upgrader = new Upgrader(plan); + var result = upgrader.Execute(MigrationPlanExecutor, ScopeProvider, KeyValueService); + + Assert.IsTrue(result.Successful); + Assert.AreEqual(2, result.CompletedTransitions.Count); + Assert.AreEqual("b", result.FinalState); + } + + [Test] + [TestCase(true)] + [TestCase(false)] + public void UmbracoPlanExecutedNotificationIsAlwaysPublished(bool shouldSucceed) + { + var notificationPublished = false; + ErrorMigration.ShouldExplode = shouldSucceed is false; + + UmbracoPlanExecutedTestNotificationHandler.HandleNotification += notification => + { + notificationPublished = true; + Assert.Multiple(() => + { + var executedPlan = notification.ExecutedPlan; + + if (shouldSucceed) + { + Assert.IsTrue(executedPlan.Successful); + Assert.IsNull(executedPlan.Exception); + Assert.AreEqual("c", executedPlan.FinalState); + Assert.AreEqual(3, executedPlan.CompletedTransitions.Count); + } + else + { + Assert.IsFalse(executedPlan.Successful); + Assert.IsNotNull(executedPlan.Exception); + Assert.IsInstanceOf(executedPlan.Exception); + Assert.AreEqual("a", executedPlan.FinalState); + Assert.AreEqual(1, executedPlan.CompletedTransitions.Count); + } + }); + }; + + // We have to use the DatabaseBuilder otherwise the notification isn't published + var databaseBuilder = GetRequiredService(); + var plan = new TestUmbracoPlan(null!); + databaseBuilder.UpgradeSchemaAndData(plan); + + Assert.IsTrue(notificationPublished); + } + + private bool ColumnExists(string tableName, string columnName, IScope scope) => + scope.Database.SqlContext.SqlSyntax.GetColumnsInSchema(scope.Database) + .Any(x => x.TableName.Equals(tableName) && x.ColumnName.Equals(columnName)); +} + + +// This is just some basic migrations to test the migration plans... +internal class ErrorMigration : MigrationBase +{ + // Used to determine if an exception should be thrown, used to test re-running migrations + public static bool ShouldExplode { get; set; } = true; + + public ErrorMigration(IMigrationContext context) : base(context) + { + } + + protected override void Migrate() + { + if (ShouldExplode) + { + throw new PanicException(); + } + } +} + +internal class CreateTableMigration : MigrationBase +{ + public CreateTableMigration(IMigrationContext context) : base(context) + { + } + + protected override void Migrate() => Create.Table().Do(); +} + +internal class AddColumnMigration : MigrationBase +{ + public AddColumnMigration(IMigrationContext context) : base(context) + { + } + + protected override void Migrate() => Create + .Column(PartialMigrationsTests.ColumnName) + .OnTable(PartialMigrationsTests.TableName) + .AsString() + .Do(); +} + +internal class AssertScopeUnscopedTestMigration : UnscopedMigrationBase +{ + private readonly IScopeProvider _scopeProvider; + private readonly IScopeAccessor _scopeAccessor; + + public AssertScopeUnscopedTestMigration( + IMigrationContext context, + IScopeProvider scopeProvider, + IScopeAccessor scopeAccessor) : base(context) + { + _scopeProvider = scopeProvider; + _scopeAccessor = scopeAccessor; + } + + protected override void Migrate() + { + // Since this is a scopeless migration both ambient scope and the parent scope should be null + Assert.IsNull(_scopeAccessor.AmbientScope); + + using var scope = _scopeProvider.CreateScope(); + Assert.IsNull(((Scope)scope).ParentScope); + } +} + +internal class AsserScopeScopedTestMigration : MigrationBase +{ + private readonly IScopeProvider _scopeProvider; + private readonly IScopeAccessor _scopeAccessor; + + public AsserScopeScopedTestMigration( + IMigrationContext context, + IScopeProvider scopeProvider, + IScopeAccessor scopeAccessor) : base(context) + { + _scopeProvider = scopeProvider; + _scopeAccessor = scopeAccessor; + } + + protected override void Migrate() + { + Assert.IsNotNull(_scopeAccessor.AmbientScope); + + using var scope = _scopeProvider.CreateScope(); + + Assert.IsNotNull(((Scope)scope).ParentScope); + } +} + +[TableName(PartialMigrationsTests.TableName)] +[PrimaryKey("id", AutoIncrement = true)] +internal class TestDto +{ + [Column("id")] + [PrimaryKeyColumn(Name = "PK_testTable")] + public int Id { get; set; } +} + +internal class UmbracoPlanExecutedTestNotificationHandler : INotificationHandler +{ + public static Action? HandleNotification { get; set; } + + public void Handle(UmbracoPlanExecutedNotification notification) => HandleNotification?.Invoke(notification); +} + + +/// +/// This is a fake UmbracoPlan used for testing of the DatabaseBuilder, this overrides everything to be of type +/// UmbracoPlan but behave like a normal migration plan. +/// +internal class TestUmbracoPlan : UmbracoPlan +{ + public TestUmbracoPlan(IUmbracoVersion umbracoVersion) : base(umbracoVersion) + { + } + + public override string InitialState => string.Empty; + + public override bool IgnoreCurrentState => true; + + protected override void DefinePlan() + { + From(InitialState); + To("a"); + To("b"); + To("c"); + } +} \ No newline at end of file diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Migrations/MigrationPlanTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Migrations/MigrationPlanTests.cs index 56c85e4157..59d6539b4f 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Migrations/MigrationPlanTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Migrations/MigrationPlanTests.cs @@ -40,6 +40,11 @@ public class MigrationPlanTests .Setup(x => x.Database) .Returns(database); + var databaseFactory = Mock.Of(); + Mock.Get(databaseFactory) + .Setup(x => x.CreateDatabase()) + .Returns(database); + var sqlContext = new SqlContext( new SqlServerSyntaxProvider(Options.Create(new GlobalSettings())), DatabaseType.SQLCe, @@ -62,17 +67,11 @@ public class MigrationPlanTests } }); - var databaseFactory = Mock.Of(); - var publishedSnapshotService = Mock.Of(); - var distributedCache = new DistributedCache(Mock.Of(), new CacheRefresherCollection(Enumerable.Empty)); - var executor = new MigrationPlanExecutor( - scopeProvider, - scopeProvider, - loggerFactory, - migrationBuilder, - databaseFactory, - publishedSnapshotService, - distributedCache); + var distributedCache = new DistributedCache( + Mock.Of(), + new CacheRefresherCollection(() => Enumerable.Empty())); + + var executor = new MigrationPlanExecutor(scopeProvider, scopeProvider, loggerFactory, migrationBuilder, databaseFactory, Mock.Of(), distributedCache); var plan = new MigrationPlan("default") .From(string.Empty) @@ -90,7 +89,8 @@ public class MigrationPlanTests var sourceState = kvs.GetValue("Umbraco.Tests.MigrationPlan") ?? string.Empty; // execute plan - state = executor.Execute(plan, sourceState); + var result = executor.ExecutePlan(plan, sourceState); + state = result.FinalState; // save new state kvs.SetValue("Umbraco.Tests.MigrationPlan", sourceState, state); diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Migrations/PostMigrationTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Migrations/PostMigrationTests.cs deleted file mode 100644 index 69fc050d31..0000000000 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Migrations/PostMigrationTests.cs +++ /dev/null @@ -1,172 +0,0 @@ -// Copyright (c) Umbraco. -// See LICENSE for more details. - -using System; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.Extensions.Options; -using Moq; -using NPoco; -using NUnit.Framework; -using Umbraco.Cms.Core.Cache; -using Umbraco.Cms.Core.Configuration.Models; -using Umbraco.Cms.Core.Events; -using Umbraco.Cms.Core.Migrations; -using Umbraco.Cms.Core.PublishedCache; -using Umbraco.Cms.Core.Scoping; -using Umbraco.Cms.Core.Services; -using Umbraco.Cms.Core.Sync; -using Umbraco.Cms.Infrastructure.Migrations; -using Umbraco.Cms.Infrastructure.Migrations.Upgrade; -using Umbraco.Cms.Infrastructure.Persistence; -using Umbraco.Cms.Infrastructure.Scoping; -using Umbraco.Cms.Persistence.SqlServer.Services; -using Umbraco.Cms.Tests.Common.TestHelpers; -using IScope = Umbraco.Cms.Infrastructure.Scoping.IScope; - -namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Migrations; - -[TestFixture] -public class PostMigrationTests -{ - private static readonly ILoggerFactory s_loggerFactory = NullLoggerFactory.Instance; - - private IMigrationPlanExecutor GetMigrationPlanExecutor( - ICoreScopeProvider scopeProvider, - IScopeAccessor scopeAccessor, - IMigrationBuilder builder) - { - var databaseFactory = Mock.Of(); - var publishedSnapshotService = Mock.Of(); - var distributedCache = new DistributedCache(Mock.Of(), new CacheRefresherCollection(Enumerable.Empty)); - return new MigrationPlanExecutor(scopeProvider, scopeAccessor, s_loggerFactory, builder, databaseFactory, publishedSnapshotService, distributedCache); - } - - [Test] - public void ExecutesPlanPostMigration() - { - var builder = Mock.Of(); - Mock.Get(builder) - .Setup(x => x.Build(It.IsAny(), It.IsAny())) - .Returns((t, c) => - { - switch (t.Name) - { - case nameof(NoopMigration): - return new NoopMigration(c); - case nameof(TestPostMigration): - return new TestPostMigration(c); - default: - throw new NotSupportedException(); - } - }); - - var database = new TestDatabase(); - var scope = Mock.Of(x => x.Notifications == Mock.Of()); - Mock.Get(scope) - .Setup(x => x.Database) - .Returns(database); - - var sqlContext = new SqlContext( - new SqlServerSyntaxProvider(Options.Create(new GlobalSettings())), - DatabaseType.SQLCe, - Mock.Of()); - var scopeProvider = new MigrationTests.TestScopeProvider(scope) { SqlContext = sqlContext }; - - var plan = new MigrationPlan("Test") - .From(string.Empty).To("done"); - - plan.AddPostMigration(); - TestPostMigration.MigrateCount = 0; - - var upgrader = new Upgrader(plan); - var executor = GetMigrationPlanExecutor(scopeProvider, scopeProvider, builder); - upgrader.Execute( - executor, - scopeProvider, - Mock.Of()); - - Assert.AreEqual(1, TestPostMigration.MigrateCount); - } - - [Test] - public void MigrationCanAddPostMigration() - { - var builder = Mock.Of(); - Mock.Get(builder) - .Setup(x => x.Build(It.IsAny(), It.IsAny())) - .Returns((t, c) => - { - switch (t.Name) - { - case nameof(NoopMigration): - return new NoopMigration(c); - case nameof(TestMigration): - return new TestMigration(c); - case nameof(TestPostMigration): - return new TestPostMigration(c); - default: - throw new NotSupportedException(); - } - }); - - var database = new TestDatabase(); - var scope = Mock.Of(x => x.Notifications == Mock.Of()); - Mock.Get(scope) - .Setup(x => x.Database) - .Returns(database); - - var sqlContext = new SqlContext( - new SqlServerSyntaxProvider(Options.Create(new GlobalSettings())), - DatabaseType.SQLCe, - Mock.Of()); - var scopeProvider = new MigrationTests.TestScopeProvider(scope) { SqlContext = sqlContext }; - - var plan = new MigrationPlan("Test") - .From(string.Empty).To("done"); - - TestMigration.MigrateCount = 0; - TestPostMigration.MigrateCount = 0; - - new MigrationContext(plan, database, s_loggerFactory.CreateLogger()); - - var upgrader = new Upgrader(plan); - var executor = GetMigrationPlanExecutor(scopeProvider, scopeProvider, builder); - upgrader.Execute( - executor, - scopeProvider, - Mock.Of()); - - Assert.AreEqual(1, TestMigration.MigrateCount); - Assert.AreEqual(1, TestPostMigration.MigrateCount); - } - - public class TestMigration : MigrationBase - { - public TestMigration(IMigrationContext context) - : base(context) - { - } - - public static int MigrateCount { get; set; } - - protected override void Migrate() - { - MigrateCount++; - - Context.AddPostMigration(); - } - } - - public class TestPostMigration : MigrationBase - { - public TestPostMigration(IMigrationContext context) - : base(context) - { - } - - public static int MigrateCount { get; set; } - - protected override void Migrate() => MigrateCount++; - } -}