HttpsProxyTestcontainersIntegrationTest.java

/*
 *    Copyright (c) 2025 AsyncHttpClient Project. All rights reserved.
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *        http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */
package org.asynchttpclient.proxy;

import io.github.artsok.RepeatedIfExceptionsTest;
import org.asynchttpclient.AsyncHttpClient;
import org.asynchttpclient.AsyncHttpClientConfig;
import org.asynchttpclient.Response;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.testcontainers.DockerClientFactory;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.output.Slf4jLogConsumer;
import org.testcontainers.containers.wait.strategy.Wait;
import org.testcontainers.images.builder.ImageFromDockerfile;
import org.testcontainers.junit.jupiter.Testcontainers;

import java.nio.file.Path;
import java.time.Duration;
import java.util.concurrent.TimeUnit;

import static org.asynchttpclient.Dsl.asyncHttpClient;
import static org.asynchttpclient.Dsl.config;
import static org.asynchttpclient.Dsl.get;
import static org.asynchttpclient.Dsl.proxyServer;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assumptions.assumeTrue;

@Testcontainers
public class HttpsProxyTestcontainersIntegrationTest {

    private static final Logger LOGGER = LoggerFactory.getLogger(HttpsProxyTestcontainersIntegrationTest.class);

    private static final int SQUID_HTTP_PORT = 3128;
    private static final int SQUID_HTTPS_PORT = 3129;

    private static final String TARGET_HTTP_URL = "http://httpbin.org/get";
    private static final String TARGET_HTTPS_URL = "https://www.example.com/";

    private static boolean dockerAvailable = false;
    private static GenericContainer<?> squidProxy;

    @BeforeAll
    static void checkDockerAvailability() {
        try {
            dockerAvailable = DockerClientFactory.instance().isDockerAvailable();
            LOGGER.info("Docker availability check: {}", dockerAvailable);
        } catch (Exception e) {
            LOGGER.warn("Failed to check Docker availability: {}", e.getMessage());
            dockerAvailable = false;
        }
        // Skip tests if Docker not available, unless force-enabled
        if (!dockerAvailable && !"true".equals(System.getProperty("docker.tests"))) {
            assumeTrue(false, "Docker is not available - skipping integration tests. Use -Ddocker.tests=true to force run.");
        }
        // Allow force-disabling Docker tests
        if ("true".equals(System.getProperty("no.docker.tests"))) {
            assumeTrue(false, "Docker tests disabled via -Dno.docker.tests=true");
        }
        // Only start container if Docker is available
        if (dockerAvailable) {
            squidProxy = new GenericContainer<>(
                    new ImageFromDockerfile()
                            .withFileFromPath("Dockerfile", Path.of("src/test/resources/squid/Dockerfile"))
                            .withFileFromPath("squid.conf", Path.of("src/test/resources/squid/squid.conf"))
            )
                    .withExposedPorts(SQUID_HTTP_PORT, SQUID_HTTPS_PORT)
                    .withLogConsumer(new Slf4jLogConsumer(LOGGER).withPrefix("SQUID"))
                    .waitingFor(Wait.forLogMessage(".*Accepting HTTP.*", 1)
                            .withStartupTimeout(Duration.ofMinutes(2)));
            squidProxy.start();
        }
    }

    @AfterAll
    static void stopContainer() {
        if (squidProxy != null && squidProxy.isRunning()) {
            squidProxy.stop();
        }
    }

    @RepeatedIfExceptionsTest(repeats = 3)
    public void testHttpProxyToHttpTarget() throws Exception {
        assumeTrue(dockerAvailable, "Docker is not available - skipping test");
        LOGGER.info("Testing HTTP proxy to HTTP target");
        AsyncHttpClientConfig config = config()
                .setProxyServer(proxyServer("localhost", squidProxy.getMappedPort(SQUID_HTTP_PORT))
                        .setProxyType(ProxyType.HTTP)
                        .build())
                .setConnectTimeout(Duration.ofMillis(10000))
                .setRequestTimeout(Duration.ofMillis(30000))
                .build();
        try (AsyncHttpClient client = asyncHttpClient(config)) {
            Response response = client.executeRequest(get(TARGET_HTTP_URL)).get(30, TimeUnit.SECONDS);
            assertEquals(200, response.getStatusCode());
            assertTrue(response.getResponseBody().contains("httpbin"));
            LOGGER.info("HTTP proxy to HTTP target test passed");
        }
    }

