From c3965da3735bbcfedf2e2200f12c80db1bbb0d76 Mon Sep 17 00:00:00 2001 From: Adrian Hall Date: Fri, 25 Apr 2025 10:01:00 -0700 Subject: [PATCH 1/2] (#344) TestContainer for MySQL. --- .github/copilot-instructions.md | 12 +++ Directory.Packages.props | 1 + infra/modules/mysql.bicep | 89 ------------------- infra/resources.bicep | 15 ---- ...ync.Server.EntityFrameworkCore.Test.csproj | 1 + .../MySqlEntityTableRepository_Tests.cs | 28 ++++-- 6 files changed, 36 insertions(+), 110 deletions(-) create mode 100644 .github/copilot-instructions.md delete mode 100644 infra/modules/mysql.bicep diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 00000000..d5ccdd00 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,12 @@ +# Datasync Community Toolkit + +This is a set of instructions for the GitHub Copilot to support the Datasync Community Toolkit. + +## Project Layout + +* `/docs` - documentation, mostly Markdown supporting `mkdocs` document generation. +* `/infra` - infrastructure definition to support live testing. +* `/samples` - sample code for the library. +* `/src` - source code for the NuGet packages. +* `/templates` - the template source code. +* `/tests` - the test projects diff --git a/Directory.Packages.props b/Directory.Packages.props index b695c1d4..5f844ff2 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -40,6 +40,7 @@ + diff --git a/infra/modules/mysql.bicep b/infra/modules/mysql.bicep deleted file mode 100644 index 8d0397eb..00000000 --- a/infra/modules/mysql.bicep +++ /dev/null @@ -1,89 +0,0 @@ -targetScope = 'resourceGroup' - -@description('The list of firewall rules to install') -param firewallRules FirewallRule[] = [ - { startIpAddress: '0.0.0.0', endIpAddress: '0.0.0.0' } -] - -@minLength(1) -@description('The name of the test database to create') -param databaseName string = 'unittests' - -@minLength(1) -@description('Primary location for all resources') -param location string = resourceGroup().location - -@description('The name of the SQL Server to create.') -param sqlServerName string - -@description('Optional - the SQL Server administrator password. If not provided, the username will be \'appadmin\'.') -param sqlAdminUsername string = 'appadmin' - -@secure() -@description('Optional - SQL Server administrator password. If not provided, a random password will be generated.') -param sqlAdminPassword string = newGuid() - -@description('The list of tags to apply to all resources.') -param tags object = {} - -/*********************************************************************************/ - -resource mysql_server 'Microsoft.DBforMySQL/flexibleServers@2024-10-01-preview' = { - name: sqlServerName - location: location - tags: tags - sku: { - name: 'Standard_B1ms' - tier: 'Burstable' - } - properties: { - administratorLogin: sqlAdminUsername - administratorLoginPassword: sqlAdminPassword - createMode: 'Default' - authConfig: { - activeDirectoryAuth: 'Disabled' - passwordAuth: 'Enabled' - } - backup: { - backupRetentionDays: 7 - geoRedundantBackup: 'Disabled' - } - highAvailability: { - mode: 'Disabled' - } - storage: { - storageSizeGB: 32 - autoGrow: 'Disabled' - } - version: '8.0.21' - } - - resource fw 'firewallRules@2023-12-30' = [ for (fwRule, idx) in firewallRules : { - name: 'fw${idx}' - properties: { - startIpAddress: fwRule.startIpAddress - endIpAddress: fwRule.endIpAddress - } - }] -} - -resource mysql_database 'Microsoft.DBforMySQL/flexibleServers/databases@2023-12-30' = { - name: databaseName - parent: mysql_server - properties: { - charset: 'ascii' - collation: 'ascii_general_ci' - } -} - -/*********************************************************************************/ - -#disable-next-line outputs-should-not-contain-secrets -output MYSQL_CONNECTIONSTRING string = 'server=${mysql_server.properties.fullyQualifiedDomainName};database=${mysql_database.name};user=${mysql_server.properties.administratorLogin};password=${sqlAdminPassword}' - -/*********************************************************************************/ - -type FirewallRule = { - startIpAddress: string - endIpAddress: string -} diff --git a/infra/resources.bicep b/infra/resources.bicep index 3be157e9..42b2501c 100644 --- a/infra/resources.bicep +++ b/infra/resources.bicep @@ -34,7 +34,6 @@ var appServiceName = 'web-${resourceToken}' var azsqlServerName = 'sql-${resourceToken}' var cosmosServerName = 'cosmos-${resourceToken}' var pgsqlServerName = 'pgsql-${resourceToken}' -var mysqlServerName = 'mysql-${resourceToken}' var mongoServerName = 'mongo-${resourceToken}' var mongoaciServerName = 'mongoaci-${resourceToken}' @@ -81,19 +80,6 @@ module pgsql './modules/postgresql.bicep' = { } } -module mysql './modules/mysql.bicep' = { - name: 'mysql-deployment-${resourceToken}' - params: { - location: location - tags: tags - databaseName: testDatabaseName - firewallRules: clientIpFirewallRules - sqlServerName: mysqlServerName - sqlAdminUsername: sqlAdminUsername - sqlAdminPassword: sqlAdminPassword - } -} - module cosmos './modules/cosmos.bicep' = { name: 'cosmos-deployment-${resourceToken}' params: { @@ -150,6 +136,5 @@ output AZSQL_CONNECTIONSTRING string = azuresql.outputs.AZSQL_CONNECTIONSTRING output COSMOS_CONNECTIONSTRING string = cosmos.outputs.COSMOS_CONNECTIONSTRING output MONGO_CONNECTIONSTRING string = mongodb.outputs.MONGO_CONNECTIONSTRING output MONGOACI_CONNECTIONSTRING string = mongoaci.outputs.MONGO_CONNECTIONSTRING -output MYSQL_CONNECTIONSTRING string = mysql.outputs.MYSQL_CONNECTIONSTRING output PGSQL_CONNECTIONSTRING string = pgsql.outputs.PGSQL_CONNECTIONSTRING output SERVICE_ENDPOINT string = app_service.outputs.SERVICE_ENDPOINT diff --git a/tests/CommunityToolkit.Datasync.Server.EntityFrameworkCore.Test/CommunityToolkit.Datasync.Server.EntityFrameworkCore.Test.csproj b/tests/CommunityToolkit.Datasync.Server.EntityFrameworkCore.Test/CommunityToolkit.Datasync.Server.EntityFrameworkCore.Test.csproj index 965b7729..eed3869c 100644 --- a/tests/CommunityToolkit.Datasync.Server.EntityFrameworkCore.Test/CommunityToolkit.Datasync.Server.EntityFrameworkCore.Test.csproj +++ b/tests/CommunityToolkit.Datasync.Server.EntityFrameworkCore.Test/CommunityToolkit.Datasync.Server.EntityFrameworkCore.Test.csproj @@ -19,5 +19,6 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all + diff --git a/tests/CommunityToolkit.Datasync.Server.EntityFrameworkCore.Test/MySqlEntityTableRepository_Tests.cs b/tests/CommunityToolkit.Datasync.Server.EntityFrameworkCore.Test/MySqlEntityTableRepository_Tests.cs index 3e233001..4d350da2 100644 --- a/tests/CommunityToolkit.Datasync.Server.EntityFrameworkCore.Test/MySqlEntityTableRepository_Tests.cs +++ b/tests/CommunityToolkit.Datasync.Server.EntityFrameworkCore.Test/MySqlEntityTableRepository_Tests.cs @@ -5,6 +5,7 @@ using CommunityToolkit.Datasync.TestCommon; using CommunityToolkit.Datasync.TestCommon.Databases; using Microsoft.EntityFrameworkCore; +using Testcontainers.MySql; using Xunit.Abstractions; #pragma warning disable CS9113 // Parameter is unread. @@ -18,14 +19,24 @@ public class MysqlEntityTableRepository_Tests(DatabaseFixture fixture, ITestOutp #region Setup private readonly Random random = new(); private List movies = []; + private MySqlContainer _container; public async Task InitializeAsync() { - if (!string.IsNullOrEmpty(ConnectionStrings.MySql)) - { - Context = await MysqlDbContext.CreateContextAsync(ConnectionStrings.MySql, output); - this.movies = await Context.Movies.AsNoTracking().ToListAsync(); - } + this._container = new MySqlBuilder() + .WithImage("mysql:lts-oracle") + .WithCleanUp(true) + .WithUsername("testuser") + .WithPassword("testpassword") + .WithDatabase("testdb") + .Build(); + + await this._container.StartAsync(); + + string connectionString = this._container.GetConnectionString(); + + Context = await MysqlDbContext.CreateContextAsync(connectionString, output); + this.movies = await Context.Movies.AsNoTracking().ToListAsync(); } public async Task DisposeAsync() @@ -34,11 +45,16 @@ public async Task DisposeAsync() { await Context.DisposeAsync(); } + + if (this._container is not null) + { + await this._container.DisposeAsync(); + } } private MysqlDbContext Context { get; set; } - protected override bool CanRunLiveTests() => !string.IsNullOrEmpty(ConnectionStrings.MySql); + protected override bool CanRunLiveTests() => true; protected override async Task GetEntityAsync(string id) => await Context.Movies.AsNoTracking().SingleOrDefaultAsync(m => m.Id == id); From da0d1165934ca6d173d62c1ebfbe011b865f1801 Mon Sep 17 00:00:00 2001 From: Adrian Hall Date: Fri, 25 Apr 2025 12:09:42 -0700 Subject: [PATCH 2/2] (#344) Added MySqlDatabaseFixture --- ...ync.Server.EntityFrameworkCore.Test.csproj | 1 - .../MySqlEntityTableRepository_Tests.cs | 24 ++------- .../Live/MySQL_Controller_Tests.cs | 17 ++----- ...ommunityToolkit.Datasync.TestCommon.csproj | 3 +- .../Databases/ConnectionStrings.cs | 2 - .../Fixtures/MySqlDatabaseFixture.cs | 49 +++++++++++++++++++ .../Fixtures/MySqlTestFixture.cs | 0 7 files changed, 59 insertions(+), 37 deletions(-) create mode 100644 tests/CommunityToolkit.Datasync.TestCommon/Fixtures/MySqlDatabaseFixture.cs create mode 100644 tests/CommunityToolkit.Datasync.TestCommon/Fixtures/MySqlTestFixture.cs diff --git a/tests/CommunityToolkit.Datasync.Server.EntityFrameworkCore.Test/CommunityToolkit.Datasync.Server.EntityFrameworkCore.Test.csproj b/tests/CommunityToolkit.Datasync.Server.EntityFrameworkCore.Test/CommunityToolkit.Datasync.Server.EntityFrameworkCore.Test.csproj index eed3869c..965b7729 100644 --- a/tests/CommunityToolkit.Datasync.Server.EntityFrameworkCore.Test/CommunityToolkit.Datasync.Server.EntityFrameworkCore.Test.csproj +++ b/tests/CommunityToolkit.Datasync.Server.EntityFrameworkCore.Test/CommunityToolkit.Datasync.Server.EntityFrameworkCore.Test.csproj @@ -19,6 +19,5 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all - diff --git a/tests/CommunityToolkit.Datasync.Server.EntityFrameworkCore.Test/MySqlEntityTableRepository_Tests.cs b/tests/CommunityToolkit.Datasync.Server.EntityFrameworkCore.Test/MySqlEntityTableRepository_Tests.cs index 4d350da2..e9df8a0d 100644 --- a/tests/CommunityToolkit.Datasync.Server.EntityFrameworkCore.Test/MySqlEntityTableRepository_Tests.cs +++ b/tests/CommunityToolkit.Datasync.Server.EntityFrameworkCore.Test/MySqlEntityTableRepository_Tests.cs @@ -4,8 +4,8 @@ using CommunityToolkit.Datasync.TestCommon; using CommunityToolkit.Datasync.TestCommon.Databases; +using CommunityToolkit.Datasync.TestCommon.Fixtures; using Microsoft.EntityFrameworkCore; -using Testcontainers.MySql; using Xunit.Abstractions; #pragma warning disable CS9113 // Parameter is unread. @@ -14,28 +14,15 @@ namespace CommunityToolkit.Datasync.Server.EntityFrameworkCore.Test; [ExcludeFromCodeCoverage] [Collection("LiveTestsCollection")] -public class MysqlEntityTableRepository_Tests(DatabaseFixture fixture, ITestOutputHelper output) : RepositoryTests, IAsyncLifetime +public class MysqlEntityTableRepository_Tests(MySqlDatabaseFixture fixture, ITestOutputHelper output) : RepositoryTests, IClassFixture, IAsyncLifetime { #region Setup private readonly Random random = new(); private List movies = []; - private MySqlContainer _container; public async Task InitializeAsync() { - this._container = new MySqlBuilder() - .WithImage("mysql:lts-oracle") - .WithCleanUp(true) - .WithUsername("testuser") - .WithPassword("testpassword") - .WithDatabase("testdb") - .Build(); - - await this._container.StartAsync(); - - string connectionString = this._container.GetConnectionString(); - - Context = await MysqlDbContext.CreateContextAsync(connectionString, output); + Context = await MysqlDbContext.CreateContextAsync(fixture.ConnectionString, output); this.movies = await Context.Movies.AsNoTracking().ToListAsync(); } @@ -45,11 +32,6 @@ public async Task DisposeAsync() { await Context.DisposeAsync(); } - - if (this._container is not null) - { - await this._container.DisposeAsync(); - } } private MysqlDbContext Context { get; set; } diff --git a/tests/CommunityToolkit.Datasync.Server.Test/Live/MySQL_Controller_Tests.cs b/tests/CommunityToolkit.Datasync.Server.Test/Live/MySQL_Controller_Tests.cs index d0f03e24..4d6dcc0e 100644 --- a/tests/CommunityToolkit.Datasync.Server.Test/Live/MySQL_Controller_Tests.cs +++ b/tests/CommunityToolkit.Datasync.Server.Test/Live/MySQL_Controller_Tests.cs @@ -5,6 +5,7 @@ using CommunityToolkit.Datasync.Server.EntityFrameworkCore; using CommunityToolkit.Datasync.Server.Test.Helpers; using CommunityToolkit.Datasync.TestCommon.Databases; +using CommunityToolkit.Datasync.TestCommon.Fixtures; using Microsoft.EntityFrameworkCore; using Microsoft.VisualStudio.TestPlatform.Utilities; using Xunit.Abstractions; @@ -13,7 +14,7 @@ namespace CommunityToolkit.Datasync.Server.Test.Live; [ExcludeFromCodeCoverage] [Collection("LiveTestsCollection")] -public class MySQL_Controller_Tests(DatabaseFixture fixture, ITestOutputHelper output) : LiveControllerTests, IAsyncLifetime +public class MySQL_Controller_Tests(MySqlDatabaseFixture fixture, ITestOutputHelper output) : LiveControllerTests, IClassFixture, IAsyncLifetime { #region Setup private readonly Random random = new(); @@ -21,16 +22,8 @@ public class MySQL_Controller_Tests(DatabaseFixture fixture, ITestOutputHelper o public async Task InitializeAsync() { - if (!string.IsNullOrEmpty(ConnectionStrings.MySql)) - { - // Note: we don't clear entities on every run to speed up the test runs. This can only be done because - // the tests are read-only (associated with the query and get capabilities). If the test being run writes - // to the database then change clearEntities to true. - output.WriteLine($"MysqlIsInitialized = {fixture.MysqlIsInitialized}"); - Context = await MysqlDbContext.CreateContextAsync(ConnectionStrings.MySql, output, clearEntities: !fixture.MysqlIsInitialized); - this.movies = await Context.Movies.AsNoTracking().ToListAsync(); - fixture.MysqlIsInitialized = true; - } + Context = await MysqlDbContext.CreateContextAsync(fixture.ConnectionString, output); + this.movies = await Context.Movies.AsNoTracking().ToListAsync(); } public async Task DisposeAsync() @@ -45,7 +38,7 @@ public async Task DisposeAsync() protected override string DriverName { get; } = "MySQL"; - protected override bool CanRunLiveTests() => !string.IsNullOrEmpty(ConnectionStrings.MySql); + protected override bool CanRunLiveTests() => true; protected override async Task GetEntityAsync(string id) => await Context.Movies.AsNoTracking().SingleOrDefaultAsync(m => m.Id == id); diff --git a/tests/CommunityToolkit.Datasync.TestCommon/CommunityToolkit.Datasync.TestCommon.csproj b/tests/CommunityToolkit.Datasync.TestCommon/CommunityToolkit.Datasync.TestCommon.csproj index 6c0154d8..7695fb3b 100644 --- a/tests/CommunityToolkit.Datasync.TestCommon/CommunityToolkit.Datasync.TestCommon.csproj +++ b/tests/CommunityToolkit.Datasync.TestCommon/CommunityToolkit.Datasync.TestCommon.csproj @@ -1,4 +1,4 @@ - + false @@ -20,6 +20,7 @@ + diff --git a/tests/CommunityToolkit.Datasync.TestCommon/Databases/ConnectionStrings.cs b/tests/CommunityToolkit.Datasync.TestCommon/Databases/ConnectionStrings.cs index 47113a2f..dfca137f 100644 --- a/tests/CommunityToolkit.Datasync.TestCommon/Databases/ConnectionStrings.cs +++ b/tests/CommunityToolkit.Datasync.TestCommon/Databases/ConnectionStrings.cs @@ -11,9 +11,7 @@ public static class ConnectionStrings public static readonly string CosmosDb = Environment.GetEnvironmentVariable("COSMOS_CONNECTION_STRING"); public static readonly string MongoCommunity = Environment.GetEnvironmentVariable("MONGOACI_CONNECTION_STRING"); public static readonly string CosmosMongo = Environment.GetEnvironmentVariable("MONGO_CONNECTION_STRING"); - public static readonly string MySql = Environment.GetEnvironmentVariable("MYSQL_CONNECTION_STRING"); public static readonly string PgSql = Environment.GetEnvironmentVariable("PGSQL_CONNECTION_STRING"); - public static readonly string Service = Environment.GetEnvironmentVariable("SERVICE_ENDPOINT"); public static readonly bool EnableLogging = (Environment.GetEnvironmentVariable("ENABLE_SQL_LOGGING") ?? "false") == "true"; } \ No newline at end of file diff --git a/tests/CommunityToolkit.Datasync.TestCommon/Fixtures/MySqlDatabaseFixture.cs b/tests/CommunityToolkit.Datasync.TestCommon/Fixtures/MySqlDatabaseFixture.cs new file mode 100644 index 00000000..faaaf8c3 --- /dev/null +++ b/tests/CommunityToolkit.Datasync.TestCommon/Fixtures/MySqlDatabaseFixture.cs @@ -0,0 +1,49 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Testcontainers.MySql; +using Xunit; + +namespace CommunityToolkit.Datasync.TestCommon.Fixtures; + +/// +/// A test fixture for impementing a MySQL database using Testcontainers. +/// +[ExcludeFromCodeCoverage] +public class MySqlDatabaseFixture : IAsyncLifetime +{ + private readonly MySqlContainer _container; + + public MySqlDatabaseFixture() + { + this._container = new MySqlBuilder() + .WithImage("mysql:lts-oracle") + .WithCleanUp(true) + .WithUsername("testuser") + .WithPassword("testpassword") + .WithDatabase("testdb") + .Build(); + } + + /// + public async Task DisposeAsync() + { + if (this._container is not null) + { + await this._container.DisposeAsync(); + } + } + + /// + public async Task InitializeAsync() + { + await this._container.StartAsync(); + ConnectionString = this._container.GetConnectionString(); + } + + /// + /// The connection string for the MySQL database. + /// + public string ConnectionString { get; private set; } = string.Empty; +} diff --git a/tests/CommunityToolkit.Datasync.TestCommon/Fixtures/MySqlTestFixture.cs b/tests/CommunityToolkit.Datasync.TestCommon/Fixtures/MySqlTestFixture.cs new file mode 100644 index 00000000..e69de29b