HttpResponseTest.java

/*
 * Copyright (c) 2010 Google Inc.
 *
 * 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.api.client.http;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import com.google.api.client.json.Json;
import com.google.api.client.testing.http.HttpTesting;
import com.google.api.client.testing.http.MockHttpTransport;
import com.google.api.client.testing.http.MockLowLevelHttpRequest;
import com.google.api.client.testing.http.MockLowLevelHttpResponse;
import com.google.api.client.testing.util.LogRecordingHandler;
import com.google.api.client.testing.util.TestableByteArrayInputStream;
import com.google.api.client.util.Key;
import com.google.common.io.ByteStreams;
import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Type;
import java.nio.charset.StandardCharsets;
import java.text.NumberFormat;
import java.util.Arrays;
import java.util.Locale;
import java.util.logging.Level;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

/**
 * Tests {@link HttpResponse}.
 *
 * @author Yaniv Inbar
 */
@RunWith(JUnit4.class)
public class HttpResponseTest {

  @Test
  public void testParseAsString_none() throws Exception {
    HttpTransport transport = new MockHttpTransport();
    HttpRequest request =
        transport.createRequestFactory().buildGetRequest(HttpTesting.SIMPLE_GENERIC_URL);
    HttpResponse response = request.execute();
    assertEquals("", response.parseAsString());
  }

  private static final String SAMPLE = "123\u05D9\u05e0\u05D9\u05D1";
  private static final String SAMPLE2 = "123abc";
  private static final String JSON_SAMPLE = "{\"foo\": \"��ar\"}";
  private static final String ERROR_SAMPLE =
      "{domain:'global',reason:'domainPolicy',message:'msg'}";
  private static final String VALID_CONTENT_TYPE = "text/plain";
  private static final String VALID_CONTENT_TYPE_WITH_PARAMS =
      "application/vnd.com.google.datastore.entity+json; charset=utf-8; version=v1; q=0.9";
  private static final String VALID_CONTENT_TYPE_WITHOUT_CHARSET = "text/csv; version=v1; q=0.9";
  private static final String INVALID_CONTENT_TYPE = "!!!invalid!!!";
  private static final String JSON_CONTENT_TYPE = "application/json";

  @Test
  public void testParseAsString_utf8() throws Exception {
    HttpTransport transport =
        new MockHttpTransport() {
          @Override
          public LowLevelHttpRequest buildRequest(String method, String url) throws IOException {
            return new MockLowLevelHttpRequest() {
              @Override
              public LowLevelHttpResponse execute() throws IOException {
                MockLowLevelHttpResponse result = new MockLowLevelHttpResponse();
                result.setContentType(Json.MEDIA_TYPE);
                result.setContent(SAMPLE);
                return result;
              }
            };
          }
        };
    HttpRequest request =
        transport.createRequestFactory().buildGetRequest(HttpTesting.SIMPLE_GENERIC_URL);
    HttpResponse response = request.execute();
    assertEquals(SAMPLE, response.parseAsString());
    assertEquals("UTF-8", response.getContentCharset().name());
  }

  @Test
  public void testParseAsString_noContentType() throws Exception {
    HttpTransport transport =
        new MockHttpTransport() {
          @Override
          public LowLevelHttpRequest buildRequest(String method, String url) throws IOException {
            return new MockLowLevelHttpRequest() {
              @Override
              public LowLevelHttpResponse execute() throws IOException {
                MockLowLevelHttpResponse result = new MockLowLevelHttpResponse();
                result.setContent(SAMPLE2);
                return result;
              }
            };
          }
        };
    HttpRequest request =
        transport.createRequestFactory().buildGetRequest(HttpTesting.SIMPLE_GENERIC_URL);
    HttpResponse response = request.execute();
    assertEquals(SAMPLE2, response.parseAsString());
    assertEquals("ISO-8859-1", response.getContentCharset().name());
  }

