File: ApplicationModel\Docker\DockerfileBuilderEdgeCasesTests.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 ASPIREDOCKERFILEBUILDER001 // Type is for evaluation purposes only and is subject to change or removal in future updates
 
using System.Collections.ObjectModel;
using System.Text;
using Aspire.Hosting.ApplicationModel.Docker;
 
namespace Aspire.Hosting.Tests.ApplicationModel.Docker;
 
public class DockerfileBuilderEdgeCasesTests
{
    [Fact]
    public async Task EmptyDockerfile_WritesNothing()
    {
        // Arrange
        var builder = new DockerfileBuilder();
        using var stream = new MemoryStream();
        using var writer = new StreamWriter(stream, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false));
 
        // Act
        await builder.WriteAsync(writer);
        await writer.FlushAsync();
 
        // Assert
        Assert.Equal(0, stream.Length);
    }
 
    [Fact]
    public async Task SingleStageWithOnlyFrom_WritesOnlyFromStatement()
    {
        // Arrange
        var builder = new DockerfileBuilder();
        builder.From("ubuntu");
        using var stream = new MemoryStream();
        using var writer = new StreamWriter(stream, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false));
        writer.NewLine = "\n"; // Use LF line endings for Dockerfiles
 
        // Act
        await builder.WriteAsync(writer);
        await writer.FlushAsync();
 
        // Assert
        var content = Encoding.UTF8.GetString(stream.ToArray());
        Assert.Equal("FROM ubuntu\n", content);
    }
 
    [Fact]
    public void EnvStatement_WithEmptyStringValue_AllowedAndWorksCorrectly()
    {
        // Arrange
        var builder = new DockerfileBuilder();
        var stage = builder.From("alpine");
 
        // Act & Assert - Should not throw
        stage.Env("EMPTY_VAR", "");
        Assert.Equal(2, stage.Statements.Count);
    }
 
    [Fact]
    public async Task EnvStatement_WithEmptyStringValue_WritesCorrectly()
    {
        // Arrange
        var builder = new DockerfileBuilder();
        var stage = builder.From("alpine");
        stage.Env("EMPTY_VAR", "");
        using var stream = new MemoryStream();
        using var writer = new StreamWriter(stream, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false));
        writer.NewLine = "\n"; // Use LF line endings for Dockerfiles
 
        // Act
        await builder.WriteAsync(writer);
        await writer.FlushAsync();
 
        // Assert
        var content = Encoding.UTF8.GetString(stream.ToArray());
        var expectedContent = """
            FROM alpine
            ENV EMPTY_VAR=
 
            """.ReplaceLineEndings("\n");
        Assert.Equal(expectedContent, content);
    }
 
    [Fact]
    public async Task CmdStatement_WithComplexCommand_SerializesCorrectly()
    {
        // Arrange
        var builder = new DockerfileBuilder();
        var stage = builder.From("node");
        stage.Cmd(["node", "-e", "console.log('Hello, World!')", "--port", "3000"]);
        using var stream = new MemoryStream();
        using var writer = new StreamWriter(stream, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false));
        writer.NewLine = "\n"; // Use LF line endings for Dockerfiles
 
        // Act
        await builder.WriteAsync(writer);
        await writer.FlushAsync();
 
        // Assert
        var content = Encoding.UTF8.GetString(stream.ToArray());
        var expectedContent = """
            FROM node
            CMD ["node","-e","console.log('Hello, World!')","--port","3000"]
 
            """.ReplaceLineEndings("\n");
        Assert.Equal(expectedContent, content);
    }
 
    [Fact]
    public async Task StageOrdering_PreservesSequence()
    {
        // Arrange
        var builder = new DockerfileBuilder();
        
        // Add stages in specific order
        _ = builder.From("alpine:3.16", "first");
        _ = builder.From("node:18", "second");
        _ = builder.From("nginx", "third");
 
        using var stream = new MemoryStream();
        using var writer = new StreamWriter(stream, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false));
        writer.NewLine = "\n"; // Use LF line endings for Dockerfiles
 
        // Act
        await builder.WriteAsync(writer);
        await writer.FlushAsync();
 
        // Assert
        var content = Encoding.UTF8.GetString(stream.ToArray());
        var expectedContent = """
            FROM alpine:3.16 AS first
 
            FROM node:18 AS second
 
            FROM nginx AS third
 
            """.ReplaceLineEndings("\n");
        Assert.Equal(expectedContent, content);
    }
 
    [Fact]
    public void StatementCollection_IsModifiable()
    {
        // Arrange
        var builder = new DockerfileBuilder();
        var stage = builder.From("ubuntu");
        
        // Act
        stage.Run("apt-get update");
        stage.Run("apt-get install -y curl");
        
        // Verify we can access and modify the collection directly
        Assert.Equal(3, stage.Statements.Count); // FROM + 2 RUN
        
        // Remove the first RUN statement (index 1, since FROM is at index 0)
        stage.Statements.RemoveAt(1);
        
        // Assert
        Assert.Equal(2, stage.Statements.Count); // FROM + 1 RUN
    }
 
    [Fact]
    public async Task RunStatement_WithMultiLineCommand_PreservesFormatting()
    {
        // Arrange
        var builder = new DockerfileBuilder();
        var stage = builder.From("ubuntu");
        var multiLineCommand = """
            apt-get update && \
            apt-get install -y \
                curl \
                wget \
                vim && \
            rm -rf /var/lib/apt/lists/*
            """.ReplaceLineEndings("\n");
        stage.Run(multiLineCommand);
        using var stream = new MemoryStream();
        using var writer = new StreamWriter(stream, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false));
        writer.NewLine = "\n"; // Use LF line endings for Dockerfiles
 
        // Act
        await builder.WriteAsync(writer);
        await writer.FlushAsync();
 
        // Assert
        var content = Encoding.UTF8.GetString(stream.ToArray());
        var expectedContent = $$"""
            FROM ubuntu
            RUN {{multiLineCommand}}

            """.ReplaceLineEndings("\n");
        Assert.Equal(expectedContent, content);
    }
 
    [Fact]
    public void ReadOnlyStagesCollection_PreventsDirectModification()
    {
        // Arrange
        var builder = new DockerfileBuilder();
        builder.From("alpine");
 
        // Act & Assert
        Assert.IsType<ReadOnlyCollection<DockerfileStage>>(builder.Stages);
        Assert.Single(builder.Stages);
    }
 
    [Fact]
    public async Task WriteAsync_WithLargeDockerfile_HandlesCorrectly()
    {
        // Arrange
        var builder = new DockerfileBuilder();
        var stage = builder.From("ubuntu:20.04");
        
        // Add many statements to test performance and correctness
        for (var i = 0; i < 100; i++)
        {
            stage.Run($"echo 'Statement {i}'");
        }
        
        using var stream = new MemoryStream();
        using var writer = new StreamWriter(stream, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false));
 
        // Act
        await builder.WriteAsync(writer);
        await writer.FlushAsync();
 
        // Assert
        var content = Encoding.UTF8.GetString(stream.ToArray());
        var lines = content.Split('\n', StringSplitOptions.RemoveEmptyEntries);
        
        Assert.Equal(101, lines.Length); // FROM + 100 RUN statements
        Assert.StartsWith("FROM ubuntu:20.04", lines[0]);
        Assert.StartsWith("RUN echo 'Statement 0'", lines[1]);
        Assert.StartsWith("RUN echo 'Statement 99'", lines[100]);
    }
}