|
// 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}");
}
}
|