From 344b6f9f53e283bc6d27cd1d917703ac9ce321e9 Mon Sep 17 00:00:00 2001 From: Sebastien Ros Date: Tue, 5 Dec 2023 17:46:55 -0800 Subject: [PATCH 01/20] Add smoke tests for mongodb --- .../DistributedApplicationTests.cs | 42 ++++++++++++++--- ...locatedEndpointAnnotationTestExtensions.cs | 45 ++++++++++++++++--- .../TestProject.AppHost/TestProgram.cs | 2 +- .../MongoDB/MongoDBExtensions.cs | 37 +++++++++++++++ .../MongoDB/Movie.cs | 4 ++ .../Program.cs | 7 ++- 6 files changed, 124 insertions(+), 13 deletions(-) create mode 100644 tests/testproject/TestProject.IntegrationServiceA/MongoDB/MongoDBExtensions.cs create mode 100644 tests/testproject/TestProject.IntegrationServiceA/MongoDB/Movie.cs diff --git a/tests/Aspire.Hosting.Tests/DistributedApplicationTests.cs b/tests/Aspire.Hosting.Tests/DistributedApplicationTests.cs index 5460f49a18..5b9cb1a3a3 100644 --- a/tests/Aspire.Hosting.Tests/DistributedApplicationTests.cs +++ b/tests/Aspire.Hosting.Tests/DistributedApplicationTests.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Globalization; +using System.Net.Http.Json; using Aspire.Hosting.Dcp; using Aspire.Hosting.Dcp.Model; using Aspire.Hosting.Lifecycle; @@ -351,7 +352,7 @@ public async Task VerifyHealthyOnIntegrationServiceA() await using var app = testProgram.Build(); - var client = app.Services.GetRequiredService().CreateClient(); + using var client = app.Services.GetRequiredService().CreateClient(); using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(1)); @@ -365,7 +366,38 @@ public async Task VerifyHealthyOnIntegrationServiceA() // We wait until timeout for the /health endpoint to return successfully. We assume // that components wired up into this project have health checks enabled. - await testProgram.IntegrationServiceABuilder!.WaitForHealthyStatus(client, "http", cts.Token); + await testProgram.IntegrationServiceABuilder!.WaitForHealthyStatusAsync(client, "http", cts.Token); + } + + [LocalOnlyFact()] + public async Task VerifyMongoDBWorks() + { + var testProgram = CreateTestProgram(includeIntegrationServices: true); + testProgram.AppBuilder.Services.AddLogging(b => b.AddXunit(_testOutputHelper)); + + testProgram.AppBuilder.Services + .AddHttpClient() + .ConfigureHttpClientDefaults(b => + { + b.UseSocketsHttpHandler((handler, sp) => handler.PooledConnectionLifetime = TimeSpan.FromSeconds(5)); + }); + + await using var app = testProgram.Build(); + + using var client = app.Services.GetRequiredService().CreateClient(); + + using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(1)); + + await app.StartAsync(cts.Token); + + var response1 = await testProgram.IntegrationServiceABuilder!.HttpGetAsync(client, "http", "/mongodb/databases", cts.Token); + var databases = await response1.Content.ReadFromJsonAsync(cts.Token); + + var response2 = await testProgram.IntegrationServiceABuilder!.HttpGetAsync(client, "http", "/mongodb/movies", cts.Token); + var movies = await response2.Content.ReadFromJsonAsync(cts.Token); + + Assert.Equivalent(new[] { "admin", "config", "local", "mymongodb" }, databases); + Assert.Equivalent(new[] { "Rocky I", "Rocky II" }, movies); } [LocalOnlyFact("node")] @@ -384,14 +416,14 @@ public async Task VerifyNodeAppWorks() await using var app = testProgram.Build(); - var client = app.Services.GetRequiredService().CreateClient(); + using var client = app.Services.GetRequiredService().CreateClient(); using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(1)); await app.StartAsync(cts.Token); - var response0 = await testProgram.NodeAppBuilder!.HttpGetWithRetryAsync(client, "http", "/", cts.Token); - var response1 = await testProgram.NpmAppBuilder!.HttpGetWithRetryAsync(client, "http", "/", cts.Token); + var response0 = await testProgram.NodeAppBuilder!.HttpGetStringWithRetryAsync(client, "http", "/", cts.Token); + var response1 = await testProgram.NpmAppBuilder!.HttpGetStringWithRetryAsync(client, "http", "/", cts.Token); Assert.Equal("Hello from node!", response0); Assert.Equal("Hello from node!", response1); diff --git a/tests/Aspire.Hosting.Tests/Helpers/AllocatedEndpointAnnotationTestExtensions.cs b/tests/Aspire.Hosting.Tests/Helpers/AllocatedEndpointAnnotationTestExtensions.cs index 226279a906..31558f718e 100644 --- a/tests/Aspire.Hosting.Tests/Helpers/AllocatedEndpointAnnotationTestExtensions.cs +++ b/tests/Aspire.Hosting.Tests/Helpers/AllocatedEndpointAnnotationTestExtensions.cs @@ -2,9 +2,20 @@ // The .NET Foundation licenses this file to you under the MIT license. namespace Aspire.Hosting.Tests.Helpers; + public static class AllocatedEndpointAnnotationTestExtensions { - public static async Task HttpGetAsync(this IResourceBuilder builder, HttpClient client, string bindingName, string path, CancellationToken cancellationToken) + /// + /// Sends a GET request to the specified resource and returns the response body as a string. + /// + /// The type of the resource. + /// The resource. + /// The instance to use. + /// The name of the binding. + /// The path the request is sent to. + /// The cancellation token to cancel the operation. + /// The string representing the response body. + public static async Task HttpGetStringAsync(this IResourceBuilder builder, HttpClient client, string bindingName, string path, CancellationToken cancellationToken) where T : IResourceWithBindings { // We have to get the allocated endpoint each time through the loop @@ -16,25 +27,47 @@ public static async Task HttpGetAsync(this IResourceBuilder builde return response; } - public static Task WaitForHealthyStatus(this IResourceBuilder builder, HttpClient client, string bindingName, CancellationToken cancellationToken) + /// + /// Sends a GET request to the specified resource and returns the response message. + /// + /// The type of the resource. + /// The resource. + /// The instance to use. + /// The name of the binding. + /// The path the request is sent to. + /// The cancellation token to cancel the operation. + /// The response message. + public static async Task HttpGetAsync(this IResourceBuilder builder, HttpClient client, string bindingName, string path, CancellationToken cancellationToken) + where T : IResourceWithBindings + { + // We have to get the allocated endpoint each time through the loop + // because it may not be populated yet by the time we get here. + var allocatedEndpoint = builder.Resource.Annotations.OfType().Single(a => a.Name == bindingName); + var url = $"{allocatedEndpoint.UriString}{path}"; + + var response = await client.GetAsync(url, cancellationToken); + return response; + } + + public static Task WaitForHealthyStatusAsync(this IResourceBuilder builder, HttpClient client, string bindingName, CancellationToken cancellationToken) { - return HttpGetWithRetryAsync(builder, client, bindingName, "/health", cancellationToken); + return HttpGetStringWithRetryAsync(builder, client, bindingName, "/health", cancellationToken); } public static Task HttpGetPidAsync(this IResourceBuilder builder, HttpClient client, string bindingName, CancellationToken cancellationToken) where T : IResourceWithBindings { - return HttpGetWithRetryAsync(builder, client, bindingName, "/pid", cancellationToken); + return HttpGetStringWithRetryAsync(builder, client, bindingName, "/pid", cancellationToken); } - public static async Task HttpGetWithRetryAsync(this IResourceBuilder builder, HttpClient client, string bindingName, string request, CancellationToken cancellationToken) + public static async Task HttpGetStringWithRetryAsync(this IResourceBuilder builder, HttpClient client, string bindingName, string request, CancellationToken cancellationToken) where T : IResourceWithBindings { while (true) { try { - return await builder.HttpGetAsync(client, bindingName, request, cancellationToken); + return await builder.HttpGetStringAsync(client, bindingName, request, cancellationToken); } catch (HttpRequestException ex) { diff --git a/tests/testproject/TestProject.AppHost/TestProgram.cs b/tests/testproject/TestProject.AppHost/TestProgram.cs index 4f47ab34b8..f5b93ed8e6 100644 --- a/tests/testproject/TestProject.AppHost/TestProgram.cs +++ b/tests/testproject/TestProject.AppHost/TestProgram.cs @@ -37,7 +37,7 @@ private TestProgram(string[] args, Assembly assembly, bool includeIntegrationSer var redis = AppBuilder.AddRedisContainer("redis"); var postgres = AppBuilder.AddPostgresContainer("postgres"); var rabbitmq = AppBuilder.AddRabbitMQContainer("rabbitmq"); - var mongodb = AppBuilder.AddMongoDBContainer("mongodb"); + var mongodb = AppBuilder.AddMongoDBContainer("mongodb").AddDatabase("mymongodb"); IntegrationServiceABuilder = AppBuilder.AddProject("integrationservicea") .WithReference(sqlserver) diff --git a/tests/testproject/TestProject.IntegrationServiceA/MongoDB/MongoDBExtensions.cs b/tests/testproject/TestProject.IntegrationServiceA/MongoDB/MongoDBExtensions.cs new file mode 100644 index 0000000000..a6bd96b3fd --- /dev/null +++ b/tests/testproject/TestProject.IntegrationServiceA/MongoDB/MongoDBExtensions.cs @@ -0,0 +1,37 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using MongoDB.Driver; + +public static class MongoDBExtensions +{ + public static void ConfigureMongoDB(this WebApplication app) + { + // Resolving IMongoDatabase creates the database automatically from the connection string property + + var database = app.Services.GetRequiredService(); + database.CreateCollection("movies"); + var collection = database.GetCollection("movies"); + collection.InsertOne(new Movie(1, "Rocky I")); + collection.InsertOne(new Movie(2, "Rocky II")); + + app.MapGet("/mongodb/databases", GetDatabaseNamesAsync); + + app.MapGet("/mongodb/movies", GetMoviesAsync); + } + private static async Task> GetDatabaseNamesAsync(IMongoClient client) + { + var databaseNames = new List(); + + await client.ListDatabaseNames().ForEachAsync(databaseNames.Add); + + return databaseNames; + } + + private static async Task> GetMoviesAsync(IMongoDatabase db) + { + var moviesCollection = db.GetCollection("movies"); + + return (await moviesCollection.Find(x => true).ToListAsync()).Select(x => x.Name).ToList(); + } +} diff --git a/tests/testproject/TestProject.IntegrationServiceA/MongoDB/Movie.cs b/tests/testproject/TestProject.IntegrationServiceA/MongoDB/Movie.cs new file mode 100644 index 0000000000..8581521d72 --- /dev/null +++ b/tests/testproject/TestProject.IntegrationServiceA/MongoDB/Movie.cs @@ -0,0 +1,4 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +public record class Movie(int Id, string Name); diff --git a/tests/testproject/TestProject.IntegrationServiceA/Program.cs b/tests/testproject/TestProject.IntegrationServiceA/Program.cs index 0448552746..8259247652 100644 --- a/tests/testproject/TestProject.IntegrationServiceA/Program.cs +++ b/tests/testproject/TestProject.IntegrationServiceA/Program.cs @@ -7,11 +7,16 @@ builder.AddRedis("redis"); builder.AddNpgsqlDataSource("postgres"); builder.AddRabbitMQ("rabbitmq"); -builder.AddMongoDBClient("mongodb"); +builder.AddMongoDBClient("mymongodb"); var app = builder.Build(); app.MapHealthChecks("/health"); + app.MapGet("/", () => "Hello World!"); + app.MapGet("/pid", () => Environment.ProcessId); + +app.ConfigureMongoDB(); + app.Run(); From 1ccbfa0250ecba457f0a26ed5b0af9ba16fa01f2 Mon Sep 17 00:00:00 2001 From: Sebastien Ros Date: Tue, 5 Dec 2023 21:33:26 -0800 Subject: [PATCH 02/20] Rename ConfigureMongoDB to MapMongoMovieApi --- .../MongoDB/MongoDBExtensions.cs | 2 +- tests/testproject/TestProject.IntegrationServiceA/Program.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/testproject/TestProject.IntegrationServiceA/MongoDB/MongoDBExtensions.cs b/tests/testproject/TestProject.IntegrationServiceA/MongoDB/MongoDBExtensions.cs index a6bd96b3fd..fe8d6c7841 100644 --- a/tests/testproject/TestProject.IntegrationServiceA/MongoDB/MongoDBExtensions.cs +++ b/tests/testproject/TestProject.IntegrationServiceA/MongoDB/MongoDBExtensions.cs @@ -5,7 +5,7 @@ public static class MongoDBExtensions { - public static void ConfigureMongoDB(this WebApplication app) + public static void MapMongoMovieApi(this WebApplication app) { // Resolving IMongoDatabase creates the database automatically from the connection string property diff --git a/tests/testproject/TestProject.IntegrationServiceA/Program.cs b/tests/testproject/TestProject.IntegrationServiceA/Program.cs index 8259247652..cf5bbd41bf 100644 --- a/tests/testproject/TestProject.IntegrationServiceA/Program.cs +++ b/tests/testproject/TestProject.IntegrationServiceA/Program.cs @@ -17,6 +17,6 @@ app.MapGet("/pid", () => Environment.ProcessId); -app.ConfigureMongoDB(); +app.MapMongoMovieApi(); app.Run(); From 76375dd077e9e23af785c9628674a3a8f5c84f99 Mon Sep 17 00:00:00 2001 From: Sebastien Ros Date: Wed, 6 Dec 2023 16:12:27 -0800 Subject: [PATCH 03/20] Use XUnit fixture to share same app host --- .../DistributedApplicationTests.cs | 61 --------- ...locatedEndpointAnnotationTestExtensions.cs | 23 ++++ .../Helpers/LocalOnlyFactAttribute.cs | 2 + .../MongoDB/MongoDBFunctionalTests.cs | 47 +++++++ .../Node/NodeFunctionalTests.cs | 33 +++++ .../{ => Redis}/AddRedisTests.cs | 2 +- .../Redis/RedisFunctionalTests.cs | 34 +++++ .../TestProgramFixture.cs | 118 ++++++++++++++++++ .../Program.cs | 2 + .../Redis/RedisExtensions.cs | 30 +++++ 10 files changed, 290 insertions(+), 62 deletions(-) create mode 100644 tests/Aspire.Hosting.Tests/MongoDB/MongoDBFunctionalTests.cs create mode 100644 tests/Aspire.Hosting.Tests/Node/NodeFunctionalTests.cs rename tests/Aspire.Hosting.Tests/{ => Redis}/AddRedisTests.cs (99%) create mode 100644 tests/Aspire.Hosting.Tests/Redis/RedisFunctionalTests.cs create mode 100644 tests/Aspire.Hosting.Tests/TestProgramFixture.cs create mode 100644 tests/testproject/TestProject.IntegrationServiceA/Redis/RedisExtensions.cs diff --git a/tests/Aspire.Hosting.Tests/DistributedApplicationTests.cs b/tests/Aspire.Hosting.Tests/DistributedApplicationTests.cs index 5b9cb1a3a3..aead8b438f 100644 --- a/tests/Aspire.Hosting.Tests/DistributedApplicationTests.cs +++ b/tests/Aspire.Hosting.Tests/DistributedApplicationTests.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Globalization; -using System.Net.Http.Json; using Aspire.Hosting.Dcp; using Aspire.Hosting.Dcp.Model; using Aspire.Hosting.Lifecycle; @@ -369,66 +368,6 @@ public async Task VerifyHealthyOnIntegrationServiceA() await testProgram.IntegrationServiceABuilder!.WaitForHealthyStatusAsync(client, "http", cts.Token); } - [LocalOnlyFact()] - public async Task VerifyMongoDBWorks() - { - var testProgram = CreateTestProgram(includeIntegrationServices: true); - testProgram.AppBuilder.Services.AddLogging(b => b.AddXunit(_testOutputHelper)); - - testProgram.AppBuilder.Services - .AddHttpClient() - .ConfigureHttpClientDefaults(b => - { - b.UseSocketsHttpHandler((handler, sp) => handler.PooledConnectionLifetime = TimeSpan.FromSeconds(5)); - }); - - await using var app = testProgram.Build(); - - using var client = app.Services.GetRequiredService().CreateClient(); - - using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(1)); - - await app.StartAsync(cts.Token); - - var response1 = await testProgram.IntegrationServiceABuilder!.HttpGetAsync(client, "http", "/mongodb/databases", cts.Token); - var databases = await response1.Content.ReadFromJsonAsync(cts.Token); - - var response2 = await testProgram.IntegrationServiceABuilder!.HttpGetAsync(client, "http", "/mongodb/movies", cts.Token); - var movies = await response2.Content.ReadFromJsonAsync(cts.Token); - - Assert.Equivalent(new[] { "admin", "config", "local", "mymongodb" }, databases); - Assert.Equivalent(new[] { "Rocky I", "Rocky II" }, movies); - } - - [LocalOnlyFact("node")] - public async Task VerifyNodeAppWorks() - { - var testProgram = CreateTestProgram(includeNodeApp: true); - testProgram.AppBuilder.Services.AddLogging(b => b.AddXunit(_testOutputHelper)); - - testProgram.AppBuilder.Services - .AddHttpClient() - .ConfigureHttpClientDefaults(b => - { - b.UseSocketsHttpHandler((handler, sp) => handler.PooledConnectionLifetime = TimeSpan.FromSeconds(5)); - b.AddStandardResilienceHandler(); - }); - - await using var app = testProgram.Build(); - - using var client = app.Services.GetRequiredService().CreateClient(); - - using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(1)); - - await app.StartAsync(cts.Token); - - var response0 = await testProgram.NodeAppBuilder!.HttpGetStringWithRetryAsync(client, "http", "/", cts.Token); - var response1 = await testProgram.NpmAppBuilder!.HttpGetStringWithRetryAsync(client, "http", "/", cts.Token); - - Assert.Equal("Hello from node!", response0); - Assert.Equal("Hello from node!", response1); - } - [LocalOnlyFact("docker")] public async Task VerifyDockerAppWorks() { diff --git a/tests/Aspire.Hosting.Tests/Helpers/AllocatedEndpointAnnotationTestExtensions.cs b/tests/Aspire.Hosting.Tests/Helpers/AllocatedEndpointAnnotationTestExtensions.cs index 31558f718e..37363698f3 100644 --- a/tests/Aspire.Hosting.Tests/Helpers/AllocatedEndpointAnnotationTestExtensions.cs +++ b/tests/Aspire.Hosting.Tests/Helpers/AllocatedEndpointAnnotationTestExtensions.cs @@ -49,6 +49,29 @@ public static async Task HttpGetAsync(this IResourceBuil return response; } + /// + /// Sends a POST request to the specified resource and returns the response message. + /// + /// The type of the resource. + /// The resource. + /// The instance to use. + /// The name of the binding. + /// The path the request is sent to. + /// The HTTP request content sent to the server. + /// The cancellation token to cancel the operation. + /// The response message. + public static async Task HttpPostAsync(this IResourceBuilder builder, HttpClient client, string bindingName, string path, HttpContent? content, CancellationToken cancellationToken) + where T : IResourceWithBindings + { + // We have to get the allocated endpoint each time through the loop + // because it may not be populated yet by the time we get here. + var allocatedEndpoint = builder.Resource.Annotations.OfType().Single(a => a.Name == bindingName); + var url = $"{allocatedEndpoint.UriString}{path}"; + + var response = await client.PostAsync(url, content, cancellationToken); + return response; + } + public static Task WaitForHealthyStatusAsync(this IResourceBuilder builder, HttpClient client, string bindingName, CancellationToken cancellationToken) { return HttpGetStringWithRetryAsync(builder, client, bindingName, "/health", cancellationToken); diff --git a/tests/Aspire.Hosting.Tests/Helpers/LocalOnlyFactAttribute.cs b/tests/Aspire.Hosting.Tests/Helpers/LocalOnlyFactAttribute.cs index b478357689..c376c71d75 100644 --- a/tests/Aspire.Hosting.Tests/Helpers/LocalOnlyFactAttribute.cs +++ b/tests/Aspire.Hosting.Tests/Helpers/LocalOnlyFactAttribute.cs @@ -12,6 +12,8 @@ public override string Skip { get { + // BUILD_BUILDID is defined by Azure Dev Ops + if (Environment.GetEnvironmentVariable("BUILD_BUILDID") != null) { return "LocalOnlyFactAttribute tests are not run as part of CI."; diff --git a/tests/Aspire.Hosting.Tests/MongoDB/MongoDBFunctionalTests.cs b/tests/Aspire.Hosting.Tests/MongoDB/MongoDBFunctionalTests.cs new file mode 100644 index 0000000000..f91b89c829 --- /dev/null +++ b/tests/Aspire.Hosting.Tests/MongoDB/MongoDBFunctionalTests.cs @@ -0,0 +1,47 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Net.Http.Json; +using Aspire.Hosting.Tests.Helpers; +using Xunit; + +namespace Aspire.Hosting.Tests.MongoDB; + +[Collection("IntegrationServices")] +public class MongoDBFunctionalTests +{ + private readonly IntegrationServicesFixture _integrationServicesFixture; + + public MongoDBFunctionalTests(IntegrationServicesFixture integrationServicesFixture) + { + _integrationServicesFixture = integrationServicesFixture; + } + + [LocalOnlyFact()] + public async Task DatabaseIsCreatedOnDemand() + { + var testProgram = _integrationServicesFixture.TestProgram; + var client = _integrationServicesFixture.HttpClient; + + using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(1)); + + var response = await testProgram.IntegrationServiceABuilder!.HttpGetAsync(client, "http", "/mongodb/databases", cts.Token); + var databases = await response.Content.ReadFromJsonAsync(cts.Token); + + Assert.Equivalent(new[] { "admin", "config", "local", "mymongodb" }, databases); + } + + [LocalOnlyFact()] + public async Task VerifyMongoWorks() + { + var testProgram = _integrationServicesFixture.TestProgram; + var client = _integrationServicesFixture.HttpClient; + + using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(1)); + + var response = await testProgram.IntegrationServiceABuilder!.HttpGetAsync(client, "http", "/mongodb/movies", cts.Token); + var movies = await response.Content.ReadFromJsonAsync(cts.Token); + + Assert.Equivalent(new[] { "Rocky I", "Rocky II" }, movies); + } +} diff --git a/tests/Aspire.Hosting.Tests/Node/NodeFunctionalTests.cs b/tests/Aspire.Hosting.Tests/Node/NodeFunctionalTests.cs new file mode 100644 index 0000000000..6daf69547b --- /dev/null +++ b/tests/Aspire.Hosting.Tests/Node/NodeFunctionalTests.cs @@ -0,0 +1,33 @@ +// 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.Tests.Helpers; +using Xunit; + +namespace Aspire.Hosting.Tests.Node; + +[Collection("NodeJs")] +public class NodeFunctionalTests +{ + private readonly NodeJsFixture _nodeJsFixture; + + public NodeFunctionalTests(NodeJsFixture nodeJsFixture) + { + _nodeJsFixture = nodeJsFixture; + } + + [LocalOnlyFact("node")] + public async Task VerifyNodeAppWorks() + { + var testProgram = _nodeJsFixture.TestProgram; + var client = _nodeJsFixture.HttpClient; + + using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(1)); + + var response0 = await testProgram.NodeAppBuilder!.HttpGetStringWithRetryAsync(client, "http", "/", cts.Token); + var response1 = await testProgram.NpmAppBuilder!.HttpGetStringWithRetryAsync(client, "http", "/", cts.Token); + + Assert.Equal("Hello from node!", response0); + Assert.Equal("Hello from node!", response1); + } +} diff --git a/tests/Aspire.Hosting.Tests/AddRedisTests.cs b/tests/Aspire.Hosting.Tests/Redis/AddRedisTests.cs similarity index 99% rename from tests/Aspire.Hosting.Tests/AddRedisTests.cs rename to tests/Aspire.Hosting.Tests/Redis/AddRedisTests.cs index 3c9f20f92b..d37a8eaeb4 100644 --- a/tests/Aspire.Hosting.Tests/AddRedisTests.cs +++ b/tests/Aspire.Hosting.Tests/Redis/AddRedisTests.cs @@ -5,7 +5,7 @@ using Microsoft.Extensions.DependencyInjection; using Xunit; -namespace Aspire.Hosting.Tests; +namespace Aspire.Hosting.Tests.Redis; public class AddRedisTests { diff --git a/tests/Aspire.Hosting.Tests/Redis/RedisFunctionalTests.cs b/tests/Aspire.Hosting.Tests/Redis/RedisFunctionalTests.cs new file mode 100644 index 0000000000..b8a70d283e --- /dev/null +++ b/tests/Aspire.Hosting.Tests/Redis/RedisFunctionalTests.cs @@ -0,0 +1,34 @@ +// 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.Tests.Helpers; +using Xunit; + +namespace Aspire.Hosting.Tests.Redis; + +[Collection("IntegrationServices")] +public class RedisFunctionalTests +{ + private readonly IntegrationServicesFixture _integrationServicesFixture; + + public RedisFunctionalTests(IntegrationServicesFixture integrationServicesFixture) + { + _integrationServicesFixture = integrationServicesFixture; + } + + [LocalOnlyFact()] + public async Task VerifyRedisWorks() + { + var testProgram = _integrationServicesFixture.TestProgram; + var client = _integrationServicesFixture.HttpClient; + var data = "Hello World!"; + + using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(1)); + + await testProgram.IntegrationServiceABuilder!.HttpPostAsync(client, "http", "/redis/hello", new StringContent(data), cts.Token); + var response = await testProgram.IntegrationServiceABuilder!.HttpGetAsync(client, "http", "/redis/hello", cts.Token); + var content = await response.Content.ReadAsStringAsync(); + + Assert.Equal(data, content); + } +} diff --git a/tests/Aspire.Hosting.Tests/TestProgramFixture.cs b/tests/Aspire.Hosting.Tests/TestProgramFixture.cs new file mode 100644 index 0000000000..55de461622 --- /dev/null +++ b/tests/Aspire.Hosting.Tests/TestProgramFixture.cs @@ -0,0 +1,118 @@ +// 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.Tests.Helpers; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Aspire.Hosting.Tests; + +/// +/// This fixture ensures the TestProgram application is started before a test is executed. +/// +public abstract class TestProgramFixture : IAsyncLifetime +{ + private DistributedApplication? _app; + private TestProgram? _testProgram; + private HttpClient? _httpClient; + + public TestProgram TestProgram => _testProgram!; + + public DistributedApplication App => _app!; + + public HttpClient HttpClient => _httpClient!; + + public abstract TestProgram CreateTestProgram(); + + public abstract Task WaitReadyStateAsync(CancellationToken cancellationToken = default); + + public async Task InitializeAsync() + { + _testProgram = CreateTestProgram(); + + _app = _testProgram.Build(); + + _httpClient = _app.Services.GetRequiredService().CreateClient(); + + using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(1)); + + await _app.StartAsync(cts.Token); + + await WaitReadyStateAsync(cts.Token); + } + + public async Task DisposeAsync() + { + if (_app != null) + { + await _app.StopAsync(); + await _app!.DisposeAsync(); + } + + if (_httpClient != null) + { + _httpClient!.Dispose(); + } + } +} + +public class IntegrationServicesFixture : TestProgramFixture +{ + public override TestProgram CreateTestProgram() + { + var testProgram = TestProgram.Create(includeIntegrationServices: true); + + testProgram.AppBuilder.Services + .AddHttpClient() + .ConfigureHttpClientDefaults(b => + { + b.UseSocketsHttpHandler((handler, sp) => handler.PooledConnectionLifetime = TimeSpan.FromSeconds(5)); + }); + + return testProgram; + } + + public override Task WaitReadyStateAsync(CancellationToken cancellationToken = default) + { + return TestProgram.IntegrationServiceABuilder!.HttpGetPidAsync(HttpClient, "http", cancellationToken); + } +} + +public class NodeJsFixture : TestProgramFixture +{ + public override TestProgram CreateTestProgram() + { + var testProgram = TestProgram.Create(includeNodeApp: true); + + testProgram.AppBuilder.Services + .AddHttpClient() + .ConfigureHttpClientDefaults(b => + { + b.UseSocketsHttpHandler((handler, sp) => handler.PooledConnectionLifetime = TimeSpan.FromSeconds(5)); + b.AddStandardResilienceHandler(); + }); + + return testProgram; + } + + public override Task WaitReadyStateAsync(CancellationToken cancellationToken = default) + { + return TestProgram.NodeAppBuilder!.HttpGetStringWithRetryAsync(HttpClient, "http", "/", cancellationToken); + } +} + +[CollectionDefinition("IntegrationServices")] +public class IntegrationServicesCollection : ICollectionFixture +{ + // This class has no code, and is never created. Its purpose is simply + // to be the place to apply [CollectionDefinition] and all the + // ICollectionFixture<> interfaces. +} + +[CollectionDefinition("NodeJs")] +public class NodeJsCollection : ICollectionFixture +{ + // This class has no code, and is never created. Its purpose is simply + // to be the place to apply [CollectionDefinition] and all the + // ICollectionFixture<> interfaces. +} diff --git a/tests/testproject/TestProject.IntegrationServiceA/Program.cs b/tests/testproject/TestProject.IntegrationServiceA/Program.cs index cf5bbd41bf..7a01282e85 100644 --- a/tests/testproject/TestProject.IntegrationServiceA/Program.cs +++ b/tests/testproject/TestProject.IntegrationServiceA/Program.cs @@ -17,6 +17,8 @@ app.MapGet("/pid", () => Environment.ProcessId); +app.MapRedisApi(); + app.MapMongoMovieApi(); app.Run(); diff --git a/tests/testproject/TestProject.IntegrationServiceA/Redis/RedisExtensions.cs b/tests/testproject/TestProject.IntegrationServiceA/Redis/RedisExtensions.cs new file mode 100644 index 0000000000..3216bff182 --- /dev/null +++ b/tests/testproject/TestProject.IntegrationServiceA/Redis/RedisExtensions.cs @@ -0,0 +1,30 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using StackExchange.Redis; + +public static class RedisExtensions +{ + public static void MapRedisApi(this WebApplication app) + { + app.MapPost("/redis/{key}", SetKeyAsync); + + app.MapGet("/redis/{key}", GetKeyAsync); + } + + private static async Task SetKeyAsync(string key, HttpContext context, IConnectionMultiplexer cm) + { + using var sr = new StreamReader(context.Request.Body); + var body = await sr.ReadToEndAsync(); + + var database = cm.GetDatabase(); + await database.StringSetAsync(key, body); + return Results.Ok(); + } + + private static async Task GetKeyAsync(string key, IConnectionMultiplexer cm) + { + var database = cm.GetDatabase(); + return Results.Content(await database.StringGetAsync(key)); + } +} From 677295cbeefe8fbad3ba8061b1af3fbb71bea54f Mon Sep 17 00:00:00 2001 From: Sebastien Ros Date: Wed, 6 Dec 2023 16:26:08 -0800 Subject: [PATCH 04/20] Don't run MongoDB code on startup --- .../MongoDB/MongoDBExtensions.cs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/tests/testproject/TestProject.IntegrationServiceA/MongoDB/MongoDBExtensions.cs b/tests/testproject/TestProject.IntegrationServiceA/MongoDB/MongoDBExtensions.cs index fe8d6c7841..8ea3cca3b9 100644 --- a/tests/testproject/TestProject.IntegrationServiceA/MongoDB/MongoDBExtensions.cs +++ b/tests/testproject/TestProject.IntegrationServiceA/MongoDB/MongoDBExtensions.cs @@ -7,20 +7,17 @@ public static class MongoDBExtensions { public static void MapMongoMovieApi(this WebApplication app) { - // Resolving IMongoDatabase creates the database automatically from the connection string property - - var database = app.Services.GetRequiredService(); - database.CreateCollection("movies"); - var collection = database.GetCollection("movies"); - collection.InsertOne(new Movie(1, "Rocky I")); - collection.InsertOne(new Movie(2, "Rocky II")); - app.MapGet("/mongodb/databases", GetDatabaseNamesAsync); app.MapGet("/mongodb/movies", GetMoviesAsync); } - private static async Task> GetDatabaseNamesAsync(IMongoClient client) + + private static async Task> GetDatabaseNamesAsync(IMongoClient client, IMongoDatabase db) { + // Ensure the database is created + var randomCollection = db.GetCollection("random"); + randomCollection.InsertOne(new Movie(1, "123")); + var databaseNames = new List(); await client.ListDatabaseNames().ForEachAsync(databaseNames.Add); @@ -30,7 +27,10 @@ private static async Task> GetDatabaseNamesAsync(IMongoClient clien private static async Task> GetMoviesAsync(IMongoDatabase db) { + db.CreateCollection("movies"); var moviesCollection = db.GetCollection("movies"); + moviesCollection.InsertOne(new Movie(1, "Rocky I")); + moviesCollection.InsertOne(new Movie(2, "Rocky II")); return (await moviesCollection.Find(x => true).ToListAsync()).Select(x => x.Name).ToList(); } From 30d77456020bf538698087e699755d6fcb0110db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Ros?= Date: Thu, 7 Dec 2023 09:08:26 -0800 Subject: [PATCH 05/20] Update tests/Aspire.Hosting.Tests/TestProgramFixture.cs [ci-skip] Co-authored-by: Eric Erhardt --- tests/Aspire.Hosting.Tests/TestProgramFixture.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Aspire.Hosting.Tests/TestProgramFixture.cs b/tests/Aspire.Hosting.Tests/TestProgramFixture.cs index 55de461622..fb09d09456 100644 --- a/tests/Aspire.Hosting.Tests/TestProgramFixture.cs +++ b/tests/Aspire.Hosting.Tests/TestProgramFixture.cs @@ -46,7 +46,7 @@ public async Task DisposeAsync() if (_app != null) { await _app.StopAsync(); - await _app!.DisposeAsync(); + await _app.DisposeAsync(); } if (_httpClient != null) From 88c337f869a337e642643ef44381fc23a79e7f26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Ros?= Date: Thu, 7 Dec 2023 14:49:15 -0800 Subject: [PATCH 06/20] Update tests/Aspire.Hosting.Tests/TestProgramFixture.cs [ci-skip] Co-authored-by: Eric Erhardt --- tests/Aspire.Hosting.Tests/TestProgramFixture.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/tests/Aspire.Hosting.Tests/TestProgramFixture.cs b/tests/Aspire.Hosting.Tests/TestProgramFixture.cs index fb09d09456..e881e1e9c2 100644 --- a/tests/Aspire.Hosting.Tests/TestProgramFixture.cs +++ b/tests/Aspire.Hosting.Tests/TestProgramFixture.cs @@ -49,10 +49,7 @@ public async Task DisposeAsync() await _app.DisposeAsync(); } - if (_httpClient != null) - { - _httpClient!.Dispose(); - } + _httpClient?.Dispose(); } } From 6891e928c9c25bf01f1aba945b6645a4a53d9fc9 Mon Sep 17 00:00:00 2001 From: Sebastien Ros Date: Thu, 7 Dec 2023 14:48:13 -0800 Subject: [PATCH 07/20] Make mongo apis idempotent --- .../MongoDB/MongoDBExtensions.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/testproject/TestProject.IntegrationServiceA/MongoDB/MongoDBExtensions.cs b/tests/testproject/TestProject.IntegrationServiceA/MongoDB/MongoDBExtensions.cs index 8ea3cca3b9..8def847bff 100644 --- a/tests/testproject/TestProject.IntegrationServiceA/MongoDB/MongoDBExtensions.cs +++ b/tests/testproject/TestProject.IntegrationServiceA/MongoDB/MongoDBExtensions.cs @@ -16,6 +16,7 @@ private static async Task> GetDatabaseNamesAsync(IMongoClient clien { // Ensure the database is created var randomCollection = db.GetCollection("random"); + await randomCollection.DeleteManyAsync(x => true); randomCollection.InsertOne(new Movie(1, "123")); var databaseNames = new List(); @@ -29,8 +30,9 @@ private static async Task> GetMoviesAsync(IMongoDatabase db) { db.CreateCollection("movies"); var moviesCollection = db.GetCollection("movies"); - moviesCollection.InsertOne(new Movie(1, "Rocky I")); - moviesCollection.InsertOne(new Movie(2, "Rocky II")); + await moviesCollection.DeleteManyAsync(x => true); + await moviesCollection.InsertOneAsync(new Movie(1, "Rocky I")); + await moviesCollection.InsertOneAsync(new Movie(2, "Rocky II")); return (await moviesCollection.Find(x => true).ToListAsync()).Select(x => x.Name).ToList(); } From a89ff934a1e174db40dbbb5edb7dd9e443ddd017 Mon Sep 17 00:00:00 2001 From: Sebastien Ros Date: Fri, 8 Dec 2023 16:36:50 -0800 Subject: [PATCH 08/20] Add remaining functional tests --- .../DistributedApplicationTests.cs | 136 ------------------ .../IntegrationServicesTests.cs | 37 +++++ .../MySql/MySqlFunctionalTests.cs | 34 +++++ .../Node/NodeFunctionalTests.cs | 6 +- .../{ => Postgres}/AddPostgresTests.cs | 2 +- .../Postgres/PostgresFunctionalTests.cs | 31 ++++ .../RabbitMQ/RabbitMQFunctionalTests.cs | 31 ++++ .../SlimTestProgramTests.cs | 76 ++++++++++ .../SqlServer/SqlServerFunctionalTests.cs | 31 ++++ .../TestProgramFixture.cs | 61 +++++++- .../TestProject.AppHost/TestProgram.cs | 22 ++- .../MySql/MySqlExtensions.cs | 35 +++++ .../Postgres/PostgresExtensions.cs | 35 +++++ .../Program.cs | 14 +- .../RabbitMQ/RabbitMQExtensions.cs | 35 +++++ .../SqlServer/SqlServerExtensions.cs | 35 +++++ 16 files changed, 470 insertions(+), 151 deletions(-) create mode 100644 tests/Aspire.Hosting.Tests/IntegrationServicesTests.cs create mode 100644 tests/Aspire.Hosting.Tests/MySql/MySqlFunctionalTests.cs rename tests/Aspire.Hosting.Tests/{ => Postgres}/AddPostgresTests.cs (99%) create mode 100644 tests/Aspire.Hosting.Tests/Postgres/PostgresFunctionalTests.cs create mode 100644 tests/Aspire.Hosting.Tests/RabbitMQ/RabbitMQFunctionalTests.cs create mode 100644 tests/Aspire.Hosting.Tests/SlimTestProgramTests.cs create mode 100644 tests/Aspire.Hosting.Tests/SqlServer/SqlServerFunctionalTests.cs create mode 100644 tests/testproject/TestProject.IntegrationServiceA/MySql/MySqlExtensions.cs create mode 100644 tests/testproject/TestProject.IntegrationServiceA/Postgres/PostgresExtensions.cs create mode 100644 tests/testproject/TestProject.IntegrationServiceA/RabbitMQ/RabbitMQExtensions.cs create mode 100644 tests/testproject/TestProject.IntegrationServiceA/SqlServer/SqlServerExtensions.cs diff --git a/tests/Aspire.Hosting.Tests/DistributedApplicationTests.cs b/tests/Aspire.Hosting.Tests/DistributedApplicationTests.cs index aead8b438f..6708f1d286 100644 --- a/tests/Aspire.Hosting.Tests/DistributedApplicationTests.cs +++ b/tests/Aspire.Hosting.Tests/DistributedApplicationTests.cs @@ -18,7 +18,6 @@ public class DistributedApplicationTests { private readonly ITestOutputHelper _testOutputHelper; - // Primary constructors don't get ITestOutputHelper injected public DistributedApplicationTests(ITestOutputHelper testOutputHelper) { _testOutputHelper = testOutputHelper; @@ -187,109 +186,6 @@ public Task AfterEndpointsAllocatedAsync(DistributedApplicationModel appModel, C } } - [LocalOnlyFact] - public async Task TestProjectStartsAndStopsCleanly() - { - var testProgram = CreateTestProgram(); - testProgram.AppBuilder.Services.AddLogging(b => b.AddXunit(_testOutputHelper)); - - testProgram.AppBuilder.Services - .AddHttpClient() - .ConfigureHttpClientDefaults(b => - { - b.UseSocketsHttpHandler((handler, sp) => handler.PooledConnectionLifetime = TimeSpan.FromSeconds(5)); - }); - - await using var app = testProgram.Build(); - - var client = app.Services.GetRequiredService().CreateClient(); - - using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(1)); - - await app.StartAsync(cts.Token); - - // Make sure each service is running - await testProgram.ServiceABuilder.HttpGetPidAsync(client, "http", cts.Token); - await testProgram.ServiceBBuilder.HttpGetPidAsync(client, "http", cts.Token); - await testProgram.ServiceCBuilder.HttpGetPidAsync(client, "http", cts.Token); - } - - [LocalOnlyFact] - public async Task TestPortOnServiceBindingAnnotationAndAllocatedEndpointAnnotationMatch() - { - var testProgram = CreateTestProgram(); - testProgram.AppBuilder.Services.AddLogging(b => b.AddXunit(_testOutputHelper)); - - testProgram.AppBuilder.Services - .AddHttpClient() - .ConfigureHttpClientDefaults(b => - { - b.UseSocketsHttpHandler((handler, sp) => handler.PooledConnectionLifetime = TimeSpan.FromSeconds(5)); - }); - - await using var app = testProgram.Build(); - - var client = app.Services.GetRequiredService().CreateClient(); - - using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(1)); - - await app.StartAsync(cts.Token); - - // Make sure each service is running - await testProgram.ServiceABuilder.HttpGetPidAsync(client, "http", cts.Token); - await testProgram.ServiceBBuilder.HttpGetPidAsync(client, "http", cts.Token); - await testProgram.ServiceCBuilder.HttpGetPidAsync(client, "http", cts.Token); - - foreach (var projectBuilders in testProgram.ServiceProjectBuilders) - { - var serviceBinding = projectBuilders.Resource.Annotations.OfType().Single(); - var allocatedEndpoint = projectBuilders.Resource.Annotations.OfType().Single(); - - Assert.Equal(serviceBinding.Port, allocatedEndpoint.Port); - } - } - - [LocalOnlyFact] - public async Task TestPortOnServiceBindingAnnotationAndAllocatedEndpointAnnotationMatchForReplicatedServices() - { - var testProgram = CreateTestProgram(); - - foreach (var serviceBuilder in testProgram.ServiceProjectBuilders) - { - serviceBuilder.WithReplicas(2); - } - - testProgram.AppBuilder.Services.AddLogging(b => b.AddXunit(_testOutputHelper)); - - testProgram.AppBuilder.Services - .AddHttpClient() - .ConfigureHttpClientDefaults(b => - { - b.UseSocketsHttpHandler((handler, sp) => handler.PooledConnectionLifetime = TimeSpan.FromSeconds(5)); - }); - - await using var app = testProgram.Build(); - - var client = app.Services.GetRequiredService().CreateClient(); - - using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(1)); - - await app.StartAsync(cts.Token); - - // Make sure each service is running - await testProgram.ServiceABuilder.HttpGetPidAsync(client, "http", cts.Token); - await testProgram.ServiceBBuilder.HttpGetPidAsync(client, "http", cts.Token); - await testProgram.ServiceCBuilder.HttpGetPidAsync(client, "http", cts.Token); - - foreach (var projectBuilders in testProgram.ServiceProjectBuilders) - { - var serviceBinding = projectBuilders.Resource.Annotations.OfType().Single(); - var allocatedEndpoint = projectBuilders.Resource.Annotations.OfType().Single(); - - Assert.Equal(serviceBinding.Port, allocatedEndpoint.Port); - } - } - [LocalOnlyFact] public async Task TestServicesWithMultipleReplicas() { @@ -336,38 +232,6 @@ public async Task TestServicesWithMultipleReplicas() } } - [LocalOnlyFact] - public async Task VerifyHealthyOnIntegrationServiceA() - { - var testProgram = CreateTestProgram(includeIntegrationServices: true); - testProgram.AppBuilder.Services.AddLogging(b => b.AddXunit(_testOutputHelper)); - - testProgram.AppBuilder.Services - .AddHttpClient() - .ConfigureHttpClientDefaults(b => - { - b.UseSocketsHttpHandler((handler, sp) => handler.PooledConnectionLifetime = TimeSpan.FromSeconds(5)); - }); - - await using var app = testProgram.Build(); - - using var client = app.Services.GetRequiredService().CreateClient(); - - using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(1)); - - await app.StartAsync(cts.Token); - - // Make sure all services are running - await testProgram.ServiceABuilder.HttpGetPidAsync(client, "http", cts.Token); - await testProgram.ServiceBBuilder.HttpGetPidAsync(client, "http", cts.Token); - await testProgram.ServiceCBuilder.HttpGetPidAsync(client, "http", cts.Token); - await testProgram.IntegrationServiceABuilder!.HttpGetPidAsync(client, "http", cts.Token); - - // We wait until timeout for the /health endpoint to return successfully. We assume - // that components wired up into this project have health checks enabled. - await testProgram.IntegrationServiceABuilder!.WaitForHealthyStatusAsync(client, "http", cts.Token); - } - [LocalOnlyFact("docker")] public async Task VerifyDockerAppWorks() { diff --git a/tests/Aspire.Hosting.Tests/IntegrationServicesTests.cs b/tests/Aspire.Hosting.Tests/IntegrationServicesTests.cs new file mode 100644 index 0000000000..9ce5cb4445 --- /dev/null +++ b/tests/Aspire.Hosting.Tests/IntegrationServicesTests.cs @@ -0,0 +1,37 @@ +// 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.Tests.Helpers; +using Xunit; + +namespace Aspire.Hosting.Tests; + +[Collection("IntegrationServices")] +public class IntegrationServicesTests +{ + private readonly IntegrationServicesFixture _integrationServicesFixture; + + public IntegrationServicesTests(IntegrationServicesFixture integrationServicesFixture) + { + _integrationServicesFixture = integrationServicesFixture; + } + + [LocalOnlyFact] + public async Task VerifyHealthyOnIntegrationServiceA() + { + var testProgram = _integrationServicesFixture.TestProgram; + var client = _integrationServicesFixture.HttpClient; + + using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(1)); + + // Make sure all services are running + await testProgram.ServiceABuilder.HttpGetPidAsync(client, "http", cts.Token); + await testProgram.ServiceBBuilder.HttpGetPidAsync(client, "http", cts.Token); + await testProgram.ServiceCBuilder.HttpGetPidAsync(client, "http", cts.Token); + await testProgram.IntegrationServiceABuilder!.HttpGetPidAsync(client, "http", cts.Token); + + // We wait until timeout for the /health endpoint to return successfully. We assume + // that components wired up into this project have health checks enabled. + await testProgram.IntegrationServiceABuilder!.WaitForHealthyStatusAsync(client, "http", cts.Token); + } +} diff --git a/tests/Aspire.Hosting.Tests/MySql/MySqlFunctionalTests.cs b/tests/Aspire.Hosting.Tests/MySql/MySqlFunctionalTests.cs new file mode 100644 index 0000000000..d9623d6652 --- /dev/null +++ b/tests/Aspire.Hosting.Tests/MySql/MySqlFunctionalTests.cs @@ -0,0 +1,34 @@ +// 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.Tests.Helpers; +using Xunit; + +namespace Aspire.Hosting.Tests.MySql; + +[Collection("IntegrationServices")] +public class MySqlFunctionalTests +{ + private readonly IntegrationServicesFixture _integrationServicesFixture; + + public MySqlFunctionalTests(IntegrationServicesFixture integrationServicesFixture) + { + _integrationServicesFixture = integrationServicesFixture; + } + + [LocalOnlyFact()] + public async Task VerifyMySqlWorks() + { + // MySql health check reports healthy during temporary server phase, c.f. https://github.com/Xabaril/AspNetCore.Diagnostics.HealthChecks/issues/2031 + // This is mitigated by standard resilience handlers in the IntegrationServicesFixture HttpClient configuration + + var testProgram = _integrationServicesFixture.TestProgram; + var client = _integrationServicesFixture.HttpClient; + + using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(1)); + + var response = await testProgram.IntegrationServiceABuilder!.HttpGetAsync(client, "http", "/mysql/verify", cts.Token); + + Assert.True(response.IsSuccessStatusCode); + } +} diff --git a/tests/Aspire.Hosting.Tests/Node/NodeFunctionalTests.cs b/tests/Aspire.Hosting.Tests/Node/NodeFunctionalTests.cs index 6daf69547b..5ed0fa3471 100644 --- a/tests/Aspire.Hosting.Tests/Node/NodeFunctionalTests.cs +++ b/tests/Aspire.Hosting.Tests/Node/NodeFunctionalTests.cs @@ -6,12 +6,12 @@ namespace Aspire.Hosting.Tests.Node; -[Collection("NodeJs")] +[Collection("NodeApp")] public class NodeFunctionalTests { - private readonly NodeJsFixture _nodeJsFixture; + private readonly NodeAppFixture _nodeJsFixture; - public NodeFunctionalTests(NodeJsFixture nodeJsFixture) + public NodeFunctionalTests(NodeAppFixture nodeJsFixture) { _nodeJsFixture = nodeJsFixture; } diff --git a/tests/Aspire.Hosting.Tests/AddPostgresTests.cs b/tests/Aspire.Hosting.Tests/Postgres/AddPostgresTests.cs similarity index 99% rename from tests/Aspire.Hosting.Tests/AddPostgresTests.cs rename to tests/Aspire.Hosting.Tests/Postgres/AddPostgresTests.cs index 70e8a0ec97..0e5e5c8487 100644 --- a/tests/Aspire.Hosting.Tests/AddPostgresTests.cs +++ b/tests/Aspire.Hosting.Tests/Postgres/AddPostgresTests.cs @@ -5,7 +5,7 @@ using Microsoft.Extensions.DependencyInjection; using Xunit; -namespace Aspire.Hosting.Tests; +namespace Aspire.Hosting.Tests.Postgres; public class AddPostgresTests { diff --git a/tests/Aspire.Hosting.Tests/Postgres/PostgresFunctionalTests.cs b/tests/Aspire.Hosting.Tests/Postgres/PostgresFunctionalTests.cs new file mode 100644 index 0000000000..d9ed300659 --- /dev/null +++ b/tests/Aspire.Hosting.Tests/Postgres/PostgresFunctionalTests.cs @@ -0,0 +1,31 @@ +// 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.Tests.Helpers; +using Xunit; + +namespace Aspire.Hosting.Tests.Postgres; + +[Collection("IntegrationServices")] +public class PostgresFunctionalTests +{ + private readonly IntegrationServicesFixture _integrationServicesFixture; + + public PostgresFunctionalTests(IntegrationServicesFixture integrationServicesFixture) + { + _integrationServicesFixture = integrationServicesFixture; + } + + [LocalOnlyFact()] + public async Task VerifyPostgresWorks() + { + var testProgram = _integrationServicesFixture.TestProgram; + var client = _integrationServicesFixture.HttpClient; + + using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(1)); + + var response = await testProgram.IntegrationServiceABuilder!.HttpGetAsync(client, "http", "/postgres/verify", cts.Token); + + Assert.True(response.IsSuccessStatusCode); + } +} diff --git a/tests/Aspire.Hosting.Tests/RabbitMQ/RabbitMQFunctionalTests.cs b/tests/Aspire.Hosting.Tests/RabbitMQ/RabbitMQFunctionalTests.cs new file mode 100644 index 0000000000..85833ec308 --- /dev/null +++ b/tests/Aspire.Hosting.Tests/RabbitMQ/RabbitMQFunctionalTests.cs @@ -0,0 +1,31 @@ +// 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.Tests.Helpers; +using Xunit; + +namespace Aspire.Hosting.Tests.RabbitMQ; + +[Collection("IntegrationServices")] +public class RabbitMQFunctionalTests +{ + private readonly IntegrationServicesFixture _integrationServicesFixture; + + public RabbitMQFunctionalTests(IntegrationServicesFixture integrationServicesFixture) + { + _integrationServicesFixture = integrationServicesFixture; + } + + [LocalOnlyFact()] + public async Task VerifyRabbitMQWorks() + { + var testProgram = _integrationServicesFixture.TestProgram; + var client = _integrationServicesFixture.HttpClient; + + using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(1)); + + var response = await testProgram.IntegrationServiceABuilder!.HttpGetAsync(client, "http", "/rabbit/verify", cts.Token); + + Assert.True(response.IsSuccessStatusCode); + } +} diff --git a/tests/Aspire.Hosting.Tests/SlimTestProgramTests.cs b/tests/Aspire.Hosting.Tests/SlimTestProgramTests.cs new file mode 100644 index 0000000000..904f244dda --- /dev/null +++ b/tests/Aspire.Hosting.Tests/SlimTestProgramTests.cs @@ -0,0 +1,76 @@ +// 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.Tests.Helpers; +using Xunit; + +namespace Aspire.Hosting.Tests; + +[Collection("SlimTestProgram")] +public class SlimTestProgramTests +{ + private readonly SlimTestProgramFixture _slimTestProgramFixture; + + public SlimTestProgramTests(SlimTestProgramFixture slimTestProgramFixture) + { + _slimTestProgramFixture = slimTestProgramFixture; + } + + [LocalOnlyFact] + public async Task TestProjectStartsAndStopsCleanly() + { + var testProgram = _slimTestProgramFixture.TestProgram; + var client = _slimTestProgramFixture.HttpClient; + + using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(1)); + + // Make sure each service is running + await testProgram.ServiceABuilder.HttpGetPidAsync(client, "http", cts.Token); + await testProgram.ServiceBBuilder.HttpGetPidAsync(client, "http", cts.Token); + await testProgram.ServiceCBuilder.HttpGetPidAsync(client, "http", cts.Token); + } + + [LocalOnlyFact] + public async Task TestPortOnServiceBindingAnnotationAndAllocatedEndpointAnnotationMatch() + { + var testProgram = _slimTestProgramFixture.TestProgram; + var client = _slimTestProgramFixture.HttpClient; + + using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(1)); + + // Make sure each service is running + await testProgram.ServiceABuilder.HttpGetPidAsync(client, "http", cts.Token); + await testProgram.ServiceBBuilder.HttpGetPidAsync(client, "http", cts.Token); + await testProgram.ServiceCBuilder.HttpGetPidAsync(client, "http", cts.Token); + + foreach (var projectBuilders in testProgram.ServiceProjectBuilders) + { + var serviceBinding = projectBuilders.Resource.Annotations.OfType().Single(); + var allocatedEndpoint = projectBuilders.Resource.Annotations.OfType().Single(); + + Assert.Equal(serviceBinding.Port, allocatedEndpoint.Port); + } + } + + [LocalOnlyFact] + public async Task TestPortOnServiceBindingAnnotationAndAllocatedEndpointAnnotationMatchForReplicatedServices() + { + var testProgram = _slimTestProgramFixture.TestProgram; + var client = _slimTestProgramFixture.HttpClient; + + using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(1)); + + // Make sure each service is running + await testProgram.ServiceABuilder.HttpGetPidAsync(client, "http", cts.Token); + await testProgram.ServiceBBuilder.HttpGetPidAsync(client, "http", cts.Token); + await testProgram.ServiceCBuilder.HttpGetPidAsync(client, "http", cts.Token); + + foreach (var projectBuilders in testProgram.ServiceProjectBuilders) + { + var serviceBinding = projectBuilders.Resource.Annotations.OfType().Single(); + var allocatedEndpoint = projectBuilders.Resource.Annotations.OfType().Single(); + + Assert.Equal(serviceBinding.Port, allocatedEndpoint.Port); + } + } +} diff --git a/tests/Aspire.Hosting.Tests/SqlServer/SqlServerFunctionalTests.cs b/tests/Aspire.Hosting.Tests/SqlServer/SqlServerFunctionalTests.cs new file mode 100644 index 0000000000..4aefc9ca90 --- /dev/null +++ b/tests/Aspire.Hosting.Tests/SqlServer/SqlServerFunctionalTests.cs @@ -0,0 +1,31 @@ +// 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.Tests.Helpers; +using Xunit; + +namespace Aspire.Hosting.Tests.SqlServer; + +[Collection("IntegrationServices")] +public class SqlServerFunctionalTests +{ + private readonly IntegrationServicesFixture _integrationServicesFixture; + + public SqlServerFunctionalTests(IntegrationServicesFixture integrationServicesFixture) + { + _integrationServicesFixture = integrationServicesFixture; + } + + [LocalOnlyFact()] + public async Task VerifySqlServerWorks() + { + var testProgram = _integrationServicesFixture.TestProgram; + var client = _integrationServicesFixture.HttpClient; + + using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(1)); + + var response = await testProgram.IntegrationServiceABuilder!.HttpGetAsync(client, "http", "/sqlserver/verify", cts.Token); + + Assert.True(response.IsSuccessStatusCode); + } +} diff --git a/tests/Aspire.Hosting.Tests/TestProgramFixture.cs b/tests/Aspire.Hosting.Tests/TestProgramFixture.cs index e881e1e9c2..b0a711b14d 100644 --- a/tests/Aspire.Hosting.Tests/TestProgramFixture.cs +++ b/tests/Aspire.Hosting.Tests/TestProgramFixture.cs @@ -53,6 +53,43 @@ public async Task DisposeAsync() } } +/// +/// TestProgram with no dashboard, node app or integration services. +/// +/// +/// Use [Collection("SlimTestProgram")] to inject this fixture in test constructors. +/// +public class SlimTestProgramFixture : TestProgramFixture +{ + public override TestProgram CreateTestProgram() + { + var testProgram = TestProgram.Create(); + + testProgram.AppBuilder.Services + .AddHttpClient() + .ConfigureHttpClientDefaults(b => + { + b.UseSocketsHttpHandler((handler, sp) => handler.PooledConnectionLifetime = TimeSpan.FromSeconds(5)); + }); + + return testProgram; + } + + public override async Task WaitReadyStateAsync(CancellationToken cancellationToken = default) + { + // Make sure services A, B and C are running + await TestProgram.ServiceABuilder.HttpGetPidAsync(HttpClient, "http", cancellationToken); + await TestProgram.ServiceBBuilder.HttpGetPidAsync(HttpClient, "http", cancellationToken); + await TestProgram.ServiceCBuilder.HttpGetPidAsync(HttpClient, "http", cancellationToken); + } +} + +/// +/// TestProgram with integration services but no dashboard or node app. +/// +/// +/// Use [Collection("IntegrationServices")] to inject this fixture in test constructors. +/// public class IntegrationServicesFixture : TestProgramFixture { public override TestProgram CreateTestProgram() @@ -64,6 +101,9 @@ public override TestProgram CreateTestProgram() .ConfigureHttpClientDefaults(b => { b.UseSocketsHttpHandler((handler, sp) => handler.PooledConnectionLifetime = TimeSpan.FromSeconds(5)); + + // Ensure transient errors are retried. + b.AddStandardResilienceHandler(); }); return testProgram; @@ -75,7 +115,13 @@ public override Task WaitReadyStateAsync(CancellationToken cancellationToken = d } } -public class NodeJsFixture : TestProgramFixture +/// +/// TestProgram with node app but no dashboard or integration services. +/// +/// +/// Use [Collection("NodeApp")] to inject this fixture in test constructors. +/// +public class NodeAppFixture : TestProgramFixture { public override TestProgram CreateTestProgram() { @@ -86,7 +132,6 @@ public override TestProgram CreateTestProgram() .ConfigureHttpClientDefaults(b => { b.UseSocketsHttpHandler((handler, sp) => handler.PooledConnectionLifetime = TimeSpan.FromSeconds(5)); - b.AddStandardResilienceHandler(); }); return testProgram; @@ -98,6 +143,14 @@ public override Task WaitReadyStateAsync(CancellationToken cancellationToken = d } } +[CollectionDefinition("SlimTestProgram")] +public class SlimTestProgramCollection : ICollectionFixture +{ + // This class has no code, and is never created. Its purpose is simply + // to be the place to apply [CollectionDefinition] and all the + // ICollectionFixture<> interfaces. +} + [CollectionDefinition("IntegrationServices")] public class IntegrationServicesCollection : ICollectionFixture { @@ -106,8 +159,8 @@ public class IntegrationServicesCollection : ICollectionFixture interfaces. } -[CollectionDefinition("NodeJs")] -public class NodeJsCollection : ICollectionFixture +[CollectionDefinition("NodeApp")] +public class NodeJsCollection : ICollectionFixture { // This class has no code, and is never created. Its purpose is simply // to be the place to apply [CollectionDefinition] and all the diff --git a/tests/testproject/TestProject.AppHost/TestProgram.cs b/tests/testproject/TestProject.AppHost/TestProgram.cs index f5b93ed8e6..7ea3acdf47 100644 --- a/tests/testproject/TestProject.AppHost/TestProgram.cs +++ b/tests/testproject/TestProject.AppHost/TestProgram.cs @@ -32,12 +32,26 @@ private TestProgram(string[] args, Assembly assembly, bool includeIntegrationSer if (includeIntegrationServices) { - var sqlserver = AppBuilder.AddSqlServerContainer("sqlserver"); - var mysql = AppBuilder.AddMySqlContainer("mysql"); + var sqlserverDbName = "tempdb"; + var sqlserver = AppBuilder.AddSqlServerContainer("sqlserver") + .AddDatabase(sqlserverDbName); + + var mysqlDbName = "mysqldb"; + var mysql = AppBuilder.AddMySqlContainer("mysql") + .WithEnvironment("MYSQL_DATABASE", mysqlDbName) + .AddDatabase(mysqlDbName); + var redis = AppBuilder.AddRedisContainer("redis"); - var postgres = AppBuilder.AddPostgresContainer("postgres"); + + var postgresDbName = "postgresdb"; + var postgres = AppBuilder.AddPostgresContainer("postgres") + .WithEnvironment("POSTGRES_DB", postgresDbName) + .AddDatabase(postgresDbName); + var rabbitmq = AppBuilder.AddRabbitMQContainer("rabbitmq"); - var mongodb = AppBuilder.AddMongoDBContainer("mongodb").AddDatabase("mymongodb"); + + var mongodb = AppBuilder.AddMongoDBContainer("mongodb") + .AddDatabase("mymongodb"); IntegrationServiceABuilder = AppBuilder.AddProject("integrationservicea") .WithReference(sqlserver) diff --git a/tests/testproject/TestProject.IntegrationServiceA/MySql/MySqlExtensions.cs b/tests/testproject/TestProject.IntegrationServiceA/MySql/MySqlExtensions.cs new file mode 100644 index 0000000000..1bb0cf1a6d --- /dev/null +++ b/tests/testproject/TestProject.IntegrationServiceA/MySql/MySqlExtensions.cs @@ -0,0 +1,35 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using MySqlConnector; + +public static class MySqlExtensions +{ + public static void MapMySqlApi(this WebApplication app) + { + app.MapGet("/mysql/verify", VerifyMySqlAsync); + } + + private static async Task VerifyMySqlAsync(MySqlConnection connection) + { + try + { + await connection.OpenAsync(); + + var tableName = $"t" + Guid.NewGuid().ToString("n"); + var command = connection.CreateCommand(); + command.CommandText = $""" + CREATE TABLE {tableName} (x INT); + INSERT INTO {tableName} VALUES (1); + SELECT x FROM {tableName}; + """; + var results = await command.ExecuteReaderAsync(); + + return results.HasRows ? Results.Ok("Success!") : Results.Problem("Failed"); + } + catch (Exception e) + { + return Results.Problem(e.ToString()); + } + } +} diff --git a/tests/testproject/TestProject.IntegrationServiceA/Postgres/PostgresExtensions.cs b/tests/testproject/TestProject.IntegrationServiceA/Postgres/PostgresExtensions.cs new file mode 100644 index 0000000000..5cf432a36e --- /dev/null +++ b/tests/testproject/TestProject.IntegrationServiceA/Postgres/PostgresExtensions.cs @@ -0,0 +1,35 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Npgsql; + +public static class PostgresExtensions +{ + public static void MapPostgresApi(this WebApplication app) + { + app.MapGet("/postgres/verify", VerifyPostgresAsync); + } + + private static async Task VerifyPostgresAsync(NpgsqlConnection connection) + { + try + { + await connection.OpenAsync(); + + var tableName = $"t" + Guid.NewGuid().ToString("n"); + var command = connection.CreateCommand(); + command.CommandText = $""" + CREATE TABLE {tableName} (x INT); + INSERT INTO {tableName} VALUES (1); + SELECT x FROM {tableName}; + """; + var results = await command.ExecuteReaderAsync(); + + return results.HasRows ? Results.Ok("Success!") : Results.Problem("Failed"); + } + catch (Exception e) + { + return Results.Problem(e.ToString()); + } + } +} diff --git a/tests/testproject/TestProject.IntegrationServiceA/Program.cs b/tests/testproject/TestProject.IntegrationServiceA/Program.cs index 7a01282e85..eaf64ec623 100644 --- a/tests/testproject/TestProject.IntegrationServiceA/Program.cs +++ b/tests/testproject/TestProject.IntegrationServiceA/Program.cs @@ -2,10 +2,10 @@ // The .NET Foundation licenses this file to you under the MIT license. var builder = WebApplication.CreateBuilder(args); -builder.AddSqlServerClient("sqlserver"); -builder.AddMySqlDataSource("mysql"); +builder.AddSqlServerClient("tempdb"); +builder.AddMySqlDataSource("mysqldb"); builder.AddRedis("redis"); -builder.AddNpgsqlDataSource("postgres"); +builder.AddNpgsqlDataSource("postgresdb"); builder.AddRabbitMQ("rabbitmq"); builder.AddMongoDBClient("mymongodb"); @@ -21,4 +21,12 @@ app.MapMongoMovieApi(); +app.MapMySqlApi(); + +app.MapPostgresApi(); + +app.MapSqlServerApi(); + +app.MapRabbitMQApi(); + app.Run(); diff --git a/tests/testproject/TestProject.IntegrationServiceA/RabbitMQ/RabbitMQExtensions.cs b/tests/testproject/TestProject.IntegrationServiceA/RabbitMQ/RabbitMQExtensions.cs new file mode 100644 index 0000000000..3170bb78a0 --- /dev/null +++ b/tests/testproject/TestProject.IntegrationServiceA/RabbitMQ/RabbitMQExtensions.cs @@ -0,0 +1,35 @@ +// 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; +using RabbitMQ.Client; + +public static class RabbitMQExtensions +{ + public static void MapRabbitMQApi(this WebApplication app) + { + app.MapGet("/rabbit/verify", VerifyRabbitMQ); + } + + private static IResult VerifyRabbitMQ(IConnection connection) + { + try + { + using var channel = connection.CreateModel(); + const string queueName = "hello"; + channel.QueueDeclare(queue: queueName, durable: false, exclusive: false, autoDelete: false, arguments: null); + + const string message = "Hello World!"; + var body = Encoding.UTF8.GetBytes(message); + + channel.BasicPublish(exchange: string.Empty, routingKey: queueName, basicProperties: null, body: body); + var result = channel.BasicGet(queueName, true); + + return result.Body.Span.SequenceEqual(body) ? Results.Ok("Success!") : Results.Problem("Failed"); + } + catch (Exception e) + { + return Results.Problem(e.ToString()); + } + } +} diff --git a/tests/testproject/TestProject.IntegrationServiceA/SqlServer/SqlServerExtensions.cs b/tests/testproject/TestProject.IntegrationServiceA/SqlServer/SqlServerExtensions.cs new file mode 100644 index 0000000000..bbea67d94d --- /dev/null +++ b/tests/testproject/TestProject.IntegrationServiceA/SqlServer/SqlServerExtensions.cs @@ -0,0 +1,35 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Data.SqlClient; + +public static class SqlServerExtensions +{ + public static void MapSqlServerApi(this WebApplication app) + { + app.MapGet("/sqlserver/verify", VerifySqlServerAsync); + } + + private static async Task VerifySqlServerAsync(SqlConnection connection) + { + try + { + await connection.OpenAsync(); + + var tableName = $"t" + Guid.NewGuid().ToString("n"); + var command = connection.CreateCommand(); + command.CommandText = $""" + CREATE TABLE {tableName} (x INT); + INSERT INTO {tableName} VALUES (1); + SELECT x FROM {tableName}; + """; + var results = await command.ExecuteReaderAsync(); + + return results.HasRows ? Results.Ok("Success!") : Results.Problem("Failed"); + } + catch (Exception e) + { + return Results.Problem(e.ToString()); + } + } +} From bdf78e2f0cbaea03207cb8754a595025450c064d Mon Sep 17 00:00:00 2001 From: Sebastien Ros Date: Mon, 11 Dec 2023 10:27:51 -0800 Subject: [PATCH 09/20] Run functional tests on Linux --- tests/Aspire.Hosting.Tests/Helpers/LocalOnlyFactAttribute.cs | 2 +- tests/Aspire.Hosting.Tests/TestProgramFixture.cs | 5 +++++ tests/testproject/TestProject.AppHost/TestProgram.cs | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/tests/Aspire.Hosting.Tests/Helpers/LocalOnlyFactAttribute.cs b/tests/Aspire.Hosting.Tests/Helpers/LocalOnlyFactAttribute.cs index c376c71d75..a6762312bd 100644 --- a/tests/Aspire.Hosting.Tests/Helpers/LocalOnlyFactAttribute.cs +++ b/tests/Aspire.Hosting.Tests/Helpers/LocalOnlyFactAttribute.cs @@ -14,7 +14,7 @@ public override string Skip { // BUILD_BUILDID is defined by Azure Dev Ops - if (Environment.GetEnvironmentVariable("BUILD_BUILDID") != null) + if (Environment.GetEnvironmentVariable("BUILD_BUILDID") != null && !OperatingSystem.IsLinux()) { return "LocalOnlyFactAttribute tests are not run as part of CI."; } diff --git a/tests/Aspire.Hosting.Tests/TestProgramFixture.cs b/tests/Aspire.Hosting.Tests/TestProgramFixture.cs index b0a711b14d..1916460fcb 100644 --- a/tests/Aspire.Hosting.Tests/TestProgramFixture.cs +++ b/tests/Aspire.Hosting.Tests/TestProgramFixture.cs @@ -28,6 +28,11 @@ public abstract class TestProgramFixture : IAsyncLifetime public async Task InitializeAsync() { + if (Environment.GetEnvironmentVariable("BUILD_BUILDID") != null && !OperatingSystem.IsLinux()) + { + return; + } + _testProgram = CreateTestProgram(); _app = _testProgram.Build(); diff --git a/tests/testproject/TestProject.AppHost/TestProgram.cs b/tests/testproject/TestProject.AppHost/TestProgram.cs index facf592ecf..a70741af9b 100644 --- a/tests/testproject/TestProject.AppHost/TestProgram.cs +++ b/tests/testproject/TestProject.AppHost/TestProgram.cs @@ -35,7 +35,7 @@ private TestProgram(string[] args, Assembly assembly, bool includeIntegrationSer var sqlserverDbName = "tempdb"; var mysqlDbName = "mysqldb"; var postgresDbName = "postgresdb"; - var mongoDbName = "mongodb"; + var mongoDbName = "mymongodb"; var sqlserverContainer = AppBuilder.AddSqlServerContainer("sqlservercontainer") .AddDatabase(sqlserverDbName); From ed4016c6c4acfcbd27c6fcd44e076b9a1186aa08 Mon Sep 17 00:00:00 2001 From: Sebastien Ros Date: Mon, 11 Dec 2023 10:39:45 -0800 Subject: [PATCH 10/20] Make database queries idempotent --- .../MySql/MySqlExtensions.cs | 7 +------ .../Postgres/PostgresExtensions.cs | 7 +------ .../SqlServer/SqlServerExtensions.cs | 7 +------ 3 files changed, 3 insertions(+), 18 deletions(-) diff --git a/tests/testproject/TestProject.IntegrationServiceA/MySql/MySqlExtensions.cs b/tests/testproject/TestProject.IntegrationServiceA/MySql/MySqlExtensions.cs index 1bb0cf1a6d..eeae11fd47 100644 --- a/tests/testproject/TestProject.IntegrationServiceA/MySql/MySqlExtensions.cs +++ b/tests/testproject/TestProject.IntegrationServiceA/MySql/MySqlExtensions.cs @@ -16,13 +16,8 @@ private static async Task VerifyMySqlAsync(MySqlConnection connection) { await connection.OpenAsync(); - var tableName = $"t" + Guid.NewGuid().ToString("n"); var command = connection.CreateCommand(); - command.CommandText = $""" - CREATE TABLE {tableName} (x INT); - INSERT INTO {tableName} VALUES (1); - SELECT x FROM {tableName}; - """; + command.CommandText = $"SELECT 1"; var results = await command.ExecuteReaderAsync(); return results.HasRows ? Results.Ok("Success!") : Results.Problem("Failed"); diff --git a/tests/testproject/TestProject.IntegrationServiceA/Postgres/PostgresExtensions.cs b/tests/testproject/TestProject.IntegrationServiceA/Postgres/PostgresExtensions.cs index 5cf432a36e..449c650440 100644 --- a/tests/testproject/TestProject.IntegrationServiceA/Postgres/PostgresExtensions.cs +++ b/tests/testproject/TestProject.IntegrationServiceA/Postgres/PostgresExtensions.cs @@ -16,13 +16,8 @@ private static async Task VerifyPostgresAsync(NpgsqlConnection connecti { await connection.OpenAsync(); - var tableName = $"t" + Guid.NewGuid().ToString("n"); var command = connection.CreateCommand(); - command.CommandText = $""" - CREATE TABLE {tableName} (x INT); - INSERT INTO {tableName} VALUES (1); - SELECT x FROM {tableName}; - """; + command.CommandText = $"SELECT 1"; var results = await command.ExecuteReaderAsync(); return results.HasRows ? Results.Ok("Success!") : Results.Problem("Failed"); diff --git a/tests/testproject/TestProject.IntegrationServiceA/SqlServer/SqlServerExtensions.cs b/tests/testproject/TestProject.IntegrationServiceA/SqlServer/SqlServerExtensions.cs index bbea67d94d..565263be09 100644 --- a/tests/testproject/TestProject.IntegrationServiceA/SqlServer/SqlServerExtensions.cs +++ b/tests/testproject/TestProject.IntegrationServiceA/SqlServer/SqlServerExtensions.cs @@ -16,13 +16,8 @@ private static async Task VerifySqlServerAsync(SqlConnection connection { await connection.OpenAsync(); - var tableName = $"t" + Guid.NewGuid().ToString("n"); var command = connection.CreateCommand(); - command.CommandText = $""" - CREATE TABLE {tableName} (x INT); - INSERT INTO {tableName} VALUES (1); - SELECT x FROM {tableName}; - """; + command.CommandText = $"SELECT 1"; var results = await command.ExecuteReaderAsync(); return results.HasRows ? Results.Ok("Success!") : Results.Problem("Failed"); From 6c6ffa0217acc0dc64e38919012ab9947f7068a0 Mon Sep 17 00:00:00 2001 From: Sebastien Ros Date: Mon, 11 Dec 2023 10:45:18 -0800 Subject: [PATCH 11/20] Disable runs on Linux CI --- tests/Aspire.Hosting.Tests/Helpers/LocalOnlyFactAttribute.cs | 2 +- tests/Aspire.Hosting.Tests/TestProgramFixture.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Aspire.Hosting.Tests/Helpers/LocalOnlyFactAttribute.cs b/tests/Aspire.Hosting.Tests/Helpers/LocalOnlyFactAttribute.cs index a6762312bd..c376c71d75 100644 --- a/tests/Aspire.Hosting.Tests/Helpers/LocalOnlyFactAttribute.cs +++ b/tests/Aspire.Hosting.Tests/Helpers/LocalOnlyFactAttribute.cs @@ -14,7 +14,7 @@ public override string Skip { // BUILD_BUILDID is defined by Azure Dev Ops - if (Environment.GetEnvironmentVariable("BUILD_BUILDID") != null && !OperatingSystem.IsLinux()) + if (Environment.GetEnvironmentVariable("BUILD_BUILDID") != null) { return "LocalOnlyFactAttribute tests are not run as part of CI."; } diff --git a/tests/Aspire.Hosting.Tests/TestProgramFixture.cs b/tests/Aspire.Hosting.Tests/TestProgramFixture.cs index 1916460fcb..1b41449ec2 100644 --- a/tests/Aspire.Hosting.Tests/TestProgramFixture.cs +++ b/tests/Aspire.Hosting.Tests/TestProgramFixture.cs @@ -28,7 +28,7 @@ public abstract class TestProgramFixture : IAsyncLifetime public async Task InitializeAsync() { - if (Environment.GetEnvironmentVariable("BUILD_BUILDID") != null && !OperatingSystem.IsLinux()) + if (Environment.GetEnvironmentVariable("BUILD_BUILDID") != null) { return; } From 6eb7a46b98095e1c02ac1f8a5ed70e1cbb256a40 Mon Sep 17 00:00:00 2001 From: Sebastien Ros Date: Mon, 11 Dec 2023 10:47:15 -0800 Subject: [PATCH 12/20] Remove unrelated loop comments --- .../Helpers/AllocatedEndpointAnnotationTestExtensions.cs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/tests/Aspire.Hosting.Tests/Helpers/AllocatedEndpointAnnotationTestExtensions.cs b/tests/Aspire.Hosting.Tests/Helpers/AllocatedEndpointAnnotationTestExtensions.cs index 37363698f3..4e8aacecd6 100644 --- a/tests/Aspire.Hosting.Tests/Helpers/AllocatedEndpointAnnotationTestExtensions.cs +++ b/tests/Aspire.Hosting.Tests/Helpers/AllocatedEndpointAnnotationTestExtensions.cs @@ -18,8 +18,6 @@ public static class AllocatedEndpointAnnotationTestExtensions public static async Task HttpGetStringAsync(this IResourceBuilder builder, HttpClient client, string bindingName, string path, CancellationToken cancellationToken) where T : IResourceWithBindings { - // We have to get the allocated endpoint each time through the loop - // because it may not be populated yet by the time we get here. var allocatedEndpoint = builder.Resource.Annotations.OfType().Single(a => a.Name == bindingName); var url = $"{allocatedEndpoint.UriString}{path}"; @@ -40,8 +38,6 @@ public static async Task HttpGetStringAsync(this IResourceBuilder public static async Task HttpGetAsync(this IResourceBuilder builder, HttpClient client, string bindingName, string path, CancellationToken cancellationToken) where T : IResourceWithBindings { - // We have to get the allocated endpoint each time through the loop - // because it may not be populated yet by the time we get here. var allocatedEndpoint = builder.Resource.Annotations.OfType().Single(a => a.Name == bindingName); var url = $"{allocatedEndpoint.UriString}{path}"; @@ -63,8 +59,6 @@ public static async Task HttpGetAsync(this IResourceBuil public static async Task HttpPostAsync(this IResourceBuilder builder, HttpClient client, string bindingName, string path, HttpContent? content, CancellationToken cancellationToken) where T : IResourceWithBindings { - // We have to get the allocated endpoint each time through the loop - // because it may not be populated yet by the time we get here. var allocatedEndpoint = builder.Resource.Annotations.OfType().Single(a => a.Name == bindingName); var url = $"{allocatedEndpoint.UriString}{path}"; From 33dc3b6943402c9fe4a9528f2ec465a9e4464f2c Mon Sep 17 00:00:00 2001 From: Sebastien Ros Date: Mon, 11 Dec 2023 15:17:01 -0800 Subject: [PATCH 13/20] Simplify MongoDB tests --- .../MongoDB/MongoDBFunctionalTests.cs | 20 +------- .../TestProject.AppHost/TestProgram.cs | 1 + .../MongoDB/MongoDBExtensions.cs | 50 +++++++++---------- .../Program.cs | 2 +- 4 files changed, 28 insertions(+), 45 deletions(-) diff --git a/tests/Aspire.Hosting.Tests/MongoDB/MongoDBFunctionalTests.cs b/tests/Aspire.Hosting.Tests/MongoDB/MongoDBFunctionalTests.cs index f91b89c829..9ea180bd4d 100644 --- a/tests/Aspire.Hosting.Tests/MongoDB/MongoDBFunctionalTests.cs +++ b/tests/Aspire.Hosting.Tests/MongoDB/MongoDBFunctionalTests.cs @@ -1,7 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Net.Http.Json; using Aspire.Hosting.Tests.Helpers; using Xunit; @@ -17,20 +16,6 @@ public MongoDBFunctionalTests(IntegrationServicesFixture integrationServicesFixt _integrationServicesFixture = integrationServicesFixture; } - [LocalOnlyFact()] - public async Task DatabaseIsCreatedOnDemand() - { - var testProgram = _integrationServicesFixture.TestProgram; - var client = _integrationServicesFixture.HttpClient; - - using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(1)); - - var response = await testProgram.IntegrationServiceABuilder!.HttpGetAsync(client, "http", "/mongodb/databases", cts.Token); - var databases = await response.Content.ReadFromJsonAsync(cts.Token); - - Assert.Equivalent(new[] { "admin", "config", "local", "mymongodb" }, databases); - } - [LocalOnlyFact()] public async Task VerifyMongoWorks() { @@ -39,9 +24,8 @@ public async Task VerifyMongoWorks() using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(1)); - var response = await testProgram.IntegrationServiceABuilder!.HttpGetAsync(client, "http", "/mongodb/movies", cts.Token); - var movies = await response.Content.ReadFromJsonAsync(cts.Token); + var response = await testProgram.IntegrationServiceABuilder!.HttpGetAsync(client, "http", "/mongodb/verify", cts.Token); - Assert.Equivalent(new[] { "Rocky I", "Rocky II" }, movies); + Assert.True(response.IsSuccessStatusCode); } } diff --git a/tests/testproject/TestProject.AppHost/TestProgram.cs b/tests/testproject/TestProject.AppHost/TestProgram.cs index a70741af9b..b5f1a20e71 100644 --- a/tests/testproject/TestProject.AppHost/TestProgram.cs +++ b/tests/testproject/TestProject.AppHost/TestProgram.cs @@ -49,6 +49,7 @@ private TestProgram(string[] args, Assembly assembly, bool includeIntegrationSer var rabbitmqContainer = AppBuilder.AddRabbitMQContainer("rabbitmqcontainer"); var mongodbContainer = AppBuilder.AddMongoDBContainer("mongodbcontainer") .AddDatabase(mongoDbName); + var sqlserverAbstract = AppBuilder.AddSqlServerContainer("sqlserverabstract"); var mysqlAbstract = AppBuilder.AddMySqlContainer("mysqlabstract"); var redisAbstract = AppBuilder.AddRedisContainer("redisabstract"); diff --git a/tests/testproject/TestProject.IntegrationServiceA/MongoDB/MongoDBExtensions.cs b/tests/testproject/TestProject.IntegrationServiceA/MongoDB/MongoDBExtensions.cs index 8def847bff..a52b94e4f6 100644 --- a/tests/testproject/TestProject.IntegrationServiceA/MongoDB/MongoDBExtensions.cs +++ b/tests/testproject/TestProject.IntegrationServiceA/MongoDB/MongoDBExtensions.cs @@ -5,35 +5,33 @@ public static class MongoDBExtensions { - public static void MapMongoMovieApi(this WebApplication app) + public static void MapMongoDBApi(this WebApplication app) { - app.MapGet("/mongodb/databases", GetDatabaseNamesAsync); - - app.MapGet("/mongodb/movies", GetMoviesAsync); - } - - private static async Task> GetDatabaseNamesAsync(IMongoClient client, IMongoDatabase db) - { - // Ensure the database is created - var randomCollection = db.GetCollection("random"); - await randomCollection.DeleteManyAsync(x => true); - randomCollection.InsertOne(new Movie(1, "123")); - - var databaseNames = new List(); - - await client.ListDatabaseNames().ForEachAsync(databaseNames.Add); - - return databaseNames; + app.MapGet("/mongodb/verify", VerifyMongoDBAsync); } - private static async Task> GetMoviesAsync(IMongoDatabase db) + private static async Task VerifyMongoDBAsync(IMongoDatabase db) { - db.CreateCollection("movies"); - var moviesCollection = db.GetCollection("movies"); - await moviesCollection.DeleteManyAsync(x => true); - await moviesCollection.InsertOneAsync(new Movie(1, "Rocky I")); - await moviesCollection.InsertOneAsync(new Movie(2, "Rocky II")); - - return (await moviesCollection.Find(x => true).ToListAsync()).Select(x => x.Name).ToList(); + try + { + // Use a random collection to make the test idempotent + + var collectionName = Guid.NewGuid().ToString("N"); + db.CreateCollection(collectionName); + var moviesCollection = db.GetCollection(collectionName); + await moviesCollection.InsertOneAsync(new Movie(1, "Rocky I")); + await moviesCollection.InsertOneAsync(new Movie(2, "Rocky II")); + + var moviesCount = (await moviesCollection.Find(x => true).ToListAsync()).Count; + + return moviesCount == 2 ? Results.Ok("Success!") : Results.Problem("Failed"); + } + catch (Exception e) + { + return Results.Problem(e.ToString()); + } + + + } } diff --git a/tests/testproject/TestProject.IntegrationServiceA/Program.cs b/tests/testproject/TestProject.IntegrationServiceA/Program.cs index 766af14f62..b809f3e7d8 100644 --- a/tests/testproject/TestProject.IntegrationServiceA/Program.cs +++ b/tests/testproject/TestProject.IntegrationServiceA/Program.cs @@ -26,7 +26,7 @@ app.MapRedisApi(); -app.MapMongoMovieApi(); +app.MapMongoDBApi(); app.MapMySqlApi(); From 3a5c29b06503de0ac9ed422892fb9bc1ee6a1b89 Mon Sep 17 00:00:00 2001 From: Sebastien Ros Date: Mon, 11 Dec 2023 15:17:27 -0800 Subject: [PATCH 14/20] Test functional tests on windows --- tests/Aspire.Hosting.Tests/Helpers/LocalOnlyFactAttribute.cs | 2 +- tests/Aspire.Hosting.Tests/TestProgramFixture.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Aspire.Hosting.Tests/Helpers/LocalOnlyFactAttribute.cs b/tests/Aspire.Hosting.Tests/Helpers/LocalOnlyFactAttribute.cs index c376c71d75..3f295eb991 100644 --- a/tests/Aspire.Hosting.Tests/Helpers/LocalOnlyFactAttribute.cs +++ b/tests/Aspire.Hosting.Tests/Helpers/LocalOnlyFactAttribute.cs @@ -14,7 +14,7 @@ public override string Skip { // BUILD_BUILDID is defined by Azure Dev Ops - if (Environment.GetEnvironmentVariable("BUILD_BUILDID") != null) + if (Environment.GetEnvironmentVariable("BUILD_BUILDID") != null || OperatingSystem.IsWindows()) { return "LocalOnlyFactAttribute tests are not run as part of CI."; } diff --git a/tests/Aspire.Hosting.Tests/TestProgramFixture.cs b/tests/Aspire.Hosting.Tests/TestProgramFixture.cs index 1b41449ec2..e2f5d2beb7 100644 --- a/tests/Aspire.Hosting.Tests/TestProgramFixture.cs +++ b/tests/Aspire.Hosting.Tests/TestProgramFixture.cs @@ -28,7 +28,7 @@ public abstract class TestProgramFixture : IAsyncLifetime public async Task InitializeAsync() { - if (Environment.GetEnvironmentVariable("BUILD_BUILDID") != null) + if (Environment.GetEnvironmentVariable("BUILD_BUILDID") != null || OperatingSystem.IsWindows()) { return; } From 950d2fe8a567e360fbc60940733f9bab19a6c2e1 Mon Sep 17 00:00:00 2001 From: Sebastien Ros Date: Mon, 11 Dec 2023 15:44:30 -0800 Subject: [PATCH 15/20] Bool logic is hard --- tests/Aspire.Hosting.Tests/Helpers/LocalOnlyFactAttribute.cs | 2 +- tests/Aspire.Hosting.Tests/TestProgramFixture.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Aspire.Hosting.Tests/Helpers/LocalOnlyFactAttribute.cs b/tests/Aspire.Hosting.Tests/Helpers/LocalOnlyFactAttribute.cs index 3f295eb991..51f629f8e3 100644 --- a/tests/Aspire.Hosting.Tests/Helpers/LocalOnlyFactAttribute.cs +++ b/tests/Aspire.Hosting.Tests/Helpers/LocalOnlyFactAttribute.cs @@ -14,7 +14,7 @@ public override string Skip { // BUILD_BUILDID is defined by Azure Dev Ops - if (Environment.GetEnvironmentVariable("BUILD_BUILDID") != null || OperatingSystem.IsWindows()) + if (Environment.GetEnvironmentVariable("BUILD_BUILDID") != null && OperatingSystem.IsLinux()) { return "LocalOnlyFactAttribute tests are not run as part of CI."; } diff --git a/tests/Aspire.Hosting.Tests/TestProgramFixture.cs b/tests/Aspire.Hosting.Tests/TestProgramFixture.cs index e2f5d2beb7..bff45e95af 100644 --- a/tests/Aspire.Hosting.Tests/TestProgramFixture.cs +++ b/tests/Aspire.Hosting.Tests/TestProgramFixture.cs @@ -28,7 +28,7 @@ public abstract class TestProgramFixture : IAsyncLifetime public async Task InitializeAsync() { - if (Environment.GetEnvironmentVariable("BUILD_BUILDID") != null || OperatingSystem.IsWindows()) + if (Environment.GetEnvironmentVariable("BUILD_BUILDID") != null && OperatingSystem.IsLinux()) { return; } From 6ba5814e766540314b81013e9ce3c933e0f9e56c Mon Sep 17 00:00:00 2001 From: Sebastien Ros Date: Mon, 11 Dec 2023 16:45:26 -0800 Subject: [PATCH 16/20] Limit Console usage that would slow down TestServicesWithMultipleReplicas --- tests/Aspire.Hosting.Tests/DistributedApplicationTests.cs | 2 ++ .../Helpers/AllocatedEndpointAnnotationTestExtensions.cs | 4 ---- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/Aspire.Hosting.Tests/DistributedApplicationTests.cs b/tests/Aspire.Hosting.Tests/DistributedApplicationTests.cs index 6330f8db4f..61e83dd1fd 100644 --- a/tests/Aspire.Hosting.Tests/DistributedApplicationTests.cs +++ b/tests/Aspire.Hosting.Tests/DistributedApplicationTests.cs @@ -211,6 +211,8 @@ public async Task TestServicesWithMultipleReplicas() await app.StartAsync(cts.Token); + await Task.Delay(1000, cts.Token); + // Make sure services A and C are running await testProgram.ServiceABuilder.HttpGetPidAsync(client, "http", cts.Token); await testProgram.ServiceCBuilder.HttpGetPidAsync(client, "http", cts.Token); diff --git a/tests/Aspire.Hosting.Tests/Helpers/AllocatedEndpointAnnotationTestExtensions.cs b/tests/Aspire.Hosting.Tests/Helpers/AllocatedEndpointAnnotationTestExtensions.cs index 4e8aacecd6..d6bf624230 100644 --- a/tests/Aspire.Hosting.Tests/Helpers/AllocatedEndpointAnnotationTestExtensions.cs +++ b/tests/Aspire.Hosting.Tests/Helpers/AllocatedEndpointAnnotationTestExtensions.cs @@ -86,10 +86,6 @@ public static async Task HttpGetStringWithRetryAsync(this IResourceBu { return await builder.HttpGetStringAsync(client, bindingName, request, cancellationToken); } - catch (HttpRequestException ex) - { - Console.WriteLine(ex); - } catch { await Task.Delay(100, cancellationToken); From 71a8ec3a36ce0787ed6e9dd054d6d1f0e6ab3e61 Mon Sep 17 00:00:00 2001 From: Sebastien Ros Date: Wed, 13 Dec 2023 11:25:33 -0800 Subject: [PATCH 17/20] Don't run on CI for now --- tests/Aspire.Hosting.Tests/Helpers/LocalOnlyFactAttribute.cs | 2 +- tests/Aspire.Hosting.Tests/TestProgramFixture.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Aspire.Hosting.Tests/Helpers/LocalOnlyFactAttribute.cs b/tests/Aspire.Hosting.Tests/Helpers/LocalOnlyFactAttribute.cs index 51f629f8e3..cc031820b2 100644 --- a/tests/Aspire.Hosting.Tests/Helpers/LocalOnlyFactAttribute.cs +++ b/tests/Aspire.Hosting.Tests/Helpers/LocalOnlyFactAttribute.cs @@ -14,7 +14,7 @@ public override string Skip { // BUILD_BUILDID is defined by Azure Dev Ops - if (Environment.GetEnvironmentVariable("BUILD_BUILDID") != null && OperatingSystem.IsLinux()) + if (Environment.GetEnvironmentVariable("BUILD_BUILDID")) { return "LocalOnlyFactAttribute tests are not run as part of CI."; } diff --git a/tests/Aspire.Hosting.Tests/TestProgramFixture.cs b/tests/Aspire.Hosting.Tests/TestProgramFixture.cs index bff45e95af..45cbfce453 100644 --- a/tests/Aspire.Hosting.Tests/TestProgramFixture.cs +++ b/tests/Aspire.Hosting.Tests/TestProgramFixture.cs @@ -28,7 +28,7 @@ public abstract class TestProgramFixture : IAsyncLifetime public async Task InitializeAsync() { - if (Environment.GetEnvironmentVariable("BUILD_BUILDID") != null && OperatingSystem.IsLinux()) + if (Environment.GetEnvironmentVariable("BUILD_BUILDID")) { return; } From df7c3c739c158fa16ecf9041f38290c218b32a48 Mon Sep 17 00:00:00 2001 From: Sebastien Ros Date: Wed, 13 Dec 2023 11:31:38 -0800 Subject: [PATCH 18/20] Fix build --- tests/Aspire.Hosting.Tests/Helpers/LocalOnlyFactAttribute.cs | 2 +- tests/Aspire.Hosting.Tests/TestProgramFixture.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Aspire.Hosting.Tests/Helpers/LocalOnlyFactAttribute.cs b/tests/Aspire.Hosting.Tests/Helpers/LocalOnlyFactAttribute.cs index cc031820b2..c376c71d75 100644 --- a/tests/Aspire.Hosting.Tests/Helpers/LocalOnlyFactAttribute.cs +++ b/tests/Aspire.Hosting.Tests/Helpers/LocalOnlyFactAttribute.cs @@ -14,7 +14,7 @@ public override string Skip { // BUILD_BUILDID is defined by Azure Dev Ops - if (Environment.GetEnvironmentVariable("BUILD_BUILDID")) + if (Environment.GetEnvironmentVariable("BUILD_BUILDID") != null) { return "LocalOnlyFactAttribute tests are not run as part of CI."; } diff --git a/tests/Aspire.Hosting.Tests/TestProgramFixture.cs b/tests/Aspire.Hosting.Tests/TestProgramFixture.cs index 45cbfce453..1b41449ec2 100644 --- a/tests/Aspire.Hosting.Tests/TestProgramFixture.cs +++ b/tests/Aspire.Hosting.Tests/TestProgramFixture.cs @@ -28,7 +28,7 @@ public abstract class TestProgramFixture : IAsyncLifetime public async Task InitializeAsync() { - if (Environment.GetEnvironmentVariable("BUILD_BUILDID")) + if (Environment.GetEnvironmentVariable("BUILD_BUILDID") != null) { return; } From 24c71051207fcc1a12b9956d0faa7d5b4990c752 Mon Sep 17 00:00:00 2001 From: Sebastien Ros Date: Wed, 13 Dec 2023 14:58:25 -0800 Subject: [PATCH 19/20] Add code comment --- tests/Aspire.Hosting.Tests/DistributedApplicationTests.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/Aspire.Hosting.Tests/DistributedApplicationTests.cs b/tests/Aspire.Hosting.Tests/DistributedApplicationTests.cs index 61e83dd1fd..e43f6553d5 100644 --- a/tests/Aspire.Hosting.Tests/DistributedApplicationTests.cs +++ b/tests/Aspire.Hosting.Tests/DistributedApplicationTests.cs @@ -211,6 +211,9 @@ public async Task TestServicesWithMultipleReplicas() await app.StartAsync(cts.Token); + // Give the server some time to be ready to handle requests to + // minimize the amount of retries the clients have to do (and log). + await Task.Delay(1000, cts.Token); // Make sure services A and C are running From b6ce5470b378d98931ea94eebe7701640e87080e Mon Sep 17 00:00:00 2001 From: Sebastien Ros Date: Wed, 13 Dec 2023 14:58:33 -0800 Subject: [PATCH 20/20] Log problem details --- .../MongoDB/MongoDBFunctionalTests.cs | 3 ++- .../MySql/MySqlFunctionalTests.cs | 3 ++- .../Postgres/PostgresFunctionalTests.cs | 3 ++- .../RabbitMQ/RabbitMQFunctionalTests.cs | 3 ++- .../Redis/RedisFunctionalTests.cs | 8 +++--- .../Redis/RedisExtensions.cs | 27 ++++++++++--------- 6 files changed, 25 insertions(+), 22 deletions(-) diff --git a/tests/Aspire.Hosting.Tests/MongoDB/MongoDBFunctionalTests.cs b/tests/Aspire.Hosting.Tests/MongoDB/MongoDBFunctionalTests.cs index 9ea180bd4d..125a1570a0 100644 --- a/tests/Aspire.Hosting.Tests/MongoDB/MongoDBFunctionalTests.cs +++ b/tests/Aspire.Hosting.Tests/MongoDB/MongoDBFunctionalTests.cs @@ -25,7 +25,8 @@ public async Task VerifyMongoWorks() using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(1)); var response = await testProgram.IntegrationServiceABuilder!.HttpGetAsync(client, "http", "/mongodb/verify", cts.Token); + var responseContent = await response.Content.ReadAsStringAsync(); - Assert.True(response.IsSuccessStatusCode); + Assert.True(response.IsSuccessStatusCode, responseContent); } } diff --git a/tests/Aspire.Hosting.Tests/MySql/MySqlFunctionalTests.cs b/tests/Aspire.Hosting.Tests/MySql/MySqlFunctionalTests.cs index d9623d6652..a61d177781 100644 --- a/tests/Aspire.Hosting.Tests/MySql/MySqlFunctionalTests.cs +++ b/tests/Aspire.Hosting.Tests/MySql/MySqlFunctionalTests.cs @@ -28,7 +28,8 @@ public async Task VerifyMySqlWorks() using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(1)); var response = await testProgram.IntegrationServiceABuilder!.HttpGetAsync(client, "http", "/mysql/verify", cts.Token); + var responseContent = await response.Content.ReadAsStringAsync(); - Assert.True(response.IsSuccessStatusCode); + Assert.True(response.IsSuccessStatusCode, responseContent); } } diff --git a/tests/Aspire.Hosting.Tests/Postgres/PostgresFunctionalTests.cs b/tests/Aspire.Hosting.Tests/Postgres/PostgresFunctionalTests.cs index d9ed300659..27e83a0461 100644 --- a/tests/Aspire.Hosting.Tests/Postgres/PostgresFunctionalTests.cs +++ b/tests/Aspire.Hosting.Tests/Postgres/PostgresFunctionalTests.cs @@ -25,7 +25,8 @@ public async Task VerifyPostgresWorks() using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(1)); var response = await testProgram.IntegrationServiceABuilder!.HttpGetAsync(client, "http", "/postgres/verify", cts.Token); + var responseContent = await response.Content.ReadAsStringAsync(); - Assert.True(response.IsSuccessStatusCode); + Assert.True(response.IsSuccessStatusCode, responseContent); } } diff --git a/tests/Aspire.Hosting.Tests/RabbitMQ/RabbitMQFunctionalTests.cs b/tests/Aspire.Hosting.Tests/RabbitMQ/RabbitMQFunctionalTests.cs index 85833ec308..de1182ccf8 100644 --- a/tests/Aspire.Hosting.Tests/RabbitMQ/RabbitMQFunctionalTests.cs +++ b/tests/Aspire.Hosting.Tests/RabbitMQ/RabbitMQFunctionalTests.cs @@ -25,7 +25,8 @@ public async Task VerifyRabbitMQWorks() using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(1)); var response = await testProgram.IntegrationServiceABuilder!.HttpGetAsync(client, "http", "/rabbit/verify", cts.Token); + var responseContent = await response.Content.ReadAsStringAsync(); - Assert.True(response.IsSuccessStatusCode); + Assert.True(response.IsSuccessStatusCode, responseContent); } } diff --git a/tests/Aspire.Hosting.Tests/Redis/RedisFunctionalTests.cs b/tests/Aspire.Hosting.Tests/Redis/RedisFunctionalTests.cs index b8a70d283e..0c1a292d7b 100644 --- a/tests/Aspire.Hosting.Tests/Redis/RedisFunctionalTests.cs +++ b/tests/Aspire.Hosting.Tests/Redis/RedisFunctionalTests.cs @@ -21,14 +21,12 @@ public async Task VerifyRedisWorks() { var testProgram = _integrationServicesFixture.TestProgram; var client = _integrationServicesFixture.HttpClient; - var data = "Hello World!"; using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(1)); - await testProgram.IntegrationServiceABuilder!.HttpPostAsync(client, "http", "/redis/hello", new StringContent(data), cts.Token); - var response = await testProgram.IntegrationServiceABuilder!.HttpGetAsync(client, "http", "/redis/hello", cts.Token); - var content = await response.Content.ReadAsStringAsync(); + var response = await testProgram.IntegrationServiceABuilder!.HttpGetAsync(client, "http", "/redis/verify", cts.Token); + var responseContent = await response.Content.ReadAsStringAsync(); - Assert.Equal(data, content); + Assert.True(response.IsSuccessStatusCode, responseContent); } } diff --git a/tests/testproject/TestProject.IntegrationServiceA/Redis/RedisExtensions.cs b/tests/testproject/TestProject.IntegrationServiceA/Redis/RedisExtensions.cs index 3216bff182..aa00aa9082 100644 --- a/tests/testproject/TestProject.IntegrationServiceA/Redis/RedisExtensions.cs +++ b/tests/testproject/TestProject.IntegrationServiceA/Redis/RedisExtensions.cs @@ -7,24 +7,25 @@ public static class RedisExtensions { public static void MapRedisApi(this WebApplication app) { - app.MapPost("/redis/{key}", SetKeyAsync); - - app.MapGet("/redis/{key}", GetKeyAsync); + app.MapGet("/redis/verify", VerifyRedisAsync); } - private static async Task SetKeyAsync(string key, HttpContext context, IConnectionMultiplexer cm) + private static async Task VerifyRedisAsync(IConnectionMultiplexer cm) { - using var sr = new StreamReader(context.Request.Body); - var body = await sr.ReadToEndAsync(); + try + { + var key = "somekey"; + var content = "somecontent"; var database = cm.GetDatabase(); - await database.StringSetAsync(key, body); - return Results.Ok(); - } + await database.StringSetAsync(key, content); + var data = await database.StringGetAsync(key); - private static async Task GetKeyAsync(string key, IConnectionMultiplexer cm) - { - var database = cm.GetDatabase(); - return Results.Content(await database.StringGetAsync(key)); + return data == content ? Results.Ok("Success!") : Results.Problem("Failed"); + } + catch (Exception e) + { + return Results.Problem(e.ToString()); + } } }