File: AddViteAppTests.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.
 
#pragma warning disable ASPIREDOCKERFILEBUILDER001 // Type is for evaluation purposes only
 
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.Utils;
 
namespace Aspire.Hosting.JavaScript.Tests;
 
public class AddViteAppTests
{
    [Fact]
    public async Task VerifyDefaultDockerfile()
    {
        using var tempDir = new TempDirectory();
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish, outputPath: tempDir.Path).WithResourceCleanUp(true);
 
        // Create vite directory to ensure manifest generates correct relative build context path
        var viteDir = Path.Combine(tempDir.Path, "vite");
        Directory.CreateDirectory(viteDir);
 
        // Create a lock file so npm ci is used in the Dockerfile
        File.WriteAllText(Path.Combine(viteDir, "package-lock.json"), "empty");
 
        var nodeApp = builder.AddViteApp("vite", viteDir)
            .WithNpm(install: true);
 
        var manifest = await ManifestUtils.GetManifest(nodeApp.Resource, tempDir.Path);
 
        var expectedManifest = $$"""
            {
              "type": "container.v1",
              "build": {
                "context": "vite",
                "dockerfile": "vite.Dockerfile"
              },
              "env": {
                "NODE_ENV": "production",
                "PORT": "{vite.bindings.http.targetPort}"
              },
              "bindings": {
                "http": {
                  "scheme": "http",
                  "protocol": "tcp",
                  "transport": "http",
                  "targetPort": 8000
                }
              }
            }
            """;
        Assert.Equal(expectedManifest, manifest.ToString());
 
        var dockerfilePath = Path.Combine(tempDir.Path, "vite.Dockerfile");
        var dockerfileContents = File.ReadAllText(dockerfilePath);
        var expectedDockerfile = $$"""
            FROM node:22-slim
            WORKDIR /app
            COPY . .
            RUN npm ci
            RUN npm run build

            """.Replace("\r\n", "\n");
        Assert.Equal(expectedDockerfile, dockerfileContents);
 
        var dockerBuildAnnotation = nodeApp.Resource.Annotations.OfType<DockerfileBuildAnnotation>().Single();
        Assert.False(dockerBuildAnnotation.HasEntrypoint);
 
