diff --git a/.github/workflows/github-actions-ci.yaml b/.github/workflows/github-actions-ci.yaml index 7457777..dad5f40 100644 --- a/.github/workflows/github-actions-ci.yaml +++ b/.github/workflows/github-actions-ci.yaml @@ -8,6 +8,66 @@ on: jobs: build: + runs-on: ubuntu-latest + permissions: + checks: write + pull-requests: write + services: + sqlserver: + image: mcr.microsoft.com/mssql/server:2022-latest + env: + SA_PASSWORD: "P@ssw0rd12345!" + ACCEPT_EULA: "Y" + ports: + - 1433:1433 + volumes: + - /tmp/other_databases_path:/tmp/other_databases_path + 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: 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 "/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: "/tmp/other_databases_path/" + + - name: Publish Test Results + uses: EnricoMi/publish-unit-test-result-action@v2 + if: (!cancelled()) + with: + files: | + TestResults/**/*.trx + + build-samples: runs-on: windows-latest steps: - uses: actions/checkout@v4 @@ -15,35 +75,11 @@ jobs: - name: Setup NuGet uses: nuget/setup-nuget@v2 - - name: Restore NuGet packages - run: nuget restore PosInformatique.Testing.Databases.sln - + - name: Restore NuGet packages of the samples + 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/** diff --git a/.github/workflows/github-actions-release.yml b/.github/workflows/github-actions-release.yml index 3d176f0..a3a1959 100644 --- a/.github/workflows/github-actions-release.yml +++ b/.github/workflows/github-actions-release.yml @@ -7,12 +7,12 @@ on: type: string description: The version of the library required: true - default: 2.3.0 + default: 3.0.0 VersionSuffix: 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: @@ -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/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 diff --git a/Directory.Packages.props b/Directory.Packages.props index 546b9aa..f3d984f 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -12,7 +12,7 @@ - + \ No newline at end of file diff --git a/PosInformatique.Testing.Databases.sln b/PosInformatique.Testing.Databases.sln index d1c2105..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 @@ -51,12 +51,22 @@ 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 +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 +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 @@ -101,6 +111,18 @@ 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 + {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 @@ -115,4 +137,15 @@ Global GlobalSection(ExtensibilityGlobals) = postSolution 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/README.md b/README.md index c761dd4..4b9ed3a 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,34 @@ 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 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 + +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/Directory.Build.props b/src/Directory.Build.props index 26a5ca9..9b643d3 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. @@ -42,7 +49,7 @@ - + <_Parameter1>$(AssemblyName).Tests diff --git a/src/Testing.Databases.SqlServer.Dac/Icon.png b/src/Testing.Databases.SqlServer.Dac/Icon.png index 4d3e4c8..8ed8d93 100644 Binary files a/src/Testing.Databases.SqlServer.Dac/Icon.png and b/src/Testing.Databases.SqlServer.Dac/Icon.png differ diff --git a/src/Testing.Databases.SqlServer.Dac/SqlServerDacDatabaseInitializer.cs b/src/Testing.Databases.SqlServer.Dac/SqlServerDacDatabaseInitializer.cs index 1724b3e..9528978 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,9 +23,23 @@ 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. + /// 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) + 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); @@ -34,7 +48,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/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..d72cdb8 100644 --- a/src/Testing.Databases.SqlServer.Dac/SqlServerDacExtensions.cs +++ b/src/Testing.Databases.SqlServer.Dac/SqlServerDacExtensions.cs @@ -21,16 +21,31 @@ 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. + /// 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) + 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)) { - 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..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,8 @@ 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 @@ -23,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/Icon.png b/src/Testing.Databases.SqlServer.EntityFramework/Icon.png index 76744e4..8ed8d93 100644 Binary files a/src/Testing.Databases.SqlServer.EntityFramework/Icon.png and b/src/Testing.Databases.SqlServer.EntityFramework/Icon.png differ 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..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 @@ -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/Icon.png b/src/Testing.Databases.SqlServer.SqlCmd/Icon.png new file mode 100644 index 0000000..8ed8d93 Binary files /dev/null and b/src/Testing.Databases.SqlServer.SqlCmd/Icon.png differ 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..676dbf8 --- /dev/null +++ b/src/Testing.Databases.SqlServer.SqlCmd/SqlCmdDatabaseInitializer.cs @@ -0,0 +1,64 @@ +//----------------------------------------------------------------------- +// +// 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. + /// 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) + { + Guard.ThrowIfNull(initializer, nameof(initializer)); + Guard.ThrowIfNull(fileName, nameof(fileName)); + Guard.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); + + 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..567f1ef --- /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 readonly List output; + + private Process? process; + + 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..032e656 --- /dev/null +++ b/src/Testing.Databases.SqlServer.SqlCmd/SqlCmdSqlServerDatabaseExtensions.cs @@ -0,0 +1,55 @@ +//----------------------------------------------------------------------- +// +// 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 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) + { + Guard.ThrowIfNull(database, nameof(database)); + Guard.ThrowIfNull(fileName, nameof(fileName)); + + if (!File.Exists(fileName)) + { + throw new FileNotFoundException($"Could not find file '{fileName}'", fileName); + } + + 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}'.{Environment.NewLine}{Environment.NewLine}-- Output --{Environment.NewLine}{sqlCmdProcess.Output}", 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..1a4198c --- /dev/null +++ b/src/Testing.Databases.SqlServer.SqlCmd/Testing.Databases.SqlServer.SqlCmd.csproj @@ -0,0 +1,25 @@ + + + + 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. + testing;unittest;sqlcmd;sqlserver;repository;tdd;dataaccesslayer + + + + + + + + + + + + + + + + + diff --git a/src/Testing.Databases.SqlServer/Icon.png b/src/Testing.Databases.SqlServer/Icon.png index 4d3e4c8..8ed8d93 100644 Binary files a/src/Testing.Databases.SqlServer/Icon.png and b/src/Testing.Databases.SqlServer/Icon.png differ 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..de49e6b 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,25 @@ 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)) + { + 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}')"); + } + } + + return sql.ToString(); + } } } 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 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/Directory.Build.props b/tests/Directory.Build.props index 65d79b0..c02c472 100644 --- a/tests/Directory.Build.props +++ b/tests/Directory.Build.props @@ -2,6 +2,18 @@ + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + 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..a8bf9e0 --- /dev/null +++ b/tests/Testing.Databases.SqlServer.Dac.Tests/SqlServerDacExtensionsTest.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 SqlServerDacExtensionsTest + { + private static readonly string ConnectionString = ConnectionStrings.Get(); + + [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("PosInformatique.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_WithSpecificDataFileName() + { + // Create existing database to be sure the database is recreated when deploying the database with a DACPAC + CreateDatabase("SqlServerDacExtensionsTest_DeployDacPackage_WithSpecificDataFileName"); + + using var otherDataPath = OtherDatabasePath.Create(); + + var server = new SqlServer(ConnectionString); + + var settings = new SqlServerDacDeploymentSettings() + { + DataFileName = Path.Combine(otherDataPath.Path, "TheSpecificDataFileName.mdf"), + }; + + var database = server.DeployDacPackage("PosInformatique.Testing.Databases.SqlServer.Tests.DacPac.dacpac", "SqlServerDacExtensionsTest_DeployDacPackage_WithSpecificDataFileName", 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(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(otherDataPath.Path, "TheSpecificDataFileName.mdf")); + result.Rows[0]["type_desc"].Should().Be("ROWS"); + + 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"); + + // Delete the database (for deleting the temporary folder). + 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); + + 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.Dac.Tests/SqlServerDatabaseInitializerTest.cs b/tests/Testing.Databases.SqlServer.Dac.Tests/SqlServerDatabaseInitializerTest.cs new file mode 100644 index 0000000..4af4ca1 --- /dev/null +++ b/tests/Testing.Databases.SqlServer.Dac.Tests/SqlServerDatabaseInitializerTest.cs @@ -0,0 +1,223 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) P.O.S Informatique. All rights reserved. +// +//----------------------------------------------------------------------- + +namespace PosInformatique.Testing.Databases.SqlServer.Tests +{ + [Collection("PosInformatique.Testing.Databases.SqlServer.Tests")] + public class SqlServerDatabaseInitializerTest : IClassFixture + { + private static readonly string ConnectionString = ConnectionStrings.Get(nameof(SqlServerDatabaseInitializerTest)); + + private readonly SqlServerDatabase database; + + private readonly SqlServerDatabaseInitializer initializer; + + public SqlServerDatabaseInitializerTest(SqlServerDatabaseInitializer initializer) + { + this.initializer = initializer; + this.database = initializer.Initialize("PosInformatique.Testing.Databases.SqlServer.Tests.DacPac.dacpac", ConnectionString); + + 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(ConnectionStrings.ExtractUserName(ConnectionString)); + + // 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(ConnectionStrings.ExtractUserName(ConnectionString)); + + // 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()", 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", TestContext.Current.CancellationToken); + + 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()", 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", TestContext.Current.CancellationToken); + + 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 void Initialize_WithSpecificDataFileName() + { + this.initializer.IsInitialized.Should().BeTrue(); + + // Create existing database to be sure the database is recreated when deploying the database with a DACPAC + CreateDatabase("SqlServerDatabaseInitializerTest_Initialize_WithSpecificDataFileName"); + + using var otherDataPath = OtherDatabasePath.Create(); + + var server = new SqlServer(ConnectionString); + + var settings = new SqlServerDacDeploymentSettings() + { + DataFileName = Path.Combine(otherDataPath.Path, "TheSpecificDataFileName.mdf"), + }; + + var database = server.DeployDacPackage("PosInformatique.Testing.Databases.SqlServer.Tests.DacPac.dacpac", "SqlServerDatabaseInitializerTest_Initialize_WithSpecificDataFileName", 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(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(otherDataPath.Path, "TheSpecificDataFileName.mdf")); + result.Rows[0]["type_desc"].Should().Be("ROWS"); + + 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"); + + // Delete the database (for deleting the temporary folder). + 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); + + 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.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..3ad7b37 --- /dev/null +++ b/tests/Testing.Databases.SqlServer.Dac.Tests/Testing.Databases.SqlServer.Dac.Tests.csproj @@ -0,0 +1,21 @@ + + + + net8.0 + Exe + + + + + PreserveNewest + + + + + + + + + + + diff --git a/tests/Testing.Databases.SqlServer.EntityFramework.Tests/EntityFrameworkDatabaseInitializerExtensionsTest.cs b/tests/Testing.Databases.SqlServer.EntityFramework.Tests/EntityFrameworkDatabaseInitializerExtensionsTest.cs index 4ab4d63..503d882 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; @@ -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"); @@ -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..bf7b1e9 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,9 +26,9 @@ 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(); + var tables = await database.GetTablesAsync(TestContext.Current.CancellationToken); tables.Should().HaveCount(1); @@ -54,9 +54,9 @@ 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(); + 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("Data Source=(localDB)\\posinfo-tests;Initial Catalog=EntityFrameworkSqlServerExtensionsTest;Integrated Security=True"); + 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("Data Source=(localDB)\\posinfo-tests;Initial Catalog=EntityFrameworkSqlServerExtensionsTest;Integrated Security=True"); + database.ConnectionString.Should().Be(ConnectionStrings.Get("EntityFrameworkSqlServerExtensionsTest")); - var tables = await database.GetTablesAsync(); + var tables = await database.GetTablesAsync(TestContext.Current.CancellationToken); tables.Should().HaveCount(1); @@ -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.EntityFramework.Tests/Testing.Databases.SqlServer.EntityFramework.Tests.csproj b/tests/Testing.Databases.SqlServer.EntityFramework.Tests/Testing.Databases.SqlServer.EntityFramework.Tests.csproj index a9979d0..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,24 +2,13 @@ net8.0 + Exe - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - + + 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..eac8d77 --- /dev/null +++ b/tests/Testing.Databases.SqlServer.Shared.Tests/ConnectionStrings.cs @@ -0,0 +1,42 @@ +//----------------------------------------------------------------------- +// +// 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(); + } + + 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.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 new file mode 100644 index 0000000..277c408 --- /dev/null +++ b/tests/Testing.Databases.SqlServer.Shared.Tests/Testing.Databases.SqlServer.Shared.Tests.projitems @@ -0,0 +1,15 @@ + + + + $(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/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.Tests/SqlServerDatabaseInitializerTest.cs b/tests/Testing.Databases.SqlServer.SqlCmd.Tests/SqlCmdDatabaseInitializerTest.cs similarity index 51% rename from tests/Testing.Databases.SqlServer.Tests/SqlServerDatabaseInitializerTest.cs rename to tests/Testing.Databases.SqlServer.SqlCmd.Tests/SqlCmdDatabaseInitializerTest.cs index 44c72e0..35a427f 100644 --- a/tests/Testing.Databases.SqlServer.Tests/SqlServerDatabaseInitializerTest.cs +++ b/tests/Testing.Databases.SqlServer.SqlCmd.Tests/SqlCmdDatabaseInitializerTest.cs @@ -1,5 +1,5 @@ //----------------------------------------------------------------------- -// +// // Copyright (c) P.O.S Informatique. All rights reserved. // //----------------------------------------------------------------------- @@ -7,20 +7,33 @@ namespace PosInformatique.Testing.Databases.SqlServer.Tests { [Collection("PosInformatique.Testing.Databases.SqlServer.Tests")] - public class SqlServerDatabaseInitializerTest : IClassFixture + public class SqlCmdDatabaseInitializerTest : 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(SqlCmdDatabaseInitializerTest)); private readonly SqlServerDatabase database; - public SqlServerDatabaseInitializerTest(SqlServerDatabaseInitializer initializer) + private readonly SqlServerDatabaseInitializer initializer; + + public SqlCmdDatabaseInitializerTest(SqlServerDatabaseInitializer initializer) { - this.database = initializer.Initialize("Testing.Databases.SqlServer.Tests.DacPac.dacpac", ConnectionString); + 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" }); } @@ -28,8 +41,10 @@ public SqlServerDatabaseInitializerTest(SqlServerDatabaseInitializer initializer [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}"); + currentUser.Rows[0][0].Should().Be(ConnectionStrings.ExtractUserName(ConnectionString)); // Check the constructor has been called var table = this.database.ExecuteQuery("SELECT * FROM MyTable"); @@ -49,8 +64,10 @@ public void Test1() [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}"); + currentUser.Rows[0][0].Should().Be(ConnectionStrings.ExtractUserName(ConnectionString)); // Check the constructor has been called var table = this.database.ExecuteQuery("SELECT * FROM MyTable"); @@ -70,11 +87,13 @@ public void Test2() [Fact] public async Task Test1Async() { - var currentUser = await this.database.ExecuteQueryAsync("SELECT SUSER_NAME()"); - currentUser.Rows[0][0].Should().Be($"{Environment.UserDomainName}\\{Environment.UserName}"); + this.initializer.IsInitialized.Should().BeTrue(); + + 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); @@ -91,11 +110,13 @@ public async Task Test1Async() [Fact] public async Task Test2Async() { - var currentUser = await this.database.ExecuteQueryAsync("SELECT SUSER_NAME()"); - currentUser.Rows[0][0].Should().Be($"{Environment.UserDomainName}\\{Environment.UserName}"); + this.initializer.IsInitialized.Should().BeTrue(); + + 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); @@ -108,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_WithInitializerArgumentNull() + { + 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/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..3c6c158 --- /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.{Environment.NewLine}Object 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/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 new file mode 100644 index 0000000..27d45dd --- /dev/null +++ b/tests/Testing.Databases.SqlServer.SqlCmd.Tests/SqlCmdSqlServerExtensionsTest.cs @@ -0,0 +1,175 @@ +//----------------------------------------------------------------------- +// +// 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 static readonly string ConnectionString = ConnectionStrings.Get(); + + [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_WithVariables"); + + var table = database.ExecuteQuery("SELECT * FROM MyTable"); + + table.Rows.Should().BeEmpty(); + } + + [Fact] + public void RunScript_WithErrors() + { + var server = new SqlServer(ConnectionString); + + server.DeleteDatabase("SqlCmdSqlServerExtensionsTest_RunScript_WithErrors"); + + var settings = new SqlCmdRunScriptSettings() + { + Variables = + { + { "DatabaseName", "SqlCmdSqlServerExtensionsTest_RunScript_WithErrors" }, + { "TableName", "MyTable" }, + }, + }; + + using var temporaryFile = TemporaryFile.Create(); + + File.WriteAllText( + temporaryFile.FileName, + """ + PRINT 'GOOOOOO !' + GO + CREATE DATABASE [$(DatabaseName)] + GO + USE [$(DatabaseName)] + GO + CREATE TABLE ErrorBlabla + """); + + 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_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_WithErrors"); + + var table = database.ExecuteQuery("SELECT * FROM sys.tables"); + + 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 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..5e8124b --- /dev/null +++ b/tests/Testing.Databases.SqlServer.SqlCmd.Tests/Testing.Databases.SqlServer.SqlCmd.Tests.csproj @@ -0,0 +1,20 @@ + + + + net8.0 + Exe + + + + + PreserveNewest + + + + + + + + + + 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..30ca847 --- /dev/null +++ b/tests/Testing.Databases.SqlServer.Tests.DacPac/Testing.Databases.SqlServer.Tests.DacPac.csproj @@ -0,0 +1,26 @@ + + + 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..30ca847 --- /dev/null +++ b/tests/Testing.Databases.SqlServer.Tests.Source/Testing.Databases.SqlServer.Tests.Source.csproj @@ -0,0 +1,26 @@ + + + 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..c9df446 --- /dev/null +++ b/tests/Testing.Databases.SqlServer.Tests.Target/Testing.Databases.SqlServer.Tests.Target.csproj @@ -0,0 +1,36 @@ + + + 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/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 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/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/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 03eb07b..43a5045 100644 --- a/tests/Testing.Databases.SqlServer.Tests/SqlServerDatabaseComparerTest.cs +++ b/tests/Testing.Databases.SqlServer.Tests/SqlServerDatabaseComparerTest.cs @@ -9,15 +9,15 @@ 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() { 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"), 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(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, TestContext.Current.CancellationToken); // 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]{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"); + 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"); + 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"); - 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]{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"); + 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"); + 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"); - 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]{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"); diff --git a/tests/Testing.Databases.SqlServer.Tests/SqlServerDatabaseExtensionsTest.cs b/tests/Testing.Databases.SqlServer.Tests/SqlServerDatabaseExtensionsTest.cs index 4718aec..5ca9f01 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() @@ -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 e4824da..c9f344e 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")] @@ -22,22 +22,134 @@ public void Constructor(string connectionString, string expectedMasterConnection server.Master.Server.Should().BeSameAs(server); } + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task CreateAndDelete(bool withCreationSettings) + { + var server = new SqlServer(ConnectionString); + + SqlDatabaseCreationSettings settings = null; + + if (withCreationSettings) + { + settings = new SqlDatabaseCreationSettings(); + } + + var database = server.CreateEmptyDatabase("CreateAndDeleteDB", settings); + + database.ConnectionString.Should().Be(ConnectionStrings.Get("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'", TestContext.Current.CancellationToken); + table.Rows.Should().BeEmpty(); + } + + [Fact] + public async Task CreateAndDelete_WithSpecificDataFileName() + { + using var otherDataPath = OtherDatabasePath.Create(); + + var server = new SqlServer(ConnectionString); + + var settings = new SqlDatabaseCreationSettings() + { + DataFileName = Path.Combine(otherDataPath.Path, "TheSpecificDataFileName.mdf"), + }; + + var database = server.CreateEmptyDatabase("CreateAndDeleteDB_WithSpecificDataFileName", settings); + + database.ConnectionString.Should().Be(ConnectionStrings.Get("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 + 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(otherDataPath.Path, "TheSpecificDataFileName.mdf")); + result.Rows[0]["type_desc"].Should().Be("ROWS"); + + 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"); + + // Delete the database + server.DeleteDatabase("CreateAndDeleteDB_WithSpecificDataFileName"); + + table = await server.Master.ExecuteQueryAsync("SELECT * FROM [sys].[databases] WHERE [name] = 'CreateAndDeleteDB_WithSpecificDataFileName'", TestContext.Current.CancellationToken); + table.Rows.Should().BeEmpty(); + } + [Fact] public async Task CreateAndDeleteAsync() { var server = new SqlServer(ConnectionString); - var database = await server.CreateEmptyDatabaseAsync("CreateAndDeleteDB", CancellationToken.None); + var database = await server.CreateEmptyDatabaseAsync("CreateAndDeleteDBAsync", new SqlDatabaseCreationSettings(), CancellationToken.None); - database.ConnectionString.Should().Be("Data Source=(localDB)\\posinfo-tests;Initial Catalog=CreateAndDeleteDB;Integrated Security=True"); + database.ConnectionString.Should().Be(ConnectionStrings.Get("CreateAndDeleteDBAsync")); - 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] = 'CreateAndDeleteDBAsync'", TestContext.Current.CancellationToken); table.Rows.Should().HaveCount(1); // Delete the database - await server.DeleteDatabaseAsync("CreateAndDeleteDB", CancellationToken.None); + await server.DeleteDatabaseAsync("CreateAndDeleteDBAsync", CancellationToken.None); + + table = await server.Master.ExecuteQueryAsync("SELECT * FROM [sys].[databases] WHERE [name] = 'CreateAndDeleteDBAsync'", TestContext.Current.CancellationToken); + table.Rows.Should().BeEmpty(); + } + + [Fact] + public async Task CreateAndDeleteAsync_WithSpecificDataFileName() + { + using var otherDataPath = OtherDatabasePath.Create(); + + var server = new SqlServer(ConnectionString); + + var settings = new SqlDatabaseCreationSettings() + { + DataFileName = Path.Combine(otherDataPath.Path, "TheSpecificDataFileNameAsync.mdf"), + }; + + 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'", TestContext.Current.CancellationToken); + table.Rows.Should().HaveCount(1); + + // 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(); + + 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(otherDataPath.Path, "TheSpecificDataFileNameAsync.mdf")); + result.Rows[0]["type_desc"].Should().Be("ROWS"); + + 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"); + + // Delete the database + await server.DeleteDatabaseAsync("CreateAndDeleteDB_WithSpecificDataFileNameAsync", TestContext.Current.CancellationToken); - table = await server.Master.ExecuteQueryAsync("SELECT * FROM [sys].[databases] WHERE [name] = 'CreateAndDeleteDB'"); + 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 966431e..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 @@ -9,16 +10,10 @@ - + PreserveNewest - - - - - PreserveNewest - - + PreserveNewest @@ -27,24 +22,11 @@ - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - + + + - - - - - - +