File: AddJavaScriptAppTests.cs
Web Access
Project: src\tests\Aspire.Hosting.JavaScript.Tests\Aspire.Hosting.JavaScript.Tests.csproj (Aspire.Hosting.JavaScript.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.Diagnostics;
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.Utils;
using Aspire.TestUtilities;
 
namespace Aspire.Hosting.JavaScript.Tests;
 
public class AddJavaScriptAppTests
{
    [Fact]
    public async Task VerifyDockerfile()
    {
        using var tempDir = new TestTempDirectory();
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish, outputPath: tempDir.Path).WithResourceCleanUp(true);
 
        // Create directory to ensure manifest generates correct relative build context path
        var appDir = Path.Combine(tempDir.Path, "js");
        Directory.CreateDirectory(appDir);
 
        var yarnApp = builder.AddJavaScriptApp("js", appDir)
            .WithYarn(installArgs: ["--immutable"])
            .WithBuildScript("do", ["--build"]);
 
        await ManifestUtils.GetManifest(yarnApp.Resource, tempDir.Path);
 
        var dockerfilePath = Path.Combine(tempDir.Path, "js.Dockerfile");
        var dockerfileContents = File.ReadAllText(dockerfilePath);
        var expectedDockerfile = $$"""
            FROM node:22-slim
            WORKDIR /app
            COPY package.json ./
            RUN --mount=type=cache,target=/root/.cache/yarn yarn install --immutable
            COPY . .
            RUN yarn run do --build

            """.Replace("\r\n", "\n");
        Assert.Equal(expectedDockerfile, dockerfileContents);
 
        var dockerBuildAnnotation = yarnApp.Resource.Annotations.OfType<DockerfileBuildAnnotation>().Single();
        Assert.False(dockerBuildAnnotation.HasEntrypoint);
 
        var containerFilesSource = yarnApp.Resource.Annotations.OfType<ContainerFilesSourceAnnotation>().Single();
        Assert.Equal("/app/dist", containerFilesSource.SourcePath);
    }
 
    [Theory]
    [InlineData(true)]
    [InlineData(false)]
    public async Task VerifyPnpmDockerfile(bool hasLockFile)
    {
        using var tempDir = new TestTempDirectory();
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish, outputPath: tempDir.Path).WithResourceCleanUp(true);
 
        // Create directory to ensure manifest generates correct relative build context path
        var appDir = Path.Combine(tempDir.Path, "js");
        Directory.CreateDirectory(appDir);
 
        if (hasLockFile)
        {
            File.WriteAllText(Path.Combine(appDir, "pnpm-lock.yaml"), string.Empty);
        }
 
        var pnpmApp = builder.AddJavaScriptApp("js", appDir)
            .WithPnpm(installArgs: ["--prefer-frozen-lockfile"])
            .WithBuildScript("mybuild");
 
        await ManifestUtils.GetManifest(pnpmApp.Resource, tempDir.Path);
 
        var dockerfilePath = Path.Combine(tempDir.Path, "js.Dockerfile");
        var dockerfileContents = File.ReadAllText(dockerfilePath);
 
        await Verify(dockerfileContents);
    }
 
    [Fact]
    [RequiresDocker]
    [OuterloopTest("Builds a Docker image to verify the generated pnpm Dockerfile works")]
    public async Task VerifyPnpmDockerfileBuildSucceeds()
    {
        using var tempDir = new TestTempDirectory();
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish, outputPath: tempDir.Path).WithResourceCleanUp(true);
 
        // Create app directory
        var appDir = Path.Combine(tempDir.Path, "pnpm-app");
        Directory.CreateDirectory(appDir);
 
        // Create a minimal package.json with no dependencies
        var packageJson = """
            {
              "name": "pnpm-test-app",
              "version": "1.0.0",
              "scripts": {
                "build": "echo 'build completed'"
              }
            }
            """;
        await File.WriteAllTextAsync(Path.Combine(appDir, "package.json"), packageJson);
 
        var pnpmApp = builder.AddJavaScriptApp("pnpm-app", appDir)
            .WithPnpm()
            .WithBuildScript("build");
 
        await ManifestUtils.GetManifest(pnpmApp.Resource, tempDir.Path);
 
        var dockerfilePath = Path.Combine(tempDir.Path, "pnpm-app.Dockerfile");
        Assert.True(File.Exists(dockerfilePath), $"Dockerfile should exist at {dockerfilePath}");
 
        // Read the generated Dockerfile and verify it contains the corepack enable pnpm command
        var dockerfileContent = await File.ReadAllTextAsync(dockerfilePath);
        Assert.Contains("corepack enable pnpm", dockerfileContent);
 
        // Modify the Dockerfile to add NODE_TLS_REJECT_UNAUTHORIZED=0 for test environments
        // that may have corporate proxies with self-signed certificates
        var modifiedDockerfile = dockerfileContent.Replace(
            "WORKDIR /app",
            "WORKDIR /app\nENV NODE_TLS_REJECT_UNAUTHORIZED=0");
        var dockerfileInContext = Path.Combine(appDir, "Dockerfile");
        await File.WriteAllTextAsync(dockerfileInContext, modifiedDockerfile);
 
        // Build the Docker image using docker build with host network for registry access
        var imageName = $"aspire-pnpm-test-{Guid.NewGuid():N}";
        var processStartInfo = new ProcessStartInfo
        {
            FileName = "docker",
            Arguments = $"build --network=host -t {imageName} -f Dockerfile .",
            WorkingDirectory = appDir,
            RedirectStandardOutput = true,
            RedirectStandardError = true,
            UseShellExecute = false,
            CreateNoWindow = true
        };
 
        using var process = Process.Start(processStartInfo);
        Assert.NotNull(process);
 
        var stdout = await process.StandardOutput.ReadToEndAsync();
        var stderr = await process.StandardError.ReadToEndAsync();
 
        await process.WaitForExitAsync();
 
        // Clean up the image regardless of success/failure
        try
        {
            using var cleanupProcess = Process.Start(new ProcessStartInfo
            {
                FileName = "docker",
                Arguments = $"rmi {imageName}",
                UseShellExecute = false,
                CreateNoWindow = true,
                RedirectStandardOutput = true,
                RedirectStandardError = true
            });
            if (cleanupProcess != null)
            {
                await cleanupProcess.WaitForExitAsync();
            }
        }
        catch
        {
            // Ignore cleanup errors
        }
 
        // Assert the build succeeded
        Assert.True(process.ExitCode == 0, $"Docker build failed with exit code {process.ExitCode}.\nStdout: {stdout}\nStderr: {stderr}");
    }
}