        var containerFilesSource = nodeApp.Resource.Annotations.OfType<ContainerFilesSourceAnnotation>().Single();
        Assert.Equal("/app/dist", containerFilesSource.SourcePath);
    }
 
    [Fact]
    public async Task VerifyDockerfileWithNodeVersionFromPackageJson()
    {
        using var tempDir = new TempDirectory();
 
        // Create a package.json with engines.node specification
        var packageJson = """
            {
              "name": "test-vite",
              "engines": {
                "node": ">=20.12"
              }
            }
            """;
        File.WriteAllText(Path.Combine(tempDir.Path, "package.json"), packageJson);
 
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish, outputPath: tempDir.Path).WithResourceCleanUp(true);
        var nodeApp = builder.AddViteApp("vite", tempDir.Path)
            .WithNpm();
 
        var manifest = await ManifestUtils.GetManifest(nodeApp.Resource, tempDir.Path);
 
        var dockerfileContents = File.ReadAllText(Path.Combine(tempDir.Path, "vite.Dockerfile"));
        
        // Should detect version 20 from package.json
        Assert.Contains("FROM node:20-slim", dockerfileContents);
    }
 
    [Fact]
    public async Task VerifyDockerfileWithNodeVersionFromNvmrc()
    {
        using var tempDir = new TempDirectory();
 
        // Create an .nvmrc file
        File.WriteAllText(Path.Combine(tempDir.Path, ".nvmrc"), "18.20.0");
 
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish, outputPath: tempDir.Path).WithResourceCleanUp(true);
        var nodeApp = builder.AddViteApp("vite", tempDir.Path)
            .WithNpm();
 
        var manifest = await ManifestUtils.GetManifest(nodeApp.Resource, tempDir.Path);
 
        var dockerfileContents = File.ReadAllText(Path.Combine(tempDir.Path, "vite.Dockerfile"));
        
        // Should detect version 18 from .nvmrc
        Assert.Contains("FROM node:18-slim", dockerfileContents);
    }
 
    [Fact]
    public async Task VerifyDockerfileWithNodeVersionFromNodeVersion()
    {
        using var tempDir = new TempDirectory();
 
        // Create a .node-version file
        File.WriteAllText(Path.Combine(tempDir.Path, ".node-version"), "v21.5.0");
 
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish, outputPath: tempDir.Path).WithResourceCleanUp(true);
        var nodeApp = builder.AddViteApp("vite", tempDir.Path)
            .WithNpm();
 
        var manifest = await ManifestUtils.GetManifest(nodeApp.Resource, tempDir.Path);
 
        var dockerfileContents = File.ReadAllText(Path.Combine(tempDir.Path, "vite.Dockerfile"));
        
        // Should detect version 21 from .node-version
        Assert.Contains("FROM node:21-slim", dockerfileContents);
    }
 
    [Fact]
    public async Task VerifyDockerfileWithNodeVersionFromToolVersions()
    {
        using var tempDir = new TempDirectory();
 
        // Create a .tool-versions file
        var toolVersions = """
            ruby 3.2.0
            nodejs 19.8.1
            python 3.11.0
            """;
        File.WriteAllText(Path.Combine(tempDir.Path, ".tool-versions"), toolVersions);
 
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish, outputPath: tempDir.Path).WithResourceCleanUp(true);
        var nodeApp = builder.AddViteApp("vite", tempDir.Path)
            .WithNpm();
 
        var manifest = await ManifestUtils.GetManifest(nodeApp.Resource, tempDir.Path);
 
        var dockerfileContents = File.ReadAllText(Path.Combine(tempDir.Path, "vite.Dockerfile"));
        
        // Should detect version 19 from .tool-versions
        Assert.Contains("FROM node:19-slim", dockerfileContents);
    }
 
    [Fact]
    public async Task VerifyDockerfileDefaultsTo22WhenNoVersionFound()
    {
        using var tempDir = new TempDirectory();
 
        // Don't create any version files - should default to 22
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish, outputPath: tempDir.Path).WithResourceCleanUp(true);
        var nodeApp = builder.AddViteApp("vite", tempDir.Path)
            .WithNpm();
 
        var manifest = await ManifestUtils.GetManifest(nodeApp.Resource, tempDir.Path);
 
        var dockerfileContents = File.ReadAllText(Path.Combine(tempDir.Path, "vite.Dockerfile"));
        
        // Should default to version 22
        Assert.Contains("FROM node:22-slim", dockerfileContents);
    }
 
    [Theory]
    [InlineData("18", "node:18-slim")]
    [InlineData("v20.1.0", "node:20-slim")]
    [InlineData(">=18.12", "node:18-slim")]
    [InlineData("^16.0.0", "node:16-slim")]
    [InlineData("~19.5.0", "node:19-slim")]
    public async Task VerifyDockerfileHandlesVariousVersionFormats(string versionString, string expectedImage)
    {
        using var tempDir = new TempDirectory();
 
        File.WriteAllText(Path.Combine(tempDir.Path, ".nvmrc"), versionString);
 
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish, outputPath: tempDir.Path).WithResourceCleanUp(true);
        var nodeApp = builder.AddViteApp("vite", tempDir.Path)
            .WithNpm();
 
        var manifest = await ManifestUtils.GetManifest(nodeApp.Resource, tempDir.Path);
 
        var dockerfileContents = File.ReadAllText(Path.Combine(tempDir.Path, "vite.Dockerfile"));
        
        Assert.Contains($"FROM {expectedImage}", dockerfileContents);
    }
 
    [Fact]
    public async Task VerifyCustomBaseImage()
    {
        using var tempDir = new TempDirectory();
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish, outputPath: tempDir.Path).WithResourceCleanUp(true);
 
        var customImage = "node:22-myspecialimage";
        var nodeApp = builder.AddViteApp("vite", tempDir.Path)
            .WithNpm(install: true)
            .WithDockerfileBaseImage(buildImage: customImage);
 
        var manifest = await ManifestUtils.GetManifest(nodeApp.Resource, tempDir.Path);
 
        // Verify the manifest structure
        Assert.Equal("container.v1", manifest["type"]?.ToString());
 
        // Verify the Dockerfile contains the custom base image
        var dockerfileContents = File.ReadAllText(Path.Combine(tempDir.Path, "vite.Dockerfile"));
        Assert.Contains($"FROM {customImage}", dockerfileContents);
    }
}