File: AzureKustoHealthCheckTests.cs
Web Access
Project: src\tests\Aspire.Hosting.Azure.Kusto.Tests\Aspire.Hosting.Azure.Kusto.Tests.csproj (Aspire.Hosting.Azure.Kusto.Tests)
// 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.Sockets;
using System.Text;
using Aspire.Hosting.Utils;
using Microsoft.Extensions.Diagnostics.HealthChecks;
 
namespace Aspire.Hosting.Azure.Kusto.Tests;
 
public class AzureKustoHealthCheckTests
{
    [Theory]
    [InlineData("http://localhost:1234", "http://127.0.0.1:1234", null)]
    [InlineData("http://LocalHost:1234;Initial Catalog=TestDb", "http://127.0.0.1:1234", "TestDb")]
    [InlineData("http://127.0.0.1:1234", "http://127.0.0.1:1234", null)]
    [InlineData("http://kusto.example:1234", "http://kusto.example:1234", null)]
    public void GetConnectionStringBuilder_ForEmulator_UsesIPv4LoopbackForLocalhost(string connectionString, string expectedDataSource, string? expectedDatabase)
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        var kusto = builder.AddAzureKustoCluster("kusto").RunAsEmulator();
 
        var kcsb = AzureKustoBuilderExtensions.GetConnectionStringBuilder(kusto.Resource, connectionString);
 
        Assert.Equal(expectedDataSource, kcsb.DataSource);
        if (expectedDatabase is not null)
        {
            Assert.Equal(expectedDatabase, kcsb.InitialCatalog);
        }
    }
 
    [Fact]
    public async Task EmulatorHealthCheck_ReachesEndpointThatOnlyListensOnIPv4Loopback()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        var kusto = builder.AddAzureKustoCluster("kusto").RunAsEmulator();
 
        using var listener = new TcpListener(IPAddress.Loopback, 0);
        listener.Start();
        var port = ((IPEndPoint)listener.LocalEndpoint).Port;
 
        // Hold [::1]:port bound without listening so IPv6 loopback connections are refused. This mirrors an emulator
        // endpoint that is only reachable over IPv4 even though "localhost" also resolves to ::1.
        using var ipv6Blocker = TryBindIPv6Loopback(port);
 
        using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(60));
        var acceptTask = AcceptAndRespondAsync(listener, cts.Token);
 
        var kcsb = AzureKustoBuilderExtensions.GetConnectionStringBuilder(kusto.Resource, $"http://localhost:{port}");
        var healthCheck = new AzureKustoHealthCheck(kcsb, isClusterCheck: true);
 
        var result = await healthCheck.CheckHealthAsync(new HealthCheckContext(), cts.Token);
 
        // The request must reach the IPv4-only endpoint. If it never connects, the timeout cancels the accept and fails the test.
        await acceptTask;
 
        // The fake endpoint answers with 400, so the check is unhealthy.
        Assert.Equal(HealthStatus.Unhealthy, result.Status);
    }
 
    private static Socket? TryBindIPv6Loopback(int port)
    {
        if (!Socket.OSSupportsIPv6)
        {
            return null;
        }
 
        var socket = new Socket(AddressFamily.InterNetworkV6, SocketType.Stream, ProtocolType.Tcp);
        try
        {
            socket.Bind(new IPEndPoint(IPAddress.IPv6Loopback, port));
            return socket;
        }
        catch (SocketException)
        {
            socket.Dispose();
            return null;
        }
    }
 
    private static async Task AcceptAndRespondAsync(TcpListener listener, CancellationToken cancellationToken)
    {
        using var client = await listener.AcceptTcpClientAsync(cancellationToken);
        var stream = client.GetStream();
 
        var buffer = new byte[4096];
        var request = new StringBuilder();
        while (!request.ToString().Contains("\r\n\r\n", StringComparison.Ordinal))
        {
            var read = await stream.ReadAsync(buffer, cancellationToken);
            if (read == 0)
            {
                break;
            }
            request.Append(Encoding.ASCII.GetString(buffer, 0, read));
        }
 
        var response = "HTTP/1.1 400 Bad Request\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"u8.ToArray();
        await stream.WriteAsync(response, cancellationToken);
    }
}