| /* |
| * 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, ¬_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, ¬_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 |