Skip to content

Commit 11e90df

Browse files
Alirexaaradicaleerhardt
authored
Extract Aspire.Hosting.Nats.Tests project (#5002)
* Extract Aspire.Hosting.Nats.Tests project * Apply suggested changes * apply suggested changes --------- Co-authored-by: Ankit Jain <[email protected]> Co-authored-by: Eric Erhardt <[email protected]>
1 parent 25f623d commit 11e90df

File tree

6 files changed

+288
-4
lines changed

6 files changed

+288
-4
lines changed

Aspire.sln

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -550,6 +550,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Aspire.Hosting.Keycloak.Tes
550550
EndProject
551551
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}"
552552
EndProject
553+
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}"
553554
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Aspire.Hosting.Azure.Tests", "tests\Aspire.Hosting.Azure.Tests\Aspire.Hosting.Azure.Tests.csproj", "{8691F993-7B19-496E-B8E1-EF1199ACF2E1}"
554555
EndProject
555556
Global
@@ -1458,6 +1459,10 @@ Global
14581459
{48FF09E9-7D33-4A3F-9FF2-4C43A219C7B7}.Debug|Any CPU.Build.0 = Debug|Any CPU
14591460
{48FF09E9-7D33-4A3F-9FF2-4C43A219C7B7}.Release|Any CPU.ActiveCfg = Release|Any CPU
14601461
{48FF09E9-7D33-4A3F-9FF2-4C43A219C7B7}.Release|Any CPU.Build.0 = Release|Any CPU
1462+
{F492357C-682E-4CBB-A374-1A124B3976A3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
1463+
{F492357C-682E-4CBB-A374-1A124B3976A3}.Debug|Any CPU.Build.0 = Debug|Any CPU
1464+
{F492357C-682E-4CBB-A374-1A124B3976A3}.Release|Any CPU.ActiveCfg = Release|Any CPU
1465+
{F492357C-682E-4CBB-A374-1A124B3976A3}.Release|Any CPU.Build.0 = Release|Any CPU
14611466
{8691F993-7B19-496E-B8E1-EF1199ACF2E1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
14621467
{8691F993-7B19-496E-B8E1-EF1199ACF2E1}.Debug|Any CPU.Build.0 = Debug|Any CPU
14631468
{8691F993-7B19-496E-B8E1-EF1199ACF2E1}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -1731,6 +1736,7 @@ Global
17311736
{C556D61C-7E11-43EC-9098-C8D170FEA905} = {EBC55A17-B0D6-4E0A-9DC2-7D264E96F631}
17321737
{5867BAF2-FEF0-4661-BFDE-9ADCDC2921CD} = {830A89EC-4029-4753-B25A-068BAE37DEC7}
17331738
{48FF09E9-7D33-4A3F-9FF2-4C43A219C7B7} = {C424395C-1235-41A4-BF55-07880A04368C}
1739+
{F492357C-682E-4CBB-A374-1A124B3976A3} = {830A89EC-4029-4753-B25A-068BAE37DEC7}
17341740
{8691F993-7B19-496E-B8E1-EF1199ACF2E1} = {830A89EC-4029-4753-B25A-068BAE37DEC7}
17351741
EndGlobalSection
17361742
GlobalSection(ExtensibilityGlobals) = postSolution

tests/Aspire.Hosting.Tests/Nats/AddNatsTests.cs renamed to tests/Aspire.Hosting.Nats.Tests/AddNatsTests.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4-
using Aspire.Hosting.Nats;
54
using Aspire.Hosting.Utils;
65
using System.Net.Sockets;
76
using Microsoft.Extensions.DependencyInjection;
87
using Xunit;
8+
using Aspire.Hosting.ApplicationModel;
99

10-
namespace Aspire.Hosting.Tests.Nats;
10+
namespace Aspire.Hosting.Nats.Tests;
1111

1212
public class AddNatsTests
1313
{
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Text.Json.Serialization;
5+
6+
namespace Aspire.Hosting.Nats.Tests;
7+
8+
public record AppEvent(string Subject, string Name, string Description, decimal Priority);
9+
10+
[JsonSerializable(typeof(AppEvent))]
11+
[JsonSourceGenerationOptions(PropertyNameCaseInsensitive = true)]
12+
public partial class AppJsonContext : JsonSerializerContext
13+
{
14+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>$(NetCurrent)</TargetFramework>
5+
</PropertyGroup>
6+
7+
<ItemGroup>
8+
<ProjectReference Include="..\..\src\Aspire.Hosting.AppHost\Aspire.Hosting.AppHost.csproj" />
9+
<ProjectReference Include="..\..\src\Components\Aspire.NATS.Net\Aspire.NATS.Net.csproj" />
10+
<ProjectReference Include="..\Aspire.Hosting.Tests\Aspire.Hosting.Tests.csproj" />
11+
<ProjectReference Include="..\..\src\Aspire.Hosting.Nats\Aspire.Hosting.Nats.csproj" />
12+
</ItemGroup>
13+
14+
<ItemGroup>
15+
<Compile Include="$(RepoRoot)src\Aspire.Hosting.Nats\NatsContainerImageTags.cs" />
16+
<Compile Include="$(SharedDir)VolumeNameGenerator.cs" Link="Utils\VolumeNameGenerator.cs" />
17+
</ItemGroup>
18+
19+
</Project>
Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using Aspire.Components.Common.Tests;
5+
using Aspire.Hosting.Utils;
6+
using Xunit;
7+
using Xunit.Abstractions;
8+
using Microsoft.Extensions.Logging;
9+
using Polly;
10+
using Microsoft.Extensions.Hosting;
11+
using Microsoft.Extensions.DependencyInjection;
12+
using NATS.Client.Core;
13+
using NATS.Client.JetStream;
14+
using NATS.Client.JetStream.Models;
15+
namespace Aspire.Hosting.Nats.Tests;
16+
17+
public class NatsFunctionalTests(ITestOutputHelper testOutputHelper)
18+
{
19+
private const string StreamName = "test-stream";
20+
private const string SubjectName = "test-subject";
21+
22+
[Fact]
23+
[RequiresDocker]
24+
public async Task VerifyNatsResource()
25+
{
26+
var cts = new CancellationTokenSource(TimeSpan.FromMinutes(5));
27+
var pipeline = new ResiliencePipelineBuilder()
28+
.AddRetry(new() { MaxRetryAttempts = 10, Delay = TimeSpan.FromSeconds(1), ShouldHandle = new PredicateBuilder().Handle<NatsException>() })
29+
.Build();
30+
31+
var builder = CreateDistributedApplicationBuilder();
32+
33+
var nats = builder.AddNats("nats")
34+
.WithJetStream();
35+
36+
using var app = builder.Build();
37+
38+
await app.StartAsync();
39+
40+
var hb = Host.CreateApplicationBuilder();
41+
42+
hb.Configuration[$"ConnectionStrings:{nats.Resource.Name}"] = await nats.Resource.ConnectionStringExpression.GetValueAsync(default);
43+
44+
hb.AddNatsClient("nats", configureOptions: opts =>
45+
{
46+
var jsonRegistry = new NatsJsonContextSerializerRegistry(AppJsonContext.Default);
47+
return opts with { SerializerRegistry = jsonRegistry };
48+
});
49+
50+
hb.AddNatsJetStream();
51+
52+
using var host = hb.Build();
53+
54+
await host.StartAsync();
55+
56+
await pipeline.ExecuteAsync(async token =>
57+
{
58+
var jetStream = host.Services.GetRequiredService<INatsJSContext>();
59+
60+
await CreateTestData(jetStream, token);
61+
await ConsumeTestData(jetStream, token);
62+
}, cts.Token);
63+
}
64+
65+
[Theory]
66+
[InlineData(true)]
67+
[InlineData(false)]
68+
[RequiresDocker]
69+
public async Task WithDataShouldPersistStateBetweenUsages(bool useVolume)
70+
{
71+
var cts = new CancellationTokenSource(TimeSpan.FromMinutes(3));
72+
var pipeline = new ResiliencePipelineBuilder()
73+
.AddRetry(new() { MaxRetryAttempts = 10, Delay = TimeSpan.FromSeconds(1), ShouldHandle = new PredicateBuilder().Handle<NatsException>() })
74+
.Build();
75+
string? volumeName = null;
76+
string? bindMountPath = null;
77+
78+
try
79+
{
80+
var builder1 = CreateDistributedApplicationBuilder();
81+
var nats1 = builder1.AddNats("nats")
82+
.WithJetStream();
83+
84+
if (useVolume)
85+
{
86+
// Use a deterministic volume name to prevent them from exhausting the machines if deletion fails
87+
volumeName = VolumeNameGenerator.CreateVolumeName(nats1, nameof(WithDataShouldPersistStateBetweenUsages));
88+
89+
// if the volume already exists (because of a crashing previous run), try to delete it
90+
DockerUtils.AttemptDeleteDockerVolume(volumeName);
91+
nats1.WithDataVolume(volumeName);
92+
}
93+
else
94+
{
95+
bindMountPath = Directory.CreateTempSubdirectory().FullName;
96+
nats1.WithDataBindMount(bindMountPath);
97+
}
98+
99+
using (var app = builder1.Build())
100+
{
101+
await app.StartAsync();
102+
try
103+
{
104+
var hb = Host.CreateApplicationBuilder();
105+
106+
hb.Configuration[$"ConnectionStrings:{nats1.Resource.Name}"] = await nats1.Resource.ConnectionStringExpression.GetValueAsync(default);
107+
108+
hb.AddNatsClient("nats", configureOptions: opts =>
109+
{
110+
var jsonRegistry = new NatsJsonContextSerializerRegistry(AppJsonContext.Default);
111+
return opts with { SerializerRegistry = jsonRegistry };
112+
});
113+
114+
hb.AddNatsJetStream();
115+
116+
using (var host = hb.Build())
117+
{
118+
await host.StartAsync();
119+
120+
await pipeline.ExecuteAsync(async token =>
121+
{
122+
var jetStream = host.Services.GetRequiredService<INatsJSContext>();
123+
await CreateTestData(jetStream, token);
124+
await ConsumeTestData(jetStream, token);
125+
126+
}, cts.Token);
127+
}
128+
}
129+
finally
130+
{
131+
// Stops the container, or the Volume/mount would still be in use
132+
await app.StopAsync();
133+
}
134+
}
135+
136+
var builder2 = CreateDistributedApplicationBuilder();
137+
var nats2 = builder2.AddNats("nats")
138+
.WithJetStream();
139+
140+
if (useVolume)
141+
{
142+
nats2.WithDataVolume(volumeName);
143+
}
144+
else
145+
{
146+
nats2.WithDataBindMount(bindMountPath!);
147+
}
148+
149+
using (var app = builder2.Build())
150+
{
151+
await app.StartAsync();
152+
try
153+
{
154+
var hb = Host.CreateApplicationBuilder();
155+
156+
hb.Configuration[$"ConnectionStrings:{nats2.Resource.Name}"] = await nats2.Resource.ConnectionStringExpression.GetValueAsync(default);
157+
hb.AddNatsClient("nats", configureOptions: opts =>
158+
{
159+
var jsonRegistry = new NatsJsonContextSerializerRegistry(AppJsonContext.Default);
160+
return opts with { SerializerRegistry = jsonRegistry };
161+
});
162+
163+
hb.AddNatsJetStream();
164+
165+
using (var host = hb.Build())
166+
{
167+
await host.StartAsync();
168+
169+
await pipeline.ExecuteAsync(async token =>
170+
{
171+
var jetStream = host.Services.GetRequiredService<INatsJSContext>();
172+
await ConsumeTestData(jetStream, token);
173+
});
174+
}
175+
}
176+
finally
177+
{
178+
// Stops the container, or the Volume/mount would still be in use
179+
await app.StopAsync();
180+
}
181+
}
182+
}
183+
finally
184+
{
185+
if (volumeName is not null)
186+
{
187+
DockerUtils.AttemptDeleteDockerVolume(volumeName);
188+
}
189+
190+
if (bindMountPath is not null)
191+
{
192+
try
193+
{
194+
Directory.Delete(bindMountPath, recursive: true);
195+
}
196+
catch
197+
{
198+
// Don't fail test if we can't clean the temporary folder
199+
}
200+
}
201+
}
202+
}
203+
204+
private static async Task ConsumeTestData(INatsJSContext jetStream, CancellationToken token)
205+
{
206+
var stream = await jetStream.GetStreamAsync(StreamName, cancellationToken: token);
207+
var consumer = await stream.CreateOrderedConsumerAsync(cancellationToken: token);
208+
209+
var events = new List<AppEvent>();
210+
await foreach (var msg in consumer.ConsumeAsync<AppEvent>(cancellationToken: token))
211+
{
212+
events.Add(msg.Data!);
213+
await msg.AckAsync(cancellationToken: token);
214+
if (msg.Metadata?.NumPending == 0)
215+
{
216+
break;
217+
}
218+
}
219+
220+
for (var i = 0; i < 10; i++)
221+
{
222+
var @event = events[i];
223+
Assert.Equal($"test-event-{i}", @event.Name);
224+
Assert.Equal($"test-event-description-{i}", @event.Description);
225+
}
226+
}
227+
228+
private static async Task CreateTestData(INatsJSContext jetStream, CancellationToken token)
229+
{
230+
var stream = await jetStream.CreateStreamAsync(new StreamConfig(StreamName, [SubjectName]), cancellationToken: token);
231+
Assert.Equal(StreamName, stream.Info.Config.Name);
232+
233+
for (var i = 0; i < 10; i++)
234+
{
235+
var appEvent = new AppEvent(SubjectName, $"test-event-{i}", $"test-event-description-{i}", i);
236+
var ack = await jetStream.PublishAsync(appEvent.Subject, appEvent, cancellationToken: token);
237+
ack.EnsureSuccess();
238+
}
239+
}
240+
241+
private TestDistributedApplicationBuilder CreateDistributedApplicationBuilder()
242+
{
243+
var builder = TestDistributedApplicationBuilder.CreateWithTestContainerRegistry();
244+
builder.Services.AddXunitLogging(testOutputHelper);
245+
return builder;
246+
}
247+
}

tests/Aspire.Hosting.Tests/Aspire.Hosting.Tests.csproj

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
<ProjectReference Include="..\..\src\Aspire.Hosting.AWS\Aspire.Hosting.AWS.csproj" IsAspireProjectResource="false" />
1414
<ProjectReference Include="..\..\src\Aspire.Hosting.Dapr\Aspire.Hosting.Dapr.csproj" IsAspireProjectResource="false" />
1515
<ProjectReference Include="..\..\src\Aspire.Hosting.MongoDB\Aspire.Hosting.MongoDB.csproj" IsAspireProjectResource="false" />
16-
<ProjectReference Include="..\..\src\Aspire.Hosting.Nats\Aspire.Hosting.Nats.csproj" IsAspireProjectResource="false" />
1716
<ProjectReference Include="..\..\src\Aspire.Hosting.Testing\Aspire.Hosting.Testing.csproj" IsAspireProjectResource="false" />
1817
<ProjectReference Include="..\Aspire.Components.Common.Tests\Aspire.Components.Common.Tests.csproj" IsAspireProjectResource="false" />
1918
<ProjectReference Include="..\testproject\TestProject.AppHost\TestProject.AppHost.csproj" IsAspireProjectResource="false" />
@@ -25,7 +24,6 @@
2524
<PackageReference Include="Microsoft.Extensions.Diagnostics.Testing" />
2625

2726
<Compile Include="$(TestsSharedDir)Logging\*.cs" LinkBase="shared/Logging" />
28-
<Compile Include="$(RepoRoot)src\Aspire.Hosting.Nats\NatsContainerImageTags.cs" />
2927
<Compile Include="$(RepoRoot)src\Aspire.Hosting.MongoDB\MongoDBContainerImageTags.cs" />
3028
<Compile Include="$(RepoRoot)src\Aspire.Hosting.Oracle\OracleContainerImageTags.cs" />
3129
<Compile Include="$(RepoRoot)src\Aspire.Hosting.PostgreSQL\PostgresContainerImageTags.cs" />

0 commit comments

Comments
 (0)