AuthTimeoutTest.java

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

import io.github.artsok.RepeatedIfExceptionsTest;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.server.handler.AbstractHandler;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;

import java.io.IOException;
import java.io.OutputStream;
import java.time.Duration;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_LENGTH;
import static org.asynchttpclient.Dsl.asyncHttpClient;
import static org.asynchttpclient.Dsl.basicAuthRealm;
import static org.asynchttpclient.Dsl.config;
import static org.asynchttpclient.Dsl.digestAuthRealm;
import static org.asynchttpclient.test.TestUtils.ADMIN;
import static org.asynchttpclient.test.TestUtils.USER;
import static org.asynchttpclient.test.TestUtils.addBasicAuthHandler;
import static org.asynchttpclient.test.TestUtils.addDigestAuthHandler;
import static org.asynchttpclient.test.TestUtils.addHttpConnector;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertThrows;

public class AuthTimeoutTest extends AbstractBasicTest {

    private static final Duration REQUEST_TIMEOUT = Duration.ofSeconds(1);
    private static final int SHORT_FUTURE_TIMEOUT = 500; // shorter than REQUEST_TIMEOUT
    private static final int LONG_FUTURE_TIMEOUT = 1500; // longer than REQUEST_TIMEOUT

    private Server server2;

    @Override
    @BeforeEach
    public void setUpGlobal() throws Exception {
        server = new Server();
        ServerConnector connector1 = addHttpConnector(server);
        addBasicAuthHandler(server, configureHandler());
        server.start();
        port1 = connector1.getLocalPort();

        server2 = new Server();
        ServerConnector connector2 = addHttpConnector(server2);
        addDigestAuthHandler(server2, configureHandler());
        server2.start();
        port2 = connector2.getLocalPort();

        logger.info("Local HTTP server started successfully");
    }

    @Override
    @AfterEach
    public void tearDownGlobal() throws Exception {
        super.tearDownGlobal();
        server2.stop();
    }

    @RepeatedIfExceptionsTest(repeats = 5)
    public void basicAuthTimeoutTest() throws Throwable {
        try (AsyncHttpClient client = newClient()) {
            execute(client, true, false).get(LONG_FUTURE_TIMEOUT, TimeUnit.MILLISECONDS);
        } catch (Exception ex) {
            assertInstanceOf(TimeoutException.class, ex.getCause());
        }
    }

    @RepeatedIfExceptionsTest(repeats = 5)
    public void basicPreemptiveAuthTimeoutTest() throws Throwable {
        try (AsyncHttpClient client = newClient()) {
            execute(client, true, true).get(LONG_FUTURE_TIMEOUT, TimeUnit.MILLISECONDS);
        } catch (Exception ex) {
            assertInstanceOf(TimeoutException.class, ex.getCause());
        }
    }

    @RepeatedIfExceptionsTest(repeats = 5)
    public void digestAuthTimeoutTest() throws Throwable {
        try (AsyncHttpClient client = newClient()) {
            execute(client, false, false).get(LONG_FUTURE_TIMEOUT, TimeUnit.MILLISECONDS);
        } catch (Exception ex) {
            assertInstanceOf(TimeoutException.class, ex.getCause());
        }
    }

    @Disabled
    @RepeatedIfExceptionsTest(repeats = 5)
    public void digestPreemptiveAuthTimeoutTest() throws Throwable {
        try (AsyncHttpClient client = newClient()) {
            assertThrows(TimeoutException.class, () -> execute(client, false, true).get(LONG_FUTURE_TIMEOUT, TimeUnit.MILLISECONDS));
        }
    }

    @RepeatedIfExceptionsTest(repeats = 5)
    public void basicAuthFutureTimeoutTest() throws Throwable {
        try (AsyncHttpClient client = newClient()) {
            assertThrows(TimeoutException.class, () -> execute(client, true, false).get(SHORT_FUTURE_TIMEOUT, TimeUnit.MILLISECONDS));
        }
    }

    @RepeatedIfExceptionsTest(repeats = 5)
    public void basicPreemptiveAuthFutureTimeoutTest() throws Throwable {
        try (AsyncHttpClient client = newClient()) {
            assertThrows(TimeoutException.class, () -> execute(client, true, true).get(SHORT_FUTURE_TIMEOUT, TimeUnit.MILLISECONDS));
        }
    }

    @RepeatedIfExceptionsTest(repeats = 5)
    public void digestAuthFutureTimeoutTest() throws Throwable {
        try (AsyncHttpClient client = newClient()) {
            assertThrows(TimeoutException.class, () -> execute(client, false, false).get(SHORT_FUTURE_TIMEOUT, TimeUnit.MILLISECONDS));
        }
    }

    @Disabled
    @RepeatedIfExceptionsTest(repeats = 5)
    public void digestPreemptiveAuthFutureTimeoutTest() throws Throwable {
        try (AsyncHttpClient client = newClient()) {
            assertThrows(TimeoutException.class, () -> execute(client, false, true).get(SHORT_FUTURE_TIMEOUT, TimeUnit.MILLISECONDS));
        }
    }

    private static AsyncHttpClient newClient() {
        return asyncHttpClient(config().setRequestTimeout(REQUEST_TIMEOUT));
    }

    protected Future<Response> execute(AsyncHttpClient client, boolean basic, boolean preemptive) {
        Realm.Builder realm;
        String url;

        if (basic) {
            realm = basicAuthRealm(USER, ADMIN);
            url = getTargetUrl();
        } else {
            realm = digestAuthRealm(USER, ADMIN);
            url = getTargetUrl2();
            if (preemptive) {
                realm.setRealmName("MyRealm");
                realm.setAlgorithm("MD5");
                realm.setQop("auth");
                realm.setNonce("fFDVc60re9zt8fFDvht0tNrYuvqrcchN");
            }
        }

        return client.prepareGet(url).setRealm(realm.setUsePreemptiveAuth(preemptive).build()).execute();
    }

    @Override
    protected String getTargetUrl() {
        return "http://localhost:" + port1 + '/';
    }

    @Override
    protected String getTargetUrl2() {
        return "http://localhost:" + port2 + '/';
    }

    @Override
    public AbstractHandler configureHandler() throws Exception {
        return new IncompleteResponseHandler();
    }

    private static class IncompleteResponseHandler extends AbstractHandler {

        @Override
        public void handle(String s, Request r, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
            // NOTE: handler sends less bytes than are given in Content-Length, which should lead to timeout
            response.setStatus(200);
            OutputStream out = response.getOutputStream();
            response.setIntHeader(CONTENT_LENGTH.toString(), 1000);
            out.write(0);
            out.flush();
            try {
                Thread.sleep(LONG_FUTURE_TIMEOUT + 100);
            } catch (InterruptedException e) {
                //
            }
        }
    }
}