File: ImageBuilderTests.cs
Web Access
Project: ..\..\..\test\Microsoft.NET.Build.Containers.UnitTests\Microsoft.NET.Build.Containers.UnitTests.csproj (Microsoft.NET.Build.Containers.UnitTests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Runtime.CompilerServices;
using System.Text.Json.Nodes;
using Microsoft.NET.TestFramework;
using Xunit;
using Xunit.Abstractions;
 
namespace Microsoft.NET.Build.Containers.UnitTests;
 
public class ImageBuilderTests
{
    private readonly TestLoggerFactory _loggerFactory;
 
    private static readonly string StaticKnownDigestValue = "sha256:338c0b702da88157ba4bb706678e43346ece2e4397b888d59fb2d9f6113c8070";
 
    public ImageBuilderTests(ITestOutputHelper output)
    {
        _loggerFactory = new TestLoggerFactory(output);
    }
 
    [Fact]
    public void CanAddLabelsToImage()
    {
        string simpleImageConfig =
            """
                {
                    "architecture": "amd64",
                    "config": {
                      "Hostname": "",
                      "Domainname": "",
                      "User": "",
                      "AttachStdin": false,
                      "AttachStdout": false,
                      "AttachStderr": false,
                      "Tty": false,
                      "OpenStdin": false,
                      "StdinOnce": false,
                      "Env": [
                        "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
                        "ASPNETCORE_URLS=http://+:80",
                        "DOTNET_RUNNING_IN_CONTAINER=true",
                        "DOTNET_VERSION=7.0.2",
                        "ASPNET_VERSION=7.0.2"
                      ],
                      "Cmd": ["bash"],
                      "Image": "sha256:d772d27ebeec80393349a4770dc37f977be2c776a01c88b624d43f93fa369d69",
                      "Volumes": null,
                      "WorkingDir": "",
                      "Entrypoint": null,
                      "OnBuild": null,
                      "Labels": null
                    },
                    "created": "2023-02-04T08:14:52.000901321Z",
                    "os": "linux",
                    "rootfs": {
                      "type": "layers",
                      "diff_ids": [
                        "sha256:bd2fe8b74db65d82ea10db97368d35b92998d4ea0e7e7dc819481fe4a68f64cf",
                        "sha256:94100d1041b650c6f7d7848c550cd98c25d0bdc193d30692e5ea5474d7b3b085",
                        "sha256:53c2a75a33c8f971b4b5036d34764373e134f91ee01d8053b4c3573c42e1cf5d",
                        "sha256:49a61320e585180286535a2545be5722b09e40ad44c7c190b20ec96c9e42e4a3",
                        "sha256:8a379cce2ac272aa71aa029a7bbba85c852ba81711d9f90afaefd3bf5036dc48"
                      ]
                    }
                }
                """;
 
        JsonNode? node = JsonNode.Parse(simpleImageConfig);
        Assert.NotNull(node);
 
        ImageConfig baseConfig = new(node);
 
        baseConfig.AddLabel("testLabel1", "v1");
        baseConfig.AddLabel("testLabel2", "v2");
 
        string readyImage = baseConfig.BuildConfig();
 
        JsonNode? result = JsonNode.Parse(readyImage);
 
        var resultLabels = result?["config"]?["Labels"] as JsonObject;
        Assert.NotNull(resultLabels);
 
        Assert.Equal(2, resultLabels.Count);
        Assert.Equal("v1", resultLabels["testLabel1"]?.ToString());
        Assert.Equal("v2", resultLabels["testLabel2"]?.ToString());
    }
 
    [Fact]
    public void CanPreserveExistingLabels()
    {
        string simpleImageConfig =
            """
                {
                    "architecture": "amd64",
                    "config": {
                      "Hostname": "",
                      "Domainname": "",
                      "User": "",
                      "AttachStdin": false,
                      "AttachStdout": false,
                      "AttachStderr": false,
                      "Tty": false,
                      "OpenStdin": false,
                      "StdinOnce": false,
                      "Env": [
                        "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
                        "ASPNETCORE_URLS=http://+:80",
                        "DOTNET_RUNNING_IN_CONTAINER=true",
                        "DOTNET_VERSION=7.0.2",
                        "ASPNET_VERSION=7.0.2"
                      ],
                      "Cmd": ["bash"],
                      "Image": "sha256:d772d27ebeec80393349a4770dc37f977be2c776a01c88b624d43f93fa369d69",
                      "Volumes": null,
                      "WorkingDir": "",
                      "Entrypoint": null,
                      "OnBuild": null,
                      "Labels":
                      {
                        "existing" : "e1",
                        "existing2" : "e2"
                      }
                    },
                    "created": "2023-02-04T08:14:52.000901321Z",
                    "os": "linux",
                    "rootfs": {
                      "type": "layers",
                      "diff_ids": [
                        "sha256:bd2fe8b74db65d82ea10db97368d35b92998d4ea0e7e7dc819481fe4a68f64cf",
                        "sha256:94100d1041b650c6f7d7848c550cd98c25d0bdc193d30692e5ea5474d7b3b085",
                        "sha256:53c2a75a33c8f971b4b5036d34764373e134f91ee01d8053b4c3573c42e1cf5d",
                        "sha256:49a61320e585180286535a2545be5722b09e40ad44c7c190b20ec96c9e42e4a3",
                        "sha256:8a379cce2ac272aa71aa029a7bbba85c852ba81711d9f90afaefd3bf5036dc48"
                      ]
                    }
                }
                """;
 
        JsonNode? node = JsonNode.Parse(simpleImageConfig);
        Assert.NotNull(node);
 
        ImageConfig baseConfig = new(node);
 
        baseConfig.AddLabel("testLabel1", "v1");
        baseConfig.AddLabel("existing2", "v2");
 
        string readyImage = baseConfig.BuildConfig();
 
        JsonNode? result = JsonNode.Parse(readyImage);
 
        var resultLabels = result?["config"]?["Labels"] as JsonObject;
        Assert.NotNull(resultLabels);
 
        Assert.Equal(3, resultLabels.Count);
        Assert.Equal("v1", resultLabels["testLabel1"]?.ToString());
        Assert.Equal("v2", resultLabels["existing2"]?.ToString());
        Assert.Equal("e1", resultLabels["existing"]?.ToString());
    }
 
    [Fact]
    public void CanAddPortsToImage()
    {
        string simpleImageConfig =
            """
                {
                    "architecture": "amd64",
                    "config": {
                      "Hostname": "",
                      "Domainname": "",
                      "User": "",
                      "AttachStdin": false,
                      "AttachStdout": false,
                      "AttachStderr": false,
                      "Tty": false,
                      "OpenStdin": false,
                      "StdinOnce": false,
                      "Env": [
                        "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
                        "ASPNETCORE_URLS=http://+:80",
                        "DOTNET_RUNNING_IN_CONTAINER=true",
                        "DOTNET_VERSION=7.0.2",
                        "ASPNET_VERSION=7.0.2"
                      ],
                      "Cmd": ["bash"],
                      "Image": "sha256:d772d27ebeec80393349a4770dc37f977be2c776a01c88b624d43f93fa369d69",
                      "Volumes": null,
                      "WorkingDir": "",
                      "Entrypoint": null,
                      "OnBuild": null,
                      "Labels": null
                    },
                    "created": "2023-02-04T08:14:52.000901321Z",
                    "os": "linux",
                    "rootfs": {
                      "type": "layers",
                      "diff_ids": [
                        "sha256:bd2fe8b74db65d82ea10db97368d35b92998d4ea0e7e7dc819481fe4a68f64cf",
                        "sha256:94100d1041b650c6f7d7848c550cd98c25d0bdc193d30692e5ea5474d7b3b085",
                        "sha256:53c2a75a33c8f971b4b5036d34764373e134f91ee01d8053b4c3573c42e1cf5d",
                        "sha256:49a61320e585180286535a2545be5722b09e40ad44c7c190b20ec96c9e42e4a3",
                        "sha256:8a379cce2ac272aa71aa029a7bbba85c852ba81711d9f90afaefd3bf5036dc48"
                      ]
                    }
                }
                """;
 
        JsonNode? node = JsonNode.Parse(simpleImageConfig);
        Assert.NotNull(node);
 
        ImageConfig baseConfig = new(node);
 
        baseConfig.ExposePort(6000, PortType.tcp);
        baseConfig.ExposePort(6010, PortType.udp);
 
        string readyImage = baseConfig.BuildConfig();
 
        JsonNode? result = JsonNode.Parse(readyImage);
 
        var resultPorts = result?["config"]?["ExposedPorts"] as JsonObject;
        Assert.NotNull(resultPorts);
 
        Assert.Equal(2, resultPorts.Count);
        Assert.NotNull(resultPorts["6000/tcp"] as JsonObject);
        Assert.NotNull(resultPorts["6010/udp"] as JsonObject);
    }
 
    [Fact]
    public void CanPreserveExistingPorts()
    {
        string simpleImageConfig =
            """
                {
                    "architecture": "amd64",
                    "config": {
                      "Hostname": "",
                      "Domainname": "",
                      "User": "",
                      "AttachStdin": false,
                      "AttachStdout": false,
                      "AttachStderr": false,
                      "Tty": false,
                      "OpenStdin": false,
                      "StdinOnce": false,
                      "Env": [
                        "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
                        "ASPNETCORE_URLS=http://+:80",
                        "DOTNET_RUNNING_IN_CONTAINER=true",
                        "DOTNET_VERSION=7.0.2",
                        "ASPNET_VERSION=7.0.2"
                      ],
                      "Cmd": ["bash"],
                      "Image": "sha256:d772d27ebeec80393349a4770dc37f977be2c776a01c88b624d43f93fa369d69",
                      "Volumes": null,
                      "WorkingDir": "",
                      "Entrypoint": null,
                      "OnBuild": null,
                      "Labels": null,
                      "ExposedPorts":
                      {
                        "6100/tcp": {},
                        "6200": {}
                      }
                    },
                    "created": "2023-02-04T08:14:52.000901321Z",
                    "os": "linux",
                    "rootfs": {
                      "type": "layers",
                      "diff_ids": [
                        "sha256:bd2fe8b74db65d82ea10db97368d35b92998d4ea0e7e7dc819481fe4a68f64cf",
                        "sha256:94100d1041b650c6f7d7848c550cd98c25d0bdc193d30692e5ea5474d7b3b085",
                        "sha256:53c2a75a33c8f971b4b5036d34764373e134f91ee01d8053b4c3573c42e1cf5d",
                        "sha256:49a61320e585180286535a2545be5722b09e40ad44c7c190b20ec96c9e42e4a3",
                        "sha256:8a379cce2ac272aa71aa029a7bbba85c852ba81711d9f90afaefd3bf5036dc48"
                      ]
                    }
                }
                """;
 
        JsonNode? node = JsonNode.Parse(simpleImageConfig);
        Assert.NotNull(node);
 
        ImageConfig baseConfig = new(node);
 
        baseConfig.ExposePort(6000, PortType.tcp);
        baseConfig.ExposePort(6010, PortType.udp);
        baseConfig.ExposePort(6100, PortType.udp);
        baseConfig.ExposePort(6200, PortType.tcp);
 
        string readyImage = baseConfig.BuildConfig();
 
        JsonNode? result = JsonNode.Parse(readyImage);
 
        var resultPorts = result?["config"]?["ExposedPorts"] as JsonObject;
        Assert.NotNull(resultPorts);
 
        Assert.Equal(5, resultPorts.Count);
        Assert.NotNull(resultPorts["6000/tcp"] as JsonObject);
        Assert.NotNull(resultPorts["6010/udp"] as JsonObject);
        Assert.NotNull(resultPorts["6100/udp"] as JsonObject);
        Assert.NotNull(resultPorts["6100/tcp"] as JsonObject);
        Assert.NotNull(resultPorts["6200/tcp"] as JsonObject);
    }
 
    [Fact]
    public void HistoryEntriesMatchNonEmptyLayers()
    {
        // Note how the base image config is already "corrupt" by having
        // only 5 layers with only 2 history entries. We expect the
        // final config to also have 5 (non empty) history entries.
 
        string simpleImageConfig =
            """
                {
                    "architecture": "amd64",
                    "config": {
                      "Hostname": "",
                      "Domainname": "",
                      "User": "",
                      "AttachStdin": false,
                      "AttachStdout": false,
                      "AttachStderr": false,
                      "Tty": false,
                      "OpenStdin": false,
                      "StdinOnce": false,
                      "Env": [
                        "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
                        "ASPNETCORE_URLS=http://+:80",
                        "DOTNET_RUNNING_IN_CONTAINER=true",
                        "DOTNET_VERSION=7.0.2",
                        "ASPNET_VERSION=7.0.2"
                      ],
                      "Cmd": ["bash"],
                      "Image": "sha256:d772d27ebeec80393349a4770dc37f977be2c776a01c88b624d43f93fa369d69",
                      "Volumes": null,
                      "WorkingDir": "",
                      "Entrypoint": null,
                      "OnBuild": null,
                      "Labels": null,
                      "ExposedPorts":
                      {
                        "6100/tcp": {},
                        "6200": {}
                      }
                    },
                    "created": "2023-02-04T08:14:52.000901321Z",
                    "os": "linux",
                    "rootfs": {
                      "type": "layers",
                      "diff_ids": [
                        "sha256:bd2fe8b74db65d82ea10db97368d35b92998d4ea0e7e7dc819481fe4a68f64cf",
                        "sha256:94100d1041b650c6f7d7848c550cd98c25d0bdc193d30692e5ea5474d7b3b085",
                        "sha256:53c2a75a33c8f971b4b5036d34764373e134f91ee01d8053b4c3573c42e1cf5d",
                        "sha256:49a61320e585180286535a2545be5722b09e40ad44c7c190b20ec96c9e42e4a3",
                        "sha256:8a379cce2ac272aa71aa029a7bbba85c852ba81711d9f90afaefd3bf5036dc48"
                      ]
                    },
                    "history": [{
                        "created": "2023-03-01T04:09:58.669479866Z",
                        "created_by": "/bin/sh -c #(nop) ADD file:493a5b0c8d2d63a1343258b3f9aa5fcd59a93f44fe26ad9e56b094c3a08fd3be in / "
                    }, {
                        "created": "2023-03-01T04:09:59.032972609Z",
                        "created_by": "/bin/sh -c #(nop)  CMD [\u0022bash\u0022]",
                        "empty_layer": true
                    }]
                }
                """;
 
        JsonNode? node = JsonNode.Parse(simpleImageConfig);
        Assert.NotNull(node);
 
        ImageConfig baseConfig = new(node);
 
        string readyImage = baseConfig.BuildConfig();
 
        JsonNode? result = JsonNode.Parse(readyImage);
 
        var historyNode = result?["history"];
        Assert.NotNull(historyNode);
 
        var layerDiffsNode = result?["rootfs"]?["diff_ids"];
        Assert.NotNull(layerDiffsNode);
 
        int nonEmptyHistoryNodes = historyNode.AsArray()
            .Count(h => h?.AsObject()["empty_layer"]?.GetValue<bool>() is null or false);
        int layerCount = layerDiffsNode.AsArray().Count;
        Assert.Equal(nonEmptyHistoryNodes, layerCount);
    }
 
 
    [Fact]
    public void CanSetUserFromAppUIDEnvVarFromBaseImage()
    {
        var expectedUid = "12345";
        var builder = FromBaseImageConfig($$"""
                {
                    "architecture": "amd64",
                    "config": {
                      "Hostname": "",
                      "Domainname": "",
                      "User": "",
                      "Env": [
                        "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
                        "{{ImageBuilder.EnvironmentVariables.APP_UID}}={{expectedUid}}"
                      ],
                      "Cmd": ["bash"],
                      "Image": "sha256:d772d27ebeec80393349a4770dc37f977be2c776a01c88b624d43f93fa369d69",
                      "WorkingDir": ""
                    },
                    "created": "2023-02-04T08:14:52.000901321Z",
                    "os": "linux",
                    "rootfs": {
                      "type": "layers",
                      "diff_ids": [
                        "sha256:bd2fe8b74db65d82ea10db97368d35b92998d4ea0e7e7dc819481fe4a68f64cf",
                        "sha256:94100d1041b650c6f7d7848c550cd98c25d0bdc193d30692e5ea5474d7b3b085",
                        "sha256:53c2a75a33c8f971b4b5036d34764373e134f91ee01d8053b4c3573c42e1cf5d",
                        "sha256:49a61320e585180286535a2545be5722b09e40ad44c7c190b20ec96c9e42e4a3",
                        "sha256:8a379cce2ac272aa71aa029a7bbba85c852ba81711d9f90afaefd3bf5036dc48"
                      ]
                    }
                }
                """);
 
        var builtImage = builder.Build();
 
        JsonNode? result = JsonNode.Parse(builtImage.Config);
        Assert.NotNull(result);
        var assignedUid = result["config"]?["User"]?.GetValue<string>();
        Assert.Equal(assignedUid, expectedUid);
    }
 
    [Fact]
    public void CanSetUserFromAppUIDEnvVarFromUser()
    {
        var expectedUid = "12345";
        var builder = FromBaseImageConfig($$"""
                {
                    "architecture": "amd64",
                    "config": {
                      "Hostname": "",
                      "Domainname": "",
                      "User": "",
                      "Env": [
                        "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
                      ],
                      "Cmd": ["bash"],
                      "Image": "sha256:d772d27ebeec80393349a4770dc37f977be2c776a01c88b624d43f93fa369d69",
                      "WorkingDir": ""
                    },
                    "created": "2023-02-04T08:14:52.000901321Z",
                    "os": "linux",
                    "rootfs": {
                      "type": "layers",
                      "diff_ids": [
                        "sha256:bd2fe8b74db65d82ea10db97368d35b92998d4ea0e7e7dc819481fe4a68f64cf",
                        "sha256:94100d1041b650c6f7d7848c550cd98c25d0bdc193d30692e5ea5474d7b3b085",
                        "sha256:53c2a75a33c8f971b4b5036d34764373e134f91ee01d8053b4c3573c42e1cf5d",
                        "sha256:49a61320e585180286535a2545be5722b09e40ad44c7c190b20ec96c9e42e4a3",
                        "sha256:8a379cce2ac272aa71aa029a7bbba85c852ba81711d9f90afaefd3bf5036dc48"
                      ]
                    }
                }
                """);
 
        builder.AddEnvironmentVariable(ImageBuilder.EnvironmentVariables.APP_UID, "12345");
        var builtImage = builder.Build();
 
        JsonNode? result = JsonNode.Parse(builtImage.Config);
        Assert.NotNull(result);
        var assignedUser = result["config"]?["User"]?.GetValue<string>();
        Assert.Equal(assignedUser, expectedUid);
    }
 
    [InlineData("ASPNETCORE_URLS", "https://*:12345;http://+:1234;http://localhost:123;http://1.2.3.4:12", 12345, 1234, 123, 12)]
    [InlineData("ASPNETCORE_HTTP_PORTS", "999;666", 999, 666)]
    [InlineData("ASPNETCORE_HTTPS_PORTS", "456;789", 456, 789)]
    [Theory]
    public void CanSetPortFromEnvVarFromBaseImage(string envVar, string envValue, params int[] expectedPorts)
    {
        var builder = FromBaseImageConfig($$"""
        {
            "architecture": "amd64",
            "config": {
                "Hostname": "",
                "Domainname": "",
                "User": "",
                "Env": [
                "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
                "{{envVar}}={{envValue}}"
                ],
                "Cmd": ["bash"],
                "Image": "sha256:d772d27ebeec80393349a4770dc37f977be2c776a01c88b624d43f93fa369d69",
                "WorkingDir": ""
            },
            "created": "2023-02-04T08:14:52.000901321Z",
            "os": "linux",
            "rootfs": {
                "type": "layers",
                "diff_ids": [
                "sha256:bd2fe8b74db65d82ea10db97368d35b92998d4ea0e7e7dc819481fe4a68f64cf",
                "sha256:94100d1041b650c6f7d7848c550cd98c25d0bdc193d30692e5ea5474d7b3b085",
                "sha256:53c2a75a33c8f971b4b5036d34764373e134f91ee01d8053b4c3573c42e1cf5d",
                "sha256:49a61320e585180286535a2545be5722b09e40ad44c7c190b20ec96c9e42e4a3",
                "sha256:8a379cce2ac272aa71aa029a7bbba85c852ba81711d9f90afaefd3bf5036dc48"
                ]
            }
        }
        """);
 
        var builtImage = builder.Build();
 
        JsonNode? result = JsonNode.Parse(builtImage.Config);
        Assert.NotNull(result);
        var portsObject = result["config"]?["ExposedPorts"]?.AsObject();
        var assignedPorts = portsObject?.AsEnumerable().Select(portString => int.Parse(portString.Key.Split('/')[0])).ToArray();
        Assert.Equal(assignedPorts, expectedPorts);
    }
 
    [InlineData("ASPNETCORE_URLS", "https://*:12345;http://+:1234;http://localhost:123;http://1.2.3.4:12", 12345, 1234, 123, 12)]
    [InlineData("ASPNETCORE_HTTP_PORTS", "999;666", 999, 666)]
    [InlineData("ASPNETCORE_HTTPS_PORTS", "456;789", 456, 789)]
    [Theory]
    public void CanSetPortFromEnvVarFromUser(string envVar, string envValue, params int[] expectedPorts)
    {
        var builder = FromBaseImageConfig($$"""
        {
            "architecture": "amd64",
            "config": {
                "Hostname": "",
                "Domainname": "",
                "User": "",
                "Env": [
                "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
                ],
                "Cmd": ["bash"],
                "Image": "sha256:d772d27ebeec80393349a4770dc37f977be2c776a01c88b624d43f93fa369d69",
                "WorkingDir": ""
            },
            "created": "2023-02-04T08:14:52.000901321Z",
            "os": "linux",
            "rootfs": {
                "type": "layers",
                "diff_ids": [
                "sha256:bd2fe8b74db65d82ea10db97368d35b92998d4ea0e7e7dc819481fe4a68f64cf",
                "sha256:94100d1041b650c6f7d7848c550cd98c25d0bdc193d30692e5ea5474d7b3b085",
                "sha256:53c2a75a33c8f971b4b5036d34764373e134f91ee01d8053b4c3573c42e1cf5d",
                "sha256:49a61320e585180286535a2545be5722b09e40ad44c7c190b20ec96c9e42e4a3",
                "sha256:8a379cce2ac272aa71aa029a7bbba85c852ba81711d9f90afaefd3bf5036dc48"
                ]
            }
        }
        """);
 
        builder.AddEnvironmentVariable(envVar, envValue);
 
        var builtImage = builder.Build();
 
        JsonNode? result = JsonNode.Parse(builtImage.Config);
        Assert.NotNull(result);
        var portsObject = result["config"]?["ExposedPorts"]?.AsObject();
        var assignedPorts = portsObject?.AsEnumerable().Select(portString => int.Parse(portString.Key.Split('/')[0])).ToArray();
        Assert.Equal(assignedPorts, expectedPorts);
    }
 
 
    [Fact]
    public void CanSetContainerUserAndOverrideAppUID()
    {
        var userId = "1646";
        var baseConfigBuilder = FromBaseImageConfig($$"""
        {
            "architecture": "amd64",
            "config": {
                "Hostname": "",
                "Domainname": "",
                "User": "",
                "Env": [
                "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
                "{{ImageBuilder.EnvironmentVariables.APP_UID}}=12345"
                ],
                "Cmd": ["bash"],
                "Image": "sha256:d772d27ebeec80393349a4770dc37f977be2c776a01c88b624d43f93fa369d69",
                "WorkingDir": ""
            },
            "created": "2023-02-04T08:14:52.000901321Z",
            "os": "linux",
            "rootfs": {
                "type": "layers",
                "diff_ids": [
                "sha256:bd2fe8b74db65d82ea10db97368d35b92998d4ea0e7e7dc819481fe4a68f64cf",
                "sha256:94100d1041b650c6f7d7848c550cd98c25d0bdc193d30692e5ea5474d7b3b085",
                "sha256:53c2a75a33c8f971b4b5036d34764373e134f91ee01d8053b4c3573c42e1cf5d",
                "sha256:49a61320e585180286535a2545be5722b09e40ad44c7c190b20ec96c9e42e4a3",
                "sha256:8a379cce2ac272aa71aa029a7bbba85c852ba81711d9f90afaefd3bf5036dc48"
                ]
            }
        }
        """);
 
        baseConfigBuilder.SetUser(userId);
        var config = JsonNode.Parse(baseConfigBuilder.Build().Config);
        config!["config"]?["User"]?.GetValue<string>().Should().Be(expected: userId, because: "The precedence of SetUser should override inferred user ids");
    }
 
    [Fact]
    public void WhenMultipleUrlSourcesAreSetOnlyAspnetcoreUrlsIsUsed()
    {
        int[] expected = [12345];
        var builder = FromBaseImageConfig($$"""
        {
            "architecture": "amd64",
            "config": {
                "Hostname": "",
                "Domainname": "",
                "User": "",
                "Env": [
                "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
                ],
                "Cmd": ["bash"],
                "Image": "sha256:d772d27ebeec80393349a4770dc37f977be2c776a01c88b624d43f93fa369d69",
                "WorkingDir": ""
            },
            "created": "2023-02-04T08:14:52.000901321Z",
            "os": "linux",
            "rootfs": {
                "type": "layers",
                "diff_ids": [
                "sha256:bd2fe8b74db65d82ea10db97368d35b92998d4ea0e7e7dc819481fe4a68f64cf",
                "sha256:94100d1041b650c6f7d7848c550cd98c25d0bdc193d30692e5ea5474d7b3b085",
                "sha256:53c2a75a33c8f971b4b5036d34764373e134f91ee01d8053b4c3573c42e1cf5d",
                "sha256:49a61320e585180286535a2545be5722b09e40ad44c7c190b20ec96c9e42e4a3",
                "sha256:8a379cce2ac272aa71aa029a7bbba85c852ba81711d9f90afaefd3bf5036dc48"
                ]
            }
        }
        """);
 
        builder.AddEnvironmentVariable(ImageBuilder.EnvironmentVariables.ASPNETCORE_URLS, "https://*:12345");
        builder.AddEnvironmentVariable(ImageBuilder.EnvironmentVariables.ASPNETCORE_HTTPS_PORTS, "456");
        var builtImage = builder.Build();
        JsonNode? result = JsonNode.Parse(builtImage.Config);
        Assert.NotNull(result);
        var portsObject = result["config"]?["ExposedPorts"]?.AsObject();
        var assignedPorts = portsObject?.AsEnumerable().Select(portString => int.Parse(portString.Key.Split('/')[0])).ToArray();
        Assert.Equal(expected, assignedPorts);
    }
 
    [Fact]
    public void CanSetBaseImageDigestLabel()
    {
        var builder = FromBaseImageConfig($$"""
        {
            "architecture": "amd64",
            "config": {
                "Hostname": "",
                "Domainname": "",
                "User": "",
                "Env": [
                "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
                ],
                "Cmd": ["bash"],
                "Image": "sha256:d772d27ebeec80393349a4770dc37f977be2c776a01c88b624d43f93fa369d69",
                "WorkingDir": ""
            },
            "created": "2023-02-04T08:14:52.000901321Z",
            "os": "linux",
            "rootfs": {
                "type": "layers",
                "diff_ids": [
                "sha256:bd2fe8b74db65d82ea10db97368d35b92998d4ea0e7e7dc819481fe4a68f64cf",
                "sha256:94100d1041b650c6f7d7848c550cd98c25d0bdc193d30692e5ea5474d7b3b085",
                "sha256:53c2a75a33c8f971b4b5036d34764373e134f91ee01d8053b4c3573c42e1cf5d",
                "sha256:49a61320e585180286535a2545be5722b09e40ad44c7c190b20ec96c9e42e4a3",
                "sha256:8a379cce2ac272aa71aa029a7bbba85c852ba81711d9f90afaefd3bf5036dc48"
                ]
            }
        }
        """);
 
        builder.AddBaseImageDigestLabel();
        var builtImage = builder.Build();
        JsonNode? result = JsonNode.Parse(builtImage.Config);
        Assert.NotNull(result);
        var labels = result["config"]?["Labels"]?.AsObject();
        var digest = labels?.AsEnumerable().First(label => label.Key == "org.opencontainers.image.base.digest").Value!;
        digest.GetValue<string>().Should().Be(StaticKnownDigestValue);
    }
 
    private ImageBuilder FromBaseImageConfig(string baseImageConfig, [CallerMemberName] string testName = "")
    {
        var manifest = new ManifestV2()
        {
            SchemaVersion = 2,
            MediaType = SchemaTypes.DockerManifestV2,
            Config = new ManifestConfig()
            {
                mediaType = "",
                size = 0,
                digest = "sha256:"
            },
            Layers = new List<ManifestLayer>(),
            KnownDigest = StaticKnownDigestValue
        };
        return new ImageBuilder(manifest, manifest.MediaType, new ImageConfig(baseImageConfig), _loggerFactory.CreateLogger(testName));
    }
}