| 
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Net;
using System.Net.Security;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using Microsoft.DotNet.Cli.Utils;
using Microsoft.Extensions.Logging;
using Microsoft.NET.Build.Containers.Resources;
using System.Net.Sockets;
using Moq;
 
namespace Microsoft.NET.Build.Containers.UnitTests;
 
public class RegistryTests : IDisposable
{
    private ITestOutputHelper _testOutput;
    private readonly TestLoggerFactory _loggerFactory;
 
    public RegistryTests(ITestOutputHelper testOutput)
    {
        _testOutput = testOutput;
        _loggerFactory = new TestLoggerFactory(testOutput);
    }
 
    public void Dispose()
    {
        _loggerFactory.Dispose();
    }
 
    [InlineData("us-south1-docker.pkg.dev", true)]
    [InlineData("us.gcr.io", false)]
    [Theory]
    public void CheckIfGoogleArtifactRegistry(string registryName, bool isECR)
    {
        ILogger logger = _loggerFactory.CreateLogger(nameof(CheckIfGoogleArtifactRegistry));
        Registry registry = new(registryName, logger, RegistryMode.Push);
        Assert.Equal(isECR, registry.IsGoogleArtifactRegistry);
    }
 
    [Fact]
    public void DockerIoAlias()
    {
        ILogger logger = _loggerFactory.CreateLogger(nameof(DockerIoAlias));
        Registry registry = new("docker.io", logger, RegistryMode.Push);
        Assert.True(registry.IsDockerHub);
        Assert.Equal("docker.io", registry.RegistryName);
        Assert.Equal("registry-1.docker.io", registry.BaseUri.Host);
    }
 
    [Fact]
    public async Task RegistriesThatProvideNoUploadSizeAttemptFullUpload()
    {
        ILogger logger = _loggerFactory.CreateLogger(nameof(RegistriesThatProvideNoUploadSizeAttemptFullUpload));
        var repoName = "testRepo";
        var layerDigest = "sha256:fafafafafafafafafafafafafafafafa";
        var mockLayer = new Mock<Layer>(MockBehavior.Strict);
        mockLayer
            .Setup(l => l.OpenBackingFile()).Returns(new MemoryStream(new byte[1000]));
        mockLayer
            .Setup(l => l.Descriptor).Returns(new Descriptor("blah", layerDigest, 1234));
 
        var uploadPath = new Uri("/uploads/foo/12345", UriKind.Relative);
        var api = new Mock<IRegistryAPI>(MockBehavior.Loose);
        api.Setup(api => api.Blob.ExistsAsync(repoName, layerDigest, It.IsAny<CancellationToken>())).Returns(Task.FromResult(false));
        api.Setup(api => api.Blob.Upload.StartAsync(repoName, It.IsAny<CancellationToken>())).Returns(Task.FromResult(new StartUploadInformation(uploadPath)));
        api.Setup(api => api.Blob.Upload.UploadAtomicallyAsync(uploadPath, It.IsAny<Stream>(), It.IsAny<CancellationToken>())).Returns(Task.FromResult(new FinalizeUploadInformation(uploadPath)));
 
        Registry registry = new("public.ecr.aws", logger, api.Object);
        await registry.PushLayerAsync(mockLayer.Object, repoName, CancellationToken.None);
 
        api.Verify(api => api.Blob.Upload.UploadChunkAsync(uploadPath, It.IsAny<HttpContent>(), It.IsAny<CancellationToken>()), Times.Never());
        api.Verify(api => api.Blob.Upload.UploadAtomicallyAsync(uploadPath, It.IsAny<Stream>(), It.IsAny<CancellationToken>()), Times.Once());
    }
 
