diff --git a/Aspire.sln b/Aspire.sln index 1daf4ea13d..b7547f986f 100644 --- a/Aspire.sln +++ b/Aspire.sln @@ -154,6 +154,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Aspire.RabbitMQ.Client.Test EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Aspire.MySqlConnector", "src\Components\Aspire.MySqlConnector\Aspire.MySqlConnector.csproj", "{CA283D7F-EB95-4353-B196-C409965D2B42}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Aspire.MySqlConnector.Tests", "tests\Aspire.MySqlConnector.Tests\Aspire.MySqlConnector.Tests.csproj", "{C8079F06-304F-49B1-A0C1-45AA3782A923}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -412,6 +414,10 @@ Global {CA283D7F-EB95-4353-B196-C409965D2B42}.Debug|Any CPU.Build.0 = Debug|Any CPU {CA283D7F-EB95-4353-B196-C409965D2B42}.Release|Any CPU.ActiveCfg = Release|Any CPU {CA283D7F-EB95-4353-B196-C409965D2B42}.Release|Any CPU.Build.0 = Release|Any CPU + {C8079F06-304F-49B1-A0C1-45AA3782A923}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C8079F06-304F-49B1-A0C1-45AA3782A923}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C8079F06-304F-49B1-A0C1-45AA3782A923}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C8079F06-304F-49B1-A0C1-45AA3782A923}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -484,6 +490,7 @@ Global {4D8A92AB-4E77-4965-AD8E-8E206DCE66A4} = {27381127-6C45-4B4C-8F18-41FF48DFE4B2} {165411FE-755E-4869-A756-F87F455860AC} = {4981B3A5-4AFD-4191-BF7D-8692D9783D60} {CA283D7F-EB95-4353-B196-C409965D2B42} = {27381127-6C45-4B4C-8F18-41FF48DFE4B2} + {C8079F06-304F-49B1-A0C1-45AA3782A923} = {4981B3A5-4AFD-4191-BF7D-8692D9783D60} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {6DCEDFEC-988E-4CB3-B45B-191EB5086E0C} diff --git a/src/Components/Aspire.MySqlConnector/README.md b/src/Components/Aspire.MySqlConnector/README.md index 217ccbb1a3..7dbc7db221 100644 --- a/src/Components/Aspire.MySqlConnector/README.md +++ b/src/Components/Aspire.MySqlConnector/README.md @@ -52,7 +52,7 @@ And then the connection string will be retrieved from the `ConnectionStrings` co ```json { "ConnectionStrings": { - "myConnection": "Host=mysql;Database=test" + "myConnection": "Server=mysql;Database=test" } } ``` diff --git a/tests/Aspire.MySqlConnector.Tests/Aspire.MySqlConnector.Tests.csproj b/tests/Aspire.MySqlConnector.Tests/Aspire.MySqlConnector.Tests.csproj new file mode 100644 index 0000000000..ab93f67996 --- /dev/null +++ b/tests/Aspire.MySqlConnector.Tests/Aspire.MySqlConnector.Tests.csproj @@ -0,0 +1,12 @@ + + + + $(NetCurrent) + + + + + + + + diff --git a/tests/Aspire.MySqlConnector.Tests/AspireMySqlConnectorExtensionsTests.cs b/tests/Aspire.MySqlConnector.Tests/AspireMySqlConnectorExtensionsTests.cs new file mode 100644 index 0000000000..148da3e1c0 --- /dev/null +++ b/tests/Aspire.MySqlConnector.Tests/AspireMySqlConnectorExtensionsTests.cs @@ -0,0 +1,104 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using MySqlConnector; +using Xunit; + +namespace Aspire.MySqlConnector.Tests; + +public class AspireMySqlConnectorExtensionsTests +{ + private const string ConnectionString = "Server=localhost;Database=test_aspire_mysql"; + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void ReadsFromConnectionStringsCorrectly(bool useKeyed) + { + var builder = Host.CreateEmptyApplicationBuilder(null); + builder.Configuration.AddInMemoryCollection([ + new KeyValuePair("ConnectionStrings:mysql", ConnectionString) + ]); + + if (useKeyed) + { + builder.AddKeyedMySqlDataSource("mysql"); + } + else + { + builder.AddMySqlDataSource("mysql"); + } + + var host = builder.Build(); + var dataSource = useKeyed ? + host.Services.GetRequiredKeyedService("mysql") : + host.Services.GetRequiredService(); + + Assert.Equal(ConnectionString, dataSource.ConnectionString); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void ConnectionStringCanBeSetInCode(bool useKeyed) + { + var builder = Host.CreateEmptyApplicationBuilder(null); + builder.Configuration.AddInMemoryCollection([ + new KeyValuePair("ConnectionStrings:mysql", "unused") + ]); + + static void SetConnectionString(MySqlConnectorSettings settings) => settings.ConnectionString = ConnectionString; + if (useKeyed) + { + builder.AddKeyedMySqlDataSource("mysql", SetConnectionString); + } + else + { + builder.AddMySqlDataSource("mysql", SetConnectionString); + } + + var host = builder.Build(); + var dataSource = useKeyed ? + host.Services.GetRequiredKeyedService("mysql") : + host.Services.GetRequiredService(); + + Assert.Equal(ConnectionString, dataSource.ConnectionString); + // the connection string from config should not be used since code set it explicitly + Assert.DoesNotContain("unused", dataSource.ConnectionString); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void ConnectionNameWinsOverConfigSection(bool useKeyed) + { + var builder = Host.CreateEmptyApplicationBuilder(null); + + var key = useKeyed ? "mysql" : null; + builder.Configuration.AddInMemoryCollection([ + new KeyValuePair(ConformanceTests.CreateConfigKey("Aspire:MySqlConnector", key, "ConnectionString"), "unused"), + new KeyValuePair("ConnectionStrings:mysql", ConnectionString) + ]); + + if (useKeyed) + { + builder.AddKeyedMySqlDataSource("mysql"); + } + else + { + builder.AddMySqlDataSource("mysql"); + } + + var host = builder.Build(); + var dataSource = useKeyed ? + host.Services.GetRequiredKeyedService("mysql") : + host.Services.GetRequiredService(); + + Assert.Equal(ConnectionString, dataSource.ConnectionString); + // the connection string from config should not be used since it was found in ConnectionStrings + Assert.DoesNotContain("unused", dataSource.ConnectionString); + } +} diff --git a/tests/Aspire.MySqlConnector.Tests/ConfigurationTests.cs b/tests/Aspire.MySqlConnector.Tests/ConfigurationTests.cs new file mode 100644 index 0000000000..62bb546041 --- /dev/null +++ b/tests/Aspire.MySqlConnector.Tests/ConfigurationTests.cs @@ -0,0 +1,25 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Xunit; + +namespace Aspire.MySqlConnector.Tests; + +public class ConfigurationTests +{ + [Fact] + public void ConnectionStringIsNullByDefault() + => Assert.Null(new MySqlConnectorSettings().ConnectionString); + + [Fact] + public void HealthCheckIsEnabledByDefault() + => Assert.True(new MySqlConnectorSettings().HealthChecks); + + [Fact] + public void TracingIsEnabledByDefault() + => Assert.True(new MySqlConnectorSettings().Tracing); + + [Fact] + public void MetricsAreEnabledByDefault() + => Assert.True(new MySqlConnectorSettings().Metrics); +} diff --git a/tests/Aspire.MySqlConnector.Tests/ConformanceTests.cs b/tests/Aspire.MySqlConnector.Tests/ConformanceTests.cs new file mode 100644 index 0000000000..380c924f49 --- /dev/null +++ b/tests/Aspire.MySqlConnector.Tests/ConformanceTests.cs @@ -0,0 +1,162 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Data.Common; +using Aspire.Components.ConformanceTests; +using Microsoft.DotNet.RemoteExecutor; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using MySqlConnector; +using Xunit; + +namespace Aspire.MySqlConnector.Tests; + +public class ConformanceTests : ConformanceTests +{ + private const string ConnectionSting = "Host=localhost;Database=test_aspire_mysql;Username=root;Password=password"; + + private static readonly Lazy s_canConnectToServer = new(GetCanConnect); + + protected override ServiceLifetime ServiceLifetime => ServiceLifetime.Singleton; + + // https://github.com/mysql-net/MySqlConnector/blob/d895afc013a5849d33a123a7061442e2cbb9ce76/src/MySqlConnector/Utilities/ActivitySourceHelper.cs#L61 + protected override string ActivitySourceName => "MySqlConnector"; + + protected override string[] RequiredLogCategories => [ + "MySqlConnector.ConnectionPool", + "MySqlConnector.MySqlBulkCopy", + "MySqlConnector.MySqlCommand", + "MySqlConnector.MySqlConnection", + "MySqlConnector.MySqlDataSource", + ]; + + protected override bool SupportsKeyedRegistrations => true; + + protected override bool CanConnectToServer => s_canConnectToServer.Value; + + protected override string JsonSchemaPath => "src/Components/Aspire.MySqlConnector/ConfigurationSchema.json"; + + protected override string ValidJsonConfig => """ + { + "Aspire": { + "MySqlConnector": { + "Npgsql": { + "ConnectionString": "YOUR_CONNECTION_STRING", + "HealthChecks": false, + "Tracing": true, + "Metrics": true + } + } + } + } + """; + + protected override (string json, string error)[] InvalidJsonToErrorMessage => new[] + { + ("""{"Aspire": { "MySqlConnector":{ "Metrics": 0}}}""", "Value is \"integer\" but should be \"boolean\""), + ("""{"Aspire": { "MySqlConnector":{ "ConnectionString": "Con", "HealthChecks": "false"}}}""", "Value is \"string\" but should be \"boolean\"") + }; + + protected override void PopulateConfiguration(ConfigurationManager configuration, string? key = null) + => configuration.AddInMemoryCollection(new KeyValuePair[1] + { + new KeyValuePair(CreateConfigKey("Aspire:MySqlConnector", key, "ConnectionString"), ConnectionSting) + }); + + protected override void RegisterComponent(HostApplicationBuilder builder, Action? configure = null, string? key = null) + { + if (key is null) + { + builder.AddMySqlDataSource("mysql", configure); + } + else + { + builder.AddKeyedMySqlDataSource(key, configure); + } + } + + protected override void SetHealthCheck(MySqlConnectorSettings options, bool enabled) + => options.HealthChecks = enabled; + + protected override void SetTracing(MySqlConnectorSettings options, bool enabled) + => options.Tracing = enabled; + + protected override void SetMetrics(MySqlConnectorSettings options, bool enabled) + => options.Metrics = enabled; + + protected override void TriggerActivity(MySqlDataSource service) + { + using MySqlConnection connection = service.CreateConnection(); + connection.Open(); + using MySqlCommand command = connection.CreateCommand(); + command.CommandText = "Select 1;"; + command.ExecuteScalar(); + } + + [Theory] + [InlineData(null)] + [InlineData("key")] + public void BothDataSourceAndConnectionCanBeResolved(string? key) + { + using IHost host = CreateHostWithComponent(key: key); + + MySqlDataSource? mySqlDataSource = Resolve(); + DbDataSource? dbDataSource = Resolve(); + MySqlConnection? mySqlConnection = Resolve(); + DbConnection? dbConnection = Resolve(); + + Assert.NotNull(mySqlDataSource); + Assert.Same(mySqlDataSource, dbDataSource); + + Assert.NotNull(mySqlConnection); + Assert.NotNull(dbConnection); + + Assert.Equal(dbConnection.ConnectionString, mySqlConnection.ConnectionString); + Assert.Equal(mySqlDataSource.ConnectionString, mySqlConnection.ConnectionString); + + T? Resolve() => key is null ? host.Services.GetService() : host.Services.GetKeyedService(key); + } + + [ConditionalFact] + public void TracingEnablesTheRightActivitySource() + { + SkipIfCanNotConnectToServer(); + + RemoteExecutor.Invoke(() => ActivitySourceTest(key: null)).Dispose(); + } + + [ConditionalFact] + public void TracingEnablesTheRightActivitySource_Keyed() + { + SkipIfCanNotConnectToServer(); + + RemoteExecutor.Invoke(() => ActivitySourceTest(key: "key")).Dispose(); + } + + private static bool GetCanConnect() + { + using MySqlConnection connection = new(ConnectionSting); + + try + { + // clear the database from the connection string so we can create it + var builder = new MySqlConnectionStringBuilder(connection.ConnectionString); + string dbName = connection.Database; + builder.Database = null; + + using var noDatabaseConnection = new MySqlConnection(builder.ConnectionString); + + noDatabaseConnection.Open(); + + using var cmd = new MySqlCommand($"CREATE DATABASE IF NOT EXISTS `{dbName}`", noDatabaseConnection); + cmd.ExecuteNonQuery(); + } + catch (Exception) + { + return false; + } + + return true; + } +}