blob: d48aebcbd35ef8f6d4a404d428559d6a75693860 [file] [log] [blame]
// Copyright 2011 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.
// Author: jmarantz@google.com (Joshua Marantz)
// Unit-test InflatingFetch.
#include <cstddef>
#include "net/instaweb/http/public/inflating_fetch.h"
#include "net/instaweb/http/public/async_fetch.h" // for StringAsyncFetch
#include "net/instaweb/http/public/http_value.h"
#include "net/instaweb/http/public/request_context.h"
#include "pagespeed/kernel/base/basictypes.h"
#include "pagespeed/kernel/base/google_message_handler.h"
#include "pagespeed/kernel/base/gtest.h"
#include "pagespeed/kernel/base/scoped_ptr.h"
#include "pagespeed/kernel/base/string.h"
#include "pagespeed/kernel/base/string_util.h"
#include "pagespeed/kernel/base/thread_system.h"
#include "pagespeed/kernel/http/http_names.h"
#include "pagespeed/kernel/http/request_headers.h"
#include "pagespeed/kernel/http/response_headers.h"
#include "pagespeed/kernel/util/platform.h"
namespace {
const char kClearData[] = "Hello";
// This was generated with 'xxd -i hello.gz' after gzipping a file with "Hello".
const unsigned char kGzippedData[] = {
0x1f, 0x8b, 0x08, 0x08, 0x3b, 0x3a, 0xf3, 0x4e, 0x00, 0x03, 0x68, 0x65,
0x6c, 0x6c, 0x6f, 0x00, 0xf3, 0x48, 0xcd, 0xc9, 0xc9, 0x07, 0x00, 0x82,
0x89, 0xd1, 0xf7, 0x05, 0x00, 0x00, 0x00
};
bool binary_data_same(const void* left, size_t left_len,
const void* right, size_t right_len) {
return left_len == right_len && memcmp(left, right, left_len) == 0;
}
} // namespace
namespace net_instaweb {
class MockFetch : public StringAsyncFetch {
public:
explicit MockFetch(const RequestContextPtr& ctx) : StringAsyncFetch(ctx) {}
virtual ~MockFetch() {}
void ExpectAcceptEncoding(const StringPiece& encoding) {
encoding.CopyToString(&accept_encoding_);
}
virtual void HandleHeadersComplete() {
if (!accept_encoding_.empty()) {
EXPECT_TRUE(request_headers()->HasValue(
HttpAttributes::kAcceptEncoding, accept_encoding_));
}
StringAsyncFetch::HandleHeadersComplete();
}
private:
// If non-empty, EXPECT that each request must accept this encoding.
GoogleString accept_encoding_;
DISALLOW_COPY_AND_ASSIGN(MockFetch);
};
class InflatingFetchTest : public testing::Test {
protected:
InflatingFetchTest()
: inflating_fetch_(NULL),
gzipped_data_(reinterpret_cast<const char*>(kGzippedData),
STATIC_STRLEN(kGzippedData)),
thread_system_(Platform::CreateThreadSystem()) {
}
virtual void SetUp() {
mock_fetch_.reset(new MockFetch(
RequestContext::NewTestRequestContext(thread_system_.get())));
inflating_fetch_ = new InflatingFetch(mock_fetch_.get());
}
scoped_ptr<MockFetch> mock_fetch_;
// Self-deletes in Done(), so no need to deallocate.
InflatingFetch* inflating_fetch_;
GoogleMessageHandler message_handler_;
StringPiece gzipped_data_;
scoped_ptr<ThreadSystem> thread_system_;
};
// Tests that if we ask for clear text & get it, we pass through the data
// unchanged.
TEST_F(InflatingFetchTest, ClearRequestResponse) {
inflating_fetch_->response_headers()->SetStatusAndReason(HttpStatus::kOK);
inflating_fetch_->Write(kClearData, &message_handler_);
inflating_fetch_->Done(true);
EXPECT_EQ(kClearData, mock_fetch_->buffer());
EXPECT_TRUE(mock_fetch_->done());
EXPECT_TRUE(mock_fetch_->success());
}
// Tests that if we ask for clear text, and get a response that claims to
// be gzipped but is actually garbage, our mock callback gets HandleDone(false)
// called, despite the fact that the fetcher (mocked by this code below) called
// Done(true).
TEST_F(InflatingFetchTest, AutoInflateGarbage) {
inflating_fetch_->response_headers()->Add(
HttpAttributes::kContentEncoding, HttpAttributes::kGzip);
inflating_fetch_->response_headers()->SetStatusAndReason(HttpStatus::kOK);
inflating_fetch_->Write("this garbage won't inflate", &message_handler_);
inflating_fetch_->Done(true);
EXPECT_TRUE(mock_fetch_->done());
EXPECT_FALSE(mock_fetch_->success());
}
// Tests that if we ask for clear text but get a properly compressed buffer,
// that our inflating-fetch will make this transparent to our Expect callback.
TEST_F(InflatingFetchTest, AutoInflate) {
inflating_fetch_->response_headers()->Add(
HttpAttributes::kContentEncoding, HttpAttributes::kGzip);
inflating_fetch_->response_headers()->SetStatusAndReason(HttpStatus::kOK);
inflating_fetch_->Write(gzipped_data_, &message_handler_);
inflating_fetch_->Done(true);
EXPECT_EQ(kClearData, mock_fetch_->buffer())
<< "data should be auto-inflated.";
EXPECT_TRUE(mock_fetch_->response_headers()->Lookup1(
HttpAttributes::kContentEncoding) == NULL)
<< "Content encoding should be stripped since we inflated the data.";
EXPECT_TRUE(mock_fetch_->done());
EXPECT_TRUE(mock_fetch_->success());
}
// Tests that if we asked for a gzipped response in the first place that
// we don't inflate or strip the content-encoding header.
TEST_F(InflatingFetchTest, ExpectGzipped) {
inflating_fetch_->request_headers()->Add(
HttpAttributes::kAcceptEncoding, HttpAttributes::kGzip);
inflating_fetch_->response_headers()->Add(
HttpAttributes::kContentEncoding, HttpAttributes::kGzip);
inflating_fetch_->response_headers()->SetStatusAndReason(HttpStatus::kOK);
inflating_fetch_->Write(gzipped_data_, &message_handler_);
inflating_fetch_->Done(true);
EXPECT_TRUE(
binary_data_same(
gzipped_data_.data(), gzipped_data_.length(),
mock_fetch_->buffer().data(), mock_fetch_->buffer().size()))
<< "data should be untouched.";
EXPECT_STREQ(HttpAttributes::kGzip, mock_fetch_->response_headers()->Lookup1(
HttpAttributes::kContentEncoding)) << "content-encoding not stripped.";
EXPECT_TRUE(mock_fetch_->done());
EXPECT_TRUE(mock_fetch_->success());
}
// Check that empty blacklist is processed correctly and everything is inflated.
// The blacklist feature has been removed since after this test was written,
// but behavior should be unchanged.
TEST_F(InflatingFetchTest, ExpectUnGzippedOnEmptyBlacklist) {
inflating_fetch_->response_headers()->Add(
HttpAttributes::kContentEncoding, HttpAttributes::kGzip);
// We need to set Content-Type to one of the octet-streams types.
inflating_fetch_->response_headers()->Add(HttpAttributes::kContentType,
"binary/octet-stream");
inflating_fetch_->response_headers()->SetStatusAndReason(HttpStatus::kOK);
inflating_fetch_->Write(gzipped_data_, &message_handler_);
inflating_fetch_->Done(true);
EXPECT_STREQ(kClearData, mock_fetch_->buffer())
<< "data should be uncompressed when blacklist filter is empty.";
EXPECT_FALSE(
mock_fetch_->response_headers()->Has(HttpAttributes::kContentEncoding))
<< "content-encoding is not stripped.";
EXPECT_TRUE(mock_fetch_->done());
EXPECT_TRUE(mock_fetch_->success());
// Check some other type.
mock_fetch_->Reset();
inflating_fetch_ = new InflatingFetch(mock_fetch_.get());
inflating_fetch_->response_headers()->Add(
HttpAttributes::kContentEncoding, HttpAttributes::kGzip);
inflating_fetch_->response_headers()->Add(HttpAttributes::kContentType,
"image/gif");
inflating_fetch_->response_headers()->SetStatusAndReason(HttpStatus::kOK);
inflating_fetch_->Write(gzipped_data_, &message_handler_);
inflating_fetch_->Done(true);
EXPECT_STREQ(kClearData, mock_fetch_->buffer())
<< "data should be inflated when content-type is not in blacklist.";
EXPECT_FALSE(
mock_fetch_->response_headers()->Has(HttpAttributes::kContentEncoding))
<< "content-encoding is not stripped.";
EXPECT_TRUE(mock_fetch_->done());
EXPECT_TRUE(mock_fetch_->success());
}
TEST_F(InflatingFetchTest, ContentGzipAndDeflatedButWantClear) {
inflating_fetch_->response_headers()->Add(
HttpAttributes::kContentEncoding, HttpAttributes::kDeflate);
// Apply gzip second so that it gets decoded first as we want to decode
// in reverse order to how the encoding was done.
inflating_fetch_->response_headers()->Add(
HttpAttributes::kContentEncoding, HttpAttributes::kGzip);
inflating_fetch_->response_headers()->SetStatusAndReason(HttpStatus::kOK);
inflating_fetch_->Write(gzipped_data_, &message_handler_);
inflating_fetch_->Done(true);
EXPECT_EQ(kClearData, mock_fetch_->buffer())
<< "data should be auto-unzipped but deflate is not attempted.";
EXPECT_STREQ(HttpAttributes::kDeflate,
mock_fetch_->response_headers()->Lookup1(
HttpAttributes::kContentEncoding))
<< "deflate encoding remains though gzip encoding is stripped.";
EXPECT_TRUE(mock_fetch_->done());
EXPECT_TRUE(mock_fetch_->success());
}
// Tests that content that was first gzipped, and then encoded with
// some encoder ("frob") unknown to our system does not get touched.
// We should not attempt to gunzip the 'frob' data.
TEST_F(InflatingFetchTest, GzippedAndFrobbedNotChanged) {
inflating_fetch_->response_headers()->Add(
HttpAttributes::kContentEncoding, HttpAttributes::kGzip);
inflating_fetch_->response_headers()->Add(
HttpAttributes::kContentEncoding, "frob");
inflating_fetch_->response_headers()->SetStatusAndReason(HttpStatus::kOK);
inflating_fetch_->Write(gzipped_data_, &message_handler_);
inflating_fetch_->Done(true);
EXPECT_EQ(gzipped_data_, mock_fetch_->buffer())
<< "data should be not be altered (even though it happens to be gzipped)";
ConstStringStarVector encodings;
ASSERT_TRUE(mock_fetch_->response_headers()->Lookup(
HttpAttributes::kContentEncoding, &encodings))
<< "deflate encoding remains though gzip encoding is stripped.";
ASSERT_EQ(2, encodings.size());
EXPECT_STREQ(HttpAttributes::kGzip, *encodings[0]);
EXPECT_STREQ("frob", *encodings[1]);
}
TEST_F(InflatingFetchTest, TestEnableGzipFromBackend) {
mock_fetch_->ExpectAcceptEncoding(HttpAttributes::kGzip);
inflating_fetch_->EnableGzipFromBackend();
inflating_fetch_->response_headers()->Add(
HttpAttributes::kContentEncoding, HttpAttributes::kGzip);
inflating_fetch_->response_headers()->SetStatusAndReason(HttpStatus::kOK);
inflating_fetch_->Write(gzipped_data_, &message_handler_);
inflating_fetch_->Done(true);
EXPECT_EQ(kClearData, mock_fetch_->buffer())
<< "data should be auto-inflated.";
EXPECT_TRUE(mock_fetch_->response_headers()->Lookup1(
HttpAttributes::kContentEncoding) == NULL)
<< "Content encoding should be stripped since we inflated the data.";
EXPECT_TRUE(mock_fetch_->done());
EXPECT_TRUE(mock_fetch_->success());
}
TEST_F(InflatingFetchTest, TestEnableGzipFromBackendWithCleartext) {
mock_fetch_->ExpectAcceptEncoding(HttpAttributes::kGzip);
inflating_fetch_->EnableGzipFromBackend();
// We are going to ask the mock server for gzip, but we'll get cleartext.
inflating_fetch_->response_headers()->SetStatusAndReason(HttpStatus::kOK);
inflating_fetch_->Write(kClearData, &message_handler_);
inflating_fetch_->Done(true);
EXPECT_EQ(kClearData, mock_fetch_->buffer());
EXPECT_TRUE(mock_fetch_->done());
EXPECT_TRUE(mock_fetch_->success());
}
TEST_F(InflatingFetchTest, TestEnableGzipFromBackendExpectingGzip) {
inflating_fetch_->request_headers()->Add(
HttpAttributes::kAcceptEncoding, HttpAttributes::kGzip);
inflating_fetch_->response_headers()->Add(
HttpAttributes::kContentEncoding, HttpAttributes::kGzip);
// Calling EnableGzipFromBackend here has no effect in this case,
// because above we declare that we want to see gzipped data coming
// into our Write methods.
inflating_fetch_->EnableGzipFromBackend();
mock_fetch_->ExpectAcceptEncoding(HttpAttributes::kGzip);
inflating_fetch_->response_headers()->SetStatusAndReason(HttpStatus::kOK);
inflating_fetch_->Write(gzipped_data_, &message_handler_);
inflating_fetch_->Done(true);
EXPECT_TRUE(
binary_data_same(
gzipped_data_.data(), gzipped_data_.length(),
mock_fetch_->buffer().data(), mock_fetch_->buffer().size()))
<< "data should be untouched.";
EXPECT_STREQ(HttpAttributes::kGzip, mock_fetch_->response_headers()->Lookup1(
HttpAttributes::kContentEncoding)) << "content-encoding not stripped.";
EXPECT_TRUE(mock_fetch_->done());
EXPECT_TRUE(mock_fetch_->success());
}
TEST(StaticInflatingFetchTest, CompressUncompressValue) {
static const char kHello[] = "hello";
GoogleMessageHandler handler;
HTTPValue value;
value.Write(kHello, &handler);
ResponseHeaders headers;
headers.Add(HttpAttributes::kContentType, "text/html");
value.SetHeaders(&headers);
HTTPValue compressed_value;
EXPECT_TRUE(InflatingFetch::GzipValue(9, value, &compressed_value, &headers,
&handler));
StringPiece contents;
compressed_value.ExtractContents(&contents);
// Extract the compressed version, it shouldn't be the same as the initial
// text.
EXPECT_NE(kHello, contents);
EXPECT_STREQ(HttpAttributes::kGzip,
headers.Lookup1(HttpAttributes::kContentEncoding));
compressed_value.ExtractHeaders(&headers, &handler);
HTTPValue uncompressed_value;
ResponseHeaders temp_headers;
temp_headers.Add("a", "b");
uncompressed_value.SetHeaders(&temp_headers);
ASSERT_TRUE(InflatingFetch::UnGzipValueIfCompressed(
compressed_value, &headers, &uncompressed_value, &handler));
uncompressed_value.ExtractContents(&contents);
// We've unzipped the compressed value, it should now say "hello".
EXPECT_EQ(kHello, contents);
}
// TODO(jmarantz): test 'deflate' without gzip
} // namespace net_instaweb