  @Test
  public void testParseAsString_validContentType() throws Exception {
    HttpTransport transport =
        new MockHttpTransport() {
          @Override
          public LowLevelHttpRequest buildRequest(String method, String url) throws IOException {
            return new MockLowLevelHttpRequest() {
              @Override
              public LowLevelHttpResponse execute() throws IOException {
                MockLowLevelHttpResponse result = new MockLowLevelHttpResponse();
                result.setContent(SAMPLE2);
                result.setContentType(VALID_CONTENT_TYPE);
                return result;
              }
            };
          }
        };
    HttpRequest request =
        transport.createRequestFactory().buildGetRequest(HttpTesting.SIMPLE_GENERIC_URL);

    HttpResponse response = request.execute();
    assertEquals(SAMPLE2, response.parseAsString());
    assertEquals(VALID_CONTENT_TYPE, response.getContentType());
    assertNotNull(response.getMediaType());
    assertEquals("ISO-8859-1", response.getContentCharset().name());
  }

  @Test
  public void testParseAsString_validContentTypeWithParams() throws Exception {
    HttpTransport transport =
        new MockHttpTransport() {
          @Override
          public LowLevelHttpRequest buildRequest(String method, String url) throws IOException {
            return new MockLowLevelHttpRequest() {
              @Override
              public LowLevelHttpResponse execute() throws IOException {
                MockLowLevelHttpResponse result = new MockLowLevelHttpResponse();
                result.setContent(SAMPLE2);
                result.setContentType(VALID_CONTENT_TYPE_WITH_PARAMS);
                return result;
              }
            };
          }
        };
    HttpRequest request =
        transport.createRequestFactory().buildGetRequest(HttpTesting.SIMPLE_GENERIC_URL);

    HttpResponse response = request.execute();
    assertEquals(SAMPLE2, response.parseAsString());
    assertEquals(VALID_CONTENT_TYPE_WITH_PARAMS, response.getContentType());
    assertNotNull(response.getMediaType());
    assertEquals("UTF-8", response.getContentCharset().name());
  }

  @Test
  public void testParseAsString_invalidContentType() throws Exception {
    HttpTransport transport =
        new MockHttpTransport() {
          @Override
          public LowLevelHttpRequest buildRequest(String method, String url) throws IOException {
            return new MockLowLevelHttpRequest() {
              @Override
              public LowLevelHttpResponse execute() throws IOException {
                MockLowLevelHttpResponse result = new MockLowLevelHttpResponse();
                result.setContent(SAMPLE2);
                result.setContentType(INVALID_CONTENT_TYPE);
                return result;
              }
            };
          }
        };
    HttpRequest request =
        transport.createRequestFactory().buildGetRequest(HttpTesting.SIMPLE_GENERIC_URL);

    HttpResponse response = request.execute();
    assertEquals(SAMPLE2, response.parseAsString());
    assertEquals(INVALID_CONTENT_TYPE, response.getContentType());
    assertNull(response.getMediaType());
    assertEquals("ISO-8859-1", response.getContentCharset().name());
  }

  @Test
  public void testParseAsString_validContentTypeWithoutCharSetWithParams() throws Exception {
    HttpTransport transport =
        new MockHttpTransport() {
          @Override
          public LowLevelHttpRequest buildRequest(String method, String url) throws IOException {
            return new MockLowLevelHttpRequest() {
              @Override
              public LowLevelHttpResponse execute() throws IOException {
                MockLowLevelHttpResponse result = new MockLowLevelHttpResponse();
                result.setContent(SAMPLE2);
                result.setContentType(VALID_CONTENT_TYPE_WITHOUT_CHARSET);
                return result;
              }
            };
          }
        };
    HttpRequest request =
        transport.createRequestFactory().buildGetRequest(HttpTesting.SIMPLE_GENERIC_URL);

    HttpResponse response = request.execute();
    assertEquals(SAMPLE2, response.parseAsString());
    assertEquals(VALID_CONTENT_TYPE_WITHOUT_CHARSET, response.getContentType());
    assertNotNull(response.getMediaType());
    assertEquals("UTF-8", response.getContentCharset().name());
  }

