File: AddNodeAppTests.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.
 
#pragma warning disable ASPIREDOCKERFILEBUILDER001 // Type is for evaluation purposes only
 
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.Tests.Utils;
using Aspire.Hosting.Utils;
using Microsoft.Extensions.DependencyInjection;
 
namespace Aspire.Hosting.NodeJs.Tests;
 
public class AddNodeAppTests
{
    [Fact]
    public async Task VerifyManifest()
    {
        using var builder = TestDistributedApplicationBuilder.Create().WithResourceCleanUp(true);
 
        var workingDirectory = AppContext.BaseDirectory;
        var nodeApp = builder.AddNodeApp("nodeapp", workingDirectory, "..\\foo\\app.js")
            .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": "{{builder.Environment.EnvironmentName.ToLowerInvariant()}}",
                "PORT": "{nodeapp.bindings.http.targetPort}"
              },
              "bindings": {
                "http": {
                  "scheme": "http",
                  "protocol": "tcp",
                  "transport": "http",
                  "port": 5031,
                  "targetPort": 8000
                }
              }
            }
            """;
        Assert.Equal(expectedManifest, manifest.ToString());
 
#pragma warning disable CS0618 // Type or member is obsolete
        var npmApp = builder.AddNpmApp("npmapp", workingDirectory)
            .WithHttpEndpoint(port: 5032, env: "PORT");
#pragma warning restore CS0618 // Type or member is obsolete
 
        manifest = await ManifestUtils.GetManifest(npmApp.Resource);
 
        expectedManifest = $$"""
            {
              "type": "executable.v0",
              "workingDirectory": ".",
              "command": "npm",
              "args": [
                "run",
                "start"
              ],
              "env": {
                "NODE_ENV": "{{builder.Environment.EnvironmentName.ToLowerInvariant()}}",
                "PORT": "{npmapp.bindings.http.targetPort}"
              },
              "bindings": {
                "http": {
                  "scheme": "http",
                  "protocol": "tcp",
                  "transport": "http",
                  "port": 5032,
                  "targetPort": 8000
                }
              }
            }
            """;
        Assert.Equal(expectedManifest, manifest.ToString());
    }
 
    [Theory]
    [InlineData(true)]
    [InlineData(false)]
    public async Task VerifyDockerfile(bool includePackageJson)
    {
        using var tempDir = new TempDirectory();
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish, outputPath: tempDir.Path).WithResourceCleanUp(true);
 
        var appDir = Path.Combine(tempDir.Path, "js");
        Directory.CreateDirectory(appDir);
 
        if (includePackageJson)
        {
            File.WriteAllText(Path.Combine(appDir, "package.json"), "{}");
            File.WriteAllText(Path.Combine(appDir, "package-lock.json"), "{}");
        }
 
        var nodeApp = builder.AddNodeApp("js", appDir, "app.js");
 
        await ManifestUtils.GetManifest(nodeApp.Resource, tempDir.Path);
 
        var dockerfilePath = Path.Combine(tempDir.Path, "js.Dockerfile");
        var dockerfileContents = File.ReadAllText(dockerfilePath);
        var expectedDockerfile = $"""
            FROM node:22-alpine AS build
            
            WORKDIR /app
            COPY . .
            
            {(includePackageJson ? "RUN npm ci\n" : "")}
            FROM node:22-alpine AS runtime
            
            WORKDIR /app
            COPY --from=build /app /app
            
            ENV NODE_ENV=production
            EXPOSE 3000
            
            USER node
            
            ENTRYPOINT ["node","app.js"]

            """.Replace("\r\n", "\n");
        Assert.Equal(expectedDockerfile, dockerfileContents);
 
        var dockerBuildAnnotation = nodeApp.Resource.Annotations.OfType<DockerfileBuildAnnotation>().Single();
        Assert.True(dockerBuildAnnotation.HasEntrypoint);
 
        Assert.Empty(nodeApp.Resource.Annotations.OfType<ContainerFilesSourceAnnotation>());
    }
 
    [Fact]
    public async Task VerifyDockerfileWithBuildScript()
    {
        using var tempDir = new TempDirectory();
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish, outputPath: tempDir.Path).WithResourceCleanUp(true);
 
        var appDir = Path.Combine(tempDir.Path, "js");
        Directory.CreateDirectory(appDir);
        File.WriteAllText(Path.Combine(appDir, "package.json"), "{}");
 
        var nodeApp = builder.AddNodeApp("js", appDir, "app.js")
            .WithAnnotation(new JavaScriptPackageManagerAnnotation("mypm", runScriptCommand: null))
            .WithAnnotation(new JavaScriptInstallCommandAnnotation(["myinstall"]))
            .WithBuildScript("mybuild");
 
        await ManifestUtils.GetManifest(nodeApp.Resource, tempDir.Path);
 
        var dockerfilePath = Path.Combine(tempDir.Path, "js.Dockerfile");
        var dockerfileContents = File.ReadAllText(dockerfilePath);
        var expectedDockerfile = $"""
            FROM node:22-alpine AS build
 
            WORKDIR /app
            COPY . .
 
            RUN mypm myinstall
            RUN mypm mybuild
 
            FROM node:22-alpine AS runtime
 
            WORKDIR /app
            COPY --from=build /app /app
 
            ENV NODE_ENV=production
            EXPOSE 3000
 
            USER node
 
            ENTRYPOINT ["node","app.js"]

            """.Replace("\r\n", "\n");
        Assert.Equal(expectedDockerfile, dockerfileContents);
    }
 
    [Fact]
    public async Task VerifyDockerfileWithCustomBaseImage()
    {
        using var tempDir = new TempDirectory();
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish, outputPath: tempDir.Path).WithResourceCleanUp(true);
 
        var appDir = Path.Combine(tempDir.Path, "js");
        Directory.CreateDirectory(appDir);
        File.WriteAllText(Path.Combine(appDir, "package.json"), "{}");
 
        var customBuildImage = "node:22-mySpecialBuildImage";
        var customRuntimeImage = "node:22-mySpecialRuntimeImage";
        var nodeApp = builder.AddNodeApp("js", appDir, "app.js")
            .WithNpm(install: true)
            .WithDockerfileBaseImage(customBuildImage, customRuntimeImage);
 
        await ManifestUtils.GetManifest(nodeApp.Resource, tempDir.Path);
 
        // Verify the Dockerfile contains the custom base image
        var dockerfileContents = File.ReadAllText(Path.Combine(tempDir.Path, "js.Dockerfile"));
        Assert.Contains($"FROM {customBuildImage}", dockerfileContents);
        Assert.Contains($"FROM {customRuntimeImage}", dockerfileContents);
    }
 
    [Fact]
    public void AddNodeApp_DoesNotAddNpmWhenNoPackageJson()
    {
        var tempDir = new TempDirectory();
        File.WriteAllText(Path.Combine(tempDir.Path, "app.js"), "{}");
 
        var builder = DistributedApplication.CreateBuilder();
 
        builder.AddNodeApp("nodeapp", tempDir.Path, "app.js");
 
        using var app = builder.Build();
 
        var appModel = app.Services.GetRequiredService<DistributedApplicationModel>();
 
        // Verify the JavaScriptApp resource exists
        var nodeResource = Assert.Single(appModel.Resources.OfType<NodeAppResource>());
 
        // Verify the package manager annotation
        Assert.False(nodeResource.TryGetLastAnnotation<JavaScriptPackageManagerAnnotation>(out _));
 
        // Verify the install command annotation
        Assert.False(nodeResource.TryGetLastAnnotation<JavaScriptInstallCommandAnnotation>(out _));
 
        // Verify the JavaScriptInstallerResource resource does not exist
        Assert.Empty(appModel.Resources.OfType<JavaScriptInstallerResource>());
    }
 
    [Fact]
    public void AddNodeApp_AddsNpmWhenPackageJsonExists()
    {
        var tempDir = new TempDirectory();
        File.WriteAllText(Path.Combine(tempDir.Path, "package.json"), "{}");
 
        var builder = DistributedApplication.CreateBuilder();
 
        builder.AddNodeApp("nodeapp", tempDir.Path, "app.js");
 
        using var app = builder.Build();
 
        var appModel = app.Services.GetRequiredService<DistributedApplicationModel>();
 
        // Verify the JavaScriptApp resource exists
        var nodeResource = Assert.Single(appModel.Resources.OfType<NodeAppResource>());
 
        // Verify the package manager annotation
        Assert.True(nodeResource.TryGetLastAnnotation<JavaScriptPackageManagerAnnotation>(out var packageManager));
        Assert.Equal("npm", packageManager.ExecutableName);
        Assert.Equal("run", packageManager.ScriptCommand);
 
        // Verify the install command annotation
        Assert.True(nodeResource.TryGetLastAnnotation<JavaScriptInstallCommandAnnotation>(out var installAnnotation));
        Assert.Equal(["install"], installAnnotation.Args);
 
        // Verify the JavaScriptInstallerResource resource exists
        var installerResources = Assert.Single(appModel.Resources.OfType<JavaScriptInstallerResource>());
        Assert.NotNull(installerResources);
    }
 
    [Fact]
    public async Task WithRunScript_SetsCustomRunCommand()
    {
        var builder = DistributedApplication.CreateBuilder();
 
        builder.AddNodeApp("nodeapp", ".", "app.js")
            .WithYarn()
            .WithRunScript("start", ["--my-arg1"]);
 
        using var app = builder.Build();
 
        var appModel = app.Services.GetRequiredService<DistributedApplicationModel>();
 
        // Verify the JavaScriptApp resource exists
        var nodeResource = Assert.Single(appModel.Resources.OfType<NodeAppResource>());
 
        var args = await ArgumentEvaluator.GetArgumentListAsync(nodeResource);
 
        Assert.Collection(args,
            arg => Assert.Equal("run", arg),
            arg => Assert.Equal("start", arg),
            arg => Assert.Equal("--my-arg1", arg));
    }
}