| // Copyright 2018 The Bazel Authors. 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 com.google.devtools.build.lib.remote.http; |
| |
| import static com.google.common.truth.Truth.assertThat; |
| import static org.mockito.Mockito.never; |
| import static org.mockito.Mockito.verify; |
| |
| import build.bazel.remote.execution.v2.Digest; |
| import com.google.common.collect.ImmutableList; |
| import com.google.common.net.HttpHeaders; |
| import com.google.devtools.build.lib.remote.util.DigestUtil; |
| import com.google.devtools.build.lib.vfs.DigestHashFunction; |
| import io.netty.buffer.ByteBuf; |
| import io.netty.buffer.ByteBufUtil; |
| import io.netty.buffer.Unpooled; |
| import io.netty.channel.ChannelPromise; |
| import io.netty.channel.embedded.EmbeddedChannel; |
| import io.netty.handler.codec.http.DefaultHttpContent; |
| import io.netty.handler.codec.http.DefaultHttpResponse; |
| import io.netty.handler.codec.http.DefaultLastHttpContent; |
| import io.netty.handler.codec.http.HttpHeaderNames; |
| import io.netty.handler.codec.http.HttpHeaderValues; |
| import io.netty.handler.codec.http.HttpMethod; |
| import io.netty.handler.codec.http.HttpRequest; |
| import io.netty.handler.codec.http.HttpResponse; |
| import io.netty.handler.codec.http.HttpResponseStatus; |
| import io.netty.handler.codec.http.HttpVersion; |
| import io.netty.handler.codec.http.LastHttpContent; |
| import java.io.ByteArrayOutputStream; |
| import java.io.IOException; |
| import java.net.URI; |
| import org.junit.Test; |
| import org.junit.runner.RunWith; |
| import org.junit.runners.JUnit4; |
| import org.mockito.Mockito; |
| |
| /** Tests for {@link HttpDownloadHandler}. */ |
| @RunWith(JUnit4.class) |
| public class HttpDownloadHandlerTest extends AbstractHttpHandlerTest { |
| |
| private static final URI CACHE_URI = URI.create("http://storage.googleapis.com:80/cache-bucket"); |
| private static final DigestUtil DIGEST_UTIL = new DigestUtil(DigestHashFunction.SHA256); |
| private static final Digest DIGEST = DIGEST_UTIL.computeAsUtf8("foo"); |
| |
| /** |
| * Test that downloading blobs works from both the Action Cache and the CAS. Also test that the |
| * handler is reusable. |
| */ |
| @Test |
| public void downloadShouldWork() throws IOException { |
| EmbeddedChannel ch = new EmbeddedChannel(new HttpDownloadHandler(null, ImmutableList.of())); |
| downloadShouldWork(true, ch); |
| downloadShouldWork(false, ch); |
| } |
| |
| private void downloadShouldWork(boolean casDownload, EmbeddedChannel ch) throws IOException { |
| ByteArrayOutputStream out = Mockito.spy(new ByteArrayOutputStream()); |
| DownloadCommand cmd = new DownloadCommand(CACHE_URI, casDownload, DIGEST, out); |
| ChannelPromise writePromise = ch.newPromise(); |
| ch.writeOneOutbound(cmd, writePromise); |
| |
| HttpRequest request = ch.readOutbound(); |
| assertThat(request.method()).isEqualTo(HttpMethod.GET); |
| assertThat(request.headers().get(HttpHeaderNames.HOST)).isEqualTo(CACHE_URI.getHost()); |
| if (casDownload) { |
| assertThat(request.uri()).isEqualTo("/cache-bucket/cas/" + DIGEST.getHash()); |
| } else { |
| assertThat(request.uri()).isEqualTo("/cache-bucket/ac/" + DIGEST.getHash()); |
| } |
| |
| assertThat(writePromise.isDone()).isFalse(); |
| |
| HttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK); |
| response.headers().set(HttpHeaders.CONTENT_LENGTH, 5); |
| response.headers().set(HttpHeaders.CONNECTION, HttpHeaderValues.KEEP_ALIVE); |
| ch.writeInbound(response); |
| ByteBuf content = Unpooled.buffer(); |
| content.writeBytes(new byte[] {1, 2, 3, 4, 5}); |
| ch.writeInbound(new DefaultLastHttpContent(content)); |
| |
| assertThat(writePromise.isDone()).isTrue(); |
| assertThat(out.toByteArray()).isEqualTo(new byte[] {1, 2, 3, 4, 5}); |
| verify(out, never()).close(); |
| assertThat(ch.isActive()).isTrue(); |
| } |
| |
| /** Test that the handler correctly supports http error codes i.e. 404 (NOT FOUND). */ |
| @Test |
| public void httpErrorsAreSupported() throws IOException { |
| EmbeddedChannel ch = new EmbeddedChannel(new HttpDownloadHandler(null, ImmutableList.of())); |
| ByteArrayOutputStream out = Mockito.spy(new ByteArrayOutputStream()); |
| DownloadCommand cmd = new DownloadCommand(CACHE_URI, true, DIGEST, out); |
| ChannelPromise writePromise = ch.newPromise(); |
| ch.writeOneOutbound(cmd, writePromise); |
| |
| HttpResponse response = |
| new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.NOT_FOUND); |
| response.headers().set(HttpHeaders.HOST, "localhost"); |
| response.headers().set(HttpHeaders.CONTENT_LENGTH, 0); |
| response.headers().set(HttpHeaders.CONNECTION, HttpHeaderValues.KEEP_ALIVE); |
| ch.writeInbound(response); |
| ch.writeInbound(LastHttpContent.EMPTY_LAST_CONTENT); |
| assertThat(writePromise.isDone()).isTrue(); |
| assertThat(writePromise.cause()).isInstanceOf(HttpException.class); |
| assertThat(((HttpException) writePromise.cause()).response().status()) |
| .isEqualTo(HttpResponseStatus.NOT_FOUND); |
| // No data should have been written to the OutputStream and it should have been closed. |
| assertThat(out.size()).isEqualTo(0); |
| // The caller is responsible for closing the stream. |
| verify(out, never()).close(); |
| assertThat(ch.isOpen()).isTrue(); |
| } |
| |
| /** |
| * Test that the handler correctly supports http error codes i.e. 404 (NOT FOUND) with a |
| * Content-Length header. |
| */ |
| @Test |
| public void httpErrorsWithContentAreSupported() throws IOException { |
| EmbeddedChannel ch = new EmbeddedChannel(new HttpDownloadHandler(null, ImmutableList.of())); |
| ByteArrayOutputStream out = Mockito.spy(new ByteArrayOutputStream()); |
| DownloadCommand cmd = new DownloadCommand(CACHE_URI, true, DIGEST, out); |
| ChannelPromise writePromise = ch.newPromise(); |
| ch.writeOneOutbound(cmd, writePromise); |
| |
| HttpResponse response = |
| new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.NOT_FOUND); |
| ByteBuf errorMessage = ByteBufUtil.writeAscii(ch.alloc(), "Error message"); |
| response.headers().set(HttpHeaders.HOST, "localhost"); |
| response |
| .headers() |
| .set(HttpHeaders.CONTENT_LENGTH, String.valueOf(errorMessage.readableBytes())); |
| response.headers().set(HttpHeaders.CONNECTION, HttpHeaderValues.CLOSE); |
| |
| ch.writeInbound(response); |
| // The promise must not be done because we haven't received the error message yet. |
| assertThat(writePromise.isDone()).isFalse(); |
| |
| ch.writeInbound(new DefaultHttpContent(errorMessage)); |
| ch.writeInbound(LastHttpContent.EMPTY_LAST_CONTENT); |
| assertThat(writePromise.isDone()).isTrue(); |
| assertThat(writePromise.cause()).isInstanceOf(HttpException.class); |
| assertThat(((HttpException) writePromise.cause()).response().status()) |
| .isEqualTo(HttpResponseStatus.NOT_FOUND); |
| // No data should have been written to the OutputStream and it should have been closed. |
| assertThat(out.size()).isEqualTo(0); |
| // The caller is responsible for closing the stream. |
| verify(out, never()).close(); |
| assertThat(ch.isOpen()).isFalse(); |
| } |
| } |