using System; using System.Data.SqlClient; using System.IO; using System.IO.Compression; using System.Runtime.ExceptionServices; using Centron.BusinessLogic; using Centron.BusinessLogic.Administration.Scripts; using Centron.BusinessLogic.Sales.Receipts; using Centron.Common; using Centron.Core.Extensions; using Centron.Core.Utils; using Centron.DAO; using Centron.DAO.DAOConnections; using Centron.Interfaces.BL; using Xunit; namespace Centron.Tests.EndToEnd.Infrastructure { public class Database { #region Database Server public static string Server { get { var server = GetEnvironmentVariable("CENTRON_TESTS_DATABASE_SERVER"); if (string.IsNullOrWhiteSpace(server) == false) return server; throw new Exception("You have to configure the SQL server for EndToEndTests"); // return "YOUR-MACHINE\\SQL2017"; //Enter your local SQL server here } } public static string Username { get { var username = GetEnvironmentVariable("CENTRON_TESTS_DATABASE_USERNAME"); if (string.IsNullOrWhiteSpace(username) == false) return username; throw new Exception("You have to configure the SQL server username for EndToEndTests"); // return "sql-username"; //Enter your local SQL username here } } public static string Password { get { var password = GetEnvironmentVariable("CENTRON_TESTS_DATABASE_PASSWORD"); if (string.IsNullOrWhiteSpace(password) == false) return password; throw new Exception("You have to configure the SQL server password for EndToEndTests"); // return "password"; //Enter your local SQL password here } } #endregion private static object _lock = new object(); private static Exception _initializeException; private static string _databaseName; private static string _snapshotName; public static IDisposable Initialize() { lock (_lock) { if (string.IsNullOrWhiteSpace(_databaseName) == false || string.IsNullOrWhiteSpace(_snapshotName) == false) throw new Exception("Database.Initialize can only be called once."); // If there was an error when initializing the Database before // Rethrow that exception here, so we don't waste a lot of time restoring the database again if (_initializeException is not null) ExceptionDispatchInfo.Throw(_initializeException); // This keeps the original exception stack-trace // Start database initialization in the beginning to let it run parallel to database restore DAOFactory.Instance.InitializeAsync(); string databaseName = GetDatabaseName(); string snapshotName = GetSnapshotName(databaseName); RestoreDatabase(databaseName); try { Connect(databaseName); UpdateDatabase(); } catch (Exception e) { try { // If something fails when connecting to the database, or executing scripts // we want to Drop the database again. // The test will not be able to run, but we were leaving old database backups behind. // So, lets clean up after ourselves and Drop the database if something goes wrong. DropDatabase(databaseName); } catch { // Dropping the database failed. Unlucky, but not much we can do. } _initializeException = e; throw; } try { CreateSnapshot(snapshotName, databaseName); } catch (Exception e) { // The test will not be able to run, but we were leaving old database backups behind. // So, lets clean up after ourselves and Drop the database if something goes wrong. try { DropSnapshot(snapshotName); } catch { //Dropping the snapshot failed. Unlucky, but not much we can do. } try { DropDatabase(databaseName); } catch { //Dropping the database failed. Unlucky, but not much we can do. } _initializeException = e; throw; } _databaseName = databaseName; _snapshotName = snapshotName; return new DisposableAction(() => { DropSnapshot(snapshotName); DropDatabase(databaseName); }); } } public static void RestoreFromSnapshot() { RestoreFromSnapshot(_databaseName, _snapshotName); ReceiptItemSpecialArticleHelperBL.ClearSpecialArticlesCache(); } #region Names private static string GetDatabaseName() { var user = Environment.UserName; var time = DateTime.Now.ToString("dd-MM-yyyy-HH:mm:ss"); var guid = Guid.NewGuid().ToString("N"); return $"EndToEndTests-{user}-{time}-{guid}".Truncate(120); // The MSSQL server database name max length is 128 characters, just make sure to not go too long } private static string GetSnapshotName(string databaseName) { return databaseName + "-snapshot"; } #endregion #region Snapshots private static void CreateSnapshot(string snapshotName, string databaseName) { ExecuteSql(command => { command.CommandText = GetCreateSnapshotSQL(snapshotName, databaseName); command.ExecuteNonQuery(); }); } private static string GetCreateSnapshotSQL(string snapshotName, string databaseName) { string dataPath = null; ExecuteSql(command => { command.CommandText = "SELECT serverproperty('InstanceDefaultDataPath')"; using (var reader = command.ExecuteReader()) { while (reader.Read()) { dataPath = reader.GetString(0); } } }); return $@"CREATE DATABASE [{snapshotName}] ON ( NAME = CentronWork_Data, FILENAME = N'{dataPath}{databaseName.Replace("-", "").Replace(":", "")}.ss' ) AS SNAPSHOT OF [{databaseName}]"; } private static void DropSnapshot(string snapshotName) { ExecuteSql(command => { command.CommandText = GetDropSnapshotSQL(snapshotName); command.ExecuteNonQuery(); }); } private static string GetDropSnapshotSQL(string snapshotName) { return $"DROP DATABASE [{snapshotName}]"; } private static void RestoreFromSnapshot(string databaseName, string snapshotName) { ExecuteSql(command => { command.CommandText = GetRestoreFromSnapshotSQL(databaseName, snapshotName); command.ExecuteNonQuery(); }); } private static string GetRestoreFromSnapshotSQL(string databaseName, string snapshotName) { return $@" ALTER DATABASE [{databaseName}] SET SINGLE_USER WITH ROLLBACK IMMEDIATE; RESTORE DATABASE [{databaseName}] FROM DATABASE_SNAPSHOT = '{snapshotName}'; ALTER DATABASE [{databaseName}] SET MULTI_USER WITH ROLLBACK IMMEDIATE; "; } #endregion #region Database private static void RestoreDatabase(string databaseName) { ExecuteSql(command => { command.CommandText = GetRestoreDatabaseSQL(databaseName); command.CommandTimeout = (int)TimeSpan.FromMinutes(2).TotalSeconds; command.ExecuteNonQuery(); }); } private static string GetRestoreDatabaseSQL(string databaseName) { var backupPath = GetEnvironmentVariable("DATABASE_BACKUP_PATH"); if (string.IsNullOrEmpty(backupPath)) backupPath = Path.GetFullPath(".\\Infrastructure\\DatabaseBackup.bak"); string dataPath = null; string logPath = null; ExecuteSql(command => { command.CommandText = "SELECT serverproperty('InstanceDefaultDataPath'), serverproperty('InstanceDefaultLogPath')"; using (var reader = command.ExecuteReader()) { while (reader.Read()) { dataPath = reader.GetString(0); logPath = reader.GetString(1); } } }); return $@"RESTORE DATABASE [{databaseName}] FROM DISK = N'{backupPath}' WITH REPLACE, MOVE N'CentronWork_Data' TO N'{dataPath}{databaseName.Replace("-", "").Replace(":", "")}.mdf', MOVE N'CentronWork_Log' TO N'{logPath}{databaseName.Replace("-", "").Replace(":", "")}.ldf'"; } private static void DropDatabase(string databaseName) { ExecuteSql(command => { command.CommandText = GetDropDatabaseSQL(databaseName); command.ExecuteNonQuery(); }); } private static string GetDropDatabaseSQL(string databaseName) { return $"ALTER DATABASE [{databaseName}] SET SINGLE_USER WITH ROLLBACK IMMEDIATE; DROP DATABASE [{databaseName}]"; } #endregion #region Private Methods private static void Connect(string databaseName) { DAOFactory.Instance.SetConnection(new InCodeDAOConnection(Server, databaseName, Username, Password), "c-entron Tests"); DAOFactory.Instance.InitializeAsync().Wait(); Assert.True(DAOFactory.Instance.IsInitialized, "DAOFactory.Instance.IsInitialized"); Assert.True(DAOFactory.Instance.HasConnection, "DAOFactory.Instance.HasConnection"); } private static void UpdateDatabase() { using (var session = new BLSession()) { var scriptEngineBL = session.GetBL(); var version = new Version(20, 0, 0, 0); var result = scriptEngineBL.ExecuteScripts(currentVersionOverride: version); Assert.Null(result.Message); Assert.Equal(ResultStatus.Success, result.Status); } } private static void ExecuteSql(Action execute) { var builder = new SqlConnectionStringBuilder { DataSource = Server, UserID = Username, Password = Password, InitialCatalog = "master" }; using (var connection = new SqlConnection(builder.ToString())) { connection.Open(); using (var command = connection.CreateCommand()) { execute(command); } } } private static string GetEnvironmentVariable(string name) { var targets = new[] { EnvironmentVariableTarget.Process, EnvironmentVariableTarget.User, EnvironmentVariableTarget.Machine }; foreach (var target in targets) { var value = Environment.GetEnvironmentVariable(name, target); if (string.IsNullOrWhiteSpace(value) == false) return value; } return null; } #endregion } }