File: Commands\CacheCommandTests.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.Tests.Utils;
using Aspire.Cli.Utils;
using Microsoft.AspNetCore.InternalTesting;
using Microsoft.Extensions.DependencyInjection;
 
namespace Aspire.Cli.Tests.Commands;
 
public class CacheCommandTests(ITestOutputHelper outputHelper)
{
    [Fact]
    public async Task CacheCommand_WithoutSubcommand_ReturnsInvalidCommand()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper);
        using var provider = services.BuildServiceProvider();
 
        var command = provider.GetRequiredService<RootCommand>();
        var result = command.Parse("cache");
 
        var exitCode = await result.InvokeAsync().DefaultTimeout();
 
        Assert.Equal(CliExitCodes.InvalidCommand, exitCode);
    }
 
    [Fact]
    public async Task CacheCommandWithHelpArgumentReturnsZero()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper);
        using var provider = services.BuildServiceProvider();
 
        var command = provider.GetRequiredService<RootCommand>();
        var result = command.Parse("cache --help");
 
        var exitCode = await result.InvokeAsync().DefaultTimeout();
        Assert.Equal(0, exitCode);
    }
 
    [Fact]
    public async Task CacheClear_ClearsPackagesDirectory()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        var packagesDir = new DirectoryInfo(Path.Combine(workspace.WorkspaceRoot.FullName, ".aspire", "packages"));
        var restoreDir = packagesDir.CreateSubdirectory("restore").CreateSubdirectory("ABC123");
        File.WriteAllText(Path.Combine(restoreDir.FullName, "test.dll"), "fake");
 
        var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options =>
        {
            options.PackagesDirectory = packagesDir;
        });
        using var provider = services.BuildServiceProvider();
 
        var command = provider.GetRequiredService<RootCommand>();
        var result = command.Parse("cache clear");
 
        var exitCode = await result.InvokeAsync().DefaultTimeout();
 
        Assert.Equal(CliExitCodes.Success, exitCode);
        Assert.False(File.Exists(Path.Combine(restoreDir.FullName, "test.dll")));
    }
 
    [Fact]
    public async Task CacheClear_ClearsAppHostInfoDiskCache()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        var appHostInfoCacheDir = new DirectoryInfo(Path.Combine(workspace.WorkspaceRoot.FullName, ".aspire", "cache", "apphost-info"));
        appHostInfoCacheDir.Create();
        var cacheEntry = Path.Combine(appHostInfoCacheDir.FullName, "entry.json");
        await File.WriteAllTextAsync(cacheEntry, "{}").DefaultTimeout();
 
        var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper);
        using var provider = services.BuildServiceProvider();
 
        var command = provider.GetRequiredService<RootCommand>();
        var result = command.Parse("cache clear");
 
        var exitCode = await result.InvokeAsync().DefaultTimeout();
 
        Assert.Equal(CliExitCodes.Success, exitCode);
        Assert.False(File.Exists(cacheEntry));
        Assert.False(appHostInfoCacheDir.Exists);
    }
 
    [Fact]
    public async Task CacheClear_ClearsStagingNuGetPackagesCache()
    {
        // Pins that `aspire cache clear` wipes the SHA-keyed staging NuGet package caches under
        // <ASPIRE_HOME>/.nugetpackages — produced by PrebuiltAppHostServer's temporary
        // nuget.config for the staging channel. Without this, a wedged staging restore can only
        // be recovered by manual filesystem surgery.
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
 
        var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper);
        using var provider = services.BuildServiceProvider();
        var executionContext = provider.GetRequiredService<CliExecutionContext>();
 
        var stagingCacheRoot = new DirectoryInfo(
            CliPathHelper.GetStagingNuGetPackagesDirectory(executionContext.AspireHomeDirectory));
        var firstBuildCache = stagingCacheRoot.CreateSubdirectory("deadbeef").CreateSubdirectory("aspire.hosting").CreateSubdirectory("13.4.0");
        var secondBuildCache = stagingCacheRoot.CreateSubdirectory("cafef00d").CreateSubdirectory("aspire.hosting").CreateSubdirectory("13.4.0");
        await File.WriteAllTextAsync(Path.Combine(firstBuildCache.FullName, "Aspire.Hosting.dll"), "fake").DefaultTimeout();
        await File.WriteAllTextAsync(Path.Combine(secondBuildCache.FullName, "Aspire.Hosting.dll"), "fake").DefaultTimeout();
 
        var command = provider.GetRequiredService<RootCommand>();
        var result = command.Parse("cache clear");
 
        var exitCode = await result.InvokeAsync().DefaultTimeout();
 
        Assert.Equal(CliExitCodes.Success, exitCode);
        // SHA-keyed subdirectories should be gone; the parent stays so the next staging restore
        // can populate a fresh cache without recreating the .nugetpackages root.
        Assert.False(Directory.Exists(Path.Combine(stagingCacheRoot.FullName, "deadbeef")));
        Assert.False(Directory.Exists(Path.Combine(stagingCacheRoot.FullName, "cafef00d")));
    }
 
    [Fact]
    public async Task CacheClear_HandlesMissingStagingNuGetPackagesCache()
    {
        // Common case for fresh installs and non-staging users: the staging cache root simply
        // doesn't exist yet. The command must still succeed.
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
 
        var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper);
        using var provider = services.BuildServiceProvider();
        var executionContext = provider.GetRequiredService<CliExecutionContext>();
 
        var stagingCacheRoot = new DirectoryInfo(
            CliPathHelper.GetStagingNuGetPackagesDirectory(executionContext.AspireHomeDirectory));
        Assert.False(stagingCacheRoot.Exists);
 
        var command = provider.GetRequiredService<RootCommand>();
        var result = command.Parse("cache clear");
 
        var exitCode = await result.InvokeAsync().DefaultTimeout();
 
        Assert.Equal(CliExitCodes.Success, exitCode);
    }
 
    [Fact]
    public async Task CacheClear_HandlesNonExistentPackagesDirectory()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        var packagesDir = new DirectoryInfo(Path.Combine(workspace.WorkspaceRoot.FullName, ".aspire", "packages-nonexistent"));
 
        var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options =>
        {
            options.PackagesDirectory = packagesDir;
        });
        using var provider = services.BuildServiceProvider();
 
        var command = provider.GetRequiredService<RootCommand>();
        var result = command.Parse("cache clear");
 
        var exitCode = await result.InvokeAsync().DefaultTimeout();
 
        Assert.Equal(CliExitCodes.Success, exitCode);
    }
 
    [Fact]
    public void ClearDirectoryContents_DeletesFilesAndSubdirectories()
    {
        var tempDir = new DirectoryInfo(Path.Combine(Path.GetTempPath(), $"aspire-test-{Guid.NewGuid():N}"));
        try
        {
            tempDir.Create();
            var subDir = tempDir.CreateSubdirectory("nested");
            File.WriteAllText(Path.Combine(tempDir.FullName, "root.txt"), "root");
            File.WriteAllText(Path.Combine(subDir.FullName, "nested.txt"), "nested");
 
            var deleted = CacheCommand.ClearCommand.ClearDirectoryContents(tempDir);
 
            Assert.Equal(2, deleted);
            Assert.Empty(tempDir.GetFiles("*", SearchOption.AllDirectories));
            Assert.Empty(tempDir.GetDirectories());
        }
        finally
        {
            if (tempDir.Exists)
            {
                tempDir.Delete(recursive: true);
            }
        }
    }
 
    [Fact]
    public void ClearDirectoryContents_RespectsSkipPredicate()
    {
        var tempDir = new DirectoryInfo(Path.Combine(Path.GetTempPath(), $"aspire-test-{Guid.NewGuid():N}"));
        try
        {
            tempDir.Create();
            var keepFile = Path.Combine(tempDir.FullName, "keep.txt");
            var deleteFile = Path.Combine(tempDir.FullName, "delete.txt");
            File.WriteAllText(keepFile, "keep");
            File.WriteAllText(deleteFile, "delete");
 
            var deleted = CacheCommand.ClearCommand.ClearDirectoryContents(
                tempDir,
                skipFile: f => f.Name == "keep.txt");
 
            Assert.Equal(1, deleted);
            Assert.True(File.Exists(keepFile));
            Assert.False(File.Exists(deleteFile));
        }
        finally
        {
            if (tempDir.Exists)
            {
                tempDir.Delete(recursive: true);
            }
        }
    }
 
    [Fact]
    public void ClearDirectoryContents_ReturnsZero_WhenDirectoryDoesNotExist()
    {
        var nonExistent = new DirectoryInfo(Path.Combine(Path.GetTempPath(), $"aspire-test-nonexistent-{Guid.NewGuid():N}"));
 
        var deleted = CacheCommand.ClearCommand.ClearDirectoryContents(nonExistent);
 
        Assert.Equal(0, deleted);
    }
 
    [Fact]
    public void ClearDirectoryContents_ReturnsZero_WhenNull()
    {
        var deleted = CacheCommand.ClearCommand.ClearDirectoryContents(null);
 
        Assert.Equal(0, deleted);
    }
}