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 @@
+