  @Test
  public void testParseAsString_jsonContentType() throws IOException {
    HttpTransport transport =
        new MockHttpTransport() {
          @Override
          public LowLevelHttpRequest buildRequest(String method, String url) throws IOException {
            return new MockLowLevelHttpRequest() {
              @Override
              public LowLevelHttpResponse execute() throws IOException {
                MockLowLevelHttpResponse result = new MockLowLevelHttpResponse();
                result.setContent(JSON_SAMPLE);
                result.setContentType(JSON_CONTENT_TYPE);
                return result;
              }
            };
          }
        };
    HttpRequest request =
        transport.createRequestFactory().buildGetRequest(HttpTesting.SIMPLE_GENERIC_URL);

    HttpResponse response = request.execute();
    assertEquals(JSON_SAMPLE, response.parseAsString());
    assertEquals(JSON_CONTENT_TYPE, response.getContentType());
    assertEquals("UTF-8", response.getContentCharset().name());
  }

  @Test
  public void testStatusCode_negative_dontThrowException() throws Exception {
    subtestStatusCode_negative(false);
  }

  @Test
  public void testStatusCode_negative_throwException() throws Exception {
    subtestStatusCode_negative(true);
  }

  private void subtestStatusCode_negative(boolean throwExceptionOnExecuteError) throws Exception {
    HttpTransport transport =
        new MockHttpTransport() {
          @Override
          public LowLevelHttpRequest buildRequest(String method, String url) throws IOException {
            return new MockLowLevelHttpRequest()
                .setResponse(new MockLowLevelHttpResponse().setStatusCode(-1));
          }
        };
    HttpRequest request =
        transport.createRequestFactory().buildGetRequest(HttpTesting.SIMPLE_GENERIC_URL);
    request.setThrowExceptionOnExecuteError(throwExceptionOnExecuteError);
    try {
      // HttpResponse converts a negative status code to zero
      HttpResponse response = request.execute();
      assertEquals(0, response.getStatusCode());
      assertFalse(throwExceptionOnExecuteError);
    } catch (HttpResponseException e) {
      // exception should be thrown only if throwExceptionOnExecuteError is true
      assertTrue(throwExceptionOnExecuteError);
      assertEquals(0, e.getStatusCode());
    }
  }

  public static class MyHeaders extends HttpHeaders {

    @Key public String foo;

    @Key public Object obj;

    @Key String[] r;
  }

  static final String ETAG_VALUE = "\"abc\"";

  @Test
  public void testHeaderParsing() throws Exception {
    HttpTransport transport =
        new MockHttpTransport() {
          @Override
          public LowLevelHttpRequest buildRequest(String method, String url) throws IOException {
            return new MockLowLevelHttpRequest() {
              @Override
              public LowLevelHttpResponse execute() throws IOException {
                MockLowLevelHttpResponse result = new MockLowLevelHttpResponse();
                result.addHeader("accept", "value");
                result.addHeader("foo", "bar");
                result.addHeader("goo", "car");
                result.addHeader("hoo", "dar");
                result.addHeader("hoo", "far");
                result.addHeader("obj", "o");
                result.addHeader("r", "a1");
                result.addHeader("r", "a2");
                result.addHeader("ETAG", ETAG_VALUE);
                return result;
              }
            };
          }
        };
    HttpRequest request =
        transport.createRequestFactory().buildGetRequest(HttpTesting.SIMPLE_GENERIC_URL);
    request.setResponseHeaders(new MyHeaders());
    HttpResponse response = request.execute();
    assertEquals("value", response.getHeaders().getAccept());
    assertEquals("bar", ((MyHeaders) response.getHeaders()).foo);
    assertEquals(Arrays.asList("o"), ((MyHeaders) response.getHeaders()).obj);
    assertEquals(Arrays.asList("a1", "a2"), Arrays.asList(((MyHeaders) response.getHeaders()).r));
    assertEquals(Arrays.asList("car"), response.getHeaders().get("goo"));
    assertEquals(Arrays.asList("dar", "far"), response.getHeaders().get("hoo"));
    assertEquals(ETAG_VALUE, response.getHeaders().getETag());
  }

