All Downloads are FREE. Search and download functionalities are using the official Maven repository.

software.amazon.awssdk.http.SdkAsyncHttpClientH1TestSuite Maven / Gradle / Ivy

Go to download

A set of acceptance tests that can be extended by an SDK HTTP plugin implementation to help ensure it behaves in manner consistent with what is expected by the SDK.

There is a newer version: 2.29.39
Show newest version
/*
 * Copyright Amazon.com, Inc. or its affiliates. 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.
 * A copy of the License is located at
 *
 *  http://aws.amazon.com/apache2.0
 *
 * or in the "license" file accompanying this file. This file 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 software.amazon.awssdk.http;

import static io.netty.handler.codec.http.HttpHeaderNames.CONNECTION;
import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_LENGTH;
import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_TYPE;
import static io.netty.handler.codec.http.HttpHeaderValues.CLOSE;
import static io.netty.handler.codec.http.HttpHeaderValues.TEXT_PLAIN;
import static io.netty.handler.codec.http.HttpResponseStatus.INTERNAL_SERVER_ERROR;
import static io.netty.handler.codec.http.HttpResponseStatus.OK;
import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy;
import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.Unpooled;
import io.netty.channel.Channel;
import io.netty.channel.ChannelDuplexHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.ServerSocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.http.DefaultFullHttpResponse;
import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.codec.http.HttpRequest;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.codec.http.HttpVersion;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslContextBuilder;
import io.netty.handler.ssl.util.SelfSignedCertificate;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import software.amazon.awssdk.http.async.SdkAsyncHttpClient;

/**
 * A set of tests validating that the functionality implemented by a {@link SdkAsyncHttpClient} for HTTP/1 requests
 *
 * This is used by an HTTP plugin implementation by extending this class and implementing the abstract methods to provide this
 * suite with a testable HTTP client implementation.
 */
public abstract class SdkAsyncHttpClientH1TestSuite {
    private Server server;
    private SdkAsyncHttpClient client;

    protected abstract SdkAsyncHttpClient setupClient();

    @BeforeEach
    public void setup() throws Exception {
        server = new Server();
        server.init();

        this.client = setupClient();
    }

    @AfterEach
    public void teardown() throws InterruptedException {
        if (server != null) {
            server.shutdown();
        }

        if (client != null) {
            client.close();
        }
        server = null;
    }

    @Test
    public void connectionReceiveServerErrorStatusShouldNotReuseConnection() {
        server.return500OnFirstRequest = true;
        server.closeConnection = false;

        HttpTestUtils.sendGetRequest(server.port(), client).join();
        HttpTestUtils.sendGetRequest(server.port(), client).join();
        assertThat(server.channels.size()).isEqualTo(2);
    }

    @Test
    public void connectionReceiveOkStatusShouldReuseConnection() throws Exception {
        server.return500OnFirstRequest = false;
        server.closeConnection = false;

        HttpTestUtils.sendGetRequest(server.port(), client).join();

        // The request-complete-future does not await the channel-release-future
        // Wait a small amount to allow the channel release to complete
        Thread.sleep(100);
        
        HttpTestUtils.sendGetRequest(server.port(), client).join();

        assertThat(server.channels.size()).isEqualTo(1);
    }

    @Test
    public void connectionReceiveCloseHeaderShouldNotReuseConnection() throws InterruptedException {
        server.return500OnFirstRequest = false;
        server.closeConnection = true;

        HttpTestUtils.sendGetRequest(server.port(), client).join();
        Thread.sleep(1000);

        HttpTestUtils.sendGetRequest(server.port(), client).join();
        assertThat(server.channels.size()).isEqualTo(2);
    }

    @Test
    public void headRequestResponsesHaveNoPayload() {
        byte[] responseData = HttpTestUtils.sendHeadRequest(server.port(), client).join();

        // The SDK core differentiates between NO data and ZERO bytes of data. Core expects it to be NO data, not ZERO bytes of
        // data for head requests.
        assertThat(responseData).isNull();
    }

    @Test
    public void naughtyHeaderCharactersDoNotGetToServer() {
        String naughtyHeader = "foo\r\nbar";
        assertThatThrownBy(() -> HttpTestUtils.sendRequest(client,
                                                           SdkHttpFullRequest.builder()
                                                                             .uri(URI.create("https://localhost:" + server.port()))
                                                                             .method(SdkHttpMethod.POST)
                                                                             .appendHeader("h", naughtyHeader)
                                                                             .build())
                                              .join())
            .hasCauseInstanceOf(Exception.class);
    }

    @Test
    public void connectionsArePooledByHostAndPort() throws InterruptedException {
        HttpTestUtils.sendRequest(client,
                                  SdkHttpFullRequest.builder()
                                                    .uri(URI.create("https://127.0.0.1:" + server.port() + "/foo?foo"))
                                                    .method(SdkHttpMethod.GET)
                                                    .build())
                     .join();

        Thread.sleep(1_000);

        HttpTestUtils.sendRequest(client,
                                  SdkHttpFullRequest.builder()
                                                    .uri(URI.create("https://127.0.0.1:" + server.port() + "/bar?bar"))
                                                    .method(SdkHttpMethod.GET)
                                                    .build())
            .join();

        assertThat(server.channels.size()).isEqualTo(1);

    }

    private static class Server extends ChannelInitializer {
        private static final byte[] CONTENT = "helloworld".getBytes(StandardCharsets.UTF_8);
        private ServerBootstrap bootstrap;
        private ServerSocketChannel serverSock;
        private List channels = new ArrayList<>();
        private final NioEventLoopGroup group = new NioEventLoopGroup();
        private SslContext sslCtx;
        private boolean return500OnFirstRequest;
        private boolean closeConnection;
        private volatile HttpRequest lastRequestReceived;

        public void init() throws Exception {
            SelfSignedCertificate ssc = new SelfSignedCertificate();
            sslCtx = SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey()).build();

            bootstrap = new ServerBootstrap()
                .channel(NioServerSocketChannel.class)
                .handler(new LoggingHandler(LogLevel.DEBUG))
                .group(group)
                .childHandler(this);

            serverSock = (ServerSocketChannel) bootstrap.bind(0).sync().channel();
        }

        public void shutdown() throws InterruptedException {
            group.shutdownGracefully().await();
            serverSock.close();
        }

        public int port() {
            return serverSock.localAddress().getPort();
        }

        @Override
        protected void initChannel(Channel ch) {
            channels.add(ch);
            ChannelPipeline pipeline = ch.pipeline();
            pipeline.addLast(sslCtx.newHandler(ch.alloc()));
            pipeline.addLast(new HttpServerCodec());
            pipeline.addLast(new BehaviorTestChannelHandler());
        }

        private class BehaviorTestChannelHandler extends ChannelDuplexHandler {

            @Override
            public void channelRead(ChannelHandlerContext ctx, Object msg) {
                if (msg instanceof HttpRequest) {
                    lastRequestReceived = (HttpRequest) msg;

                    HttpResponseStatus status;
                    if (ctx.channel().equals(channels.get(0)) && return500OnFirstRequest) {
                        status = INTERNAL_SERVER_ERROR;
                    } else {
                        status = OK;
                    }

                    FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, status,
                                                                            Unpooled.wrappedBuffer(CONTENT));

                    response.headers()
                            .set(CONTENT_TYPE, TEXT_PLAIN)
                            .setInt(CONTENT_LENGTH, response.content().readableBytes());

                    if (closeConnection) {
                        response.headers().set(CONNECTION, CLOSE);
                    }

                    ctx.writeAndFlush(response);
                }
            }
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy