blob: 6c93b3d27fc12b2fa308959bdbce186a7e714b73 [file] [log] [blame]
/*
* Copyright 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.
*/
// Author: abliss@google.com (Adam Bliss)
// Unit-test the server context
#include "net/instaweb/rewriter/public/server_context.h"
#include <cstddef> // for size_t
#include "base/logging.h"
#include "net/instaweb/http/public/async_fetch.h"
#include "net/instaweb/http/public/counting_url_async_fetcher.h"
#include "net/instaweb/http/public/http_cache.h"
#include "net/instaweb/http/public/http_cache_failure.h"
#include "net/instaweb/http/public/http_value.h"
#include "net/instaweb/http/public/mock_url_fetcher.h"
#include "net/instaweb/rewriter/public/beacon_critical_images_finder.h"
#include "net/instaweb/rewriter/public/critical_finder_support_util.h"
#include "net/instaweb/rewriter/public/critical_images_finder.h"
#include "net/instaweb/rewriter/public/critical_selector_finder.h"
#include "net/instaweb/rewriter/public/css_outline_filter.h"
#include "net/instaweb/rewriter/public/domain_lawyer.h"
#include "net/instaweb/rewriter/public/file_load_policy.h"
#include "net/instaweb/rewriter/public/mock_resource_callback.h"
#include "net/instaweb/rewriter/public/output_resource.h"
#include "net/instaweb/rewriter/public/output_resource_kind.h"
#include "net/instaweb/rewriter/public/resource.h"
#include "net/instaweb/rewriter/public/resource_namer.h"
#include "net/instaweb/rewriter/public/rewrite_driver.h"
#include "net/instaweb/rewriter/public/rewrite_filter.h"
#include "net/instaweb/rewriter/public/rewrite_options.h"
#include "net/instaweb/rewriter/public/rewrite_query.h"
#include "net/instaweb/rewriter/public/rewrite_test_base.h"
#include "net/instaweb/rewriter/public/test_rewrite_driver_factory.h"
#include "net/instaweb/rewriter/rendered_image.pb.h"
#include "net/instaweb/util/public/mock_property_page.h"
#include "net/instaweb/util/public/property_cache.h"
#include "pagespeed/kernel/base/basictypes.h"
#include "pagespeed/kernel/base/gtest.h"
#include "pagespeed/kernel/base/mock_message_handler.h"
#include "pagespeed/kernel/base/mock_timer.h"
#include "pagespeed/kernel/base/ref_counted_ptr.h"
#include "pagespeed/kernel/base/scoped_ptr.h"
#include "pagespeed/kernel/base/statistics.h"
#include "pagespeed/kernel/base/string.h"
#include "pagespeed/kernel/base/string_hash.h"
#include "pagespeed/kernel/base/string_util.h"
#include "pagespeed/kernel/base/timer.h"
#include "pagespeed/kernel/cache/lru_cache.h"
#include "pagespeed/kernel/html/html_element.h"
#include "pagespeed/kernel/html/html_parse_test_base.h"
#include "pagespeed/kernel/http/content_type.h"
#include "pagespeed/kernel/http/google_url.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/http/user_agent_matcher.h"
#include "pagespeed/kernel/http/user_agent_matcher_test_base.h"
#include "pagespeed/kernel/util/url_escaper.h"
namespace {
const char kResourceUrl[] = "http://example.com/image.png";
const char kResourceUrlBase[] = "http://example.com";
const char kResourceUrlPath[] = "/image.png";
const char kOptionsHash[] = "1234";
const char kUrlPrefix[] = "http://www.example.com/";
const size_t kUrlPrefixLength = STATIC_STRLEN(kUrlPrefix);
} // namespace
namespace net_instaweb {
class VerifyContentsCallback : public Resource::AsyncCallback {
public:
VerifyContentsCallback(const ResourcePtr& resource,
const GoogleString& contents) :
Resource::AsyncCallback(resource),
contents_(contents),
called_(false) {
}
VerifyContentsCallback(const OutputResourcePtr& resource,
const GoogleString& contents) :
Resource::AsyncCallback(ResourcePtr(resource)),
contents_(contents),
called_(false) {
}
virtual void Done(bool lock_failure, bool resource_ok) {
EXPECT_FALSE(lock_failure);
EXPECT_STREQ(contents_, resource()->ExtractUncompressedContents());
called_ = true;
}
void AssertCalled() {
EXPECT_TRUE(called_);
}
GoogleString contents_;
bool called_;
};
class ServerContextTest : public RewriteTestBase {
protected:
// Fetches data (which is expected to exist) for given resource,
// but making sure to go through the path that checks for its
// non-existence and potentially doing locking, too.
// Note: resource must have hash set.
bool FetchExtantOutputResourceHelper(const OutputResourcePtr& resource,
StringAsyncFetch* async_fetch) {
async_fetch->set_response_headers(resource->response_headers());
RewriteFilter* null_filter = NULL; // We want to test the cache only.
EXPECT_TRUE(rewrite_driver()->FetchOutputResource(resource, null_filter,
async_fetch));
rewrite_driver()->WaitForCompletion();
EXPECT_TRUE(async_fetch->done());
return async_fetch->success();
}
// Helper for testing of FetchOutputResource. Assumes that output_resource
// is to be handled by the filter with 2-letter code filter_id, and
// verifies result to match expect_success and expect_content.
void TestFetchOutputResource(const OutputResourcePtr& output_resource,
const char* filter_id,
bool expect_success,
StringPiece expect_content) {
ASSERT_TRUE(output_resource.get());
RewriteFilter* filter = rewrite_driver()->FindFilter(filter_id);
ASSERT_TRUE(filter != NULL);
StringAsyncFetch fetch_result(CreateRequestContext());
EXPECT_TRUE(rewrite_driver()->FetchOutputResource(
output_resource, filter, &fetch_result));
rewrite_driver()->WaitForCompletion();
EXPECT_TRUE(fetch_result.done());
EXPECT_EQ(expect_success, fetch_result.success());
EXPECT_EQ(expect_content, fetch_result.buffer());
}
GoogleString GetOutputResource(const OutputResourcePtr& resource) {
StringAsyncFetch fetch(RequestContext::NewTestRequestContext(
server_context()->thread_system()));
EXPECT_TRUE(FetchExtantOutputResourceHelper(resource, &fetch));
return fetch.buffer();
}
// Returns whether there was an existing copy of data for the resource.
// If not, makes sure the resource is wrapped.
bool TryFetchExtantOutputResource(const OutputResourcePtr& resource) {
StringAsyncFetch dummy_fetch(CreateRequestContext());
return FetchExtantOutputResourceHelper(resource, &dummy_fetch);
}
// Asserts that the given url starts with an appropriate prefix;
// then cuts off that prefix.
virtual void RemoveUrlPrefix(const GoogleString& prefix, GoogleString* url) {
EXPECT_TRUE(StringPiece(*url).starts_with(prefix));
url->erase(0, prefix.length());
}
OutputResourcePtr CreateOutputResourceForFetch(const StringPiece& url) {
RewriteFilter* dummy;
rewrite_driver()->SetBaseUrlForFetch(url);
GoogleUrl gurl(url);
return rewrite_driver()->DecodeOutputResource(gurl, &dummy);
}
ResourcePtr CreateInputResourceAndReadIfCached(const StringPiece& url) {
rewrite_driver()->SetBaseUrlForFetch(url);
GoogleUrl resource_url(url);
bool unused;
ResourcePtr resource(rewrite_driver()->CreateInputResource(resource_url,
&unused));
if ((resource.get() != NULL) && !ReadIfCached(resource)) {
resource.clear();
}
return resource;
}
// Tests for the lifecycle and various flows of a named output resource.
void TestNamed() {
const char* filter_prefix = RewriteOptions::kCssFilterId;
const char* name = "I.name"; // valid name for CSS filter.
const char* contents = "contents";
GoogleString failure_reason;
OutputResourcePtr output(
rewrite_driver()->CreateOutputResourceWithPath(
kUrlPrefix, filter_prefix, name, kRewrittenResource,
&failure_reason));
ASSERT_TRUE(output.get() != NULL);
EXPECT_EQ("", failure_reason);
// Check name_key against url_prefix/fp.name
GoogleString name_key = output->name_key();
RemoveUrlPrefix(kUrlPrefix, &name_key);
EXPECT_EQ(output->full_name().EncodeIdName(), name_key);
// Make sure the resource hasn't already been created. We do need to give it
// a hash for fetching to do anything.
output->SetHash("42");
EXPECT_FALSE(TryFetchExtantOutputResource(output));
EXPECT_FALSE(output->IsWritten());
{
// Check that a non-blocking attempt to create another resource
// with the same name returns quickly. We don't need a hash in this
// case since we're just trying to create the resource, not fetch it.
OutputResourcePtr output1(
rewrite_driver()->CreateOutputResourceWithPath(
kUrlPrefix, filter_prefix, name, kRewrittenResource,
&failure_reason));
ASSERT_TRUE(output1.get() != NULL);
EXPECT_EQ("", failure_reason);
EXPECT_FALSE(output1->IsWritten());
}
{
// Here we attempt to create the object with the hash and fetch it.
// The fetch fails as there is no active filter to resolve it.
ResourceNamer namer;
namer.CopyFrom(output->full_name());
namer.set_hash("0");
namer.set_ext("txt");
GoogleString name = StrCat(kUrlPrefix, namer.Encode());
OutputResourcePtr output1(CreateOutputResourceForFetch(name));
ASSERT_TRUE(output1.get() != NULL);
// blocking but stealing
EXPECT_FALSE(TryFetchExtantOutputResource(output1));
}
// Write some data
ASSERT_TRUE(output->has_hash());
EXPECT_EQ(kRewrittenResource, output->kind());
EXPECT_TRUE(rewrite_driver()->Write(
ResourceVector(), contents, &kContentTypeText, "utf-8", output.get()));
EXPECT_TRUE(output->IsWritten());
// Check that hash and ext are correct.
EXPECT_EQ("0", output->hash());
EXPECT_EQ("txt", output->extension());
EXPECT_STREQ("utf-8", output->charset());
// With the URL (which contains the hash), we can retrieve it
// from the http_cache.
OutputResourcePtr output4(CreateOutputResourceForFetch(output->url()));
EXPECT_EQ(output->url(), output4->url());
EXPECT_EQ(contents, GetOutputResource(output4));
}
bool ResourceIsCached() {
ResourcePtr resource(CreateResource(kResourceUrlBase, kResourceUrlPath));
return ReadIfCached(resource);
}
void StartRead() {
ResourcePtr resource(CreateResource(kResourceUrlBase, kResourceUrlPath));
InitiateResourceRead(resource);
}
GoogleString MakeEvilUrl(const StringPiece& host, const StringPiece& name) {
GoogleString escaped_abs;
UrlEscaper::EncodeToUrlSegment(name, &escaped_abs);
// Do not use Encode, which will make the URL non-evil.
// TODO(matterbury): Rewrite this for a non-standard UrlNamer?
return StrCat("http://", host, "/dir/123/", escaped_abs,
".pagespeed.jm.0.js");
}
// Accessor for ServerContext field; also cleans up
// deferred_release_rewrite_drivers_.
void EnableRewriteDriverCleanupMode(bool s) {
server_context()->trying_to_cleanup_rewrite_drivers_ = s;
server_context()->deferred_release_rewrite_drivers_.clear();
}
// Creates a response with given ttl and extra cache control under given URL.
void SetCustomCachingResponse(const StringPiece& url,
int ttl_ms,
const StringPiece& extra_cache_control) {
ResponseHeaders response_headers;
DefaultResponseHeaders(kContentTypeCss, ttl_ms, &response_headers);
response_headers.SetDateAndCaching(
http_cache()->timer()->NowMs(), ttl_ms * Timer::kSecondMs,
extra_cache_control);
response_headers.ComputeCaching();
SetFetchResponse(AbsolutifyUrl(url), response_headers, "payload");
}
// Creates a resource with given ttl and extra cache control under given URL.
ResourcePtr CreateCustomCachingResource(
const StringPiece& url, int ttl_ms,
const StringPiece& extra_cache_control) {
SetCustomCachingResponse(url, ttl_ms, extra_cache_control);
GoogleUrl gurl(AbsolutifyUrl(url));
rewrite_driver()->SetBaseUrlForFetch(kTestDomain);
bool unused;
ResourcePtr resource(rewrite_driver()->CreateInputResource(gurl, &unused));
VerifyContentsCallback callback(resource, "payload");
resource->LoadAsync(Resource::kLoadEvenIfNotCacheable,
rewrite_driver()->request_context(),
&callback);
callback.AssertCalled();
return resource;
}
void RefererTest(const RequestHeaders* headers,
bool is_background_fetch) {
GoogleString url = "test.jpg";
rewrite_driver()->SetBaseUrlForFetch(kTestDomain);
SetCustomCachingResponse(url, 100, "foo");
GoogleUrl gurl(AbsolutifyUrl(url));
bool unused;
ResourcePtr resource(rewrite_driver()->CreateInputResource(gurl, &unused));
if (!is_background_fetch) {
rewrite_driver()->SetRequestHeaders(*headers);
}
resource->set_is_background_fetch(is_background_fetch);
VerifyContentsCallback callback(resource, "payload");
resource->LoadAsync(Resource::kLoadEvenIfNotCacheable,
rewrite_driver()->request_context(),
&callback);
callback.AssertCalled();
}
void DefaultHeaders(ResponseHeaders* headers) {
SetDefaultLongCacheHeaders(&kContentTypeCss, headers);
}
const RewriteDriver* decoding_driver() {
return server_context()->decoding_driver_;
}
RewriteOptions* GetCustomOptions(const StringPiece& url,
RequestHeaders* request_headers,
RewriteOptions* domain_options) {
// The default url_namer does not yield any name-derived options, and we
// have not specified any URL params or request-headers, so there will be
// no custom options, and no errors.
GoogleUrl gurl(url);
RewriteOptions* copy_options = domain_options != NULL ?
domain_options->Clone() : NULL;
RewriteQuery rewrite_query;
RequestContextPtr null_request_context;
EXPECT_TRUE(server_context()->GetQueryOptions(
null_request_context, NULL, &gurl, request_headers, NULL,
&rewrite_query));
RewriteOptions* options =
server_context()->GetCustomOptions(request_headers, copy_options,
rewrite_query.ReleaseOptions());
return options;
}
void CheckExtendCache(RewriteOptions* options, bool x) {
EXPECT_EQ(x, options->Enabled(RewriteOptions::kExtendCacheCss));
EXPECT_EQ(x, options->Enabled(RewriteOptions::kExtendCacheImages));
EXPECT_EQ(x, options->Enabled(RewriteOptions::kExtendCacheScripts));
}
};
TEST_F(ServerContextTest, CustomOptionsWithNoUrlNamerOptions) {
// The default url_namer does not yield any name-derived options, and we
// have not specified any URL params or request-headers, so there will be
// no custom options, and no errors.
RequestHeaders request_headers;
scoped_ptr<RewriteOptions> options(
GetCustomOptions("http://example.com/", &request_headers, NULL));
ASSERT_TRUE(options.get() == NULL);
// Now put a query-param in, just turning on PageSpeed. The core filters
// should be enabled.
options.reset(GetCustomOptions(
"http://example.com/?PageSpeed=on",
&request_headers, NULL));
ASSERT_TRUE(options.get() != NULL);
EXPECT_TRUE(options->enabled());
CheckExtendCache(options.get(), true);
EXPECT_TRUE(options->Enabled(RewriteOptions::kCombineCss));
EXPECT_FALSE(options->Enabled(RewriteOptions::kDeferJavascript));
// Now explicitly enable a filter, which should disable others.
options.reset(GetCustomOptions(
"http://example.com/?PageSpeedFilters=extend_cache",
&request_headers, NULL));
ASSERT_TRUE(options.get() != NULL);
CheckExtendCache(options.get(), true);
EXPECT_FALSE(options->Enabled(RewriteOptions::kCombineCss));
EXPECT_FALSE(options->Enabled(RewriteOptions::kDeferJavascript));
// Now put a request-header in, turning off pagespeed. request-headers get
// priority over query-params.
request_headers.Add("PageSpeed", "off");
options.reset(GetCustomOptions(
"http://example.com/?PageSpeed=on",
&request_headers, NULL));
ASSERT_TRUE(options.get() != NULL);
EXPECT_FALSE(options->enabled());
// Now explicitly enable a bogus filter, which should will cause the
// options to be uncomputable.
GoogleUrl gurl("http://example.com/?PageSpeedFilters=bogus_filter");
RewriteQuery rewrite_query;
RequestContextPtr null_request_context;
EXPECT_FALSE(server_context()->GetQueryOptions(
null_request_context, options.get(), &gurl, &request_headers, NULL,
&rewrite_query));
// The default url_namer does not yield any name-derived options, and we
// have not specified any URL params or request-headers, and kXRequestedWith
// header is set with bogus value, so there will be no custom options, and no
// errors.
request_headers.Add(
HttpAttributes::kXRequestedWith, "bogus");
options.reset(
GetCustomOptions("http://example.com/", &request_headers, NULL));
ASSERT_TRUE(options.get() == NULL);
// The default url_namer does not yield any name-derived options, and we
// have not specified any URL params or request-headers, but kXRequestedWith
// header is set to 'XmlHttpRequest', so there will be custom options with
// all js inserting filters disabled.
request_headers.RemoveAll(HttpAttributes::kXRequestedWith);
request_headers.Add(
HttpAttributes::kXRequestedWith, HttpAttributes::kXmlHttpRequest);
options.reset(
GetCustomOptions("http://example.com/", &request_headers, NULL));
// Disable DelayImages for XmlHttpRequests.
ASSERT_TRUE(options.get() != NULL);
EXPECT_TRUE(options->enabled());
EXPECT_FALSE(options->Enabled(RewriteOptions::kDelayImages));
// As kDelayImages filter is present in the disabled list, so it will not get
// enabled even if it is enabled via EnableFilter().
options->EnableFilter(RewriteOptions::kDelayImages);
EXPECT_FALSE(options->Enabled(RewriteOptions::kDelayImages));
options->EnableFilter(RewriteOptions::kCachePartialHtml);
EXPECT_FALSE(options->Enabled(RewriteOptions::kCachePartialHtml));
options->EnableFilter(RewriteOptions::kDeferIframe);
EXPECT_FALSE(options->Enabled(RewriteOptions::kDeferIframe));
options->EnableFilter(RewriteOptions::kDeferJavascript);
EXPECT_FALSE(options->Enabled(RewriteOptions::kDeferJavascript));
options->EnableFilter(RewriteOptions::kFlushSubresources);
EXPECT_FALSE(options->Enabled(RewriteOptions::kFlushSubresources));
options->EnableFilter(RewriteOptions::kLazyloadImages);
EXPECT_FALSE(options->Enabled(RewriteOptions::kLazyloadImages));
options->EnableFilter(RewriteOptions::kLocalStorageCache);
EXPECT_FALSE(options->Enabled(RewriteOptions::kLocalStorageCache));
options->EnableFilter(RewriteOptions::kSplitHtml);
EXPECT_FALSE(options->Enabled(RewriteOptions::kSplitHtml));
options->EnableFilter(RewriteOptions::kPrioritizeCriticalCss);
EXPECT_FALSE(options->Enabled(RewriteOptions::kPrioritizeCriticalCss));
}
TEST_F(ServerContextTest, CustomOptionsWithUrlNamerOptions) {
// Inject a url-namer that will establish a domain configuration.
RewriteOptions namer_options(factory()->thread_system());
namer_options.EnableFilter(RewriteOptions::kCombineJavascript);
namer_options.EnableFilter(RewriteOptions::kDelayImages);
RequestHeaders request_headers;
scoped_ptr<RewriteOptions> options(
GetCustomOptions("http://example.com/", &request_headers,
&namer_options));
// Even with no query-params or request-headers, we get the custom
// options as domain options provided as argument.
ASSERT_TRUE(options.get() != NULL);
EXPECT_TRUE(options->enabled());
CheckExtendCache(options.get(), false);
EXPECT_FALSE(options->Enabled(RewriteOptions::kCombineCss));
EXPECT_TRUE(options->Enabled(RewriteOptions::kCombineJavascript));
EXPECT_TRUE(options->Enabled(RewriteOptions::kDelayImages));
// Now combine with query params, which turns core-filters on.
options.reset(GetCustomOptions(
"http://example.com/?PageSpeed=on",
&request_headers, &namer_options));
ASSERT_TRUE(options.get() != NULL);
EXPECT_TRUE(options->enabled());
CheckExtendCache(options.get(), true);
EXPECT_TRUE(options->Enabled(RewriteOptions::kCombineCss));
EXPECT_TRUE(options->Enabled(RewriteOptions::kCombineJavascript));
// Explicitly enable a filter in query-params, which will turn off
// the core filters that have not been explicitly enabled. Note
// that explicit filter-setting in query-params overrides completely
// the options provided as a parameter.
options.reset(GetCustomOptions(
"http://example.com/?PageSpeedFilters=combine_css",
&request_headers, &namer_options));
ASSERT_TRUE(options.get() != NULL);
EXPECT_TRUE(options->enabled());
CheckExtendCache(options.get(), false);
EXPECT_TRUE(options->Enabled(RewriteOptions::kCombineCss));
EXPECT_FALSE(options->Enabled(RewriteOptions::kCombineJavascript));
// Now explicitly enable a bogus filter, which should will cause the
// options to be uncomputable.
GoogleUrl gurl("http://example.com/?PageSpeedFilters=bogus_filter");
RewriteQuery rewrite_query;
RequestContextPtr null_request_context;
EXPECT_FALSE(server_context()->GetQueryOptions(
null_request_context, options.get(), &gurl, &request_headers, NULL,
&rewrite_query));
request_headers.Add(
HttpAttributes::kXRequestedWith, "bogus");
options.reset(
GetCustomOptions("http://example.com/", &request_headers,
&namer_options));
// Don't disable DelayImages for Non-XmlHttpRequests.
ASSERT_TRUE(options.get() != NULL);
EXPECT_TRUE(options->enabled());
CheckExtendCache(options.get(), false);
EXPECT_FALSE(options->Enabled(RewriteOptions::kCombineCss));
EXPECT_TRUE(options->Enabled(RewriteOptions::kCombineJavascript));
EXPECT_TRUE(options->Enabled(RewriteOptions::kDelayImages));
request_headers.RemoveAll(HttpAttributes::kXRequestedWith);
request_headers.Add(
HttpAttributes::kXRequestedWith, HttpAttributes::kXmlHttpRequest);
options.reset(
GetCustomOptions("http://example.com/", &request_headers,
&namer_options));
// Disable DelayImages for XmlHttpRequests.
ASSERT_TRUE(options.get() != NULL);
EXPECT_TRUE(options->enabled());
CheckExtendCache(options.get(), false);
EXPECT_FALSE(options->Enabled(RewriteOptions::kCombineCss));
EXPECT_TRUE(options->Enabled(RewriteOptions::kCombineJavascript));
EXPECT_FALSE(options->Enabled(RewriteOptions::kDelayImages));
}
TEST_F(ServerContextTest, TestNamed) {
TestNamed();
}
TEST_F(ServerContextTest, TestOutputInputUrl) {
options()->EnableFilter(RewriteOptions::kRewriteJavascriptExternal);
rewrite_driver()->AddFilters();
GoogleString url = Encode("http://example.com/dir/123/",
RewriteOptions::kJavascriptMinId,
"0", "orig", "js");
SetResponseWithDefaultHeaders(
"http://example.com/dir/123/orig", kContentTypeJavascript,
"foo() /*comment */;", 100);
OutputResourcePtr output_resource(CreateOutputResourceForFetch(url));
TestFetchOutputResource(output_resource, RewriteOptions::kJavascriptMinId,
true, "foo();");
}
TEST_F(ServerContextTest, TestOutputInputUrlEvil) {
options()->EnableFilter(RewriteOptions::kRewriteJavascriptExternal);
rewrite_driver()->AddFilters();
GoogleString url = MakeEvilUrl("example.com", "http://www.evil.com");
SetResponseWithDefaultHeaders(
"http://www.evil.com/", kContentTypeJavascript,
"foo() /*comment */;", 100);
OutputResourcePtr output_resource(CreateOutputResourceForFetch(url));
TestFetchOutputResource(output_resource, RewriteOptions::kJavascriptMinId,
false, "");
}
TEST_F(ServerContextTest, TestOutputInputUrlBusy) {
EXPECT_TRUE(options()->WriteableDomainLawyer()->AddOriginDomainMapping(
"www.busy.com", "example.com", "", message_handler()));
options()->EnableFilter(RewriteOptions::kRewriteJavascriptExternal);
rewrite_driver()->AddFilters();
GoogleString url = MakeEvilUrl("example.com", "http://www.busy.com");
SetResponseWithDefaultHeaders(
"http://www.busy.com/", kContentTypeJavascript,
"foo() /*comment */;", 100);
OutputResourcePtr output_resource(CreateOutputResourceForFetch(url));
TestFetchOutputResource(output_resource, RewriteOptions::kJavascriptMinId,
false, "");
}
// Check that we can origin-map a domain referenced from an HTML file
// to 'localhost', but rewrite-map it to 'cdn.com'. This was not working
// earlier because RewriteDriver::CreateInputResource was mapping to the
// rewrite domain, preventing us from finding the origin-mapping when
// fetching the URL.
TEST_F(ServerContextTest, TestMapRewriteAndOrigin) {
ASSERT_TRUE(options()->WriteableDomainLawyer()->AddOriginDomainMapping(
"localhost", kTestDomain, "", message_handler()));
EXPECT_TRUE(options()->WriteableDomainLawyer()->AddRewriteDomainMapping(
"cdn.com", kTestDomain, message_handler()));
ResourcePtr input(CreateResource(StrCat(kTestDomain, "index.html"),
"style.css"));
ASSERT_TRUE(input.get() != NULL);
EXPECT_EQ(StrCat(kTestDomain, "style.css"), input->url());
// The absolute input URL is in test.com, but we will only be
// able to serve it from localhost, per the origin mapping above.
static const char kStyleContent[] = "style content";
const int kOriginTtlSec = 300;
SetResponseWithDefaultHeaders("http://localhost/style.css", kContentTypeCss,
kStyleContent, kOriginTtlSec);
EXPECT_TRUE(ReadIfCached(input));
// When we rewrite the resource as an ouptut, it will show up in the
// CDN per the rewrite mapping.
GoogleString failure_reason;
OutputResourcePtr output(
rewrite_driver()->CreateOutputResourceFromResource(
RewriteOptions::kCacheExtenderId, rewrite_driver()->default_encoder(),
NULL, input, kRewrittenResource,
&failure_reason));
ASSERT_TRUE(output.get() != NULL);
EXPECT_EQ("", failure_reason);
// We need to 'Write' an output resource before we can determine its
// URL.
rewrite_driver()->Write(
ResourceVector(), StringPiece(kStyleContent), &kContentTypeCss,
StringPiece(), output.get());
EXPECT_EQ(Encode("http://cdn.com/", "ce", "0", "style.css", "css"),
output->url());
}
TEST_F(ServerContextTest, ScanSplitHtmlRequestSplitEnabled) {
options()->EnableFilter(RewriteOptions::kSplitHtml);
RequestContextPtr ctx(CreateRequestContext());
GoogleString url("http://test.com/?x_split=btf");
EXPECT_EQ(RequestContext::SPLIT_FULL, ctx->split_request_type());
EXPECT_TRUE(server_context()->ScanSplitHtmlRequest(ctx, options(), &url));
EXPECT_EQ(RequestContext::SPLIT_BELOW_THE_FOLD, ctx->split_request_type());
EXPECT_EQ("http://test.com/", url);
url = "http://test.com/?a=b&x_split=btf";
ctx.reset(CreateRequestContext());
EXPECT_EQ(RequestContext::SPLIT_FULL, ctx->split_request_type());
EXPECT_TRUE(server_context()->ScanSplitHtmlRequest(ctx, options(), &url));
EXPECT_EQ(RequestContext::SPLIT_BELOW_THE_FOLD, ctx->split_request_type());
EXPECT_EQ("http://test.com/?a=b", url);
url = "http://test.com/?a=b&x_split=atf";
ctx.reset(CreateRequestContext());
EXPECT_EQ(RequestContext::SPLIT_FULL, ctx->split_request_type());
EXPECT_TRUE(server_context()->ScanSplitHtmlRequest(ctx, options(), &url));
EXPECT_EQ(RequestContext::SPLIT_ABOVE_THE_FOLD, ctx->split_request_type());
EXPECT_EQ("http://test.com/?a=b", url);
url = "http://test.com/?a=b&x_split=junk";
ctx.reset(CreateRequestContext());
EXPECT_EQ(RequestContext::SPLIT_FULL, ctx->split_request_type());
EXPECT_TRUE(server_context()->ScanSplitHtmlRequest(ctx, options(), &url));
EXPECT_EQ(RequestContext::SPLIT_FULL, ctx->split_request_type());
EXPECT_EQ("http://test.com/?a=b", url);
ctx.reset(CreateRequestContext());
EXPECT_EQ(RequestContext::SPLIT_FULL, ctx->split_request_type());
EXPECT_FALSE(server_context()->ScanSplitHtmlRequest(ctx, options(), &url));
EXPECT_EQ(RequestContext::SPLIT_FULL, ctx->split_request_type());
EXPECT_EQ("http://test.com/?a=b", url);
}
TEST_F(ServerContextTest, ScanSplitHtmlRequestOptionsNull) {
RequestContextPtr ctx(CreateRequestContext());
GoogleString url("http://test.com/?x_split=btf");
EXPECT_EQ(RequestContext::SPLIT_FULL, ctx->split_request_type());
EXPECT_FALSE(server_context()->ScanSplitHtmlRequest(ctx, NULL, &url));
EXPECT_EQ(RequestContext::SPLIT_FULL, ctx->split_request_type());
EXPECT_EQ("http://test.com/?x_split=btf", url);
}
TEST_F(ServerContextTest, ScanSplitHtmlRequestSplitDisabled) {
options()->DisableFilter(RewriteOptions::kSplitHtml);
RequestContextPtr ctx(CreateRequestContext());
GoogleString url("http://test.com/?x_split=btf");
EXPECT_EQ(RequestContext::SPLIT_FULL, ctx->split_request_type());
EXPECT_FALSE(server_context()->ScanSplitHtmlRequest(ctx, options(), &url));
EXPECT_EQ(RequestContext::SPLIT_FULL, ctx->split_request_type());
EXPECT_EQ("http://test.com/?x_split=btf", url);
}
class MockRewriteFilter : public RewriteFilter {
public:
explicit MockRewriteFilter(RewriteDriver* driver)
: RewriteFilter(driver) {}
virtual ~MockRewriteFilter() {}
virtual const char* id() const { return "mk"; }
virtual const char* Name() const { return "mock_filter"; }
virtual void StartDocumentImpl() {}
virtual void StartElementImpl(HtmlElement* element) {}
virtual void EndElementImpl(HtmlElement* element) {}
private:
DISALLOW_COPY_AND_ASSIGN(MockRewriteFilter);
};
class CreateMockRewriterCallback
: public TestRewriteDriverFactory::CreateRewriterCallback {
public:
CreateMockRewriterCallback() {}
virtual ~CreateMockRewriterCallback() {}
virtual RewriteFilter* Done(RewriteDriver* driver) {
return new MockRewriteFilter(driver);
}
private:
DISALLOW_COPY_AND_ASSIGN(CreateMockRewriterCallback);
};
class MockPlatformConfigCallback
: public TestRewriteDriverFactory::PlatformSpecificConfigurationCallback {
public:
explicit MockPlatformConfigCallback(RewriteDriver** result_ptr)
: result_ptr_(result_ptr) {
}
virtual void Done(RewriteDriver* driver) {
*result_ptr_ = driver;
}
private:
RewriteDriver** result_ptr_;
DISALLOW_COPY_AND_ASSIGN(MockPlatformConfigCallback);
};
// Tests that platform-specific configuration hook runs for various
// factory methods.
TEST_F(ServerContextTest, TestPlatformSpecificConfiguration) {
RewriteDriver* rec_normal_driver = NULL;
RewriteDriver* rec_custom_driver = NULL;
MockPlatformConfigCallback normal_callback(&rec_normal_driver);
MockPlatformConfigCallback custom_callback(&rec_custom_driver);
factory()->AddPlatformSpecificConfigurationCallback(&normal_callback);
RewriteDriver* normal_driver = server_context()->NewRewriteDriver(
RequestContext::NewTestRequestContext(server_context()->thread_system()));
EXPECT_EQ(normal_driver, rec_normal_driver);
factory()->ClearPlatformSpecificConfigurationCallback();
normal_driver->Cleanup();
factory()->AddPlatformSpecificConfigurationCallback(&custom_callback);
RewriteDriver* custom_driver =
server_context()->NewCustomRewriteDriver(
new RewriteOptions(factory()->thread_system()),
RequestContext::NewTestRequestContext(
server_context()->thread_system()));
EXPECT_EQ(custom_driver, rec_custom_driver);
custom_driver->Cleanup();
}
// Tests that platform-specific rewriters are used for decoding fetches.
TEST_F(ServerContextTest, TestPlatformSpecificRewritersDecoding) {
GoogleString url = Encode("http://example.com/dir/123/",
"mk", "0", "orig", "js");
GoogleUrl gurl(url);
RewriteFilter* dummy;
// Without the mock rewriter enabled, this URL should not be decoded.
OutputResourcePtr bad_output(
decoding_driver()->DecodeOutputResource(gurl, &dummy));
ASSERT_TRUE(bad_output.get() == NULL);
// With the mock rewriter enabled, this URL should be decoded.
CreateMockRewriterCallback callback;
factory()->AddCreateRewriterCallback(&callback);
factory()->set_add_platform_specific_decoding_passes(true);
factory()->RebuildDecodingDriverForTests(server_context());
// TODO(sligocki): Do we still want to expose decoding_driver() for
// platform-specific rewriters? Or should we just use IsPagespeedResource()
// in these tests?
OutputResourcePtr good_output(
decoding_driver()->DecodeOutputResource(gurl, &dummy));
ASSERT_TRUE(good_output.get() != NULL);
EXPECT_EQ(url, good_output->url());
}
// Tests that platform-specific rewriters are used for decoding fetches even
// if they are only added in AddPlatformSpecificRewritePasses, not
// AddPlatformSpecificDecodingPasses. Required for backwards compatibility.
TEST_F(ServerContextTest, TestPlatformSpecificRewritersImplicitDecoding) {
GoogleString url = Encode("http://example.com/dir/123/",
"mk", "0", "orig", "js");
GoogleUrl gurl(url);
RewriteFilter* dummy;
// The URL should be decoded even if AddPlatformSpecificDecodingPasses is
// suppressed.
CreateMockRewriterCallback callback;
factory()->AddCreateRewriterCallback(&callback);
factory()->set_add_platform_specific_decoding_passes(false);
factory()->RebuildDecodingDriverForTests(server_context());
OutputResourcePtr good_output(
decoding_driver()->DecodeOutputResource(gurl, &dummy));
ASSERT_TRUE(good_output.get() != NULL);
EXPECT_EQ(url, good_output->url());
}
// DecodeOutputResource should drop query
TEST_F(ServerContextTest, TestOutputResourceFetchQuery) {
GoogleString url = Encode("http://example.com/dir/123/",
"jm", "0", "orig", "js");
RewriteFilter* dummy;
GoogleUrl gurl(StrCat(url, "?query"));
OutputResourcePtr output_resource(
rewrite_driver()->DecodeOutputResource(gurl, &dummy));
ASSERT_TRUE(output_resource.get() != NULL);
EXPECT_EQ(url, output_resource->url());
}
// Input resources and corresponding output resources should keep queries
TEST_F(ServerContextTest, TestInputResourceQuery) {
const char kUrl[] = "test?param";
ResourcePtr resource(CreateResource(kResourceUrlBase, kUrl));
ASSERT_TRUE(resource.get() != NULL);
EXPECT_EQ(StrCat(GoogleString(kResourceUrlBase), "/", kUrl), resource->url());
GoogleString failure_reason;
OutputResourcePtr output(rewrite_driver()->CreateOutputResourceFromResource(
"sf", rewrite_driver()->default_encoder(), NULL, resource,
kRewrittenResource, &failure_reason));
ASSERT_TRUE(output.get() != NULL);
EXPECT_EQ("", failure_reason);
GoogleString included_name;
EXPECT_TRUE(UrlEscaper::DecodeFromUrlSegment(output->name(), &included_name));
EXPECT_EQ(GoogleString(kUrl), included_name);
}
TEST_F(ServerContextTest, TestRemember404) {
// Make sure our resources remember that a page 404'd, for limited time.
http_cache()->set_failure_caching_ttl_sec(
kFetchStatusUncacheableError, 10000);
http_cache()->set_failure_caching_ttl_sec(kFetchStatus4xxError, 100);
ResponseHeaders not_found;
SetDefaultLongCacheHeaders(&kContentTypeHtml, &not_found);
not_found.SetStatusAndReason(HttpStatus::kNotFound);
SetFetchResponse("http://example.com/404", not_found, "");
ResourcePtr resource(
CreateInputResourceAndReadIfCached("http://example.com/404"));
EXPECT_EQ(NULL, resource.get());
HTTPValue value_out;
ResponseHeaders headers_out;
EXPECT_EQ(
HTTPCache::FindResult(HTTPCache::kRecentFailure, kFetchStatus4xxError),
HttpBlockingFind(
"http://example.com/404", http_cache(), &value_out, &headers_out));
AdvanceTimeMs(150 * Timer::kSecondMs);
EXPECT_EQ(kNotFoundResult, HttpBlockingFind(
"http://example.com/404", http_cache(), &value_out, &headers_out));
}
TEST_F(ServerContextTest, TestRememberDropped) {
// Fake resource being dropped by adding the appropriate header to the
// resource proper.
ResponseHeaders not_found;
SetDefaultLongCacheHeaders(&kContentTypeHtml, &not_found);
not_found.SetStatusAndReason(HttpStatus::kNotFound);
not_found.Add(HttpAttributes::kXPsaLoadShed, "1");
SetFetchResponse("http://example.com/404", not_found, "");
ResourcePtr resource(
CreateInputResourceAndReadIfCached("http://example.com/404"));
EXPECT_EQ(NULL, resource.get());
HTTPValue value_out;
ResponseHeaders headers_out;
EXPECT_EQ(
HTTPCache::FindResult(HTTPCache::kRecentFailure, kFetchStatusDropped),
HttpBlockingFind(
"http://example.com/404", http_cache(), &value_out, &headers_out));
AdvanceTimeMs(11 * Timer::kSecondMs);
EXPECT_EQ(kNotFoundResult, HttpBlockingFind(
"http://example.com/404", http_cache(), &value_out, &headers_out));
}
TEST_F(ServerContextTest, TestNonCacheable) {
const char kContents[] = "ok";
// Make sure that when we get non-cacheable resources
// we mark the fetch as not cacheable in the cache.
ResponseHeaders no_cache;
SetDefaultLongCacheHeaders(&kContentTypeHtml, &no_cache);
no_cache.Replace(HttpAttributes::kCacheControl, "no-cache");
no_cache.ComputeCaching();
SetFetchResponse("http://example.com/", no_cache, kContents);
ResourcePtr resource(CreateResource("http://example.com/", "/"));
ASSERT_TRUE(resource.get() != NULL);
VerifyContentsCallback callback(resource, kContents);
resource->LoadAsync(Resource::kReportFailureIfNotCacheable,
rewrite_driver()->request_context(),
&callback);
callback.AssertCalled();
HTTPValue value_out;
ResponseHeaders headers_out;
EXPECT_EQ(
HTTPCache::FindResult(HTTPCache::kRecentFailure,
kFetchStatusUncacheable200),
HttpBlockingFind(
"http://example.com/", http_cache(), &value_out, &headers_out));
}
TEST_F(ServerContextTest, TestNonCacheableReadResultPolicy) {
// Make sure we report the success/failure for non-cacheable resources
// depending on the policy. (TestNonCacheable also covers the value).
ResponseHeaders no_cache;
SetDefaultLongCacheHeaders(&kContentTypeHtml, &no_cache);
no_cache.Replace(HttpAttributes::kCacheControl, "no-cache");
no_cache.ComputeCaching();
SetFetchResponse("http://example.com/", no_cache, "stuff");
ResourcePtr resource1(CreateResource("http://example.com/", "/"));
ASSERT_TRUE(resource1.get() != NULL);
MockResourceCallback callback1(resource1, factory()->thread_system());
resource1->LoadAsync(Resource::kReportFailureIfNotCacheable,
rewrite_driver()->request_context(),
&callback1);
EXPECT_TRUE(callback1.done());
EXPECT_FALSE(callback1.success());
ResourcePtr resource2(CreateResource("http://example.com/", "/"));
ASSERT_TRUE(resource2.get() != NULL);
MockResourceCallback callback2(resource2, factory()->thread_system());
resource2->LoadAsync(Resource::kLoadEvenIfNotCacheable,
rewrite_driver()->request_context(),
&callback2);
EXPECT_TRUE(callback2.done());
EXPECT_TRUE(callback2.success());
}
TEST_F(ServerContextTest, TestRememberEmpty) {
// Make sure our resources remember that a page is empty, for limited time.
http_cache()->set_failure_caching_ttl_sec(kFetchStatusEmpty, 100);
ResponseHeaders headers;
SetDefaultLongCacheHeaders(&kContentTypeHtml, &headers);
headers.SetStatusAndReason(HttpStatus::kOK);
static const char kUrl[] = "http://example.com/empty.html";
SetFetchResponse(kUrl, headers, "");
ResourcePtr resource(CreateInputResourceAndReadIfCached(kUrl));
EXPECT_EQ(NULL, resource.get());
HTTPValue value_out;
ResponseHeaders headers_out;
EXPECT_EQ(HTTPCache::FindResult(HTTPCache::kRecentFailure,
kFetchStatusEmpty),
HttpBlockingFind(kUrl, http_cache(), &value_out, &headers_out));
AdvanceTimeMs(150 * Timer::kSecondMs);
EXPECT_EQ(kNotFoundResult,
HttpBlockingFind(kUrl, http_cache(), &value_out, &headers_out));
}
TEST_F(ServerContextTest, TestNotRememberEmptyRedirect) {
// Parallel to TestRememberEmpty for empty 301 redirect.
http_cache()->set_failure_caching_ttl_sec(kFetchStatusEmpty, 100);
ResponseHeaders headers;
SetDefaultLongCacheHeaders(&kContentTypeHtml, &headers);
headers.SetStatusAndReason(HttpStatus::kMovedPermanently);
headers.Add(HttpAttributes::kLocation, "http://example.com/destination.html");
static const char kUrl[] = "http://example.com/redirect.html";
SetFetchResponse(kUrl, headers, "");
ResourcePtr resource(CreateInputResourceAndReadIfCached(kUrl));
EXPECT_EQ(NULL, resource.get());
HTTPValue value_out;
ResponseHeaders headers_out;
// Currently we are remembering 301 as not cacheable, but in the future if
// that changes the important thing here is that we don't remember non-200
// as empty (and thus fail to use them.
EXPECT_NE(HTTPCache::FindResult(HTTPCache::kRecentFailure,
kFetchStatusEmpty),
HttpBlockingFind(kUrl, http_cache(), &value_out, &headers_out));
AdvanceTimeMs(150 * Timer::kSecondMs);
EXPECT_NE(HTTPCache::FindResult(HTTPCache::kRecentFailure,
kFetchStatusEmpty),
HttpBlockingFind(kUrl, http_cache(), &value_out, &headers_out));
}
TEST_F(ServerContextTest, TestVaryOption) {
// Make sure that when we get non-cacheable resources
// we mark the fetch as not-cacheable in the cache.
options()->set_respect_vary(true);
ResponseHeaders no_cache;
const char kContents[] = "ok";
SetDefaultLongCacheHeaders(&kContentTypeHtml, &no_cache);
no_cache.Add(HttpAttributes::kVary, HttpAttributes::kAcceptEncoding);
no_cache.Add(HttpAttributes::kVary, HttpAttributes::kUserAgent);
no_cache.ComputeCaching();
SetFetchResponse("http://example.com/", no_cache, kContents);
ResourcePtr resource(CreateResource("http://example.com/", "/"));
ASSERT_TRUE(resource.get() != NULL);
VerifyContentsCallback callback(resource, kContents);
resource->LoadAsync(Resource::kReportFailureIfNotCacheable,
rewrite_driver()->request_context(),
&callback);
callback.AssertCalled();
EXPECT_FALSE(resource->IsValidAndCacheable());
HTTPValue valueOut;
ResponseHeaders headersOut;
EXPECT_EQ(
HTTPCache::FindResult(HTTPCache::kRecentFailure,
kFetchStatusUncacheable200),
HttpBlockingFind(
"http://example.com/", http_cache(), &valueOut, &headersOut));
}
TEST_F(ServerContextTest, TestOutlined) {
// Outliner resources should not produce extra cache traffic
// due to rname/ entries we can't use anyway.
EXPECT_EQ(0, lru_cache()->num_hits());
EXPECT_EQ(0, lru_cache()->num_misses());
EXPECT_EQ(0, lru_cache()->num_inserts());
EXPECT_EQ(0, lru_cache()->num_identical_reinserts());
GoogleString failure_reason;
OutputResourcePtr output_resource(
rewrite_driver()->CreateOutputResourceWithPath(
kUrlPrefix, CssOutlineFilter::kFilterId, "_", kOutlinedResource,
&failure_reason));
ASSERT_TRUE(output_resource.get() != NULL);
EXPECT_EQ("", failure_reason);
EXPECT_EQ(NULL, output_resource->cached_result());
EXPECT_EQ(0, lru_cache()->num_hits());
EXPECT_EQ(0, lru_cache()->num_misses());
EXPECT_EQ(0, lru_cache()->num_inserts());
EXPECT_EQ(0, lru_cache()->num_identical_reinserts());
rewrite_driver()->Write(
ResourceVector(), "foo", &kContentTypeCss, StringPiece(),
output_resource.get());
EXPECT_EQ(NULL, output_resource->cached_result());
EXPECT_EQ(0, lru_cache()->num_hits());
EXPECT_EQ(0, lru_cache()->num_misses());
EXPECT_EQ(1, lru_cache()->num_inserts());
EXPECT_EQ(0, lru_cache()->num_identical_reinserts());
// Now try fetching again. It should not get a cached_result either.
output_resource.reset(
rewrite_driver()->CreateOutputResourceWithPath(
kUrlPrefix, CssOutlineFilter::kFilterId, "_", kOutlinedResource,
&failure_reason));
ASSERT_TRUE(output_resource.get() != NULL);
EXPECT_EQ("", failure_reason);
EXPECT_EQ(NULL, output_resource->cached_result());
EXPECT_EQ(0, lru_cache()->num_hits());
EXPECT_EQ(0, lru_cache()->num_misses());
EXPECT_EQ(1, lru_cache()->num_inserts());
EXPECT_EQ(0, lru_cache()->num_identical_reinserts());
}
TEST_F(ServerContextTest, TestOnTheFly) {
// Test to make sure that an on-fly insert does not insert the data,
// just the rname/
// For derived resources we can and should use the rewrite
// summary/metadata cache
EXPECT_EQ(0, lru_cache()->num_hits());
EXPECT_EQ(0, lru_cache()->num_misses());
EXPECT_EQ(0, lru_cache()->num_inserts());
EXPECT_EQ(0, lru_cache()->num_identical_reinserts());
GoogleString failure_reason;
OutputResourcePtr output_resource(
rewrite_driver()->CreateOutputResourceWithPath(
kUrlPrefix, RewriteOptions::kCssFilterId, "_", kOnTheFlyResource,
&failure_reason));
ASSERT_TRUE(output_resource.get() != NULL);
EXPECT_EQ("", failure_reason);
EXPECT_EQ(NULL, output_resource->cached_result());
EXPECT_EQ(0, lru_cache()->num_hits());
EXPECT_EQ(0, lru_cache()->num_misses());
EXPECT_EQ(0, lru_cache()->num_inserts());
EXPECT_EQ(0, lru_cache()->num_identical_reinserts());
rewrite_driver()->Write(
ResourceVector(), "foo", &kContentTypeCss, StringPiece(),
output_resource.get());
EXPECT_TRUE(output_resource->cached_result() != NULL);
EXPECT_EQ(0, lru_cache()->num_hits());
EXPECT_EQ(0, lru_cache()->num_misses());
EXPECT_EQ(0, lru_cache()->num_inserts());
EXPECT_EQ(0, lru_cache()->num_identical_reinserts());
}
TEST_F(ServerContextTest, TestNotGenerated) {
// For derived resources we can and should use the rewrite
// summary/metadata cache
EXPECT_EQ(0, lru_cache()->num_hits());
EXPECT_EQ(0, lru_cache()->num_misses());
EXPECT_EQ(0, lru_cache()->num_inserts());
EXPECT_EQ(0, lru_cache()->num_identical_reinserts());
GoogleString failure_reason;
OutputResourcePtr output_resource(
rewrite_driver()->CreateOutputResourceWithPath(
kUrlPrefix, RewriteOptions::kCssFilterId, "_", kRewrittenResource,
&failure_reason));
ASSERT_TRUE(output_resource.get() != NULL);
EXPECT_EQ("", failure_reason);
EXPECT_EQ(NULL, output_resource->cached_result());
EXPECT_EQ(0, lru_cache()->num_hits());
EXPECT_EQ(0, lru_cache()->num_misses());
EXPECT_EQ(0, lru_cache()->num_inserts());
EXPECT_EQ(0, lru_cache()->num_identical_reinserts());
rewrite_driver()->Write(
ResourceVector(), "foo", &kContentTypeCss, StringPiece(),
output_resource.get());
EXPECT_TRUE(output_resource->cached_result() != NULL);
EXPECT_EQ(0, lru_cache()->num_hits());
EXPECT_EQ(0, lru_cache()->num_misses());
EXPECT_EQ(1, lru_cache()->num_inserts());
EXPECT_EQ(0, lru_cache()->num_identical_reinserts());
}
TEST_F(ServerContextTest, TestHandleBeaconNoLoadParam) {
EXPECT_FALSE(server_context()->HandleBeacon(
"", UserAgentMatcherTestBase::kChromeUserAgent,
CreateRequestContext()));
}
TEST_F(ServerContextTest, TestHandleBeaconInvalidLoadParam) {
EXPECT_FALSE(server_context()->HandleBeacon(
"ets=asd", UserAgentMatcherTestBase::kChromeUserAgent,
CreateRequestContext()));
}
TEST_F(ServerContextTest, TestHandleBeaconNoUrl) {
EXPECT_FALSE(server_context()->HandleBeacon(
"ets=load:34", UserAgentMatcherTestBase::kChromeUserAgent,
CreateRequestContext()));
}
TEST_F(ServerContextTest, TestHandleBeaconInvalidUrl) {
EXPECT_FALSE(server_context()->HandleBeacon(
"url=%2f%2finvalidurl&ets=load:34",
UserAgentMatcherTestBase::kChromeUserAgent, CreateRequestContext()));
}
TEST_F(ServerContextTest, TestHandleBeaconMissingValue) {
EXPECT_FALSE(server_context()->HandleBeacon(
"url=http%3A%2F%2Flocalhost%3A8080%2Findex.html&ets=load:",
UserAgentMatcherTestBase::kChromeUserAgent, CreateRequestContext()));
}
TEST_F(ServerContextTest, TestHandleBeacon) {
EXPECT_TRUE(server_context()->HandleBeacon(
"url=http%3A%2F%2Flocalhost%3A8080%2Findex.html&ets=load:34",
UserAgentMatcherTestBase::kChromeUserAgent, CreateRequestContext()));
}
class BeaconTest : public ServerContextTest {
protected:
BeaconTest() : property_cache_(NULL) { }
virtual ~BeaconTest() { }
virtual void SetUp() {
ServerContextTest::SetUp();
property_cache_ = server_context()->page_property_cache();
property_cache_->set_enabled(true);
const PropertyCache::Cohort* beacon_cohort =
SetupCohort(property_cache_, RewriteDriver::kBeaconCohort);
server_context()->set_beacon_cohort(beacon_cohort);
server_context()->set_critical_images_finder(
new BeaconCriticalImagesFinder(
beacon_cohort, factory()->nonce_generator(), statistics()));
server_context()->set_critical_selector_finder(
new BeaconCriticalSelectorFinder(beacon_cohort,
factory()->nonce_generator(),
statistics()));
ResetDriver();
candidates_.insert("#foo");
candidates_.insert(".bar");
candidates_.insert("img");
}
void ResetDriver() {
rewrite_driver()->Clear();
SetDriverRequestHeaders();
}
MockPropertyPage* MockPageForUA(StringPiece user_agent) {
UserAgentMatcher::DeviceType device_type =
server_context()->user_agent_matcher()->GetDeviceTypeForUA(
user_agent);
MockPropertyPage* page = NewMockPage(
kUrlPrefix, kOptionsHash, device_type);
property_cache_->Read(page);
return page;
}
void InsertCssBeacon(StringPiece user_agent) {
// Simulate effects on pcache of CSS beacon insertion.
rewrite_driver()->set_property_page(MockPageForUA(user_agent));
factory()->mock_timer()->AdvanceMs(
options()->beacon_reinstrument_time_sec() * Timer::kSecondMs);
last_beacon_metadata_ =
server_context()->critical_selector_finder()->
PrepareForBeaconInsertion(candidates_, rewrite_driver());
ASSERT_EQ(kBeaconWithNonce, last_beacon_metadata_.status);
ASSERT_FALSE(last_beacon_metadata_.nonce.empty());
rewrite_driver()->property_page()->WriteCohort(
server_context()->beacon_cohort());
}
void InsertImageBeacon(StringPiece user_agent) {
// Simulate effects on pcache of image beacon insertion.
rewrite_driver()->set_property_page(MockPageForUA(user_agent));
// Some of the critical image tests send enough beacons with the same set of
// images that we can go into low frequency beaconing mode, so advance time
// by the low frequency rebeacon interval.
factory()->mock_timer()->AdvanceMs(
options()->beacon_reinstrument_time_sec() * Timer::kSecondMs *
kLowFreqBeaconMult);
last_beacon_metadata_ =
server_context()->critical_images_finder()->
PrepareForBeaconInsertion(rewrite_driver());
ASSERT_EQ(kBeaconWithNonce, last_beacon_metadata_.status);
ASSERT_FALSE(last_beacon_metadata_.nonce.empty());
rewrite_driver()->property_page()->WriteCohort(
server_context()->beacon_cohort());
}
// Send a beacon through ServerContext::HandleBeacon and verify that the
// property cache entries for critical images, critical selectors and rendered
// dimensions of images were updated correctly.
void TestBeacon(const StringSet* critical_image_hashes,
const StringSet* critical_css_selectors,
const GoogleString* rendered_images_json_map,
StringPiece user_agent) {
ASSERT_EQ(kBeaconWithNonce, last_beacon_metadata_.status)
<< "Remember to insert a beacon!";
// Setup the beacon_url and pass to HandleBeacon.
GoogleString beacon_url = StrCat(
"url=http%3A%2F%2Fwww.example.com"
"&oh=", kOptionsHash, "&n=", last_beacon_metadata_.nonce);
if (critical_image_hashes != NULL) {
StrAppend(&beacon_url, "&ci=");
AppendJoinCollection(&beacon_url, *critical_image_hashes, ",");
}
if (critical_css_selectors != NULL) {
StrAppend(&beacon_url, "&cs=");
AppendJoinCollection(&beacon_url, *critical_css_selectors, ",");
}
if (rendered_images_json_map != NULL) {
StrAppend(&beacon_url, "&rd=", *rendered_images_json_map);
}
EXPECT_TRUE(server_context()->HandleBeacon(
beacon_url, user_agent, CreateRequestContext()));
// Read the property cache value for critical images, and verify that it has
// the expected value.
ResetDriver();
scoped_ptr<MockPropertyPage> page(MockPageForUA(user_agent));
rewrite_driver()->set_property_page(page.release());
if (critical_image_hashes != NULL) {
critical_html_images_ = server_context()->critical_images_finder()->
GetHtmlCriticalImages(rewrite_driver());
}
if (critical_css_selectors != NULL) {
critical_css_selectors_ = server_context()->critical_selector_finder()->
GetCriticalSelectors(rewrite_driver());
}
if (rendered_images_json_map != NULL) {
rendered_images_.reset(server_context()->critical_images_finder()->
ExtractRenderedImageDimensionsFromCache(
rewrite_driver()));
}
}
PropertyCache* property_cache_;
// These fields hold data deserialized from the pcache after TestBeacon.
StringSet critical_html_images_;
StringSet critical_css_selectors_;
// This field holds the data deserialized from pcache after a BeaconTest call.
scoped_ptr<RenderedImages> rendered_images_;
// This field holds candidate critical css selectors.
StringSet candidates_;
BeaconMetadata last_beacon_metadata_;
};
TEST_F(BeaconTest, BasicPcacheSetup) {
const PropertyCache::Cohort* cohort = property_cache_->GetCohort(
RewriteDriver::kBeaconCohort);
UserAgentMatcher::DeviceType device_type =
server_context()->user_agent_matcher()->GetDeviceTypeForUA(
UserAgentMatcherTestBase::kChromeUserAgent);
scoped_ptr<MockPropertyPage> page(
NewMockPage(kUrlPrefix, kOptionsHash, device_type));
property_cache_->Read(page.get());
PropertyValue* property = page->GetProperty(cohort, "critical_images");
EXPECT_FALSE(property->has_value());
}
TEST_F(BeaconTest, HandleBeaconRenderedDimensionsofImages) {
GoogleString img1 = "http://www.example.com/img1.png";
GoogleString hash1 = IntegerToString(
HashString<CasePreserve, int>(img1.c_str(), img1.size()));
options()->EnableFilter(RewriteOptions::kResizeToRenderedImageDimensions);
RenderedImages rendered_images;
RenderedImages_Image* images = rendered_images.add_image();
images->set_src(hash1);
images->set_rendered_width(40);
images->set_rendered_height(50);
GoogleString json_map_rendered_dimensions = StrCat(
"{\"", hash1, "\":{\"rw\":40,", "\"rh\":50,\"ow\":160,\"oh\":200}}");
InsertImageBeacon(UserAgentMatcherTestBase::kChromeUserAgent);
TestBeacon(NULL, NULL, &json_map_rendered_dimensions,
UserAgentMatcherTestBase::kChromeUserAgent);
ASSERT_FALSE(rendered_images_.get() == NULL);
EXPECT_EQ(1, rendered_images_->image_size());
EXPECT_STREQ(hash1, rendered_images_->image(0).src());
EXPECT_EQ(40, rendered_images_->image(0).rendered_width());
EXPECT_EQ(50, rendered_images_->image(0).rendered_height());
}
TEST_F(BeaconTest, HandleBeaconCritImages) {
GoogleString img1 = "http://www.example.com/img1.png";
GoogleString img2 = "http://www.example.com/img2.png";
GoogleString hash1 = IntegerToString(
HashString<CasePreserve, int>(img1.c_str(), img1.size()));
GoogleString hash2 = IntegerToString(
HashString<CasePreserve, int>(img2.c_str(), img2.size()));
StringSet critical_image_hashes;
critical_image_hashes.insert(hash1);
InsertImageBeacon(UserAgentMatcherTestBase::kChromeUserAgent);
TestBeacon(&critical_image_hashes, NULL, NULL,
UserAgentMatcherTestBase::kChromeUserAgent);
EXPECT_STREQ(hash1, JoinCollection(critical_html_images_, ","));
// Beacon both images as critical. Since we require 80% support, img2 won't
// show as critical until we've beaconed four times. It doesn't require five
// beacon results because we weight recent beacon values more heavily and
// beacon support decays over time.
critical_image_hashes.insert(hash2);
for (int i = 0; i < 3; ++i) {
InsertImageBeacon(UserAgentMatcherTestBase::kChromeUserAgent);
TestBeacon(&critical_image_hashes, NULL, NULL,
UserAgentMatcherTestBase::kChromeUserAgent);
EXPECT_STREQ(hash1, JoinCollection(critical_html_images_, ","));
}
GoogleString expected = StrCat(hash1, ",", hash2);
InsertImageBeacon(UserAgentMatcherTestBase::kChromeUserAgent);
TestBeacon(&critical_image_hashes, NULL, NULL,
UserAgentMatcherTestBase::kChromeUserAgent);
EXPECT_STREQ(expected, JoinCollection(critical_html_images_, ","));
// Test with a different user agent, providing support only for img1.
critical_image_hashes.clear();
critical_image_hashes.insert(hash1);
InsertImageBeacon(UserAgentMatcherTestBase::kIPhoneUserAgent);
TestBeacon(&critical_image_hashes, NULL, NULL,
UserAgentMatcherTestBase::kIPhoneUserAgent);
EXPECT_STREQ(hash1, JoinCollection(critical_html_images_, ","));
// Beacon once more with the original user agent and with only img1; img2
// loses 80% support again.
InsertImageBeacon(UserAgentMatcherTestBase::kChromeUserAgent);
TestBeacon(&critical_image_hashes, NULL, NULL,
UserAgentMatcherTestBase::kChromeUserAgent);
EXPECT_STREQ(hash1, JoinCollection(critical_html_images_, ","));
}
TEST_F(BeaconTest, HandleBeaconCriticalCss) {
InsertCssBeacon(UserAgentMatcherTestBase::kChromeUserAgent);
StringSet critical_css_selector;
critical_css_selector.insert("%23foo");
critical_css_selector.insert(".bar");
critical_css_selector.insert("%23noncandidate");
TestBeacon(NULL, &critical_css_selector, NULL,
UserAgentMatcherTestBase::kChromeUserAgent);
EXPECT_STREQ("#foo,.bar",
JoinCollection(critical_css_selectors_, ","));
// Send another beacon response, and make sure we are storing a history of
// responses.
InsertCssBeacon(UserAgentMatcherTestBase::kChromeUserAgent);
critical_css_selector.clear();
critical_css_selector.insert(".bar");
critical_css_selector.insert("img");
critical_css_selector.insert("%23noncandidate");
TestBeacon(NULL, &critical_css_selector, NULL,
UserAgentMatcherTestBase::kChromeUserAgent);
EXPECT_STREQ("#foo,.bar,img",
JoinCollection(critical_css_selectors_, ","));
}
TEST_F(BeaconTest, EmptyCriticalCss) {
InsertCssBeacon(UserAgentMatcherTestBase::kChromeUserAgent);
StringSet empty_critical_selectors;
TestBeacon(NULL, &empty_critical_selectors, NULL,
UserAgentMatcherTestBase::kChromeUserAgent);
EXPECT_TRUE(critical_css_selectors_.empty());
}
class ResourceFreshenTest : public ServerContextTest {
protected:
virtual void SetUp() {
ServerContextTest::SetUp();
HTTPCache::InitStats(statistics());
expirations_ = statistics()->GetVariable(HTTPCache::kCacheExpirations);
CHECK(expirations_ != NULL);
SetDefaultLongCacheHeaders(&kContentTypePng, &response_headers_);
response_headers_.SetStatusAndReason(HttpStatus::kOK);
response_headers_.RemoveAll(HttpAttributes::kCacheControl);
response_headers_.RemoveAll(HttpAttributes::kExpires);
}
Variable* expirations_;
ResponseHeaders response_headers_;
};
// Many resources expire in 5 minutes, because that is our default for
// when caching headers are not present. This test ensures that iff
// we ask for the resource when there's just a minute left, we proactively
// fetch it rather than allowing it to expire.
TEST_F(ResourceFreshenTest, TestFreshenImminentlyExpiringResources) {
SetupWaitFetcher();
FetcherUpdateDateHeaders();
// Make sure we don't try to insert non-cacheable resources
// into the cache wastefully, but still fetch them well.
int max_age_sec =
RewriteOptions::kDefaultImplicitCacheTtlMs / Timer::kSecondMs;
response_headers_.Add(HttpAttributes::kCacheControl,
StringPrintf("max-age=%d", max_age_sec));
SetFetchResponse(kResourceUrl, response_headers_, "foo");
// The test here is not that the ReadIfCached will succeed, because
// it's a fake url fetcher.
StartRead();
CallFetcherCallbacks();
EXPECT_TRUE(ResourceIsCached());
// Now let the time expire with no intervening fetches to freshen the cache.
// This is because we do not proactively initiate refreshes for all resources;
// only the ones that are actually asked for on a regular basis. So a
// completely inactive site will not see its resources freshened.
AdvanceTimeMs((max_age_sec + 1) * Timer::kSecondMs);
expirations_->Clear();
StartRead();
EXPECT_EQ(1, expirations_->Get());
expirations_->Clear();
CallFetcherCallbacks();
EXPECT_TRUE(ResourceIsCached());
// But if we have just a little bit of traffic then when we get a request
// for a soon-to-expire resource it will auto-freshen.
AdvanceTimeMs((1 + (max_age_sec * 4) / 5) * Timer::kSecondMs);
EXPECT_TRUE(ResourceIsCached());
CallFetcherCallbacks(); // freshens cache.
AdvanceTimeMs((max_age_sec / 5) * Timer::kSecondMs);
EXPECT_TRUE(ResourceIsCached()); // Yay, no cache misses after 301 seconds
EXPECT_EQ(0, expirations_->Get());
}
// Tests that freshining will not be performed when we have caching
// forced. Nothing will ever be evicted due to time, so there is no
// need to freshen.
TEST_F(ResourceFreshenTest, NoFreshenOfForcedCachedResources) {
http_cache()->set_force_caching(true);
FetcherUpdateDateHeaders();
response_headers_.Add(HttpAttributes::kCacheControl, "max-age=0");
SetFetchResponse(kResourceUrl, response_headers_, "foo");
// We should get just 1 fetch. If we were aggressively freshening
// we would get 2.
EXPECT_TRUE(ResourceIsCached());
EXPECT_EQ(1, counting_url_async_fetcher()->fetch_count());
// There should be no extra fetches required because our cache is
// still active. We shouldn't have needed an extra fetch to freshen,
// either, because the cache expiration time is irrelevant -- we are
// forcing caching so we consider the resource to always be fresh.
// So even after an hour we should have no expirations.
AdvanceTimeMs(1 * Timer::kHourMs);
EXPECT_TRUE(ResourceIsCached());
EXPECT_EQ(1, counting_url_async_fetcher()->fetch_count());
// Nothing expires with force-caching on.
EXPECT_EQ(0, expirations_->Get());
}
// Tests that freshining will not occur for short-lived resources,
// which could impact the performance of the server.
TEST_F(ResourceFreshenTest, NoFreshenOfShortLivedResources) {
FetcherUpdateDateHeaders();
int max_age_sec =
RewriteOptions::kDefaultImplicitCacheTtlMs / Timer::kSecondMs - 1;
response_headers_.Add(HttpAttributes::kCacheControl,
StringPrintf("max-age=%d", max_age_sec));
SetFetchResponse(kResourceUrl, response_headers_, "foo");
EXPECT_TRUE(ResourceIsCached());
EXPECT_EQ(1, counting_url_async_fetcher()->fetch_count());
// There should be no extra fetches required because our cache is
// still active. We shouldn't have needed an extra fetch to freshen,
// either.
AdvanceTimeMs((max_age_sec - 1) * Timer::kSecondMs);
EXPECT_TRUE(ResourceIsCached());
EXPECT_EQ(1, counting_url_async_fetcher()->fetch_count());
EXPECT_EQ(0, expirations_->Get());
// Now let the resource expire. We'll need another fetch since we did not
// freshen.
AdvanceTimeMs(2 * Timer::kSecondMs);
EXPECT_TRUE(ResourceIsCached());
EXPECT_EQ(2, counting_url_async_fetcher()->fetch_count());
EXPECT_EQ(1, expirations_->Get());
}
class ServerContextShardedTest : public ServerContextTest {
protected:
virtual void SetUp() {
ServerContextTest::SetUp();
EXPECT_TRUE(options()->WriteableDomainLawyer()->AddShard(
"example.com", "shard0.com,shard1.com", message_handler()));
}
};
TEST_F(ServerContextShardedTest, TestNamed) {
GoogleString url = Encode("http://example.com/dir/123/",
"jm", "0", "orig", "js");
GoogleString failure_reason;
OutputResourcePtr output_resource(
rewrite_driver()->CreateOutputResourceWithPath(
"http://example.com/dir/",
"jm",
"orig.js",
kRewrittenResource,
&failure_reason));
ASSERT_TRUE(output_resource.get());
EXPECT_EQ("", failure_reason);
ASSERT_TRUE(rewrite_driver()->Write(ResourceVector(),
"alert('hello');",
&kContentTypeJavascript,
StringPiece(),
output_resource.get()));
// This always gets mapped to shard0 because we are using the mock
// hasher for the content hash. Note that the sharding sensitivity
// to the hash value is tested in DomainLawyerTest.Shard, and will
// also be covered in a system test.
EXPECT_EQ(Encode("http://shard0.com/dir/", "jm", "0", "orig.js", "js"),
output_resource->url());
}
TEST_F(ServerContextTest, TestMergeNonCachingResponseHeaders) {
ResponseHeaders input, output;
input.Add("X-Extra-Header", "Extra Value"); // should be copied to output
input.Add(HttpAttributes::kCacheControl, "max-age=300"); // should not be
server_context()->MergeNonCachingResponseHeaders(input, &output);
ConstStringStarVector v;
EXPECT_FALSE(output.Lookup(HttpAttributes::kCacheControl, &v));
ASSERT_TRUE(output.Lookup("X-Extra-Header", &v));
ASSERT_EQ(1, v.size());
EXPECT_EQ("Extra Value", *v[0]);
}
class ServerContextCacheControlTest : public ServerContextTest {
protected:
virtual void SetUp() {
ServerContextTest::SetUp();
implicit_public_100_ = CreateCustomCachingResource("ipub_100", 100, "");
implicit_public_200_ = CreateCustomCachingResource("ipub_200", 200, "");
explicit_public_200_ = CreateCustomCachingResource(
"epub_200", 200, ",public");
private_300_ = CreateCustomCachingResource("pri_300", 300, ",private");
private_400_ = CreateCustomCachingResource("pri_400", 400, ",private");
no_cache_150_ = CreateCustomCachingResource("noc_150", 400, ",no-cache");
no_store_200_ = CreateCustomCachingResource("nos_200", 200, ",no-store");
DefaultHeaders(&response_headers_);
}
GoogleString LongCacheTtl() const {
return StrCat(
"max-age=",
Integer64ToString(ServerContext::kGeneratedMaxAgeMs /
Timer::kSecondMs));
}
bool HasCacheControl(StringPiece value) {
return response_headers_.HasValue(HttpAttributes::kCacheControl, value);
}
ResourcePtr implicit_public_100_;
ResourcePtr implicit_public_200_;
ResourcePtr explicit_public_200_;
ResourcePtr private_300_;
ResourcePtr private_400_;
ResourcePtr no_cache_150_;
ResourcePtr no_store_200_;
ResourceVector resources_;
ResponseHeaders response_headers_;
};
TEST_F(ServerContextCacheControlTest, ImplicitPublic) {
// If we feed in just implicitly public resources, we should get
// something with ultra-long TTL, regardless of how soon they
// expire.
resources_.push_back(implicit_public_100_);
resources_.push_back(implicit_public_200_);
server_context()->ApplyInputCacheControl(resources_, &response_headers_);
EXPECT_STREQ(LongCacheTtl(),
response_headers_.Lookup1(HttpAttributes::kCacheControl));
}
TEST_F(ServerContextCacheControlTest, ExplicitPublic) {
// An explicit 'public' gets reflected in the output.
resources_.push_back(explicit_public_200_);
server_context()->ApplyInputCacheControl(resources_, &response_headers_);
EXPECT_TRUE(HasCacheControl("public"));
EXPECT_FALSE(HasCacheControl("private"));
EXPECT_TRUE(HasCacheControl(LongCacheTtl()));
}
TEST_F(ServerContextCacheControlTest, Private) {
// If an input is private, however, we must mark output appropriately
// and not cache-extend.
resources_.push_back(implicit_public_100_);
resources_.push_back(private_300_);
resources_.push_back(private_400_);
server_context()->ApplyInputCacheControl(resources_, &response_headers_);
EXPECT_FALSE(HasCacheControl("public"));
EXPECT_TRUE(HasCacheControl("private"));
EXPECT_TRUE(HasCacheControl("max-age=100"));
}
TEST_F(ServerContextCacheControlTest, NoCache) {
// Similarly no-cache should be incorporated --- but then we also need
// to have 0 ttl.
resources_.push_back(implicit_public_100_);
resources_.push_back(private_300_);
resources_.push_back(private_400_);
resources_.push_back(no_cache_150_);
server_context()->ApplyInputCacheControl(resources_, &response_headers_);
EXPECT_FALSE(HasCacheControl("public"));
EXPECT_TRUE(HasCacheControl("no-cache"));
EXPECT_TRUE(HasCacheControl("max-age=0"));
}
TEST_F(ServerContextCacheControlTest, NoStore) {
// Make sure we save no-store as well.
resources_.push_back(implicit_public_100_);
resources_.push_back(private_300_);
resources_.push_back(private_400_);
resources_.push_back(no_cache_150_);
resources_.push_back(no_store_200_);
server_context()->ApplyInputCacheControl(resources_, &response_headers_);
EXPECT_FALSE(HasCacheControl("public"));
EXPECT_TRUE(HasCacheControl("no-cache"));
EXPECT_TRUE(HasCacheControl("no-store"));
EXPECT_TRUE(HasCacheControl("max-age=0"));
}
TEST_F(ServerContextTest, WriteChecksInputVector) {
// Make sure ->Write incorporates the cache control info from inputs,
// and doesn't cache a private resource improperly. Also make sure
// we get the charset right (including quoting).
ResourcePtr private_400(
CreateCustomCachingResource("pri_400", 400, ",private"));
// Should have the 'it's not cacheable!' entry here; see also below.
EXPECT_EQ(1, http_cache()->cache_inserts()->Get());
GoogleString failure_reason;
OutputResourcePtr output_resource(
rewrite_driver()->CreateOutputResourceFromResource(
"cf", rewrite_driver()->default_encoder(), NULL /* no context*/,
private_400, kRewrittenResource, &failure_reason));
ASSERT_TRUE(output_resource.get() != NULL);
EXPECT_EQ("", failure_reason);
rewrite_driver()->Write(ResourceVector(1, private_400),
"boo!",
&kContentTypeText,
"\"\\koi8-r\"", // covers escaping behavior, too.
output_resource.get());
ResponseHeaders* headers = output_resource->response_headers();
EXPECT_FALSE(headers->HasValue(HttpAttributes::kCacheControl, "public"));
EXPECT_TRUE(headers->HasValue(HttpAttributes::kCacheControl, "private"));
EXPECT_TRUE(headers->HasValue(HttpAttributes::kCacheControl, "max-age=400"));
EXPECT_STREQ("text/plain; charset=\"\\koi8-r\"",
headers->Lookup1(HttpAttributes::kContentType));
// Make sure nothing extra in the cache at this point.
EXPECT_EQ(1, http_cache()->cache_inserts()->Get());
}
TEST_F(ServerContextTest, IsPagespeedResource) {
GoogleUrl rewritten(Encode("http://shard0.com/dir/", "jm", "0",
"orig.js", "js"));
EXPECT_TRUE(server_context()->IsPagespeedResource(rewritten));
GoogleUrl normal("http://jqueryui.com/jquery-1.6.2.js");
EXPECT_FALSE(server_context()->IsPagespeedResource(normal));
}
TEST_F(ServerContextTest, PartlyFailedFetch) {
// Regression test for invalid Resource state when the fetch physically
// succeeds but does not get added to cache due to invalid cacheability.
// In that case, we would end up with headers claiming successful fetch,
// but an HTTPValue without headers set (which would also crash on
// access if no data was emitted by fetcher via Write).
static const char kCssName[] = "a.css";
GoogleString abs_url = AbsolutifyUrl(kCssName);
ResponseHeaders non_cacheable;
SetDefaultLongCacheHeaders(&kContentTypeCss, &non_cacheable);
non_cacheable.SetDateAndCaching(start_time_ms() /* date */, 0 /* ttl */,
"private, no-cache");
non_cacheable.ComputeCaching();
SetFetchResponse(abs_url, non_cacheable, "foo");
// We tell the fetcher to quash the zero-bytes writes, as that behavior
// (which Serf has) made the bug more severe, with not only
// loaded() and HttpStatusOk() lying, but also contents() crashing.
mock_url_fetcher()->set_omit_empty_writes(true);
// We tell the fetcher to output the headers and then immediately fail.
mock_url_fetcher()->set_fail_after_headers(true);
GoogleUrl gurl(abs_url);
SetBaseUrlForFetch(abs_url);
bool is_authorized;
ResourcePtr resource = rewrite_driver()->CreateInputResource(gurl,
&is_authorized);
ASSERT_TRUE(resource.get() != NULL);
EXPECT_TRUE(is_authorized);
MockResourceCallback callback(resource, factory()->thread_system());
resource->LoadAsync(Resource::kReportFailureIfNotCacheable,
rewrite_driver()->request_context(),
&callback);
EXPECT_TRUE(callback.done());
EXPECT_FALSE(callback.success());
EXPECT_FALSE(resource->IsValidAndCacheable());
EXPECT_FALSE(resource->loaded());
EXPECT_FALSE(resource->HttpStatusOk())
<< " Unexpectedly got access to resource contents:"
<< resource->ExtractUncompressedContents();
}
TEST_F(ServerContextTest, LoadFromFileReadAsync) {
// This reads a resource twice, to make sure that there is no misbehavior
// (read: check failures or crashes) when cache invalidation logic tries to
// deal with FileInputResource.
const char kContents[] = "lots of bits of data";
options()->file_load_policy()->Associate("http://test.com/", "/test/");
GoogleUrl test_url("http://test.com/a.css");
// Init file resources.
WriteFile("/test/a.css", kContents);
SetBaseUrlForFetch("http://test.com");
bool unused;
ResourcePtr resource(rewrite_driver()->CreateInputResource(test_url,
&unused));
VerifyContentsCallback callback(resource, kContents);
resource->LoadAsync(Resource::kReportFailureIfNotCacheable,
rewrite_driver()->request_context(),
&callback);
callback.AssertCalled();
resource = rewrite_driver()->CreateInputResource(test_url, &unused);
VerifyContentsCallback callback2(resource, kContents);
resource->LoadAsync(Resource::kReportFailureIfNotCacheable,
rewrite_driver()->request_context(),
&callback2);
callback2.AssertCalled();
}
namespace {
void CheckMatchesHeaders(const ResponseHeaders& headers,
const InputInfo& input) {
ASSERT_TRUE(input.has_type());
EXPECT_EQ(InputInfo::CACHED, input.type());
ASSERT_TRUE(input.has_last_modified_time_ms());
EXPECT_EQ(headers.last_modified_time_ms(), input.last_modified_time_ms());
ASSERT_TRUE(input.has_expiration_time_ms());
EXPECT_EQ(headers.CacheExpirationTimeMs(), input.expiration_time_ms());
ASSERT_TRUE(input.has_date_ms());
EXPECT_EQ(headers.date_ms(), input.date_ms());
}
} // namespace
TEST_F(ServerContextTest, FillInPartitionInputInfo) {
// Test for Resource::FillInPartitionInputInfo.
const char kUrl[] = "http://example.com/page.html";
const char kContents[] = "bits";
SetBaseUrlForFetch("http://example.com/");
ResponseHeaders headers;
SetDefaultLongCacheHeaders(&kContentTypeHtml, &headers);
headers.ComputeCaching();
SetFetchResponse(kUrl, headers, kContents);
GoogleUrl gurl(kUrl);
bool unused;
ResourcePtr resource(rewrite_driver()->CreateInputResource(gurl, &unused));
VerifyContentsCallback callback(resource, kContents);
resource->LoadAsync(Resource::kReportFailureIfNotCacheable,
rewrite_driver()->request_context(),
&callback);
callback.AssertCalled();
InputInfo with_hash, without_hash;
resource->FillInPartitionInputInfo(Resource::kIncludeInputHash, &with_hash);
resource->FillInPartitionInputInfo(Resource::kOmitInputHash, &without_hash);
CheckMatchesHeaders(headers, with_hash);
CheckMatchesHeaders(headers, without_hash);
ASSERT_TRUE(with_hash.has_input_content_hash());
EXPECT_STREQ("zEEebBNnDlISRim4rIP30", with_hash.input_content_hash());
EXPECT_FALSE(without_hash.has_input_content_hash());
}
// Test of referer for BackgroundFetch: When the resource fetching request
// header misses referer, we set the driver base url as its referer.
TEST_F(ServerContextTest, TestRefererBackgroundFetch) {
RefererTest(NULL, true);
EXPECT_EQ(rewrite_driver()->base_url().Spec(),
mock_url_fetcher()->last_referer());
}
// Test of referer for NonBackgroundFetch: When the resource fetching request
// header misses referer and the original request referer header misses, no
// referer would be added.
TEST_F(ServerContextTest, TestRefererNonBackgroundFetch) {
RequestHeaders headers;
RefererTest(&headers, false);
EXPECT_EQ("", mock_url_fetcher()->last_referer());
}
// Test of referer for NonBackgroundFetch: When the resource fetching request
// header misses referer but the original request header has referer set, we set
// this referer as the referer of resource fetching request.
TEST_F(ServerContextTest, TestRefererNonBackgroundFetchWithDriverRefer) {
RequestHeaders headers;
const char kReferer[] = "http://other.com/";
headers.Add(HttpAttributes::kReferer, kReferer);
RefererTest(&headers, false);
EXPECT_EQ(kReferer, mock_url_fetcher()->last_referer());
}
// Regression test for RewriteTestBase::DefaultResponseHeaders, which is based
// on ServerContext methods. It used to not set 'Expires' correctly.
TEST_F(ServerContextTest, RewriteTestBaseDefaultResponseHeaders) {
ResponseHeaders headers;
DefaultResponseHeaders(kContentTypeCss, 100 /* ttl_sec */, &headers);
int64 expire_time_ms = 0;
ASSERT_TRUE(
headers.ParseDateHeader(HttpAttributes::kExpires, &expire_time_ms));
EXPECT_EQ(timer()->NowMs() + 100 * Timer::kSecondMs, expire_time_ms);
}
} // namespace net_instaweb