  @Test
  public void testParseAs_noParser() throws Exception {
    try {
      new MockHttpTransport()
          .createRequestFactory()
          .buildGetRequest(HttpTesting.SIMPLE_GENERIC_URL)
          .execute()
          .parseAs(Object.class);
      fail("expected " + NullPointerException.class);
    } catch (NullPointerException e) {
      // expected
    }
  }

  @Test
  public void testParseAs_classNoContent() throws Exception {
    final MockLowLevelHttpResponse result = new MockLowLevelHttpResponse();

    for (final int status :
        new int[] {
          HttpStatusCodes.STATUS_CODE_NO_CONTENT, HttpStatusCodes.STATUS_CODE_NOT_MODIFIED, 102
        }) {
      HttpTransport transport =
          new MockHttpTransport() {
            @Override
            public LowLevelHttpRequest buildRequest(String method, final String url)
                throws IOException {
              return new MockLowLevelHttpRequest() {
                @Override
                public LowLevelHttpResponse execute() throws IOException {
                  result.setStatusCode(status);
                  result.setContentType(null);
                  result.setContent(new ByteArrayInputStream(new byte[0]));
                  return result;
                }
              };
            }
          };

      // Confirm that 'null' is returned when getting the response object of a
      // request with no message body.
      Object parsed =
          transport
              .createRequestFactory()
              .buildGetRequest(HttpTesting.SIMPLE_GENERIC_URL)
              .setThrowExceptionOnExecuteError(false)
              .execute()
              .parseAs(Object.class);
      assertNull(parsed);
    }
  }

  @Test
  public void testParseAs_typeNoContent() throws Exception {
    final MockLowLevelHttpResponse result = new MockLowLevelHttpResponse();

    for (final int status :
        new int[] {
          HttpStatusCodes.STATUS_CODE_NO_CONTENT, HttpStatusCodes.STATUS_CODE_NOT_MODIFIED, 102
        }) {
      HttpTransport transport =
          new MockHttpTransport() {
            @Override
            public LowLevelHttpRequest buildRequest(String method, final String url)
                throws IOException {
              return new MockLowLevelHttpRequest() {
                @Override
                public LowLevelHttpResponse execute() throws IOException {
                  result.setStatusCode(status);
                  result.setContentType(null);
                  result.setContent(new ByteArrayInputStream(new byte[0]));
                  return result;
                }
              };
            }
          };

      // Confirm that 'null' is returned when getting the response object of a
      // request with no message body.
      Object parsed =
          transport
              .createRequestFactory()
              .buildGetRequest(HttpTesting.SIMPLE_GENERIC_URL)
              .setThrowExceptionOnExecuteError(false)
              .execute()
              .parseAs((Type) Object.class);
      assertNull(parsed);
    }
  }

  @Test
  public void testDownload() throws Exception {
    HttpTransport transport =
        new MockHttpTransport() {
          @Override
          public LowLevelHttpRequest buildRequest(String method, String url) throws IOException {
            return new MockLowLevelHttpRequest() {
              @Override
              public LowLevelHttpResponse execute() throws IOException {
                MockLowLevelHttpResponse result = new MockLowLevelHttpResponse();
                result.setContentType(Json.MEDIA_TYPE);
                result.setContent(SAMPLE);
                return result;
              }
            };
          }
        };
    HttpRequest request =
        transport.createRequestFactory().buildGetRequest(HttpTesting.SIMPLE_GENERIC_URL);
    HttpResponse response = request.execute();
    ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
    response.download(outputStream);
    assertEquals(SAMPLE, outputStream.toString("UTF-8"));
  }

  @Test
  public void testDisconnectWithContent() throws Exception {
    final MockLowLevelHttpResponse lowLevelHttpResponse = new MockLowLevelHttpResponse();

    HttpTransport transport =
        new MockHttpTransport() {
          @Override
          public LowLevelHttpRequest buildRequest(String method, String url) throws IOException {
            return new MockLowLevelHttpRequest() {
              @Override
              public LowLevelHttpResponse execute() throws IOException {
                lowLevelHttpResponse.setContentType(Json.MEDIA_TYPE);
                lowLevelHttpResponse.setContent(SAMPLE);
                return lowLevelHttpResponse;
              }
            };
          }
        };
    HttpRequest request =
        transport.createRequestFactory().buildGetRequest(HttpTesting.SIMPLE_GENERIC_URL);
    HttpResponse response = request.execute();

    assertFalse(lowLevelHttpResponse.isDisconnected());
    TestableByteArrayInputStream content =
        (TestableByteArrayInputStream) lowLevelHttpResponse.getContent();
    assertFalse(content.isClosed());
    response.disconnect();
    assertTrue(lowLevelHttpResponse.isDisconnected());
    assertTrue(content.isClosed());
  }

  @Test
  public void testDisconnectWithNoContent() throws Exception {
    final MockLowLevelHttpResponse lowLevelHttpResponse = new MockLowLevelHttpResponse();

    HttpTransport transport =
        new MockHttpTransport() {
          @Override
          public LowLevelHttpRequest buildRequest(String method, String url) throws IOException {
            return new MockLowLevelHttpRequest() {
              @Override
              public LowLevelHttpResponse execute() throws IOException {
                return lowLevelHttpResponse;
              }
            };
          }
        };
    HttpRequest request =
        transport.createRequestFactory().buildGetRequest(HttpTesting.SIMPLE_GENERIC_URL);
    HttpResponse response = request.execute();

    assertFalse(lowLevelHttpResponse.isDisconnected());
    response.disconnect();
    assertTrue(lowLevelHttpResponse.isDisconnected());
  }

  @Test
  public void testContentLoggingLimitWithLoggingEnabledAndDisabled() throws Exception {
    subtestContentLoggingLimit("", 2, false);
    subtestContentLoggingLimit("A", 2, false);
    subtestContentLoggingLimit("ABC" + '\0' + "DEF", 20, true, "Total: 7 bytes", "ABC DEF");
    subtestContentLoggingLimit("A", 2, true, "Total: 1 byte", "A");
    try {
      subtestContentLoggingLimit("ABC", -1, true);
      fail("Expected: " + IllegalArgumentException.class);
    } catch (IllegalArgumentException e) {
      // Expected.
    }
    subtestContentLoggingLimit("ABC", 0, true, "Total: 3 bytes");
    subtestContentLoggingLimit("ABC", 2, true, "Total: 3 bytes (logging first 2 bytes)", "AB");
    subtestContentLoggingLimit("ABC", 3, true, "Total: 3 bytes", "ABC");
    subtestContentLoggingLimit("ABC", 4, true, "Total: 3 bytes", "ABC");
    char[] a = new char[18000];
    Arrays.fill(a, 'x');
    String big = new String(a);
    String formated18kInteger = NumberFormat.getInstance().format(18000);
    subtestContentLoggingLimit(
        big, Integer.MAX_VALUE, true, String.format("Total: %s bytes", formated18kInteger), big);
    subtestContentLoggingLimit(
        big,
        4,
        true,
        String.format("Total: %s bytes (logging first 4 bytes)", formated18kInteger),
        "xxxx");
  }

  public void subtestContentLoggingLimit(
      final String content,
      int contentLoggingLimit,
      boolean loggingEnabled,
      String... expectedMessages)
      throws Exception {
    HttpTransport transport =
        new MockHttpTransport() {
          @Override
          public LowLevelHttpRequest buildRequest(String method, final String url)
              throws IOException {
            return new MockLowLevelHttpRequest() {
              @Override
              public LowLevelHttpResponse execute() throws IOException {
                MockLowLevelHttpResponse result = new MockLowLevelHttpResponse();
                result.setContent(content);
                result.setContentType("text/plain");
                return result;
              }
            };
          }
        };
    HttpTransport.LOGGER.setLevel(Level.CONFIG);

    HttpRequest request =
        transport.createRequestFactory().buildGetRequest(HttpTesting.SIMPLE_GENERIC_URL);
    request.setLoggingEnabled(loggingEnabled);
    HttpResponse response = request.execute();
    assertEquals(loggingEnabled, response.isLoggingEnabled());

    response.setContentLoggingLimit(contentLoggingLimit);
    LogRecordingHandler recorder = new LogRecordingHandler();
    HttpTransport.LOGGER.addHandler(recorder);
    response.parseAsString();
    assertEquals(Arrays.asList(expectedMessages), recorder.messages());
  }