    [Fact]
    public async Task RegistriesThatProvideUploadSizePrefersFullUploadWhenChunkSizeIsLowerThanContentLength()
    {
        ILogger logger = _loggerFactory.CreateLogger(nameof(RegistriesThatProvideUploadSizePrefersFullUploadWhenChunkSizeIsLowerThanContentLength));
        var repoName = "testRepo";
        var layerDigest = "sha256:fafafafafafafafafafafafafafafafa";
        var mockLayer = new Mock<Layer>(MockBehavior.Strict);
        var chunkSizeLessThanContentLength = 10000;
        var registryUri = new Uri("https://public.ecr.aws");;
        mockLayer
            .Setup(l => l.OpenBackingFile()).Returns(new MemoryStream(new byte[100000]));
        mockLayer
            .Setup(l => l.Descriptor).Returns(new Descriptor("blah", layerDigest, 1234));
 
        var uploadPath = new Uri("/uploads/foo/12345", UriKind.Relative);
        var absoluteUploadUri = new Uri(registryUri, uploadPath);
        var api = new Mock<IRegistryAPI>(MockBehavior.Loose);
        var uploadedCount = 0;
        api.Setup(api => api.Blob.ExistsAsync(repoName, layerDigest, It.IsAny<CancellationToken>())).Returns(Task.FromResult(false));
        api.Setup(api => api.Blob.Upload.StartAsync(repoName, It.IsAny<CancellationToken>())).Returns(Task.FromResult(new StartUploadInformation(uploadPath)));
        api.Setup(api => api.Blob.Upload.UploadAtomicallyAsync(uploadPath, It.IsAny<Stream>(), It.IsAny<CancellationToken>())).Returns(Task.FromResult(new FinalizeUploadInformation(uploadPath)));
        api.Setup(api => api.Blob.Upload.UploadChunkAsync(It.IsIn(absoluteUploadUri, uploadPath), It.IsAny<HttpContent>(), It.IsAny<CancellationToken>())).Returns(() =>
        {
            uploadedCount += chunkSizeLessThanContentLength;
            return Task.FromResult(ChunkUploadSuccessful(absoluteUploadUri, uploadPath, uploadedCount));
        });
 
        Registry registry = new(registryUri, logger, api.Object);
        await registry.PushLayerAsync(mockLayer.Object, repoName, CancellationToken.None);
 
        api.Verify(api => api.Blob.Upload.UploadAtomicallyAsync(It.IsIn(absoluteUploadUri, uploadPath), It.IsAny<Stream>(), It.IsAny<CancellationToken>()), Times.Exactly(1));
        api.Verify(api => api.Blob.Upload.UploadChunkAsync(It.IsIn(absoluteUploadUri, uploadPath), It.IsAny<HttpContent>(), It.IsAny<CancellationToken>()), Times.Never);
    }
 
    [Fact]
    public async Task RegistriesThatFailAtomicUploadFallbackToChunked()
    {
        ILogger logger = _loggerFactory.CreateLogger(nameof(RegistriesThatFailAtomicUploadFallbackToChunked));
        var repoName = "testRepo";
        var layerDigest = "sha256:fafafafafafafafafafafafafafafafa";
        var mockLayer = new Mock<Layer>(MockBehavior.Strict);
        var contentLength = 100000;
        var chunkSizeLessThanContentLength = 100000;
        var registryUri = new Uri("https://public.ecr.aws");;
        mockLayer
            .Setup(l => l.OpenBackingFile()).Returns(new MemoryStream(new byte[contentLength]));
        mockLayer
            .Setup(l => l.Descriptor).Returns(new Descriptor("blah", layerDigest, 1234));
 
        var uploadPath = new Uri("/uploads/foo/12345", UriKind.Relative);
        var absoluteUploadUri = new Uri(registryUri, uploadPath);
        var api = new Mock<IRegistryAPI>(MockBehavior.Loose);
        var uploadedCount = 0;
        api.Setup(api => api.Blob.ExistsAsync(repoName, layerDigest, It.IsAny<CancellationToken>())).Returns(Task.FromResult(false));
        api.Setup(api => api.Blob.Upload.StartAsync(repoName, It.IsAny<CancellationToken>())).Returns(Task.FromResult(new StartUploadInformation(uploadPath)));
        api.Setup(api => api.Blob.Upload.UploadAtomicallyAsync(It.IsIn(absoluteUploadUri, uploadPath), It.IsAny<Stream>(), It.IsAny<CancellationToken>())).Throws(new Exception("Server-side shutdown the thing"));
        api.Setup(api => api.Blob.Upload.UploadChunkAsync(It.IsIn(absoluteUploadUri, uploadPath), It.IsAny<HttpContent>(), It.IsAny<CancellationToken>())).Returns(() =>
        {
            uploadedCount += chunkSizeLessThanContentLength;
            return Task.FromResult(ChunkUploadSuccessful(absoluteUploadUri, uploadPath, uploadedCount));
        });
 
        Registry registry = new(registryUri, logger, api.Object);
        await registry.PushLayerAsync(mockLayer.Object, repoName, CancellationToken.None);
 
        api.Verify(api => api.Blob.Upload.UploadAtomicallyAsync(It.IsIn(absoluteUploadUri, uploadPath), It.IsAny<Stream>(), It.IsAny<CancellationToken>()), Times.Once());
        api.Verify(api => api.Blob.Upload.UploadChunkAsync(It.IsIn(absoluteUploadUri, uploadPath), It.IsAny<HttpContent>(), It.IsAny<CancellationToken>()), Times.Exactly(contentLength / chunkSizeLessThanContentLength));
    }
 
    [Fact]
    public async Task ChunkedUploadCalculatesChunksCorrectly()
    {
        ILogger logger = _loggerFactory.CreateLogger(nameof(RegistriesThatFailAtomicUploadFallbackToChunked));
        var repoName = "testRepo";
        var layerDigest = "sha256:fafafafafafafafafafafafafafafafa";
        var mockLayer = new Mock<Layer>(MockBehavior.Strict);
        var contentLength = 1000000;
        var chunkSize = 100000;
        var registryUri = new Uri("https://public.ecr.aws");;
        mockLayer
            .Setup(l => l.OpenBackingFile()).Returns(new MemoryStream(new byte[contentLength]));
        mockLayer
            .Setup(l => l.Descriptor).Returns(new Descriptor("blah", layerDigest, 1234));
 
        var uploadPath = new Uri("/uploads/foo/12345", UriKind.Relative);
        var absoluteUploadUri = new Uri(registryUri, uploadPath);
        var api = new Mock<IRegistryAPI>(MockBehavior.Loose);
        var uploadedCount = 0;
        api.Setup(api => api.Blob.ExistsAsync(repoName, layerDigest, It.IsAny<CancellationToken>())).Returns(Task.FromResult(false));
        api.Setup(api => api.Blob.Upload.StartAsync(repoName, It.IsAny<CancellationToken>())).Returns(Task.FromResult(new StartUploadInformation(uploadPath)));
        api.Setup(api => api.Blob.Upload.UploadAtomicallyAsync(It.IsIn(absoluteUploadUri, uploadPath), It.IsAny<Stream>(), It.IsAny<CancellationToken>())).Throws(new Exception("Server-side shutdown the thing"));
        api.Setup(api => api.Blob.Upload.UploadChunkAsync(It.IsIn(absoluteUploadUri, uploadPath), It.IsAny<HttpContent>(), It.IsAny<CancellationToken>())).Returns(() =>
        {
            uploadedCount += chunkSize;
            return Task.FromResult(ChunkUploadSuccessful(absoluteUploadUri, uploadPath, uploadedCount));
        });
 
        RegistrySettings settings = new()
        {
            ParallelUploadEnabled = false,
            ForceChunkedUpload = false,
            ChunkedUploadSizeBytes = chunkSize,
        };
 
        Registry registry = new(registryUri, logger, api.Object, settings);
        await registry.PushLayerAsync(mockLayer.Object, repoName, CancellationToken.None);
 
        api.Verify(api => api.Blob.Upload.UploadAtomicallyAsync(It.IsIn(absoluteUploadUri, uploadPath), It.IsAny<Stream>(), It.IsAny<CancellationToken>()), Times.Once());
        api.Verify(api => api.Blob.Upload.UploadChunkAsync(It.IsIn(absoluteUploadUri, uploadPath), It.IsAny<HttpContent>(), It.IsAny<CancellationToken>()), Times.Exactly(10));
    }
 
    [Fact]
    public async Task PushAsync_Logging()
    {
        using TestLoggerFactory loggerFactory = new(_testOutput);
        List<(LogLevel, string)> loggedMessages = new();
        loggerFactory.AddProvider(new InMemoryLoggerProvider(loggedMessages));
        ILogger logger = loggerFactory.CreateLogger(nameof(PushAsync_Logging));
 
        var repoName = "testRepo";
        var layerDigest = "sha256:fafafafafafafafafafafafafafafafa";
        var mockLayer = new Mock<Layer>(MockBehavior.Strict);
        mockLayer
            .Setup(l => l.OpenBackingFile()).Returns(new MemoryStream(new byte[1000]));
        mockLayer
            .Setup(l => l.Descriptor).Returns(new Descriptor("blah", layerDigest, 1234));
 
        var uploadPath = new Uri("/uploads/foo/12345", UriKind.Relative);
        var api = new Mock<IRegistryAPI>(MockBehavior.Loose);
        api.Setup(api => api.Blob.ExistsAsync(repoName, layerDigest, It.IsAny<CancellationToken>())).Returns(Task.FromResult(false));
        api.Setup(api => api.Blob.Upload.StartAsync(repoName, It.IsAny<CancellationToken>())).Returns(Task.FromResult(new StartUploadInformation(uploadPath)));
        api.Setup(api => api.Blob.Upload.UploadAtomicallyAsync(uploadPath, It.IsAny<Stream>(), It.IsAny<CancellationToken>())).Returns(Task.FromResult(new FinalizeUploadInformation(uploadPath)));
 
        Registry registry = new("public.ecr.aws", logger, api.Object);
        await registry.PushLayerAsync(mockLayer.Object, repoName, CancellationToken.None);
 
        Assert.NotEmpty(loggedMessages);
        Assert.True(loggedMessages.All(m => m.Item1 == LogLevel.Trace));
        var messages = loggedMessages.Select(m => m.Item2).ToList();
        Assert.Contains(messages, m => m == "Started upload session for sha256:fafafafafafafafafafafafafafafafa");
        Assert.Contains(messages, m => m == "Finalized upload session for sha256:fafafafafafafafafafafafafafafafa");
    }
 
    [Fact]
    public async Task PushAsync_ForceChunkedUpload()
    {
        ILogger logger = _loggerFactory.CreateLogger(nameof(PushAsync_ForceChunkedUpload));
 
        string repoName = "testRepo";
        string layerDigest = "sha256:fafafafafafafafafafafafafafafafa";
        Mock<Layer> mockLayer = new(MockBehavior.Strict);
        int contentLength = 1000000;
        int chunkSize = 100000;
        var registryUri = new Uri("https://public.ecr.aws");;
        mockLayer
            .Setup(l => l.OpenBackingFile()).Returns(new MemoryStream(new byte[contentLength]));
        mockLayer
            .Setup(l => l.Descriptor).Returns(new Descriptor("blah", layerDigest, 1234));
 
        Uri uploadPath = new("/uploads/foo/12345", UriKind.Relative);
        Uri absoluteUploadUri = new(registryUri, uploadPath);
        Mock<IRegistryAPI> api = new(MockBehavior.Loose);
        int uploadedCount = 0;
        api.Setup(api => api.Blob.ExistsAsync(repoName, layerDigest, It.IsAny<CancellationToken>())).Returns(Task.FromResult(false));
        api.Setup(api => api.Blob.Upload.StartAsync(repoName, It.IsAny<CancellationToken>())).Returns(Task.FromResult(new StartUploadInformation(uploadPath)));
        api.Setup(api => api.Blob.Upload.UploadChunkAsync(It.IsIn(absoluteUploadUri, uploadPath), It.IsAny<HttpContent>(), It.IsAny<CancellationToken>())).Returns(() =>
        {
            uploadedCount += chunkSize;
            return Task.FromResult(ChunkUploadSuccessful(absoluteUploadUri, uploadPath, uploadedCount));
        });
 
        RegistrySettings settings = new()
        {
            ParallelUploadEnabled = false,
            ForceChunkedUpload = true,
            ChunkedUploadSizeBytes = chunkSize,
        };
 
        Registry registry = new(registryUri, logger, api.Object, settings);
        await registry.PushLayerAsync(mockLayer.Object, repoName, CancellationToken.None);
 
        api.Verify(api => api.Blob.Upload.UploadAtomicallyAsync(It.IsIn(absoluteUploadUri, uploadPath), It.IsAny<Stream>(), It.IsAny<CancellationToken>()), Times.Never());
        api.Verify(api => api.Blob.Upload.UploadChunkAsync(It.IsIn(absoluteUploadUri, uploadPath), It.IsAny<HttpContent>(), It.IsAny<CancellationToken>()), Times.Exactly(10));
    }
 
    [Fact]
    public async Task CanParseRegistryDeclaredChunkSize_FromRange()
    {
        ILogger logger = _loggerFactory.CreateLogger(nameof(CanParseRegistryDeclaredChunkSize_FromRange));
        string repoName = "testRepo";
 
        Mock<HttpClient> client = new(MockBehavior.Loose);
        HttpResponseMessage httpResponse = new()
        {
            StatusCode = HttpStatusCode.Accepted,
        };
        httpResponse.Headers.Add("Range", "0-100000");
        httpResponse.Headers.Location = new Uri("https://my-registy.com/v2/testRepo/blobs/uploads/");
        client.Setup(client => client.SendAsync(It.Is<HttpRequestMessage>(m => m.RequestUri == new Uri("https://my-registy.com/v2/testRepo/blobs/uploads/")), It.IsAny<CancellationToken>())).Returns(Task.FromResult(httpResponse));
 
        HttpClient finalClient = client.Object;
        DefaultBlobUploadOperations operations = new(new Uri("https://my-registy.com"), finalClient, logger);
        StartUploadInformation result = await operations.StartAsync(repoName, CancellationToken.None);
 
        Assert.Equal("https://my-registy.com/v2/testRepo/blobs/uploads/", result.UploadUri.AbsoluteUri);
    }
 
    [Fact]
    public async Task CanParseRegistryDeclaredChunkSize_FromOCIChunkMinLength()
    {
        ILogger logger = _loggerFactory.CreateLogger(nameof(CanParseRegistryDeclaredChunkSize_FromOCIChunkMinLength));
        string repoName = "testRepo";
 
        Mock<HttpClient> client = new(MockBehavior.Loose);
        HttpResponseMessage httpResponse = new()
        {
            StatusCode = HttpStatusCode.Accepted,
        };
        httpResponse.Headers.Add("OCI-Chunk-Min-Length", "100000");
        httpResponse.Headers.Location = new Uri("https://my-registy.com/v2/testRepo/blobs/uploads/");
        client.Setup(client => client.SendAsync(It.Is<HttpRequestMessage>(m => m.RequestUri == new Uri("https://my-registy.com/v2/testRepo/blobs/uploads/")), It.IsAny<CancellationToken>())).Returns(Task.FromResult(httpResponse));
 
        HttpClient finalClient = client.Object;
        DefaultBlobUploadOperations operations = new(new Uri("https://my-registy.com"), finalClient, logger);
        StartUploadInformation result = await operations.StartAsync(repoName, CancellationToken.None);
 
        Assert.Equal("https://my-registy.com/v2/testRepo/blobs/uploads/", result.UploadUri.AbsoluteUri);
    }
 
    [Fact]
    public async Task CanParseRegistryDeclaredChunkSize_None()
    {
        ILogger logger = _loggerFactory.CreateLogger(nameof(CanParseRegistryDeclaredChunkSize_None));
        string repoName = "testRepo";
 
        Mock<HttpClient> client = new(MockBehavior.Loose);
        HttpResponseMessage httpResponse = new()
        {
            StatusCode = HttpStatusCode.Accepted,
        };
        httpResponse.Headers.Location = new Uri("https://my-registy.com/v2/testRepo/blobs/uploads/");
        client.Setup(client => client.SendAsync(It.Is<HttpRequestMessage>(m => m.RequestUri == new Uri("https://my-registy.com/v2/testRepo/blobs/uploads/")), It.IsAny<CancellationToken>())).Returns(Task.FromResult(httpResponse));
 
        HttpClient finalClient = client.Object;
        DefaultBlobUploadOperations operations = new(new Uri("https://my-registy.com"), finalClient, logger);
        StartUploadInformation result = await operations.StartAsync(repoName, CancellationToken.None);
 
        Assert.Equal("https://my-registy.com/v2/testRepo/blobs/uploads/", result.UploadUri.AbsoluteUri);
    }
 
    [Fact]
    public async Task UploadBlobChunkedAsync_NormalFlow()
    {
        ILogger logger = _loggerFactory.CreateLogger(nameof(UploadBlobChunkedAsync_NormalFlow));
        var registryUri = new Uri("https://public.ecr.aws");;
 
        int contentLength = 50000000;
        int chunkSize = 10000000;
 
        Stream testStream = new MemoryStream(new byte[contentLength]);
 
        Uri uploadPath = new("/uploads/foo/12345", UriKind.Relative);
        Uri absoluteUploadUri = new(registryUri, uploadPath);
        Mock<IRegistryAPI> api = new(MockBehavior.Loose);
        int uploadedCount = 0;
        api.Setup(api => api.Blob.Upload.UploadChunkAsync(It.IsIn(absoluteUploadUri, uploadPath), It.IsAny<HttpContent>(), It.IsAny<CancellationToken>())).Returns(() =>
        {
            uploadedCount += chunkSize;
            return Task.FromResult(ChunkUploadSuccessful(absoluteUploadUri, uploadPath, uploadedCount));
        });
 
        RegistrySettings settings = new()
        {
            ParallelUploadEnabled = false,
            ForceChunkedUpload = false,
            ChunkedUploadSizeBytes = chunkSize,
        };
 
        Registry registry = new(registryUri, logger, api.Object, settings);
        await registry.UploadBlobChunkedAsync(testStream, new StartUploadInformation(absoluteUploadUri), CancellationToken.None);
 
        api.Verify(api => api.Blob.Upload.UploadChunkAsync(It.IsIn(absoluteUploadUri, uploadPath), It.IsAny<HttpContent>(), It.IsAny<CancellationToken>()), Times.Exactly(5));
    }
 
