From 0edd56351b66df95c7fdb19a5d0b6232a29d72b8 Mon Sep 17 00:00:00 2001 From: Gilles TOURREAU Date: Mon, 22 Sep 2025 10:06:12 +0200 Subject: [PATCH 01/61] Updates the CreateEmptyDatabase() to specify the location of the database to create. --- .../SqlDatabaseCreationSettings.cs | 23 +++ src/Testing.Databases.SqlServer/SqlServer.cs | 26 ++- .../SqlDatabaseCreationSettingsTest.cs | 29 ++++ .../SqlServerTest.cs | 151 +++++++++++++++++- 4 files changed, 221 insertions(+), 8 deletions(-) create mode 100644 src/Testing.Databases.SqlServer/SqlDatabaseCreationSettings.cs create mode 100644 tests/Testing.Databases.SqlServer.Tests/SqlDatabaseCreationSettingsTest.cs diff --git a/src/Testing.Databases.SqlServer/SqlDatabaseCreationSettings.cs b/src/Testing.Databases.SqlServer/SqlDatabaseCreationSettings.cs new file mode 100644 index 0000000..8d90eef --- /dev/null +++ b/src/Testing.Databases.SqlServer/SqlDatabaseCreationSettings.cs @@ -0,0 +1,23 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) P.O.S Informatique. All rights reserved. +// +//----------------------------------------------------------------------- + +namespace PosInformatique.Testing.Databases.SqlServer +{ + /// + /// Settings of the to create using the + /// + /// or + /// + /// methods. + /// + public class SqlDatabaseCreationSettings + { + /// + /// Gets or sets the data file name (full path) of the database to create. + /// + public string? DataFileName { get; set; } + } +} diff --git a/src/Testing.Databases.SqlServer/SqlServer.cs b/src/Testing.Databases.SqlServer/SqlServer.cs index c848b12..145e461 100644 --- a/src/Testing.Databases.SqlServer/SqlServer.cs +++ b/src/Testing.Databases.SqlServer/SqlServer.cs @@ -6,6 +6,7 @@ namespace PosInformatique.Testing.Databases.SqlServer { + using System.Text; using Microsoft.Data.SqlClient; /// @@ -42,11 +43,12 @@ public SqlServer(string connectionString) /// If the database already exists, it will be delete. /// /// Name of the database to create. + /// Settings of the database to create. /// An instance of which allows to execute SQL commands/queries. - public SqlServerDatabase CreateEmptyDatabase(string name) + public SqlServerDatabase CreateEmptyDatabase(string name, SqlDatabaseCreationSettings? settings = null) { this.DeleteDatabase(name); - this.Master.ExecuteNonQuery($"CREATE DATABASE [{name}]"); + this.Master.ExecuteNonQuery(BuildCreateDatabaseSqlCommand(name, settings)); return this.GetDatabase(name); } @@ -56,12 +58,13 @@ public SqlServerDatabase CreateEmptyDatabase(string name) /// If the database already exists, it will be delete. /// /// Name of the database to create. + /// Settings of the database to create. /// used to cancel the asynchronous operation. /// A which represents the asynchronous operation and contains an instance of which allows to execute SQL commands/queries. - public async Task CreateEmptyDatabaseAsync(string name, CancellationToken cancellationToken = default) + public async Task CreateEmptyDatabaseAsync(string name, SqlDatabaseCreationSettings? settings = null, CancellationToken cancellationToken = default) { await this.DeleteDatabaseAsync(name, cancellationToken); - await this.Master.ExecuteNonQueryAsync($"CREATE DATABASE [{name}]", cancellationToken); + await this.Master.ExecuteNonQueryAsync(BuildCreateDatabaseSqlCommand(name, settings), cancellationToken); return this.GetDatabase(name); } @@ -102,5 +105,20 @@ public SqlServerDatabase GetDatabase(string name) return new SqlServerDatabase(this, databaseConnectionString.ToString()); } + + private static string BuildCreateDatabaseSqlCommand(string name, SqlDatabaseCreationSettings? settings) + { + var sql = new StringBuilder($"CREATE DATABASE [{name}]"); + + if (settings is not null) + { + if (!string.IsNullOrEmpty(settings.DataFileName)) + { + sql.Append($"ON (NAME = '{name}', FILENAME = '{settings.DataFileName}')"); + } + } + + return sql.ToString(); + } } } diff --git a/tests/Testing.Databases.SqlServer.Tests/SqlDatabaseCreationSettingsTest.cs b/tests/Testing.Databases.SqlServer.Tests/SqlDatabaseCreationSettingsTest.cs new file mode 100644 index 0000000..7617dbe --- /dev/null +++ b/tests/Testing.Databases.SqlServer.Tests/SqlDatabaseCreationSettingsTest.cs @@ -0,0 +1,29 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) P.O.S Informatique. All rights reserved. +// +//----------------------------------------------------------------------- + +namespace PosInformatique.Testing.Databases.SqlServer.Tests +{ + public class SqlDatabaseCreationSettingsTest + { + [Fact] + public void Constructor() + { + var settings = new SqlDatabaseCreationSettings(); + + settings.DataFileName.Should().BeNull(); + } + + [Fact] + public void DataFileName_ValueChanged() + { + var settings = new SqlDatabaseCreationSettings(); + + settings.DataFileName = "New value"; + + settings.DataFileName.Should().Be("New value"); + } + } +} \ No newline at end of file diff --git a/tests/Testing.Databases.SqlServer.Tests/SqlServerTest.cs b/tests/Testing.Databases.SqlServer.Tests/SqlServerTest.cs index e4824da..d547f1d 100644 --- a/tests/Testing.Databases.SqlServer.Tests/SqlServerTest.cs +++ b/tests/Testing.Databases.SqlServer.Tests/SqlServerTest.cs @@ -22,12 +22,21 @@ public void Constructor(string connectionString, string expectedMasterConnection server.Master.Server.Should().BeSameAs(server); } - [Fact] - public async Task CreateAndDeleteAsync() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task CreateAndDelete(bool withCreationSettings) { var server = new SqlServer(ConnectionString); - var database = await server.CreateEmptyDatabaseAsync("CreateAndDeleteDB", CancellationToken.None); + SqlDatabaseCreationSettings settings = null; + + if (withCreationSettings) + { + settings = new SqlDatabaseCreationSettings(); + } + + var database = server.CreateEmptyDatabase("CreateAndDeleteDB", settings); database.ConnectionString.Should().Be("Data Source=(localDB)\\posinfo-tests;Initial Catalog=CreateAndDeleteDB;Integrated Security=True"); @@ -35,10 +44,144 @@ public async Task CreateAndDeleteAsync() table.Rows.Should().HaveCount(1); // Delete the database - await server.DeleteDatabaseAsync("CreateAndDeleteDB", CancellationToken.None); + server.DeleteDatabase("CreateAndDeleteDB"); table = await server.Master.ExecuteQueryAsync("SELECT * FROM [sys].[databases] WHERE [name] = 'CreateAndDeleteDB'"); table.Rows.Should().BeEmpty(); } + + [Fact] + public async Task CreateAndDelete_WithSpecificName() + { + using var temporaryFolder = TemporaryFolder.Create(); + + var server = new SqlServer(ConnectionString); + + var settings = new SqlDatabaseCreationSettings() + { + DataFileName = Path.Combine(temporaryFolder.Path, "TheSpecificName.mdf"), + }; + + var database = server.CreateEmptyDatabase("CreateAndDeleteDB_WithSpecificName", settings); + + database.ConnectionString.Should().Be("Data Source=(localDB)\\posinfo-tests;Initial Catalog=CreateAndDeleteDB_WithSpecificName;Integrated Security=True"); + + var table = await server.Master.ExecuteQueryAsync("SELECT * FROM [sys].[databases] WHERE [name] = 'CreateAndDeleteDB_WithSpecificName'"); + table.Rows.Should().HaveCount(1); + + // Check the location of the database + File.Exists(Path.Combine(temporaryFolder.Path, "TheSpecificName.mdf")).Should().BeTrue(); + File.Exists(Path.Combine(temporaryFolder.Path, "TheSpecificName_log.ldf")).Should().BeTrue(); + + var result = database.ExecuteQuery("SELECT * FROM [sys].[database_files] ORDER BY [physical_name]"); + + result.Rows.Should().HaveCount(2); + + result.Rows[0]["name"].Should().Be("CreateAndDeleteDB_WithSpecificName"); + result.Rows[0]["physical_name"].Should().Be(Path.Combine(temporaryFolder.Path, "TheSpecificName.mdf")); + result.Rows[0]["type_desc"].Should().Be("ROWS"); + + result.Rows[1]["name"].Should().Be("TheSpecificName_log"); + result.Rows[1]["physical_name"].Should().Be(Path.Combine(temporaryFolder.Path, "TheSpecificName_log.ldf")); + result.Rows[1]["type_desc"].Should().Be("LOG"); + + // Delete the database + server.DeleteDatabase("CreateAndDeleteDB_WithSpecificName"); + + table = await server.Master.ExecuteQueryAsync("SELECT * FROM [sys].[databases] WHERE [name] = 'CreateAndDeleteDB_WithSpecificName'"); + table.Rows.Should().BeEmpty(); + } + + [Fact] + public async Task CreateAndDeleteAsync() + { + var server = new SqlServer(ConnectionString); + + var database = await server.CreateEmptyDatabaseAsync("CreateAndDeleteDBAsync", new SqlDatabaseCreationSettings(), CancellationToken.None); + + database.ConnectionString.Should().Be("Data Source=(localDB)\\posinfo-tests;Initial Catalog=CreateAndDeleteDBAsync;Integrated Security=True"); + + var table = await server.Master.ExecuteQueryAsync("SELECT * FROM [sys].[databases] WHERE [name] = 'CreateAndDeleteDBAsync'"); + table.Rows.Should().HaveCount(1); + + // Delete the database + await server.DeleteDatabaseAsync("CreateAndDeleteDBAsync", CancellationToken.None); + + table = await server.Master.ExecuteQueryAsync("SELECT * FROM [sys].[databases] WHERE [name] = 'CreateAndDeleteDBAsync'"); + table.Rows.Should().BeEmpty(); + } + + [Fact] + public async Task CreateAndDeleteAsync_WithSpecificName() + { + using var temporaryFolder = TemporaryFolder.Create(); + + var server = new SqlServer(ConnectionString); + + var settings = new SqlDatabaseCreationSettings() + { + DataFileName = Path.Combine(temporaryFolder.Path, "TheSpecificNameAsync.mdf"), + }; + + var database = await server.CreateEmptyDatabaseAsync("CreateAndDeleteDB_WithSpecificNameAsync", settings); + + database.ConnectionString.Should().Be("Data Source=(localDB)\\posinfo-tests;Initial Catalog=CreateAndDeleteDB_WithSpecificNameAsync;Integrated Security=True"); + + var table = await server.Master.ExecuteQueryAsync("SELECT * FROM [sys].[databases] WHERE [name] = 'CreateAndDeleteDB_WithSpecificNameAsync'"); + table.Rows.Should().HaveCount(1); + + // Check the location of the database + File.Exists(Path.Combine(temporaryFolder.Path, "TheSpecificNameAsync.mdf")).Should().BeTrue(); + File.Exists(Path.Combine(temporaryFolder.Path, "TheSpecificNameAsync_log.ldf")).Should().BeTrue(); + + var result = database.ExecuteQuery("SELECT * FROM [sys].[database_files] ORDER BY [physical_name]"); + + result.Rows.Should().HaveCount(2); + + result.Rows[0]["name"].Should().Be("CreateAndDeleteDB_WithSpecificNameAsync"); + result.Rows[0]["physical_name"].Should().Be(Path.Combine(temporaryFolder.Path, "TheSpecificNameAsync.mdf")); + result.Rows[0]["type_desc"].Should().Be("ROWS"); + + result.Rows[1]["name"].Should().Be("TheSpecificNameAsync_log"); + result.Rows[1]["physical_name"].Should().Be(Path.Combine(temporaryFolder.Path, "TheSpecificNameAsync_log.ldf")); + result.Rows[1]["type_desc"].Should().Be("LOG"); + + // Delete the database + await server.DeleteDatabaseAsync("CreateAndDeleteDB_WithSpecificNameAsync"); + + table = await server.Master.ExecuteQueryAsync("SELECT * FROM [sys].[databases] WHERE [name] = 'CreateAndDeleteDB_WithSpecificNameAsync'"); + table.Rows.Should().BeEmpty(); + } + + private sealed class TemporaryFolder : IDisposable + { + private TemporaryFolder(string path) + { + this.Path = path; + } + + public string Path { get; } + + public static TemporaryFolder Create() + { + var temporaryFolder = System.IO.Path.Combine(System.IO.Path.GetTempPath(), "PosInformatique.Testing.Databases.SqlServer.Tests", Guid.NewGuid().ToString()); + + Directory.CreateDirectory(temporaryFolder); + + return new TemporaryFolder(temporaryFolder); + } + + public void Dispose() + { + try + { + Directory.Delete(this.Path, true); + } + catch (IOException) + { + // Ignore the errors. + } + } + } } } \ No newline at end of file From 6dd10838b2d22b6a499935ffadc2a781b0cc36ee Mon Sep 17 00:00:00 2001 From: Gilles TOURREAU Date: Mon, 22 Sep 2025 11:18:11 +0200 Subject: [PATCH 02/61] Add the support in SqlServerDacExtensions to deploy database in a specific folder. --- PosInformatique.Testing.Databases.sln | 8 +- .../SqlServerDacDatabaseInitializer.cs | 2 +- .../SqlServerDacDeploymentSettings.cs | 20 ++++ .../SqlServerDacExtensions.cs | 10 +- .../Testing.Databases.SqlServer.Dac.csproj | 1 + .../SqlServerDacDeploymentSettingsTest.cs | 29 ++++++ .../SqlServerDacExtensionsTest.cs | 96 +++++++++++++++++++ .../SqlServerDatabaseInitializerTest.cs | 1 + ...sting.Databases.SqlServer.Dac.Tests.csproj | 35 +++++++ .../SqlServerTest.cs | 83 +++++----------- .../TemporaryFolder.cs | 39 ++++++++ .../Testing.Databases.SqlServer.Tests.csproj | 7 -- 12 files changed, 261 insertions(+), 70 deletions(-) create mode 100644 src/Testing.Databases.SqlServer.Dac/SqlServerDacDeploymentSettings.cs create mode 100644 tests/Testing.Databases.SqlServer.Dac.Tests/SqlServerDacDeploymentSettingsTest.cs create mode 100644 tests/Testing.Databases.SqlServer.Dac.Tests/SqlServerDacExtensionsTest.cs rename tests/{Testing.Databases.SqlServer.Tests => Testing.Databases.SqlServer.Dac.Tests}/SqlServerDatabaseInitializerTest.cs (98%) create mode 100644 tests/Testing.Databases.SqlServer.Dac.Tests/Testing.Databases.SqlServer.Dac.Tests.csproj create mode 100644 tests/Testing.Databases.SqlServer.Tests/TemporaryFolder.cs diff --git a/PosInformatique.Testing.Databases.sln b/PosInformatique.Testing.Databases.sln index d1c2105..408dbe0 100644 --- a/PosInformatique.Testing.Databases.sln +++ b/PosInformatique.Testing.Databases.sln @@ -51,12 +51,14 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Testing.Databases.SqlServer EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{8500A9B6-CAA0-432C-BABB-DDC86CE08994}" ProjectSection(SolutionItems) = preProject - docs\WriteTest.md = docs\WriteTest.md docs\WriteDatabaseMigrationTest.md = docs\WriteDatabaseMigrationTest.md + docs\WriteTest.md = docs\WriteTest.md EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Testing.Databases.SqlServer.Dac", "src\Testing.Databases.SqlServer.Dac\Testing.Databases.SqlServer.Dac.csproj", "{8BE60460-EBA5-43DE-B85D-C756E2988DC8}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testing.Databases.SqlServer.Dac.Tests", "tests\Testing.Databases.SqlServer.Dac.Tests\Testing.Databases.SqlServer.Dac.Tests.csproj", "{6751A585-1BB0-49A1-BF68-D7FBD3392DF6}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -101,6 +103,10 @@ Global {8BE60460-EBA5-43DE-B85D-C756E2988DC8}.Debug|Any CPU.Build.0 = Debug|Any CPU {8BE60460-EBA5-43DE-B85D-C756E2988DC8}.Release|Any CPU.ActiveCfg = Release|Any CPU {8BE60460-EBA5-43DE-B85D-C756E2988DC8}.Release|Any CPU.Build.0 = Release|Any CPU + {6751A585-1BB0-49A1-BF68-D7FBD3392DF6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6751A585-1BB0-49A1-BF68-D7FBD3392DF6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6751A585-1BB0-49A1-BF68-D7FBD3392DF6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6751A585-1BB0-49A1-BF68-D7FBD3392DF6}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/Testing.Databases.SqlServer.Dac/SqlServerDacDatabaseInitializer.cs b/src/Testing.Databases.SqlServer.Dac/SqlServerDacDatabaseInitializer.cs index 1724b3e..ff59715 100644 --- a/src/Testing.Databases.SqlServer.Dac/SqlServerDacDatabaseInitializer.cs +++ b/src/Testing.Databases.SqlServer.Dac/SqlServerDacDatabaseInitializer.cs @@ -13,7 +13,7 @@ namespace PosInformatique.Testing.Databases.SqlServer /// Call the method to initialize a database from /// a DACPAC file. /// - /// The database will be created the call of the method. For the next calls + /// The database will be created the call of the method. For the next calls /// the database is preserved but all the data are deleted. public static class SqlServerDacDatabaseInitializer { diff --git a/src/Testing.Databases.SqlServer.Dac/SqlServerDacDeploymentSettings.cs b/src/Testing.Databases.SqlServer.Dac/SqlServerDacDeploymentSettings.cs new file mode 100644 index 0000000..285aa03 --- /dev/null +++ b/src/Testing.Databases.SqlServer.Dac/SqlServerDacDeploymentSettings.cs @@ -0,0 +1,20 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) P.O.S Informatique. All rights reserved. +// +//----------------------------------------------------------------------- + +namespace PosInformatique.Testing.Databases.SqlServer +{ + /// + /// Contains additional settings when deploying a + /// using or . + /// + public class SqlServerDacDeploymentSettings + { + /// + /// Gets or sets the data file name (full path) of the database to create. + /// + public string? DataFileName { get; set; } + } +} diff --git a/src/Testing.Databases.SqlServer.Dac/SqlServerDacExtensions.cs b/src/Testing.Databases.SqlServer.Dac/SqlServerDacExtensions.cs index e37145f..17850d5 100644 --- a/src/Testing.Databases.SqlServer.Dac/SqlServerDacExtensions.cs +++ b/src/Testing.Databases.SqlServer.Dac/SqlServerDacExtensions.cs @@ -21,16 +21,18 @@ public static class SqlServerDacExtensions /// instance where the DACPAC file will be deployed. /// File name (including the path) for the DACPAC file to deploy. /// Name of the database which will be created. + /// Additional settings of the database to deploy. /// An instance of the which represents the deployed database. - public static SqlServerDatabase DeployDacPackage(this SqlServer server, string fileName, string databaseName) + public static SqlServerDatabase DeployDacPackage(this SqlServer server, string fileName, string databaseName, SqlServerDacDeploymentSettings? settings = null) { using (var package = DacPackage.Load(fileName)) { - var options = new DacDeployOptions(); - options.CreateNewDatabase = true; + // Currently DacFx does not support to define explicitly the location of the database files. + // So, we create an empty database and after we run the deployment without deleting the database. + server.CreateEmptyDatabase(databaseName, new SqlDatabaseCreationSettings() { DataFileName = settings?.DataFileName }); var services = new DacServices(server.Master.ConnectionString); - services.Deploy(package, databaseName, true, options: options); + services.Deploy(package, databaseName, true); } return server.GetDatabase(databaseName); diff --git a/src/Testing.Databases.SqlServer.Dac/Testing.Databases.SqlServer.Dac.csproj b/src/Testing.Databases.SqlServer.Dac/Testing.Databases.SqlServer.Dac.csproj index 205776f..d563a67 100644 --- a/src/Testing.Databases.SqlServer.Dac/Testing.Databases.SqlServer.Dac.csproj +++ b/src/Testing.Databases.SqlServer.Dac/Testing.Databases.SqlServer.Dac.csproj @@ -5,6 +5,7 @@ Testing.Databases.SqlServer.Dac is a library that contains a set of tools for testing to deploy DAC (Data-tier Applications) packages (.dacpac files). testing unittest sqlserver repository tdd dataaccesslayer dacpac dac + True diff --git a/tests/Testing.Databases.SqlServer.Dac.Tests/SqlServerDacDeploymentSettingsTest.cs b/tests/Testing.Databases.SqlServer.Dac.Tests/SqlServerDacDeploymentSettingsTest.cs new file mode 100644 index 0000000..452982d --- /dev/null +++ b/tests/Testing.Databases.SqlServer.Dac.Tests/SqlServerDacDeploymentSettingsTest.cs @@ -0,0 +1,29 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) P.O.S Informatique. All rights reserved. +// +//----------------------------------------------------------------------- + +namespace PosInformatique.Testing.Databases.SqlServer.Tests +{ + public class SqlServerDacDeploymentSettingsTest + { + [Fact] + public void Constructor() + { + var settings = new SqlServerDacDeploymentSettings(); + + settings.DataFileName.Should().BeNull(); + } + + [Fact] + public void DataFileName_ValueChanged() + { + var settings = new SqlServerDacDeploymentSettings(); + + settings.DataFileName = "The value"; + + settings.DataFileName.Should().Be("The value"); + } + } +} \ No newline at end of file diff --git a/tests/Testing.Databases.SqlServer.Dac.Tests/SqlServerDacExtensionsTest.cs b/tests/Testing.Databases.SqlServer.Dac.Tests/SqlServerDacExtensionsTest.cs new file mode 100644 index 0000000..dd81754 --- /dev/null +++ b/tests/Testing.Databases.SqlServer.Dac.Tests/SqlServerDacExtensionsTest.cs @@ -0,0 +1,96 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) P.O.S Informatique. All rights reserved. +// +//----------------------------------------------------------------------- + +namespace PosInformatique.Testing.Databases.SqlServer.Tests +{ + [Collection("PosInformatique.Testing.Databases.SqlServer.Tests")] + public class SqlServerDacExtensionsTest + { + private const string ConnectionString = $"Data Source=(localDB)\\posinfo-tests; Integrated Security=True"; + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void DeployDacPackage(bool withSettings) + { + // Create existing database to be sure the database is recreated when deploying the database with a DACPAC + CreateDatabase("SqlServerDacExtensionsTest_DeployDacPackage"); + + var server = new SqlServer(ConnectionString); + + SqlServerDacDeploymentSettings settings = null; + + if (withSettings) + { + settings = new SqlServerDacDeploymentSettings(); + } + + var database = server.DeployDacPackage("Testing.Databases.SqlServer.Tests.DacPac.dacpac", "SqlServerDacExtensionsTest_DeployDacPackage", settings); + + var table = database.ExecuteQuery("SELECT * FROM MyTable"); + + table.Rows.Should().BeEmpty(); + + // Insert data to check the connection. + database.InsertInto("MyTable", new { Id = 1, Name = "Name 1" }); + database.InsertInto("MyTable", new { Id = 2, Name = "Name 2" }); + } + + [Fact] + public void DeployDacPackage_WithSpecificFile() + { + // Create existing database to be sure the database is recreated when deploying the database with a DACPAC + CreateDatabase("SqlServerDacExtensionsTest_DeployDacPackage_WithSpecificFile"); + + using var temporaryFolder = TemporaryFolder.Create(); + + var server = new SqlServer(ConnectionString); + + var settings = new SqlServerDacDeploymentSettings() + { + DataFileName = Path.Combine(temporaryFolder.Path, "TheSpecificDataFileName.mdf"), + }; + + var database = server.DeployDacPackage("Testing.Databases.SqlServer.Tests.DacPac.dacpac", "SqlServerDacExtensionsTest_DeployDacPackage_WithSpecificFile", settings); + + var table = database.ExecuteQuery("SELECT * FROM MyTable"); + + table.Rows.Should().BeEmpty(); + + // Insert data to check the connection. + database.InsertInto("MyTable", new { Id = 1, Name = "Name 1" }); + database.InsertInto("MyTable", new { Id = 2, Name = "Name 2" }); + + // Check the location of the database + File.Exists(Path.Combine(temporaryFolder.Path, "TheSpecificDataFileName.mdf")).Should().BeTrue(); + File.Exists(Path.Combine(temporaryFolder.Path, "TheSpecificDataFileName_log.ldf")).Should().BeTrue(); + + var result = database.ExecuteQuery("SELECT * FROM [sys].[database_files] ORDER BY [physical_name]"); + + result.Rows.Should().HaveCount(2); + + result.Rows[0]["name"].Should().Be("SqlServerDacExtensionsTest_DeployDacPackage_WithSpecificFile"); + result.Rows[0]["physical_name"].Should().Be(Path.Combine(temporaryFolder.Path, "TheSpecificDataFileName.mdf")); + result.Rows[0]["type_desc"].Should().Be("ROWS"); + + result.Rows[1]["name"].Should().Be("TheSpecificDataFileName_log"); + result.Rows[1]["physical_name"].Should().Be(Path.Combine(temporaryFolder.Path, "TheSpecificDataFileName_log.ldf")); + result.Rows[1]["type_desc"].Should().Be("LOG"); + + // Delete the database (for deleting the temporary folder). + server.DeleteDatabase("SqlServerDacExtensionsTest_DeployDacPackage_WithSpecificFile"); + } + + private static void CreateDatabase(string name) + { + var server = new SqlServer(ConnectionString); + + var database = server.CreateEmptyDatabase(name); + + database.ExecuteNonQuery("CREATE TABLE OtherTable (Id INT)"); + } + } +} \ No newline at end of file diff --git a/tests/Testing.Databases.SqlServer.Tests/SqlServerDatabaseInitializerTest.cs b/tests/Testing.Databases.SqlServer.Dac.Tests/SqlServerDatabaseInitializerTest.cs similarity index 98% rename from tests/Testing.Databases.SqlServer.Tests/SqlServerDatabaseInitializerTest.cs rename to tests/Testing.Databases.SqlServer.Dac.Tests/SqlServerDatabaseInitializerTest.cs index 44c72e0..d23f286 100644 --- a/tests/Testing.Databases.SqlServer.Tests/SqlServerDatabaseInitializerTest.cs +++ b/tests/Testing.Databases.SqlServer.Dac.Tests/SqlServerDatabaseInitializerTest.cs @@ -21,6 +21,7 @@ public SqlServerDatabaseInitializerTest(SqlServerDatabaseInitializer initializer table.Rows.Should().BeEmpty(); + // Insert data to check the connection. this.database.InsertInto("MyTable", new { Id = 1, Name = "Name 1" }); this.database.InsertInto("MyTable", new { Id = 2, Name = "Name 2" }); } diff --git a/tests/Testing.Databases.SqlServer.Dac.Tests/Testing.Databases.SqlServer.Dac.Tests.csproj b/tests/Testing.Databases.SqlServer.Dac.Tests/Testing.Databases.SqlServer.Dac.Tests.csproj new file mode 100644 index 0000000..f7d6c15 --- /dev/null +++ b/tests/Testing.Databases.SqlServer.Dac.Tests/Testing.Databases.SqlServer.Dac.Tests.csproj @@ -0,0 +1,35 @@ + + + + net8.0 + + + + + + + + + PreserveNewest + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + diff --git a/tests/Testing.Databases.SqlServer.Tests/SqlServerTest.cs b/tests/Testing.Databases.SqlServer.Tests/SqlServerTest.cs index d547f1d..b1485fb 100644 --- a/tests/Testing.Databases.SqlServer.Tests/SqlServerTest.cs +++ b/tests/Testing.Databases.SqlServer.Tests/SqlServerTest.cs @@ -51,7 +51,7 @@ public async Task CreateAndDelete(bool withCreationSettings) } [Fact] - public async Task CreateAndDelete_WithSpecificName() + public async Task CreateAndDelete_WithSpecificDataFileName() { using var temporaryFolder = TemporaryFolder.Create(); @@ -59,36 +59,36 @@ public async Task CreateAndDelete_WithSpecificName() var settings = new SqlDatabaseCreationSettings() { - DataFileName = Path.Combine(temporaryFolder.Path, "TheSpecificName.mdf"), + DataFileName = Path.Combine(temporaryFolder.Path, "TheSpecificDataFileName.mdf"), }; - var database = server.CreateEmptyDatabase("CreateAndDeleteDB_WithSpecificName", settings); + var database = server.CreateEmptyDatabase("CreateAndDeleteDB_WithSpecificDataFileName", settings); - database.ConnectionString.Should().Be("Data Source=(localDB)\\posinfo-tests;Initial Catalog=CreateAndDeleteDB_WithSpecificName;Integrated Security=True"); + database.ConnectionString.Should().Be("Data Source=(localDB)\\posinfo-tests;Initial Catalog=CreateAndDeleteDB_WithSpecificDataFileName;Integrated Security=True"); - var table = await server.Master.ExecuteQueryAsync("SELECT * FROM [sys].[databases] WHERE [name] = 'CreateAndDeleteDB_WithSpecificName'"); + var table = await server.Master.ExecuteQueryAsync("SELECT * FROM [sys].[databases] WHERE [name] = 'CreateAndDeleteDB_WithSpecificDataFileName'"); table.Rows.Should().HaveCount(1); // Check the location of the database - File.Exists(Path.Combine(temporaryFolder.Path, "TheSpecificName.mdf")).Should().BeTrue(); - File.Exists(Path.Combine(temporaryFolder.Path, "TheSpecificName_log.ldf")).Should().BeTrue(); + File.Exists(Path.Combine(temporaryFolder.Path, "TheSpecificDataFileName.mdf")).Should().BeTrue(); + File.Exists(Path.Combine(temporaryFolder.Path, "TheSpecificDataFileName_log.ldf")).Should().BeTrue(); var result = database.ExecuteQuery("SELECT * FROM [sys].[database_files] ORDER BY [physical_name]"); result.Rows.Should().HaveCount(2); - result.Rows[0]["name"].Should().Be("CreateAndDeleteDB_WithSpecificName"); - result.Rows[0]["physical_name"].Should().Be(Path.Combine(temporaryFolder.Path, "TheSpecificName.mdf")); + result.Rows[0]["name"].Should().Be("CreateAndDeleteDB_WithSpecificDataFileName"); + result.Rows[0]["physical_name"].Should().Be(Path.Combine(temporaryFolder.Path, "TheSpecificDataFileName.mdf")); result.Rows[0]["type_desc"].Should().Be("ROWS"); - result.Rows[1]["name"].Should().Be("TheSpecificName_log"); - result.Rows[1]["physical_name"].Should().Be(Path.Combine(temporaryFolder.Path, "TheSpecificName_log.ldf")); + result.Rows[1]["name"].Should().Be("TheSpecificDataFileName_log"); + result.Rows[1]["physical_name"].Should().Be(Path.Combine(temporaryFolder.Path, "TheSpecificDataFileName_log.ldf")); result.Rows[1]["type_desc"].Should().Be("LOG"); // Delete the database - server.DeleteDatabase("CreateAndDeleteDB_WithSpecificName"); + server.DeleteDatabase("CreateAndDeleteDB_WithSpecificDataFileName"); - table = await server.Master.ExecuteQueryAsync("SELECT * FROM [sys].[databases] WHERE [name] = 'CreateAndDeleteDB_WithSpecificName'"); + table = await server.Master.ExecuteQueryAsync("SELECT * FROM [sys].[databases] WHERE [name] = 'CreateAndDeleteDB_WithSpecificDataFileName'"); table.Rows.Should().BeEmpty(); } @@ -112,7 +112,7 @@ public async Task CreateAndDeleteAsync() } [Fact] - public async Task CreateAndDeleteAsync_WithSpecificName() + public async Task CreateAndDeleteAsync_WithSpecificDataFileName() { using var temporaryFolder = TemporaryFolder.Create(); @@ -120,68 +120,37 @@ public async Task CreateAndDeleteAsync_WithSpecificName() var settings = new SqlDatabaseCreationSettings() { - DataFileName = Path.Combine(temporaryFolder.Path, "TheSpecificNameAsync.mdf"), + DataFileName = Path.Combine(temporaryFolder.Path, "TheSpecificDataFileNameAsync.mdf"), }; - var database = await server.CreateEmptyDatabaseAsync("CreateAndDeleteDB_WithSpecificNameAsync", settings); + var database = await server.CreateEmptyDatabaseAsync("CreateAndDeleteDB_WithSpecificDataFileNameAsync", settings); - database.ConnectionString.Should().Be("Data Source=(localDB)\\posinfo-tests;Initial Catalog=CreateAndDeleteDB_WithSpecificNameAsync;Integrated Security=True"); + database.ConnectionString.Should().Be("Data Source=(localDB)\\posinfo-tests;Initial Catalog=CreateAndDeleteDB_WithSpecificDataFileNameAsync;Integrated Security=True"); - var table = await server.Master.ExecuteQueryAsync("SELECT * FROM [sys].[databases] WHERE [name] = 'CreateAndDeleteDB_WithSpecificNameAsync'"); + var table = await server.Master.ExecuteQueryAsync("SELECT * FROM [sys].[databases] WHERE [name] = 'CreateAndDeleteDB_WithSpecificDataFileNameAsync'"); table.Rows.Should().HaveCount(1); // Check the location of the database - File.Exists(Path.Combine(temporaryFolder.Path, "TheSpecificNameAsync.mdf")).Should().BeTrue(); - File.Exists(Path.Combine(temporaryFolder.Path, "TheSpecificNameAsync_log.ldf")).Should().BeTrue(); + File.Exists(Path.Combine(temporaryFolder.Path, "TheSpecificDataFileNameAsync.mdf")).Should().BeTrue(); + File.Exists(Path.Combine(temporaryFolder.Path, "TheSpecificDataFileNameAsync_log.ldf")).Should().BeTrue(); var result = database.ExecuteQuery("SELECT * FROM [sys].[database_files] ORDER BY [physical_name]"); result.Rows.Should().HaveCount(2); - result.Rows[0]["name"].Should().Be("CreateAndDeleteDB_WithSpecificNameAsync"); - result.Rows[0]["physical_name"].Should().Be(Path.Combine(temporaryFolder.Path, "TheSpecificNameAsync.mdf")); + result.Rows[0]["name"].Should().Be("CreateAndDeleteDB_WithSpecificDataFileNameAsync"); + result.Rows[0]["physical_name"].Should().Be(Path.Combine(temporaryFolder.Path, "TheSpecificDataFileNameAsync.mdf")); result.Rows[0]["type_desc"].Should().Be("ROWS"); - result.Rows[1]["name"].Should().Be("TheSpecificNameAsync_log"); - result.Rows[1]["physical_name"].Should().Be(Path.Combine(temporaryFolder.Path, "TheSpecificNameAsync_log.ldf")); + result.Rows[1]["name"].Should().Be("TheSpecificDataFileNameAsync_log"); + result.Rows[1]["physical_name"].Should().Be(Path.Combine(temporaryFolder.Path, "TheSpecificDataFileNameAsync_log.ldf")); result.Rows[1]["type_desc"].Should().Be("LOG"); // Delete the database - await server.DeleteDatabaseAsync("CreateAndDeleteDB_WithSpecificNameAsync"); + await server.DeleteDatabaseAsync("CreateAndDeleteDB_WithSpecificDataFileNameAsync"); - table = await server.Master.ExecuteQueryAsync("SELECT * FROM [sys].[databases] WHERE [name] = 'CreateAndDeleteDB_WithSpecificNameAsync'"); + table = await server.Master.ExecuteQueryAsync("SELECT * FROM [sys].[databases] WHERE [name] = 'CreateAndDeleteDB_WithSpecificDataFileNameAsync'"); table.Rows.Should().BeEmpty(); } - - private sealed class TemporaryFolder : IDisposable - { - private TemporaryFolder(string path) - { - this.Path = path; - } - - public string Path { get; } - - public static TemporaryFolder Create() - { - var temporaryFolder = System.IO.Path.Combine(System.IO.Path.GetTempPath(), "PosInformatique.Testing.Databases.SqlServer.Tests", Guid.NewGuid().ToString()); - - Directory.CreateDirectory(temporaryFolder); - - return new TemporaryFolder(temporaryFolder); - } - - public void Dispose() - { - try - { - Directory.Delete(this.Path, true); - } - catch (IOException) - { - // Ignore the errors. - } - } - } } } \ No newline at end of file diff --git a/tests/Testing.Databases.SqlServer.Tests/TemporaryFolder.cs b/tests/Testing.Databases.SqlServer.Tests/TemporaryFolder.cs new file mode 100644 index 0000000..f8220de --- /dev/null +++ b/tests/Testing.Databases.SqlServer.Tests/TemporaryFolder.cs @@ -0,0 +1,39 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) P.O.S Informatique. All rights reserved. +// +//----------------------------------------------------------------------- + +namespace PosInformatique.Testing.Databases.SqlServer.Tests +{ + public sealed class TemporaryFolder : IDisposable + { + private TemporaryFolder(string path) + { + this.Path = path; + } + + public string Path { get; } + + public static TemporaryFolder Create() + { + var temporaryFolder = System.IO.Path.Combine(System.IO.Path.GetTempPath(), "PosInformatique.Testing.Databases.SqlServer.Tests", Guid.NewGuid().ToString()); + + Directory.CreateDirectory(temporaryFolder); + + return new TemporaryFolder(temporaryFolder); + } + + public void Dispose() + { + try + { + Directory.Delete(this.Path, true); + } + catch (IOException) + { + // Ignore the errors. + } + } + } +} diff --git a/tests/Testing.Databases.SqlServer.Tests/Testing.Databases.SqlServer.Tests.csproj b/tests/Testing.Databases.SqlServer.Tests/Testing.Databases.SqlServer.Tests.csproj index 966431e..0d98455 100644 --- a/tests/Testing.Databases.SqlServer.Tests/Testing.Databases.SqlServer.Tests.csproj +++ b/tests/Testing.Databases.SqlServer.Tests/Testing.Databases.SqlServer.Tests.csproj @@ -8,12 +8,6 @@ - - - PreserveNewest - - - PreserveNewest @@ -42,7 +36,6 @@ - From 616eb95e00c854307e1ff0957e3cb87d9e4b7cda Mon Sep 17 00:00:00 2001 From: Gilles TOURREAU Date: Mon, 22 Sep 2025 11:28:26 +0200 Subject: [PATCH 03/61] Update the SqlServerDacDatabaseInitializer to add the support of additional settings to specify the locations of the database. --- .../SqlServerDacDatabaseInitializer.cs | 9 +-- .../SqlServerDacExtensionsTest.cs | 10 +-- .../SqlServerDatabaseInitializerTest.cs | 67 +++++++++++++++++++ 3 files changed, 77 insertions(+), 9 deletions(-) diff --git a/src/Testing.Databases.SqlServer.Dac/SqlServerDacDatabaseInitializer.cs b/src/Testing.Databases.SqlServer.Dac/SqlServerDacDatabaseInitializer.cs index ff59715..76c3ae1 100644 --- a/src/Testing.Databases.SqlServer.Dac/SqlServerDacDatabaseInitializer.cs +++ b/src/Testing.Databases.SqlServer.Dac/SqlServerDacDatabaseInitializer.cs @@ -10,10 +10,10 @@ namespace PosInformatique.Testing.Databases.SqlServer /// /// Initializer used to initialize the database for the tests. - /// Call the method to initialize a database from + /// Call the method to initialize a database from /// a DACPAC file. /// - /// The database will be created the call of the method. For the next calls + /// The database will be created the call of the method. For the next calls /// the database is preserved but all the data are deleted. public static class SqlServerDacDatabaseInitializer { @@ -23,8 +23,9 @@ public static class SqlServerDacDatabaseInitializer /// which the initialization will be perform on. /// Full path of the DACPAC file. /// Connection string to the SQL Server with administrator rights. + /// Additionnal settings for the DACPAC to deploy. /// An instance of the which allows to perform query to initialize the data. - public static SqlServerDatabase Initialize(this SqlServerDatabaseInitializer initializer, string packageName, string connectionString) + public static SqlServerDatabase Initialize(this SqlServerDatabaseInitializer initializer, string packageName, string connectionString, SqlServerDacDeploymentSettings? settings = null) { var connectionStringBuilder = new SqlConnectionStringBuilder(connectionString); @@ -34,7 +35,7 @@ public static SqlServerDatabase Initialize(this SqlServerDatabaseInitializer ini if (!initializer.IsInitialized) { - database = server.DeployDacPackage(packageName, connectionStringBuilder.InitialCatalog); + database = server.DeployDacPackage(packageName, connectionStringBuilder.InitialCatalog, settings); initializer.IsInitialized = true; } diff --git a/tests/Testing.Databases.SqlServer.Dac.Tests/SqlServerDacExtensionsTest.cs b/tests/Testing.Databases.SqlServer.Dac.Tests/SqlServerDacExtensionsTest.cs index dd81754..54e5824 100644 --- a/tests/Testing.Databases.SqlServer.Dac.Tests/SqlServerDacExtensionsTest.cs +++ b/tests/Testing.Databases.SqlServer.Dac.Tests/SqlServerDacExtensionsTest.cs @@ -40,10 +40,10 @@ public void DeployDacPackage(bool withSettings) } [Fact] - public void DeployDacPackage_WithSpecificFile() + public void DeployDacPackage_WithSpecificDataFileName() { // Create existing database to be sure the database is recreated when deploying the database with a DACPAC - CreateDatabase("SqlServerDacExtensionsTest_DeployDacPackage_WithSpecificFile"); + CreateDatabase("SqlServerDacExtensionsTest_DeployDacPackage_WithSpecificDataFileName"); using var temporaryFolder = TemporaryFolder.Create(); @@ -54,7 +54,7 @@ public void DeployDacPackage_WithSpecificFile() DataFileName = Path.Combine(temporaryFolder.Path, "TheSpecificDataFileName.mdf"), }; - var database = server.DeployDacPackage("Testing.Databases.SqlServer.Tests.DacPac.dacpac", "SqlServerDacExtensionsTest_DeployDacPackage_WithSpecificFile", settings); + var database = server.DeployDacPackage("Testing.Databases.SqlServer.Tests.DacPac.dacpac", "SqlServerDacExtensionsTest_DeployDacPackage_WithSpecificDataFileName", settings); var table = database.ExecuteQuery("SELECT * FROM MyTable"); @@ -72,7 +72,7 @@ public void DeployDacPackage_WithSpecificFile() result.Rows.Should().HaveCount(2); - result.Rows[0]["name"].Should().Be("SqlServerDacExtensionsTest_DeployDacPackage_WithSpecificFile"); + result.Rows[0]["name"].Should().Be("SqlServerDacExtensionsTest_DeployDacPackage_WithSpecificDataFileName"); result.Rows[0]["physical_name"].Should().Be(Path.Combine(temporaryFolder.Path, "TheSpecificDataFileName.mdf")); result.Rows[0]["type_desc"].Should().Be("ROWS"); @@ -81,7 +81,7 @@ public void DeployDacPackage_WithSpecificFile() result.Rows[1]["type_desc"].Should().Be("LOG"); // Delete the database (for deleting the temporary folder). - server.DeleteDatabase("SqlServerDacExtensionsTest_DeployDacPackage_WithSpecificFile"); + server.DeleteDatabase("SqlServerDacExtensionsTest_DeployDacPackage_WithSpecificDataFileName"); } private static void CreateDatabase(string name) diff --git a/tests/Testing.Databases.SqlServer.Dac.Tests/SqlServerDatabaseInitializerTest.cs b/tests/Testing.Databases.SqlServer.Dac.Tests/SqlServerDatabaseInitializerTest.cs index d23f286..aeaddb7 100644 --- a/tests/Testing.Databases.SqlServer.Dac.Tests/SqlServerDatabaseInitializerTest.cs +++ b/tests/Testing.Databases.SqlServer.Dac.Tests/SqlServerDatabaseInitializerTest.cs @@ -13,8 +13,11 @@ public class SqlServerDatabaseInitializerTest : IClassFixture Date: Mon, 22 Sep 2025 11:30:58 +0200 Subject: [PATCH 04/61] Upgrade the version of CI. --- .github/workflows/github-actions-release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/github-actions-release.yml b/.github/workflows/github-actions-release.yml index 3d176f0..4cb29d3 100644 --- a/.github/workflows/github-actions-release.yml +++ b/.github/workflows/github-actions-release.yml @@ -7,7 +7,7 @@ on: type: string description: The version of the library required: true - default: 2.3.0 + default: 2.4.0 VersionSuffix: type: string description: The version suffix of the library (for example rc.1) From c075a61c498e4cc505ce30effbbdf1489058aaae Mon Sep 17 00:00:00 2001 From: Gilles TOURREAU Date: Tue, 23 Sep 2025 08:58:52 +0200 Subject: [PATCH 05/61] Add SqlCmd library. --- .github/workflows/github-actions-release.yml | 13 +- PosInformatique.Testing.Databases.sln | 12 ++ .../SqlCmdCommandLineArgumentsBuilder.cs | 91 +++++++++ .../SqlCmdDatabaseInitializer.cs | 51 +++++ .../SqlCmdException.cs | 62 ++++++ .../SqlCmdProcess.cs | 94 +++++++++ .../SqlCmdRunScriptSettings.cs | 29 +++ .../SqlCmdSqlServerDatabaseExtensions.cs | 44 ++++ .../Testing.Databases.SqlServer.SqlCmd.csproj | 23 +++ tests/.editorconfig | 3 + .../SqlCmdCommandLineArgumentsBuilderTest.cs | 188 ++++++++++++++++++ .../SqlCmdDatabaseInitializerTest.Script.sql | 18 ++ .../SqlCmdDatabaseInitializerTest.cs | 133 +++++++++++++ .../SqlCmdExceptionTest.cs | 52 +++++ .../SqlCmdProcessTest.cs | 26 +++ .../SqlCmdSqlServerExtensionsTest.cs | 139 +++++++++++++ .../TemporaryFile.cs | 40 ++++ ...ng.Databases.SqlServer.SqlCmd.Tests.csproj | 32 +++ 18 files changed, 1047 insertions(+), 3 deletions(-) create mode 100644 src/Testing.Databases.SqlServer.SqlCmd/SqlCmdCommandLineArgumentsBuilder.cs create mode 100644 src/Testing.Databases.SqlServer.SqlCmd/SqlCmdDatabaseInitializer.cs create mode 100644 src/Testing.Databases.SqlServer.SqlCmd/SqlCmdException.cs create mode 100644 src/Testing.Databases.SqlServer.SqlCmd/SqlCmdProcess.cs create mode 100644 src/Testing.Databases.SqlServer.SqlCmd/SqlCmdRunScriptSettings.cs create mode 100644 src/Testing.Databases.SqlServer.SqlCmd/SqlCmdSqlServerDatabaseExtensions.cs create mode 100644 src/Testing.Databases.SqlServer.SqlCmd/Testing.Databases.SqlServer.SqlCmd.csproj create mode 100644 tests/Testing.Databases.SqlServer.SqlCmd.Tests/SqlCmdCommandLineArgumentsBuilderTest.cs create mode 100644 tests/Testing.Databases.SqlServer.SqlCmd.Tests/SqlCmdDatabaseInitializerTest.Script.sql create mode 100644 tests/Testing.Databases.SqlServer.SqlCmd.Tests/SqlCmdDatabaseInitializerTest.cs create mode 100644 tests/Testing.Databases.SqlServer.SqlCmd.Tests/SqlCmdExceptionTest.cs create mode 100644 tests/Testing.Databases.SqlServer.SqlCmd.Tests/SqlCmdProcessTest.cs create mode 100644 tests/Testing.Databases.SqlServer.SqlCmd.Tests/SqlCmdSqlServerExtensionsTest.cs create mode 100644 tests/Testing.Databases.SqlServer.SqlCmd.Tests/TemporaryFile.cs create mode 100644 tests/Testing.Databases.SqlServer.SqlCmd.Tests/Testing.Databases.SqlServer.SqlCmd.Tests.csproj diff --git a/.github/workflows/github-actions-release.yml b/.github/workflows/github-actions-release.yml index 4cb29d3..32027d5 100644 --- a/.github/workflows/github-actions-release.yml +++ b/.github/workflows/github-actions-release.yml @@ -7,7 +7,7 @@ on: type: string description: The version of the library required: true - default: 2.4.0 + default: 3.0.0 VersionSuffix: type: string description: The version suffix of the library (for example rc.1) @@ -32,6 +32,13 @@ jobs: --property:VersionSuffix=${{ github.event.inputs.VersionSuffix }} "src/Testing.Databases.SqlServer/Testing.Databases.SqlServer.csproj" + - name: Build Testing.Databases.SqlServer.Dac + run: dotnet pack + --property:Configuration=Release + --property:VersionPrefix=${{ github.event.inputs.VersionPrefix }} + --property:VersionSuffix=${{ github.event.inputs.VersionSuffix }} + "src/Testing.Databases.SqlServer.Dac/Testing.Databases.SqlServer.Dac.csproj" + - name: Build Testing.Databases.SqlServer.EntityFramework run: dotnet pack --property:Configuration=Release @@ -39,12 +46,12 @@ jobs: --property:VersionSuffix=${{ github.event.inputs.VersionSuffix }} "src/Testing.Databases.SqlServer.EntityFramework/Testing.Databases.SqlServer.EntityFramework.csproj" - - name: Build Testing.Databases.SqlServer.Dac + - name: Build Testing.Databases.SqlServer.SqlCmd run: dotnet pack --property:Configuration=Release --property:VersionPrefix=${{ github.event.inputs.VersionPrefix }} --property:VersionSuffix=${{ github.event.inputs.VersionSuffix }} - "src/Testing.Databases.SqlServer.Dac/Testing.Databases.SqlServer.Dac.csproj" + "src/Testing.Databases.SqlServer.SqlCmd/Testing.Databases.SqlServer.SqlCmd.csproj" - name: Publish the package to nuget.org run: dotnet nuget push "src/**/bin/Release/*.nupkg" --api-key "${{ secrets.NUGET_APIKEY }}" --source https://api.nuget.org/v3/index.json diff --git a/PosInformatique.Testing.Databases.sln b/PosInformatique.Testing.Databases.sln index 408dbe0..19170f9 100644 --- a/PosInformatique.Testing.Databases.sln +++ b/PosInformatique.Testing.Databases.sln @@ -59,6 +59,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Testing.Databases.SqlServer EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testing.Databases.SqlServer.Dac.Tests", "tests\Testing.Databases.SqlServer.Dac.Tests\Testing.Databases.SqlServer.Dac.Tests.csproj", "{6751A585-1BB0-49A1-BF68-D7FBD3392DF6}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testing.Databases.SqlServer.SqlCmd", "src\Testing.Databases.SqlServer.SqlCmd\Testing.Databases.SqlServer.SqlCmd.csproj", "{D3004122-CCDD-4EAD-BD9E-DA6DFF470943}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testing.Databases.SqlServer.SqlCmd.Tests", "tests\Testing.Databases.SqlServer.SqlCmd.Tests\Testing.Databases.SqlServer.SqlCmd.Tests.csproj", "{F8E025D7-4E2F-437A-ABFA-C43A1368E15A}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -107,6 +111,14 @@ Global {6751A585-1BB0-49A1-BF68-D7FBD3392DF6}.Debug|Any CPU.Build.0 = Debug|Any CPU {6751A585-1BB0-49A1-BF68-D7FBD3392DF6}.Release|Any CPU.ActiveCfg = Release|Any CPU {6751A585-1BB0-49A1-BF68-D7FBD3392DF6}.Release|Any CPU.Build.0 = Release|Any CPU + {D3004122-CCDD-4EAD-BD9E-DA6DFF470943}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D3004122-CCDD-4EAD-BD9E-DA6DFF470943}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D3004122-CCDD-4EAD-BD9E-DA6DFF470943}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D3004122-CCDD-4EAD-BD9E-DA6DFF470943}.Release|Any CPU.Build.0 = Release|Any CPU + {F8E025D7-4E2F-437A-ABFA-C43A1368E15A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F8E025D7-4E2F-437A-ABFA-C43A1368E15A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F8E025D7-4E2F-437A-ABFA-C43A1368E15A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F8E025D7-4E2F-437A-ABFA-C43A1368E15A}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/Testing.Databases.SqlServer.SqlCmd/SqlCmdCommandLineArgumentsBuilder.cs b/src/Testing.Databases.SqlServer.SqlCmd/SqlCmdCommandLineArgumentsBuilder.cs new file mode 100644 index 0000000..0e67e83 --- /dev/null +++ b/src/Testing.Databases.SqlServer.SqlCmd/SqlCmdCommandLineArgumentsBuilder.cs @@ -0,0 +1,91 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) P.O.S Informatique. All rights reserved. +// +//----------------------------------------------------------------------- + +namespace PosInformatique.Testing.Databases.SqlServer +{ + using System.Text; + using Microsoft.Data.SqlClient; + + internal sealed class SqlCmdCommandLineArgumentsBuilder + { + private readonly Dictionary variables; + + public SqlCmdCommandLineArgumentsBuilder(SqlConnectionStringBuilder connectionString) + { + this.Database = connectionString.InitialCatalog; + this.LoginId = connectionString.UserID; + this.Password = connectionString.Password; + this.Server = connectionString.DataSource; + this.TrustedConnection = connectionString.IntegratedSecurity; + + this.variables = new Dictionary(); + } + + public string? Database { get; set; } + + public string? InputFile { get; set; } + + public string? LoginId { get; set; } + + public string? Password { get; set; } + + public string? Server { get; set; } + + public bool TrustedConnection { get; set; } + + public IReadOnlyDictionary Variables => this.variables; + + public void AddVariable(string name, string value) + { + this.variables.Add(name, value); + } + + public override string ToString() + { + var parameters = new StringBuilder(); + + if (!string.IsNullOrEmpty(this.Database)) + { + parameters.Append($"-d \"{this.Database}\" "); + } + + if (!string.IsNullOrEmpty(this.InputFile)) + { + parameters.Append($"-i \"{this.InputFile}\" "); + } + + if (!string.IsNullOrEmpty(this.LoginId)) + { + parameters.Append($"-U \"{this.LoginId}\" "); + } + + if (!string.IsNullOrEmpty(this.Password)) + { + parameters.Append($"-P \"{this.Password}\" "); + } + + if (this.TrustedConnection) + { + parameters.Append($"-E "); + } + + if (!string.IsNullOrEmpty(this.Server)) + { + parameters.Append($"-S \"{this.Server}\" "); + } + + foreach (var variable in this.variables) + { + parameters.Append($"-v {variable.Key}=\"{variable.Value}\" "); + } + + // To have exit error code when the script contains errors. + parameters.Append("-b"); + + return parameters.ToString(); + } + } +} diff --git a/src/Testing.Databases.SqlServer.SqlCmd/SqlCmdDatabaseInitializer.cs b/src/Testing.Databases.SqlServer.SqlCmd/SqlCmdDatabaseInitializer.cs new file mode 100644 index 0000000..c58f858 --- /dev/null +++ b/src/Testing.Databases.SqlServer.SqlCmd/SqlCmdDatabaseInitializer.cs @@ -0,0 +1,51 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) P.O.S Informatique. All rights reserved. +// +//----------------------------------------------------------------------- + +namespace PosInformatique.Testing.Databases.SqlServer +{ + using Microsoft.Data.SqlClient; + + /// + /// Initializer used to initialize the database for the tests. + /// Call the method to initialize a database from + /// a T-SQL script file using sqlcmd. + /// + /// The database will be created the call of the method. For the next calls + /// the database is preserved but all the data are deleted. + public static class SqlCmdDatabaseInitializer + { + /// + /// Initialize a SQL Server database by executing the T-SQL script specified in the argument. + /// The script will be executed on the master database specified in the , so + /// you have to switch the connection if need in your script using the T-SQL USE directive. + /// + /// which the initialization will be perform on. + /// Full path of the T-SQL file to execute. + /// Connection string to the SQL Server with administrator rights. + /// Additionnal settings to run the sqlcmd tool. + /// An instance of the which allows to perform query to initialize the data. + public static SqlServerDatabase Initialize(this SqlServerDatabaseInitializer initializer, string fileName, string connectionString, SqlCmdRunScriptSettings? settings = null) + { + var connectionStringBuilder = new SqlConnectionStringBuilder(connectionString); + + var server = new SqlServer(connectionString); + + SqlServerDatabase database; + + if (!initializer.IsInitialized) + { + server.Master.RunScript(fileName, settings); + + initializer.IsInitialized = true; + } + + database = server.GetDatabase(connectionStringBuilder.InitialCatalog); + database.ClearAllData(); + + return database; + } + } +} diff --git a/src/Testing.Databases.SqlServer.SqlCmd/SqlCmdException.cs b/src/Testing.Databases.SqlServer.SqlCmd/SqlCmdException.cs new file mode 100644 index 0000000..d250ca4 --- /dev/null +++ b/src/Testing.Databases.SqlServer.SqlCmd/SqlCmdException.cs @@ -0,0 +1,62 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) P.O.S Informatique. All rights reserved. +// +//----------------------------------------------------------------------- + +namespace PosInformatique.Testing.Databases.SqlServer +{ + /// + /// Occured when an error has been raised by the SQL Server sqlcmd tool. + /// + public class SqlCmdException : Exception + { + /// + /// Initializes a new instance of the class. + /// + public SqlCmdException() + : base() + { + } + + /// + /// Initializes a new instance of the class + /// with the specified . + /// + /// Exception message. + public SqlCmdException(string message) + : base(message) + { + } + + /// + /// Initializes a new instance of the class + /// with the specified and + /// of the sqlcmd process. + /// + /// Exception message. + /// Output of the sqlcmd process. + public SqlCmdException(string message, string output) + : base(message) + { + this.Output = output; + } + + /// + /// Initializes a new instance of the class + /// with the specified raised by + /// an other . + /// + /// Exception message. + /// Previous which raised the . + public SqlCmdException(string message, Exception innerException) + : base(message, innerException) + { + } + + /// + /// Gets the output of the sqlcmd execution. + /// + public string? Output { get; } + } +} diff --git a/src/Testing.Databases.SqlServer.SqlCmd/SqlCmdProcess.cs b/src/Testing.Databases.SqlServer.SqlCmd/SqlCmdProcess.cs new file mode 100644 index 0000000..2b66916 --- /dev/null +++ b/src/Testing.Databases.SqlServer.SqlCmd/SqlCmdProcess.cs @@ -0,0 +1,94 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) P.O.S Informatique. All rights reserved. +// +//----------------------------------------------------------------------- + +namespace PosInformatique.Testing.Databases.SqlServer +{ + using System.Diagnostics; + using Microsoft.Data.SqlClient; + + internal sealed class SqlCmdProcess : IDisposable + { + private Process? process; + + private List output; + + private SqlCmdProcess(string arguments) + { + this.output = new List(); + + this.process = new Process() + { + StartInfo = + { + Arguments = arguments, + FileName = "sqlcmd", + RedirectStandardError = true, + RedirectStandardOutput = true, + UseShellExecute = false, + }, + }; + + this.process.ErrorDataReceived += this.OnOutputDataReceived; + this.process.OutputDataReceived += this.OnOutputDataReceived; + + this.process.Start(); + + this.process.BeginErrorReadLine(); + this.process.BeginOutputReadLine(); + } + + public string Output => string.Join(Environment.NewLine, this.output); + + public static SqlCmdProcess RunScript(SqlConnectionStringBuilder connectionString, string inputFile, SqlCmdRunScriptSettings settings) + { + var commandLineBuilder = new SqlCmdCommandLineArgumentsBuilder(connectionString) + { + InputFile = inputFile, + }; + + foreach (var variable in settings.Variables) + { + commandLineBuilder.AddVariable(variable.Key, variable.Value); + } + + var process = new SqlCmdProcess(commandLineBuilder.ToString()); + + return process; + } + + public void Dispose() + { + if (this.process is not null) + { + this.process.Dispose(); + this.process = null; + } + } + + public int WaitForExit() + { + if (this.process is null) + { + throw new ObjectDisposedException(this.GetType().FullName); + } + + this.process.WaitForExit(); + + return this.process.ExitCode; + } + + private void OnOutputDataReceived(object sender, DataReceivedEventArgs e) + { + if (e.Data is not null) + { + lock (this.output) + { + this.output.Add(e.Data); + } + } + } + } +} diff --git a/src/Testing.Databases.SqlServer.SqlCmd/SqlCmdRunScriptSettings.cs b/src/Testing.Databases.SqlServer.SqlCmd/SqlCmdRunScriptSettings.cs new file mode 100644 index 0000000..5c24932 --- /dev/null +++ b/src/Testing.Databases.SqlServer.SqlCmd/SqlCmdRunScriptSettings.cs @@ -0,0 +1,29 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) P.O.S Informatique. All rights reserved. +// +//----------------------------------------------------------------------- + +namespace PosInformatique.Testing.Databases.SqlServer +{ + /// + /// Contains additional settings when runing the sqlcmd. + /// + public class SqlCmdRunScriptSettings + { + /// + /// Initializes a new instance of the class. + /// + public SqlCmdRunScriptSettings() + { + this.Variables = new Dictionary(); + } + + /// + /// Gets a collection of variables and associated values which will be applied + /// on the script to run. The variables can be referenced in the T-SQL script + /// using the $(Variable) syntax. + /// + public IDictionary Variables { get; } + } +} diff --git a/src/Testing.Databases.SqlServer.SqlCmd/SqlCmdSqlServerDatabaseExtensions.cs b/src/Testing.Databases.SqlServer.SqlCmd/SqlCmdSqlServerDatabaseExtensions.cs new file mode 100644 index 0000000..e110a14 --- /dev/null +++ b/src/Testing.Databases.SqlServer.SqlCmd/SqlCmdSqlServerDatabaseExtensions.cs @@ -0,0 +1,44 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) P.O.S Informatique. All rights reserved. +// +//----------------------------------------------------------------------- + +namespace PosInformatique.Testing.Databases.SqlServer +{ + using Microsoft.Data.SqlClient; + + /// + /// Contains extensions methods for the to run script using the SQL Server sqlcmd tool. + /// + public static class SqlCmdSqlServerDatabaseExtensions + { + /// + /// Run the T-SQL script specified in with the sqlcmd tool. + /// + /// where the script will be executed on. + /// T-SQL script to execute on the . + /// Additional settings to run the script. + /// If an error has been occured when running the T-SQL script. Check the + /// to retrieve the output result of the script execution. + public static void RunScript(this SqlServerDatabase database, string fileName, SqlCmdRunScriptSettings? settings = null) + { + if (settings is null) + { + settings = new SqlCmdRunScriptSettings(); + } + + var connectionStringBuilder = new SqlConnectionStringBuilder(database.ConnectionString); + + using (var sqlCmdProcess = SqlCmdProcess.RunScript(connectionStringBuilder, fileName, settings)) + { + var exitCode = sqlCmdProcess.WaitForExit(); + + if (exitCode != 0) + { + throw new SqlCmdException($"Some errors has been occurred when executing the '{fileName}'. Check the {nameof(SqlCmdException.Output)} property of the exception to retrieve the output of the sqlcmd utility.", sqlCmdProcess.Output); + } + } + } + } +} diff --git a/src/Testing.Databases.SqlServer.SqlCmd/Testing.Databases.SqlServer.SqlCmd.csproj b/src/Testing.Databases.SqlServer.SqlCmd/Testing.Databases.SqlServer.SqlCmd.csproj new file mode 100644 index 0000000..5373ed1 --- /dev/null +++ b/src/Testing.Databases.SqlServer.SqlCmd/Testing.Databases.SqlServer.SqlCmd.csproj @@ -0,0 +1,23 @@ + + + + net6.0 + True + + Testing.Databases.SqlServer.SqlCmd is a library that contains a set of tools for testing Data Access Layer using SQLCMD tool to initialize the database with a SQL script. + testing unittest sqlcmd sqlserver repository tdd dataaccesslayer + + + + + + + + + + + + + + + diff --git a/tests/.editorconfig b/tests/.editorconfig index eedb7c8..3132794 100644 --- a/tests/.editorconfig +++ b/tests/.editorconfig @@ -1,3 +1,6 @@ [*.cs] ### StyleCop + +# SA1118: Parameter should not span multiple lines +dotnet_diagnostic.SA1118.severity = none \ No newline at end of file diff --git a/tests/Testing.Databases.SqlServer.SqlCmd.Tests/SqlCmdCommandLineArgumentsBuilderTest.cs b/tests/Testing.Databases.SqlServer.SqlCmd.Tests/SqlCmdCommandLineArgumentsBuilderTest.cs new file mode 100644 index 0000000..adaed43 --- /dev/null +++ b/tests/Testing.Databases.SqlServer.SqlCmd.Tests/SqlCmdCommandLineArgumentsBuilderTest.cs @@ -0,0 +1,188 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) P.O.S Informatique. All rights reserved. +// +//----------------------------------------------------------------------- + +namespace PosInformatique.Testing.Databases.SqlServer.Tests +{ + using Microsoft.Data.SqlClient; + + public class SqlCmdCommandLineArgumentsBuilderTest + { + [Fact] + public void Constructor() + { + var connectionString = new SqlConnectionStringBuilder() + { + DataSource = "The data source", + InitialCatalog = "The initial catalog", + IntegratedSecurity = true, + Password = "The password", + UserID = "The login", + }; + + var argumentsBuilder = new SqlCmdCommandLineArgumentsBuilder(connectionString); + + argumentsBuilder.Database.Should().Be("The initial catalog"); + argumentsBuilder.InputFile.Should().BeNull(); + argumentsBuilder.LoginId.Should().Be("The login"); + argumentsBuilder.Password.Should().Be("The password"); + argumentsBuilder.Server.Should().Be("The data source"); + argumentsBuilder.TrustedConnection.Should().BeTrue(); + argumentsBuilder.Variables.Should().BeEmpty(); + } + + [Fact] + public void Constructor_WithEmptyConnnectionString() + { + var connectionString = new SqlConnectionStringBuilder(); + + var argumentsBuilder = new SqlCmdCommandLineArgumentsBuilder(connectionString); + + argumentsBuilder.Database.Should().BeEmpty(); + argumentsBuilder.InputFile.Should().BeNull(); + argumentsBuilder.LoginId.Should().BeEmpty(); + argumentsBuilder.Password.Should().BeEmpty(); + argumentsBuilder.Server.Should().BeEmpty(); + argumentsBuilder.TrustedConnection.Should().BeFalse(); + argumentsBuilder.Variables.Should().BeEmpty(); + } + + [Fact] + public void Database_ValueChanged() + { + var connectionString = new SqlConnectionStringBuilder(); + + var argumentsBuilder = new SqlCmdCommandLineArgumentsBuilder(connectionString); + + argumentsBuilder.Database = "The database"; + + argumentsBuilder.Database.Should().Be("The database"); + } + + [Fact] + public void InputFile_ValueChanged() + { + var connectionString = new SqlConnectionStringBuilder(); + + var argumentsBuilder = new SqlCmdCommandLineArgumentsBuilder(connectionString); + + argumentsBuilder.InputFile = "The input file"; + + argumentsBuilder.InputFile.Should().Be("The input file"); + } + + [Fact] + public void Password_ValueChanged() + { + var connectionString = new SqlConnectionStringBuilder(); + + var argumentsBuilder = new SqlCmdCommandLineArgumentsBuilder(connectionString); + + argumentsBuilder.Password = "The password"; + + argumentsBuilder.Password.Should().Be("The password"); + } + + [Fact] + public void Server_ValueChanged() + { + var connectionString = new SqlConnectionStringBuilder(); + + var argumentsBuilder = new SqlCmdCommandLineArgumentsBuilder(connectionString); + + argumentsBuilder.Server = "The server"; + + argumentsBuilder.Server.Should().Be("The server"); + } + + [Fact] + public void LoginId_ValueChanged() + { + var connectionString = new SqlConnectionStringBuilder(); + + var argumentsBuilder = new SqlCmdCommandLineArgumentsBuilder(connectionString); + + argumentsBuilder.LoginId = "The login"; + + argumentsBuilder.LoginId.Should().Be("The login"); + } + + [Fact] + public void TrustedConnection_ValueChanged() + { + var connectionString = new SqlConnectionStringBuilder(); + + var argumentsBuilder = new SqlCmdCommandLineArgumentsBuilder(connectionString); + + argumentsBuilder.TrustedConnection = true; + + argumentsBuilder.TrustedConnection.Should().BeTrue(); + } + + [Fact] + public void AddVariable() + { + var connectionString = new SqlConnectionStringBuilder(); + + var argumentsBuilder = new SqlCmdCommandLineArgumentsBuilder(connectionString); + + argumentsBuilder.AddVariable("v1", "Value 1"); + argumentsBuilder.AddVariable("v2", "Value 2"); + + argumentsBuilder.Variables.Should().HaveCount(2); + + argumentsBuilder.Variables["v1"].Should().Be("Value 1"); + argumentsBuilder.Variables["v2"].Should().Be("Value 2"); + } + + [Theory] + [InlineData("TheServer", "TheDatabase", "TheLogin", "ThePassword", false, "TheInputFile", "-d \"TheDatabase\" -i \"TheInputFile\" -U \"TheLogin\" -P \"ThePassword\" -S \"TheServer\" -v v1=\"Value 1\" -b")] + [InlineData("TheServer", "TheDatabase", null, null, true, "TheInputFile", "-d \"TheDatabase\" -i \"TheInputFile\" -E -S \"TheServer\" -v v1=\"Value 1\" -b")] + [InlineData("TheServer", "TheDatabase", "", "", true, "TheInputFile", "-d \"TheDatabase\" -i \"TheInputFile\" -E -S \"TheServer\" -v v1=\"Value 1\" -b")] + [InlineData("", "", "", "", false, "", "-v v1=\"Value 1\" -b")] + [InlineData(null, null, null, null, false, null, "-v v1=\"Value 1\" -b")] + public void ToString_ReturnsCommandLineArguments(string server, string database, string loginId, string password, bool trustedConnection, string inputFile, string expectedCommandLineArguments) + { + var argumentsBuilder = new SqlCmdCommandLineArgumentsBuilder(new SqlConnectionStringBuilder(string.Empty)) + { + Database = database, + InputFile = inputFile, + LoginId = loginId, + Password = password, + Server = server, + TrustedConnection = trustedConnection, + }; + + argumentsBuilder.AddVariable("v1", "Value 1"); + + var commandLineArguments = argumentsBuilder.ToString(); + + commandLineArguments.Should().Be(expectedCommandLineArguments); + } + + [Theory] + [InlineData("TheServer", "TheDatabase", "TheLogin", "ThePassword", false, "TheInputFile", "-d \"TheDatabase\" -i \"TheInputFile\" -U \"TheLogin\" -P \"ThePassword\" -S \"TheServer\" -b")] + [InlineData("TheServer", "TheDatabase", null, null, true, "TheInputFile", "-d \"TheDatabase\" -i \"TheInputFile\" -E -S \"TheServer\" -b")] + [InlineData("TheServer", "TheDatabase", "", "", true, "TheInputFile", "-d \"TheDatabase\" -i \"TheInputFile\" -E -S \"TheServer\" -b")] + [InlineData("", "", "", "", false, "", "-b")] + [InlineData(null, null, null, null, false, null, "-b")] + public void ToString_ReturnsCommandLineArguments_WithNoVariables(string server, string database, string loginId, string password, bool trustedConnection, string inputFile, string expectedCommandLineArguments) + { + var argumentsBuilder = new SqlCmdCommandLineArgumentsBuilder(new SqlConnectionStringBuilder(string.Empty)) + { + Database = database, + InputFile = inputFile, + LoginId = loginId, + Password = password, + Server = server, + TrustedConnection = trustedConnection, + }; + + var commandLineArguments = argumentsBuilder.ToString(); + + commandLineArguments.Should().Be(expectedCommandLineArguments); + } + } +} diff --git a/tests/Testing.Databases.SqlServer.SqlCmd.Tests/SqlCmdDatabaseInitializerTest.Script.sql b/tests/Testing.Databases.SqlServer.SqlCmd.Tests/SqlCmdDatabaseInitializerTest.Script.sql new file mode 100644 index 0000000..177e0d6 --- /dev/null +++ b/tests/Testing.Databases.SqlServer.SqlCmd.Tests/SqlCmdDatabaseInitializerTest.Script.sql @@ -0,0 +1,18 @@ +IF (DB_ID(N'$(DatabaseName)') IS NOT NULL) +BEGIN + ALTER DATABASE [$(DatabaseName)] + SET SINGLE_USER WITH ROLLBACK IMMEDIATE; + DROP DATABASE [$(DatabaseName)]; +END +GO +PRINT N'Creating database $(DatabaseName)...' +GO +CREATE DATABASE [$(DatabaseName)] +GO +USE [$(DatabaseName)]; +GO +CREATE TABLE MyTable +( + [Id] INT, + [Name] VARCHAR(50) +) \ No newline at end of file diff --git a/tests/Testing.Databases.SqlServer.SqlCmd.Tests/SqlCmdDatabaseInitializerTest.cs b/tests/Testing.Databases.SqlServer.SqlCmd.Tests/SqlCmdDatabaseInitializerTest.cs new file mode 100644 index 0000000..0c4822d --- /dev/null +++ b/tests/Testing.Databases.SqlServer.SqlCmd.Tests/SqlCmdDatabaseInitializerTest.cs @@ -0,0 +1,133 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) P.O.S Informatique. All rights reserved. +// +//----------------------------------------------------------------------- + +namespace PosInformatique.Testing.Databases.SqlServer.Tests +{ + [Collection("PosInformatique.Testing.Databases.SqlServer.Tests")] + public class SqlCmdDatabaseInitializerTest : IClassFixture + { + private const string ConnectionString = $"Data Source=(localDB)\\posinfo-tests; Initial Catalog={nameof(SqlCmdDatabaseInitializerTest)}; Integrated Security=True"; + + private readonly SqlServerDatabase database; + + private readonly SqlServerDatabaseInitializer initializer; + + public SqlCmdDatabaseInitializerTest(SqlServerDatabaseInitializer initializer) + { + this.initializer = initializer; + + var settings = new SqlCmdRunScriptSettings() + { + Variables = + { + { "DatabaseName", nameof(SqlCmdDatabaseInitializerTest) }, + }, + }; + + this.database = initializer.Initialize("SqlCmdDatabaseInitializerTest.Script.sql", ConnectionString, settings); + + var table = this.database.ExecuteQuery("SELECT * FROM MyTable"); + + table.Rows.Should().BeEmpty(); + + // Insert data to check the connection. + this.database.InsertInto("MyTable", new { Id = 1, Name = "Name 1" }); + this.database.InsertInto("MyTable", new { Id = 2, Name = "Name 2" }); + } + + [Fact] + public void Test1() + { + this.initializer.IsInitialized.Should().BeTrue(); + + var currentUser = this.database.ExecuteQuery("SELECT SUSER_NAME()"); + currentUser.Rows[0][0].Should().Be($"{Environment.UserDomainName}\\{Environment.UserName}"); + + // Check the constructor has been called + var table = this.database.ExecuteQuery("SELECT * FROM MyTable"); + + table.Rows.Should().HaveCount(2); + + table.Rows[0]["Id"].Should().Be(1); + table.Rows[0]["Name"].Should().Be("Name 1"); + + table.Rows[1]["Id"].Should().Be(2); + table.Rows[1]["Name"].Should().Be("Name 2"); + + // Insert a row which should not be use in other tests. + this.database.InsertInto("MyTable", new { Id = 99, Name = "Should not be here for the next test" }); + } + + [Fact] + public void Test2() + { + this.initializer.IsInitialized.Should().BeTrue(); + + var currentUser = this.database.ExecuteQuery("SELECT SUSER_NAME()"); + currentUser.Rows[0][0].Should().Be($"{Environment.UserDomainName}\\{Environment.UserName}"); + + // Check the constructor has been called + var table = this.database.ExecuteQuery("SELECT * FROM MyTable"); + + table.Rows.Should().HaveCount(2); + + table.Rows[0]["Id"].Should().Be(1); + table.Rows[0]["Name"].Should().Be("Name 1"); + + table.Rows[1]["Id"].Should().Be(2); + table.Rows[1]["Name"].Should().Be("Name 2"); + + // Insert a row which should not be use in other tests. + this.database.InsertInto("MyTable", new { Id = 99, Name = "Should not be here for the next test" }); + } + + [Fact] + public async Task Test1Async() + { + this.initializer.IsInitialized.Should().BeTrue(); + + var currentUser = await this.database.ExecuteQueryAsync("SELECT SUSER_NAME()"); + currentUser.Rows[0][0].Should().Be($"{Environment.UserDomainName}\\{Environment.UserName}"); + + // Check the constructor has been called + var table = await this.database.ExecuteQueryAsync("SELECT * FROM MyTable"); + + table.Rows.Should().HaveCount(2); + + table.Rows[0]["Id"].Should().Be(1); + table.Rows[0]["Name"].Should().Be("Name 1"); + + table.Rows[1]["Id"].Should().Be(2); + table.Rows[1]["Name"].Should().Be("Name 2"); + + // Insert a row which should not be use in other tests. + await this.database.InsertIntoAsync("MyTable", new { Id = 99, Name = "Should not be here for the next test" }); + } + + [Fact] + public async Task Test2Async() + { + this.initializer.IsInitialized.Should().BeTrue(); + + var currentUser = await this.database.ExecuteQueryAsync("SELECT SUSER_NAME()"); + currentUser.Rows[0][0].Should().Be($"{Environment.UserDomainName}\\{Environment.UserName}"); + + // Check the constructor has been called + var table = await this.database.ExecuteQueryAsync("SELECT * FROM MyTable"); + + table.Rows.Should().HaveCount(2); + + table.Rows[0]["Id"].Should().Be(1); + table.Rows[0]["Name"].Should().Be("Name 1"); + + table.Rows[1]["Id"].Should().Be(2); + table.Rows[1]["Name"].Should().Be("Name 2"); + + // Insert a row which should not be use in other tests. + await this.database.InsertIntoAsync("MyTable", new { Id = 99, Name = "Should not be here for the next test" }); + } + } +} \ No newline at end of file diff --git a/tests/Testing.Databases.SqlServer.SqlCmd.Tests/SqlCmdExceptionTest.cs b/tests/Testing.Databases.SqlServer.SqlCmd.Tests/SqlCmdExceptionTest.cs new file mode 100644 index 0000000..d0243ea --- /dev/null +++ b/tests/Testing.Databases.SqlServer.SqlCmd.Tests/SqlCmdExceptionTest.cs @@ -0,0 +1,52 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) P.O.S Informatique. All rights reserved. +// +//----------------------------------------------------------------------- + +namespace PosInformatique.Testing.Databases.SqlServer.Tests +{ + public class SqlCmdExceptionTest + { + [Fact] + public void Constructor() + { + var exception = new SqlCmdException(); + + exception.Message.Should().Be("Exception of type 'PosInformatique.Testing.Databases.SqlServer.SqlCmdException' was thrown."); + exception.Output.Should().BeNull(); + exception.InnerException.Should().BeNull(); + } + + [Fact] + public void Constructor_WithMessage() + { + var exception = new SqlCmdException("The message"); + + exception.Message.Should().Be("The message"); + exception.Output.Should().BeNull(); + exception.InnerException.Should().BeNull(); + } + + [Fact] + public void Constructor_WithMessageAndOutput() + { + var exception = new SqlCmdException("The message", "The output"); + + exception.Message.Should().Be("The message"); + exception.Output.Should().Be("The output"); + exception.InnerException.Should().BeNull(); + } + + [Fact] + public void Constructor_WithMessageAndInnerException() + { + var innerException = new FormatException("The inner exception"); + var exception = new SqlCmdException("The message", innerException); + + exception.Message.Should().Be("The message"); + exception.Output.Should().BeNull(); + exception.InnerException.Should().BeSameAs(innerException); + } + } +} \ No newline at end of file diff --git a/tests/Testing.Databases.SqlServer.SqlCmd.Tests/SqlCmdProcessTest.cs b/tests/Testing.Databases.SqlServer.SqlCmd.Tests/SqlCmdProcessTest.cs new file mode 100644 index 0000000..d62d798 --- /dev/null +++ b/tests/Testing.Databases.SqlServer.SqlCmd.Tests/SqlCmdProcessTest.cs @@ -0,0 +1,26 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) P.O.S Informatique. All rights reserved. +// +//----------------------------------------------------------------------- + +namespace PosInformatique.Testing.Databases.SqlServer.Tests +{ + using Microsoft.Data.SqlClient; + + public class SqlCmdProcessTest + { + [Fact] + public void WaitForExit_Disposed() + { + var process = SqlCmdProcess.RunScript(new SqlConnectionStringBuilder(), "NoFile.sql", new SqlCmdRunScriptSettings()); + + process.Dispose(); + + process.Invoking(p => p.WaitForExit()) + .Should().ThrowExactly() + .WithMessage("Cannot access a disposed object.\r\nObject name: 'PosInformatique.Testing.Databases.SqlServer.SqlCmdProcess'.") + .Which.ObjectName.Should().Be("PosInformatique.Testing.Databases.SqlServer.SqlCmdProcess"); + } + } +} \ No newline at end of file diff --git a/tests/Testing.Databases.SqlServer.SqlCmd.Tests/SqlCmdSqlServerExtensionsTest.cs b/tests/Testing.Databases.SqlServer.SqlCmd.Tests/SqlCmdSqlServerExtensionsTest.cs new file mode 100644 index 0000000..db942fe --- /dev/null +++ b/tests/Testing.Databases.SqlServer.SqlCmd.Tests/SqlCmdSqlServerExtensionsTest.cs @@ -0,0 +1,139 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) P.O.S Informatique. All rights reserved. +// +//----------------------------------------------------------------------- + +namespace PosInformatique.Testing.Databases.SqlServer.Tests +{ + [Collection("PosInformatique.Testing.Databases.SqlServer.Tests")] + public class SqlCmdSqlServerExtensionsTest + { + private const string ConnectionString = $"Data Source=(localDB)\\posinfo-tests; Integrated Security=True"; + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void RunScript(bool withSettings) + { + var server = new SqlServer(ConnectionString); + + server.DeleteDatabase("SqlCmdSqlServerExtensionsTest_RunScript"); + + SqlCmdRunScriptSettings settings = null; + + if (withSettings) + { + settings = new SqlCmdRunScriptSettings(); + } + + using var temporaryFile = TemporaryFile.Create(); + + File.WriteAllText( + temporaryFile.FileName, + """ + PRINT 'GOOOOOO !' + GO + CREATE DATABASE SqlCmdSqlServerExtensionsTest_RunScript + GO + USE SqlCmdSqlServerExtensionsTest_RunScript + GO + CREATE TABLE MyTable (Name VARCHAR(50)) + """); + + server.Master.RunScript(temporaryFile.FileName, settings); + + var database = server.GetDatabase("SqlCmdSqlServerExtensionsTest_RunScript"); + + var table = database.ExecuteQuery("SELECT * FROM MyTable"); + + table.Rows.Should().BeEmpty(); + } + + [Fact] + public void RunScript_WithVariables() + { + var server = new SqlServer(ConnectionString); + + server.DeleteDatabase("SqlCmdSqlServerExtensionsTest_RunScript_WithVariables"); + + var settings = new SqlCmdRunScriptSettings() + { + Variables = + { + { "DatabaseName", "SqlCmdSqlServerExtensionsTest_RunScript_WithVariables" }, + { "TableName", "MyTable" }, + }, + }; + + using var temporaryFile = TemporaryFile.Create(); + + File.WriteAllText( + temporaryFile.FileName, + """ + PRINT 'GOOOOOO !' + GO + CREATE DATABASE [$(DatabaseName)] + GO + USE [$(DatabaseName)] + GO + CREATE TABLE [$(TableName)] (Name VARCHAR(50)) + """); + + server.Master.RunScript(temporaryFile.FileName, settings); + + var database = server.GetDatabase("SqlCmdSqlServerExtensionsTest_RunScript"); + + var table = database.ExecuteQuery("SELECT * FROM MyTable"); + + table.Rows.Should().BeEmpty(); + } + + [Fact] + public void RunScript_WithErros() + { + var server = new SqlServer(ConnectionString); + + server.DeleteDatabase("SqlCmdSqlServerExtensionsTest_RunScript_WithErros"); + + var settings = new SqlCmdRunScriptSettings() + { + Variables = + { + { "DatabaseName", "SqlCmdSqlServerExtensionsTest_RunScript_WithErros" }, + { "TableName", "MyTable" }, + }, + }; + + using var temporaryFile = TemporaryFile.Create(); + + File.WriteAllText( + temporaryFile.FileName, + """ + PRINT 'GOOOOOO !' + GO + CREATE DATABASE [$(DatabaseName)] + GO + USE [$(DatabaseName)] + GO + CREATE TABLE ErrorBlabla + """); + + server.Master.Invoking(m => m.RunScript(temporaryFile.FileName, settings)) + .Should().ThrowExactly() + .Which.Output.Should().Be( + """ + GOOOOOO ! + Changed database context to 'SqlCmdSqlServerExtensionsTest_RunScript_WithErros'. + Msg 102, Level 15, State 1, Server TOURREAU-LAPTOP\LOCALDB#19CCEEF8, Line 1 + Incorrect syntax near 'ErrorBlabla'. + """); + + var database = server.GetDatabase("SqlCmdSqlServerExtensionsTest_RunScript_WithErros"); + + var table = database.ExecuteQuery("SELECT * FROM sys.tables"); + + table.Rows.Should().BeEmpty(); + } + } +} \ No newline at end of file diff --git a/tests/Testing.Databases.SqlServer.SqlCmd.Tests/TemporaryFile.cs b/tests/Testing.Databases.SqlServer.SqlCmd.Tests/TemporaryFile.cs new file mode 100644 index 0000000..b1ffd60 --- /dev/null +++ b/tests/Testing.Databases.SqlServer.SqlCmd.Tests/TemporaryFile.cs @@ -0,0 +1,40 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) P.O.S Informatique. All rights reserved. +// +//----------------------------------------------------------------------- + +namespace PosInformatique.Testing.Databases.SqlServer.Tests +{ + internal sealed class TemporaryFile : IDisposable + { + private TemporaryFile(string fileName) + { + this.FileName = fileName; + } + + public string FileName { get; } + + public static TemporaryFile Create() + { + var temporaryFileName = Path.GetTempFileName(); + + return new TemporaryFile(temporaryFileName); + } + + public void Dispose() + { + try + { + if (File.Exists(this.FileName)) + { + File.Delete(this.FileName); + } + } + catch (IOException) + { + // Ignore the errors. + } + } + } +} diff --git a/tests/Testing.Databases.SqlServer.SqlCmd.Tests/Testing.Databases.SqlServer.SqlCmd.Tests.csproj b/tests/Testing.Databases.SqlServer.SqlCmd.Tests/Testing.Databases.SqlServer.SqlCmd.Tests.csproj new file mode 100644 index 0000000..e75b07c --- /dev/null +++ b/tests/Testing.Databases.SqlServer.SqlCmd.Tests/Testing.Databases.SqlServer.SqlCmd.Tests.csproj @@ -0,0 +1,32 @@ + + + + net8.0 + + + + + PreserveNewest + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + From 88dc0188d5ec146f87902ed09ef8079e5478020c Mon Sep 17 00:00:00 2001 From: Gilles TOURREAU Date: Tue, 23 Sep 2025 09:57:56 +0200 Subject: [PATCH 06/61] Updates the README. --- README.md | 59 ++++++++++++++---- src/Testing.Databases.SqlServer.Dac/Icon.png | Bin 44371 -> 22116 bytes .../Icon.png | Bin 14391 -> 22116 bytes .../Icon.png | Bin 0 -> 22116 bytes src/Testing.Databases.SqlServer/Icon.png | Bin 44371 -> 22116 bytes .../Testing.Databases.SqlServer.csproj | 4 -- 6 files changed, 48 insertions(+), 15 deletions(-) create mode 100644 src/Testing.Databases.SqlServer.SqlCmd/Icon.png diff --git a/README.md b/README.md index c761dd4..6a96ec7 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,11 @@ # PosInformatique.Testing.Databases -[![NuGet Version](https://img.shields.io/nuget/v/PosInformatique.Testing.Databases.SqlServer?label=PosInformatique.Testing.Databases.SqlServer)](https://www.nuget.org/packages/PosInformatique.Testing.Databases.SqlServer) -[![NuGet Version](https://img.shields.io/nuget/v/PosInformatique.Testing.Databases.SqlServer.Dac?label=PosInformatique.Testing.Databases.SqlServer.Dac)](https://www.nuget.org/packages/PosInformatique.Testing.Databases.SqlServer.Dac) -[![NuGet Version](https://img.shields.io/nuget/v/PosInformatique.Testing.Databases.SqlServer.EntityFramework?label=PosInformatique.Testing.Databases.SqlServer.EntityFramework)](https://www.nuget.org/packages/PosInformatique.Testing.Databases.SqlServer.EntityFramework) +| Package | NuGet | +|---------|-------| +| PosInformatique.Testing.Databases.SqlServer | [![NuGet](https://img.shields.io/nuget/v/PosInformatique.Testing.Databases.SqlServer)](https://www.nuget.org/packages/PosInformatique.Testing.Databases.SqlServer) | +| PosInformatique.Testing.Databases.SqlServer.Dac | [![NuGet](https://img.shields.io/nuget/v/PosInformatique.Testing.Databases.SqlServer.Dac)](https://www.nuget.org/packages/PosInformatique.Testing.Databases.SqlServer.Dac) | +| PosInformatique.Testing.Databases.SqlServer.EntityFramework | [![NuGet](https://img.shields.io/nuget/v/PosInformatique.Testing.Databases.SqlServer.EntityFramework)](https://www.nuget.org/packages/PosInformatique.Testing.Databases.SqlServer.EntityFramework) | +| PosInformatique.Testing.Databases.SqlServer.SqlCmd | [![NuGet](https://img.shields.io/nuget/v/PosInformatique.Testing.Databases.SqlServer.SqlCmd)](https://www.nuget.org/packages/PosInformatique.Testing.Databases.SqlServer.SqlCmd) | **PosInformatique.Testing.Databases** is a set of tools for testing databases. It simplifies writing and executing tests, helping ensure your database and data access code are reliable and bug-free. @@ -19,7 +22,10 @@ You can also use this tools to create and run integration tests with the [Integration tests in ASP.NET Core](https://learn.microsoft.com/en-us/aspnet/core/test/integration-tests?view=aspnetcore-8.0) approach. -Since the version 2.0.0 this tools provide a comparer to compare the schema of two SQL databases. +### Main release improvements +- v2.0: This tools provide a comparer to compare the schema of two SQL databases. +- v3.0: Add new [PosInformatique.Testing.Databases.SqlServer.SqlCmd](https://www.nuget.org/packages/PosInformatique.Testing.Databases.SqlServer.SqlCmd) which allows +to deploy database using a T-SQL script with the SQL Server [sqlcmd utility](https://learn.microsoft.com/en-us/sql/tools/sqlcmd/sqlcmd-utility). ## 💡 The approach of these tools @@ -46,9 +52,10 @@ Before each test (`TestMethod` or `Fact` methods): 1. Create an empty database with the SQL schema of the application. - There are two ways to do this: - - Deploy a DACPAC file (built by a SQL Server Database project). - - Or create a database from a `DbContext` using Entity Framework. + There are three ways to do this: + - Deploy a DACPAC file (built by a SQL Server Database project) using [PosInformatique.Testing.Databases.SqlServer.Dac](https://www.nuget.org/packages/PosInformatique.Testing.Databases.SqlServer.Dac) library. + - Create a database from a `DbContext` using Entity Framework using [PosInformatique.Testing.Databases.SqlServer.EntityFramework](https://www.nuget.org/packages/PosInformatique.Testing.Databases.SqlServer.EntityFramework) library. + - Or create a database since a T-SQL script file using [PosInformatique.Testing.Databases.SqlServer.SqlCmd](https://www.nuget.org/packages/PosInformatique.Testing.Databases.SqlServer.SqlCmd) library 2. Fill the tables with the sample data needed. @@ -69,19 +76,21 @@ To perform tests of a database migration, the approach is straightforward and re 2. Create a secondary database with the targeted schema (*target database*). - There are two ways to do this: - - Deploy a DACPAC file (built by a SQL Server Database project). - - Or create a database from a `DbContext` using Entity Framework. + There are three ways to do this: + - Deploy a DACPAC file (built by a SQL Server Database project) using [PosInformatique.Testing.Databases.SqlServer.Dac](https://www.nuget.org/packages/PosInformatique.Testing.Databases.SqlServer.Dac) library. + - Create a database from a `DbContext` using Entity Framework using [PosInformatique.Testing.Databases.SqlServer.EntityFramework](https://www.nuget.org/packages/PosInformatique.Testing.Databases.SqlServer.EntityFramework) library. + - Or create a database since a T-SQL script file using [PosInformatique.Testing.Databases.SqlServer.SqlCmd](https://www.nuget.org/packages/PosInformatique.Testing.Databases.SqlServer.SqlCmd) library 3. Execute your database *migration code* on the *initial database*. Your database *migration code* can be: - A simple SQL script file. - An Entity Framework migration sets executed with the `MigrateAsync()` method. + - Or any other way that you usually use to migrate the schema of your database. 4. Compare the two databases schemas (*initial* and *target*). - If the database *migration code* works, the *initial* and *target* must have the same schema. + If the database *migration code* works, the *initial* and *target* must have **EXACTLY** the same schema. > **NB**: The initial database is not necessarily empty. It can be at a specific schema version X if we want to test the migration from version X to Y. @@ -119,6 +128,10 @@ The [PosInformatique.Testing.Databases](https://github.com/PosInformatique/PosIn - [PosInformatique.Testing.Databases.SqlServer.EntityFramework](https://www.nuget.org/packages/PosInformatique.Testing.Databases.SqlServer.EntityFramework) NuGet package which contains: - Tools to deploy a SQL Server database using a DbContext. +- [PosInformatique.Testing.Databases.SqlServer.SqlCmd](https://www.nuget.org/packages/PosInformatique.Testing.Databases.SqlServer.SqlCmd) NuGet package which contains: + - Tools to execute T-SQL script using the SQL Server [sqlcmd utility](https://learn.microsoft.com/en-us/sql/tools/sqlcmd/sqlcmd-utility). This script can be use to deploy a + SQL Server database. + ## 🚀 Samples / Demo A complete sample solution is available in this repository inside the [samples](./samples) folder. @@ -160,3 +173,27 @@ For Entity Framework migration: - [Add the NuGet packages](./docs/WriteDatabaseMigrationTest.md#add-the-nuget-packages) - [Write test to check the migration of the database](./docs/WriteDatabaseMigrationTest.md#write-test-to-check-the-migration-of-the-database) - [Check the report details of the `SqlServerDatabaseComparer` tool](./docs/WriteDatabaseMigrationTest.md#check-the-report-details-of-the-sqlserverdatabasecomparer-tool) + +## 📦 NuGet package dependency versions + +These tools rely on a minimal set of NuGet dependencies to ensure broad compatibility. +They are built for **.NET 6.0** but also work seamlessly with newer versions of .NET: + +- .NET 7.0 +- .NET 8.0 +- .NET 9.0 +- .NET 10.0 + +### Dependency versions + +All NuGet packages depend on **low baseline versions** of Microsoft libraries to remain compatible with any modern version: + +- [Microsoft.Data.SqlClient](https://www.nuget.org/packages/microsoft.data.sqlclient) >= 5.0.1 +- [Microsoft.EntityFrameworkCore](https://www.nuget.org/packages/microsoft.entityframeworkcore) >= 6.0.0 +- [Microsoft.EntityFrameworkCore.SqlServer](https://www.nuget.org/packages/microsoft.entityframeworkcore.sqlserver) >= 6.0.0 +- [Microsoft.EntityFrameworkCore.Tools](https://www.nuget.org/packages/microsoft.entityframeworkcore.tools) >= 6.0.0 +- [Microsoft.SqlServer.DacFx](https://www.nuget.org/packages/microsoft.sqlserver.dacfx) >= 162.1.172 + +### Recommendation + +We recommend using the **latest versions** of these libraries in your own projects to benefit from the most recent features, performance improvements, and security fixes. diff --git a/src/Testing.Databases.SqlServer.Dac/Icon.png b/src/Testing.Databases.SqlServer.Dac/Icon.png index 4d3e4c81690af8993690ebc636b55a9e4b4834fd..8ed8d93bcbb674d166300791a93c1a022ac8dd2d 100644 GIT binary patch literal 22116 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 literal 44371 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 literal 14391 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 EKV1c}asU7T diff --git a/src/Testing.Databases.SqlServer.SqlCmd/Icon.png b/src/Testing.Databases.SqlServer.SqlCmd/Icon.png new file mode 100644 index 0000000000000000000000000000000000000000..8ed8d93bcbb674d166300791a93c1a022ac8dd2d GIT binary patch literal 22116 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 literal 0 HcmV?d00001 diff --git a/src/Testing.Databases.SqlServer/Icon.png b/src/Testing.Databases.SqlServer/Icon.png index 4d3e4c81690af8993690ebc636b55a9e4b4834fd..8ed8d93bcbb674d166300791a93c1a022ac8dd2d 100644 GIT binary patch literal 22116 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 literal 44371 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 Jh3> - - - - From 17701de544d407772073f4a9b21396fd96f5c818 Mon Sep 17 00:00:00 2001 From: Gilles TOURREAU Date: Tue, 23 Sep 2025 10:08:19 +0200 Subject: [PATCH 07/61] Fix unit tests failed. --- .../SqlCmdSqlServerExtensionsTest.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/Testing.Databases.SqlServer.SqlCmd.Tests/SqlCmdSqlServerExtensionsTest.cs b/tests/Testing.Databases.SqlServer.SqlCmd.Tests/SqlCmdSqlServerExtensionsTest.cs index db942fe..20ec8e0 100644 --- a/tests/Testing.Databases.SqlServer.SqlCmd.Tests/SqlCmdSqlServerExtensionsTest.cs +++ b/tests/Testing.Databases.SqlServer.SqlCmd.Tests/SqlCmdSqlServerExtensionsTest.cs @@ -121,13 +121,13 @@ CREATE TABLE ErrorBlabla server.Master.Invoking(m => m.RunScript(temporaryFile.FileName, settings)) .Should().ThrowExactly() - .Which.Output.Should().Be( + .Which.Output.Should().StartWith( """ GOOOOOO ! Changed database context to 'SqlCmdSqlServerExtensionsTest_RunScript_WithErros'. - Msg 102, Level 15, State 1, Server TOURREAU-LAPTOP\LOCALDB#19CCEEF8, Line 1 - Incorrect syntax near 'ErrorBlabla'. - """); + Msg 102, Level 15, State 1, + """) + .And.EndWith("Incorrect syntax near 'ErrorBlabla'."); var database = server.GetDatabase("SqlCmdSqlServerExtensionsTest_RunScript_WithErros"); From 03bee0ef4d56dca61fdd75ac2adf1bf2bc41264d Mon Sep 17 00:00:00 2001 From: Gilles TOURREAU Date: Tue, 23 Sep 2025 10:10:31 +0200 Subject: [PATCH 08/61] Fix build previously failed. --- .../Testing.Databases.SqlServer.csproj | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Testing.Databases.SqlServer/Testing.Databases.SqlServer.csproj b/src/Testing.Databases.SqlServer/Testing.Databases.SqlServer.csproj index 1dbeddb..cbd5830 100644 --- a/src/Testing.Databases.SqlServer/Testing.Databases.SqlServer.csproj +++ b/src/Testing.Databases.SqlServer/Testing.Databases.SqlServer.csproj @@ -12,6 +12,10 @@ + + + + From 8ad39fd6c37b0d20d06679e339c03e0a7f14ddea Mon Sep 17 00:00:00 2001 From: Gilles TOURREAU Date: Wed, 24 Sep 2025 06:11:10 +0200 Subject: [PATCH 09/61] Add guard for public methods in SqlCmd project. --- src/Directory.Build.props | 7 +++ .../SqlCmdDatabaseInitializer.cs | 13 ++++++ .../SqlCmdProcess.cs | 4 +- .../SqlCmdSqlServerDatabaseExtensions.cs | 11 +++++ .../SqlCmdDatabaseInitializerTest.cs | 43 +++++++++++++++++++ .../SqlCmdRunScriptSettingsTest.cs | 19 ++++++++ .../SqlCmdSqlServerExtensionsTest.cs | 33 ++++++++++++++ 7 files changed, 128 insertions(+), 2 deletions(-) create mode 100644 tests/Testing.Databases.SqlServer.SqlCmd.Tests/SqlCmdRunScriptSettingsTest.cs diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 26a5ca9..135a517 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -11,6 +11,13 @@ README.md MIT + 3.0.0 + - Add new PosInformatique.Testing.Databases.SqlCmd to initialize database using sqlcmd utility. + - For DACPAC deployment or database creation it is now possible to specify the location of the data and log files. + + 2.3.0 + - Add the support to compare the seed and increment for the IDENTITY columns + 2.2.0 - Add SqlServerDatabase.ClearDataAsync() method. - Add SqlServer.CreateDatabase() method to create database from an Entity Framework DbContext. diff --git a/src/Testing.Databases.SqlServer.SqlCmd/SqlCmdDatabaseInitializer.cs b/src/Testing.Databases.SqlServer.SqlCmd/SqlCmdDatabaseInitializer.cs index c58f858..8c45055 100644 --- a/src/Testing.Databases.SqlServer.SqlCmd/SqlCmdDatabaseInitializer.cs +++ b/src/Testing.Databases.SqlServer.SqlCmd/SqlCmdDatabaseInitializer.cs @@ -26,9 +26,22 @@ public static class SqlCmdDatabaseInitializer /// Full path of the T-SQL file to execute. /// Connection string to the SQL Server with administrator rights. /// Additionnal settings to run the sqlcmd tool. + /// If the specified argument is . + /// If the specified argument is . + /// If the specified argument is . + /// If no file exists with the specified argument. /// An instance of the which allows to perform query to initialize the data. public static SqlServerDatabase Initialize(this SqlServerDatabaseInitializer initializer, string fileName, string connectionString, SqlCmdRunScriptSettings? settings = null) { + ArgumentNullException.ThrowIfNull(initializer, nameof(initializer)); + ArgumentNullException.ThrowIfNull(fileName, nameof(fileName)); + ArgumentNullException.ThrowIfNull(connectionString, nameof(connectionString)); + + if (!File.Exists(fileName)) + { + throw new FileNotFoundException($"Could not find file '{fileName}'", fileName); + } + var connectionStringBuilder = new SqlConnectionStringBuilder(connectionString); var server = new SqlServer(connectionString); diff --git a/src/Testing.Databases.SqlServer.SqlCmd/SqlCmdProcess.cs b/src/Testing.Databases.SqlServer.SqlCmd/SqlCmdProcess.cs index 2b66916..567f1ef 100644 --- a/src/Testing.Databases.SqlServer.SqlCmd/SqlCmdProcess.cs +++ b/src/Testing.Databases.SqlServer.SqlCmd/SqlCmdProcess.cs @@ -11,9 +11,9 @@ namespace PosInformatique.Testing.Databases.SqlServer internal sealed class SqlCmdProcess : IDisposable { - private Process? process; + private readonly List output; - private List output; + private Process? process; private SqlCmdProcess(string arguments) { diff --git a/src/Testing.Databases.SqlServer.SqlCmd/SqlCmdSqlServerDatabaseExtensions.cs b/src/Testing.Databases.SqlServer.SqlCmd/SqlCmdSqlServerDatabaseExtensions.cs index e110a14..7c6920d 100644 --- a/src/Testing.Databases.SqlServer.SqlCmd/SqlCmdSqlServerDatabaseExtensions.cs +++ b/src/Testing.Databases.SqlServer.SqlCmd/SqlCmdSqlServerDatabaseExtensions.cs @@ -19,10 +19,21 @@ public static class SqlCmdSqlServerDatabaseExtensions /// where the script will be executed on. /// T-SQL script to execute on the . /// Additional settings to run the script. + /// If the specified argument is . + /// If the specified argument is . + /// If no file exists with the specified argument. /// If an error has been occured when running the T-SQL script. Check the /// to retrieve the output result of the script execution. public static void RunScript(this SqlServerDatabase database, string fileName, SqlCmdRunScriptSettings? settings = null) { + ArgumentNullException.ThrowIfNull(database, nameof(database)); + ArgumentNullException.ThrowIfNull(fileName, nameof(fileName)); + + if (!File.Exists(fileName)) + { + throw new FileNotFoundException($"Could not find file '{fileName}'", fileName); + } + if (settings is null) { settings = new SqlCmdRunScriptSettings(); diff --git a/tests/Testing.Databases.SqlServer.SqlCmd.Tests/SqlCmdDatabaseInitializerTest.cs b/tests/Testing.Databases.SqlServer.SqlCmd.Tests/SqlCmdDatabaseInitializerTest.cs index 0c4822d..f606cc5 100644 --- a/tests/Testing.Databases.SqlServer.SqlCmd.Tests/SqlCmdDatabaseInitializerTest.cs +++ b/tests/Testing.Databases.SqlServer.SqlCmd.Tests/SqlCmdDatabaseInitializerTest.cs @@ -129,5 +129,48 @@ public async Task Test2Async() // Insert a row which should not be use in other tests. await this.database.InsertIntoAsync("MyTable", new { Id = 99, Name = "Should not be here for the next test" }); } + + [Fact] + public void Initialize_WithDatabaseArgumentNull() + { + var act = () => + { + SqlCmdDatabaseInitializer.Initialize(null, default, default); + }; + + act.Should().ThrowExactly() + .WithParameterName("initializer"); + } + + [Fact] + public void Initialize_WithFileNameArgumentNull() + { + var initializer = new SqlServerDatabaseInitializer(); + + initializer.Invoking(i => i.Initialize(null, default)) + .Should().ThrowExactly() + .WithParameterName("fileName"); + } + + [Fact] + public void Initialize_WithConnectionStringArgumentNull() + { + var initializer = new SqlServerDatabaseInitializer(); + + initializer.Invoking(i => i.Initialize("The file name", null)) + .Should().ThrowExactly() + .WithParameterName("connectionString"); + } + + [Fact] + public void Initialize_WithFileNotFound() + { + var initializer = new SqlServerDatabaseInitializer(); + + initializer.Invoking(i => i.Initialize("C:/Directory/FileNotFound.sql", "The connection stirng")) + .Should().ThrowExactly() + .WithMessage("Could not find file 'C:/Directory/FileNotFound.sql'") + .Which.FileName.Should().Be("C:/Directory/FileNotFound.sql"); + } } } \ No newline at end of file diff --git a/tests/Testing.Databases.SqlServer.SqlCmd.Tests/SqlCmdRunScriptSettingsTest.cs b/tests/Testing.Databases.SqlServer.SqlCmd.Tests/SqlCmdRunScriptSettingsTest.cs new file mode 100644 index 0000000..2b4c560 --- /dev/null +++ b/tests/Testing.Databases.SqlServer.SqlCmd.Tests/SqlCmdRunScriptSettingsTest.cs @@ -0,0 +1,19 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) P.O.S Informatique. All rights reserved. +// +//----------------------------------------------------------------------- + +namespace PosInformatique.Testing.Databases.SqlServer.Tests +{ + public class SqlCmdRunScriptSettingsTest + { + [Fact] + public void Constructor() + { + var settings = new SqlCmdRunScriptSettings(); + + settings.Variables.Should().BeEmpty(); + } + } +} \ No newline at end of file diff --git a/tests/Testing.Databases.SqlServer.SqlCmd.Tests/SqlCmdSqlServerExtensionsTest.cs b/tests/Testing.Databases.SqlServer.SqlCmd.Tests/SqlCmdSqlServerExtensionsTest.cs index 20ec8e0..a3d8ab4 100644 --- a/tests/Testing.Databases.SqlServer.SqlCmd.Tests/SqlCmdSqlServerExtensionsTest.cs +++ b/tests/Testing.Databases.SqlServer.SqlCmd.Tests/SqlCmdSqlServerExtensionsTest.cs @@ -135,5 +135,38 @@ Changed database context to 'SqlCmdSqlServerExtensionsTest_RunScript_WithErros'. table.Rows.Should().BeEmpty(); } + + [Fact] + public void RunScript_WithDatabaseArgumentNull() + { + var act = () => + { + SqlCmdSqlServerDatabaseExtensions.RunScript(null, default, default); + }; + + act.Should().ThrowExactly() + .WithParameterName("database"); + } + + [Fact] + public void RunScript_WithFileNameArgumentNull() + { + var server = new SqlServer(ConnectionString); + + server.Master.Invoking(m => m.RunScript(null, default)) + .Should().ThrowExactly() + .WithParameterName("fileName"); + } + + [Fact] + public void RunScript_WithFileNotFound() + { + var server = new SqlServer(ConnectionString); + + server.Master.Invoking(m => m.RunScript("C:/Directory/FileNotFound.sql", default)) + .Should().ThrowExactly() + .WithMessage("Could not find file 'C:/Directory/FileNotFound.sql'") + .Which.FileName.Should().Be("C:/Directory/FileNotFound.sql"); + } } } \ No newline at end of file From fe3c796c18ee95a0c26ad7406b60fe1a3defd89c Mon Sep 17 00:00:00 2001 From: Gilles TOURREAU Date: Wed, 24 Sep 2025 06:20:09 +0200 Subject: [PATCH 10/61] Add the output in the exception message when Sqlcmd execution failed. --- .../SqlCmdSqlServerDatabaseExtensions.cs | 2 +- .../SqlCmdSqlServerExtensionsTest.cs | 9 ++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/Testing.Databases.SqlServer.SqlCmd/SqlCmdSqlServerDatabaseExtensions.cs b/src/Testing.Databases.SqlServer.SqlCmd/SqlCmdSqlServerDatabaseExtensions.cs index 7c6920d..2ac1f1a 100644 --- a/src/Testing.Databases.SqlServer.SqlCmd/SqlCmdSqlServerDatabaseExtensions.cs +++ b/src/Testing.Databases.SqlServer.SqlCmd/SqlCmdSqlServerDatabaseExtensions.cs @@ -47,7 +47,7 @@ public static void RunScript(this SqlServerDatabase database, string fileName, S if (exitCode != 0) { - throw new SqlCmdException($"Some errors has been occurred when executing the '{fileName}'. Check the {nameof(SqlCmdException.Output)} property of the exception to retrieve the output of the sqlcmd utility.", sqlCmdProcess.Output); + throw new SqlCmdException($"Some errors has been occurred when executing the '{fileName}'.{Environment.NewLine}{Environment.NewLine}-- Output --{Environment.NewLine}{sqlCmdProcess.Output}", sqlCmdProcess.Output); } } } diff --git a/tests/Testing.Databases.SqlServer.SqlCmd.Tests/SqlCmdSqlServerExtensionsTest.cs b/tests/Testing.Databases.SqlServer.SqlCmd.Tests/SqlCmdSqlServerExtensionsTest.cs index a3d8ab4..cd60c0f 100644 --- a/tests/Testing.Databases.SqlServer.SqlCmd.Tests/SqlCmdSqlServerExtensionsTest.cs +++ b/tests/Testing.Databases.SqlServer.SqlCmd.Tests/SqlCmdSqlServerExtensionsTest.cs @@ -119,9 +119,10 @@ public void RunScript_WithErros() CREATE TABLE ErrorBlabla """); - server.Master.Invoking(m => m.RunScript(temporaryFile.FileName, settings)) - .Should().ThrowExactly() - .Which.Output.Should().StartWith( + var exception = server.Master.Invoking(m => m.RunScript(temporaryFile.FileName, settings)) + .Should().ThrowExactly(); + + exception.Which.Output.Should().StartWith( """ GOOOOOO ! Changed database context to 'SqlCmdSqlServerExtensionsTest_RunScript_WithErros'. @@ -129,6 +130,8 @@ Changed database context to 'SqlCmdSqlServerExtensionsTest_RunScript_WithErros'. """) .And.EndWith("Incorrect syntax near 'ErrorBlabla'."); + exception.Which.Message.Should().Be($"Some errors has been occurred when executing the '{temporaryFile.FileName}'.{Environment.NewLine}{Environment.NewLine}-- Output --{Environment.NewLine}{exception.Which.Output}"); + var database = server.GetDatabase("SqlCmdSqlServerExtensionsTest_RunScript_WithErros"); var table = database.ExecuteQuery("SELECT * FROM sys.tables"); From f1eec4f4609ec251a0de7af8c99d04ed684d2389 Mon Sep 17 00:00:00 2001 From: Gilles TOURREAU Date: Wed, 24 Sep 2025 09:11:46 +0200 Subject: [PATCH 11/61] Add guard to public APIs. --- PosInformatique.Testing.Databases.sln | 8 +++ README.md | 19 ++++-- .../SqlServerDacDatabaseInitializer.cs | 13 ++++ .../SqlServerDacExtensions.cs | 13 ++++ .../Testing.Databases.SqlServer.Dac.csproj | 2 + ...yFrameworkDatabaseInitializerExtensions.cs | 5 ++ .../EntityFrameworkSqlServerExtensions.cs | 14 ++++ ...Databases.SqlServer.EntityFramework.csproj | 2 + .../Guard.cs | 19 ++++++ ...sting.Databases.SqlServer.Shared.projitems | 14 ++++ .../Testing.Databases.SqlServer.Shared.shproj | 13 ++++ .../SqlCmdDatabaseInitializer.cs | 6 +- .../Testing.Databases.SqlServer.SqlCmd.csproj | 4 +- .../SqlServerDacExtensionsTest.cs | 43 +++++++++++++ .../SqlServerDatabaseInitializerTest.cs | 43 +++++++++++++ ...meworkDatabaseInitializerExtensionsTest.cs | 22 +++++++ .../EntityFrameworkSqlServerExtensionsTest.cs | 64 +++++++++++++++++++ .../SqlCmdDatabaseInitializerTest.cs | 2 +- 18 files changed, 295 insertions(+), 11 deletions(-) create mode 100644 src/Testing.Databases.SqlServer.Shared/Guard.cs create mode 100644 src/Testing.Databases.SqlServer.Shared/Testing.Databases.SqlServer.Shared.projitems create mode 100644 src/Testing.Databases.SqlServer.Shared/Testing.Databases.SqlServer.Shared.shproj diff --git a/PosInformatique.Testing.Databases.sln b/PosInformatique.Testing.Databases.sln index 19170f9..3ff08cd 100644 --- a/PosInformatique.Testing.Databases.sln +++ b/PosInformatique.Testing.Databases.sln @@ -63,6 +63,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testing.Databases.SqlServer EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testing.Databases.SqlServer.SqlCmd.Tests", "tests\Testing.Databases.SqlServer.SqlCmd.Tests\Testing.Databases.SqlServer.SqlCmd.Tests.csproj", "{F8E025D7-4E2F-437A-ABFA-C43A1368E15A}" EndProject +Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "Testing.Databases.SqlServer.Shared", "src\Testing.Databases.SqlServer.Shared\Testing.Databases.SqlServer.Shared.shproj", "{B9F8C52D-4652-4FC6-A695-DC2F61BA05C9}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -133,4 +135,10 @@ Global GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {FAC64573-D665-48A8-AC75-7B82B692EC9E} EndGlobalSection + GlobalSection(SharedMSBuildProjectFiles) = preSolution + src\Testing.Databases.SqlServer.Shared\Testing.Databases.SqlServer.Shared.projitems*{157ddf0d-9410-4646-94b9-9cee4c140f5e}*SharedItemsImports = 5 + src\Testing.Databases.SqlServer.Shared\Testing.Databases.SqlServer.Shared.projitems*{8be60460-eba5-43de-b85d-c756e2988dc8}*SharedItemsImports = 5 + src\Testing.Databases.SqlServer.Shared\Testing.Databases.SqlServer.Shared.projitems*{b9f8c52d-4652-4fc6-a695-dc2f61ba05c9}*SharedItemsImports = 13 + src\Testing.Databases.SqlServer.Shared\Testing.Databases.SqlServer.Shared.projitems*{d3004122-ccdd-4ead-bd9e-da6dff470943}*SharedItemsImports = 5 + EndGlobalSection EndGlobal diff --git a/README.md b/README.md index 6a96ec7..4b9ed3a 100644 --- a/README.md +++ b/README.md @@ -177,12 +177,19 @@ For Entity Framework migration: ## 📦 NuGet package dependency versions These tools rely on a minimal set of NuGet dependencies to ensure broad compatibility. -They are built for **.NET 6.0** but also work seamlessly with newer versions of .NET: - -- .NET 7.0 -- .NET 8.0 -- .NET 9.0 -- .NET 10.0 +They are built for **.NET Core 6.0** and **.NET Framework 4.6.2** but also work seamlessly with newer versions of .NET: + +- .NET Framework 4.6.2 +- .NET Framework 4.7 +- .NET Framework 4.7.1 +- .NET Framework 4.7.2 +- .NET Framework 4.8 +- .NET Framework 4.8.1 +- .NET Core 6.0 +- .NET Core 7.0 +- .NET Core 8.0 +- .NET Core 9.0 +- .NET Core 10.0 ### Dependency versions diff --git a/src/Testing.Databases.SqlServer.Dac/SqlServerDacDatabaseInitializer.cs b/src/Testing.Databases.SqlServer.Dac/SqlServerDacDatabaseInitializer.cs index 76c3ae1..9528978 100644 --- a/src/Testing.Databases.SqlServer.Dac/SqlServerDacDatabaseInitializer.cs +++ b/src/Testing.Databases.SqlServer.Dac/SqlServerDacDatabaseInitializer.cs @@ -24,9 +24,22 @@ public static class SqlServerDacDatabaseInitializer /// Full path of the DACPAC file. /// Connection string to the SQL Server with administrator rights. /// Additionnal settings for the DACPAC to deploy. + /// If the specified argument is . + /// If the specified argument is . + /// If the specified argument is . + /// If no file exists with the specified argument. /// An instance of the which allows to perform query to initialize the data. public static SqlServerDatabase Initialize(this SqlServerDatabaseInitializer initializer, string packageName, string connectionString, SqlServerDacDeploymentSettings? settings = null) { + Guard.ThrowIfNull(initializer, nameof(initializer)); + Guard.ThrowIfNull(packageName, nameof(packageName)); + Guard.ThrowIfNull(connectionString, nameof(connectionString)); + + if (!File.Exists(packageName)) + { + throw new FileNotFoundException($"Could not find file '{packageName}'", packageName); + } + var connectionStringBuilder = new SqlConnectionStringBuilder(connectionString); var server = new SqlServer(connectionString); diff --git a/src/Testing.Databases.SqlServer.Dac/SqlServerDacExtensions.cs b/src/Testing.Databases.SqlServer.Dac/SqlServerDacExtensions.cs index 17850d5..d72cdb8 100644 --- a/src/Testing.Databases.SqlServer.Dac/SqlServerDacExtensions.cs +++ b/src/Testing.Databases.SqlServer.Dac/SqlServerDacExtensions.cs @@ -22,9 +22,22 @@ public static class SqlServerDacExtensions /// File name (including the path) for the DACPAC file to deploy. /// Name of the database which will be created. /// Additional settings of the database to deploy. + /// If the specified argument is . + /// If the specified argument is . + /// If the specified argument is . + /// If no file exists with the specified argument. /// An instance of the which represents the deployed database. public static SqlServerDatabase DeployDacPackage(this SqlServer server, string fileName, string databaseName, SqlServerDacDeploymentSettings? settings = null) { + Guard.ThrowIfNull(server, nameof(server)); + Guard.ThrowIfNull(fileName, nameof(fileName)); + Guard.ThrowIfNull(databaseName, nameof(databaseName)); + + if (!File.Exists(fileName)) + { + throw new FileNotFoundException($"Could not find file '{fileName}'", fileName); + } + using (var package = DacPackage.Load(fileName)) { // Currently DacFx does not support to define explicitly the location of the database files. diff --git a/src/Testing.Databases.SqlServer.Dac/Testing.Databases.SqlServer.Dac.csproj b/src/Testing.Databases.SqlServer.Dac/Testing.Databases.SqlServer.Dac.csproj index d563a67..d902c1b 100644 --- a/src/Testing.Databases.SqlServer.Dac/Testing.Databases.SqlServer.Dac.csproj +++ b/src/Testing.Databases.SqlServer.Dac/Testing.Databases.SqlServer.Dac.csproj @@ -24,4 +24,6 @@ + + diff --git a/src/Testing.Databases.SqlServer.EntityFramework/EntityFrameworkDatabaseInitializerExtensions.cs b/src/Testing.Databases.SqlServer.EntityFramework/EntityFrameworkDatabaseInitializerExtensions.cs index 7d9db85..450b276 100644 --- a/src/Testing.Databases.SqlServer.EntityFramework/EntityFrameworkDatabaseInitializerExtensions.cs +++ b/src/Testing.Databases.SqlServer.EntityFramework/EntityFrameworkDatabaseInitializerExtensions.cs @@ -21,8 +21,13 @@ public static class EntityFrameworkDatabaseInitializerExtensions /// which the initialization will be perform on. /// Instance of the which represents the database schema to initialize. /// An instance of the which allows to perform query to initialize the data. + /// If the specified argument is . + /// If the specified argument is . public static SqlServerDatabase Initialize(this SqlServerDatabaseInitializer initializer, DbContext context) { + Guard.ThrowIfNull(initializer, nameof(initializer)); + Guard.ThrowIfNull(context, nameof(context)); + var connectionString = context.Database.GetConnectionString(); var connectionStringBuilder = new SqlConnectionStringBuilder(connectionString); diff --git a/src/Testing.Databases.SqlServer.EntityFramework/EntityFrameworkSqlServerExtensions.cs b/src/Testing.Databases.SqlServer.EntityFramework/EntityFrameworkSqlServerExtensions.cs index ace72cc..4525608 100644 --- a/src/Testing.Databases.SqlServer.EntityFramework/EntityFrameworkSqlServerExtensions.cs +++ b/src/Testing.Databases.SqlServer.EntityFramework/EntityFrameworkSqlServerExtensions.cs @@ -21,9 +21,16 @@ public static class EntityFrameworkSqlServerExtensions /// instance where the database will be created. /// Name of the database to create. /// which represents the database to create. + /// If the specified argument is . + /// If the specified argument is . + /// If the specified argument is . /// An instance of the which represents the deployed database. public static SqlServerDatabase CreateDatabase(this SqlServer server, string name, DbContext context) { + Guard.ThrowIfNull(server, nameof(server)); + Guard.ThrowIfNull(name, nameof(name)); + Guard.ThrowIfNull(context, nameof(context)); + var database = server.GetDatabase(name); context.Database.SetConnectionString(database.ConnectionString); @@ -41,9 +48,16 @@ public static SqlServerDatabase CreateDatabase(this SqlServer server, string nam /// instance where the database will be created. /// Name of the database to create. /// which represents the database to create. + /// If the specified argument is . + /// If the specified argument is . + /// If the specified argument is . /// A which represents the asynchronous operation and contains an instance of the that represents the deployed database. public static async Task CreateDatabaseAsync(this SqlServer server, string name, DbContext context) { + Guard.ThrowIfNull(server, nameof(server)); + Guard.ThrowIfNull(name, nameof(name)); + Guard.ThrowIfNull(context, nameof(context)); + var database = server.GetDatabase(name); context.Database.SetConnectionString(database.ConnectionString); diff --git a/src/Testing.Databases.SqlServer.EntityFramework/Testing.Databases.SqlServer.EntityFramework.csproj b/src/Testing.Databases.SqlServer.EntityFramework/Testing.Databases.SqlServer.EntityFramework.csproj index 5c8d46d..10e7a89 100644 --- a/src/Testing.Databases.SqlServer.EntityFramework/Testing.Databases.SqlServer.EntityFramework.csproj +++ b/src/Testing.Databases.SqlServer.EntityFramework/Testing.Databases.SqlServer.EntityFramework.csproj @@ -24,4 +24,6 @@ + + diff --git a/src/Testing.Databases.SqlServer.Shared/Guard.cs b/src/Testing.Databases.SqlServer.Shared/Guard.cs new file mode 100644 index 0000000..cec2e0f --- /dev/null +++ b/src/Testing.Databases.SqlServer.Shared/Guard.cs @@ -0,0 +1,19 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) P.O.S Informatique. All rights reserved. +// +//----------------------------------------------------------------------- + +namespace PosInformatique +{ + internal static class Guard + { + public static void ThrowIfNull(object? value, string paramName) + { + if (value is null) + { + throw new ArgumentNullException(paramName); + } + } + } +} diff --git a/src/Testing.Databases.SqlServer.Shared/Testing.Databases.SqlServer.Shared.projitems b/src/Testing.Databases.SqlServer.Shared/Testing.Databases.SqlServer.Shared.projitems new file mode 100644 index 0000000..f3b3b69 --- /dev/null +++ b/src/Testing.Databases.SqlServer.Shared/Testing.Databases.SqlServer.Shared.projitems @@ -0,0 +1,14 @@ + + + + $(MSBuildAllProjects);$(MSBuildThisFileFullPath) + true + b9f8c52d-4652-4fc6-a695-dc2f61ba05c9 + + + Testing.Databases.SqlServer.Shared + + + + + \ No newline at end of file diff --git a/src/Testing.Databases.SqlServer.Shared/Testing.Databases.SqlServer.Shared.shproj b/src/Testing.Databases.SqlServer.Shared/Testing.Databases.SqlServer.Shared.shproj new file mode 100644 index 0000000..572486d --- /dev/null +++ b/src/Testing.Databases.SqlServer.Shared/Testing.Databases.SqlServer.Shared.shproj @@ -0,0 +1,13 @@ + + + + b9f8c52d-4652-4fc6-a695-dc2f61ba05c9 + 14.0 + + + + + + + + diff --git a/src/Testing.Databases.SqlServer.SqlCmd/SqlCmdDatabaseInitializer.cs b/src/Testing.Databases.SqlServer.SqlCmd/SqlCmdDatabaseInitializer.cs index 8c45055..676dbf8 100644 --- a/src/Testing.Databases.SqlServer.SqlCmd/SqlCmdDatabaseInitializer.cs +++ b/src/Testing.Databases.SqlServer.SqlCmd/SqlCmdDatabaseInitializer.cs @@ -33,9 +33,9 @@ public static class SqlCmdDatabaseInitializer /// An instance of the which allows to perform query to initialize the data. public static SqlServerDatabase Initialize(this SqlServerDatabaseInitializer initializer, string fileName, string connectionString, SqlCmdRunScriptSettings? settings = null) { - ArgumentNullException.ThrowIfNull(initializer, nameof(initializer)); - ArgumentNullException.ThrowIfNull(fileName, nameof(fileName)); - ArgumentNullException.ThrowIfNull(connectionString, nameof(connectionString)); + Guard.ThrowIfNull(initializer, nameof(initializer)); + Guard.ThrowIfNull(fileName, nameof(fileName)); + Guard.ThrowIfNull(connectionString, nameof(connectionString)); if (!File.Exists(fileName)) { diff --git a/src/Testing.Databases.SqlServer.SqlCmd/Testing.Databases.SqlServer.SqlCmd.csproj b/src/Testing.Databases.SqlServer.SqlCmd/Testing.Databases.SqlServer.SqlCmd.csproj index 5373ed1..b96a39c 100644 --- a/src/Testing.Databases.SqlServer.SqlCmd/Testing.Databases.SqlServer.SqlCmd.csproj +++ b/src/Testing.Databases.SqlServer.SqlCmd/Testing.Databases.SqlServer.SqlCmd.csproj @@ -1,7 +1,7 @@  - net6.0 + net6.0;net462 True Testing.Databases.SqlServer.SqlCmd is a library that contains a set of tools for testing Data Access Layer using SQLCMD tool to initialize the database with a SQL script. @@ -20,4 +20,6 @@ + + diff --git a/tests/Testing.Databases.SqlServer.Dac.Tests/SqlServerDacExtensionsTest.cs b/tests/Testing.Databases.SqlServer.Dac.Tests/SqlServerDacExtensionsTest.cs index 54e5824..db76e8d 100644 --- a/tests/Testing.Databases.SqlServer.Dac.Tests/SqlServerDacExtensionsTest.cs +++ b/tests/Testing.Databases.SqlServer.Dac.Tests/SqlServerDacExtensionsTest.cs @@ -84,6 +84,49 @@ public void DeployDacPackage_WithSpecificDataFileName() server.DeleteDatabase("SqlServerDacExtensionsTest_DeployDacPackage_WithSpecificDataFileName"); } + [Fact] + public void DeployDacPackage_WithDatabaseArgumentNull() + { + var act = () => + { + SqlServerDacExtensions.DeployDacPackage(null, default, default); + }; + + act.Should().ThrowExactly() + .WithParameterName("server"); + } + + [Fact] + public void DeployDacPackage_WithFileNameArgumentNull() + { + var server = new SqlServer(ConnectionString); + + server.Invoking(s => s.DeployDacPackage(null, default)) + .Should().ThrowExactly() + .WithParameterName("fileName"); + } + + [Fact] + public void DeployDacPackage_WithDatabaseNameArgumentNull() + { + var server = new SqlServer(ConnectionString); + + server.Invoking(s => s.DeployDacPackage("C:/Directory/FileNotFound.sql", null)) + .Should().ThrowExactly() + .WithParameterName("databaseName"); + } + + [Fact] + public void DeployDacPackage_WithFileNotFound() + { + var server = new SqlServer(ConnectionString); + + server.Invoking(s => s.DeployDacPackage("C:/Directory/FileNotFound.sql", "The database")) + .Should().ThrowExactly() + .WithMessage("Could not find file 'C:/Directory/FileNotFound.sql'") + .Which.FileName.Should().Be("C:/Directory/FileNotFound.sql"); + } + private static void CreateDatabase(string name) { var server = new SqlServer(ConnectionString); diff --git a/tests/Testing.Databases.SqlServer.Dac.Tests/SqlServerDatabaseInitializerTest.cs b/tests/Testing.Databases.SqlServer.Dac.Tests/SqlServerDatabaseInitializerTest.cs index aeaddb7..044f00a 100644 --- a/tests/Testing.Databases.SqlServer.Dac.Tests/SqlServerDatabaseInitializerTest.cs +++ b/tests/Testing.Databases.SqlServer.Dac.Tests/SqlServerDatabaseInitializerTest.cs @@ -168,6 +168,49 @@ public void Initialize_WithSpecificDataFileName() server.DeleteDatabase("SqlServerDatabaseInitializerTest_Initialize_WithSpecificDataFileName"); } + [Fact] + public void Initialize_WithInitializerArgumentNull() + { + var act = () => + { + SqlServerDacDatabaseInitializer.Initialize(null, default, default); + }; + + act.Should().ThrowExactly() + .WithParameterName("initializer"); + } + + [Fact] + public void Initialize_WithFileNameArgumentNull() + { + var initializer = new SqlServerDatabaseInitializer(); + + initializer.Invoking(i => i.Initialize(null, default)) + .Should().ThrowExactly() + .WithParameterName("packageName"); + } + + [Fact] + public void Initialize_WithConnectionStringArgumentNull() + { + var initializer = new SqlServerDatabaseInitializer(); + + initializer.Invoking(i => i.Initialize("The file name", null)) + .Should().ThrowExactly() + .WithParameterName("connectionString"); + } + + [Fact] + public void Initialize_WithPackageNotFound() + { + var initializer = new SqlServerDatabaseInitializer(); + + initializer.Invoking(i => i.Initialize("C:/Directory/FileNotFound.sql", "The connection stirng")) + .Should().ThrowExactly() + .WithMessage("Could not find file 'C:/Directory/FileNotFound.sql'") + .Which.FileName.Should().Be("C:/Directory/FileNotFound.sql"); + } + private static void CreateDatabase(string name) { var server = new SqlServer(ConnectionString); diff --git a/tests/Testing.Databases.SqlServer.EntityFramework.Tests/EntityFrameworkDatabaseInitializerExtensionsTest.cs b/tests/Testing.Databases.SqlServer.EntityFramework.Tests/EntityFrameworkDatabaseInitializerExtensionsTest.cs index 4ab4d63..9640452 100644 --- a/tests/Testing.Databases.SqlServer.EntityFramework.Tests/EntityFrameworkDatabaseInitializerExtensionsTest.cs +++ b/tests/Testing.Databases.SqlServer.EntityFramework.Tests/EntityFrameworkDatabaseInitializerExtensionsTest.cs @@ -74,6 +74,28 @@ public void Test2() this.database.InsertInto("MyTable", new { Id = 99, Name = "Should not be here for the next test" }); } + [Fact] + public void Initialize_WithInitializerArgumentNull() + { + var act = () => + { + EntityFrameworkDatabaseInitializerExtensions.Initialize(null, default); + }; + + act.Should().ThrowExactly() + .WithParameterName("initializer"); + } + + [Fact] + public void Initialize_WithFileNameArgumentNull() + { + var initializer = new SqlServerDatabaseInitializer(); + + initializer.Invoking(i => i.Initialize(null)) + .Should().ThrowExactly() + .WithParameterName("context"); + } + private sealed class DbContextTest : DbContext { public DbContextTest(DbContextOptions options) diff --git a/tests/Testing.Databases.SqlServer.EntityFramework.Tests/EntityFrameworkSqlServerExtensionsTest.cs b/tests/Testing.Databases.SqlServer.EntityFramework.Tests/EntityFrameworkSqlServerExtensionsTest.cs index 1321b05..c54424b 100644 --- a/tests/Testing.Databases.SqlServer.EntityFramework.Tests/EntityFrameworkSqlServerExtensionsTest.cs +++ b/tests/Testing.Databases.SqlServer.EntityFramework.Tests/EntityFrameworkSqlServerExtensionsTest.cs @@ -121,6 +121,70 @@ public async Task CreateAsync_WithAlreadyExistingDatabase() tables[0].Columns[1].Name.Should().Be("Name"); } + [Fact] + public void CreateDatabase_WithServerNull() + { + var act = () => + { + EntityFrameworkSqlServerExtensions.CreateDatabase(null, default, default); + }; + + act.Should().ThrowExactly() + .WithParameterName("server"); + } + + [Fact] + public void CreateDatabase_WithNameArgumentNull() + { + var server = new SqlServer(ConnectionString); + + server.Invoking(s => s.CreateDatabase(null, default)) + .Should().ThrowExactly() + .WithParameterName("name"); + } + + [Fact] + public void CreateDatabase_WithContextArgumentNull() + { + var server = new SqlServer(ConnectionString); + + server.Invoking(s => s.CreateDatabase("The name", default)) + .Should().ThrowExactly() + .WithParameterName("context"); + } + + [Fact] + public void CreateDatabaseAsync_WithServerNull() + { + var act = async () => + { + await EntityFrameworkSqlServerExtensions.CreateDatabaseAsync(null, default, default); + }; + + act.Should().ThrowExactlyAsync() + .WithParameterName("server"); + } + + [Fact] + public void CreateDatabaseAsync_WithNameArgumentNull() + { + var server = new SqlServer(ConnectionString); + + server.Invoking(s => s.CreateDatabaseAsync(null, default)) + .Should().ThrowExactlyAsync() + .WithParameterName("name"); + } + + [Fact] + public void CreateDatabaseAsync_WithContextArgumentNull() + { + var server = new SqlServer(ConnectionString); + + server.Invoking(s => s.CreateDatabaseAsync("The name", default)) + .Should().ThrowExactlyAsync() + .WithParameterName("context"); + } + private sealed class DbContextTest : DbContext { public DbContextTest(DbContextOptions options) diff --git a/tests/Testing.Databases.SqlServer.SqlCmd.Tests/SqlCmdDatabaseInitializerTest.cs b/tests/Testing.Databases.SqlServer.SqlCmd.Tests/SqlCmdDatabaseInitializerTest.cs index f606cc5..c134a86 100644 --- a/tests/Testing.Databases.SqlServer.SqlCmd.Tests/SqlCmdDatabaseInitializerTest.cs +++ b/tests/Testing.Databases.SqlServer.SqlCmd.Tests/SqlCmdDatabaseInitializerTest.cs @@ -131,7 +131,7 @@ public async Task Test2Async() } [Fact] - public void Initialize_WithDatabaseArgumentNull() + public void Initialize_WithInitializerArgumentNull() { var act = () => { From f53f4af0b67c268fc41cc86898ec282e419e3233 Mon Sep 17 00:00:00 2001 From: Gilles TOURREAU Date: Wed, 24 Sep 2025 11:51:06 +0200 Subject: [PATCH 12/61] Fix unit tests previously failed. --- .../SqlCmdSqlServerDatabaseExtensions.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Testing.Databases.SqlServer.SqlCmd/SqlCmdSqlServerDatabaseExtensions.cs b/src/Testing.Databases.SqlServer.SqlCmd/SqlCmdSqlServerDatabaseExtensions.cs index 2ac1f1a..032e656 100644 --- a/src/Testing.Databases.SqlServer.SqlCmd/SqlCmdSqlServerDatabaseExtensions.cs +++ b/src/Testing.Databases.SqlServer.SqlCmd/SqlCmdSqlServerDatabaseExtensions.cs @@ -26,8 +26,8 @@ public static class SqlCmdSqlServerDatabaseExtensions /// to retrieve the output result of the script execution. public static void RunScript(this SqlServerDatabase database, string fileName, SqlCmdRunScriptSettings? settings = null) { - ArgumentNullException.ThrowIfNull(database, nameof(database)); - ArgumentNullException.ThrowIfNull(fileName, nameof(fileName)); + Guard.ThrowIfNull(database, nameof(database)); + Guard.ThrowIfNull(fileName, nameof(fileName)); if (!File.Exists(fileName)) { From 68c588b8c91573a18176c4c2457256a7f7a6e5db Mon Sep 17 00:00:00 2001 From: Gilles TOURREAU Date: Wed, 24 Sep 2025 12:18:29 +0200 Subject: [PATCH 13/61] Changes the connection strings to retrieve from env variable and after use localdb if no env variable exists. --- PosInformatique.Testing.Databases.sln | 7 +++++ .../SqlServerDacExtensionsTest.cs | 2 +- .../SqlServerDatabaseInitializerTest.cs | 2 +- ...sting.Databases.SqlServer.Dac.Tests.csproj | 3 ++ ...meworkDatabaseInitializerExtensionsTest.cs | 2 +- .../EntityFrameworkSqlServerExtensionsTest.cs | 10 +++---- ...ses.SqlServer.EntityFramework.Tests.csproj | 2 ++ .../ConnectionStrings.cs | 30 +++++++++++++++++++ ...Databases.SqlServer.Shared.Tests.projitems | 14 +++++++++ ...ng.Databases.SqlServer.Shared.Tests.shproj | 13 ++++++++ .../SqlCmdDatabaseInitializerTest.cs | 2 +- .../SqlCmdSqlServerExtensionsTest.cs | 2 +- ...ng.Databases.SqlServer.SqlCmd.Tests.csproj | 2 ++ .../SqlServerDatabaseComparerTest.cs | 2 +- .../SqlServerDatabaseExtensionsTest.cs | 2 +- .../SqlServerTest.cs | 10 +++---- .../Testing.Databases.SqlServer.Tests.csproj | 2 ++ 17 files changed, 90 insertions(+), 17 deletions(-) create mode 100644 tests/Testing.Databases.SqlServer.Shared.Tests/ConnectionStrings.cs create mode 100644 tests/Testing.Databases.SqlServer.Shared.Tests/Testing.Databases.SqlServer.Shared.Tests.projitems create mode 100644 tests/Testing.Databases.SqlServer.Shared.Tests/Testing.Databases.SqlServer.Shared.Tests.shproj diff --git a/PosInformatique.Testing.Databases.sln b/PosInformatique.Testing.Databases.sln index 3ff08cd..296120c 100644 --- a/PosInformatique.Testing.Databases.sln +++ b/PosInformatique.Testing.Databases.sln @@ -65,6 +65,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testing.Databases.SqlServer EndProject Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "Testing.Databases.SqlServer.Shared", "src\Testing.Databases.SqlServer.Shared\Testing.Databases.SqlServer.Shared.shproj", "{B9F8C52D-4652-4FC6-A695-DC2F61BA05C9}" EndProject +Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "Testing.Databases.SqlServer.Shared.Tests", "tests\Testing.Databases.SqlServer.Shared.Tests\Testing.Databases.SqlServer.Shared.Tests.shproj", "{1554EA1B-37C8-4F10-9770-BF4DD8A7E80C}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -136,9 +138,14 @@ Global SolutionGuid = {FAC64573-D665-48A8-AC75-7B82B692EC9E} EndGlobalSection GlobalSection(SharedMSBuildProjectFiles) = preSolution + tests\Testing.Databases.SqlServer.Shared.Tests\Testing.Databases.SqlServer.Shared.Tests.projitems*{04a7ae8f-fe77-435b-9250-600388bb8065}*SharedItemsImports = 5 + tests\Testing.Databases.SqlServer.Shared.Tests\Testing.Databases.SqlServer.Shared.Tests.projitems*{1554ea1b-37c8-4f10-9770-bf4dd8a7e80c}*SharedItemsImports = 13 src\Testing.Databases.SqlServer.Shared\Testing.Databases.SqlServer.Shared.projitems*{157ddf0d-9410-4646-94b9-9cee4c140f5e}*SharedItemsImports = 5 + tests\Testing.Databases.SqlServer.Shared.Tests\Testing.Databases.SqlServer.Shared.Tests.projitems*{6751a585-1bb0-49a1-bf68-d7fbd3392df6}*SharedItemsImports = 5 src\Testing.Databases.SqlServer.Shared\Testing.Databases.SqlServer.Shared.projitems*{8be60460-eba5-43de-b85d-c756e2988dc8}*SharedItemsImports = 5 src\Testing.Databases.SqlServer.Shared\Testing.Databases.SqlServer.Shared.projitems*{b9f8c52d-4652-4fc6-a695-dc2f61ba05c9}*SharedItemsImports = 13 + tests\Testing.Databases.SqlServer.Shared.Tests\Testing.Databases.SqlServer.Shared.Tests.projitems*{c87e8f0d-d96d-4d77-9713-5564dc2e3597}*SharedItemsImports = 5 src\Testing.Databases.SqlServer.Shared\Testing.Databases.SqlServer.Shared.projitems*{d3004122-ccdd-4ead-bd9e-da6dff470943}*SharedItemsImports = 5 + tests\Testing.Databases.SqlServer.Shared.Tests\Testing.Databases.SqlServer.Shared.Tests.projitems*{f8e025d7-4e2f-437a-abfa-c43a1368e15a}*SharedItemsImports = 5 EndGlobalSection EndGlobal diff --git a/tests/Testing.Databases.SqlServer.Dac.Tests/SqlServerDacExtensionsTest.cs b/tests/Testing.Databases.SqlServer.Dac.Tests/SqlServerDacExtensionsTest.cs index db76e8d..003319f 100644 --- a/tests/Testing.Databases.SqlServer.Dac.Tests/SqlServerDacExtensionsTest.cs +++ b/tests/Testing.Databases.SqlServer.Dac.Tests/SqlServerDacExtensionsTest.cs @@ -9,7 +9,7 @@ namespace PosInformatique.Testing.Databases.SqlServer.Tests [Collection("PosInformatique.Testing.Databases.SqlServer.Tests")] public class SqlServerDacExtensionsTest { - private const string ConnectionString = $"Data Source=(localDB)\\posinfo-tests; Integrated Security=True"; + private static readonly string ConnectionString = ConnectionStrings.Get(); [Theory] [InlineData(false)] diff --git a/tests/Testing.Databases.SqlServer.Dac.Tests/SqlServerDatabaseInitializerTest.cs b/tests/Testing.Databases.SqlServer.Dac.Tests/SqlServerDatabaseInitializerTest.cs index 044f00a..501a1f5 100644 --- a/tests/Testing.Databases.SqlServer.Dac.Tests/SqlServerDatabaseInitializerTest.cs +++ b/tests/Testing.Databases.SqlServer.Dac.Tests/SqlServerDatabaseInitializerTest.cs @@ -9,7 +9,7 @@ namespace PosInformatique.Testing.Databases.SqlServer.Tests [Collection("PosInformatique.Testing.Databases.SqlServer.Tests")] public class SqlServerDatabaseInitializerTest : IClassFixture { - private const string ConnectionString = $"Data Source=(localDB)\\posinfo-tests; Initial Catalog={nameof(SqlServerDatabaseInitializerTest)}; Integrated Security=True"; + private static readonly string ConnectionString = ConnectionStrings.Get(nameof(SqlServerDatabaseInitializerTest)); private readonly SqlServerDatabase database; diff --git a/tests/Testing.Databases.SqlServer.Dac.Tests/Testing.Databases.SqlServer.Dac.Tests.csproj b/tests/Testing.Databases.SqlServer.Dac.Tests/Testing.Databases.SqlServer.Dac.Tests.csproj index f7d6c15..cf50d2e 100644 --- a/tests/Testing.Databases.SqlServer.Dac.Tests/Testing.Databases.SqlServer.Dac.Tests.csproj +++ b/tests/Testing.Databases.SqlServer.Dac.Tests/Testing.Databases.SqlServer.Dac.Tests.csproj @@ -27,9 +27,12 @@ runtime; build; native; contentfiles; analyzers; buildtransitive + + + diff --git a/tests/Testing.Databases.SqlServer.EntityFramework.Tests/EntityFrameworkDatabaseInitializerExtensionsTest.cs b/tests/Testing.Databases.SqlServer.EntityFramework.Tests/EntityFrameworkDatabaseInitializerExtensionsTest.cs index 9640452..ac25658 100644 --- a/tests/Testing.Databases.SqlServer.EntityFramework.Tests/EntityFrameworkDatabaseInitializerExtensionsTest.cs +++ b/tests/Testing.Databases.SqlServer.EntityFramework.Tests/EntityFrameworkDatabaseInitializerExtensionsTest.cs @@ -11,7 +11,7 @@ namespace PosInformatique.Testing.Databases.SqlServer.Tests [Collection("PosInformatique.Testing.Databases.SqlServer.Tests")] public class EntityFrameworkDatabaseInitializerExtensionsTest : IClassFixture { - private const string ConnectionString = $"Data Source=(localDB)\\posinfo-tests; Initial Catalog={nameof(EntityFrameworkDatabaseInitializerExtensionsTest)}; Integrated Security=True"; + private static readonly string ConnectionString = ConnectionStrings.Get(nameof(EntityFrameworkDatabaseInitializerExtensionsTest)); private readonly SqlServerDatabase database; diff --git a/tests/Testing.Databases.SqlServer.EntityFramework.Tests/EntityFrameworkSqlServerExtensionsTest.cs b/tests/Testing.Databases.SqlServer.EntityFramework.Tests/EntityFrameworkSqlServerExtensionsTest.cs index c54424b..9d23e0f 100644 --- a/tests/Testing.Databases.SqlServer.EntityFramework.Tests/EntityFrameworkSqlServerExtensionsTest.cs +++ b/tests/Testing.Databases.SqlServer.EntityFramework.Tests/EntityFrameworkSqlServerExtensionsTest.cs @@ -11,7 +11,7 @@ namespace PosInformatique.Testing.Databases.SqlServer.Tests [Collection("PosInformatique.Testing.Databases.SqlServer.Tests")] public class EntityFrameworkSqlServerExtensionsTest { - private const string ConnectionString = $"Data Source=(localDB)\\posinfo-tests; Integrated Security=True"; + private static readonly string ConnectionString = ConnectionStrings.Get(); [Fact] public async Task Create_WithNoExistingDatabase() @@ -26,7 +26,7 @@ public async Task Create_WithNoExistingDatabase() var database = server.CreateDatabase(nameof(EntityFrameworkSqlServerExtensionsTest), dbContext); - database.ConnectionString.Should().Be("Data Source=(localDB)\\posinfo-tests;Initial Catalog=EntityFrameworkSqlServerExtensionsTest;Integrated Security=True"); + database.ConnectionString.Should().Be(ConnectionStrings.Get("EntityFrameworkSqlServerExtensionsTest")); var tables = await database.GetTablesAsync(); @@ -54,7 +54,7 @@ public async Task Create_WithAlreadyExistingDatabase() var database = server.CreateDatabase(nameof(EntityFrameworkSqlServerExtensionsTest), dbContext); - database.ConnectionString.Should().Be("Data Source=(localDB)\\posinfo-tests;Initial Catalog=EntityFrameworkSqlServerExtensionsTest;Integrated Security=True"); + database.ConnectionString.Should().Be(ConnectionStrings.Get("EntityFrameworkSqlServerExtensionsTest")); var tables = await database.GetTablesAsync(); @@ -80,7 +80,7 @@ public async Task CreateAsync_WithNoExistingDatabase() var database = await server.CreateDatabaseAsync(nameof(EntityFrameworkSqlServerExtensionsTest), dbContext); - database.ConnectionString.Should().Be("Data Source=(localDB)\\posinfo-tests;Initial Catalog=EntityFrameworkSqlServerExtensionsTest;Integrated Security=True"); + database.ConnectionString.Should().Be(ConnectionStrings.Get("EntityFrameworkSqlServerExtensionsTest")); var tables = await database.GetTablesAsync(); @@ -108,7 +108,7 @@ public async Task CreateAsync_WithAlreadyExistingDatabase() var database = await server.CreateDatabaseAsync(nameof(EntityFrameworkSqlServerExtensionsTest), dbContext); - database.ConnectionString.Should().Be("Data Source=(localDB)\\posinfo-tests;Initial Catalog=EntityFrameworkSqlServerExtensionsTest;Integrated Security=True"); + database.ConnectionString.Should().Be(ConnectionStrings.Get("EntityFrameworkSqlServerExtensionsTest")); var tables = await database.GetTablesAsync(); diff --git a/tests/Testing.Databases.SqlServer.EntityFramework.Tests/Testing.Databases.SqlServer.EntityFramework.Tests.csproj b/tests/Testing.Databases.SqlServer.EntityFramework.Tests/Testing.Databases.SqlServer.EntityFramework.Tests.csproj index a9979d0..366096d 100644 --- a/tests/Testing.Databases.SqlServer.EntityFramework.Tests/Testing.Databases.SqlServer.EntityFramework.Tests.csproj +++ b/tests/Testing.Databases.SqlServer.EntityFramework.Tests/Testing.Databases.SqlServer.EntityFramework.Tests.csproj @@ -22,4 +22,6 @@ + + diff --git a/tests/Testing.Databases.SqlServer.Shared.Tests/ConnectionStrings.cs b/tests/Testing.Databases.SqlServer.Shared.Tests/ConnectionStrings.cs new file mode 100644 index 0000000..159457a --- /dev/null +++ b/tests/Testing.Databases.SqlServer.Shared.Tests/ConnectionStrings.cs @@ -0,0 +1,30 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) P.O.S Informatique. All rights reserved. +// +//----------------------------------------------------------------------- + +namespace PosInformatique.Testing.Databases.SqlServer +{ + using Microsoft.Data.SqlClient; + + public static class ConnectionStrings + { + public static string Get(string databaseName = "master") + { + var connectionString = Environment.GetEnvironmentVariable("SQL_SERVER_UNIT_TESTS_CONNECTION_STRING"); + + if (connectionString is null) + { + connectionString = $"Data Source=(localDB)\\posinfo-tests; Integrated Security=True"; + } + + var connectionStringBuilder = new SqlConnectionStringBuilder(connectionString) + { + InitialCatalog = databaseName, + }; + + return connectionStringBuilder.ToString(); + } + } +} diff --git a/tests/Testing.Databases.SqlServer.Shared.Tests/Testing.Databases.SqlServer.Shared.Tests.projitems b/tests/Testing.Databases.SqlServer.Shared.Tests/Testing.Databases.SqlServer.Shared.Tests.projitems new file mode 100644 index 0000000..9d1f76c --- /dev/null +++ b/tests/Testing.Databases.SqlServer.Shared.Tests/Testing.Databases.SqlServer.Shared.Tests.projitems @@ -0,0 +1,14 @@ + + + + $(MSBuildAllProjects);$(MSBuildThisFileFullPath) + true + 1554ea1b-37c8-4f10-9770-bf4dd8a7e80c + + + Testing.Databases.SqlServer.Shared.Tests + + + + + \ No newline at end of file diff --git a/tests/Testing.Databases.SqlServer.Shared.Tests/Testing.Databases.SqlServer.Shared.Tests.shproj b/tests/Testing.Databases.SqlServer.Shared.Tests/Testing.Databases.SqlServer.Shared.Tests.shproj new file mode 100644 index 0000000..f47ed9e --- /dev/null +++ b/tests/Testing.Databases.SqlServer.Shared.Tests/Testing.Databases.SqlServer.Shared.Tests.shproj @@ -0,0 +1,13 @@ + + + + 1554ea1b-37c8-4f10-9770-bf4dd8a7e80c + 14.0 + + + + + + + + diff --git a/tests/Testing.Databases.SqlServer.SqlCmd.Tests/SqlCmdDatabaseInitializerTest.cs b/tests/Testing.Databases.SqlServer.SqlCmd.Tests/SqlCmdDatabaseInitializerTest.cs index c134a86..144e6bd 100644 --- a/tests/Testing.Databases.SqlServer.SqlCmd.Tests/SqlCmdDatabaseInitializerTest.cs +++ b/tests/Testing.Databases.SqlServer.SqlCmd.Tests/SqlCmdDatabaseInitializerTest.cs @@ -9,7 +9,7 @@ namespace PosInformatique.Testing.Databases.SqlServer.Tests [Collection("PosInformatique.Testing.Databases.SqlServer.Tests")] public class SqlCmdDatabaseInitializerTest : IClassFixture { - private const string ConnectionString = $"Data Source=(localDB)\\posinfo-tests; Initial Catalog={nameof(SqlCmdDatabaseInitializerTest)}; Integrated Security=True"; + private static readonly string ConnectionString = ConnectionStrings.Get(nameof(SqlCmdDatabaseInitializerTest)); private readonly SqlServerDatabase database; diff --git a/tests/Testing.Databases.SqlServer.SqlCmd.Tests/SqlCmdSqlServerExtensionsTest.cs b/tests/Testing.Databases.SqlServer.SqlCmd.Tests/SqlCmdSqlServerExtensionsTest.cs index cd60c0f..02a4c89 100644 --- a/tests/Testing.Databases.SqlServer.SqlCmd.Tests/SqlCmdSqlServerExtensionsTest.cs +++ b/tests/Testing.Databases.SqlServer.SqlCmd.Tests/SqlCmdSqlServerExtensionsTest.cs @@ -9,7 +9,7 @@ namespace PosInformatique.Testing.Databases.SqlServer.Tests [Collection("PosInformatique.Testing.Databases.SqlServer.Tests")] public class SqlCmdSqlServerExtensionsTest { - private const string ConnectionString = $"Data Source=(localDB)\\posinfo-tests; Integrated Security=True"; + private static readonly string ConnectionString = ConnectionStrings.Get(); [Theory] [InlineData(false)] diff --git a/tests/Testing.Databases.SqlServer.SqlCmd.Tests/Testing.Databases.SqlServer.SqlCmd.Tests.csproj b/tests/Testing.Databases.SqlServer.SqlCmd.Tests/Testing.Databases.SqlServer.SqlCmd.Tests.csproj index e75b07c..ddd8e61 100644 --- a/tests/Testing.Databases.SqlServer.SqlCmd.Tests/Testing.Databases.SqlServer.SqlCmd.Tests.csproj +++ b/tests/Testing.Databases.SqlServer.SqlCmd.Tests/Testing.Databases.SqlServer.SqlCmd.Tests.csproj @@ -29,4 +29,6 @@ + + diff --git a/tests/Testing.Databases.SqlServer.Tests/SqlServerDatabaseComparerTest.cs b/tests/Testing.Databases.SqlServer.Tests/SqlServerDatabaseComparerTest.cs index 03eb07b..40debff 100644 --- a/tests/Testing.Databases.SqlServer.Tests/SqlServerDatabaseComparerTest.cs +++ b/tests/Testing.Databases.SqlServer.Tests/SqlServerDatabaseComparerTest.cs @@ -9,7 +9,7 @@ namespace PosInformatique.Testing.Databases.SqlServer.Tests [Collection("PosInformatique.Testing.Databases.SqlServer.Tests")] public class SqlServerDatabaseComparerTest { - private const string ConnectionString = $"Data Source=(localDB)\\posinfo-tests; Integrated Security=True"; + private static readonly string ConnectionString = ConnectionStrings.Get(); [Fact] public async Task CompareAsync() diff --git a/tests/Testing.Databases.SqlServer.Tests/SqlServerDatabaseExtensionsTest.cs b/tests/Testing.Databases.SqlServer.Tests/SqlServerDatabaseExtensionsTest.cs index 4718aec..51631fa 100644 --- a/tests/Testing.Databases.SqlServer.Tests/SqlServerDatabaseExtensionsTest.cs +++ b/tests/Testing.Databases.SqlServer.Tests/SqlServerDatabaseExtensionsTest.cs @@ -9,7 +9,7 @@ namespace PosInformatique.Testing.Databases.SqlServer.Tests [Collection("PosInformatique.Testing.Databases.SqlServer.Tests")] public class SqlServerDatabaseExtensionsTest { - private const string ConnectionString = $"Data Source=(localDB)\\posinfo-tests; Initial Catalog={nameof(SqlServerDatabaseExtensionsTest)}; Integrated Security=True"; + private static readonly string ConnectionString = ConnectionStrings.Get(nameof(SqlServerDatabaseExtensionsTest)); [Fact] public void InsertInto_EnableIdentity() diff --git a/tests/Testing.Databases.SqlServer.Tests/SqlServerTest.cs b/tests/Testing.Databases.SqlServer.Tests/SqlServerTest.cs index b1485fb..6383421 100644 --- a/tests/Testing.Databases.SqlServer.Tests/SqlServerTest.cs +++ b/tests/Testing.Databases.SqlServer.Tests/SqlServerTest.cs @@ -9,7 +9,7 @@ namespace PosInformatique.Testing.Databases.SqlServer.Tests [Collection("PosInformatique.Testing.Databases.SqlServer.Tests")] public class SqlServerTest { - private const string ConnectionString = $"Data Source=(localDB)\\posinfo-tests; Initial Catalog={nameof(SqlServerTest)}; Integrated Security=True"; + private static readonly string ConnectionString = ConnectionStrings.Get(nameof(SqlServerTest)); [Theory] [InlineData("Data Source=TheServer; Initial Catalog=TheDB; User ID=TheID; Password=ThePassword", "Data Source=TheServer;Initial Catalog=master;User ID=TheID;Password=ThePassword")] @@ -38,7 +38,7 @@ public async Task CreateAndDelete(bool withCreationSettings) var database = server.CreateEmptyDatabase("CreateAndDeleteDB", settings); - database.ConnectionString.Should().Be("Data Source=(localDB)\\posinfo-tests;Initial Catalog=CreateAndDeleteDB;Integrated Security=True"); + database.ConnectionString.Should().Be(ConnectionStrings.Get("CreateAndDeleteDB")); var table = await server.Master.ExecuteQueryAsync("SELECT * FROM [sys].[databases] WHERE [name] = 'CreateAndDeleteDB'"); table.Rows.Should().HaveCount(1); @@ -64,7 +64,7 @@ public async Task CreateAndDelete_WithSpecificDataFileName() var database = server.CreateEmptyDatabase("CreateAndDeleteDB_WithSpecificDataFileName", settings); - database.ConnectionString.Should().Be("Data Source=(localDB)\\posinfo-tests;Initial Catalog=CreateAndDeleteDB_WithSpecificDataFileName;Integrated Security=True"); + database.ConnectionString.Should().Be(ConnectionStrings.Get("CreateAndDeleteDB_WithSpecificDataFileName")); var table = await server.Master.ExecuteQueryAsync("SELECT * FROM [sys].[databases] WHERE [name] = 'CreateAndDeleteDB_WithSpecificDataFileName'"); table.Rows.Should().HaveCount(1); @@ -99,7 +99,7 @@ public async Task CreateAndDeleteAsync() var database = await server.CreateEmptyDatabaseAsync("CreateAndDeleteDBAsync", new SqlDatabaseCreationSettings(), CancellationToken.None); - database.ConnectionString.Should().Be("Data Source=(localDB)\\posinfo-tests;Initial Catalog=CreateAndDeleteDBAsync;Integrated Security=True"); + database.ConnectionString.Should().Be(ConnectionStrings.Get("CreateAndDeleteDBAsync")); var table = await server.Master.ExecuteQueryAsync("SELECT * FROM [sys].[databases] WHERE [name] = 'CreateAndDeleteDBAsync'"); table.Rows.Should().HaveCount(1); @@ -125,7 +125,7 @@ public async Task CreateAndDeleteAsync_WithSpecificDataFileName() var database = await server.CreateEmptyDatabaseAsync("CreateAndDeleteDB_WithSpecificDataFileNameAsync", settings); - database.ConnectionString.Should().Be("Data Source=(localDB)\\posinfo-tests;Initial Catalog=CreateAndDeleteDB_WithSpecificDataFileNameAsync;Integrated Security=True"); + database.ConnectionString.Should().Be(ConnectionStrings.Get("CreateAndDeleteDB_WithSpecificDataFileNameAsync")); var table = await server.Master.ExecuteQueryAsync("SELECT * FROM [sys].[databases] WHERE [name] = 'CreateAndDeleteDB_WithSpecificDataFileNameAsync'"); table.Rows.Should().HaveCount(1); diff --git a/tests/Testing.Databases.SqlServer.Tests/Testing.Databases.SqlServer.Tests.csproj b/tests/Testing.Databases.SqlServer.Tests/Testing.Databases.SqlServer.Tests.csproj index 0d98455..01bd981 100644 --- a/tests/Testing.Databases.SqlServer.Tests/Testing.Databases.SqlServer.Tests.csproj +++ b/tests/Testing.Databases.SqlServer.Tests/Testing.Databases.SqlServer.Tests.csproj @@ -40,4 +40,6 @@ + + From 614824a561722651d4c35716827a245e1946c90f Mon Sep 17 00:00:00 2001 From: Gilles TOURREAU Date: Wed, 24 Sep 2025 14:36:18 +0200 Subject: [PATCH 14/61] Use the MSBuild.Sdk.SqlProj for the database projects. --- PosInformatique.Testing.Databases.sln | 6 +- ...sting.Databases.SqlServer.Dac.Tests.csproj | 4 +- ...ng.Databases.SqlServer.SqlCmd.Tests.csproj | 1 - ...ng.Databases.SqlServer.Tests.DacPac.csproj | 18 +++ ...g.Databases.SqlServer.Tests.DacPac.sqlproj | 64 --------- ...ng.Databases.SqlServer.Tests.Source.csproj | 18 +++ ...g.Databases.SqlServer.Tests.Source.sqlproj | 108 ---------------- ...ng.Databases.SqlServer.Tests.Target.csproj | 28 ++++ ...g.Databases.SqlServer.Tests.Target.sqlproj | 121 ------------------ ...erverDatabaseComparerTest.CompareAsync.txt | 6 + .../SqlServerDatabaseComparerTest.cs | 26 ++-- .../Testing.Databases.SqlServer.Tests.csproj | 10 +- 12 files changed, 93 insertions(+), 317 deletions(-) create mode 100644 tests/Testing.Databases.SqlServer.Tests.DacPac/Testing.Databases.SqlServer.Tests.DacPac.csproj delete mode 100644 tests/Testing.Databases.SqlServer.Tests.DacPac/Testing.Databases.SqlServer.Tests.DacPac.sqlproj create mode 100644 tests/Testing.Databases.SqlServer.Tests.Source/Testing.Databases.SqlServer.Tests.Source.csproj delete mode 100644 tests/Testing.Databases.SqlServer.Tests.Source/Testing.Databases.SqlServer.Tests.Source.sqlproj create mode 100644 tests/Testing.Databases.SqlServer.Tests.Target/Testing.Databases.SqlServer.Tests.Target.csproj delete mode 100644 tests/Testing.Databases.SqlServer.Tests.Target/Testing.Databases.SqlServer.Tests.Target.sqlproj diff --git a/PosInformatique.Testing.Databases.sln b/PosInformatique.Testing.Databases.sln index 296120c..c19db51 100644 --- a/PosInformatique.Testing.Databases.sln +++ b/PosInformatique.Testing.Databases.sln @@ -31,7 +31,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{49103176-7D0 src\Directory.Build.props = src\Directory.Build.props EndProjectSection EndProject -Project("{00D1A9C2-B5F0-4AF3-8072-F6C62B433612}") = "Testing.Databases.SqlServer.Tests.DacPac", "tests\Testing.Databases.SqlServer.Tests.DacPac\Testing.Databases.SqlServer.Tests.DacPac.sqlproj", "{5F618225-0E1C-46A7-BBCC-23A6243D5CEE}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Testing.Databases.SqlServer.Tests.DacPac", "tests\Testing.Databases.SqlServer.Tests.DacPac\Testing.Databases.SqlServer.Tests.DacPac.csproj", "{5F618225-0E1C-46A7-BBCC-23A6243D5CEE}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".github", ".github", "{91BFD2B1-6AB6-4B07-9D2E-430C93F150D4}" EndProject @@ -41,9 +41,9 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{ .github\workflows\github-actions-release.yml = .github\workflows\github-actions-release.yml EndProjectSection EndProject -Project("{00D1A9C2-B5F0-4AF3-8072-F6C62B433612}") = "Testing.Databases.SqlServer.Tests.Source", "tests\Testing.Databases.SqlServer.Tests.Source\Testing.Databases.SqlServer.Tests.Source.sqlproj", "{A261D4FF-9BEA-475C-8671-E9BACFDCE960}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Testing.Databases.SqlServer.Tests.Source", "tests\Testing.Databases.SqlServer.Tests.Source\Testing.Databases.SqlServer.Tests.Source.csproj", "{A261D4FF-9BEA-475C-8671-E9BACFDCE960}" EndProject -Project("{00D1A9C2-B5F0-4AF3-8072-F6C62B433612}") = "Testing.Databases.SqlServer.Tests.Target", "tests\Testing.Databases.SqlServer.Tests.Target\Testing.Databases.SqlServer.Tests.Target.sqlproj", "{6CD3F177-053F-4816-A37E-5CA6F293D34C}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Testing.Databases.SqlServer.Tests.Target", "tests\Testing.Databases.SqlServer.Tests.Target\Testing.Databases.SqlServer.Tests.Target.csproj", "{6CD3F177-053F-4816-A37E-5CA6F293D34C}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Testing.Databases.SqlServer.EntityFramework", "src\Testing.Databases.SqlServer.EntityFramework\Testing.Databases.SqlServer.EntityFramework.csproj", "{157DDF0D-9410-4646-94B9-9CEE4C140F5E}" EndProject diff --git a/tests/Testing.Databases.SqlServer.Dac.Tests/Testing.Databases.SqlServer.Dac.Tests.csproj b/tests/Testing.Databases.SqlServer.Dac.Tests/Testing.Databases.SqlServer.Dac.Tests.csproj index cf50d2e..ce34841 100644 --- a/tests/Testing.Databases.SqlServer.Dac.Tests/Testing.Databases.SqlServer.Dac.Tests.csproj +++ b/tests/Testing.Databases.SqlServer.Dac.Tests/Testing.Databases.SqlServer.Dac.Tests.csproj @@ -9,7 +9,7 @@ - + PreserveNewest @@ -30,7 +30,7 @@ - + diff --git a/tests/Testing.Databases.SqlServer.SqlCmd.Tests/Testing.Databases.SqlServer.SqlCmd.Tests.csproj b/tests/Testing.Databases.SqlServer.SqlCmd.Tests/Testing.Databases.SqlServer.SqlCmd.Tests.csproj index ddd8e61..3ff5a22 100644 --- a/tests/Testing.Databases.SqlServer.SqlCmd.Tests/Testing.Databases.SqlServer.SqlCmd.Tests.csproj +++ b/tests/Testing.Databases.SqlServer.SqlCmd.Tests/Testing.Databases.SqlServer.SqlCmd.Tests.csproj @@ -26,7 +26,6 @@ - diff --git a/tests/Testing.Databases.SqlServer.Tests.DacPac/Testing.Databases.SqlServer.Tests.DacPac.csproj b/tests/Testing.Databases.SqlServer.Tests.DacPac/Testing.Databases.SqlServer.Tests.DacPac.csproj new file mode 100644 index 0000000..fe47423 --- /dev/null +++ b/tests/Testing.Databases.SqlServer.Tests.DacPac/Testing.Databases.SqlServer.Tests.DacPac.csproj @@ -0,0 +1,18 @@ + + + netstandard2.1 + Sql150 + + True + + True + False + True + + + + + + + + \ No newline at end of file diff --git a/tests/Testing.Databases.SqlServer.Tests.DacPac/Testing.Databases.SqlServer.Tests.DacPac.sqlproj b/tests/Testing.Databases.SqlServer.Tests.DacPac/Testing.Databases.SqlServer.Tests.DacPac.sqlproj deleted file mode 100644 index c4e689c..0000000 --- a/tests/Testing.Databases.SqlServer.Tests.DacPac/Testing.Databases.SqlServer.Tests.DacPac.sqlproj +++ /dev/null @@ -1,64 +0,0 @@ - - - - Debug - AnyCPU - Testing.Databases.SqlServer.Tests.DacPac - 2.0 - 4.1 - {5f618225-0e1c-46a7-bbcc-23a6243d5cee} - Microsoft.Data.Tools.Schema.Sql.Sql150DatabaseSchemaProvider - Database - - - Testing.Databases.SqlServer.Tests.DacPac - Testing.Databases.SqlServer.Tests.DacPac - 1033, CI - BySchemaAndSchemaType - True - v4.7.2 - CS - Properties - False - True - True - - - bin\Release\ - $(MSBuildProjectName).sql - False - pdbonly - true - false - true - prompt - 4 - - - bin\Debug\ - $(MSBuildProjectName).sql - false - true - full - false - true - true - prompt - 4 - - - 11.0 - - True - 11.0 - - - - - - - - - - - \ No newline at end of file diff --git a/tests/Testing.Databases.SqlServer.Tests.Source/Testing.Databases.SqlServer.Tests.Source.csproj b/tests/Testing.Databases.SqlServer.Tests.Source/Testing.Databases.SqlServer.Tests.Source.csproj new file mode 100644 index 0000000..fe47423 --- /dev/null +++ b/tests/Testing.Databases.SqlServer.Tests.Source/Testing.Databases.SqlServer.Tests.Source.csproj @@ -0,0 +1,18 @@ + + + netstandard2.1 + Sql150 + + True + + True + False + True + + + + + + + + \ No newline at end of file diff --git a/tests/Testing.Databases.SqlServer.Tests.Source/Testing.Databases.SqlServer.Tests.Source.sqlproj b/tests/Testing.Databases.SqlServer.Tests.Source/Testing.Databases.SqlServer.Tests.Source.sqlproj deleted file mode 100644 index 60bd7b0..0000000 --- a/tests/Testing.Databases.SqlServer.Tests.Source/Testing.Databases.SqlServer.Tests.Source.sqlproj +++ /dev/null @@ -1,108 +0,0 @@ - - - - Debug - AnyCPU - Testing.Databases.SqlServer.Tests.Source - 2.0 - 4.1 - {a261d4ff-9bea-475c-8671-e9bacfdce960} - Microsoft.Data.Tools.Schema.Sql.Sql150DatabaseSchemaProvider - Database - - - Testing.Databases.SqlServer.Tests.Source - Testing.Databases.SqlServer.Tests.Source - 1033, CI - BySchemaAndSchemaType - True - v4.7.2 - CS - Properties - False - True - True - - - bin\Release\ - $(MSBuildProjectName).sql - False - pdbonly - true - false - true - prompt - 4 - - - bin\Debug\ - $(MSBuildProjectName).sql - false - true - full - false - true - true - prompt - 4 - - - 11.0 - - True - 11.0 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/tests/Testing.Databases.SqlServer.Tests.Target/Testing.Databases.SqlServer.Tests.Target.csproj b/tests/Testing.Databases.SqlServer.Tests.Target/Testing.Databases.SqlServer.Tests.Target.csproj new file mode 100644 index 0000000..4bdf1d8 --- /dev/null +++ b/tests/Testing.Databases.SqlServer.Tests.Target/Testing.Databases.SqlServer.Tests.Target.csproj @@ -0,0 +1,28 @@ + + + netstandard2.1 + Sql150 + + True + + True + False + True + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/Testing.Databases.SqlServer.Tests.Target/Testing.Databases.SqlServer.Tests.Target.sqlproj b/tests/Testing.Databases.SqlServer.Tests.Target/Testing.Databases.SqlServer.Tests.Target.sqlproj deleted file mode 100644 index d34fed2..0000000 --- a/tests/Testing.Databases.SqlServer.Tests.Target/Testing.Databases.SqlServer.Tests.Target.sqlproj +++ /dev/null @@ -1,121 +0,0 @@ - - - - Debug - AnyCPU - Testing.Databases.SqlServer.Tests.Target - 2.0 - 4.1 - {6cd3f177-053f-4816-a37e-5ca6f293d34c} - Microsoft.Data.Tools.Schema.Sql.Sql150DatabaseSchemaProvider - Database - - - Testing.Databases.SqlServer.Tests.Target - Testing.Databases.SqlServer.Tests.Target - 1033, CI - BySchemaAndSchemaType - True - v4.7.2 - CS - Properties - False - True - True - - - bin\Release\ - $(MSBuildProjectName).sql - False - pdbonly - true - false - true - prompt - 4 - - - bin\Debug\ - $(MSBuildProjectName).sql - false - true - full - false - true - true - prompt - 4 - - - 11.0 - - True - 11.0 - - - - - - - - - - - - - - - - - - - - Tables\TableIdentical.sql - - - - - Tables\PrimaryKey\PrimaryKeyIdentical.sql - - - - - - Programmability\Types\TypeIdentical.sql - - - - - Tables\UniqueConstraints\UniqueConstraintIdentical.sql - - - - Tables\ReferencedTable.sql - - - Tables\ForeignKeys\ForeignKeyIdentical.sql - - - - - - Tables\Indexes\IndexIdentical.sql - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/tests/Testing.Databases.SqlServer.Tests/SqlServerDatabaseComparerTest.CompareAsync.txt b/tests/Testing.Databases.SqlServer.Tests/SqlServerDatabaseComparerTest.CompareAsync.txt index 80b106f..ceb8e7e 100644 --- a/tests/Testing.Databases.SqlServer.Tests/SqlServerDatabaseComparerTest.CompareAsync.txt +++ b/tests/Testing.Databases.SqlServer.Tests/SqlServerDatabaseComparerTest.CompareAsync.txt @@ -142,6 +142,7 @@ BEGIN PRINT 'From source' END + Target: CREATE TRIGGER [TriggerDifference] ON [dbo].[TableDifference] @@ -150,6 +151,7 @@ BEGIN PRINT 'From target' END + ------ Unique constraints ------ - UniqueConstraintDifference * Type: @@ -197,6 +199,7 @@ AS SELECT @param2 RETURN 0 + Target: CREATE PROCEDURE [dbo].[StoredProcedureDifference] @param1 int = 0, @@ -204,6 +207,7 @@ AS SELECT @param1 RETURN 0 + - dbo.StoredProcedureTarget (Missing in the source) - dbo.StoredProcedureSource (Missing in the target) ------ User types ------ @@ -222,8 +226,10 @@ Source: CREATE VIEW [dbo].[ViewDifference] AS SELECT * FROM [TableDifference] WHERE [Type] = 10 + Target: CREATE VIEW [dbo].[ViewDifference] AS SELECT * FROM [TableDifference] WHERE [Type] = 'The type' + - dbo.ViewTarget (Missing in the source) - dbo.ViewSource (Missing in the target) diff --git a/tests/Testing.Databases.SqlServer.Tests/SqlServerDatabaseComparerTest.cs b/tests/Testing.Databases.SqlServer.Tests/SqlServerDatabaseComparerTest.cs index 40debff..aff169b 100644 --- a/tests/Testing.Databases.SqlServer.Tests/SqlServerDatabaseComparerTest.cs +++ b/tests/Testing.Databases.SqlServer.Tests/SqlServerDatabaseComparerTest.cs @@ -16,8 +16,8 @@ public async Task CompareAsync() { var server = new SqlServer(ConnectionString); - var sourceDatabase = Task.Run(() => server.DeployDacPackage("Testing.Databases.SqlServer.Tests.Source.dacpac", $"{nameof(SqlServerDatabaseComparerTest)}_Source")); - var targetDatabase = Task.Run(() => server.DeployDacPackage("Testing.Databases.SqlServer.Tests.Target.dacpac", $"{nameof(SqlServerDatabaseComparerTest)}_Target")); + var sourceDatabase = Task.Run(() => server.Master.RunScript("Testing.Databases.SqlServer.Tests.Source_Create.sql")); + var targetDatabase = Task.Run(() => server.Master.RunScript("Testing.Databases.SqlServer.Tests.Target_Create.sql")); await Task.WhenAll(sourceDatabase, targetDatabase); @@ -30,7 +30,7 @@ public async Task CompareAsync() }, }; - var differences = await SqlServerDatabaseComparer.CompareAsync(sourceDatabase.Result, targetDatabase.Result, options); + var differences = await SqlServerDatabaseComparer.CompareAsync(server.GetDatabase("Testing.Databases.SqlServer.Tests.Source"), server.GetDatabase("Testing.Databases.SqlServer.Tests.Target"), options); // StoredProcedures differences.StoredProcedures.Should().HaveCount(3); @@ -40,8 +40,8 @@ public async Task CompareAsync() differences.StoredProcedures[0].Target.Schema.Should().Be("dbo"); differences.StoredProcedures[0].Properties.Should().HaveCount(1); differences.StoredProcedures[0].Properties[0].Name.Should().Be("Code"); - differences.StoredProcedures[0].Properties[0].Source.Should().Be("CREATE PROCEDURE [dbo].[StoredProcedureDifference]\r\n\t@param1 int = 0,\r\n\t@param2 int\r\nAS\r\n\tSELECT @param2\r\nRETURN 0"); - differences.StoredProcedures[0].Properties[0].Target.Should().Be("CREATE PROCEDURE [dbo].[StoredProcedureDifference]\r\n\t@param1 int = 0,\r\n\t@param2 int\r\nAS\r\n\tSELECT @param1\r\nRETURN 0"); + differences.StoredProcedures[0].Properties[0].Source.Should().Be("CREATE PROCEDURE [dbo].[StoredProcedureDifference]\r\n\t@param1 int = 0,\r\n\t@param2 int\r\nAS\r\n\tSELECT @param2\r\nRETURN 0\r\n"); + differences.StoredProcedures[0].Properties[0].Target.Should().Be("CREATE PROCEDURE [dbo].[StoredProcedureDifference]\r\n\t@param1 int = 0,\r\n\t@param2 int\r\nAS\r\n\tSELECT @param1\r\nRETURN 0\r\n"); differences.StoredProcedures[1].Source.Should().BeNull(); differences.StoredProcedures[1].Target.Name.Should().Be("StoredProcedureTarget"); @@ -697,13 +697,13 @@ public async Task CompareAsync() differences.Tables[0].Source.Triggers.Should().HaveCount(1); differences.Tables[0].Source.Triggers[0].Name.Should().Be("TriggerDifference"); - differences.Tables[0].Source.Triggers[0].Code.Should().Be("CREATE TRIGGER [TriggerDifference]\r\n\tON [dbo].[TableDifference]\r\n\tINSTEAD OF INSERT\r\n\tAS\r\n\tBEGIN\r\n\t\tPRINT 'From source'\r\n\tEND"); + differences.Tables[0].Source.Triggers[0].Code.Should().Be("CREATE TRIGGER [TriggerDifference]\r\n\tON [dbo].[TableDifference]\r\n\tINSTEAD OF INSERT\r\n\tAS\r\n\tBEGIN\r\n\t\tPRINT 'From source'\r\n\tEND\r\n"); differences.Tables[0].Source.Triggers[0].IsInsteadOfTrigger.Should().BeTrue(); differences.Tables[0].Target.Triggers.Should().HaveCount(1); differences.Tables[0].Target.Triggers[0].Name.Should().Be("TriggerDifference"); - differences.Tables[0].Target.Triggers[0].Code.Should().Be("CREATE TRIGGER [TriggerDifference]\r\n\tON [dbo].[TableDifference]\r\n\tFOR INSERT\r\n\tAS\r\n\tBEGIN\r\n\t\tPRINT 'From target'\r\n\tEND"); + differences.Tables[0].Target.Triggers[0].Code.Should().Be("CREATE TRIGGER [TriggerDifference]\r\n\tON [dbo].[TableDifference]\r\n\tFOR INSERT\r\n\tAS\r\n\tBEGIN\r\n\t\tPRINT 'From target'\r\n\tEND\r\n"); differences.Tables[0].Target.Triggers[0].IsInsteadOfTrigger.Should().BeFalse(); differences.Tables[0].Triggers.Should().HaveCount(1); @@ -712,8 +712,8 @@ public async Task CompareAsync() differences.Tables[0].Triggers[0].Properties[0].Source.Should().Be(true); differences.Tables[0].Triggers[0].Properties[0].Target.Should().Be(false); differences.Tables[0].Triggers[0].Properties[1].Name.Should().Be("Code"); - differences.Tables[0].Triggers[0].Properties[1].Source.Should().Be("CREATE TRIGGER [TriggerDifference]\r\n\tON [dbo].[TableDifference]\r\n\tINSTEAD OF INSERT\r\n\tAS\r\n\tBEGIN\r\n\t\tPRINT 'From source'\r\n\tEND"); - differences.Tables[0].Triggers[0].Properties[1].Target.Should().Be("CREATE TRIGGER [TriggerDifference]\r\n\tON [dbo].[TableDifference]\r\n\tFOR INSERT\r\n\tAS\r\n\tBEGIN\r\n\t\tPRINT 'From target'\r\n\tEND"); + differences.Tables[0].Triggers[0].Properties[1].Source.Should().Be("CREATE TRIGGER [TriggerDifference]\r\n\tON [dbo].[TableDifference]\r\n\tINSTEAD OF INSERT\r\n\tAS\r\n\tBEGIN\r\n\t\tPRINT 'From source'\r\n\tEND\r\n"); + differences.Tables[0].Triggers[0].Properties[1].Target.Should().Be("CREATE TRIGGER [TriggerDifference]\r\n\tON [dbo].[TableDifference]\r\n\tFOR INSERT\r\n\tAS\r\n\tBEGIN\r\n\t\tPRINT 'From target'\r\n\tEND\r\n"); differences.Tables[0].Triggers[0].Source.Should().BeSameAs(differences.Tables[0].Source.Triggers[0]); differences.Tables[0].Triggers[0].Target.Should().BeSameAs(differences.Tables[0].Target.Triggers[0]); differences.Tables[0].Triggers[0].Type.Should().Be(SqlObjectDifferenceType.Different); @@ -827,7 +827,7 @@ public async Task CompareAsync() differences.Tables[1].Target.Schema.Should().Be("dbo"); differences.Tables[1].Target.Triggers.Should().HaveCount(1); differences.Tables[1].Target.Triggers[0].Name.Should().Be("TriggerTarget"); - differences.Tables[1].Target.Triggers[0].Code.Should().Be("CREATE TRIGGER [TriggerTarget]\r\n\tON [dbo].[TableTarget]\r\n\tFOR DELETE, INSERT, UPDATE\r\n\tAS\r\n\tBEGIN\r\n\t\tSET NOCOUNT ON\r\n\tEND"); + differences.Tables[1].Target.Triggers[0].Code.Should().Be("CREATE TRIGGER [TriggerTarget]\r\n\tON [dbo].[TableTarget]\r\n\tFOR DELETE, INSERT, UPDATE\r\n\tAS\r\n\tBEGIN\r\n\t\tSET NOCOUNT ON\r\n\tEND\r\n"); differences.Tables[1].Target.Triggers[0].IsInsteadOfTrigger.Should().BeFalse(); differences.Tables[1].Target.UniqueConstraints.Should().HaveCount(1); differences.Tables[1].Target.UniqueConstraints[0].Columns.Should().HaveCount(1); @@ -1047,7 +1047,7 @@ public async Task CompareAsync() differences.Tables[4].Source.PrimaryKey.Type.Should().Be("CLUSTERED"); differences.Tables[4].Source.Triggers.Should().HaveCount(1); differences.Tables[4].Source.Triggers[0].Name.Should().Be("TriggerSource"); - differences.Tables[4].Source.Triggers[0].Code.Should().Be("CREATE TRIGGER [TriggerSource]\r\n\tON [dbo].[TableSource]\r\n\tFOR DELETE, INSERT, UPDATE\r\n\tAS\r\n\tBEGIN\r\n\t\tSET NOCOUNT ON\r\n\tEND"); + differences.Tables[4].Source.Triggers[0].Code.Should().Be("CREATE TRIGGER [TriggerSource]\r\n\tON [dbo].[TableSource]\r\n\tFOR DELETE, INSERT, UPDATE\r\n\tAS\r\n\tBEGIN\r\n\t\tSET NOCOUNT ON\r\n\tEND\r\n"); differences.Tables[4].Source.Triggers[0].IsInsteadOfTrigger.Should().BeFalse(); differences.Tables[4].Source.UniqueConstraints.Should().HaveCount(1); differences.Tables[4].Source.UniqueConstraints[0].Columns.Should().HaveCount(1); @@ -1092,8 +1092,8 @@ public async Task CompareAsync() differences.Views[0].Target.Schema.Should().Be("dbo"); differences.Views[0].Properties.Should().HaveCount(1); differences.Views[0].Properties[0].Name.Should().Be("Code"); - differences.Views[0].Properties[0].Source.Should().Be("CREATE VIEW [dbo].[ViewDifference]\r\n\tAS SELECT * FROM [TableDifference] WHERE [Type] = 10"); - differences.Views[0].Properties[0].Target.Should().Be("CREATE VIEW [dbo].[ViewDifference]\r\n\tAS SELECT * FROM [TableDifference] WHERE [Type] = 'The type'"); + differences.Views[0].Properties[0].Source.Should().Be("CREATE VIEW [dbo].[ViewDifference]\r\n\tAS SELECT * FROM [TableDifference] WHERE [Type] = 10\r\n"); + differences.Views[0].Properties[0].Target.Should().Be("CREATE VIEW [dbo].[ViewDifference]\r\n\tAS SELECT * FROM [TableDifference] WHERE [Type] = 'The type'\r\n"); differences.Views[1].Source.Should().BeNull(); differences.Views[1].Target.Name.Should().Be("ViewTarget"); diff --git a/tests/Testing.Databases.SqlServer.Tests/Testing.Databases.SqlServer.Tests.csproj b/tests/Testing.Databases.SqlServer.Tests/Testing.Databases.SqlServer.Tests.csproj index 01bd981..d2b7097 100644 --- a/tests/Testing.Databases.SqlServer.Tests/Testing.Databases.SqlServer.Tests.csproj +++ b/tests/Testing.Databases.SqlServer.Tests/Testing.Databases.SqlServer.Tests.csproj @@ -9,10 +9,10 @@ - + PreserveNewest - + PreserveNewest @@ -35,9 +35,9 @@ - - - + + + From 6378685837ed8be974d543ecc38f776dad8ddd84 Mon Sep 17 00:00:00 2001 From: Gilles TOURREAU Date: Wed, 24 Sep 2025 14:38:27 +0200 Subject: [PATCH 15/61] Fix unit tests --- .../SqlServerDacExtensionsTest.cs | 4 ++-- .../SqlServerDatabaseInitializerTest.cs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/Testing.Databases.SqlServer.Dac.Tests/SqlServerDacExtensionsTest.cs b/tests/Testing.Databases.SqlServer.Dac.Tests/SqlServerDacExtensionsTest.cs index 003319f..2d9d268 100644 --- a/tests/Testing.Databases.SqlServer.Dac.Tests/SqlServerDacExtensionsTest.cs +++ b/tests/Testing.Databases.SqlServer.Dac.Tests/SqlServerDacExtensionsTest.cs @@ -28,7 +28,7 @@ public void DeployDacPackage(bool withSettings) settings = new SqlServerDacDeploymentSettings(); } - var database = server.DeployDacPackage("Testing.Databases.SqlServer.Tests.DacPac.dacpac", "SqlServerDacExtensionsTest_DeployDacPackage", settings); + var database = server.DeployDacPackage("PosInformatique.Testing.Databases.SqlServer.Tests.DacPac.dacpac", "SqlServerDacExtensionsTest_DeployDacPackage", settings); var table = database.ExecuteQuery("SELECT * FROM MyTable"); @@ -54,7 +54,7 @@ public void DeployDacPackage_WithSpecificDataFileName() DataFileName = Path.Combine(temporaryFolder.Path, "TheSpecificDataFileName.mdf"), }; - var database = server.DeployDacPackage("Testing.Databases.SqlServer.Tests.DacPac.dacpac", "SqlServerDacExtensionsTest_DeployDacPackage_WithSpecificDataFileName", settings); + var database = server.DeployDacPackage("PosInformatique.Testing.Databases.SqlServer.Tests.DacPac.dacpac", "SqlServerDacExtensionsTest_DeployDacPackage_WithSpecificDataFileName", settings); var table = database.ExecuteQuery("SELECT * FROM MyTable"); diff --git a/tests/Testing.Databases.SqlServer.Dac.Tests/SqlServerDatabaseInitializerTest.cs b/tests/Testing.Databases.SqlServer.Dac.Tests/SqlServerDatabaseInitializerTest.cs index 501a1f5..f036895 100644 --- a/tests/Testing.Databases.SqlServer.Dac.Tests/SqlServerDatabaseInitializerTest.cs +++ b/tests/Testing.Databases.SqlServer.Dac.Tests/SqlServerDatabaseInitializerTest.cs @@ -18,7 +18,7 @@ public class SqlServerDatabaseInitializerTest : IClassFixture Date: Wed, 24 Sep 2025 14:49:12 +0200 Subject: [PATCH 16/61] Update github-actions-ci.yaml --- .github/workflows/github-actions-ci.yaml | 70 +++++++++++++++--------- 1 file changed, 43 insertions(+), 27 deletions(-) diff --git a/.github/workflows/github-actions-ci.yaml b/.github/workflows/github-actions-ci.yaml index 7457777..56d448a 100644 --- a/.github/workflows/github-actions-ci.yaml +++ b/.github/workflows/github-actions-ci.yaml @@ -8,6 +8,46 @@ on: jobs: build: + runs-on: ubuntu-latest + services: + sqlserver: + image: mcr.microsoft.com/mssql/server:2022-latest + env: + SA_PASSWORD: "Your_password123" + ACCEPT_EULA: "Y" + ports: + - 1433:1433 + options: >- + --health-cmd "timeout 5s /opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P Your_password123 -Q 'SELECT 1' || exit 1" + --health-interval 10s + --health-timeout 5s + --health-retries 10 + steps: + - uses: actions/checkout@v4 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 8.0.x + + - name: Restore dependencies + run: dotnet restore PosInformatique.Testing.Databases.sln + + - name: Build solution + run: dotnet build PosInformatique.Testing.Databases.sln --configuration Release --no-restore + + - name: Restore samples + run: dotnet restore samples/PosInformatique.Testing.Databases.Samples.sln + + - name: Build samples + run: dotnet build samples/PosInformatique.Testing.Databases.Samples.sln --configuration Debug --no-restore + + - name: Run tests + run: dotnet test PosInformatique.Testing.Databases.sln --configuration Debug --no-build --logger:"trx;LogFileName=test_results.trx" + env: + SQL_SERVER_UNIT_TESTS_CONNECTION_STRING: "Data Source=localhost,1433;Database=master;User Id=sa;Password=Your_password123;TrustServerCertificate=True;" + + build-samples: runs-on: windows-latest steps: - uses: actions/checkout@v4 @@ -16,34 +56,10 @@ jobs: uses: nuget/setup-nuget@v2 - name: Restore NuGet packages - run: nuget restore PosInformatique.Testing.Databases.sln - + run: nuget restore samples/PosInformatique.Testing.Databases.Samples.sln + - name: Add msbuild to PATH uses: microsoft/setup-msbuild@v2 - - name: Build - run: msbuild "PosInformatique.Testing.Databases.sln" /p:Configuration=Debug - - - name: Restore NuGet packages - run: nuget restore "samples/PosInformatique.Testing.Databases.Samples.sln" - - - name: Build the samples + - name: Build samples with Visual Studio run: msbuild "samples/PosInformatique.Testing.Databases.Samples.sln" /p:Configuration=Debug - - - name: Creates the LocalDB for the tests - shell: cmd - run: SqlLocalDB create posinfo-tests - - - name: Creates the SQL Login service accounts for the tests - shell: cmd - run: sqlcmd -S "(localDB)\posinfo-tests" -Q "IF NOT EXISTS (SELECT 1 FROM [sys].[server_principals] WHERE [Name] = 'ServiceAccountLogin') CREATE LOGIN [ServiceAccountLogin] WITH PASSWORD = 'P@ssw0rd'" - - # Use this fix https://github.com/microsoft/vstest-action/issues/31#issuecomment-2159463764 - - name: Test with the dotnet CLI - uses: rusty-bender/vstest-action@main - with: - searchFolder: .\ - testAssembly: | - /tests/**/*tests.dll - !./**/*TestAdapter.dll - !./**/obj/** From c278445a768c73d86f889465692339f7e8952552 Mon Sep 17 00:00:00 2001 From: Gilles TOURREAU Date: Wed, 24 Sep 2025 14:55:58 +0200 Subject: [PATCH 17/61] Update github-actions-ci.yaml --- .github/workflows/github-actions-ci.yaml | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/.github/workflows/github-actions-ci.yaml b/.github/workflows/github-actions-ci.yaml index 56d448a..638fe23 100644 --- a/.github/workflows/github-actions-ci.yaml +++ b/.github/workflows/github-actions-ci.yaml @@ -13,15 +13,10 @@ jobs: sqlserver: image: mcr.microsoft.com/mssql/server:2022-latest env: - SA_PASSWORD: "Your_password123" + SA_PASSWORD: "P@ssw0rd12345!" ACCEPT_EULA: "Y" ports: - 1433:1433 - options: >- - --health-cmd "timeout 5s /opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P Your_password123 -Q 'SELECT 1' || exit 1" - --health-interval 10s - --health-timeout 5s - --health-retries 10 steps: - uses: actions/checkout@v4 @@ -42,10 +37,19 @@ jobs: - name: Build samples run: dotnet build samples/PosInformatique.Testing.Databases.Samples.sln --configuration Debug --no-restore + - name: Install SQL Server command-line tools + run: | + curl https://packages.microsoft.com/keys/microsoft.asc | sudo apt-key add - + curl https://packages.microsoft.com/config/ubuntu/20.04/prod.list | sudo tee /etc/apt/sources.list.d/msprod.list + sudo apt-get update + sudo apt-get install -y mssql-tools + echo 'export PATH="$PATH:/opt/mssql-tools/bin"' >> ~/.bashrc + source ~/.bashrc + - name: Run tests run: dotnet test PosInformatique.Testing.Databases.sln --configuration Debug --no-build --logger:"trx;LogFileName=test_results.trx" env: - SQL_SERVER_UNIT_TESTS_CONNECTION_STRING: "Data Source=localhost,1433;Database=master;User Id=sa;Password=Your_password123;TrustServerCertificate=True;" + SQL_SERVER_UNIT_TESTS_CONNECTION_STRING: "Data Source=localhost,1433;Database=master;User Id=sa;Password=P@ssw0rd12345!;TrustServerCertificate=True;" build-samples: runs-on: windows-latest From 733e91e6635b1ed3021281473a950c060704afb3 Mon Sep 17 00:00:00 2001 From: Gilles TOURREAU Date: Wed, 24 Sep 2025 15:00:05 +0200 Subject: [PATCH 18/61] Fix build previously failed. --- src/Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 135a517..9b643d3 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -49,7 +49,7 @@ - + <_Parameter1>$(AssemblyName).Tests From 8fd746ee1a5dd74cba8c4ca2257914266a99f5a9 Mon Sep 17 00:00:00 2001 From: Gilles TOURREAU Date: Wed, 24 Sep 2025 15:36:56 +0200 Subject: [PATCH 19/61] Update github-actions-ci.yaml --- .github/workflows/github-actions-ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/github-actions-ci.yaml b/.github/workflows/github-actions-ci.yaml index 638fe23..b631901 100644 --- a/.github/workflows/github-actions-ci.yaml +++ b/.github/workflows/github-actions-ci.yaml @@ -47,7 +47,7 @@ jobs: source ~/.bashrc - name: Run tests - run: dotnet test PosInformatique.Testing.Databases.sln --configuration Debug --no-build --logger:"trx;LogFileName=test_results.trx" + run: dotnet test **/*.Tests.csproj --configuration Release --no-build env: SQL_SERVER_UNIT_TESTS_CONNECTION_STRING: "Data Source=localhost,1433;Database=master;User Id=sa;Password=P@ssw0rd12345!;TrustServerCertificate=True;" From 2a7ec9b1f8726333c134f6d33061100b618bb6a1 Mon Sep 17 00:00:00 2001 From: Gilles TOURREAU Date: Wed, 24 Sep 2025 15:46:01 +0200 Subject: [PATCH 20/61] Update github-actions-ci.yaml --- .github/workflows/github-actions-ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/github-actions-ci.yaml b/.github/workflows/github-actions-ci.yaml index b631901..6a4ba4d 100644 --- a/.github/workflows/github-actions-ci.yaml +++ b/.github/workflows/github-actions-ci.yaml @@ -47,7 +47,7 @@ jobs: source ~/.bashrc - name: Run tests - run: dotnet test **/*.Tests.csproj --configuration Release --no-build + run: dotnet test PosInformatique.Testing.Databases.sln --configuration Release --no-build env: SQL_SERVER_UNIT_TESTS_CONNECTION_STRING: "Data Source=localhost,1433;Database=master;User Id=sa;Password=P@ssw0rd12345!;TrustServerCertificate=True;" From 252f246344ec823c2653847201b20b767a9d2572 Mon Sep 17 00:00:00 2001 From: Gilles TOURREAU Date: Wed, 24 Sep 2025 15:52:12 +0200 Subject: [PATCH 21/61] Update github-actions-ci.yaml --- .github/workflows/github-actions-ci.yaml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/github-actions-ci.yaml b/.github/workflows/github-actions-ci.yaml index 6a4ba4d..ee59920 100644 --- a/.github/workflows/github-actions-ci.yaml +++ b/.github/workflows/github-actions-ci.yaml @@ -43,8 +43,7 @@ jobs: curl https://packages.microsoft.com/config/ubuntu/20.04/prod.list | sudo tee /etc/apt/sources.list.d/msprod.list sudo apt-get update sudo apt-get install -y mssql-tools - echo 'export PATH="$PATH:/opt/mssql-tools/bin"' >> ~/.bashrc - source ~/.bashrc + echo "/opt/mssql-tools/bin" >> $GITHUB_PATH - name: Run tests run: dotnet test PosInformatique.Testing.Databases.sln --configuration Release --no-build From c4a9422ca573604e6e6568d477c0d17dea15e671 Mon Sep 17 00:00:00 2001 From: Gilles TOURREAU Date: Wed, 24 Sep 2025 16:07:56 +0200 Subject: [PATCH 22/61] Fix unit tests previously failed. --- .../SqlServerDatabaseInitializerTest.cs | 8 ++++---- ...meworkDatabaseInitializerExtensionsTest.cs | 4 ++-- .../ConnectionStrings.cs | 12 +++++++++++ .../SqlCmdDatabaseInitializerTest.cs | 6 +++--- .../SqlServerDatabaseComparerTest.cs | 20 +++++++++---------- 5 files changed, 31 insertions(+), 19 deletions(-) diff --git a/tests/Testing.Databases.SqlServer.Dac.Tests/SqlServerDatabaseInitializerTest.cs b/tests/Testing.Databases.SqlServer.Dac.Tests/SqlServerDatabaseInitializerTest.cs index f036895..73695f8 100644 --- a/tests/Testing.Databases.SqlServer.Dac.Tests/SqlServerDatabaseInitializerTest.cs +++ b/tests/Testing.Databases.SqlServer.Dac.Tests/SqlServerDatabaseInitializerTest.cs @@ -35,7 +35,7 @@ public void Test1() this.initializer.IsInitialized.Should().BeTrue(); var currentUser = this.database.ExecuteQuery("SELECT SUSER_NAME()"); - currentUser.Rows[0][0].Should().Be($"{Environment.UserDomainName}\\{Environment.UserName}"); + currentUser.Rows[0][0].Should().Be(ConnectionStrings.ExtractUserName(ConnectionString)); // Check the constructor has been called var table = this.database.ExecuteQuery("SELECT * FROM MyTable"); @@ -58,7 +58,7 @@ public void Test2() this.initializer.IsInitialized.Should().BeTrue(); var currentUser = this.database.ExecuteQuery("SELECT SUSER_NAME()"); - currentUser.Rows[0][0].Should().Be($"{Environment.UserDomainName}\\{Environment.UserName}"); + currentUser.Rows[0][0].Should().Be(ConnectionStrings.ExtractUserName(ConnectionString)); // Check the constructor has been called var table = this.database.ExecuteQuery("SELECT * FROM MyTable"); @@ -81,7 +81,7 @@ public async Task Test1Async() this.initializer.IsInitialized.Should().BeTrue(); var currentUser = await this.database.ExecuteQueryAsync("SELECT SUSER_NAME()"); - currentUser.Rows[0][0].Should().Be($"{Environment.UserDomainName}\\{Environment.UserName}"); + currentUser.Rows[0][0].Should().Be(ConnectionStrings.ExtractUserName(ConnectionString)); // Check the constructor has been called var table = await this.database.ExecuteQueryAsync("SELECT * FROM MyTable"); @@ -104,7 +104,7 @@ public async Task Test2Async() this.initializer.IsInitialized.Should().BeTrue(); var currentUser = await this.database.ExecuteQueryAsync("SELECT SUSER_NAME()"); - currentUser.Rows[0][0].Should().Be($"{Environment.UserDomainName}\\{Environment.UserName}"); + currentUser.Rows[0][0].Should().Be(ConnectionStrings.ExtractUserName(ConnectionString)); // Check the constructor has been called var table = await this.database.ExecuteQueryAsync("SELECT * FROM MyTable"); diff --git a/tests/Testing.Databases.SqlServer.EntityFramework.Tests/EntityFrameworkDatabaseInitializerExtensionsTest.cs b/tests/Testing.Databases.SqlServer.EntityFramework.Tests/EntityFrameworkDatabaseInitializerExtensionsTest.cs index ac25658..503d882 100644 --- a/tests/Testing.Databases.SqlServer.EntityFramework.Tests/EntityFrameworkDatabaseInitializerExtensionsTest.cs +++ b/tests/Testing.Databases.SqlServer.EntityFramework.Tests/EntityFrameworkDatabaseInitializerExtensionsTest.cs @@ -36,7 +36,7 @@ public EntityFrameworkDatabaseInitializerExtensionsTest(SqlServerDatabaseInitial public void Test1() { var currentUser = this.database.ExecuteQuery("SELECT SUSER_NAME()"); - currentUser.Rows[0][0].Should().Be($"{Environment.UserDomainName}\\{Environment.UserName}"); + currentUser.Rows[0][0].Should().Be(ConnectionStrings.ExtractUserName(ConnectionString)); // Check the constructor has been called var table = this.database.ExecuteQuery("SELECT * FROM MyTable"); @@ -57,7 +57,7 @@ public void Test1() public void Test2() { var currentUser = this.database.ExecuteQuery("SELECT SUSER_NAME()"); - currentUser.Rows[0][0].Should().Be($"{Environment.UserDomainName}\\{Environment.UserName}"); + currentUser.Rows[0][0].Should().Be(ConnectionStrings.ExtractUserName(ConnectionString)); // Check the constructor has been called var table = this.database.ExecuteQuery("SELECT * FROM MyTable"); diff --git a/tests/Testing.Databases.SqlServer.Shared.Tests/ConnectionStrings.cs b/tests/Testing.Databases.SqlServer.Shared.Tests/ConnectionStrings.cs index 159457a..eac8d77 100644 --- a/tests/Testing.Databases.SqlServer.Shared.Tests/ConnectionStrings.cs +++ b/tests/Testing.Databases.SqlServer.Shared.Tests/ConnectionStrings.cs @@ -26,5 +26,17 @@ public static string Get(string databaseName = "master") return connectionStringBuilder.ToString(); } + + public static string ExtractUserName(string connectionString) + { + var connectionStringBuilder = new SqlConnectionStringBuilder(connectionString); + + if (connectionStringBuilder.IntegratedSecurity == true) + { + return $"{Environment.UserDomainName}\\{Environment.UserName}"; + } + + return connectionStringBuilder.UserID; + } } } diff --git a/tests/Testing.Databases.SqlServer.SqlCmd.Tests/SqlCmdDatabaseInitializerTest.cs b/tests/Testing.Databases.SqlServer.SqlCmd.Tests/SqlCmdDatabaseInitializerTest.cs index 144e6bd..8fd12ba 100644 --- a/tests/Testing.Databases.SqlServer.SqlCmd.Tests/SqlCmdDatabaseInitializerTest.cs +++ b/tests/Testing.Databases.SqlServer.SqlCmd.Tests/SqlCmdDatabaseInitializerTest.cs @@ -44,7 +44,7 @@ public void Test1() this.initializer.IsInitialized.Should().BeTrue(); var currentUser = this.database.ExecuteQuery("SELECT SUSER_NAME()"); - currentUser.Rows[0][0].Should().Be($"{Environment.UserDomainName}\\{Environment.UserName}"); + currentUser.Rows[0][0].Should().Be(ConnectionStrings.ExtractUserName(ConnectionString)); // Check the constructor has been called var table = this.database.ExecuteQuery("SELECT * FROM MyTable"); @@ -67,7 +67,7 @@ public void Test2() this.initializer.IsInitialized.Should().BeTrue(); var currentUser = this.database.ExecuteQuery("SELECT SUSER_NAME()"); - currentUser.Rows[0][0].Should().Be($"{Environment.UserDomainName}\\{Environment.UserName}"); + currentUser.Rows[0][0].Should().Be(ConnectionStrings.ExtractUserName(ConnectionString)); // Check the constructor has been called var table = this.database.ExecuteQuery("SELECT * FROM MyTable"); @@ -113,7 +113,7 @@ public async Task Test2Async() this.initializer.IsInitialized.Should().BeTrue(); var currentUser = await this.database.ExecuteQueryAsync("SELECT SUSER_NAME()"); - currentUser.Rows[0][0].Should().Be($"{Environment.UserDomainName}\\{Environment.UserName}"); + currentUser.Rows[0][0].Should().Be(ConnectionStrings.ExtractUserName(ConnectionString)); // Check the constructor has been called var table = await this.database.ExecuteQueryAsync("SELECT * FROM MyTable"); diff --git a/tests/Testing.Databases.SqlServer.Tests/SqlServerDatabaseComparerTest.cs b/tests/Testing.Databases.SqlServer.Tests/SqlServerDatabaseComparerTest.cs index aff169b..d5fbc41 100644 --- a/tests/Testing.Databases.SqlServer.Tests/SqlServerDatabaseComparerTest.cs +++ b/tests/Testing.Databases.SqlServer.Tests/SqlServerDatabaseComparerTest.cs @@ -40,8 +40,8 @@ public async Task CompareAsync() differences.StoredProcedures[0].Target.Schema.Should().Be("dbo"); differences.StoredProcedures[0].Properties.Should().HaveCount(1); differences.StoredProcedures[0].Properties[0].Name.Should().Be("Code"); - differences.StoredProcedures[0].Properties[0].Source.Should().Be("CREATE PROCEDURE [dbo].[StoredProcedureDifference]\r\n\t@param1 int = 0,\r\n\t@param2 int\r\nAS\r\n\tSELECT @param2\r\nRETURN 0\r\n"); - differences.StoredProcedures[0].Properties[0].Target.Should().Be("CREATE PROCEDURE [dbo].[StoredProcedureDifference]\r\n\t@param1 int = 0,\r\n\t@param2 int\r\nAS\r\n\tSELECT @param1\r\nRETURN 0\r\n"); + differences.StoredProcedures[0].Properties[0].Source.Should().Be($"CREATE PROCEDURE [dbo].[StoredProcedureDifference]{Environment.NewLine}\t@param1 int = 0,{Environment.NewLine}\t@param2 int{Environment.NewLine}AS{Environment.NewLine}\tSELECT @param2{Environment.NewLine}RETURN 0{Environment.NewLine}"); + differences.StoredProcedures[0].Properties[0].Target.Should().Be($"CREATE PROCEDURE [dbo].[StoredProcedureDifference]{Environment.NewLine}\t@param1 int = 0,{Environment.NewLine}\t@param2 int{Environment.NewLine}AS{Environment.NewLine}\tSELECT @param1{Environment.NewLine}RETURN 0{Environment.NewLine}"); differences.StoredProcedures[1].Source.Should().BeNull(); differences.StoredProcedures[1].Target.Name.Should().Be("StoredProcedureTarget"); @@ -697,13 +697,13 @@ public async Task CompareAsync() differences.Tables[0].Source.Triggers.Should().HaveCount(1); differences.Tables[0].Source.Triggers[0].Name.Should().Be("TriggerDifference"); - differences.Tables[0].Source.Triggers[0].Code.Should().Be("CREATE TRIGGER [TriggerDifference]\r\n\tON [dbo].[TableDifference]\r\n\tINSTEAD OF INSERT\r\n\tAS\r\n\tBEGIN\r\n\t\tPRINT 'From source'\r\n\tEND\r\n"); + differences.Tables[0].Source.Triggers[0].Code.Should().Be($"CREATE TRIGGER [TriggerDifference]{Environment.NewLine}\tON [dbo].[TableDifference]{Environment.NewLine}\tINSTEAD OF INSERT{Environment.NewLine}\tAS{Environment.NewLine}\tBEGIN{Environment.NewLine}\t\tPRINT 'From source'{Environment.NewLine}\tEND{Environment.NewLine}"); differences.Tables[0].Source.Triggers[0].IsInsteadOfTrigger.Should().BeTrue(); differences.Tables[0].Target.Triggers.Should().HaveCount(1); differences.Tables[0].Target.Triggers[0].Name.Should().Be("TriggerDifference"); - differences.Tables[0].Target.Triggers[0].Code.Should().Be("CREATE TRIGGER [TriggerDifference]\r\n\tON [dbo].[TableDifference]\r\n\tFOR INSERT\r\n\tAS\r\n\tBEGIN\r\n\t\tPRINT 'From target'\r\n\tEND\r\n"); + differences.Tables[0].Target.Triggers[0].Code.Should().Be($"CREATE TRIGGER [TriggerDifference]{Environment.NewLine}\tON [dbo].[TableDifference]{Environment.NewLine}\tFOR INSERT{Environment.NewLine}\tAS{Environment.NewLine}\tBEGIN{Environment.NewLine}\t\tPRINT 'From target'{Environment.NewLine}\tEND{Environment.NewLine}"); differences.Tables[0].Target.Triggers[0].IsInsteadOfTrigger.Should().BeFalse(); differences.Tables[0].Triggers.Should().HaveCount(1); @@ -712,8 +712,8 @@ public async Task CompareAsync() differences.Tables[0].Triggers[0].Properties[0].Source.Should().Be(true); differences.Tables[0].Triggers[0].Properties[0].Target.Should().Be(false); differences.Tables[0].Triggers[0].Properties[1].Name.Should().Be("Code"); - differences.Tables[0].Triggers[0].Properties[1].Source.Should().Be("CREATE TRIGGER [TriggerDifference]\r\n\tON [dbo].[TableDifference]\r\n\tINSTEAD OF INSERT\r\n\tAS\r\n\tBEGIN\r\n\t\tPRINT 'From source'\r\n\tEND\r\n"); - differences.Tables[0].Triggers[0].Properties[1].Target.Should().Be("CREATE TRIGGER [TriggerDifference]\r\n\tON [dbo].[TableDifference]\r\n\tFOR INSERT\r\n\tAS\r\n\tBEGIN\r\n\t\tPRINT 'From target'\r\n\tEND\r\n"); + differences.Tables[0].Triggers[0].Properties[1].Source.Should().Be($"CREATE TRIGGER [TriggerDifference]{Environment.NewLine}\tON [dbo].[TableDifference]{Environment.NewLine}\tINSTEAD OF INSERT{Environment.NewLine}\tAS{Environment.NewLine}\tBEGIN{Environment.NewLine}\t\tPRINT 'From source'{Environment.NewLine}\tEND{Environment.NewLine}"); + differences.Tables[0].Triggers[0].Properties[1].Target.Should().Be($"CREATE TRIGGER [TriggerDifference]{Environment.NewLine}\tON [dbo].[TableDifference]{Environment.NewLine}\tFOR INSERT{Environment.NewLine}\tAS{Environment.NewLine}\tBEGIN{Environment.NewLine}\t\tPRINT 'From target'{Environment.NewLine}\tEND{Environment.NewLine}"); differences.Tables[0].Triggers[0].Source.Should().BeSameAs(differences.Tables[0].Source.Triggers[0]); differences.Tables[0].Triggers[0].Target.Should().BeSameAs(differences.Tables[0].Target.Triggers[0]); differences.Tables[0].Triggers[0].Type.Should().Be(SqlObjectDifferenceType.Different); @@ -827,7 +827,7 @@ public async Task CompareAsync() differences.Tables[1].Target.Schema.Should().Be("dbo"); differences.Tables[1].Target.Triggers.Should().HaveCount(1); differences.Tables[1].Target.Triggers[0].Name.Should().Be("TriggerTarget"); - differences.Tables[1].Target.Triggers[0].Code.Should().Be("CREATE TRIGGER [TriggerTarget]\r\n\tON [dbo].[TableTarget]\r\n\tFOR DELETE, INSERT, UPDATE\r\n\tAS\r\n\tBEGIN\r\n\t\tSET NOCOUNT ON\r\n\tEND\r\n"); + differences.Tables[1].Target.Triggers[0].Code.Should().Be($"CREATE TRIGGER [TriggerTarget]{Environment.NewLine}\tON [dbo].[TableTarget]{Environment.NewLine}\tFOR DELETE, INSERT, UPDATE{Environment.NewLine}\tAS{Environment.NewLine}\tBEGIN{Environment.NewLine}\t\tSET NOCOUNT ON{Environment.NewLine}\tEND{Environment.NewLine}"); differences.Tables[1].Target.Triggers[0].IsInsteadOfTrigger.Should().BeFalse(); differences.Tables[1].Target.UniqueConstraints.Should().HaveCount(1); differences.Tables[1].Target.UniqueConstraints[0].Columns.Should().HaveCount(1); @@ -1047,7 +1047,7 @@ public async Task CompareAsync() differences.Tables[4].Source.PrimaryKey.Type.Should().Be("CLUSTERED"); differences.Tables[4].Source.Triggers.Should().HaveCount(1); differences.Tables[4].Source.Triggers[0].Name.Should().Be("TriggerSource"); - differences.Tables[4].Source.Triggers[0].Code.Should().Be("CREATE TRIGGER [TriggerSource]\r\n\tON [dbo].[TableSource]\r\n\tFOR DELETE, INSERT, UPDATE\r\n\tAS\r\n\tBEGIN\r\n\t\tSET NOCOUNT ON\r\n\tEND\r\n"); + differences.Tables[4].Source.Triggers[0].Code.Should().Be($"CREATE TRIGGER [TriggerSource]{Environment.NewLine}\tON [dbo].[TableSource]{Environment.NewLine}\tFOR DELETE, INSERT, UPDATE{Environment.NewLine}\tAS{Environment.NewLine}\tBEGIN{Environment.NewLine}\t\tSET NOCOUNT ON{Environment.NewLine}\tEND{Environment.NewLine}"); differences.Tables[4].Source.Triggers[0].IsInsteadOfTrigger.Should().BeFalse(); differences.Tables[4].Source.UniqueConstraints.Should().HaveCount(1); differences.Tables[4].Source.UniqueConstraints[0].Columns.Should().HaveCount(1); @@ -1092,8 +1092,8 @@ public async Task CompareAsync() differences.Views[0].Target.Schema.Should().Be("dbo"); differences.Views[0].Properties.Should().HaveCount(1); differences.Views[0].Properties[0].Name.Should().Be("Code"); - differences.Views[0].Properties[0].Source.Should().Be("CREATE VIEW [dbo].[ViewDifference]\r\n\tAS SELECT * FROM [TableDifference] WHERE [Type] = 10\r\n"); - differences.Views[0].Properties[0].Target.Should().Be("CREATE VIEW [dbo].[ViewDifference]\r\n\tAS SELECT * FROM [TableDifference] WHERE [Type] = 'The type'\r\n"); + differences.Views[0].Properties[0].Source.Should().Be($"CREATE VIEW [dbo].[ViewDifference]{Environment.NewLine}\tAS SELECT * FROM [TableDifference] WHERE [Type] = 10{Environment.NewLine}"); + differences.Views[0].Properties[0].Target.Should().Be($"CREATE VIEW [dbo].[ViewDifference]{Environment.NewLine}\tAS SELECT * FROM [TableDifference] WHERE [Type] = 'The type'{Environment.NewLine}"); differences.Views[1].Source.Should().BeNull(); differences.Views[1].Target.Name.Should().Be("ViewTarget"); From 4c9cad794eb51c5579e26626457fbfe8ab9ab0b6 Mon Sep 17 00:00:00 2001 From: Gilles TOURREAU Date: Wed, 24 Sep 2025 16:33:06 +0200 Subject: [PATCH 23/61] Update github-actions-ci.yaml --- .github/workflows/github-actions-ci.yaml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/workflows/github-actions-ci.yaml b/.github/workflows/github-actions-ci.yaml index ee59920..c90964f 100644 --- a/.github/workflows/github-actions-ci.yaml +++ b/.github/workflows/github-actions-ci.yaml @@ -46,10 +46,18 @@ jobs: echo "/opt/mssql-tools/bin" >> $GITHUB_PATH - name: Run tests - run: dotnet test PosInformatique.Testing.Databases.sln --configuration Release --no-build + run: dotnet test PosInformatique.Testing.Databases.sln --configuration Release --no-build --logger "junit;LogFilePath=./TestResults/junit.xml" env: SQL_SERVER_UNIT_TESTS_CONNECTION_STRING: "Data Source=localhost,1433;Database=master;User Id=sa;Password=P@ssw0rd12345!;TrustServerCertificate=True;" + - name: Publish tests report + uses: dorny/test-reporter@v1 + if: always() + with: + name: PosInformatique.Testing.Databases + path: ./TestResults/junit.xml + reporter: jest-junit + build-samples: runs-on: windows-latest steps: From 53cfb4b9589c42f3629d2b817b45bbe6f63d46d8 Mon Sep 17 00:00:00 2001 From: Gilles TOURREAU Date: Wed, 24 Sep 2025 16:51:37 +0200 Subject: [PATCH 24/61] Update github-actions-ci.yaml --- .github/workflows/github-actions-ci.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/github-actions-ci.yaml b/.github/workflows/github-actions-ci.yaml index c90964f..74e1751 100644 --- a/.github/workflows/github-actions-ci.yaml +++ b/.github/workflows/github-actions-ci.yaml @@ -17,6 +17,8 @@ jobs: ACCEPT_EULA: "Y" ports: - 1433:1433 + volumes: + - ${{ runner.temp }}/other_databases_path:/other_databases_path steps: - uses: actions/checkout@v4 @@ -49,6 +51,7 @@ jobs: run: dotnet test PosInformatique.Testing.Databases.sln --configuration Release --no-build --logger "junit;LogFilePath=./TestResults/junit.xml" env: SQL_SERVER_UNIT_TESTS_CONNECTION_STRING: "Data Source=localhost,1433;Database=master;User Id=sa;Password=P@ssw0rd12345!;TrustServerCertificate=True;" + SQL_SERVER_UNIT_TESTS_OTHER_DATA_PATH: "/other_databases_path" - name: Publish tests report uses: dorny/test-reporter@v1 From 869efcb334e993838e0e075cad6d2ec5c8558153 Mon Sep 17 00:00:00 2001 From: Gilles TOURREAU Date: Wed, 24 Sep 2025 16:55:10 +0200 Subject: [PATCH 25/61] Fix unit tests. --- .../SqlServerDacExtensionsTest.cs | 12 ++--- .../SqlServerDatabaseInitializerTest.cs | 12 ++--- ...sting.Databases.SqlServer.Dac.Tests.csproj | 4 -- .../OtherDatabasePath.cs | 54 +++++++++++++++++++ ...Databases.SqlServer.Shared.Tests.projitems | 1 + .../SqlObjectPropertyDifferenceTest.cs | 4 +- .../SqlServerTest.cs | 24 ++++----- .../TemporaryFolder.cs | 39 -------------- 8 files changed, 81 insertions(+), 69 deletions(-) create mode 100644 tests/Testing.Databases.SqlServer.Shared.Tests/OtherDatabasePath.cs delete mode 100644 tests/Testing.Databases.SqlServer.Tests/TemporaryFolder.cs diff --git a/tests/Testing.Databases.SqlServer.Dac.Tests/SqlServerDacExtensionsTest.cs b/tests/Testing.Databases.SqlServer.Dac.Tests/SqlServerDacExtensionsTest.cs index 2d9d268..134a733 100644 --- a/tests/Testing.Databases.SqlServer.Dac.Tests/SqlServerDacExtensionsTest.cs +++ b/tests/Testing.Databases.SqlServer.Dac.Tests/SqlServerDacExtensionsTest.cs @@ -45,13 +45,13 @@ public void DeployDacPackage_WithSpecificDataFileName() // Create existing database to be sure the database is recreated when deploying the database with a DACPAC CreateDatabase("SqlServerDacExtensionsTest_DeployDacPackage_WithSpecificDataFileName"); - using var temporaryFolder = TemporaryFolder.Create(); + using var otherDataPath = OtherDatabasePath.Create(); var server = new SqlServer(ConnectionString); var settings = new SqlServerDacDeploymentSettings() { - DataFileName = Path.Combine(temporaryFolder.Path, "TheSpecificDataFileName.mdf"), + DataFileName = Path.Combine(otherDataPath.Path, "TheSpecificDataFileName.mdf"), }; var database = server.DeployDacPackage("PosInformatique.Testing.Databases.SqlServer.Tests.DacPac.dacpac", "SqlServerDacExtensionsTest_DeployDacPackage_WithSpecificDataFileName", settings); @@ -65,19 +65,19 @@ public void DeployDacPackage_WithSpecificDataFileName() database.InsertInto("MyTable", new { Id = 2, Name = "Name 2" }); // Check the location of the database - File.Exists(Path.Combine(temporaryFolder.Path, "TheSpecificDataFileName.mdf")).Should().BeTrue(); - File.Exists(Path.Combine(temporaryFolder.Path, "TheSpecificDataFileName_log.ldf")).Should().BeTrue(); + File.Exists(Path.Combine(otherDataPath.Path, "TheSpecificDataFileName.mdf")).Should().BeTrue(); + File.Exists(Path.Combine(otherDataPath.Path, "TheSpecificDataFileName_log.ldf")).Should().BeTrue(); var result = database.ExecuteQuery("SELECT * FROM [sys].[database_files] ORDER BY [physical_name]"); result.Rows.Should().HaveCount(2); result.Rows[0]["name"].Should().Be("SqlServerDacExtensionsTest_DeployDacPackage_WithSpecificDataFileName"); - result.Rows[0]["physical_name"].Should().Be(Path.Combine(temporaryFolder.Path, "TheSpecificDataFileName.mdf")); + result.Rows[0]["physical_name"].Should().Be(Path.Combine(otherDataPath.Path, "TheSpecificDataFileName.mdf")); result.Rows[0]["type_desc"].Should().Be("ROWS"); result.Rows[1]["name"].Should().Be("TheSpecificDataFileName_log"); - result.Rows[1]["physical_name"].Should().Be(Path.Combine(temporaryFolder.Path, "TheSpecificDataFileName_log.ldf")); + result.Rows[1]["physical_name"].Should().Be(Path.Combine(otherDataPath.Path, "TheSpecificDataFileName_log.ldf")); result.Rows[1]["type_desc"].Should().Be("LOG"); // Delete the database (for deleting the temporary folder). diff --git a/tests/Testing.Databases.SqlServer.Dac.Tests/SqlServerDatabaseInitializerTest.cs b/tests/Testing.Databases.SqlServer.Dac.Tests/SqlServerDatabaseInitializerTest.cs index 73695f8..f6f9690 100644 --- a/tests/Testing.Databases.SqlServer.Dac.Tests/SqlServerDatabaseInitializerTest.cs +++ b/tests/Testing.Databases.SqlServer.Dac.Tests/SqlServerDatabaseInitializerTest.cs @@ -129,13 +129,13 @@ public void Initialize_WithSpecificDataFileName() // Create existing database to be sure the database is recreated when deploying the database with a DACPAC CreateDatabase("SqlServerDatabaseInitializerTest_Initialize_WithSpecificDataFileName"); - using var temporaryFolder = TemporaryFolder.Create(); + using var otherDataPath = OtherDatabasePath.Create(); var server = new SqlServer(ConnectionString); var settings = new SqlServerDacDeploymentSettings() { - DataFileName = Path.Combine(temporaryFolder.Path, "TheSpecificDataFileName.mdf"), + DataFileName = Path.Combine(otherDataPath.Path, "TheSpecificDataFileName.mdf"), }; var database = server.DeployDacPackage("PosInformatique.Testing.Databases.SqlServer.Tests.DacPac.dacpac", "SqlServerDatabaseInitializerTest_Initialize_WithSpecificDataFileName", settings); @@ -149,19 +149,19 @@ public void Initialize_WithSpecificDataFileName() database.InsertInto("MyTable", new { Id = 2, Name = "Name 2" }); // Check the location of the database - File.Exists(Path.Combine(temporaryFolder.Path, "TheSpecificDataFileName.mdf")).Should().BeTrue(); - File.Exists(Path.Combine(temporaryFolder.Path, "TheSpecificDataFileName_log.ldf")).Should().BeTrue(); + File.Exists(Path.Combine(otherDataPath.Path, "TheSpecificDataFileName.mdf")).Should().BeTrue(); + File.Exists(Path.Combine(otherDataPath.Path, "TheSpecificDataFileName_log.ldf")).Should().BeTrue(); var result = database.ExecuteQuery("SELECT * FROM [sys].[database_files] ORDER BY [physical_name]"); result.Rows.Should().HaveCount(2); result.Rows[0]["name"].Should().Be("SqlServerDatabaseInitializerTest_Initialize_WithSpecificDataFileName"); - result.Rows[0]["physical_name"].Should().Be(Path.Combine(temporaryFolder.Path, "TheSpecificDataFileName.mdf")); + result.Rows[0]["physical_name"].Should().Be(Path.Combine(otherDataPath.Path, "TheSpecificDataFileName.mdf")); result.Rows[0]["type_desc"].Should().Be("ROWS"); result.Rows[1]["name"].Should().Be("TheSpecificDataFileName_log"); - result.Rows[1]["physical_name"].Should().Be(Path.Combine(temporaryFolder.Path, "TheSpecificDataFileName_log.ldf")); + result.Rows[1]["physical_name"].Should().Be(Path.Combine(otherDataPath.Path, "TheSpecificDataFileName_log.ldf")); result.Rows[1]["type_desc"].Should().Be("LOG"); // Delete the database (for deleting the temporary folder). diff --git a/tests/Testing.Databases.SqlServer.Dac.Tests/Testing.Databases.SqlServer.Dac.Tests.csproj b/tests/Testing.Databases.SqlServer.Dac.Tests/Testing.Databases.SqlServer.Dac.Tests.csproj index ce34841..b83a2bd 100644 --- a/tests/Testing.Databases.SqlServer.Dac.Tests/Testing.Databases.SqlServer.Dac.Tests.csproj +++ b/tests/Testing.Databases.SqlServer.Dac.Tests/Testing.Databases.SqlServer.Dac.Tests.csproj @@ -4,10 +4,6 @@ net8.0 - - - - PreserveNewest diff --git a/tests/Testing.Databases.SqlServer.Shared.Tests/OtherDatabasePath.cs b/tests/Testing.Databases.SqlServer.Shared.Tests/OtherDatabasePath.cs new file mode 100644 index 0000000..cd4a1b9 --- /dev/null +++ b/tests/Testing.Databases.SqlServer.Shared.Tests/OtherDatabasePath.cs @@ -0,0 +1,54 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) P.O.S Informatique. All rights reserved. +// +//----------------------------------------------------------------------- + +namespace PosInformatique.Testing.Databases.SqlServer.Tests +{ + public sealed class OtherDatabasePath : IDisposable + { + private readonly bool deleteOnDispose; + + private OtherDatabasePath(string path, bool deleteOnDispose) + { + this.Path = path; + this.deleteOnDispose = deleteOnDispose; + } + + public string Path { get; } + + public static OtherDatabasePath Create() + { + var otherDataPath = Environment.GetEnvironmentVariable("SQL_SERVER_UNIT_TESTS_OTHER_DATA_PATH"); + + var deleteOnDispose = false; + + if (otherDataPath is null) + { + otherDataPath = System.IO.Path.Combine(System.IO.Path.GetTempPath(), "PosInformatique.Testing.Databases.SqlServer.Tests", Guid.NewGuid().ToString()); + + Directory.CreateDirectory(otherDataPath); + + deleteOnDispose = true; + } + + return new OtherDatabasePath(otherDataPath, deleteOnDispose); + } + + public void Dispose() + { + if (this.deleteOnDispose) + { + try + { + Directory.Delete(this.Path, true); + } + catch (IOException) + { + // Ignore the errors. + } + } + } + } +} diff --git a/tests/Testing.Databases.SqlServer.Shared.Tests/Testing.Databases.SqlServer.Shared.Tests.projitems b/tests/Testing.Databases.SqlServer.Shared.Tests/Testing.Databases.SqlServer.Shared.Tests.projitems index 9d1f76c..277c408 100644 --- a/tests/Testing.Databases.SqlServer.Shared.Tests/Testing.Databases.SqlServer.Shared.Tests.projitems +++ b/tests/Testing.Databases.SqlServer.Shared.Tests/Testing.Databases.SqlServer.Shared.Tests.projitems @@ -10,5 +10,6 @@ + \ No newline at end of file diff --git a/tests/Testing.Databases.SqlServer.Tests/Comparer/SqlObjectPropertyDifferenceTest.cs b/tests/Testing.Databases.SqlServer.Tests/Comparer/SqlObjectPropertyDifferenceTest.cs index ec44380..7159078 100644 --- a/tests/Testing.Databases.SqlServer.Tests/Comparer/SqlObjectPropertyDifferenceTest.cs +++ b/tests/Testing.Databases.SqlServer.Tests/Comparer/SqlObjectPropertyDifferenceTest.cs @@ -13,7 +13,7 @@ public void ToString_NotNull() { var difference = new SqlObjectPropertyDifference("The name", 12, 34); - difference.ToString().Should().Be("* The name:\r\n Source: 12\r\n Target: 34\r\n"); + difference.ToString().Should().Be($"* The name:{Environment.NewLine} Source: 12{Environment.NewLine} Target: 34{Environment.NewLine}"); } [Fact] @@ -21,7 +21,7 @@ public void ToString_Null() { var difference = new SqlObjectPropertyDifference("The name", null, null); - difference.ToString().Should().Be("* The name:\r\n Source: \r\n Target: \r\n"); + difference.ToString().Should().Be($"* The name:{Environment.NewLine} Source: {Environment.NewLine} Target: {Environment.NewLine}"); } } } \ No newline at end of file diff --git a/tests/Testing.Databases.SqlServer.Tests/SqlServerTest.cs b/tests/Testing.Databases.SqlServer.Tests/SqlServerTest.cs index 6383421..87b4a7a 100644 --- a/tests/Testing.Databases.SqlServer.Tests/SqlServerTest.cs +++ b/tests/Testing.Databases.SqlServer.Tests/SqlServerTest.cs @@ -53,13 +53,13 @@ public async Task CreateAndDelete(bool withCreationSettings) [Fact] public async Task CreateAndDelete_WithSpecificDataFileName() { - using var temporaryFolder = TemporaryFolder.Create(); + using var otherDataPath = OtherDatabasePath.Create(); var server = new SqlServer(ConnectionString); var settings = new SqlDatabaseCreationSettings() { - DataFileName = Path.Combine(temporaryFolder.Path, "TheSpecificDataFileName.mdf"), + DataFileName = Path.Combine(otherDataPath.Path, "TheSpecificDataFileName.mdf"), }; var database = server.CreateEmptyDatabase("CreateAndDeleteDB_WithSpecificDataFileName", settings); @@ -70,19 +70,19 @@ public async Task CreateAndDelete_WithSpecificDataFileName() table.Rows.Should().HaveCount(1); // Check the location of the database - File.Exists(Path.Combine(temporaryFolder.Path, "TheSpecificDataFileName.mdf")).Should().BeTrue(); - File.Exists(Path.Combine(temporaryFolder.Path, "TheSpecificDataFileName_log.ldf")).Should().BeTrue(); + File.Exists(Path.Combine(otherDataPath.Path, "TheSpecificDataFileName.mdf")).Should().BeTrue(); + File.Exists(Path.Combine(otherDataPath.Path, "TheSpecificDataFileName_log.ldf")).Should().BeTrue(); var result = database.ExecuteQuery("SELECT * FROM [sys].[database_files] ORDER BY [physical_name]"); result.Rows.Should().HaveCount(2); result.Rows[0]["name"].Should().Be("CreateAndDeleteDB_WithSpecificDataFileName"); - result.Rows[0]["physical_name"].Should().Be(Path.Combine(temporaryFolder.Path, "TheSpecificDataFileName.mdf")); + result.Rows[0]["physical_name"].Should().Be(Path.Combine(otherDataPath.Path, "TheSpecificDataFileName.mdf")); result.Rows[0]["type_desc"].Should().Be("ROWS"); result.Rows[1]["name"].Should().Be("TheSpecificDataFileName_log"); - result.Rows[1]["physical_name"].Should().Be(Path.Combine(temporaryFolder.Path, "TheSpecificDataFileName_log.ldf")); + result.Rows[1]["physical_name"].Should().Be(Path.Combine(otherDataPath.Path, "TheSpecificDataFileName_log.ldf")); result.Rows[1]["type_desc"].Should().Be("LOG"); // Delete the database @@ -114,13 +114,13 @@ public async Task CreateAndDeleteAsync() [Fact] public async Task CreateAndDeleteAsync_WithSpecificDataFileName() { - using var temporaryFolder = TemporaryFolder.Create(); + using var otherDataPath = OtherDatabasePath.Create(); var server = new SqlServer(ConnectionString); var settings = new SqlDatabaseCreationSettings() { - DataFileName = Path.Combine(temporaryFolder.Path, "TheSpecificDataFileNameAsync.mdf"), + DataFileName = Path.Combine(otherDataPath.Path, "TheSpecificDataFileNameAsync.mdf"), }; var database = await server.CreateEmptyDatabaseAsync("CreateAndDeleteDB_WithSpecificDataFileNameAsync", settings); @@ -131,19 +131,19 @@ public async Task CreateAndDeleteAsync_WithSpecificDataFileName() table.Rows.Should().HaveCount(1); // Check the location of the database - File.Exists(Path.Combine(temporaryFolder.Path, "TheSpecificDataFileNameAsync.mdf")).Should().BeTrue(); - File.Exists(Path.Combine(temporaryFolder.Path, "TheSpecificDataFileNameAsync_log.ldf")).Should().BeTrue(); + File.Exists(Path.Combine(otherDataPath.Path, "TheSpecificDataFileNameAsync.mdf")).Should().BeTrue(); + File.Exists(Path.Combine(otherDataPath.Path, "TheSpecificDataFileNameAsync_log.ldf")).Should().BeTrue(); var result = database.ExecuteQuery("SELECT * FROM [sys].[database_files] ORDER BY [physical_name]"); result.Rows.Should().HaveCount(2); result.Rows[0]["name"].Should().Be("CreateAndDeleteDB_WithSpecificDataFileNameAsync"); - result.Rows[0]["physical_name"].Should().Be(Path.Combine(temporaryFolder.Path, "TheSpecificDataFileNameAsync.mdf")); + result.Rows[0]["physical_name"].Should().Be(Path.Combine(otherDataPath.Path, "TheSpecificDataFileNameAsync.mdf")); result.Rows[0]["type_desc"].Should().Be("ROWS"); result.Rows[1]["name"].Should().Be("TheSpecificDataFileNameAsync_log"); - result.Rows[1]["physical_name"].Should().Be(Path.Combine(temporaryFolder.Path, "TheSpecificDataFileNameAsync_log.ldf")); + result.Rows[1]["physical_name"].Should().Be(Path.Combine(otherDataPath.Path, "TheSpecificDataFileNameAsync_log.ldf")); result.Rows[1]["type_desc"].Should().Be("LOG"); // Delete the database diff --git a/tests/Testing.Databases.SqlServer.Tests/TemporaryFolder.cs b/tests/Testing.Databases.SqlServer.Tests/TemporaryFolder.cs deleted file mode 100644 index f8220de..0000000 --- a/tests/Testing.Databases.SqlServer.Tests/TemporaryFolder.cs +++ /dev/null @@ -1,39 +0,0 @@ -//----------------------------------------------------------------------- -// -// Copyright (c) P.O.S Informatique. All rights reserved. -// -//----------------------------------------------------------------------- - -namespace PosInformatique.Testing.Databases.SqlServer.Tests -{ - public sealed class TemporaryFolder : IDisposable - { - private TemporaryFolder(string path) - { - this.Path = path; - } - - public string Path { get; } - - public static TemporaryFolder Create() - { - var temporaryFolder = System.IO.Path.Combine(System.IO.Path.GetTempPath(), "PosInformatique.Testing.Databases.SqlServer.Tests", Guid.NewGuid().ToString()); - - Directory.CreateDirectory(temporaryFolder); - - return new TemporaryFolder(temporaryFolder); - } - - public void Dispose() - { - try - { - Directory.Delete(this.Path, true); - } - catch (IOException) - { - // Ignore the errors. - } - } - } -} From 19e67d0f13375a502de9e1431f0d5e8bdcbc37c1 Mon Sep 17 00:00:00 2001 From: Gilles TOURREAU Date: Wed, 24 Sep 2025 16:57:39 +0200 Subject: [PATCH 26/61] Update github-actions-ci.yaml --- .github/workflows/github-actions-ci.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/github-actions-ci.yaml b/.github/workflows/github-actions-ci.yaml index 74e1751..a196755 100644 --- a/.github/workflows/github-actions-ci.yaml +++ b/.github/workflows/github-actions-ci.yaml @@ -18,7 +18,7 @@ jobs: ports: - 1433:1433 volumes: - - ${{ runner.temp }}/other_databases_path:/other_databases_path + - /tmp/other_databases_path:/tmp/other_databases_path steps: - uses: actions/checkout@v4 @@ -51,7 +51,7 @@ jobs: run: dotnet test PosInformatique.Testing.Databases.sln --configuration Release --no-build --logger "junit;LogFilePath=./TestResults/junit.xml" env: SQL_SERVER_UNIT_TESTS_CONNECTION_STRING: "Data Source=localhost,1433;Database=master;User Id=sa;Password=P@ssw0rd12345!;TrustServerCertificate=True;" - SQL_SERVER_UNIT_TESTS_OTHER_DATA_PATH: "/other_databases_path" + SQL_SERVER_UNIT_TESTS_OTHER_DATA_PATH: "/tmp/other_databases_path" - name: Publish tests report uses: dorny/test-reporter@v1 From 90d3555f634db70d7beeab92687023bf41ac69cb Mon Sep 17 00:00:00 2001 From: Gilles TOURREAU Date: Wed, 24 Sep 2025 17:04:31 +0200 Subject: [PATCH 27/61] Update github-actions-ci.yaml --- .github/workflows/github-actions-ci.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/github-actions-ci.yaml b/.github/workflows/github-actions-ci.yaml index a196755..0448b92 100644 --- a/.github/workflows/github-actions-ci.yaml +++ b/.github/workflows/github-actions-ci.yaml @@ -47,6 +47,10 @@ jobs: sudo apt-get install -y mssql-tools echo "/opt/mssql-tools/bin" >> $GITHUB_PATH + - name: Install JUnit logger + run: dotnet tool install --global JUnitXml.TestLogger + shell: bash + - name: Run tests run: dotnet test PosInformatique.Testing.Databases.sln --configuration Release --no-build --logger "junit;LogFilePath=./TestResults/junit.xml" env: From 69adfc8c7cebe5fcdb7fad78042be6ad1598c0d0 Mon Sep 17 00:00:00 2001 From: Gilles TOURREAU Date: Wed, 24 Sep 2025 17:09:25 +0200 Subject: [PATCH 28/61] Update github-actions-ci.yaml --- .github/workflows/github-actions-ci.yaml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/.github/workflows/github-actions-ci.yaml b/.github/workflows/github-actions-ci.yaml index 0448b92..b32594d 100644 --- a/.github/workflows/github-actions-ci.yaml +++ b/.github/workflows/github-actions-ci.yaml @@ -52,17 +52,22 @@ jobs: shell: bash - name: Run tests - run: dotnet test PosInformatique.Testing.Databases.sln --configuration Release --no-build --logger "junit;LogFilePath=./TestResults/junit.xml" + run: dotnet test PosInformatique.Testing.Databases.sln --configuration Release --no-build --logger "trx;LogFileName=test_results.trx" --results-directory ./TestResults env: SQL_SERVER_UNIT_TESTS_CONNECTION_STRING: "Data Source=localhost,1433;Database=master;User Id=sa;Password=P@ssw0rd12345!;TrustServerCertificate=True;" SQL_SERVER_UNIT_TESTS_OTHER_DATA_PATH: "/tmp/other_databases_path" + - name: Convert TRX to JUnit + uses: NasAmin/trx2junit@v3.0.0 + with: + input: ./TestResults + - name: Publish tests report uses: dorny/test-reporter@v1 if: always() with: name: PosInformatique.Testing.Databases - path: ./TestResults/junit.xml + path: ./TestResults/*.xml reporter: jest-junit build-samples: From 619608aafbd34c5c932e99aa1cbd1526b53eae37 Mon Sep 17 00:00:00 2001 From: Gilles TOURREAU Date: Wed, 24 Sep 2025 17:19:00 +0200 Subject: [PATCH 29/61] Update github-actions-ci.yaml --- .github/workflows/github-actions-ci.yaml | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/.github/workflows/github-actions-ci.yaml b/.github/workflows/github-actions-ci.yaml index b32594d..151b860 100644 --- a/.github/workflows/github-actions-ci.yaml +++ b/.github/workflows/github-actions-ci.yaml @@ -57,19 +57,6 @@ jobs: SQL_SERVER_UNIT_TESTS_CONNECTION_STRING: "Data Source=localhost,1433;Database=master;User Id=sa;Password=P@ssw0rd12345!;TrustServerCertificate=True;" SQL_SERVER_UNIT_TESTS_OTHER_DATA_PATH: "/tmp/other_databases_path" - - name: Convert TRX to JUnit - uses: NasAmin/trx2junit@v3.0.0 - with: - input: ./TestResults - - - name: Publish tests report - uses: dorny/test-reporter@v1 - if: always() - with: - name: PosInformatique.Testing.Databases - path: ./TestResults/*.xml - reporter: jest-junit - build-samples: runs-on: windows-latest steps: From 99a05d7445dd3c571c85c2b45c65d53cf425ad54 Mon Sep 17 00:00:00 2001 From: Gilles TOURREAU Date: Wed, 24 Sep 2025 17:24:49 +0200 Subject: [PATCH 30/61] Update github-actions-ci.yaml --- .github/workflows/github-actions-ci.yaml | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/.github/workflows/github-actions-ci.yaml b/.github/workflows/github-actions-ci.yaml index 151b860..df17cf1 100644 --- a/.github/workflows/github-actions-ci.yaml +++ b/.github/workflows/github-actions-ci.yaml @@ -9,6 +9,9 @@ on: jobs: build: runs-on: ubuntu-latest + permissions: + checks: write + pull-requests: write services: sqlserver: image: mcr.microsoft.com/mssql/server:2022-latest @@ -47,15 +50,18 @@ jobs: sudo apt-get install -y mssql-tools echo "/opt/mssql-tools/bin" >> $GITHUB_PATH - - name: Install JUnit logger - run: dotnet tool install --global JUnitXml.TestLogger - shell: bash - - name: Run tests run: dotnet test PosInformatique.Testing.Databases.sln --configuration Release --no-build --logger "trx;LogFileName=test_results.trx" --results-directory ./TestResults env: SQL_SERVER_UNIT_TESTS_CONNECTION_STRING: "Data Source=localhost,1433;Database=master;User Id=sa;Password=P@ssw0rd12345!;TrustServerCertificate=True;" SQL_SERVER_UNIT_TESTS_OTHER_DATA_PATH: "/tmp/other_databases_path" + + - name: Publish Test Results + uses: EnricoMi/publish-unit-test-result-action@v2 + if: (!cancelled()) + with: + files: | + TestResults/**/*.xml build-samples: runs-on: windows-latest From 6ca09e38e34630bfb4853e46dcaf377709bc0de6 Mon Sep 17 00:00:00 2001 From: Gilles TOURREAU Date: Wed, 24 Sep 2025 17:35:28 +0200 Subject: [PATCH 31/61] Fix unit tests --- .../SqlCmdProcessTest.cs | 2 +- .../Comparer/SqlObjectDifferencesTest.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Testing.Databases.SqlServer.SqlCmd.Tests/SqlCmdProcessTest.cs b/tests/Testing.Databases.SqlServer.SqlCmd.Tests/SqlCmdProcessTest.cs index d62d798..3c6c158 100644 --- a/tests/Testing.Databases.SqlServer.SqlCmd.Tests/SqlCmdProcessTest.cs +++ b/tests/Testing.Databases.SqlServer.SqlCmd.Tests/SqlCmdProcessTest.cs @@ -19,7 +19,7 @@ public void WaitForExit_Disposed() process.Invoking(p => p.WaitForExit()) .Should().ThrowExactly() - .WithMessage("Cannot access a disposed object.\r\nObject name: 'PosInformatique.Testing.Databases.SqlServer.SqlCmdProcess'.") + .WithMessage($"Cannot access a disposed object.{Environment.NewLine}Object name: 'PosInformatique.Testing.Databases.SqlServer.SqlCmdProcess'.") .Which.ObjectName.Should().Be("PosInformatique.Testing.Databases.SqlServer.SqlCmdProcess"); } } diff --git a/tests/Testing.Databases.SqlServer.Tests/Comparer/SqlObjectDifferencesTest.cs b/tests/Testing.Databases.SqlServer.Tests/Comparer/SqlObjectDifferencesTest.cs index bf57600..87b9249 100644 --- a/tests/Testing.Databases.SqlServer.Tests/Comparer/SqlObjectDifferencesTest.cs +++ b/tests/Testing.Databases.SqlServer.Tests/Comparer/SqlObjectDifferencesTest.cs @@ -22,7 +22,7 @@ public void ToStringTest() var difference = new SqlObjectDifferences(source, target, default, properties); - difference.ToString().Should().Be("The source\r\n * The prop1:\r\n Source: 10\r\n Target: 20\r\n * The prop2:\r\n Source: 30\r\n Target: 40\r\n"); + difference.ToString().Should().Be($"The source{Environment.NewLine} * The prop1:{Environment.NewLine} Source: 10{Environment.NewLine} Target: 20{Environment.NewLine} * The prop2:{Environment.NewLine} Source: 30{Environment.NewLine} Target: 40{Environment.NewLine}"); } } } \ No newline at end of file From 6e2d3093c951d83652bb54382f8d7930df9e4789 Mon Sep 17 00:00:00 2001 From: Gilles TOURREAU Date: Wed, 24 Sep 2025 17:42:03 +0200 Subject: [PATCH 32/61] Update github-actions-ci.yaml --- .github/workflows/github-actions-ci.yaml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/github-actions-ci.yaml b/.github/workflows/github-actions-ci.yaml index df17cf1..e514a52 100644 --- a/.github/workflows/github-actions-ci.yaml +++ b/.github/workflows/github-actions-ci.yaml @@ -20,8 +20,6 @@ jobs: ACCEPT_EULA: "Y" ports: - 1433:1433 - volumes: - - /tmp/other_databases_path:/tmp/other_databases_path steps: - uses: actions/checkout@v4 @@ -54,14 +52,14 @@ jobs: run: dotnet test PosInformatique.Testing.Databases.sln --configuration Release --no-build --logger "trx;LogFileName=test_results.trx" --results-directory ./TestResults env: SQL_SERVER_UNIT_TESTS_CONNECTION_STRING: "Data Source=localhost,1433;Database=master;User Id=sa;Password=P@ssw0rd12345!;TrustServerCertificate=True;" - SQL_SERVER_UNIT_TESTS_OTHER_DATA_PATH: "/tmp/other_databases_path" + SQL_SERVER_UNIT_TESTS_OTHER_DATA_PATH: "/var/opt/mssql/data/" - name: Publish Test Results uses: EnricoMi/publish-unit-test-result-action@v2 if: (!cancelled()) with: files: | - TestResults/**/*.xml + TestResults/**/*.trx build-samples: runs-on: windows-latest From cafe1372511ee394ff60f46be9fd7d64d8780b65 Mon Sep 17 00:00:00 2001 From: Gilles TOURREAU Date: Wed, 24 Sep 2025 17:52:03 +0200 Subject: [PATCH 33/61] Fix unit tests --- .../SqlCmdDatabaseInitializerTest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Testing.Databases.SqlServer.SqlCmd.Tests/SqlCmdDatabaseInitializerTest.cs b/tests/Testing.Databases.SqlServer.SqlCmd.Tests/SqlCmdDatabaseInitializerTest.cs index 8fd12ba..1f112fa 100644 --- a/tests/Testing.Databases.SqlServer.SqlCmd.Tests/SqlCmdDatabaseInitializerTest.cs +++ b/tests/Testing.Databases.SqlServer.SqlCmd.Tests/SqlCmdDatabaseInitializerTest.cs @@ -90,7 +90,7 @@ public async Task Test1Async() this.initializer.IsInitialized.Should().BeTrue(); var currentUser = await this.database.ExecuteQueryAsync("SELECT SUSER_NAME()"); - currentUser.Rows[0][0].Should().Be($"{Environment.UserDomainName}\\{Environment.UserName}"); + currentUser.Rows[0][0].Should().Be(ConnectionStrings.ExtractUserName(ConnectionString)); // Check the constructor has been called var table = await this.database.ExecuteQueryAsync("SELECT * FROM MyTable"); From 68a7b09e5c36cf02a3378ed2a17f04bd880f9952 Mon Sep 17 00:00:00 2001 From: Gilles TOURREAU Date: Wed, 24 Sep 2025 18:03:56 +0200 Subject: [PATCH 34/61] Debug --- tests/Testing.Databases.SqlServer.Tests/SqlServerTest.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/Testing.Databases.SqlServer.Tests/SqlServerTest.cs b/tests/Testing.Databases.SqlServer.Tests/SqlServerTest.cs index 87b4a7a..e5eb222 100644 --- a/tests/Testing.Databases.SqlServer.Tests/SqlServerTest.cs +++ b/tests/Testing.Databases.SqlServer.Tests/SqlServerTest.cs @@ -130,6 +130,9 @@ public async Task CreateAndDeleteAsync_WithSpecificDataFileName() var table = await server.Master.ExecuteQueryAsync("SELECT * FROM [sys].[databases] WHERE [name] = 'CreateAndDeleteDB_WithSpecificDataFileNameAsync'"); table.Rows.Should().HaveCount(1); + var debug = string.Join(";", Directory.GetFiles(otherDataPath.Path)); + debug.Should().BeNull(); + // Check the location of the database File.Exists(Path.Combine(otherDataPath.Path, "TheSpecificDataFileNameAsync.mdf")).Should().BeTrue(); File.Exists(Path.Combine(otherDataPath.Path, "TheSpecificDataFileNameAsync_log.ldf")).Should().BeTrue(); From 779b36469d30930ff712ffbfa861168210dbe77e Mon Sep 17 00:00:00 2001 From: Gilles TOURREAU Date: Wed, 24 Sep 2025 18:09:02 +0200 Subject: [PATCH 35/61] Revert --- tests/Testing.Databases.SqlServer.Tests/SqlServerTest.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/Testing.Databases.SqlServer.Tests/SqlServerTest.cs b/tests/Testing.Databases.SqlServer.Tests/SqlServerTest.cs index e5eb222..87b4a7a 100644 --- a/tests/Testing.Databases.SqlServer.Tests/SqlServerTest.cs +++ b/tests/Testing.Databases.SqlServer.Tests/SqlServerTest.cs @@ -130,9 +130,6 @@ public async Task CreateAndDeleteAsync_WithSpecificDataFileName() var table = await server.Master.ExecuteQueryAsync("SELECT * FROM [sys].[databases] WHERE [name] = 'CreateAndDeleteDB_WithSpecificDataFileNameAsync'"); table.Rows.Should().HaveCount(1); - var debug = string.Join(";", Directory.GetFiles(otherDataPath.Path)); - debug.Should().BeNull(); - // Check the location of the database File.Exists(Path.Combine(otherDataPath.Path, "TheSpecificDataFileNameAsync.mdf")).Should().BeTrue(); File.Exists(Path.Combine(otherDataPath.Path, "TheSpecificDataFileNameAsync_log.ldf")).Should().BeTrue(); From c75b111679cfbbebe955747c4d3bdf40977d38d2 Mon Sep 17 00:00:00 2001 From: Gilles TOURREAU Date: Wed, 24 Sep 2025 18:10:06 +0200 Subject: [PATCH 36/61] Update github-actions-ci.yaml --- .github/workflows/github-actions-ci.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/github-actions-ci.yaml b/.github/workflows/github-actions-ci.yaml index e514a52..9656903 100644 --- a/.github/workflows/github-actions-ci.yaml +++ b/.github/workflows/github-actions-ci.yaml @@ -20,6 +20,8 @@ jobs: ACCEPT_EULA: "Y" ports: - 1433:1433 + volumes: + - /var/opt/mssql/data:/var/opt/mssql/data steps: - uses: actions/checkout@v4 From 9eca60cd14a230d2dcc54e1db3e35afb9d6a6aeb Mon Sep 17 00:00:00 2001 From: Gilles TOURREAU Date: Wed, 24 Sep 2025 18:18:28 +0200 Subject: [PATCH 37/61] Update github-actions-ci.yaml --- .github/workflows/github-actions-ci.yaml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/github-actions-ci.yaml b/.github/workflows/github-actions-ci.yaml index 9656903..f024f6b 100644 --- a/.github/workflows/github-actions-ci.yaml +++ b/.github/workflows/github-actions-ci.yaml @@ -20,8 +20,7 @@ jobs: ACCEPT_EULA: "Y" ports: - 1433:1433 - volumes: - - /var/opt/mssql/data:/var/opt/mssql/data + steps: - uses: actions/checkout@v4 From 572decc1b8add026f558d04aca1bdeb4ca31c931 Mon Sep 17 00:00:00 2001 From: Gilles TOURREAU Date: Thu, 25 Sep 2025 08:10:07 +0200 Subject: [PATCH 38/61] Update github-actions-ci.yaml --- .github/workflows/github-actions-ci.yaml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/github-actions-ci.yaml b/.github/workflows/github-actions-ci.yaml index f024f6b..fae183d 100644 --- a/.github/workflows/github-actions-ci.yaml +++ b/.github/workflows/github-actions-ci.yaml @@ -20,10 +20,15 @@ jobs: ACCEPT_EULA: "Y" ports: - 1433:1433 - + volumes: + - /var/opt/mssql/customdbs:/var/opt/mssql/customdbs steps: - uses: actions/checkout@v4 + - name: Prepare SQL custom dir + run: | + docker exec $(docker ps -qf "ancestor=mcr.microsoft.com/mssql/server:2022-latest") chown -R mssql:mssql /var/opt/mssql/customdbs + - name: Setup .NET uses: actions/setup-dotnet@v4 with: From 7755e10c02529776f8d53d980cbade3939fc58ee Mon Sep 17 00:00:00 2001 From: Gilles TOURREAU Date: Thu, 25 Sep 2025 08:13:43 +0200 Subject: [PATCH 39/61] Update github-actions-ci.yaml --- .github/workflows/github-actions-ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/github-actions-ci.yaml b/.github/workflows/github-actions-ci.yaml index fae183d..7c8bf20 100644 --- a/.github/workflows/github-actions-ci.yaml +++ b/.github/workflows/github-actions-ci.yaml @@ -27,7 +27,7 @@ jobs: - name: Prepare SQL custom dir run: | - docker exec $(docker ps -qf "ancestor=mcr.microsoft.com/mssql/server:2022-latest") chown -R mssql:mssql /var/opt/mssql/customdbs + docker exec --user root $(docker ps -qf "ancestor=mcr.microsoft.com/mssql/server:2022-latest") chown -R mssql:mssql /var/opt/mssql/customdbs - name: Setup .NET uses: actions/setup-dotnet@v4 From 9ed1c4dc3fe128fa8787644efc41143d98d410ab Mon Sep 17 00:00:00 2001 From: Gilles TOURREAU Date: Thu, 25 Sep 2025 08:15:11 +0200 Subject: [PATCH 40/61] Update github-actions-ci.yaml --- .github/workflows/github-actions-ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/github-actions-ci.yaml b/.github/workflows/github-actions-ci.yaml index 7c8bf20..7756c34 100644 --- a/.github/workflows/github-actions-ci.yaml +++ b/.github/workflows/github-actions-ci.yaml @@ -58,7 +58,7 @@ jobs: run: dotnet test PosInformatique.Testing.Databases.sln --configuration Release --no-build --logger "trx;LogFileName=test_results.trx" --results-directory ./TestResults env: SQL_SERVER_UNIT_TESTS_CONNECTION_STRING: "Data Source=localhost,1433;Database=master;User Id=sa;Password=P@ssw0rd12345!;TrustServerCertificate=True;" - SQL_SERVER_UNIT_TESTS_OTHER_DATA_PATH: "/var/opt/mssql/data/" + SQL_SERVER_UNIT_TESTS_OTHER_DATA_PATH: "/var/opt/mssql/customdbs/" - name: Publish Test Results uses: EnricoMi/publish-unit-test-result-action@v2 From 5159d5a20ade66f563dec11ad0ded4fa4d8a4dfd Mon Sep 17 00:00:00 2001 From: Gilles TOURREAU Date: Thu, 25 Sep 2025 08:28:04 +0200 Subject: [PATCH 41/61] Debug --- tests/Testing.Databases.SqlServer.Tests/SqlServerTest.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/Testing.Databases.SqlServer.Tests/SqlServerTest.cs b/tests/Testing.Databases.SqlServer.Tests/SqlServerTest.cs index 87b4a7a..e5eb222 100644 --- a/tests/Testing.Databases.SqlServer.Tests/SqlServerTest.cs +++ b/tests/Testing.Databases.SqlServer.Tests/SqlServerTest.cs @@ -130,6 +130,9 @@ public async Task CreateAndDeleteAsync_WithSpecificDataFileName() var table = await server.Master.ExecuteQueryAsync("SELECT * FROM [sys].[databases] WHERE [name] = 'CreateAndDeleteDB_WithSpecificDataFileNameAsync'"); table.Rows.Should().HaveCount(1); + var debug = string.Join(";", Directory.GetFiles(otherDataPath.Path)); + debug.Should().BeNull(); + // Check the location of the database File.Exists(Path.Combine(otherDataPath.Path, "TheSpecificDataFileNameAsync.mdf")).Should().BeTrue(); File.Exists(Path.Combine(otherDataPath.Path, "TheSpecificDataFileNameAsync_log.ldf")).Should().BeTrue(); From ee26a2cf52ed4de8fdbf8aa37f777359218b3adf Mon Sep 17 00:00:00 2001 From: Gilles TOURREAU Date: Thu, 25 Sep 2025 08:36:02 +0200 Subject: [PATCH 42/61] Revert "Debug" This reverts commit 5159d5a20ade66f563dec11ad0ded4fa4d8a4dfd. --- tests/Testing.Databases.SqlServer.Tests/SqlServerTest.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/Testing.Databases.SqlServer.Tests/SqlServerTest.cs b/tests/Testing.Databases.SqlServer.Tests/SqlServerTest.cs index e5eb222..87b4a7a 100644 --- a/tests/Testing.Databases.SqlServer.Tests/SqlServerTest.cs +++ b/tests/Testing.Databases.SqlServer.Tests/SqlServerTest.cs @@ -130,9 +130,6 @@ public async Task CreateAndDeleteAsync_WithSpecificDataFileName() var table = await server.Master.ExecuteQueryAsync("SELECT * FROM [sys].[databases] WHERE [name] = 'CreateAndDeleteDB_WithSpecificDataFileNameAsync'"); table.Rows.Should().HaveCount(1); - var debug = string.Join(";", Directory.GetFiles(otherDataPath.Path)); - debug.Should().BeNull(); - // Check the location of the database File.Exists(Path.Combine(otherDataPath.Path, "TheSpecificDataFileNameAsync.mdf")).Should().BeTrue(); File.Exists(Path.Combine(otherDataPath.Path, "TheSpecificDataFileNameAsync_log.ldf")).Should().BeTrue(); From 5bd089b51f4fb00c4ada02115e0bfdc14d4be1e5 Mon Sep 17 00:00:00 2001 From: Gilles TOURREAU Date: Thu, 25 Sep 2025 08:52:43 +0200 Subject: [PATCH 43/61] Update github-actions-ci.yaml --- .github/workflows/github-actions-ci.yaml | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/.github/workflows/github-actions-ci.yaml b/.github/workflows/github-actions-ci.yaml index 7756c34..daee999 100644 --- a/.github/workflows/github-actions-ci.yaml +++ b/.github/workflows/github-actions-ci.yaml @@ -21,14 +21,10 @@ jobs: ports: - 1433:1433 volumes: - - /var/opt/mssql/customdbs:/var/opt/mssql/customdbs + - /tmp/other_databases_path:/tmp/other_databases_path steps: - uses: actions/checkout@v4 - - name: Prepare SQL custom dir - run: | - docker exec --user root $(docker ps -qf "ancestor=mcr.microsoft.com/mssql/server:2022-latest") chown -R mssql:mssql /var/opt/mssql/customdbs - - name: Setup .NET uses: actions/setup-dotnet@v4 with: @@ -54,11 +50,16 @@ jobs: sudo apt-get install -y mssql-tools echo "/opt/mssql-tools/bin" >> $GITHUB_PATH + - name: Prepare SQL databases directory + run: | + # Give the rights to the 'mssql' user to read/write in the /tmp/other_databases_path directory. + docker exec --user root $(docker ps -qf "ancestor=mcr.microsoft.com/mssql/server:2022-latest") chown -R mssql:mssql /tmp/other_databases_path + - name: Run tests run: dotnet test PosInformatique.Testing.Databases.sln --configuration Release --no-build --logger "trx;LogFileName=test_results.trx" --results-directory ./TestResults env: SQL_SERVER_UNIT_TESTS_CONNECTION_STRING: "Data Source=localhost,1433;Database=master;User Id=sa;Password=P@ssw0rd12345!;TrustServerCertificate=True;" - SQL_SERVER_UNIT_TESTS_OTHER_DATA_PATH: "/var/opt/mssql/customdbs/" + SQL_SERVER_UNIT_TESTS_OTHER_DATA_PATH: "/tmp/other_databases_path/" - name: Publish Test Results uses: EnricoMi/publish-unit-test-result-action@v2 From 069a20048749a7c933d7d20a2735d52fde1d4327 Mon Sep 17 00:00:00 2001 From: Gilles TOURREAU Date: Thu, 25 Sep 2025 08:55:10 +0200 Subject: [PATCH 44/61] Fix unit tests previously failed. --- src/Testing.Databases.SqlServer/SqlServer.cs | 5 +++++ .../SqlServerDacExtensionsTest.cs | 2 +- .../SqlServerDatabaseInitializerTest.cs | 2 +- tests/Testing.Databases.SqlServer.Tests/SqlServerTest.cs | 4 ++-- 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/Testing.Databases.SqlServer/SqlServer.cs b/src/Testing.Databases.SqlServer/SqlServer.cs index 145e461..de49e6b 100644 --- a/src/Testing.Databases.SqlServer/SqlServer.cs +++ b/src/Testing.Databases.SqlServer/SqlServer.cs @@ -114,7 +114,12 @@ private static string BuildCreateDatabaseSqlCommand(string name, SqlDatabaseCrea { if (!string.IsNullOrEmpty(settings.DataFileName)) { + var logFileName = Path.Combine( + Path.GetDirectoryName(settings.DataFileName), + $"{Path.GetFileNameWithoutExtension(settings.DataFileName)}_log.ldf"); + sql.Append($"ON (NAME = '{name}', FILENAME = '{settings.DataFileName}')"); + sql.Append($"LOG ON (NAME = '{name}_log', FILENAME = '{logFileName}')"); } } diff --git a/tests/Testing.Databases.SqlServer.Dac.Tests/SqlServerDacExtensionsTest.cs b/tests/Testing.Databases.SqlServer.Dac.Tests/SqlServerDacExtensionsTest.cs index 134a733..a8bf9e0 100644 --- a/tests/Testing.Databases.SqlServer.Dac.Tests/SqlServerDacExtensionsTest.cs +++ b/tests/Testing.Databases.SqlServer.Dac.Tests/SqlServerDacExtensionsTest.cs @@ -76,7 +76,7 @@ public void DeployDacPackage_WithSpecificDataFileName() result.Rows[0]["physical_name"].Should().Be(Path.Combine(otherDataPath.Path, "TheSpecificDataFileName.mdf")); result.Rows[0]["type_desc"].Should().Be("ROWS"); - result.Rows[1]["name"].Should().Be("TheSpecificDataFileName_log"); + result.Rows[1]["name"].Should().Be("SqlServerDacExtensionsTest_DeployDacPackage_WithSpecificDataFileName_log"); result.Rows[1]["physical_name"].Should().Be(Path.Combine(otherDataPath.Path, "TheSpecificDataFileName_log.ldf")); result.Rows[1]["type_desc"].Should().Be("LOG"); diff --git a/tests/Testing.Databases.SqlServer.Dac.Tests/SqlServerDatabaseInitializerTest.cs b/tests/Testing.Databases.SqlServer.Dac.Tests/SqlServerDatabaseInitializerTest.cs index f6f9690..8843676 100644 --- a/tests/Testing.Databases.SqlServer.Dac.Tests/SqlServerDatabaseInitializerTest.cs +++ b/tests/Testing.Databases.SqlServer.Dac.Tests/SqlServerDatabaseInitializerTest.cs @@ -160,7 +160,7 @@ public void Initialize_WithSpecificDataFileName() result.Rows[0]["physical_name"].Should().Be(Path.Combine(otherDataPath.Path, "TheSpecificDataFileName.mdf")); result.Rows[0]["type_desc"].Should().Be("ROWS"); - result.Rows[1]["name"].Should().Be("TheSpecificDataFileName_log"); + result.Rows[1]["name"].Should().Be("SqlServerDatabaseInitializerTest_Initialize_WithSpecificDataFileName_log"); result.Rows[1]["physical_name"].Should().Be(Path.Combine(otherDataPath.Path, "TheSpecificDataFileName_log.ldf")); result.Rows[1]["type_desc"].Should().Be("LOG"); diff --git a/tests/Testing.Databases.SqlServer.Tests/SqlServerTest.cs b/tests/Testing.Databases.SqlServer.Tests/SqlServerTest.cs index 87b4a7a..62bb7f9 100644 --- a/tests/Testing.Databases.SqlServer.Tests/SqlServerTest.cs +++ b/tests/Testing.Databases.SqlServer.Tests/SqlServerTest.cs @@ -81,7 +81,7 @@ public async Task CreateAndDelete_WithSpecificDataFileName() result.Rows[0]["physical_name"].Should().Be(Path.Combine(otherDataPath.Path, "TheSpecificDataFileName.mdf")); result.Rows[0]["type_desc"].Should().Be("ROWS"); - result.Rows[1]["name"].Should().Be("TheSpecificDataFileName_log"); + result.Rows[1]["name"].Should().Be("CreateAndDeleteDB_WithSpecificDataFileName_log"); result.Rows[1]["physical_name"].Should().Be(Path.Combine(otherDataPath.Path, "TheSpecificDataFileName_log.ldf")); result.Rows[1]["type_desc"].Should().Be("LOG"); @@ -142,7 +142,7 @@ public async Task CreateAndDeleteAsync_WithSpecificDataFileName() result.Rows[0]["physical_name"].Should().Be(Path.Combine(otherDataPath.Path, "TheSpecificDataFileNameAsync.mdf")); result.Rows[0]["type_desc"].Should().Be("ROWS"); - result.Rows[1]["name"].Should().Be("TheSpecificDataFileNameAsync_log"); + result.Rows[1]["name"].Should().Be("CreateAndDeleteDB_WithSpecificDataFileNameAsync_log"); result.Rows[1]["physical_name"].Should().Be(Path.Combine(otherDataPath.Path, "TheSpecificDataFileNameAsync_log.ldf")); result.Rows[1]["type_desc"].Should().Be("LOG"); From 89056e8cb1ec5b56e6d8c80796795aead31a7135 Mon Sep 17 00:00:00 2001 From: Gilles TOURREAU Date: Thu, 25 Sep 2025 09:14:12 +0200 Subject: [PATCH 45/61] Migrate to xUnit v3. --- Directory.Packages.props | 4 +- tests/Directory.Build.props | 16 ++++ .../SqlServerDatabaseInitializerTest.cs | 8 +- ...sting.Databases.SqlServer.Dac.Tests.csproj | 15 +--- .../EntityFrameworkSqlServerExtensionsTest.cs | 14 ++-- ...ses.SqlServer.EntityFramework.Tests.csproj | 15 +--- .../SqlCmdDatabaseInitializerTest.cs | 8 +- ...ng.Databases.SqlServer.SqlCmd.Tests.csproj | 15 +--- ...ng.Databases.SqlServer.Tests.DacPac.csproj | 6 ++ ...ng.Databases.SqlServer.Tests.Source.csproj | 6 ++ ...ng.Databases.SqlServer.Tests.Target.csproj | 6 ++ .../SqlServerDatabaseComparerTest.cs | 6 +- .../SqlServerDatabaseExtensionsTest.cs | 79 +++++++++++-------- .../SqlServerTest.cs | 20 ++--- .../Testing.Databases.SqlServer.Tests.csproj | 15 +--- 15 files changed, 112 insertions(+), 121 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 546b9aa..bae20ac 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -9,10 +9,8 @@ - - - + \ No newline at end of file diff --git a/tests/Directory.Build.props b/tests/Directory.Build.props index 65d79b0..239e78d 100644 --- a/tests/Directory.Build.props +++ b/tests/Directory.Build.props @@ -2,6 +2,22 @@ + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + true + false + diff --git a/tests/Testing.Databases.SqlServer.Dac.Tests/SqlServerDatabaseInitializerTest.cs b/tests/Testing.Databases.SqlServer.Dac.Tests/SqlServerDatabaseInitializerTest.cs index 8843676..4af4ca1 100644 --- a/tests/Testing.Databases.SqlServer.Dac.Tests/SqlServerDatabaseInitializerTest.cs +++ b/tests/Testing.Databases.SqlServer.Dac.Tests/SqlServerDatabaseInitializerTest.cs @@ -80,11 +80,11 @@ public async Task Test1Async() { this.initializer.IsInitialized.Should().BeTrue(); - var currentUser = await this.database.ExecuteQueryAsync("SELECT SUSER_NAME()"); + var currentUser = await this.database.ExecuteQueryAsync("SELECT SUSER_NAME()", TestContext.Current.CancellationToken); currentUser.Rows[0][0].Should().Be(ConnectionStrings.ExtractUserName(ConnectionString)); // Check the constructor has been called - var table = await this.database.ExecuteQueryAsync("SELECT * FROM MyTable"); + var table = await this.database.ExecuteQueryAsync("SELECT * FROM MyTable", TestContext.Current.CancellationToken); table.Rows.Should().HaveCount(2); @@ -103,11 +103,11 @@ public async Task Test2Async() { this.initializer.IsInitialized.Should().BeTrue(); - var currentUser = await this.database.ExecuteQueryAsync("SELECT SUSER_NAME()"); + var currentUser = await this.database.ExecuteQueryAsync("SELECT SUSER_NAME()", TestContext.Current.CancellationToken); currentUser.Rows[0][0].Should().Be(ConnectionStrings.ExtractUserName(ConnectionString)); // Check the constructor has been called - var table = await this.database.ExecuteQueryAsync("SELECT * FROM MyTable"); + var table = await this.database.ExecuteQueryAsync("SELECT * FROM MyTable", TestContext.Current.CancellationToken); table.Rows.Should().HaveCount(2); diff --git a/tests/Testing.Databases.SqlServer.Dac.Tests/Testing.Databases.SqlServer.Dac.Tests.csproj b/tests/Testing.Databases.SqlServer.Dac.Tests/Testing.Databases.SqlServer.Dac.Tests.csproj index b83a2bd..3ad7b37 100644 --- a/tests/Testing.Databases.SqlServer.Dac.Tests/Testing.Databases.SqlServer.Dac.Tests.csproj +++ b/tests/Testing.Databases.SqlServer.Dac.Tests/Testing.Databases.SqlServer.Dac.Tests.csproj @@ -2,6 +2,7 @@ net8.0 + Exe @@ -9,20 +10,6 @@ PreserveNewest - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - diff --git a/tests/Testing.Databases.SqlServer.EntityFramework.Tests/EntityFrameworkSqlServerExtensionsTest.cs b/tests/Testing.Databases.SqlServer.EntityFramework.Tests/EntityFrameworkSqlServerExtensionsTest.cs index 9d23e0f..bf7b1e9 100644 --- a/tests/Testing.Databases.SqlServer.EntityFramework.Tests/EntityFrameworkSqlServerExtensionsTest.cs +++ b/tests/Testing.Databases.SqlServer.EntityFramework.Tests/EntityFrameworkSqlServerExtensionsTest.cs @@ -28,7 +28,7 @@ public async Task Create_WithNoExistingDatabase() database.ConnectionString.Should().Be(ConnectionStrings.Get("EntityFrameworkSqlServerExtensionsTest")); - var tables = await database.GetTablesAsync(); + var tables = await database.GetTablesAsync(TestContext.Current.CancellationToken); tables.Should().HaveCount(1); @@ -56,7 +56,7 @@ public async Task Create_WithAlreadyExistingDatabase() database.ConnectionString.Should().Be(ConnectionStrings.Get("EntityFrameworkSqlServerExtensionsTest")); - var tables = await database.GetTablesAsync(); + var tables = await database.GetTablesAsync(TestContext.Current.CancellationToken); tables.Should().HaveCount(1); @@ -76,13 +76,13 @@ public async Task CreateAsync_WithNoExistingDatabase() using var dbContext = new DbContextTest(optionsBuilder.Options); var server = new SqlServer(ConnectionString); - await server.DeleteDatabaseAsync(nameof(EntityFrameworkSqlServerExtensionsTest)); + await server.DeleteDatabaseAsync(nameof(EntityFrameworkSqlServerExtensionsTest), TestContext.Current.CancellationToken); var database = await server.CreateDatabaseAsync(nameof(EntityFrameworkSqlServerExtensionsTest), dbContext); database.ConnectionString.Should().Be(ConnectionStrings.Get("EntityFrameworkSqlServerExtensionsTest")); - var tables = await database.GetTablesAsync(); + var tables = await database.GetTablesAsync(TestContext.Current.CancellationToken); tables.Should().HaveCount(1); @@ -102,15 +102,15 @@ public async Task CreateAsync_WithAlreadyExistingDatabase() using var dbContext = new DbContextTest(optionsBuilder.Options); var server = new SqlServer(ConnectionString); - var emptyDatabase = await server.CreateEmptyDatabaseAsync(nameof(EntityFrameworkSqlServerExtensionsTest)); + var emptyDatabase = await server.CreateEmptyDatabaseAsync(nameof(EntityFrameworkSqlServerExtensionsTest), default, TestContext.Current.CancellationToken); - await emptyDatabase.ExecuteNonQueryAsync("CREATE TABLE [MustBeDeleted] ([Id] INT)"); + await emptyDatabase.ExecuteNonQueryAsync("CREATE TABLE [MustBeDeleted] ([Id] INT)", TestContext.Current.CancellationToken); var database = await server.CreateDatabaseAsync(nameof(EntityFrameworkSqlServerExtensionsTest), dbContext); database.ConnectionString.Should().Be(ConnectionStrings.Get("EntityFrameworkSqlServerExtensionsTest")); - var tables = await database.GetTablesAsync(); + var tables = await database.GetTablesAsync(TestContext.Current.CancellationToken); tables.Should().HaveCount(1); diff --git a/tests/Testing.Databases.SqlServer.EntityFramework.Tests/Testing.Databases.SqlServer.EntityFramework.Tests.csproj b/tests/Testing.Databases.SqlServer.EntityFramework.Tests/Testing.Databases.SqlServer.EntityFramework.Tests.csproj index 366096d..3fc3588 100644 --- a/tests/Testing.Databases.SqlServer.EntityFramework.Tests/Testing.Databases.SqlServer.EntityFramework.Tests.csproj +++ b/tests/Testing.Databases.SqlServer.EntityFramework.Tests/Testing.Databases.SqlServer.EntityFramework.Tests.csproj @@ -2,22 +2,9 @@ net8.0 + Exe - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - diff --git a/tests/Testing.Databases.SqlServer.SqlCmd.Tests/SqlCmdDatabaseInitializerTest.cs b/tests/Testing.Databases.SqlServer.SqlCmd.Tests/SqlCmdDatabaseInitializerTest.cs index 1f112fa..35a427f 100644 --- a/tests/Testing.Databases.SqlServer.SqlCmd.Tests/SqlCmdDatabaseInitializerTest.cs +++ b/tests/Testing.Databases.SqlServer.SqlCmd.Tests/SqlCmdDatabaseInitializerTest.cs @@ -89,11 +89,11 @@ public async Task Test1Async() { this.initializer.IsInitialized.Should().BeTrue(); - var currentUser = await this.database.ExecuteQueryAsync("SELECT SUSER_NAME()"); + var currentUser = await this.database.ExecuteQueryAsync("SELECT SUSER_NAME()", TestContext.Current.CancellationToken); currentUser.Rows[0][0].Should().Be(ConnectionStrings.ExtractUserName(ConnectionString)); // Check the constructor has been called - var table = await this.database.ExecuteQueryAsync("SELECT * FROM MyTable"); + var table = await this.database.ExecuteQueryAsync("SELECT * FROM MyTable", TestContext.Current.CancellationToken); table.Rows.Should().HaveCount(2); @@ -112,11 +112,11 @@ public async Task Test2Async() { this.initializer.IsInitialized.Should().BeTrue(); - var currentUser = await this.database.ExecuteQueryAsync("SELECT SUSER_NAME()"); + var currentUser = await this.database.ExecuteQueryAsync("SELECT SUSER_NAME()", TestContext.Current.CancellationToken); currentUser.Rows[0][0].Should().Be(ConnectionStrings.ExtractUserName(ConnectionString)); // Check the constructor has been called - var table = await this.database.ExecuteQueryAsync("SELECT * FROM MyTable"); + var table = await this.database.ExecuteQueryAsync("SELECT * FROM MyTable", TestContext.Current.CancellationToken); table.Rows.Should().HaveCount(2); diff --git a/tests/Testing.Databases.SqlServer.SqlCmd.Tests/Testing.Databases.SqlServer.SqlCmd.Tests.csproj b/tests/Testing.Databases.SqlServer.SqlCmd.Tests/Testing.Databases.SqlServer.SqlCmd.Tests.csproj index 3ff5a22..5e8124b 100644 --- a/tests/Testing.Databases.SqlServer.SqlCmd.Tests/Testing.Databases.SqlServer.SqlCmd.Tests.csproj +++ b/tests/Testing.Databases.SqlServer.SqlCmd.Tests/Testing.Databases.SqlServer.SqlCmd.Tests.csproj @@ -2,6 +2,7 @@ net8.0 + Exe @@ -10,20 +11,6 @@ - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - diff --git a/tests/Testing.Databases.SqlServer.Tests.DacPac/Testing.Databases.SqlServer.Tests.DacPac.csproj b/tests/Testing.Databases.SqlServer.Tests.DacPac/Testing.Databases.SqlServer.Tests.DacPac.csproj index fe47423..b99ddfc 100644 --- a/tests/Testing.Databases.SqlServer.Tests.DacPac/Testing.Databases.SqlServer.Tests.DacPac.csproj +++ b/tests/Testing.Databases.SqlServer.Tests.DacPac/Testing.Databases.SqlServer.Tests.DacPac.csproj @@ -10,6 +10,12 @@ True + + + + + + diff --git a/tests/Testing.Databases.SqlServer.Tests.Source/Testing.Databases.SqlServer.Tests.Source.csproj b/tests/Testing.Databases.SqlServer.Tests.Source/Testing.Databases.SqlServer.Tests.Source.csproj index fe47423..b99ddfc 100644 --- a/tests/Testing.Databases.SqlServer.Tests.Source/Testing.Databases.SqlServer.Tests.Source.csproj +++ b/tests/Testing.Databases.SqlServer.Tests.Source/Testing.Databases.SqlServer.Tests.Source.csproj @@ -10,6 +10,12 @@ True + + + + + + diff --git a/tests/Testing.Databases.SqlServer.Tests.Target/Testing.Databases.SqlServer.Tests.Target.csproj b/tests/Testing.Databases.SqlServer.Tests.Target/Testing.Databases.SqlServer.Tests.Target.csproj index 4bdf1d8..d7bdb79 100644 --- a/tests/Testing.Databases.SqlServer.Tests.Target/Testing.Databases.SqlServer.Tests.Target.csproj +++ b/tests/Testing.Databases.SqlServer.Tests.Target/Testing.Databases.SqlServer.Tests.Target.csproj @@ -10,6 +10,12 @@ True + + + + + + diff --git a/tests/Testing.Databases.SqlServer.Tests/SqlServerDatabaseComparerTest.cs b/tests/Testing.Databases.SqlServer.Tests/SqlServerDatabaseComparerTest.cs index d5fbc41..43a5045 100644 --- a/tests/Testing.Databases.SqlServer.Tests/SqlServerDatabaseComparerTest.cs +++ b/tests/Testing.Databases.SqlServer.Tests/SqlServerDatabaseComparerTest.cs @@ -16,8 +16,8 @@ public async Task CompareAsync() { var server = new SqlServer(ConnectionString); - var sourceDatabase = Task.Run(() => server.Master.RunScript("Testing.Databases.SqlServer.Tests.Source_Create.sql")); - var targetDatabase = Task.Run(() => server.Master.RunScript("Testing.Databases.SqlServer.Tests.Target_Create.sql")); + var sourceDatabase = Task.Run(() => server.Master.RunScript("Testing.Databases.SqlServer.Tests.Source_Create.sql"), TestContext.Current.CancellationToken); + var targetDatabase = Task.Run(() => server.Master.RunScript("Testing.Databases.SqlServer.Tests.Target_Create.sql"), TestContext.Current.CancellationToken); await Task.WhenAll(sourceDatabase, targetDatabase); @@ -30,7 +30,7 @@ public async Task CompareAsync() }, }; - var differences = await SqlServerDatabaseComparer.CompareAsync(server.GetDatabase("Testing.Databases.SqlServer.Tests.Source"), server.GetDatabase("Testing.Databases.SqlServer.Tests.Target"), options); + var differences = await SqlServerDatabaseComparer.CompareAsync(server.GetDatabase("Testing.Databases.SqlServer.Tests.Source"), server.GetDatabase("Testing.Databases.SqlServer.Tests.Target"), options, TestContext.Current.CancellationToken); // StoredProcedures differences.StoredProcedures.Should().HaveCount(3); diff --git a/tests/Testing.Databases.SqlServer.Tests/SqlServerDatabaseExtensionsTest.cs b/tests/Testing.Databases.SqlServer.Tests/SqlServerDatabaseExtensionsTest.cs index 51631fa..5ca9f01 100644 --- a/tests/Testing.Databases.SqlServer.Tests/SqlServerDatabaseExtensionsTest.cs +++ b/tests/Testing.Databases.SqlServer.Tests/SqlServerDatabaseExtensionsTest.cs @@ -227,7 +227,8 @@ public async Task ExecuteScriptAsync_String() var database = server.CreateEmptyDatabase("SqlServerDatabaseExtensionsTest"); - await database.ExecuteScriptAsync(@" + await database.ExecuteScriptAsync( + """ CREATE TABLE TableTest ( Id INT NOT NULL @@ -242,9 +243,11 @@ INSERT INTO [TableTest] ([Id]) VALUES (0) UPDATE [TableTest] SET [Id] = [Id] + 1 - GO 10"); + GO 10 + """, + TestContext.Current.CancellationToken); - var table = await database.ExecuteQueryAsync("SELECT * FROM [TableTest]"); + var table = await database.ExecuteQueryAsync("SELECT * FROM [TableTest]", TestContext.Current.CancellationToken); table.Rows.Should().HaveCount(1); @@ -258,7 +261,8 @@ public async Task ExecuteScriptAsync_String_WithEmptyLinesAtTheEnd() var database = server.CreateEmptyDatabase("SqlServerDatabaseExtensionsTest"); - await database.ExecuteScriptAsync(@" + await database.ExecuteScriptAsync( + """ CREATE TABLE TableTest ( Id INT NOT NULL @@ -274,10 +278,10 @@ UPDATE [TableTest] SET [Id] = [Id] + 1 GO 10 + """, + TestContext.Current.CancellationToken); - "); - - var table = await database.ExecuteQueryAsync("SELECT * FROM [TableTest]"); + var table = await database.ExecuteQueryAsync("SELECT * FROM [TableTest]", TestContext.Current.CancellationToken); table.Rows.Should().HaveCount(1); @@ -291,24 +295,28 @@ public async Task ExecuteScriptAsync_StringReader() var database = server.CreateEmptyDatabase("SqlServerDatabaseExtensionsTest"); - await database.ExecuteScriptAsync(new StringReader(@" - CREATE TABLE TableTest - ( - Id INT NOT NULL - ) + await database.ExecuteScriptAsync( + new StringReader( + """ + CREATE TABLE TableTest + ( + Id INT NOT NULL + ) - GO - GO + GO + GO - INSERT INTO [TableTest] ([Id]) VALUES (0) + INSERT INTO [TableTest] ([Id]) VALUES (0) - GO - UPDATE [TableTest] - SET [Id] = [Id] + 1 + GO + UPDATE [TableTest] + SET [Id] = [Id] + 1 - GO 10")); + GO 10 + """), + TestContext.Current.CancellationToken); - var table = await database.ExecuteQueryAsync("SELECT * FROM [TableTest]"); + var table = await database.ExecuteQueryAsync("SELECT * FROM [TableTest]", TestContext.Current.CancellationToken); table.Rows.Should().HaveCount(1); @@ -322,27 +330,30 @@ public async Task ExecuteScriptAsync_StringReader_WithEmptyLinesAtTheEnd() var database = server.CreateEmptyDatabase("SqlServerDatabaseExtensionsTest"); - await database.ExecuteScriptAsync(new StringReader(@" - CREATE TABLE TableTest - ( - Id INT NOT NULL - ) + await database.ExecuteScriptAsync( + new StringReader( + """ + CREATE TABLE TableTest + ( + Id INT NOT NULL + ) - GO - GO + GO + GO - INSERT INTO [TableTest] ([Id]) VALUES (0) + INSERT INTO [TableTest] ([Id]) VALUES (0) - GO - UPDATE [TableTest] - SET [Id] = [Id] + 1 + GO + UPDATE [TableTest] + SET [Id] = [Id] + 1 - GO 10 + GO 10 - ")); + """), + TestContext.Current.CancellationToken); - var table = await database.ExecuteQueryAsync("SELECT * FROM [TableTest]"); + var table = await database.ExecuteQueryAsync("SELECT * FROM [TableTest]", TestContext.Current.CancellationToken); table.Rows.Should().HaveCount(1); diff --git a/tests/Testing.Databases.SqlServer.Tests/SqlServerTest.cs b/tests/Testing.Databases.SqlServer.Tests/SqlServerTest.cs index 62bb7f9..c9f344e 100644 --- a/tests/Testing.Databases.SqlServer.Tests/SqlServerTest.cs +++ b/tests/Testing.Databases.SqlServer.Tests/SqlServerTest.cs @@ -40,13 +40,13 @@ public async Task CreateAndDelete(bool withCreationSettings) database.ConnectionString.Should().Be(ConnectionStrings.Get("CreateAndDeleteDB")); - var table = await server.Master.ExecuteQueryAsync("SELECT * FROM [sys].[databases] WHERE [name] = 'CreateAndDeleteDB'"); + var table = await server.Master.ExecuteQueryAsync("SELECT * FROM [sys].[databases] WHERE [name] = 'CreateAndDeleteDB'", TestContext.Current.CancellationToken); table.Rows.Should().HaveCount(1); // Delete the database server.DeleteDatabase("CreateAndDeleteDB"); - table = await server.Master.ExecuteQueryAsync("SELECT * FROM [sys].[databases] WHERE [name] = 'CreateAndDeleteDB'"); + table = await server.Master.ExecuteQueryAsync("SELECT * FROM [sys].[databases] WHERE [name] = 'CreateAndDeleteDB'", TestContext.Current.CancellationToken); table.Rows.Should().BeEmpty(); } @@ -66,7 +66,7 @@ public async Task CreateAndDelete_WithSpecificDataFileName() database.ConnectionString.Should().Be(ConnectionStrings.Get("CreateAndDeleteDB_WithSpecificDataFileName")); - var table = await server.Master.ExecuteQueryAsync("SELECT * FROM [sys].[databases] WHERE [name] = 'CreateAndDeleteDB_WithSpecificDataFileName'"); + var table = await server.Master.ExecuteQueryAsync("SELECT * FROM [sys].[databases] WHERE [name] = 'CreateAndDeleteDB_WithSpecificDataFileName'", TestContext.Current.CancellationToken); table.Rows.Should().HaveCount(1); // Check the location of the database @@ -88,7 +88,7 @@ public async Task CreateAndDelete_WithSpecificDataFileName() // Delete the database server.DeleteDatabase("CreateAndDeleteDB_WithSpecificDataFileName"); - table = await server.Master.ExecuteQueryAsync("SELECT * FROM [sys].[databases] WHERE [name] = 'CreateAndDeleteDB_WithSpecificDataFileName'"); + table = await server.Master.ExecuteQueryAsync("SELECT * FROM [sys].[databases] WHERE [name] = 'CreateAndDeleteDB_WithSpecificDataFileName'", TestContext.Current.CancellationToken); table.Rows.Should().BeEmpty(); } @@ -101,13 +101,13 @@ public async Task CreateAndDeleteAsync() database.ConnectionString.Should().Be(ConnectionStrings.Get("CreateAndDeleteDBAsync")); - var table = await server.Master.ExecuteQueryAsync("SELECT * FROM [sys].[databases] WHERE [name] = 'CreateAndDeleteDBAsync'"); + var table = await server.Master.ExecuteQueryAsync("SELECT * FROM [sys].[databases] WHERE [name] = 'CreateAndDeleteDBAsync'", TestContext.Current.CancellationToken); table.Rows.Should().HaveCount(1); // Delete the database await server.DeleteDatabaseAsync("CreateAndDeleteDBAsync", CancellationToken.None); - table = await server.Master.ExecuteQueryAsync("SELECT * FROM [sys].[databases] WHERE [name] = 'CreateAndDeleteDBAsync'"); + table = await server.Master.ExecuteQueryAsync("SELECT * FROM [sys].[databases] WHERE [name] = 'CreateAndDeleteDBAsync'", TestContext.Current.CancellationToken); table.Rows.Should().BeEmpty(); } @@ -123,11 +123,11 @@ public async Task CreateAndDeleteAsync_WithSpecificDataFileName() DataFileName = Path.Combine(otherDataPath.Path, "TheSpecificDataFileNameAsync.mdf"), }; - var database = await server.CreateEmptyDatabaseAsync("CreateAndDeleteDB_WithSpecificDataFileNameAsync", settings); + var database = await server.CreateEmptyDatabaseAsync("CreateAndDeleteDB_WithSpecificDataFileNameAsync", settings, TestContext.Current.CancellationToken); database.ConnectionString.Should().Be(ConnectionStrings.Get("CreateAndDeleteDB_WithSpecificDataFileNameAsync")); - var table = await server.Master.ExecuteQueryAsync("SELECT * FROM [sys].[databases] WHERE [name] = 'CreateAndDeleteDB_WithSpecificDataFileNameAsync'"); + var table = await server.Master.ExecuteQueryAsync("SELECT * FROM [sys].[databases] WHERE [name] = 'CreateAndDeleteDB_WithSpecificDataFileNameAsync'", TestContext.Current.CancellationToken); table.Rows.Should().HaveCount(1); // Check the location of the database @@ -147,9 +147,9 @@ public async Task CreateAndDeleteAsync_WithSpecificDataFileName() result.Rows[1]["type_desc"].Should().Be("LOG"); // Delete the database - await server.DeleteDatabaseAsync("CreateAndDeleteDB_WithSpecificDataFileNameAsync"); + await server.DeleteDatabaseAsync("CreateAndDeleteDB_WithSpecificDataFileNameAsync", TestContext.Current.CancellationToken); - table = await server.Master.ExecuteQueryAsync("SELECT * FROM [sys].[databases] WHERE [name] = 'CreateAndDeleteDB_WithSpecificDataFileNameAsync'"); + table = await server.Master.ExecuteQueryAsync("SELECT * FROM [sys].[databases] WHERE [name] = 'CreateAndDeleteDB_WithSpecificDataFileNameAsync'", TestContext.Current.CancellationToken); table.Rows.Should().BeEmpty(); } } diff --git a/tests/Testing.Databases.SqlServer.Tests/Testing.Databases.SqlServer.Tests.csproj b/tests/Testing.Databases.SqlServer.Tests/Testing.Databases.SqlServer.Tests.csproj index d2b7097..06277c8 100644 --- a/tests/Testing.Databases.SqlServer.Tests/Testing.Databases.SqlServer.Tests.csproj +++ b/tests/Testing.Databases.SqlServer.Tests/Testing.Databases.SqlServer.Tests.csproj @@ -2,6 +2,7 @@ net8.0 + Exe @@ -20,20 +21,6 @@ - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - From cdebb2efa330a78dee25bebef3e86da6db0520b1 Mon Sep 17 00:00:00 2001 From: Gilles TOURREAU Date: Thu, 25 Sep 2025 09:18:09 +0200 Subject: [PATCH 46/61] Update github-actions-ci.yaml --- .github/workflows/github-actions-ci.yaml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/github-actions-ci.yaml b/.github/workflows/github-actions-ci.yaml index daee999..8800e67 100644 --- a/.github/workflows/github-actions-ci.yaml +++ b/.github/workflows/github-actions-ci.yaml @@ -56,7 +56,10 @@ jobs: docker exec --user root $(docker ps -qf "ancestor=mcr.microsoft.com/mssql/server:2022-latest") chown -R mssql:mssql /tmp/other_databases_path - name: Run tests - run: dotnet test PosInformatique.Testing.Databases.sln --configuration Release --no-build --logger "trx;LogFileName=test_results.trx" --results-directory ./TestResults + run: dotnet run --project tests/PosInformatique.Testing.Databases/PosInformatique.Testing.Databases.csproj -- \ + --report-trx \ + --report-trx-filename test_results.trx \ + --results-directory ./TestResults env: SQL_SERVER_UNIT_TESTS_CONNECTION_STRING: "Data Source=localhost,1433;Database=master;User Id=sa;Password=P@ssw0rd12345!;TrustServerCertificate=True;" SQL_SERVER_UNIT_TESTS_OTHER_DATA_PATH: "/tmp/other_databases_path/" From bb11333d83eaab38301ed0fa60e82d7585c7e5a2 Mon Sep 17 00:00:00 2001 From: Gilles TOURREAU Date: Thu, 25 Sep 2025 09:26:44 +0200 Subject: [PATCH 47/61] Revert "Update github-actions-ci.yaml" This reverts commit cdebb2efa330a78dee25bebef3e86da6db0520b1. --- .github/workflows/github-actions-ci.yaml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/github-actions-ci.yaml b/.github/workflows/github-actions-ci.yaml index 8800e67..daee999 100644 --- a/.github/workflows/github-actions-ci.yaml +++ b/.github/workflows/github-actions-ci.yaml @@ -56,10 +56,7 @@ jobs: docker exec --user root $(docker ps -qf "ancestor=mcr.microsoft.com/mssql/server:2022-latest") chown -R mssql:mssql /tmp/other_databases_path - name: Run tests - run: dotnet run --project tests/PosInformatique.Testing.Databases/PosInformatique.Testing.Databases.csproj -- \ - --report-trx \ - --report-trx-filename test_results.trx \ - --results-directory ./TestResults + run: dotnet test PosInformatique.Testing.Databases.sln --configuration Release --no-build --logger "trx;LogFileName=test_results.trx" --results-directory ./TestResults env: SQL_SERVER_UNIT_TESTS_CONNECTION_STRING: "Data Source=localhost,1433;Database=master;User Id=sa;Password=P@ssw0rd12345!;TrustServerCertificate=True;" SQL_SERVER_UNIT_TESTS_OTHER_DATA_PATH: "/tmp/other_databases_path/" From 0d0430edf493fc26cc25fbbd643835e112226ca7 Mon Sep 17 00:00:00 2001 From: Gilles TOURREAU Date: Thu, 25 Sep 2025 09:33:04 +0200 Subject: [PATCH 48/61] Fix CI --- .github/workflows/github-actions-ci.yaml | 7 ++++++- PosInformatique.Testing.Databases.sln | 1 + dotnet.config | 2 ++ tests/Directory.Build.props | 2 +- 4 files changed, 10 insertions(+), 2 deletions(-) create mode 100644 dotnet.config diff --git a/.github/workflows/github-actions-ci.yaml b/.github/workflows/github-actions-ci.yaml index daee999..26a9963 100644 --- a/.github/workflows/github-actions-ci.yaml +++ b/.github/workflows/github-actions-ci.yaml @@ -56,7 +56,12 @@ jobs: docker exec --user root $(docker ps -qf "ancestor=mcr.microsoft.com/mssql/server:2022-latest") chown -R mssql:mssql /tmp/other_databases_path - name: Run tests - run: dotnet test PosInformatique.Testing.Databases.sln --configuration Release --no-build --logger "trx;LogFileName=test_results.trx" --results-directory ./TestResults + run: dotnet test PosInformatique.Testing.Databases.sln \ + --configuration Release \ + --no-build \ + -- \ + --logger "trx;LogFileName=test_results.trx" \ + --results-directory ./TestResults env: SQL_SERVER_UNIT_TESTS_CONNECTION_STRING: "Data Source=localhost,1433;Database=master;User Id=sa;Password=P@ssw0rd12345!;TrustServerCertificate=True;" SQL_SERVER_UNIT_TESTS_OTHER_DATA_PATH: "/tmp/other_databases_path/" diff --git a/PosInformatique.Testing.Databases.sln b/PosInformatique.Testing.Databases.sln index c19db51..33caa78 100644 --- a/PosInformatique.Testing.Databases.sln +++ b/PosInformatique.Testing.Databases.sln @@ -10,6 +10,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution CodeCoverage.runsettings = CodeCoverage.runsettings Directory.Build.props = Directory.Build.props Directory.Packages.props = Directory.Packages.props + dotnet.config = dotnet.config LICENSE = LICENSE README.md = README.md stylecop.json = stylecop.json diff --git a/dotnet.config b/dotnet.config new file mode 100644 index 0000000..b87edde --- /dev/null +++ b/dotnet.config @@ -0,0 +1,2 @@ +[dotnet.test.runner] +name = "Microsoft.Testing.Platform" \ No newline at end of file diff --git a/tests/Directory.Build.props b/tests/Directory.Build.props index 239e78d..572e43c 100644 --- a/tests/Directory.Build.props +++ b/tests/Directory.Build.props @@ -16,7 +16,7 @@ true - false + true From 0d90a3035aa6e94f2cec6475527f9073ff102ac7 Mon Sep 17 00:00:00 2001 From: Gilles TOURREAU Date: Thu, 25 Sep 2025 09:41:44 +0200 Subject: [PATCH 49/61] Fix CI --- .github/workflows/github-actions-ci.yaml | 1 - PosInformatique.Testing.Databases.sln | 1 - dotnet.config | 2 -- 3 files changed, 4 deletions(-) delete mode 100644 dotnet.config diff --git a/.github/workflows/github-actions-ci.yaml b/.github/workflows/github-actions-ci.yaml index 26a9963..9183a21 100644 --- a/.github/workflows/github-actions-ci.yaml +++ b/.github/workflows/github-actions-ci.yaml @@ -59,7 +59,6 @@ jobs: run: dotnet test PosInformatique.Testing.Databases.sln \ --configuration Release \ --no-build \ - -- \ --logger "trx;LogFileName=test_results.trx" \ --results-directory ./TestResults env: diff --git a/PosInformatique.Testing.Databases.sln b/PosInformatique.Testing.Databases.sln index 33caa78..c19db51 100644 --- a/PosInformatique.Testing.Databases.sln +++ b/PosInformatique.Testing.Databases.sln @@ -10,7 +10,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution CodeCoverage.runsettings = CodeCoverage.runsettings Directory.Build.props = Directory.Build.props Directory.Packages.props = Directory.Packages.props - dotnet.config = dotnet.config LICENSE = LICENSE README.md = README.md stylecop.json = stylecop.json diff --git a/dotnet.config b/dotnet.config deleted file mode 100644 index b87edde..0000000 --- a/dotnet.config +++ /dev/null @@ -1,2 +0,0 @@ -[dotnet.test.runner] -name = "Microsoft.Testing.Platform" \ No newline at end of file From 511f3c02f484c04a9823cc753e7581b4e0eeb9b5 Mon Sep 17 00:00:00 2001 From: Gilles TOURREAU Date: Thu, 25 Sep 2025 09:47:22 +0200 Subject: [PATCH 50/61] Update github-actions-ci.yaml --- .github/workflows/github-actions-ci.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/github-actions-ci.yaml b/.github/workflows/github-actions-ci.yaml index 9183a21..46d9f53 100644 --- a/.github/workflows/github-actions-ci.yaml +++ b/.github/workflows/github-actions-ci.yaml @@ -57,10 +57,10 @@ jobs: - name: Run tests run: dotnet test PosInformatique.Testing.Databases.sln \ - --configuration Release \ - --no-build \ - --logger "trx;LogFileName=test_results.trx" \ - --results-directory ./TestResults + --configuration Release \ + --no-build \ + --logger "trx;LogFileName=test_results.trx" \ + --results-directory ./TestResults env: SQL_SERVER_UNIT_TESTS_CONNECTION_STRING: "Data Source=localhost,1433;Database=master;User Id=sa;Password=P@ssw0rd12345!;TrustServerCertificate=True;" SQL_SERVER_UNIT_TESTS_OTHER_DATA_PATH: "/tmp/other_databases_path/" From 224abcaf976ba6cae832aa8456bb6ae4e2a8e90d Mon Sep 17 00:00:00 2001 From: Gilles TOURREAU Date: Thu, 25 Sep 2025 10:03:34 +0200 Subject: [PATCH 51/61] Update github-actions-ci.yaml --- .github/workflows/github-actions-ci.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/github-actions-ci.yaml b/.github/workflows/github-actions-ci.yaml index 46d9f53..30af0aa 100644 --- a/.github/workflows/github-actions-ci.yaml +++ b/.github/workflows/github-actions-ci.yaml @@ -56,7 +56,8 @@ jobs: docker exec --user root $(docker ps -qf "ancestor=mcr.microsoft.com/mssql/server:2022-latest") chown -R mssql:mssql /tmp/other_databases_path - name: Run tests - run: dotnet test PosInformatique.Testing.Databases.sln \ + run: | + dotnet test PosInformatique.Testing.Databases.sln \ --configuration Release \ --no-build \ --logger "trx;LogFileName=test_results.trx" \ From ba1a4ab221bc996f78f3e5668314a5e3c56fb785 Mon Sep 17 00:00:00 2001 From: Gilles TOURREAU Date: Thu, 25 Sep 2025 10:09:06 +0200 Subject: [PATCH 52/61] Update github-actions-ci.yaml --- .github/workflows/github-actions-ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/github-actions-ci.yaml b/.github/workflows/github-actions-ci.yaml index 30af0aa..77f8120 100644 --- a/.github/workflows/github-actions-ci.yaml +++ b/.github/workflows/github-actions-ci.yaml @@ -57,7 +57,7 @@ jobs: - name: Run tests run: | - dotnet test PosInformatique.Testing.Databases.sln \ + dotnet test PosInformatique.Testing.Databases.sln \ --configuration Release \ --no-build \ --logger "trx;LogFileName=test_results.trx" \ From 185c3f10396abfcc4df67123eeef860b7a909e5f Mon Sep 17 00:00:00 2001 From: Gilles TOURREAU Date: Thu, 25 Sep 2025 10:25:06 +0200 Subject: [PATCH 53/61] Fix CI --- tests/Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Directory.Build.props b/tests/Directory.Build.props index 572e43c..d8be50d 100644 --- a/tests/Directory.Build.props +++ b/tests/Directory.Build.props @@ -15,7 +15,7 @@ - true + false true From 0d7da54a439fc8a127688d85e06328262549e492 Mon Sep 17 00:00:00 2001 From: Gilles TOURREAU Date: Thu, 25 Sep 2025 10:42:12 +0200 Subject: [PATCH 54/61] Fix CI. --- .github/workflows/github-actions-ci.yaml | 6 +++--- tests/Directory.Build.props | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/github-actions-ci.yaml b/.github/workflows/github-actions-ci.yaml index 77f8120..2cc1fb9 100644 --- a/.github/workflows/github-actions-ci.yaml +++ b/.github/workflows/github-actions-ci.yaml @@ -56,12 +56,12 @@ jobs: docker exec --user root $(docker ps -qf "ancestor=mcr.microsoft.com/mssql/server:2022-latest") chown -R mssql:mssql /tmp/other_databases_path - name: Run tests - run: | + run: | dotnet test PosInformatique.Testing.Databases.sln \ --configuration Release \ --no-build \ - --logger "trx;LogFileName=test_results.trx" \ - --results-directory ./TestResults + --results-directory ./TestResults \ + -- MSTest.PlatformArgs="--report-trx --report-trx-filename test_results.trx" \ env: SQL_SERVER_UNIT_TESTS_CONNECTION_STRING: "Data Source=localhost,1433;Database=master;User Id=sa;Password=P@ssw0rd12345!;TrustServerCertificate=True;" SQL_SERVER_UNIT_TESTS_OTHER_DATA_PATH: "/tmp/other_databases_path/" diff --git a/tests/Directory.Build.props b/tests/Directory.Build.props index d8be50d..572e43c 100644 --- a/tests/Directory.Build.props +++ b/tests/Directory.Build.props @@ -15,7 +15,7 @@ - false + true true From fdc3ba7cf8a7090d0a3d6b0cfd55fe80667dbd86 Mon Sep 17 00:00:00 2001 From: Gilles TOURREAU Date: Thu, 25 Sep 2025 10:50:27 +0200 Subject: [PATCH 55/61] Revert usage of MTP. --- .github/workflows/github-actions-ci.yaml | 4 ++-- tests/Directory.Build.props | 6 ------ 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/.github/workflows/github-actions-ci.yaml b/.github/workflows/github-actions-ci.yaml index 2cc1fb9..301eadc 100644 --- a/.github/workflows/github-actions-ci.yaml +++ b/.github/workflows/github-actions-ci.yaml @@ -60,8 +60,8 @@ jobs: dotnet test PosInformatique.Testing.Databases.sln \ --configuration Release \ --no-build \ - --results-directory ./TestResults \ - -- MSTest.PlatformArgs="--report-trx --report-trx-filename test_results.trx" \ + --logger "trx;LogFileName=test_results.trx" \ + --results-directory ./TestResults env: SQL_SERVER_UNIT_TESTS_CONNECTION_STRING: "Data Source=localhost,1433;Database=master;User Id=sa;Password=P@ssw0rd12345!;TrustServerCertificate=True;" SQL_SERVER_UNIT_TESTS_OTHER_DATA_PATH: "/tmp/other_databases_path/" diff --git a/tests/Directory.Build.props b/tests/Directory.Build.props index 572e43c..f63f507 100644 --- a/tests/Directory.Build.props +++ b/tests/Directory.Build.props @@ -12,12 +12,6 @@ - - - - true - true - From 2efa057d9be358664417ebed011dab90067d2676 Mon Sep 17 00:00:00 2001 From: Gilles TOURREAU Date: Thu, 25 Sep 2025 11:03:55 +0200 Subject: [PATCH 56/61] Restore VS Test --- Directory.Packages.props | 2 ++ tests/Directory.Build.props | 2 ++ .../Testing.Databases.SqlServer.Tests.DacPac.csproj | 2 ++ .../Testing.Databases.SqlServer.Tests.Source.csproj | 2 ++ .../Testing.Databases.SqlServer.Tests.Target.csproj | 2 ++ 5 files changed, 10 insertions(+) diff --git a/Directory.Packages.props b/Directory.Packages.props index bae20ac..f3d984f 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -9,8 +9,10 @@ + + \ No newline at end of file diff --git a/tests/Directory.Build.props b/tests/Directory.Build.props index f63f507..c02c472 100644 --- a/tests/Directory.Build.props +++ b/tests/Directory.Build.props @@ -10,6 +10,8 @@ runtime; build; native; contentfiles; analyzers; buildtransitive + + diff --git a/tests/Testing.Databases.SqlServer.Tests.DacPac/Testing.Databases.SqlServer.Tests.DacPac.csproj b/tests/Testing.Databases.SqlServer.Tests.DacPac/Testing.Databases.SqlServer.Tests.DacPac.csproj index b99ddfc..30ca847 100644 --- a/tests/Testing.Databases.SqlServer.Tests.DacPac/Testing.Databases.SqlServer.Tests.DacPac.csproj +++ b/tests/Testing.Databases.SqlServer.Tests.DacPac/Testing.Databases.SqlServer.Tests.DacPac.csproj @@ -13,6 +13,8 @@ + + diff --git a/tests/Testing.Databases.SqlServer.Tests.Source/Testing.Databases.SqlServer.Tests.Source.csproj b/tests/Testing.Databases.SqlServer.Tests.Source/Testing.Databases.SqlServer.Tests.Source.csproj index b99ddfc..30ca847 100644 --- a/tests/Testing.Databases.SqlServer.Tests.Source/Testing.Databases.SqlServer.Tests.Source.csproj +++ b/tests/Testing.Databases.SqlServer.Tests.Source/Testing.Databases.SqlServer.Tests.Source.csproj @@ -13,6 +13,8 @@ + + diff --git a/tests/Testing.Databases.SqlServer.Tests.Target/Testing.Databases.SqlServer.Tests.Target.csproj b/tests/Testing.Databases.SqlServer.Tests.Target/Testing.Databases.SqlServer.Tests.Target.csproj index d7bdb79..c9df446 100644 --- a/tests/Testing.Databases.SqlServer.Tests.Target/Testing.Databases.SqlServer.Tests.Target.csproj +++ b/tests/Testing.Databases.SqlServer.Tests.Target/Testing.Databases.SqlServer.Tests.Target.csproj @@ -13,6 +13,8 @@ + + From 4646e2bf14e5bc9554fb9e1a8f922bc929021d6f Mon Sep 17 00:00:00 2001 From: Gilles TOURREAU Date: Thu, 25 Sep 2025 11:24:39 +0200 Subject: [PATCH 57/61] Fix unit tests --- .../SqlCmdSqlServerExtensionsTest.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/Testing.Databases.SqlServer.SqlCmd.Tests/SqlCmdSqlServerExtensionsTest.cs b/tests/Testing.Databases.SqlServer.SqlCmd.Tests/SqlCmdSqlServerExtensionsTest.cs index 02a4c89..27d45dd 100644 --- a/tests/Testing.Databases.SqlServer.SqlCmd.Tests/SqlCmdSqlServerExtensionsTest.cs +++ b/tests/Testing.Databases.SqlServer.SqlCmd.Tests/SqlCmdSqlServerExtensionsTest.cs @@ -82,7 +82,7 @@ public void RunScript_WithVariables() server.Master.RunScript(temporaryFile.FileName, settings); - var database = server.GetDatabase("SqlCmdSqlServerExtensionsTest_RunScript"); + var database = server.GetDatabase("SqlCmdSqlServerExtensionsTest_RunScript_WithVariables"); var table = database.ExecuteQuery("SELECT * FROM MyTable"); @@ -90,17 +90,17 @@ public void RunScript_WithVariables() } [Fact] - public void RunScript_WithErros() + public void RunScript_WithErrors() { var server = new SqlServer(ConnectionString); - server.DeleteDatabase("SqlCmdSqlServerExtensionsTest_RunScript_WithErros"); + server.DeleteDatabase("SqlCmdSqlServerExtensionsTest_RunScript_WithErrors"); var settings = new SqlCmdRunScriptSettings() { Variables = { - { "DatabaseName", "SqlCmdSqlServerExtensionsTest_RunScript_WithErros" }, + { "DatabaseName", "SqlCmdSqlServerExtensionsTest_RunScript_WithErrors" }, { "TableName", "MyTable" }, }, }; @@ -125,14 +125,14 @@ CREATE TABLE ErrorBlabla exception.Which.Output.Should().StartWith( """ GOOOOOO ! - Changed database context to 'SqlCmdSqlServerExtensionsTest_RunScript_WithErros'. + Changed database context to 'SqlCmdSqlServerExtensionsTest_RunScript_WithErrors'. Msg 102, Level 15, State 1, """) .And.EndWith("Incorrect syntax near 'ErrorBlabla'."); exception.Which.Message.Should().Be($"Some errors has been occurred when executing the '{temporaryFile.FileName}'.{Environment.NewLine}{Environment.NewLine}-- Output --{Environment.NewLine}{exception.Which.Output}"); - var database = server.GetDatabase("SqlCmdSqlServerExtensionsTest_RunScript_WithErros"); + var database = server.GetDatabase("SqlCmdSqlServerExtensionsTest_RunScript_WithErrors"); var table = database.ExecuteQuery("SELECT * FROM sys.tables"); From 15cccc2994aaeb9baba3df44662285a59e72051a Mon Sep 17 00:00:00 2001 From: Gilles TOURREAU Date: Thu, 25 Sep 2025 11:54:50 +0200 Subject: [PATCH 58/61] Enable analyzers code in Release --- Directory.Build.props | 3 --- 1 file changed, 3 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index 47a3a52..012c3e0 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -15,9 +15,6 @@ enable - - false - $(NoWarn);SA0001;NU1903 From c012c92ecf1be0d463e0d76d4a3e20ce73f1fb87 Mon Sep 17 00:00:00 2001 From: Gilles TOURREAU Date: Thu, 25 Sep 2025 14:59:03 +0200 Subject: [PATCH 59/61] Fix CI. --- .github/workflows/github-actions-ci.yaml | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/.github/workflows/github-actions-ci.yaml b/.github/workflows/github-actions-ci.yaml index 301eadc..dad5f40 100644 --- a/.github/workflows/github-actions-ci.yaml +++ b/.github/workflows/github-actions-ci.yaml @@ -36,12 +36,6 @@ jobs: - name: Build solution run: dotnet build PosInformatique.Testing.Databases.sln --configuration Release --no-restore - - name: Restore samples - run: dotnet restore samples/PosInformatique.Testing.Databases.Samples.sln - - - name: Build samples - run: dotnet build samples/PosInformatique.Testing.Databases.Samples.sln --configuration Debug --no-restore - - name: Install SQL Server command-line tools run: | curl https://packages.microsoft.com/keys/microsoft.asc | sudo apt-key add - @@ -81,7 +75,7 @@ jobs: - name: Setup NuGet uses: nuget/setup-nuget@v2 - - name: Restore NuGet packages + - name: Restore NuGet packages of the samples run: nuget restore samples/PosInformatique.Testing.Databases.Samples.sln - name: Add msbuild to PATH From b2a6d3f0c6bcfe681553be201946438ba720e500 Mon Sep 17 00:00:00 2001 From: Gilles TOURREAU Date: Thu, 25 Sep 2025 15:14:36 +0200 Subject: [PATCH 60/61] Fix NuGet tags --- .../Testing.Databases.SqlServer.Dac.csproj | 2 +- .../Testing.Databases.SqlServer.EntityFramework.csproj | 2 +- .../Testing.Databases.SqlServer.SqlCmd.csproj | 2 +- .../Testing.Databases.SqlServer.csproj | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Testing.Databases.SqlServer.Dac/Testing.Databases.SqlServer.Dac.csproj b/src/Testing.Databases.SqlServer.Dac/Testing.Databases.SqlServer.Dac.csproj index d902c1b..4a68d3c 100644 --- a/src/Testing.Databases.SqlServer.Dac/Testing.Databases.SqlServer.Dac.csproj +++ b/src/Testing.Databases.SqlServer.Dac/Testing.Databases.SqlServer.Dac.csproj @@ -4,7 +4,7 @@ net6.0;net462 Testing.Databases.SqlServer.Dac is a library that contains a set of tools for testing to deploy DAC (Data-tier Applications) packages (.dacpac files). - testing unittest sqlserver repository tdd dataaccesslayer dacpac dac + testing;unittest;sqlserver;repository;tdd;dataaccesslayer;dacpac;dac True diff --git a/src/Testing.Databases.SqlServer.EntityFramework/Testing.Databases.SqlServer.EntityFramework.csproj b/src/Testing.Databases.SqlServer.EntityFramework/Testing.Databases.SqlServer.EntityFramework.csproj index 10e7a89..0e5a336 100644 --- a/src/Testing.Databases.SqlServer.EntityFramework/Testing.Databases.SqlServer.EntityFramework.csproj +++ b/src/Testing.Databases.SqlServer.EntityFramework/Testing.Databases.SqlServer.EntityFramework.csproj @@ -5,7 +5,7 @@ True Testing.Databases.SqlServer.EntityFramework is a library that contains a set of tools for testing Data Access Layer (repositories) based on Entity Framework and SQL Server. - testing unittest entityframework sqlserver repository tdd dataaccesslayer + testing;unittest;entityframework;sqlserver;repository;tdd;dataaccesslayer diff --git a/src/Testing.Databases.SqlServer.SqlCmd/Testing.Databases.SqlServer.SqlCmd.csproj b/src/Testing.Databases.SqlServer.SqlCmd/Testing.Databases.SqlServer.SqlCmd.csproj index b96a39c..1a4198c 100644 --- a/src/Testing.Databases.SqlServer.SqlCmd/Testing.Databases.SqlServer.SqlCmd.csproj +++ b/src/Testing.Databases.SqlServer.SqlCmd/Testing.Databases.SqlServer.SqlCmd.csproj @@ -5,7 +5,7 @@ True Testing.Databases.SqlServer.SqlCmd is a library that contains a set of tools for testing Data Access Layer using SQLCMD tool to initialize the database with a SQL script. - testing unittest sqlcmd sqlserver repository tdd dataaccesslayer + testing;unittest;sqlcmd;sqlserver;repository;tdd;dataaccesslayer diff --git a/src/Testing.Databases.SqlServer/Testing.Databases.SqlServer.csproj b/src/Testing.Databases.SqlServer/Testing.Databases.SqlServer.csproj index cbd5830..d1c0009 100644 --- a/src/Testing.Databases.SqlServer/Testing.Databases.SqlServer.csproj +++ b/src/Testing.Databases.SqlServer/Testing.Databases.SqlServer.csproj @@ -5,7 +5,7 @@ True Testing.Databases.SqlServer is a library that contains a set of tools for testing Data Access Layer (repositories) based on SQL Server. - testing unittest sqlserver repository tdd dataaccesslayer + testing;unittest;sqlserver;repository;tdd;dataaccesslayer From fd008178803aabaf54c4fb5eef8a8137278ec012 Mon Sep 17 00:00:00 2001 From: Gilles TOURREAU Date: Mon, 29 Sep 2025 05:49:08 +0200 Subject: [PATCH 61/61] Fix release Github actions yaml. --- .github/workflows/github-actions-release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/github-actions-release.yml b/.github/workflows/github-actions-release.yml index 32027d5..a3a1959 100644 --- a/.github/workflows/github-actions-release.yml +++ b/.github/workflows/github-actions-release.yml @@ -12,7 +12,7 @@ on: type: string description: The version suffix of the library (for example rc.1) -run-name: ${{ inputs.VersionPrefix }}-${{ inputs.VersionSuffix }} +run-name: ${{ inputs.VersionSuffix && format('{0}-{1}', inputs.VersionPrefix, inputs.VersionSuffix) || inputs.VersionPrefix }} jobs: build: