File: NodeAppFixture.cs
Web Access
Project: src\tests\Aspire.Hosting.NodeJs.Tests\Aspire.Hosting.NodeJs.Tests.csproj (Aspire.Hosting.NodeJs.Tests)
// 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"
                }
            }
            """{
                "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)));
        }
    }
}