Skip to content

Commit

Permalink
Extract Aspire.Hosting.NodeJs.Tests project
Browse files Browse the repository at this point in the history
Extract the main NodeJs tests from Aspire.Hosting.Tests and the end-to-end TestProject.

Aspire.Hosting.Tests still uses NodeJs in a few places as an example "Executable" resource. Since NodeJs is just being used as an example - and not testing NodeJs itself, I decided to leave Aspire.Hosting.Tests using it.

Also fix an issue with the RequiresTools attribute when the executable isn't an ".exe" extension - like npm is on Windows - it is `npm.cmd`.

Contributes to dotnet#4294
  • Loading branch information
eerhardt committed Aug 1, 2024
1 parent 8afad96 commit bea7f83
Show file tree
Hide file tree
Showing 16 changed files with 264 additions and 153 deletions.
22 changes: 9 additions & 13 deletions Aspire.sln
Original file line number Diff line number Diff line change
Expand Up @@ -512,6 +512,7 @@ EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Aspire.Hosting.Redis.Tests", "tests\Aspire.Hosting.Redis.Tests\Aspire.Hosting.Redis.Tests.csproj", "{1BC02557-B78B-48CE-9D3C-488A6B7672F4}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aspire.Hosting.Garnet.Tests", "tests\Aspire.Hosting.Garnet.Tests\Aspire.Hosting.Garnet.Tests.csproj", "{CAA4A93F-6BEB-42EB-8680-C1CF72928023}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Aspire.Hosting.PostgreSQL.Tests", "tests\Aspire.Hosting.PostgreSQL.Tests\Aspire.Hosting.PostgreSQL.Tests.csproj", "{7425E5B2-BC47-4521-AC40-B8CECA329E08}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Aspire.Hosting.Qdrant.Tests", "tests\Aspire.Hosting.Qdrant.Tests\Aspire.Hosting.Qdrant.Tests.csproj", "{8E2AA85E-C351-47B4-AF91-58557FAD5840}"
Expand Down Expand Up @@ -543,6 +544,7 @@ EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Aspire.Keycloak.Authentication.Tests", "tests\Aspire.Keycloak.Authentication.Tests\Aspire.Keycloak.Authentication.Tests.csproj", "{48FF09E9-7D33-4A3F-9FF2-4C43A219C7B7}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aspire.Hosting.Nats.Tests", "tests\Aspire.Hosting.Nats.Tests\Aspire.Hosting.Nats.Tests.csproj", "{F492357C-682E-4CBB-A374-1A124B3976A3}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Aspire.Hosting.Azure.Tests", "tests\Aspire.Hosting.Azure.Tests\Aspire.Hosting.Azure.Tests.csproj", "{8691F993-7B19-496E-B8E1-EF1199ACF2E1}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestShop.AppHost", "playground\TestShop\TestShop.AppHost\TestShop.AppHost.csproj", "{DB3E1AD8-87F6-414D-B46F-A0DC334AECCD}"
Expand All @@ -555,6 +557,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SignalR.AppHost", "playgrou
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebPubSub.AppHost", "playground\webpubsub\WebPubSub.AppHost\WebPubSub.AppHost.csproj", "{1419BDCB-47EB-43EB-9149-C935B7208A72}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aspire.Hosting.NodeJs.Tests", "tests\Aspire.Hosting.NodeJs.Tests\Aspire.Hosting.NodeJs.Tests.csproj", "{50450FBB-CD10-4281-B22C-7FF86CEE9D9F}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -1377,14 +1381,6 @@ Global
{CAA4A93F-6BEB-42EB-8680-C1CF72928023}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CAA4A93F-6BEB-42EB-8680-C1CF72928023}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CAA4A93F-6BEB-42EB-8680-C1CF72928023}.Release|Any CPU.Build.0 = Release|Any CPU
{8E2AA85E-C351-47B4-AF91-58557FAD5840}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8E2AA85E-C351-47B4-AF91-58557FAD5840}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8E2AA85E-C351-47B4-AF91-58557FAD5840}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8E2AA85E-C351-47B4-AF91-58557FAD5840}.Release|Any CPU.Build.0 = Release|Any CPU
{986886B7-0E38-4890-92C3-5B46DE322DAF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{986886B7-0E38-4890-92C3-5B46DE322DAF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{986886B7-0E38-4890-92C3-5B46DE322DAF}.Release|Any CPU.ActiveCfg = Release|Any CPU
{986886B7-0E38-4890-92C3-5B46DE322DAF}.Release|Any CPU.Build.0 = Release|Any CPU
{7425E5B2-BC47-4521-AC40-B8CECA329E08}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7425E5B2-BC47-4521-AC40-B8CECA329E08}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7425E5B2-BC47-4521-AC40-B8CECA329E08}.Release|Any CPU.ActiveCfg = Release|Any CPU
Expand Down Expand Up @@ -1469,6 +1465,10 @@ Global
{1419BDCB-47EB-43EB-9149-C935B7208A72}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1419BDCB-47EB-43EB-9149-C935B7208A72}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1419BDCB-47EB-43EB-9149-C935B7208A72}.Release|Any CPU.Build.0 = Release|Any CPU
{50450FBB-CD10-4281-B22C-7FF86CEE9D9F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{50450FBB-CD10-4281-B22C-7FF86CEE9D9F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{50450FBB-CD10-4281-B22C-7FF86CEE9D9F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{50450FBB-CD10-4281-B22C-7FF86CEE9D9F}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -1698,9 +1698,6 @@ Global
{9FAE1602-2C69-4D24-8655-A164489441E8} = {C424395C-1235-41A4-BF55-07880A04368C}
{DF00FDA3-D3EC-4E07-B4EC-0EBB57A813A4} = {77CFE74A-32EE-400C-8930-5025E8555256}
{5CB63205-24F4-4388-A41B-BAF3BEA59866} = {B80354C7-BE58-43F6-8928-9F3A74AB7F47}
{588CD2D7-EE70-43C1-8233-330854BDF53C} = {4981B3A5-4AFD-4191-BF7D-8692D9783D60}
{588CD2D7-EE70-43C1-8233-330854BDF53C} = {4981B3A5-4AFD-4191-BF7D-8692D9783D60}
{E6BE41D3-872C-47D2-B5B1-78C37AFAEAF9} = {D173887B-AF42-4576-B9C1-96B9E9B3D9C0}
{9357EC71-823B-433A-9993-B7CB2FA082D1} = {B80354C7-BE58-43F6-8928-9F3A74AB7F47}
{3F7B206E-5457-458F-AA81-9449FA3C1B5C} = {27381127-6C45-4B4C-8F18-41FF48DFE4B2}
{6C71A90C-30AE-45D7-9347-D66F9B257CBE} = {D173887B-AF42-4576-B9C1-96B9E9B3D9C0}
Expand All @@ -1717,8 +1714,6 @@ Global
{830A89EC-4029-4753-B25A-068BAE37DEC7} = {4981B3A5-4AFD-4191-BF7D-8692D9783D60}
{1BC02557-B78B-48CE-9D3C-488A6B7672F4} = {830A89EC-4029-4753-B25A-068BAE37DEC7}
{CAA4A93F-6BEB-42EB-8680-C1CF72928023} = {830A89EC-4029-4753-B25A-068BAE37DEC7}
{8E2AA85E-C351-47B4-AF91-58557FAD5840} = {830A89EC-4029-4753-B25A-068BAE37DEC7}
{986886B7-0E38-4890-92C3-5B46DE322DAF} = {830A89EC-4029-4753-B25A-068BAE37DEC7}
{7425E5B2-BC47-4521-AC40-B8CECA329E08} = {830A89EC-4029-4753-B25A-068BAE37DEC7}
{8E2AA85E-C351-47B4-AF91-58557FAD5840} = {830A89EC-4029-4753-B25A-068BAE37DEC7}
{986886B7-0E38-4890-92C3-5B46DE322DAF} = {830A89EC-4029-4753-B25A-068BAE37DEC7}
Expand All @@ -1741,6 +1736,7 @@ Global
{355F724F-D24F-45C6-8914-574385F6FC89} = {8BAF2119-8370-4E9E-A887-D92506F8C727}
{F1D00709-50F2-4533-B38F-3517C0EDEAEE} = {E6985EED-47E3-4EAC-8222-074E5410CEDC}
{1419BDCB-47EB-43EB-9149-C935B7208A72} = {90A70EFA-F26A-49E0-A375-DB461E4E0E25}
{50450FBB-CD10-4281-B22C-7FF86CEE9D9F} = {830A89EC-4029-4753-B25A-068BAE37DEC7}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {6DCEDFEC-988E-4CB3-B45B-191EB5086E0C}
Expand Down
27 changes: 24 additions & 3 deletions tests/Aspire.Components.Common.Tests/FileUtil.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,36 @@ internal static class FileUtil
{
public static string? FindFullPathFromPath(string command) => FindFullPathFromPath(command, Environment.GetEnvironmentVariable("PATH"), Path.PathSeparator, File.Exists);

internal static string? FindFullPathFromPath(string command, string? pathVariable, char pathSeparator, Func<string, bool> fileExists)
private static string? FindFullPathFromPath(string command, string? pathVariable, char pathSeparator, Func<string, bool> fileExists)
{
Debug.Assert(!string.IsNullOrWhiteSpace(command));

if (OperatingSystem.IsWindows() && !command.EndsWith(".exe"))
var fullPath = FindFullPath(command, pathVariable, pathSeparator, fileExists);
if (fullPath is not null)
{
command += ".exe";
return fullPath;
}

if (OperatingSystem.IsWindows())
{
// On Windows, we need to check for the command with all possible extensions.
foreach (var extension in Environment.GetEnvironmentVariable("PATHEXT")?.Split(';') ?? Array.Empty<string>())
{
var fileName = command.EndsWith(extension, StringComparison.OrdinalIgnoreCase) ? command : command + extension;

fullPath = FindFullPath(fileName, pathVariable, pathSeparator, fileExists);
if (fullPath is not null)
{
return fullPath;
}
}
}

return null;
}

private static string? FindFullPath(string command, string? pathVariable, char pathSeparator, Func<string, bool> fileExists)
{
foreach (var directory in (pathVariable ?? string.Empty).Split(pathSeparator))
{
var fullPath = Path.Combine(directory, command);
Expand Down
75 changes: 75 additions & 0 deletions tests/Aspire.Hosting.NodeJs.Tests/AddNodeAppTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// 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.Utils;
using Xunit;

namespace Aspire.Hosting.NodeJs.Tests;

public class AddNodeAppTests
{
[Fact]
public async Task NodeAppIsExecutableResource()
{
using var builder = TestDistributedApplicationBuilder.Create();

var nodeApp = builder.AddNodeApp("nodeapp", "..\\foo\\app.js")
.WithHttpEndpoint(port: 5031, env: "PORT");
var manifest = await ManifestUtils.GetManifest(nodeApp.Resource);

var expectedManifest = $$"""
{
"type": "executable.v0",
"workingDirectory": "../../../../../tests/foo",
"command": "node",
"args": [
"..\\foo\\app.js"
],
"env": {
"NODE_ENV": "development",
"PORT": "{nodeapp.bindings.http.targetPort}"
},
"bindings": {
"http": {
"scheme": "http",
"protocol": "tcp",
"transport": "http",
"port": 5031,
"targetPort": 8000
}
}
}
""";
Assert.Equal(expectedManifest, manifest.ToString());

var npmApp = builder.AddNpmApp("npmapp", "..\\foo")
.WithHttpEndpoint(port: 5032, env: "PORT");
manifest = await ManifestUtils.GetManifest(npmApp.Resource);

expectedManifest = $$"""
{
"type": "executable.v0",
"workingDirectory": "../../../../../tests/foo",
"command": "npm",
"args": [
"run",
"start"
],
"env": {
"NODE_ENV": "development",
"PORT": "{npmapp.bindings.http.targetPort}"
},
"bindings": {
"http": {
"scheme": "http",
"protocol": "tcp",
"transport": "http",
"port": 5032,
"targetPort": 8000
}
}
}
""";
Assert.Equal(expectedManifest, manifest.ToString());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">

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

<ItemGroup>
<ProjectReference Include="..\..\src\Aspire.Hosting.AppHost\Aspire.Hosting.AppHost.csproj" />
<ProjectReference Include="..\..\src\Aspire.Hosting.NodeJs\Aspire.Hosting.NodeJs.csproj" />
<ProjectReference Include="..\Aspire.Hosting.Tests\Aspire.Hosting.Tests.csproj" />
</ItemGroup>

</Project>
119 changes: 119 additions & 0 deletions tests/Aspire.Hosting.NodeJs.Tests/NodeAppFixture.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Globalization;
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.Testing;
using Aspire.Hosting.Utils;
using Microsoft.Extensions.Logging;
using Xunit;
using Xunit.Abstractions;
using Xunit.Sdk;

namespace Aspire.Hosting.NodeJs.Tests;

/// <summary>
/// TestProgram with node and npm apps.
/// </summary>
public class NodeAppFixture(IMessageSink diagnosticMessageSink) : IAsyncLifetime
{
private DistributedApplication? _app;
private string? _nodeAppPath;

public DistributedApplication App => _app ?? throw new InvalidOperationException("DistributedApplication is not initialized.");

public IResourceBuilder<NodeAppResource>? NodeAppBuilder { get; private set; }
public IResourceBuilder<NodeAppResource>? NpmAppBuilder { get; private set; }

public async Task InitializeAsync()
{
var builder = TestDistributedApplicationBuilder.Create();
builder.Services.AddXunitLogging(new TestOutputWrapper(diagnosticMessageSink));

_nodeAppPath = CreateNodeApp();
var scriptPath = Path.Combine(_nodeAppPath, "app.js");

NodeAppBuilder = builder.AddNodeApp("nodeapp", scriptPath)
.WithHttpEndpoint(port: 5031, env: "PORT");

NpmAppBuilder = builder.AddNpmApp("npmapp", _nodeAppPath)
.WithHttpEndpoint(port: 5032, env: "PORT");

_app = builder.Build();

using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(5));

await _app.StartAsync(cts.Token);

await WaitReadyStateAsync(cts.Token);
}

public async Task DisposeAsync()
{
if (_app is not null)
{
await _app.StopAsync();
await _app.DisposeAsync();
}

if (_nodeAppPath is not null)
{
Directory.Delete(_nodeAppPath, recursive: true);
}
}

private static string CreateNodeApp()
{
var tempDir = Directory.CreateTempSubdirectory("aspire-nodejs-tests").FullName;

File.WriteAllText(Path.Combine(tempDir, "app.js"),
"""
const http = require('http');
const port = process.env.PORT ?? 3000;

const server = http.createServer((req, res) => {
res.statusCode = 200;
res.setHeader('Content-Type', 'text/plain');
if (process.env.npm_lifecycle_event === undefined) {
res.end('Hello from node!');
} else {
res.end('Hello from npm!');
}
});

server.listen(port, () => {
console.log('Web server running on on %s', port);
});
""");

File.WriteAllText(Path.Combine(tempDir, "package.json"),
"""
{
"scripts": {
"start": "node app.js"
}
}
""");

return tempDir;
}

private async Task WaitReadyStateAsync(CancellationToken cancellationToken = default)
{
using var client = App.CreateHttpClient(NodeAppBuilder!.Resource.Name, endpointName: "http");
await client.GetStringAsync("/", cancellationToken);
}

private sealed class TestOutputWrapper(IMessageSink messageSink) : ITestOutputHelper
{
public void WriteLine(string message)
{
messageSink.OnMessage(new DiagnosticMessage(message));
}

public void WriteLine(string format, params object[] args)
{
messageSink.OnMessage(new DiagnosticMessage(string.Format(CultureInfo.CurrentCulture, format, args)));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,9 @@
using Aspire.Hosting.Testing;
using Xunit;

namespace Aspire.Hosting.Tests.Node;
namespace Aspire.Hosting.NodeJs.Tests;

[Collection("NodeApp")]
public class NodeFunctionalTests
public class NodeFunctionalTests : IClassFixture<NodeAppFixture>
{
private readonly NodeAppFixture _nodeJsFixture;

Expand All @@ -22,26 +21,22 @@ public NodeFunctionalTests(NodeAppFixture nodeJsFixture)
[ActiveIssue("https://github.com/dotnet/aspire/issues/4508", typeof(PlatformDetection), nameof(PlatformDetection.IsRunningOnCI))]
public async Task VerifyNodeAppWorks()
{
var testProgram = _nodeJsFixture.TestProgram;

using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(1));
using var nodeClient = testProgram.App!.CreateHttpClient(testProgram.NodeAppBuilder!.Resource.Name, "http");
var response0 = await nodeClient.GetStringAsync("/", cts.Token);
using var nodeClient = _nodeJsFixture.App.CreateHttpClient(_nodeJsFixture.NodeAppBuilder!.Resource.Name, "http");
var response = await nodeClient.GetStringAsync("/", cts.Token);

Assert.Equal("Hello from node!", response0);
Assert.Equal("Hello from node!", response);
}

[Fact]
[RequiresTools(["npm"])]
[ActiveIssue("https://github.com/dotnet/aspire/issues/4508", typeof(PlatformDetection), nameof(PlatformDetection.IsRunningOnCI))]
public async Task VerifyNpmAppWorks()
{
var testProgram = _nodeJsFixture.TestProgram;

using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(1));
using var npmClient = testProgram.App!.CreateHttpClient(testProgram.NpmAppBuilder!.Resource.Name, "http");
var response0 = await npmClient.GetStringAsync("/", cts.Token);
using var npmClient = _nodeJsFixture.App.CreateHttpClient(_nodeJsFixture.NpmAppBuilder!.Resource.Name, "http");
var response = await npmClient.GetStringAsync("/", cts.Token);

Assert.Equal("Hello from npm!", response0);
Assert.Equal("Hello from npm!", response);
}
}
1 change: 1 addition & 0 deletions tests/Aspire.Hosting.Tests/Aspire.Hosting.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
<ProjectReference Include="..\..\src\Aspire.Hosting.AWS\Aspire.Hosting.AWS.csproj" IsAspireProjectResource="false" />
<ProjectReference Include="..\..\src\Aspire.Hosting.Dapr\Aspire.Hosting.Dapr.csproj" IsAspireProjectResource="false" />
<ProjectReference Include="..\..\src\Aspire.Hosting.MongoDB\Aspire.Hosting.MongoDB.csproj" IsAspireProjectResource="false" />
<ProjectReference Include="..\..\src\Aspire.Hosting.NodeJs\Aspire.Hosting.NodeJs.csproj" IsAspireProjectResource="false" />
<ProjectReference Include="..\..\src\Aspire.Hosting.Testing\Aspire.Hosting.Testing.csproj" IsAspireProjectResource="false" />
<ProjectReference Include="..\Aspire.Components.Common.Tests\Aspire.Components.Common.Tests.csproj" IsAspireProjectResource="false" />
<ProjectReference Include="..\testproject\TestProject.AppHost\TestProject.AppHost.csproj" IsAspireProjectResource="false" />
Expand Down
Loading

0 comments on commit bea7f83

Please sign in to comment.