    @RepeatedIfExceptionsTest(repeats = 3)
    public void testHttpsProxyToHttpTarget() throws Exception {
        assumeTrue(dockerAvailable, "Docker is not available - skipping test");
        LOGGER.info("Testing HTTPS proxy to HTTP target");
        AsyncHttpClientConfig config = config()
                .setProxyServer(proxyServer("localhost", squidProxy.getMappedPort(SQUID_HTTPS_PORT))
                        .setProxyType(ProxyType.HTTPS)
                        .build())
                .setUseInsecureTrustManager(true)
                .setConnectTimeout(Duration.ofMillis(10000))
                .setRequestTimeout(Duration.ofMillis(30000))
                .build();
        try (AsyncHttpClient client = asyncHttpClient(config)) {
            Response response = client.executeRequest(get(TARGET_HTTP_URL)).get(30, TimeUnit.SECONDS);
            assertEquals(200, response.getStatusCode());
            assertTrue(response.getResponseBody().contains("httpbin"));
            LOGGER.info("HTTPS proxy to HTTP target test passed");
        }
    }

    @RepeatedIfExceptionsTest(repeats = 3)
    public void testHttpProxyToHttpsTarget() throws Exception {
        assumeTrue(dockerAvailable, "Docker is not available - skipping test");
        LOGGER.info("Testing HTTP proxy to HTTPS target");
        AsyncHttpClientConfig config = config()
                .setProxyServer(proxyServer("localhost", squidProxy.getMappedPort(SQUID_HTTP_PORT))
                        .setProxyType(ProxyType.HTTP)
                        .build())
                .setUseInsecureTrustManager(true)
                .setConnectTimeout(Duration.ofMillis(10000))
                .setRequestTimeout(Duration.ofMillis(30000))
                .build();
        try (AsyncHttpClient client = asyncHttpClient(config)) {
            Response response = client.executeRequest(get(TARGET_HTTPS_URL)).get(30, TimeUnit.SECONDS);
            assertEquals(200, response.getStatusCode());
            assertTrue(response.getResponseBody().contains("Example Domain") ||
                    response.getResponseBody().contains("example"));
            LOGGER.info("HTTP proxy to HTTPS target test passed");
        }
    }

    @RepeatedIfExceptionsTest(repeats = 3)
    public void testHttpsProxyToHttpsTarget() throws Exception {
        assumeTrue(dockerAvailable, "Docker is not available - skipping test");
        LOGGER.info("Testing HTTPS proxy to HTTPS target - validates issue #1907 fix");
        AsyncHttpClientConfig config = config()
                .setProxyServer(proxyServer("localhost", squidProxy.getMappedPort(SQUID_HTTPS_PORT))
                        .setProxyType(ProxyType.HTTPS)
                        .build())
                .setUseInsecureTrustManager(true)
                .setConnectTimeout(Duration.ofMillis(10000))
                .setRequestTimeout(Duration.ofMillis(30000))
                .build();
        try (AsyncHttpClient client = asyncHttpClient(config)) {
            Response response = client.executeRequest(get(TARGET_HTTPS_URL)).get(30, TimeUnit.SECONDS);
            assertEquals(200, response.getStatusCode());
            assertTrue(response.getResponseBody().contains("Example Domain") ||
                    response.getResponseBody().contains("example"));
            LOGGER.info("HTTPS proxy to HTTPS target test passed - core issue #1907 RESOLVED!");
        }
    }

    @Test
    public void testDockerInfrastructureReady() {
        assumeTrue(dockerAvailable, "Docker is not available - skipping test");
        LOGGER.info("Docker infrastructure test - validating container is ready");
        LOGGER.info("Squid HTTP proxy available at: localhost:{}", squidProxy.getMappedPort(SQUID_HTTP_PORT));
        LOGGER.info("Squid HTTPS proxy available at: localhost:{}", squidProxy.getMappedPort(SQUID_HTTPS_PORT));
        assertTrue(squidProxy.isRunning(), "Squid container should be running");
        assertTrue(squidProxy.getMappedPort(SQUID_HTTP_PORT) > 0, "HTTP port should be mapped");
        assertTrue(squidProxy.getMappedPort(SQUID_HTTPS_PORT) > 0, "HTTPS port should be mapped");
        LOGGER.info("Docker infrastructure is ready and accessible");
    }
}