    [Fact]
    public async Task UploadBlobChunkedAsync_Failure()
    {
        ILogger logger = _loggerFactory.CreateLogger(nameof(UploadBlobChunkedAsync_NormalFlow));
        var registryUri = new Uri("https://public.ecr.aws");;
 
        int contentLength = 50000000;
        int chunkSize = 10000000;
 
        Stream testStream = new MemoryStream(new byte[contentLength]);
 
        Uri uploadPath = new("/uploads/foo/12345", UriKind.Relative);
        Uri absoluteUploadUri = new(registryUri, uploadPath);
        Mock<IRegistryAPI> api = new(MockBehavior.Loose);
 
        Exception preparedException = new ApplicationException(Resource.FormatString(nameof(Strings.BlobUploadFailed), $"PATCH <uri>", HttpStatusCode.InternalServerError));
 
        api.Setup(api => api.Blob.Upload.UploadChunkAsync(It.IsIn(absoluteUploadUri, uploadPath), It.IsAny<HttpContent>(), It.IsAny<CancellationToken>())).Returns(() =>
        {
            throw preparedException;
        });
 
        RegistrySettings settings = new()
        {
            ParallelUploadEnabled = false,
            ForceChunkedUpload = false,
            ChunkedUploadSizeBytes = chunkSize,
        };
 
        Registry registry = new(registryUri, logger, api.Object, settings);
        ApplicationException receivedException = await Assert.ThrowsAsync<ApplicationException>(() => registry.UploadBlobChunkedAsync(testStream, new StartUploadInformation(absoluteUploadUri), CancellationToken.None));
 
        Assert.Equal(preparedException, receivedException);
 
        api.Verify(api => api.Blob.Upload.UploadChunkAsync(It.IsIn(absoluteUploadUri, uploadPath), It.IsAny<HttpContent>(), It.IsAny<CancellationToken>()), Times.Exactly(1));
    }
 
