File: Templating\JavaStarterScaffoldTests.cs
Web Access
Project: src\tests\Aspire.Cli.Tests\Aspire.Cli.Tests.csproj (Aspire.Cli.Tests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using Aspire.Cli.Commands;
using Aspire.Cli.Projects;
using Aspire.Cli.Templating;
using Aspire.Cli.Tests.TestServices;
using Aspire.Cli.Tests.Utils;
using Microsoft.AspNetCore.InternalTesting;
using Microsoft.Extensions.DependencyInjection;
 
namespace Aspire.Cli.Tests.Templating;
 
/// <summary>
/// Drives <c>aspire new aspire-java-starter</c> through the real <c>CliTemplateFactory</c> writer so
/// the assertions are about what actually lands on disk, with only
/// <see cref="IAppHostServerProjectFactory"/> faked so the run stops before it reaches the network,
/// the dotnet CLI, npm, or the Java toolchain.
/// </summary>
public class JavaStarterScaffoldTests(ITestOutputHelper outputHelper)
{
    /// <summary>
    /// Embedded resources carry no file mode, so every scaffolded file used to land at the default
    /// 0644. The Java starter ships a Maven wrapper the README and the Java tooling both invoke as
    /// <c>./mvnw</c>, which fails with "Permission denied" against a non-executable file. The hosting
    /// resolver launches the wrapper through <c>sh</c> and so survives either way, which is exactly
    /// why this needs its own test: the AppHost keeps working while everything a developer types by
    /// hand does not.
    /// </summary>
    [Fact]
    [SkipOnPlatform(TestPlatforms.Windows, "File modes are a Unix concept; the wrapper is invoked as mvnw.cmd on Windows.")]
    public async Task JavaStarter_ScaffoldsAnExecutableMavenWrapper()
    {
        using var scaffold = await ScaffoldJavaStarterAsync();
        var outputDirectory = scaffold.OutputDirectory;
 
        var wrapperPath = Path.Combine(outputDirectory, "api", "mvnw");
        Assert.True(File.Exists(wrapperPath), $"Expected the scaffolded Maven wrapper at {wrapperPath}");
 
        var mode = GetUnixFileMode(wrapperPath);
        Assert.True(mode.HasFlag(UnixFileMode.UserExecute), $"Expected the scaffolded Maven wrapper to be executable, but its mode was {mode}.");
    }
 
#pragma warning disable CA1416 // Only reached from a test skipped on Windows, which the analyzer cannot see through.
    private static UnixFileMode GetUnixFileMode(string path) => File.GetUnixFileMode(path);
#pragma warning restore CA1416
 
    /// <summary>
    /// The Java service, its wrapper, and the frontend all have to survive the scaffold together.
    /// Asserting the whole relative-path set rather than a handful of probes means a template file
    /// silently dropped from the embedded resource glob fails here instead of at a user's first
    /// <c>aspire run</c>.
    /// </summary>
    [Fact]
    public async Task JavaStarter_ScaffoldsTheJavaServiceAlongsideTheFrontend()
    {
        using var scaffold = await ScaffoldJavaStarterAsync();
        var outputDirectory = scaffold.OutputDirectory;
 
        var apiFiles = Directory
            .EnumerateFiles(Path.Combine(outputDirectory, "api"), "*", SearchOption.AllDirectories)
            .Select(path => Path.GetRelativePath(outputDirectory, path).Replace(Path.DirectorySeparatorChar, '/'))
            .Order(StringComparer.Ordinal)
            .ToArray();
 
        Assert.Equal(
            [
                "api/.mvn/wrapper/maven-wrapper.properties",
                "api/mvnw",
                "api/mvnw.cmd",
                "api/pom.xml",
                "api/src/main/java/com/example/api/ApiApplication.java",
                "api/src/main/java/com/example/api/WeatherForecastController.java",
                "api/src/main/resources/application.properties",
            ],
            apiFiles);
 
        Assert.True(File.Exists(Path.Combine(outputDirectory, "frontend", "vite.config.ts")));
        Assert.True(File.Exists(Path.Combine(outputDirectory, "AppHost.java")));
    }
 
    private async Task<ScaffoldedTemplate> ScaffoldJavaStarterAsync()
    {
        // The workspace is handed back to the caller rather than disposed here: disposal deletes the
        // directory, so every assertion about the scaffolded files has to run before it happens.
        var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
 
        var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options =>
        {
            options.EnabledFeatures = [KnownFeatures.ExperimentalPolyglotJava];
        });
 
        // PrepareAsync returning failure stops the run right after the template tree is written,
        // which is the only side-effect these tests inspect.
        services.AddSingleton<IAppHostServerProjectFactory>(_ => new TestAppHostServerProjectFactory
        {
            CreateAsyncCallback = (path, _) =>
                Task.FromResult<IAppHostServerProject>(new FakeFailingAppHostServerProject(path))
        });
 
        using var serviceProvider = services.BuildServiceProvider();
        var newCommand = serviceProvider.GetRequiredService<NewCommand>();
 
        const string outputDirectoryName = "JavaStarterOut";
 
        // An explicit --version short-circuits template version resolution, which would otherwise
        // reach for the network. These tests are about the files the writer puts on disk, and the
        // version only reaches aspire.config.json.
        var parseResult = newCommand.Parse(
            $"new {KnownTemplateId.JavaStarter} --name {outputDirectoryName} --output ./{outputDirectoryName} --version 13.0.0 --localhost-tld false");
        _ = await parseResult.InvokeAsync().DefaultTimeout();
 
        return new ScaffoldedTemplate(workspace, Path.Combine(workspace.WorkspaceRoot.FullName, outputDirectoryName));
    }
 
    private sealed class ScaffoldedTemplate(TemporaryWorkspace workspace, string outputDirectory) : IDisposable
    {
        public string OutputDirectory { get; } = outputDirectory;
 
        public void Dispose() => workspace.Dispose();
    }
}