Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions Aspire.sln
Original file line number Diff line number Diff line change
Expand Up @@ -551,6 +551,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Aspire.Hosting.Keycloak.Tes
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Aspire.Keycloak.Authentication.Tests", "tests\Aspire.Keycloak.Authentication.Tests\Aspire.Keycloak.Authentication.Tests.csproj", "{48FF09E9-7D33-4A3F-9FF2-4C43A219C7B7}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aspire.Hosting.Nats.Tests", "tests\Aspire.Hosting.Nats.Tests\Aspire.Hosting.Nats.Tests.csproj", "{F492357C-682E-4CBB-A374-1A124B3976A3}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -1445,6 +1447,10 @@ Global
{48FF09E9-7D33-4A3F-9FF2-4C43A219C7B7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{48FF09E9-7D33-4A3F-9FF2-4C43A219C7B7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{48FF09E9-7D33-4A3F-9FF2-4C43A219C7B7}.Release|Any CPU.Build.0 = Release|Any CPU
{F492357C-682E-4CBB-A374-1A124B3976A3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F492357C-682E-4CBB-A374-1A124B3976A3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F492357C-682E-4CBB-A374-1A124B3976A3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F492357C-682E-4CBB-A374-1A124B3976A3}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -1709,6 +1715,7 @@ Global
{C556D61C-7E11-43EC-9098-C8D170FEA905} = {EBC55A17-B0D6-4E0A-9DC2-7D264E96F631}
{5867BAF2-FEF0-4661-BFDE-9ADCDC2921CD} = {830A89EC-4029-4753-B25A-068BAE37DEC7}
{48FF09E9-7D33-4A3F-9FF2-4C43A219C7B7} = {C424395C-1235-41A4-BF55-07880A04368C}
{F492357C-682E-4CBB-A374-1A124B3976A3} = {830A89EC-4029-4753-B25A-068BAE37DEC7}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {6DCEDFEC-988E-4CB3-B45B-191EB5086E0C}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Aspire.Hosting.Nats;
using Aspire.Hosting.Utils;
using System.Net.Sockets;
using Microsoft.Extensions.DependencyInjection;
using Xunit;
using Aspire.Hosting.ApplicationModel;

namespace Aspire.Hosting.Tests.Nats;
namespace Aspire.Hosting.Nats.Tests;

public class AddNatsTests
{
Expand Down
14 changes: 14 additions & 0 deletions tests/Aspire.Hosting.Nats.Tests/AppEvent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Text.Json.Serialization;

namespace Aspire.Hosting.Nats.Tests;

public record AppEvent(string Subject, string Name, string Description, decimal Priority);

[JsonSerializable(typeof(AppEvent))]
[JsonSourceGenerationOptions(PropertyNameCaseInsensitive = true)]
public partial class AppJsonContext : JsonSerializerContext
{
}
19 changes: 19 additions & 0 deletions tests/Aspire.Hosting.Nats.Tests/Aspire.Hosting.Nats.Tests.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>$(NetCurrent)</TargetFramework>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\Aspire.Hosting.AppHost\Aspire.Hosting.AppHost.csproj" />
<ProjectReference Include="..\..\src\Components\Aspire.NATS.Net\Aspire.NATS.Net.csproj" />
<ProjectReference Include="..\Aspire.Hosting.Tests\Aspire.Hosting.Tests.csproj" />
<ProjectReference Include="..\..\src\Aspire.Hosting.Nats\Aspire.Hosting.Nats.csproj" />
</ItemGroup>

<ItemGroup>
<Compile Include="$(RepoRoot)src\Aspire.Hosting.Nats\NatsContainerImageTags.cs" />
<Compile Include="$(SharedDir)VolumeNameGenerator.cs" Link="Utils\VolumeNameGenerator.cs" />
</ItemGroup>

</Project>
244 changes: 244 additions & 0 deletions tests/Aspire.Hosting.Nats.Tests/NatsFunctionalTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,244 @@
// 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 Xunit;
using Xunit.Abstractions;
using Microsoft.Extensions.Logging;
using Polly;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using NATS.Client.Core;
using NATS.Client.JetStream;
using NATS.Client.JetStream.Models;
namespace Aspire.Hosting.Nats.Tests;

public class NatsFunctionalTests(ITestOutputHelper testOutputHelper)
{
[Fact]
[RequiresDocker]
public async Task VerifyNatsResource()
{
var cts = new CancellationTokenSource(TimeSpan.FromMinutes(5));
var pipeline = new ResiliencePipelineBuilder()
.AddRetry(new() { MaxRetryAttempts = 10, Delay = TimeSpan.FromSeconds(2) })
.Build();

var builder = CreateDistributedApplicationBuilder();

var nats = builder.AddNats("nats");

using var app = builder.Build();

await app.StartAsync();

var hb = Host.CreateApplicationBuilder();

hb.Configuration.AddInMemoryCollection(new Dictionary<string, string?>
{
[$"ConnectionStrings:{nats.Resource.Name}"] = await nats.Resource.ConnectionStringExpression.GetValueAsync(default)
});

hb.AddNatsClient(nats.Resource.Name);

using var host = hb.Build();

await host.StartAsync();

await pipeline.ExecuteAsync(async token =>
{
var natsConnection = host.Services.GetRequiredService<INatsConnection>();

var rtt = await natsConnection.PingAsync(token);
}, cts.Token);
}

[Theory]
[InlineData(true)]
[InlineData(false)]
[RequiresDocker]
public async Task WithDataShouldPersistStateBetweenUsages(bool useVolume)
{
var cts = new CancellationTokenSource(TimeSpan.FromMinutes(3));
var streamName = "test-stream";
var subjectName = "test-subject";
string? volumeName = null;
string? bindMountPath = null;

try
{
var builder1 = CreateDistributedApplicationBuilder();
var nats1 = builder1.AddNats("nats")
.WithJetStream();

if (useVolume)
{
// Use a deterministic volume name to prevent them from exhausting the machines if deletion fails
volumeName = VolumeNameGenerator.CreateVolumeName(nats1, nameof(WithDataShouldPersistStateBetweenUsages));

// if the volume already exists (because of a crashing previous run), try to delete it
DockerUtils.AttemptDeleteDockerVolume(volumeName);
nats1.WithDataVolume(volumeName);
}
else
{
bindMountPath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
nats1.WithDataBindMount(bindMountPath);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just curious - WithDataBindMount is mounting to /var/lib/nats in the container. How is this test hitting that?

}

using (var app = builder1.Build())
{
await app.StartAsync();
try
{
var hb = Host.CreateApplicationBuilder();

hb.Configuration.AddInMemoryCollection(new Dictionary<string, string?>
{
[$"ConnectionStrings:{nats1.Resource.Name}"] = await nats1.Resource.ConnectionStringExpression.GetValueAsync(default)
});

hb.AddNatsClient("nats", configureOptions: opts =>
{
var jsonRegistry = new NatsJsonContextSerializerRegistry(AppJsonContext.Default);
return opts with { SerializerRegistry = jsonRegistry };
});

hb.AddNatsJetStream();

using (var host = hb.Build())
{
await host.StartAsync();

var pipeline = new ResiliencePipelineBuilder()
.AddRetry(new() { MaxRetryAttempts = 10, Delay = TimeSpan.FromSeconds(1), ShouldHandle = new PredicateBuilder().Handle<NatsException>() })
.Build();

await pipeline.ExecuteAsync(async token =>
{
var jetStream = host.Services.GetRequiredService<INatsJSContext>();

var stream = await jetStream.CreateStreamAsync(new StreamConfig(streamName, [subjectName]), cancellationToken: token);
var name = stream.Info.Config.Name;
Assert.Equal(streamName, stream.Info.Config.Name);

for (var i = 0; i < 10; i++)
{
var appEvent = new AppEvent(subjectName, $"test-event-{i}", $"test-event-description-{i}", i);
var ack = await jetStream.PublishAsync(appEvent.Subject, appEvent, cancellationToken: token);
ack.EnsureSuccess();
}

}, cts.Token);
}
}
finally
{
// Stops the container, or the Volume/mount would still be in use
await app.StopAsync();
}
}

var builder2 = CreateDistributedApplicationBuilder();
var nats2 = builder2.AddNats("nats")
.WithJetStream();

if (useVolume)
{
nats2.WithDataVolume(volumeName);
}
else
{
nats2.WithDataBindMount(bindMountPath!);
}

using (var app = builder2.Build())
{
await app.StartAsync();
try
{
var hb = Host.CreateApplicationBuilder();

hb.Configuration.AddInMemoryCollection(new Dictionary<string, string?>
{
[$"ConnectionStrings:{nats2.Resource.Name}"] = await nats2.Resource.ConnectionStringExpression.GetValueAsync(default)
});

hb.AddNatsClient("nats", configureOptions: opts =>
{
var jsonRegistry = new NatsJsonContextSerializerRegistry(AppJsonContext.Default);
return opts with { SerializerRegistry = jsonRegistry };
});

hb.AddNatsJetStream();

using (var host = hb.Build())
{
await host.StartAsync();

var pipeline = new ResiliencePipelineBuilder()
.AddRetry(new() { MaxRetryAttempts = 10, Delay = TimeSpan.FromSeconds(1), ShouldHandle = new PredicateBuilder().Handle<NatsException>() })
.Build();
await pipeline.ExecuteAsync(async token =>
{
var jetStream = host.Services.GetRequiredService<INatsJSContext>();

var stream = await jetStream.GetStreamAsync(streamName, cancellationToken: token);
var consumer = await stream.CreateOrderedConsumerAsync(cancellationToken: token);

var events = new List<AppEvent>();
await foreach (var msg in consumer.ConsumeAsync<AppEvent>(cancellationToken: token))
{
events.Add(msg.Data!);

if (msg.Metadata?.NumPending == 0)
{
break;
}
}

foreach (var item in events.Select((appEvent, i) => new { i, appEvent }))
{
Assert.Equal($"test-event-{item.i}", item.appEvent.Name);
Assert.Equal($"test-event-description-{item.i}", item.appEvent.Description);
}
});
}
}
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
{
Directory.Delete(bindMountPath, recursive: true);
}
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;
}
}
2 changes: 0 additions & 2 deletions tests/Aspire.Hosting.Tests/Aspire.Hosting.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@
<ProjectReference Include="..\..\src\Aspire.Hosting.Garnet\Aspire.Hosting.Garnet.csproj" IsAspireProjectResource="false" />
<ProjectReference Include="..\..\src\Aspire.Hosting.Milvus\Aspire.Hosting.Milvus.csproj" IsAspireProjectResource="false" />
<ProjectReference Include="..\..\src\Aspire.Hosting.MongoDB\Aspire.Hosting.MongoDB.csproj" IsAspireProjectResource="false" />
<ProjectReference Include="..\..\src\Aspire.Hosting.Nats\Aspire.Hosting.Nats.csproj" IsAspireProjectResource="false" />
<ProjectReference Include="..\..\src\Aspire.Hosting.Testing\Aspire.Hosting.Testing.csproj" IsAspireProjectResource="false" />
<ProjectReference Include="..\Aspire.Components.Common.Tests\Aspire.Components.Common.Tests.csproj" IsAspireProjectResource="false" />
<ProjectReference Include="..\testproject\TestProject.AppHost\TestProject.AppHost.csproj" IsAspireProjectResource="false" />
Expand All @@ -45,7 +44,6 @@

<Compile Include="$(TestsSharedDir)Logging\*.cs" LinkBase="shared/Logging" />
<Compile Include="$(RepoRoot)src\Aspire.Hosting.Garnet\GarnetContainerImageTags.cs" />
<Compile Include="$(RepoRoot)src\Aspire.Hosting.Nats\NatsContainerImageTags.cs" />
<Compile Include="$(RepoRoot)src\Aspire.Hosting.Milvus\MilvusContainerImageTags.cs" />
<Compile Include="$(RepoRoot)src\Aspire.Hosting.MongoDB\MongoDBContainerImageTags.cs" />
<Compile Include="$(RepoRoot)src\Aspire.Hosting.Oracle\OracleContainerImageTags.cs" />
Expand Down