File: FileSystemServiceTests.cs
Web Access
Project: src\tests\Aspire.Hosting.Tests\Aspire.Hosting.Tests.csproj (Aspire.Hosting.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 ASPIREFILESYSTEM001 // Type is for evaluation purposes only
 
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
 
namespace Aspire.Hosting.Tests;
 
public class FileSystemServiceTests
{
    private static IConfiguration CreateConfiguration(bool preserveTempFiles = false)
    {
        var configDict = new Dictionary<string, string?>();
        if (preserveTempFiles)
        {
            configDict["ASPIRE_PRESERVE_TEMP_FILES"] = "true";
        }
        return new ConfigurationBuilder()
            .AddInMemoryCollection(configDict)
            .Build();
    }
 
    [Fact]
    public void CreateTempSubdirectory_CreatesDirectory()
    {
        var service = new FileSystemService(CreateConfiguration());
 
        using var tempDir = service.TempDirectory.CreateTempSubdirectory();
 
        Assert.NotNull(tempDir.Path);
        Assert.True(Directory.Exists(tempDir.Path));
    }
 
    [Fact]
    public void CreateTempSubdirectory_WithPrefix_CreatesDirectoryWithPrefix()
    {
        var service = new FileSystemService(CreateConfiguration());
 
        using var tempDir = service.TempDirectory.CreateTempSubdirectory("test-prefix");
 
        Assert.NotNull(tempDir.Path);
        Assert.True(Directory.Exists(tempDir.Path));
        Assert.Contains("test-prefix", tempDir.Path);
    }
 
    [Fact]
    public void CreateTempSubdirectory_Dispose_DeletesDirectory()
    {
        var service = new FileSystemService(CreateConfiguration());
        var tempDir = service.TempDirectory.CreateTempSubdirectory();
        var path = tempDir.Path;
 
        Assert.True(Directory.Exists(path));
 
        tempDir.Dispose();
 
        Assert.False(Directory.Exists(path));
    }
 
    [Fact]
    public void CreateTempFile_CreatesFile()
    {
        var service = new FileSystemService(CreateConfiguration());
 
        using var tempFile = service.TempDirectory.CreateTempFile();
 
        Assert.NotNull(tempFile.Path);
        Assert.True(File.Exists(tempFile.Path));
    }
 
    [Fact]
    public void CreateTempFile_WithFileName_CreatesNamedFile()
    {
        var service = new FileSystemService(CreateConfiguration());
 
        using var tempFile = service.TempDirectory.CreateTempFile("config.json");
 
        Assert.NotNull(tempFile.Path);
        Assert.True(File.Exists(tempFile.Path));
        Assert.EndsWith("config.json", tempFile.Path);
    }
 
    [Fact]
    public void CreateTempFile_Dispose_DeletesFile()
    {
        var service = new FileSystemService(CreateConfiguration());
        var tempFile = service.TempDirectory.CreateTempFile();
        var path = tempFile.Path;
 
        Assert.True(File.Exists(path));
 
        tempFile.Dispose();
 
        Assert.False(File.Exists(path));
    }
 
    [Fact]
    public void CreateTempFile_WithFileName_Dispose_DeletesFileAndParentDirectory()
    {
        var service = new FileSystemService(CreateConfiguration());
        var tempFile = service.TempDirectory.CreateTempFile("test-file.txt");
        var filePath = tempFile.Path;
        var parentDir = Path.GetDirectoryName(filePath);
 
        Assert.True(File.Exists(filePath));
        Assert.True(Directory.Exists(parentDir));
 
        tempFile.Dispose();
 
        Assert.False(File.Exists(filePath));
        Assert.False(Directory.Exists(parentDir));
    }
 
    [Fact]
    public void PathExtractionPattern_DirectoryPersistsAfterScopeEnds()
    {
        // This test verifies the intentional pattern of extracting .Path
        // without disposing, which is common in the codebase
        var service = new FileSystemService(CreateConfiguration());
        string? extractedPath;
 
        // Simulate the common pattern: extract path, let object go out of scope
        {
            var tempDir = service.TempDirectory.CreateTempSubdirectory("path-extraction-test");
            extractedPath = tempDir.Path;
            // Note: intentionally NOT disposing here - this is the pattern used in production
        }
 
        // The directory should still exist because we didn't dispose
        Assert.True(Directory.Exists(extractedPath));
 
        // Clean up manually for test hygiene
        Directory.Delete(extractedPath, recursive: true);
    }
 
    [Fact]
    public void PathExtractionPattern_FilePersistsAfterScopeEnds()
    {
        // This test verifies the intentional pattern of extracting .Path
        // without disposing, which is common in the codebase
        var service = new FileSystemService(CreateConfiguration());
        string? extractedPath;
 
        // Simulate the common pattern: extract path, let object go out of scope
        {
            var tempFile = service.TempDirectory.CreateTempFile();
            extractedPath = tempFile.Path;
            // Note: intentionally NOT disposing here - this is the pattern used in production
        }
 
        // The file should still exist because we didn't dispose
        Assert.True(File.Exists(extractedPath));
 
        // Clean up manually for test hygiene
        File.Delete(extractedPath);
    }
 
    [Fact]
    public void TempDirectory_Property_ReturnsSameInstance()
    {
        var service = new FileSystemService(CreateConfiguration());
 
        var tempDir1 = service.TempDirectory;
        var tempDir2 = service.TempDirectory;
 
        Assert.Same(tempDir1, tempDir2);
    }
 
    [Fact]
    public void CreateTempSubdirectory_MultipleCallsCreateDifferentDirectories()
    {
        var service = new FileSystemService(CreateConfiguration());
 
        using var tempDir1 = service.TempDirectory.CreateTempSubdirectory();
        using var tempDir2 = service.TempDirectory.CreateTempSubdirectory();
 
        Assert.NotEqual(tempDir1.Path, tempDir2.Path);
        Assert.True(Directory.Exists(tempDir1.Path));
        Assert.True(Directory.Exists(tempDir2.Path));
    }
 
    [Fact]
    public void CreateTempFile_MultipleCallsCreateDifferentFiles()
    {
        var service = new FileSystemService(CreateConfiguration());
 
        using var tempFile1 = service.TempDirectory.CreateTempFile();
        using var tempFile2 = service.TempDirectory.CreateTempFile();
 
        Assert.NotEqual(tempFile1.Path, tempFile2.Path);
        Assert.True(File.Exists(tempFile1.Path));
        Assert.True(File.Exists(tempFile2.Path));
    }
 
    [Fact]
    public void Dispose_CalledMultipleTimes_DoesNotThrow()
    {
        var service = new FileSystemService(CreateConfiguration());
        var tempDir = service.TempDirectory.CreateTempSubdirectory();
        var tempFile = service.TempDirectory.CreateTempFile();
 
        // First dispose
        tempDir.Dispose();
        tempFile.Dispose();
 
        // Second dispose should not throw
        tempDir.Dispose();
        tempFile.Dispose();
    }
 
    [Fact]
    public void ServiceDispose_CleansUpUndisposedFiles()
    {
        var service = new FileSystemService(CreateConfiguration());
        
        // Create temp files/dirs without disposing them
        var tempDir = service.TempDirectory.CreateTempSubdirectory();
        var tempFile = service.TempDirectory.CreateTempFile();
        var dirPath = tempDir.Path;
        var filePath = tempFile.Path;
 
        Assert.True(Directory.Exists(dirPath));
        Assert.True(File.Exists(filePath));
 
        // Dispose the service - should clean up remaining files
        service.Dispose();
 
        Assert.False(Directory.Exists(dirPath));
        Assert.False(File.Exists(filePath));
    }
 
    [Fact]
    public void ServiceDispose_WithPreserveConfiguration_SkipsCleanup()
    {
        var service = new FileSystemService(CreateConfiguration(preserveTempFiles: true));
        
        var tempDir = service.TempDirectory.CreateTempSubdirectory();
        var tempFile = service.TempDirectory.CreateTempFile();
        var dirPath = tempDir.Path;
        var filePath = tempFile.Path;
 
        Assert.True(Directory.Exists(dirPath));
        Assert.True(File.Exists(filePath));
 
        // Dispose the service - should NOT clean up files
        service.Dispose();
 
        // Files should still exist
        Assert.True(Directory.Exists(dirPath));
        Assert.True(File.Exists(filePath));
 
        // Clean up manually
        Directory.Delete(dirPath, recursive: true);
        File.Delete(filePath);
    }
 
    [Fact]
    public void TempDirectory_Dispose_WithPreserveConfiguration_SkipsCleanup()
    {
        var service = new FileSystemService(CreateConfiguration(preserveTempFiles: true));
        
        var tempDir = service.TempDirectory.CreateTempSubdirectory();
        var dirPath = tempDir.Path;
 
        Assert.True(Directory.Exists(dirPath));
 
        // Dispose the temp directory - should NOT clean up
        tempDir.Dispose();
 
        // Directory should still exist
        Assert.True(Directory.Exists(dirPath));
 
        // Clean up manually
        Directory.Delete(dirPath, recursive: true);
    }
 
    [Fact]
    public void TempFile_Dispose_WithPreserveConfiguration_SkipsCleanup()
    {
        var service = new FileSystemService(CreateConfiguration(preserveTempFiles: true));
        
        var tempFile = service.TempDirectory.CreateTempFile();
        var filePath = tempFile.Path;
 
        Assert.True(File.Exists(filePath));
 
        // Dispose the temp file - should NOT clean up
        tempFile.Dispose();
 
        // File should still exist
        Assert.True(File.Exists(filePath));
 
        // Clean up manually
        File.Delete(filePath);
    }
 
    [Fact]
    public void ServiceDispose_WithMixedDisposedAndUndisposedItems_CleansUpOnlyUndisposed()
    {
        var service = new FileSystemService(CreateConfiguration());
        
        // Create multiple temp items
        var tempDir1 = service.TempDirectory.CreateTempSubdirectory();
        var tempDir2 = service.TempDirectory.CreateTempSubdirectory();
        var tempFile1 = service.TempDirectory.CreateTempFile();
        var tempFile2 = service.TempDirectory.CreateTempFile();
        
        var dir1Path = tempDir1.Path;
        var dir2Path = tempDir2.Path;
        var file1Path = tempFile1.Path;
        var file2Path = tempFile2.Path;
 
        // Dispose some of them
        tempDir1.Dispose();
        tempFile1.Dispose();
 
        Assert.False(Directory.Exists(dir1Path));
        Assert.False(File.Exists(file1Path));
        Assert.True(Directory.Exists(dir2Path));
        Assert.True(File.Exists(file2Path));
 
        // Dispose the service - should clean up remaining undisposed items
        service.Dispose();
 
        Assert.False(Directory.Exists(dir2Path));
        Assert.False(File.Exists(file2Path));
    }
 
    [Fact]
    public void ServiceDispose_EmptyTracking_DoesNotThrow()
    {
        var service = new FileSystemService(CreateConfiguration());
        
        // Dispose without creating any temp items
        service.Dispose();
        
        // Dispose again should also not throw
        service.Dispose();
    }
 
    [Fact]
    public void ServiceDispose_CalledMultipleTimes_IsIdempotent()
    {
        var service = new FileSystemService(CreateConfiguration());
        
        // Create temp items
        var tempDir = service.TempDirectory.CreateTempSubdirectory();
        var tempFile = service.TempDirectory.CreateTempFile();
        var dirPath = tempDir.Path;
        var filePath = tempFile.Path;
 
        // First dispose - should clean up
        service.Dispose();
 
        Assert.False(Directory.Exists(dirPath));
        Assert.False(File.Exists(filePath));
 
        // Second dispose - should be a no-op and not throw
        service.Dispose();
    }
 
    [Fact]
    public void CreateTempFile_WithFileName_TracksOnlyFile()
    {
        var service = new FileSystemService(CreateConfiguration());
        
        var tempFile = service.TempDirectory.CreateTempFile("test.txt");
        var filePath = tempFile.Path;
        var parentDir = Path.GetDirectoryName(filePath)!;
 
        // Both file and parent dir should exist
        Assert.True(File.Exists(filePath));
        Assert.True(Directory.Exists(parentDir));
 
        // Dispose the temp file - should delete both file and parent directory
        tempFile.Dispose();
 
        Assert.False(File.Exists(filePath));
        Assert.False(Directory.Exists(parentDir));
    }
 
    [Fact]
    public void CreateTempFile_AfterServiceDisposed_ThrowsObjectDisposedException()
    {
        var service = new FileSystemService(CreateConfiguration());
        
        // Dispose the service
        service.Dispose();
 
        // Attempting to create a temp file after disposal should throw
        Assert.Throws<ObjectDisposedException>(() => service.TempDirectory.CreateTempFile());
    }
 
    [Fact]
    public void CreateTempDirectory_AfterServiceDisposed_ThrowsObjectDisposedException()
    {
        var service = new FileSystemService(CreateConfiguration());
        
        // Dispose the service
        service.Dispose();
 
        // Attempting to create a temp directory after disposal should throw
        Assert.Throws<ObjectDisposedException>(() => service.TempDirectory.CreateTempSubdirectory());
    }
 
    [Fact]
    public void SetLogger_CanBeCalledWithoutError()
    {
        var service = new FileSystemService(CreateConfiguration());
        var loggerFactory = LoggerFactory.Create(builder => builder.AddConsole());
        var logger = loggerFactory.CreateLogger<FileSystemService>();
        
        // Should not throw
        service.SetLogger(logger);
        
        // Should still work normally
        using var tempDir = service.TempDirectory.CreateTempSubdirectory();
        Assert.True(Directory.Exists(tempDir.Path));
    }
}