diff --git a/Aspire.sln b/Aspire.sln index 283cf1d1d3f..273267fd38b 100644 --- a/Aspire.sln +++ b/Aspire.sln @@ -508,6 +508,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Hosting", "Hosting", "{830A EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Aspire.Hosting.Redis.Tests", "tests\Aspire.Hosting.Redis.Tests\Aspire.Hosting.Redis.Tests.csproj", "{1BC02557-B78B-48CE-9D3C-488A6B7672F4}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Aspire.Hosting.PostgreSQL.Tests", "tests\Aspire.Hosting.PostgreSQL.Tests\Aspire.Hosting.PostgreSQL.Tests.csproj", "{7425E5B2-BC47-4521-AC40-B8CECA329E08}" +EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Aspire.Hosting.Qdrant.Tests", "tests\Aspire.Hosting.Qdrant.Tests\Aspire.Hosting.Qdrant.Tests.csproj", "{8E2AA85E-C351-47B4-AF91-58557FAD5840}" EndProject Global @@ -1332,6 +1334,10 @@ Global {8E2AA85E-C351-47B4-AF91-58557FAD5840}.Debug|Any CPU.Build.0 = Debug|Any CPU {8E2AA85E-C351-47B4-AF91-58557FAD5840}.Release|Any CPU.ActiveCfg = Release|Any CPU {8E2AA85E-C351-47B4-AF91-58557FAD5840}.Release|Any CPU.Build.0 = Release|Any CPU + {7425E5B2-BC47-4521-AC40-B8CECA329E08}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7425E5B2-BC47-4521-AC40-B8CECA329E08}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7425E5B2-BC47-4521-AC40-B8CECA329E08}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7425E5B2-BC47-4521-AC40-B8CECA329E08}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -1576,6 +1582,7 @@ Global {830A89EC-4029-4753-B25A-068BAE37DEC7} = {4981B3A5-4AFD-4191-BF7D-8692D9783D60} {1BC02557-B78B-48CE-9D3C-488A6B7672F4} = {830A89EC-4029-4753-B25A-068BAE37DEC7} {8E2AA85E-C351-47B4-AF91-58557FAD5840} = {830A89EC-4029-4753-B25A-068BAE37DEC7} + {7425E5B2-BC47-4521-AC40-B8CECA329E08} = {830A89EC-4029-4753-B25A-068BAE37DEC7} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {6DCEDFEC-988E-4CB3-B45B-191EB5086E0C} diff --git a/src/Aspire.Hosting.PostgreSQL/Aspire.Hosting.PostgreSQL.csproj b/src/Aspire.Hosting.PostgreSQL/Aspire.Hosting.PostgreSQL.csproj index d7bc2689df3..1bbdf16ace8 100644 --- a/src/Aspire.Hosting.PostgreSQL/Aspire.Hosting.PostgreSQL.csproj +++ b/src/Aspire.Hosting.PostgreSQL/Aspire.Hosting.PostgreSQL.csproj @@ -22,7 +22,7 @@ - + diff --git a/src/Shared/SecretsStore.cs b/src/Shared/SecretsStore.cs index 424d460e05b..b230234880b 100644 --- a/src/Shared/SecretsStore.cs +++ b/src/Shared/SecretsStore.cs @@ -12,7 +12,7 @@ namespace Microsoft.Extensions.SecretManager.Tools.Internal; /// /// Adapted from dotnet user-secrets at https://github.com/dotnet/aspnetcore/blob/482730a4c773ee4b3ae9525186d10999c89b556d/src/Tools/dotnet-user-secrets/src/Internal/SecretsStore.cs /// -internal class SecretsStore +internal sealed class SecretsStore { private readonly string _secretsFilePath; private readonly Dictionary _secrets; diff --git a/tests/Aspire.Hosting.Tests/Postgres/AddPostgresTests.cs b/tests/Aspire.Hosting.PostgreSQL.Tests/AddPostgresTests.cs similarity index 98% rename from tests/Aspire.Hosting.Tests/Postgres/AddPostgresTests.cs rename to tests/Aspire.Hosting.PostgreSQL.Tests/AddPostgresTests.cs index e8a4df30d5a..9a1ad976051 100644 --- a/tests/Aspire.Hosting.Tests/Postgres/AddPostgresTests.cs +++ b/tests/Aspire.Hosting.PostgreSQL.Tests/AddPostgresTests.cs @@ -3,13 +3,14 @@ using System.Net.Sockets; using System.Text.Json; +using Aspire.Hosting.ApplicationModel; using Aspire.Hosting.Postgres; using Aspire.Hosting.Tests.Utils; using Aspire.Hosting.Utils; using Microsoft.Extensions.DependencyInjection; using Xunit; -namespace Aspire.Hosting.Tests.Postgres; +namespace Aspire.Hosting.PostgreSQL.Tests; public class AddPostgresTests { @@ -20,7 +21,7 @@ public void AddPostgresAddsGeneratedPasswordParameterWithUserSecretsParameterDef var pg = appBuilder.AddPostgres("pg"); - Assert.IsType(pg.Resource.PasswordParameter.Default); + Assert.Equal(nameof(UserSecretsParameterDefault), pg.Resource.PasswordParameter.Default?.GetType().Name); } [Fact] @@ -30,7 +31,7 @@ public void AddPostgresDoesNotAddGeneratedPasswordParameterWithUserSecretsParame var pg = appBuilder.AddPostgres("pg"); - Assert.IsNotType(pg.Resource.PasswordParameter.Default); + Assert.Equal(nameof(GenerateParameterDefault), pg.Resource.PasswordParameter.Default?.GetType().Name); } [Fact] diff --git a/tests/Aspire.Hosting.PostgreSQL.Tests/Aspire.Hosting.PostgreSQL.Tests.csproj b/tests/Aspire.Hosting.PostgreSQL.Tests/Aspire.Hosting.PostgreSQL.Tests.csproj new file mode 100644 index 00000000000..336f47c5175 --- /dev/null +++ b/tests/Aspire.Hosting.PostgreSQL.Tests/Aspire.Hosting.PostgreSQL.Tests.csproj @@ -0,0 +1,18 @@ + + + + $(NetCurrent) + + + + + + + + + + + + + + diff --git a/tests/Aspire.Hosting.PostgreSQL.Tests/PostgresFunctionalTests.cs b/tests/Aspire.Hosting.PostgreSQL.Tests/PostgresFunctionalTests.cs new file mode 100644 index 00000000000..e0aecb639e5 --- /dev/null +++ b/tests/Aspire.Hosting.PostgreSQL.Tests/PostgresFunctionalTests.cs @@ -0,0 +1,238 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Aspire.Components.Common.Tests; +using Aspire.Hosting.Utils; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Npgsql; +using Polly; +using Xunit; +using Xunit.Abstractions; + +namespace Aspire.Hosting.PostgreSQL.Tests; + +public class PostgresFunctionalTests(ITestOutputHelper testOutputHelper) +{ + [Fact] + [RequiresDocker] + public async Task VerifyPostgresResource() + { + var builder = CreateDistributedApplicationBuilder(); + + var postgresDbName = "db1"; + + var postgres = builder.AddPostgres("pg").WithEnvironment("POSTGRES_DB", postgresDbName); + var db = postgres.AddDatabase(postgresDbName); + + using var app = builder.Build(); + + await app.StartAsync(); + + var hb = Host.CreateApplicationBuilder(); + + hb.Configuration.AddInMemoryCollection(new Dictionary + { + [$"ConnectionStrings:{db.Resource.Name}"] = await db.Resource.ConnectionStringExpression.GetValueAsync(default) + }); + + hb.AddNpgsqlDataSource(db.Resource.Name); + + using var host = hb.Build(); + + await host.StartAsync(); + + var pipeline = new ResiliencePipelineBuilder() + .AddRetry(new() { MaxRetryAttempts = 10, Delay = TimeSpan.FromSeconds(1), ShouldHandle = new PredicateBuilder().Handle() }) + .AddTimeout(TimeSpan.FromSeconds(5)) + .Build(); + + await pipeline.ExecuteAsync( + async token => + { + using var connection = host.Services.GetRequiredService(); + await connection.OpenAsync(token); + + var command = connection.CreateCommand(); + command.CommandText = $"SELECT 1"; + var results = await command.ExecuteReaderAsync(token); + + Assert.True(results.HasRows); + }); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + [RequiresDocker] + public async Task WithDataShouldPersistStateBetweenUsages(bool useVolume) + { + var postgresDbName = "tempdb"; + + string? volumeName = null; + string? bindMountPath = null; + + var pipeline = new ResiliencePipelineBuilder() + .AddRetry(new() { MaxRetryAttempts = 10, Delay = TimeSpan.FromSeconds(1), ShouldHandle = new PredicateBuilder().Handle() }) + .AddTimeout(TimeSpan.FromSeconds(5)) + .Build(); + + try + { + var builder1 = CreateDistributedApplicationBuilder(); + + var username = "postgres"; + var password = "p@ssw0rd1"; + + var usernameParameter = builder1.AddParameter("user"); + var passwordParameter = builder1.AddParameter("pwd"); + builder1.Configuration["Parameters:user"] = username; + builder1.Configuration["Parameters:pwd"] = password; + var postgres1 = builder1.AddPostgres("pg", usernameParameter, passwordParameter).WithEnvironment("POSTGRES_DB", postgresDbName); + + var db1 = postgres1.AddDatabase(postgresDbName); + + if (useVolume) + { + // Use a deterministic volume name to prevent them from exhausting the machines if deletion fails + volumeName = VolumeNameGenerator.CreateVolumeName(postgres1, nameof(WithDataShouldPersistStateBetweenUsages)); + + // If the volume already exists (because of a crashing previous run), try to delete it + DockerUtils.AttemptDeleteDockerVolume(volumeName); + postgres1.WithDataVolume(volumeName); + } + else + { + bindMountPath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + postgres1.WithDataBindMount(bindMountPath); + } + + using (var app = builder1.Build()) + { + await app.StartAsync(); + + try + { + var hb = Host.CreateApplicationBuilder(); + + hb.Configuration.AddInMemoryCollection(new Dictionary + { + [$"ConnectionStrings:{db1.Resource.Name}"] = await db1.Resource.ConnectionStringExpression.GetValueAsync(default) + }); + + hb.AddNpgsqlDataSource(db1.Resource.Name); + + using (var host = hb.Build()) + { + await host.StartAsync(); + + await pipeline.ExecuteAsync( + async token => + { + using var connection = host.Services.GetRequiredService(); + await connection.OpenAsync(token); + + var command = connection.CreateCommand(); + command.CommandText = $"CREATE TABLE cars (brand VARCHAR(255)); INSERT INTO cars (brand) VALUES ('BatMobile'); SELECT * FROM cars;"; + var results = await command.ExecuteReaderAsync(token); + + Assert.True(results.HasRows); + }); + } + } + finally + { + // Stops the container, or the Volume/mount would still be in use + await app.StopAsync(); + } + } + + var builder2 = CreateDistributedApplicationBuilder(); + usernameParameter = builder2.AddParameter("user"); + passwordParameter = builder2.AddParameter("pwd"); + builder2.Configuration["Parameters:user"] = username; + builder2.Configuration["Parameters:pwd"] = password; + + var postgres2 = builder2.AddPostgres("pg", usernameParameter, passwordParameter); + var db2 = postgres2.AddDatabase(postgresDbName); + + if (useVolume) + { + postgres2.WithDataVolume(volumeName); + } + else + { + postgres2.WithDataBindMount(bindMountPath!); + } + + using (var app = builder2.Build()) + { + await app.StartAsync(); + try + { + var hb = Host.CreateApplicationBuilder(); + + hb.Configuration.AddInMemoryCollection(new Dictionary + { + [$"ConnectionStrings:{db2.Resource.Name}"] = await db2.Resource.ConnectionStringExpression.GetValueAsync(default) + }); + + hb.AddNpgsqlDataSource(db2.Resource.Name); + + using (var host = hb.Build()) + { + await host.StartAsync(); + + await pipeline.ExecuteAsync( + async token => + { + using var connection = host.Services.GetRequiredService(); + await connection.OpenAsync(token); + + var command = connection.CreateCommand(); + command.CommandText = $"SELECT * FROM cars;"; + var results = await command.ExecuteReaderAsync(token); + + Assert.True(results.HasRows); + }); + } + + } + finally + { + // Stops the container, or the Volume/mount would still be in use + await app.StopAsync(); + } + } + + } + finally + { + if (volumeName is not null) + { + DockerUtils.AttemptDeleteDockerVolume(volumeName); + } + + if (bindMountPath is not null) + { + try + { + File.Delete(bindMountPath); + } + catch + { + // Don't fail test if we can't clean the temporary folder + } + } + } + } + + private TestDistributedApplicationBuilder CreateDistributedApplicationBuilder() + { + var builder = TestDistributedApplicationBuilder.CreateWithTestContainerRegistry(); + builder.Services.AddXunitLogging(testOutputHelper); + return builder; + } +} diff --git a/tests/Aspire.Hosting.Tests/Aspire.Hosting.Tests.csproj b/tests/Aspire.Hosting.Tests/Aspire.Hosting.Tests.csproj index ee9a9e14332..7c1c1453249 100644 --- a/tests/Aspire.Hosting.Tests/Aspire.Hosting.Tests.csproj +++ b/tests/Aspire.Hosting.Tests/Aspire.Hosting.Tests.csproj @@ -30,11 +30,10 @@ - + - @@ -61,6 +60,7 @@ +