    [Theory(Skip = "https://github.com/dotnet/sdk/issues/42820")]
    [InlineData(true, true, true)]
    [InlineData(false, true, true)]
    [InlineData(true, false, true)]
    [InlineData(false, false, true)]
    [InlineData(false, false, false)]
    public async Task InsecureRegistry(bool isInsecureRegistry, bool serverIsHttps, bool httpServerCloseAbortive)
    {
        ILogger logger = _loggerFactory.CreateLogger(nameof(InsecureRegistry));
 
        // Start a dummy HTTP server that response with 200 OK.
        using TcpListener listener = new TcpListener(IPAddress.Loopback, 0);
        listener.Start();
        IPEndPoint endpoint = (listener.LocalEndpoint as IPEndPoint)!;
        Uri registryUri = new Uri($"https://{endpoint.Address}:{endpoint.Port}");
        SslServerAuthenticationOptions? sslOptions = null!;
        if (serverIsHttps)
        {
            var key = RSA.Create(2048);
            var request = new CertificateRequest("CN=localhost", key, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
            X509Certificate2 serverCertificate = request.CreateSelfSigned(DateTimeOffset.Now, DateTimeOffset.Now.AddYears(1));
 
            // https://stackoverflow.com/questions/72096812/loading-x509certificate2-from-pem-file-results-in-no-credentials-are-available/72101855#72101855
            serverCertificate = X509CertificateLoader.LoadPkcs12(serverCertificate.Export(X509ContentType.Pfx), password: "");
 
            sslOptions = new SslServerAuthenticationOptions()
            {
                ServerCertificate = serverCertificate,
                ClientCertificateRequired = false
            };
        }
        _ = Task.Run(async () =>
        {
            while (true)
            {
                using TcpClient client = await listener.AcceptTcpClientAsync();
                try
                {
                    using Stream stream = serverIsHttps ? new SslStream(client.GetStream(), leaveInnerStreamOpen: false) : client.GetStream();
                    if (stream is SslStream sslStream)
                    {
                        await sslStream.AuthenticateAsServerAsync(sslOptions!, default(CancellationToken));
                    }
                    byte[] buffer = new byte[10];
                    await stream.ReadAtLeastAsync(buffer, buffer.Length); // Wait for the request.
                    // Repond if we see '/v2/' in the buffer (since we expect that as part of the request path).
                    if (buffer.AsSpan().IndexOf("/v2/"u8) != 0)
                    {
                        await stream.WriteAsync("HTTP/1.0 200 OK\r\nContent-Length: 0\r\n\r\n"u8.ToArray());
                    }
                    else
                    {
                        if (httpServerCloseAbortive)
                        {
                            client.GetStream().Close(timeout: 0);
                        }
                    }
                }
                catch
                { }
            }
        });
 
        RegistrySettings settings = new()
        {
            IsInsecure = isInsecureRegistry
        };
        Registry registry = new(registryUri, logger, RegistryMode.Pull, settings: settings);
 
        // Make a request.
        Task getManifest = registry.GetImageManifestAsync(repositoryName: "dotnet/runtime", reference: "latest", runtimeIdentifier: "linux-x64", manifestPicker: null!, cancellationToken: default!);
 
        if (isInsecureRegistry)
        {
            // Falls back to http (when serverIsHttps is false) or ignores https certificate errors (when serverIsHttps is true).
            // Results in throwing: CONTAINER2003: The manifest for dotnet/runtime:latest from registry hwas an unknown type.
            await Assert.ThrowsAsync<NotImplementedException>(() => getManifest);
        }
        else
        {
            // Does not fall back and throws HttpRequestException with SecureConnectionError.
            Exception? exception = await Assert.ThrowsAnyAsync<Exception>(() => getManifest);
            try
            {
                // The AuthHandshakeMessageHandler may reach its retry limit and throw an ApplicationException.
                if (exception is ApplicationException)
                {
                    // Find the exception for the first failed attempt.
                    exception = (exception.InnerException as AggregateException)?.InnerExceptions.FirstOrDefault();
                    Assert.NotNull(exception);
                }
 
                Assert.IsType<HttpRequestException>(exception);
                HttpRequestException requestException = (HttpRequestException)exception;
                Assert.Equal(HttpRequestError.SecureConnectionError, requestException.HttpRequestError);
 
                // The FallbackToHttpMessageHandler should fall back (if this registry was configured as insecure).
                Assert.True(FallbackToHttpMessageHandler.ShouldAttemptFallbackToHttp(requestException));
            }
            catch
            {
                // Log a message describing the exception.
                StringBuilder sb = new();
                sb.AppendLine("Exception is not fallback exception:");
                while (exception != null)
                {
                    switch (exception)
                    {
                        case SocketException socketException:
                            sb.AppendLine($"{nameof(SocketException)}({socketException.SocketErrorCode}) - {exception.Message}");
                            break;
                        case HttpRequestException requestException:
                            sb.AppendLine($"{nameof(HttpRequestException)}({requestException.HttpRequestError}) - {exception.Message}");
                            break;
                        default:
                            sb.AppendLine($"{exception.GetType().Name} - {exception.Message}");
                            break;
                    }
 
                    exception = exception.InnerException;
                }
                logger.LogError(sb.ToString());
 
                throw;
            }
        }
    }
 
    [InlineData("localhost", null, true)]
    [InlineData("localhost:5000", null, true)]
    [InlineData("public.ecr.aws", null, false)]
    [InlineData("public.ecr.aws", "public.ecr.aws", true)]
    [InlineData("public.ecr.aws", "Public.ecr.aws", true)] // ignore case
    [InlineData("public.ecr.aws", "public.ecr.aws;docker.io", true)] // multiple registries
    [InlineData("public.ecr.aws", ";public.ecr.aws ;  docker.io ", true)] // ignore whitespace
    [InlineData("public.ecr.aws", "public.ecr.aws2;docker.io ", false)] // full name match
    [Theory]
    public void IsRegistryInsecure(string registryName, string? insecureRegistriesEnvvar, bool expectedInsecure)
    {
        var environment = new Dictionary<string, string>();
        if (insecureRegistriesEnvvar is not null)
        {
            environment["DOTNET_CONTAINER_INSECURE_REGISTRIES"] = insecureRegistriesEnvvar;
        }
 
        var registrySettings = new RegistrySettings(registryName, new MockEnvironmentProvider(environment));
 
        Assert.Equal(expectedInsecure, registrySettings.IsInsecure);
    }
 
    [Fact]
    public async Task DownloadBlobAsync_RetriesOnFailure()
    {
        // Arrange
        var logger = _loggerFactory.CreateLogger(nameof(DownloadBlobAsync_RetriesOnFailure));
 
        var repoName = "testRepo";
        var descriptor = new Descriptor(SchemaTypes.OciLayerGzipV1, "sha256:testdigest1234", 1234);
        var cancellationToken = CancellationToken.None;
 
        var mockRegistryAPI = new Mock<IRegistryAPI>(MockBehavior.Strict);
        mockRegistryAPI
            .SetupSequence(api => api.Blob.GetStreamAsync(repoName, descriptor.Digest, cancellationToken))
            .ThrowsAsync(new Exception("Simulated failure 1")) // First attempt fails
            .ThrowsAsync(new Exception("Simulated failure 2")) // Second attempt fails
            .ReturnsAsync(new MemoryStream(new byte[] { 1, 2, 3 })); // Third attempt succeeds
 
        Registry registry = new(repoName, logger, mockRegistryAPI.Object, null, () => TimeSpan.Zero);
 
        string? result = null;
        try
        {
            // Act
            result = await registry.DownloadBlobAsync(repoName, descriptor, cancellationToken);
 
            // Assert
            Assert.NotNull(result);
            Assert.True(File.Exists(result)); // Ensure the file was successfully downloaded
            mockRegistryAPI.Verify(api => api.Blob.GetStreamAsync(repoName, descriptor.Digest, cancellationToken), Times.Exactly(3)); // Verify retries
        }
        finally
        {
            // Cleanup
            if (result != null)
            {
                File.Delete(result);
            }
        }
    }
 
    [Fact]
    public async Task DownloadBlobAsync_ThrowsAfterMaxRetries()
    {
        // Arrange
        var logger = _loggerFactory.CreateLogger(nameof(DownloadBlobAsync_ThrowsAfterMaxRetries));
 
        var repoName = "testRepo";
        var descriptor = new Descriptor(SchemaTypes.OciLayerGzipV1, "sha256:testdigest1234", 1234);
        var cancellationToken = CancellationToken.None;
 
        var mockRegistryAPI = new Mock<IRegistryAPI>(MockBehavior.Strict);
        // Simulate 5 failures (assuming your retry logic attempts 5 times before throwing)
        mockRegistryAPI
            .SetupSequence(api => api.Blob.GetStreamAsync(repoName, descriptor.Digest, cancellationToken))
            .ThrowsAsync(new Exception("Simulated failure 1"))
            .ThrowsAsync(new Exception("Simulated failure 2"))
            .ThrowsAsync(new Exception("Simulated failure 3"))
            .ThrowsAsync(new Exception("Simulated failure 4"))
            .ThrowsAsync(new Exception("Simulated failure 5"));
 
        Registry registry = new(repoName, logger, mockRegistryAPI.Object, null, () => TimeSpan.Zero);
 
        // Act & Assert
        await Assert.ThrowsAsync<UnableToDownloadFromRepositoryException>(async () =>
        {
            await registry.DownloadBlobAsync(repoName, descriptor, cancellationToken);
        });
 
        mockRegistryAPI.Verify(api => api.Blob.GetStreamAsync(repoName, descriptor.Digest, cancellationToken), Times.Exactly(5));
    }
 
    private static NextChunkUploadInformation ChunkUploadSuccessful(Uri requestUri, Uri uploadUrl, int? contentLength, HttpStatusCode code = HttpStatusCode.Accepted)
    {
        return new(uploadUrl);
    }
 
    private class MockEnvironmentProvider : IEnvironmentProvider
    {
        private readonly IDictionary<string, string> _environmentVariables;
 
        public MockEnvironmentProvider(IDictionary<string, string> environmentVariables)
        {
            _environmentVariables = environmentVariables;
        }
 
        public bool GetEnvironmentVariableAsBool(string name, bool defaultValue)
        {
            string? str = GetEnvironmentVariable(name);
            if (string.IsNullOrEmpty(str))
            {
                return defaultValue;
            }
 
            switch (str.ToLowerInvariant())
            {
                case "true":
                case "1":
                case "yes":
                    return true;
                case "false":
                case "0":
                case "no":
                    return false;
                default:
                    return defaultValue;
            }
        }
 
        public string? GetEnvironmentVariable(string name)
        {
            string? value;
            _environmentVariables.TryGetValue(name, out value);
            return value;
        }
 
        public string? GetEnvironmentVariable(string variable, EnvironmentVariableTarget target)
            => GetEnvironmentVariable(variable);
 
        public int? GetEnvironmentVariableAsNullableInt(string variable)
        {
            if (GetEnvironmentVariable(variable) is string strValue && int.TryParse(strValue, out int intValue))
            {
                return intValue;
            }
 
            return null;
        }
 
        public void SetEnvironmentVariable(string variable, string value, EnvironmentVariableTarget target)
            => throw new NotImplementedException();
 
        public IEnumerable<string> ExecutableExtensions
            => throw new NotImplementedException();
 
        public string GetCommandPath(string commandName, params string[] extensions)
            => throw new NotImplementedException();
 
        public string GetCommandPathFromRootPath(string rootPath, string commandName, params string[] extensions)
            => throw new NotImplementedException();
 
        public string GetCommandPathFromRootPath(string rootPath, string commandName, IEnumerable<string> extensions)
            => throw new NotImplementedException();
    }
}
 |