// Copyright 2016 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.bazel.repository.downloader; import static com.google.common.io.ByteStreams.toByteArray; import static com.google.common.truth.Truth.assertThat; import static com.google.devtools.build.lib.bazel.repository.downloader.DownloaderTestUtils.makeUrl; import static java.nio.charset.StandardCharsets.UTF_8; import static org.junit.Assert.fail; import static org.mockito.Matchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import com.google.common.hash.Hashing; import com.google.common.io.ByteStreams; import com.google.devtools.build.lib.bazel.repository.downloader.RetryingInputStream.Reconnector; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.InterruptedIOException; import java.io.OutputStream; import java.net.HttpURLConnection; import java.net.URL; import java.util.Random; import java.util.concurrent.atomic.AtomicBoolean; import java.util.zip.GZIPOutputStream; import java.util.zip.ZipException; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; import org.junit.rules.Timeout; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; /** Integration tests for {@link HttpStream.Factory} and friends. */ @RunWith(JUnit4.class) public class HttpStreamTest { private static final Random randoCalrissian = new Random(); private static final byte[] data = "hello".getBytes(UTF_8); private static final String GOOD_CHECKSUM = "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"; private static final String BAD_CHECKSUM = "0000000000000000000000000000000000000000000000000000000000000000"; private static final URL AURL = makeUrl("http://doodle.example"); @Rule public final ExpectedException thrown = ExpectedException.none(); @Rule public final Timeout globalTimeout = new Timeout(10000); private final HttpURLConnection connection = mock(HttpURLConnection.class); private final Reconnector reconnector = mock(Reconnector.class); private final ProgressInputStream.Factory progress = mock(ProgressInputStream.Factory.class); private final HttpStream.Factory streamFactory = new HttpStream.Factory(progress); @Before public void before() throws Exception { when(connection.getInputStream()).thenReturn(new ByteArrayInputStream(data)); when(progress.create(any(InputStream.class), any(URL.class), any(URL.class))).thenAnswer( new Answer<InputStream>() { @Override public InputStream answer(InvocationOnMock invocation) throws Throwable { return (InputStream) invocation.getArguments()[0]; } }); } @Test public void noChecksum_readsOk() throws Exception { try (HttpStream stream = streamFactory.create(connection, AURL, "", reconnector)) { assertThat(toByteArray(stream)).isEqualTo(data); } } @Test public void smallDataWithValidChecksum_readsOk() throws Exception { try (HttpStream stream = streamFactory.create(connection, AURL, GOOD_CHECKSUM, reconnector)) { assertThat(toByteArray(stream)).isEqualTo(data); } } @Test public void smallDataWithInvalidChecksum_throwsIOExceptionInCreatePhase() throws Exception { thrown.expect(IOException.class); thrown.expectMessage("Checksum"); streamFactory.create(connection, AURL, BAD_CHECKSUM, reconnector); } @Test public void bigDataWithValidChecksum_readsOk() throws Exception { // at google, we know big data byte[] bigData = new byte[HttpStream.PRECHECK_BYTES + 70001]; randoCalrissian.nextBytes(bigData); when(connection.getInputStream()).thenReturn(new ByteArrayInputStream(bigData)); try (HttpStream stream = streamFactory.create( connection, AURL, Hashing.sha256().hashBytes(bigData).toString(), reconnector)) { assertThat(toByteArray(stream)).isEqualTo(bigData); } } @Test public void bigDataWithInvalidChecksum_throwsIOExceptionAfterCreateOnEof() throws Exception { // the probability of this test flaking is 8.6361686e-78 byte[] bigData = new byte[HttpStream.PRECHECK_BYTES + 70001]; randoCalrissian.nextBytes(bigData); when(connection.getInputStream()).thenReturn(new ByteArrayInputStream(bigData)); try (HttpStream stream = streamFactory.create(connection, AURL, BAD_CHECKSUM, reconnector)) { thrown.expect(IOException.class); thrown.expectMessage("Checksum"); ByteStreams.exhaust(stream); fail("Should have thrown error before close()"); } } @Test public void httpServerSaidGzippedButNotGzipped_throwsZipExceptionInCreate() throws Exception { when(connection.getURL()).thenReturn(AURL); when(connection.getContentEncoding()).thenReturn("gzip"); thrown.expect(ZipException.class); streamFactory.create(connection, AURL, "", reconnector); } @Test public void javascriptGzippedInTransit_automaticallyGunzips() throws Exception { when(connection.getURL()).thenReturn(AURL); when(connection.getContentEncoding()).thenReturn("x-gzip"); when(connection.getInputStream()).thenReturn(new ByteArrayInputStream(gzipData(data))); try (HttpStream stream = streamFactory.create(connection, AURL, "", reconnector)) { assertThat(toByteArray(stream)).isEqualTo(data); } } @Test public void serverSaysTarballPathIsGzipped_doesntAutomaticallyGunzip() throws Exception { byte[] gzData = gzipData(data); when(connection.getURL()).thenReturn(new URL("http://doodle.example/foo.tar.gz")); when(connection.getContentEncoding()).thenReturn("gzip"); when(connection.getInputStream()).thenReturn(new ByteArrayInputStream(gzData)); try (HttpStream stream = streamFactory.create(connection, AURL, "", reconnector)) { assertThat(toByteArray(stream)).isEqualTo(gzData); } } @Test public void threadInterrupted_haltsReadingAndThrowsInterrupt() throws Exception { final AtomicBoolean wasInterrupted = new AtomicBoolean(); Thread thread = new Thread( new Runnable() { @Override public void run() { try (HttpStream stream = streamFactory.create(connection, AURL, "", reconnector)) { stream.read(); Thread.currentThread().interrupt(); stream.read(); fail(); } catch (InterruptedIOException expected) { wasInterrupted.set(true); } catch (IOException ignored) { // ignored } } }); thread.start(); thread.join(); assertThat(wasInterrupted.get()).isTrue(); } private static byte[] gzipData(byte[] bytes) throws IOException { ByteArrayOutputStream baos = new ByteArrayOutputStream(); try (InputStream input = new ByteArrayInputStream(bytes); OutputStream output = new GZIPOutputStream(baos)) { ByteStreams.copy(input, output); } return baos.toByteArray(); } }