  @Test
  public void testGetContent_gzipNoContent() throws IOException {
    HttpTransport transport =
        new MockHttpTransport() {
          @Override
          public LowLevelHttpRequest buildRequest(String method, final String url)
              throws IOException {
            return new MockLowLevelHttpRequest() {
              @Override
              public LowLevelHttpResponse execute() throws IOException {
                MockLowLevelHttpResponse result = new MockLowLevelHttpResponse();
                result.setContent("");
                result.setContentEncoding("gzip");
                result.setContentType("text/plain");
                return result;
              }
            };
          }
        };
    HttpRequest request =
        transport.createRequestFactory().buildHeadRequest(HttpTesting.SIMPLE_GENERIC_URL);
    InputStream noContent = request.execute().getContent();
    assertNull(noContent);
  }

  @Test
  public void testGetContent_gzipEncoding_ReturnRawStream() throws IOException {
    HttpTransport transport =
        new MockHttpTransport() {
          @Override
          public LowLevelHttpRequest buildRequest(String method, final String url)
              throws IOException {
            return new MockLowLevelHttpRequest() {
              @Override
              public LowLevelHttpResponse execute() throws IOException {
                MockLowLevelHttpResponse result = new MockLowLevelHttpResponse();
                result.setContent("");
                result.setContentEncoding("gzip");
                result.setContentType("text/plain");
                return result;
              }
            };
          }
        };
    HttpRequest request =
        transport.createRequestFactory().buildHeadRequest(HttpTesting.SIMPLE_GENERIC_URL);
    request.setResponseReturnRawInputStream(true);
    assertFalse(
        "it should not decompress stream",
        request.execute().getContent() instanceof GZIPInputStream);
    assertFalse(
        "it should not buffer stream",
        request.execute().getContent() instanceof BufferedInputStream);
  }

  @Test
  public void testGetContent_gzipEncoding_finishReading() throws IOException {
    do_testGetContent_gzipEncoding_finishReading("gzip");
  }

  @Test
  public void testGetContent_gzipEncoding_finishReadingWithUppercaseContentEncoding()
      throws IOException {
    do_testGetContent_gzipEncoding_finishReading("GZIP");
  }

  @Test
  public void
      testGetContent_gzipEncoding_finishReadingWithDifferentDefaultLocaleAndUppercaseContentEncoding()
          throws IOException {
    Locale originalDefaultLocale = Locale.getDefault();
    try {
      Locale.setDefault(Locale.forLanguageTag("tr-TR"));
      do_testGetContent_gzipEncoding_finishReading("GZIP");
    } finally {
      Locale.setDefault(originalDefaultLocale);
    }
  }

  private void do_testGetContent_gzipEncoding_finishReading(String contentEncoding)
      throws IOException {
    byte[] dataToCompress = "abcd".getBytes(StandardCharsets.UTF_8);
    byte[] mockBytes;
    try (ByteArrayOutputStream byteStream = new ByteArrayOutputStream(dataToCompress.length);
        GZIPOutputStream zipStream = new GZIPOutputStream((byteStream))) {
      zipStream.write(dataToCompress);
      zipStream.close();

      // GZIPInputStream uses a default buffer of 512B. Add enough content to exceed this
      // limit, so that some content will be left in the connection.
      for (int i = 0; i < 1024; i++) {
        byteStream.write('7');
      }
      mockBytes = byteStream.toByteArray();
    }
    final MockLowLevelHttpResponse mockResponse = new MockLowLevelHttpResponse();
    mockResponse.setContent(mockBytes);
    mockResponse.setContentEncoding(contentEncoding);
    mockResponse.setContentType("text/plain");

    HttpTransport transport =
        new MockHttpTransport() {
          @Override
          public LowLevelHttpRequest buildRequest(String method, final String url)
              throws IOException {
            return new MockLowLevelHttpRequest() {
              @Override
              public LowLevelHttpResponse execute() throws IOException {
                return mockResponse;
              }
            };
          }
        };
    HttpRequest request =
        transport.createRequestFactory().buildHeadRequest(HttpTesting.SIMPLE_GENERIC_URL);
    HttpResponse response = request.execute();
    try (TestableByteArrayInputStream output =
        (TestableByteArrayInputStream) mockResponse.getContent()) {
      assertFalse(output.isClosed());
      assertEquals("abcd", response.parseAsString());
      assertTrue(output.isClosed());
      // The underlying stream should be fully consumed, even if gzip only returns some of it.
      assertEquals(-1, output.read());
    }
  }

  @Test
  public void testGetContent_otherEncodingWithgzipInItsName_GzipIsNotUsed() throws IOException {
    final MockLowLevelHttpResponse mockResponse = new MockLowLevelHttpResponse();
    mockResponse.setContent("abcd");
    mockResponse.setContentEncoding("otherEncodingWithgzipInItsName");
    mockResponse.setContentType("text/plain");

    HttpTransport transport =
        new MockHttpTransport() {
          @Override
          public LowLevelHttpRequest buildRequest(String method, final String url)
              throws IOException {
            return new MockLowLevelHttpRequest() {
              @Override
              public LowLevelHttpResponse execute() throws IOException {
                return mockResponse;
              }
            };
          }
        };
    HttpRequest request =
        transport.createRequestFactory().buildHeadRequest(HttpTesting.SIMPLE_GENERIC_URL);
    // If gzip was used on this response, an exception would be thrown
    HttpResponse response = request.execute();
    assertEquals("abcd", response.parseAsString());
  }

  @Test
  public void testGetContent_bufferedContent() throws IOException {
    HttpTransport transport =
        new MockHttpTransport() {
          @Override
          public LowLevelHttpRequest buildRequest(String method, String url) throws IOException {
            return new MockLowLevelHttpRequest() {
              @Override
              public LowLevelHttpResponse execute() throws IOException {
                // have to use gzip here because MockLowLevelHttpResponse.setContent()
                // returns BufferedStream by itself, so test always success
                byte[] dataToCompress = ERROR_SAMPLE.getBytes(StandardCharsets.UTF_8);
                ByteArrayOutputStream content = new ByteArrayOutputStream(dataToCompress.length);
                try (GZIPOutputStream zipStream = new GZIPOutputStream((content))) {
                  zipStream.write(dataToCompress);
                }

                MockLowLevelHttpResponse result = new MockLowLevelHttpResponse();
                result.setStatusCode(403);
                result.setContentType(JSON_CONTENT_TYPE);
                result.setContentEncoding("gzip");
                result.setContent(content.toByteArray());

                return result;
              }
            };
          }
        };
    HttpRequest request =
        transport
            .createRequestFactory()
            .buildGetRequest(HttpTesting.SIMPLE_GENERIC_URL)
            .setThrowExceptionOnExecuteError(false);

    HttpResponse response = request.execute();
    InputStream content = response.getContent();
    assertTrue(content.markSupported());

    // inspect content like in HttpUnsuccessfulResponseHandler
    try (RollbackInputStream is = new RollbackInputStream(content)) {
      byte[] bytes = ByteStreams.toByteArray(is);
      String text = new String(bytes, response.getContentCharset());
      assertEquals(ERROR_SAMPLE, text);
    }

    // original response still parsable by HttpResponseException
    HttpResponseException exception = new HttpResponseException(response);
    assertEquals(exception.getStatusCode(), 403);
    assertEquals(exception.getContent(), ERROR_SAMPLE);
  }

  static class RollbackInputStream extends FilterInputStream {
    private boolean closed;

    RollbackInputStream(InputStream in) {
      super(in);
      in.mark(8192); // big enough to keep most error messages
    }

    @Override
    public void close() throws IOException {
      if (!closed) {
        closed = true;
        in.reset();
      }
    }
  }
}