Skip to content

Commit

Permalink
Extract Aspire.Hosting.NodeJs.Tests project (#5142)
Browse files Browse the repository at this point in the history
* Extract Aspire.Hosting.NodeJs.Tests project

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 #4294
  • Loading branch information
eerhardt authored Aug 7, 2024
1 parent 5d5ce00 commit 423a768
Show file tree
Hide file tree
Showing 16 changed files with 272 additions and 140 deletions.
7 changes: 7 additions & 0 deletions Aspire.sln
Original file line number Diff line number Diff line change
Expand Up @@ -557,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
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aspire.Hosting.RabbitMQ.Tests", "tests\Aspire.Hosting.RabbitMQ.Tests\Aspire.Hosting.RabbitMQ.Tests.csproj", "{872AC635-B880-4FAC-BB43-4FD97D7B1209}"
EndProject
Global
Expand Down Expand Up @@ -1469,6 +1471,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
{872AC635-B880-4FAC-BB43-4FD97D7B1209}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{872AC635-B880-4FAC-BB43-4FD97D7B1209}.Debug|Any CPU.Build.0 = Debug|Any CPU
{872AC635-B880-4FAC-BB43-4FD97D7B1209}.Release|Any CPU.ActiveCfg = Release|Any CPU
Expand Down Expand Up @@ -1741,6 +1747,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}
{872AC635-B880-4FAC-BB43-4FD97D7B1209} = {830A89EC-4029-4753-B25A-068BAE37DEC7}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
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
76 changes: 76 additions & 0 deletions tests/Aspire.Hosting.NodeJs.Tests/AddNodeAppTests.cs
Original file line number Diff line number Diff line change
@@ -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.Utils;
using Xunit;

namespace Aspire.Hosting.NodeJs.Tests;

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

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

var expectedManifest = $$"""
{
"type": "executable.v0",
"workingDirectory": ".",
"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", workingDirectory)
.WithHttpEndpoint(port: 5032, env: "PORT");
manifest = await ManifestUtils.GetManifest(npmApp.Resource);

expectedManifest = $$"""
{
"type": "executable.v0",
"workingDirectory": ".",
"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>
128 changes: 128 additions & 0 deletions tests/Aspire.Hosting.NodeJs.Tests/NodeAppFixture.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
// 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 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 TestDistributedApplicationBuilder? _builder;
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()
{
_builder = TestDistributedApplicationBuilder.Create()
.WithTestAndResourceLogging(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()
{
_builder?.Dispose();

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

if (_nodeAppPath is not null)
{
try
{
Directory.Delete(_nodeAppPath, recursive: true);
}
catch
{
// Don't fail test if we can't clean the temporary folder
}
}
}

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 @@ -12,6 +12,7 @@
<ProjectReference Include="..\..\src\Aspire.Hosting.AppHost\Aspire.Hosting.AppHost.csproj" IsAspireProjectResource="false" />
<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.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 423a768

Please sign in to comment.