blob: 430ffe2e0af1ea70c3a8e2fbcc2200b22f88687f [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: jmarantz@google.com (Joshua D. Marantz)
#include "net/instaweb/rewriter/public/rewrite_driver.h"
#include "net/instaweb/http/public/async_fetch.h"
#include "net/instaweb/http/public/counting_url_async_fetcher.h"
#include "net/instaweb/http/public/log_record.h"
#include "net/instaweb/http/public/logging_proto_impl.h"
#include "net/instaweb/http/public/mock_url_fetcher.h"
#include "net/instaweb/http/public/wait_url_async_fetcher.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_kind.h"
#include "net/instaweb/rewriter/public/request_properties.h"
#include "net/instaweb/rewriter/public/resource.h"
#include "net/instaweb/rewriter/public/resource_slot.h"
#include "net/instaweb/rewriter/public/rewrite_options.h"
#include "net/instaweb/rewriter/public/rewrite_stats.h"
#include "net/instaweb/rewriter/public/rewrite_test_base.h"
#include "net/instaweb/rewriter/public/server_context.h"
#include "net/instaweb/rewriter/public/single_rewrite_context.h"
#include "net/instaweb/rewriter/public/test_rewrite_driver_factory.h"
#include "net/instaweb/rewriter/public/test_url_namer.h"
#include "pagespeed/kernel/base/abstract_mutex.h"
#include "pagespeed/kernel/base/basictypes.h"
#include "pagespeed/kernel/base/gtest.h"
#include "pagespeed/kernel/base/hasher.h"
#include "pagespeed/kernel/base/mock_message_handler.h"
#include "pagespeed/kernel/base/mock_timer.h"
#include "pagespeed/kernel/base/null_mutex.h"
#include "pagespeed/kernel/base/sha1_signature.h"
#include "pagespeed/kernel/base/statistics.h"
#include "pagespeed/kernel/base/string.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/empty_html_filter.h"
#include "pagespeed/kernel/html/html_name.h"
#include "pagespeed/kernel/html/html_parse_test_base.h"
#include "pagespeed/kernel/http/google_url.h"
#include "pagespeed/kernel/http/http_names.h"
#include "pagespeed/kernel/http/http_options.h"
#include "pagespeed/kernel/http/request_headers.h"
#include "pagespeed/kernel/http/semantic_type.h"
#include "pagespeed/kernel/http/user_agent_matcher_test_base.h"
#include "pagespeed/kernel/thread/worker_test_base.h"
namespace net_instaweb {
class RewriteFilter;
class RewriteDriverTest : public RewriteTestBase {
protected:
RewriteDriverTest() {}
bool CanDecodeUrl(const StringPiece& url) {
GoogleUrl gurl(url);
RewriteFilter* filter;
OutputResourcePtr resource(
rewrite_driver()->DecodeOutputResource(gurl, &filter));
return (resource.get() != NULL);
}
GoogleString BaseUrlSpec() {
return rewrite_driver()->base_url().Spec().as_string();
}
// A helper to call ComputeCurrentFlushWindowRewriteDelayMs() that allows
// us to keep it private.
int64 GetFlushTimeout() {
return rewrite_driver()->ComputeCurrentFlushWindowRewriteDelayMs();
}
bool IsDone(RewriteDriver::WaitMode wait_mode, bool deadline_reached) {
ScopedMutex lock(rewrite_driver()->rewrite_mutex());
return rewrite_driver()->IsDone(wait_mode, deadline_reached);
}
void IncrementAsyncEventsCount() {
rewrite_driver()->IncrementAsyncEventsCount();
}
void DecrementAsyncEventsCount() {
rewrite_driver()->DecrementAsyncEventsCount();
}
void SetupResponsesForDownstreamCacheTesting() {
// Setup responses for the resources.
const char kCss[] = "* { display: none; }";
SetResponseWithDefaultHeaders("a.css", kContentTypeCss, kCss, 100);
SetResponseWithDefaultHeaders("test/b.css", kContentTypeCss, kCss, 100);
// Setup a fake response for the expected purge path.
SetResponseWithDefaultHeaders("http://localhost:1234/purge/",
kContentTypeCss, "", 100);
}
void ProcessHtmlForDownstreamCacheTesting() {
GoogleString input_html(
StrCat(CssLinkHref("a.css"), " ", CssLinkHref("test/b.css")));
ParseUrl(kTestDomain, input_html);
}
void TestBlockingRewrite(RequestHeaders* request_headers,
bool expected_blocking_rewrite,
bool expected_fast_blocking_rewrite) {
rewrite_driver()->EnableBlockingRewrite(request_headers);
EXPECT_EQ(expected_blocking_rewrite,
rewrite_driver()->fully_rewrite_on_flush());
EXPECT_EQ(expected_fast_blocking_rewrite,
rewrite_driver()->fast_blocking_rewrite());
// Reset the flags to their default values after the test.
rewrite_driver()->set_fully_rewrite_on_flush(false);
rewrite_driver()->set_fast_blocking_rewrite(true);
EXPECT_FALSE(request_headers->Has(
HttpAttributes::kXPsaBlockingRewrite));
EXPECT_FALSE(request_headers->Has(
HttpAttributes::kXPsaBlockingRewriteMode));
}
void TestPendingEventsIsDone(bool wait_for_completion) {
EXPECT_TRUE(IsDone(RewriteDriver::kWaitForShutDown, false));
EXPECT_TRUE(IsDone(RewriteDriver::kWaitForCompletion, false));
IncrementAsyncEventsCount();
EXPECT_FALSE(IsDone(RewriteDriver::kWaitForShutDown, false));
EXPECT_EQ(wait_for_completion,
IsDone(RewriteDriver::kWaitForCompletion, false));
DecrementAsyncEventsCount();
EXPECT_TRUE(IsDone(RewriteDriver::kWaitForShutDown, false));
EXPECT_TRUE(IsDone(RewriteDriver::kWaitForCompletion, false));
}
void TestPendingEventsDriverCleanup(bool blocking_rewrite,
bool fast_blocking_rewrite) {
RewriteDriver* other_driver =
server_context()->NewRewriteDriver(CreateRequestContext());
other_driver->set_fully_rewrite_on_flush(blocking_rewrite);
other_driver->set_fast_blocking_rewrite(fast_blocking_rewrite);
other_driver->IncrementAsyncEventsCount();
other_driver->Cleanup();
other_driver->DecrementAsyncEventsCount();
EXPECT_EQ(0, server_context()->num_active_rewrite_drivers());
}
private:
DISALLOW_COPY_AND_ASSIGN(RewriteDriverTest);
};
namespace {
const char kBikePngFile[] = "BikeCrashIcn.png";
const char kNonRewrittenCachableHtml[] =
"<html>\n<link rel=stylesheet href=a.css> "
"<link rel=stylesheet href=test/b.css></html>";
const char kRewrittenCachableHtmlWithCacheExtension[] =
"<html>\n"
"<link rel=stylesheet href=a.css.pagespeed.ce.0.css> "
"<link rel=stylesheet href=test/b.css.pagespeed.ce.0.css>"
"</html>";
const char kRewrittenCachableHtmlWithCollapseWhitespace[] =
"<html>\n<link rel=stylesheet href=a.css> "
"<link rel=stylesheet href=test/b.css></html>";
TEST_F(RewriteDriverTest, NoChanges) {
ValidateNoChanges("no_changes",
"<head><script src=\"foo.js\"></script></head>"
"<body><form method=\"post\">"
"<input type=\"checkbox\" checked>"
"</form></body>");
}
TEST_F(RewriteDriverTest, CloneMarksNested) {
RequestHeaders request_headers;
request_headers.Add(HttpAttributes::kAccept, "image/webp");
request_headers.Add("a", "b");
request_headers.Add(HttpAttributes::kVia, "1.1 google");
rewrite_driver()->SetRequestHeaders(request_headers);
RewriteDriver* clone1 = rewrite_driver()->Clone();
EXPECT_TRUE(clone1->is_nested());
EXPECT_TRUE(clone1->request_properties()->SupportsWebpRewrittenUrls());
EXPECT_TRUE(rewrite_driver()->request_headers()->HasValue("a", "b"));
EXPECT_TRUE(clone1->request_headers()->HasValue("a", "b"));
EXPECT_TRUE(rewrite_driver()->request_headers()->HasValue(
HttpAttributes::kVia, "1.1 google"));
EXPECT_FALSE(clone1->request_headers()->HasValue(
HttpAttributes::kVia, "1.1 google"));
clone1->Cleanup();
RewriteDriver* parent2 =
server_context()->NewRewriteDriver(CreateRequestContext());
parent2->SetRequestHeaders(request_headers);
RewriteDriver* clone2 = parent2->Clone();
EXPECT_TRUE(clone2->is_nested());
clone2->Cleanup();
parent2->Cleanup();
}
TEST_F(RewriteDriverTest, TestLegacyUrl) {
GoogleString hash(32, '0');
rewrite_driver()->AddFilters();
EXPECT_FALSE(CanDecodeUrl("http://example.com/dir/123/jm." + hash + ".orig"))
<< "not enough dots";
EXPECT_FALSE(CanDecodeUrl("http://example.com/dir/123/jm.0.orig.js"))
<< "hash too short";
EXPECT_TRUE(CanDecodeUrl("http://example.com/dir/123/jm."+hash+".orig.js"));
EXPECT_TRUE(CanDecodeUrl(
"http://x.com/dir/123/jm.0123456789abcdef0123456789ABCDEF.orig.js"));
EXPECT_FALSE(CanDecodeUrl("http://example.com/dir/123/xx."+hash+".orig.js"))
<< "invalid filter xx";
GoogleString bad_hash(32, 'z');
EXPECT_FALSE(CanDecodeUrl("http://example.com/dir/123/jm." + bad_hash +
".orig.js"))
<< "invalid hash code -- not hex";
EXPECT_FALSE(CanDecodeUrl("http://example.com/dir/123/jm.ab.orig.js"))
<< "invalid hash code -- not 32 chars";
EXPECT_FALSE(CanDecodeUrl("http://example.com/dir/123/jm."
+ hash + ".orig.x"))
<< "invalid extension";
}
TEST_F(RewriteDriverTest, TestValidUrlSignatures) {
StringPiece key("helloworld");
options()->set_url_signing_key(key);
EXPECT_EQ(10, options()->sha1signature()->SignatureSizeInChars());
rewrite_driver()->AddFilters();
EXPECT_TRUE(CanDecodeUrl(
"http://signed-urls.example.com/mod_pagespeed_example/styles/"
"A.all_styles.css.pagespeed.cf.UQ_aP9rObnq.css"))
<< "valid signature";
EXPECT_FALSE(CanDecodeUrl(
"http://signed-urls.example.com/mod_pagespeed_example/styles/"
"A.all_styles.css.pagespeed.cf.UAAAAAAAAAA.css"))
<< "invalid signature";
EXPECT_FALSE(CanDecodeUrl(
"http://signed-urls.example.com/mod_pagespeed_example/styles/"
"A.all_styles.css.pagespeed.cf.U.css"))
<< "no signature";
}
TEST_F(RewriteDriverTest, TestIgnoringUrlSignatures) {
options()->set_url_signing_key("helloworld");
options()->set_accept_invalid_signatures(true);
EXPECT_EQ(10, options()->sha1signature()->SignatureSizeInChars());
rewrite_driver()->AddFilters();
EXPECT_TRUE(CanDecodeUrl(
"http://signed-urls.example.com/mod_pagespeed_example/styles/"
"A.all_styles.css.pagespeed.cf.UQ_aP9rObnq.css"))
<< "valid signature, ignored";
EXPECT_TRUE(CanDecodeUrl(
"http://signed-urls.example.com/mod_pagespeed_example/styles/"
"A.all_styles.css.pagespeed.cf.UAAAAAAAAAA.css"))
<< "invalid signature, ignored";
EXPECT_TRUE(CanDecodeUrl(
"http://signed-urls.example.com/mod_pagespeed_example/styles/"
"A.all_styles.css.pagespeed.cf.U.css"))
<< "no signature, ignored";
}
TEST_F(RewriteDriverTest, PagespeedObliviousPositiveTest) {
RewriteOptions* ops = options();
ops->set_oblivious_pagespeed_urls(false); // Decode Pagespeed URL.
rewrite_driver()->AddFilters();
EXPECT_TRUE(CanDecodeUrl(
"http://www.example.com/foresee-trigger.js.pagespeed.jm.0D45DpKAeI.js"));
}
TEST_F(RewriteDriverTest, PagespeedObliviousNegativeTest) {
RewriteOptions* ops = options();
ops->set_oblivious_pagespeed_urls(true); // Don't decode Pagespeed URL.
rewrite_driver()->AddFilters();
EXPECT_FALSE(CanDecodeUrl(
"http://www.example.com/foresee-trigger.js.pagespeed.jm.0D45DpKAeI.js"));
}
TEST_F(RewriteDriverTest, TestModernUrl) {
rewrite_driver()->AddFilters();
// Sanity-check on a valid one
EXPECT_TRUE(CanDecodeUrl(
Encode("http://example.com/", "ce", "HASH", "Puzzle.jpg", "jpg")));
// Query is OK, too.
EXPECT_TRUE(CanDecodeUrl(
StrCat(Encode("http://example.com/", "ce", "HASH", "Puzzle.jpg", "jpg"),
"?s=ok")));
// Invalid filter code
EXPECT_FALSE(CanDecodeUrl(
Encode("http://example.com/", "NOFILTER", "HASH", "Puzzle.jpg", "jpg")));
// Nonsense extension -- we will just ignore it these days.
EXPECT_TRUE(CanDecodeUrl(
Encode("http://example.com/", "ce", "HASH", "Puzzle.jpg", "jpgif")));
// No hash
GoogleString encoded_url(Encode("http://example.com/", "ce", "123456789",
"Puzzle.jpg", "jpg"));
GlobalReplaceSubstring("123456789", "", &encoded_url);
EXPECT_FALSE(CanDecodeUrl(encoded_url));
}
class RewriteDriverTestUrlNamer : public RewriteDriverTest {
public:
RewriteDriverTestUrlNamer() {
SetUseTestUrlNamer(true);
}
};
TEST_F(RewriteDriverTestUrlNamer, TestEncodedUrls) {
rewrite_driver()->AddFilters();
// Sanity-check on a valid one
EXPECT_TRUE(CanDecodeUrl(
Encode("http://example.com/", "ce", "HASH", "Puzzle.jpg", "jpg")));
// Query is OK, too.
EXPECT_TRUE(CanDecodeUrl(
StrCat(Encode("http://example.com/", "ce", "HASH", "Puzzle.jpg", "jpg"),
"?s=ok")));
// Invalid filter code
EXPECT_FALSE(CanDecodeUrl(
Encode("http://example.com/", "NOFILTER", "HASH", "Puzzle.jpg", "jpg")));
// Nonsense extension -- we will just ignore it these days.
EXPECT_TRUE(CanDecodeUrl(
Encode("http://example.com/", "ce", "HASH", "Puzzle.jpg", "jpgif")));
// No hash
GoogleString encoded_url(Encode("http://example.com/", "ce", "123456789",
"Puzzle.jpg", "jpg"));
GlobalReplaceSubstring("123456789", "", &encoded_url);
EXPECT_FALSE(CanDecodeUrl(encoded_url));
// Valid proxy domain but invalid decoded URL.
encoded_url = Encode("http://example.com/", "ce", "0", "Puzzle.jpg", "jpg");
GlobalReplaceSubstring("example.com/",
"example.comWYTHQ000JRJFCAAKYU1EMA6VUBDTS4DESLRWIPMS"
"KKMQH0XYN1FURDBBSQ9AYXVX3TZDKZEIJNLRHU05ATHBAWWAG2+"
"ADDCXPWGGP1VTHJIYU13IIFQYSYMGKIMSFIEBM+HCAACVNGO8CX"
"XO%81%9F%F1m/", &encoded_url);
// By default TestUrlNamer doesn't proxy but we need it to for this test.
TestUrlNamer::SetProxyMode(true);
EXPECT_FALSE(CanDecodeUrl(encoded_url));
}
TEST_F(RewriteDriverTestUrlNamer, TestDecodeUrls) {
// Sanity-check on a valid one
GoogleUrl gurl_good(Encode(
"http://example.com/", "ce", "HASH", "Puzzle.jpg", "jpg"));
rewrite_driver()->AddFilters();
StringVector urls;
TestUrlNamer::SetProxyMode(true);
EXPECT_TRUE(rewrite_driver()->DecodeUrl(gurl_good, &urls));
EXPECT_EQ(1, urls.size());
EXPECT_EQ("http://example.com/Puzzle.jpg", urls[0]);
// Invalid filter code
urls.clear();
GoogleUrl gurl_bad(Encode(
"http://example.com/", "NOFILTER", "HASH", "Puzzle.jpg", "jpgif"));
EXPECT_FALSE(rewrite_driver()->DecodeUrl(gurl_bad, &urls));
// Combine filters
urls.clear();
GoogleUrl gurl_multi(Encode(
"http://example.com/", "cc", "HASH", MultiUrl("a.css", "b.css"), "css"));
EXPECT_TRUE(rewrite_driver()->DecodeUrl(gurl_multi, &urls));
EXPECT_EQ(2, urls.size());
EXPECT_EQ("http://example.com/a.css", urls[0]);
EXPECT_EQ("http://example.com/b.css", urls[1]);
// Invalid Url.
urls.clear();
GoogleUrl gurl_invalid("invalid url");
EXPECT_FALSE(rewrite_driver()->DecodeUrl(gurl_invalid, &urls));
EXPECT_EQ(0, urls.size());
// ProxyMode off
urls.clear();
TestUrlNamer::SetProxyMode(false);
SetUseTestUrlNamer(false);
gurl_good.Reset(Encode(
"http://example.com/", "ce", "HASH", "Puzzle.jpg", "jpg"));
EXPECT_TRUE(rewrite_driver()->DecodeUrl(gurl_good, &urls));
EXPECT_EQ(1, urls.size());
EXPECT_EQ("http://example.com/Puzzle.jpg", urls[0]);
urls.clear();
gurl_multi.Reset(Encode(
"http://example.com/", "cc", "HASH", MultiUrl("a.css", "b.css"), "css"));
EXPECT_TRUE(rewrite_driver()->DecodeUrl(gurl_multi, &urls));
EXPECT_EQ(2, urls.size());
EXPECT_EQ("http://example.com/a.css", urls[0]);
EXPECT_EQ("http://example.com/b.css", urls[1]);
}
// Test to make sure we do not put in extra things into the cache.
// This is using the CSS rewriter, which caches the output.
TEST_F(RewriteDriverTest, TestCacheUse) {
AddFilter(RewriteOptions::kRewriteCss);
const char kCss[] = "* { display: none; }";
const char kMinCss[] = "*{display:none}";
SetResponseWithDefaultHeaders("a.css", kContentTypeCss, kCss, 100);
GoogleString css_minified_url =
Encode(kTestDomain, RewriteOptions::kCssFilterId,
hasher()->Hash(kMinCss), "a.css", "css");
// Cold load.
EXPECT_TRUE(TryFetchResource(css_minified_url));
// We should have 3 things inserted:
// 1) the source data
// 2) the result
// 3) the rname entry for the result
int cold_num_inserts = lru_cache()->num_inserts();
EXPECT_EQ(3, cold_num_inserts);
// Warm load. This one should not change the number of inserts at all
EXPECT_TRUE(TryFetchResource(css_minified_url));
EXPECT_EQ(cold_num_inserts, lru_cache()->num_inserts());
EXPECT_EQ(0, lru_cache()->num_identical_reinserts());
}
// Test to make sure when we fetch a with a Via header, "public"
// is added to the Cache-Control.
TEST_F(RewriteDriverTest, ViaPublicPageSpeedResource) {
RequestHeaders request_headers;
request_headers.Add(HttpAttributes::kVia, "1.1 google");
AddFilter(RewriteOptions::kRewriteCss);
const char kCss[] = "* { display: none; }";
const char kMinCss[] = "*{display:none}";
SetResponseWithDefaultHeaders("a.css", kContentTypeCss, kCss, 100);
GoogleString url = Encode(kTestDomain, RewriteOptions::kCssFilterId,
hasher()->Hash(kMinCss), "a.css", "css");
// Cold load.
ResponseHeaders response;
GoogleString contents;
ASSERT_TRUE(FetchResourceUrl(url, &request_headers, &contents, &response));
EXPECT_TRUE(response.HasValue(HttpAttributes::kCacheControl, "public"));
// Warm load.
response.Clear();
ASSERT_TRUE(FetchResourceUrl(url, &request_headers, &contents, &response));
EXPECT_TRUE(response.HasValue(HttpAttributes::kCacheControl, "public"));
}
// Extension of above with cache invalidation.
TEST_F(RewriteDriverTest, TestCacheUseWithInvalidation) {
AddFilter(RewriteOptions::kRewriteCss);
const char kCss[] = "* { display: none; }";
const char kMinCss[] = "*{display:none}";
SetResponseWithDefaultHeaders("a.css", kContentTypeCss, kCss, 100);
GoogleString css_minified_url =
Encode(kTestDomain, RewriteOptions::kCssFilterId,
hasher()->Hash(kMinCss), "a.css", "css");
// Cold load.
EXPECT_TRUE(TryFetchResource(css_minified_url));
// We should have 3 things inserted:
// 1) the source data
// 2) the result
// 3) the rname entry for the result.
int cold_num_inserts = lru_cache()->num_inserts();
EXPECT_EQ(3, cold_num_inserts);
// Warm load. This one should not change the number of inserts at all
EXPECT_TRUE(TryFetchResource(css_minified_url));
EXPECT_EQ(cold_num_inserts, lru_cache()->num_inserts());
EXPECT_EQ(0, lru_cache()->num_identical_reinserts());
// Set cache invalidation timestamp (to now, so that response date header is
// in the "past") and load. Should get inserted again.
ClearStats();
int64 now_ms = timer()->NowMs();
options()->ClearSignatureForTesting();
options()->UpdateCacheInvalidationTimestampMs(now_ms);
options()->ComputeSignature();
EXPECT_TRUE(TryFetchResource(css_minified_url));
// We expect: identical input a new rname entry (its version # changed),
// and the output which may not may not auto-advance due to MockTimer
// black magic.
EXPECT_EQ(1, lru_cache()->num_inserts());
EXPECT_EQ(2, lru_cache()->num_identical_reinserts());
}
TEST_F(RewriteDriverTest, TestCacheUseWithUrlPatternAllInvalidation) {
AddFilter(RewriteOptions::kRewriteCss);
const char kCss[] = "* { display: none; }";
const char kMinCss[] = "*{display:none}";
SetResponseWithDefaultHeaders("a.css", kContentTypeCss, kCss, 100);
GoogleString css_minified_url =
Encode(kTestDomain, RewriteOptions::kCssFilterId,
hasher()->Hash(kMinCss), "a.css", "css");
// Cold load.
EXPECT_TRUE(TryFetchResource(css_minified_url));
// We should have 3 things inserted:
// 1) the source data
// 2) the result
// 3) the rname entry for the result.
int cold_num_inserts = lru_cache()->num_inserts();
EXPECT_EQ(3, cold_num_inserts);
// Warm load. This one should not change the number of inserts at all
EXPECT_TRUE(TryFetchResource(css_minified_url));
EXPECT_EQ(cold_num_inserts, lru_cache()->num_inserts());
EXPECT_EQ(0, lru_cache()->num_identical_reinserts());
ClearStats();
int64 now_ms = timer()->NowMs();
options()->ClearSignatureForTesting();
// Set cache invalidation (to now) for all URLs with "a.css" and also
// invalidate all metadata (the last 'false' argument below).
options()->AddUrlCacheInvalidationEntry("*a.css*", now_ms, false);
options()->ComputeSignature();
EXPECT_TRUE(TryFetchResource(css_minified_url));
// We expect: identical input, a new rewrite entry (its version # changed),
// and the output which may not may not auto-advance due to MockTimer black
// magic.
EXPECT_EQ(1, lru_cache()->num_inserts());
EXPECT_EQ(2, lru_cache()->num_identical_reinserts());
}
TEST_F(RewriteDriverTest, TestCacheUseWithUrlPatternOnlyInvalidation) {
AddFilter(RewriteOptions::kRewriteCss);
const char kCss[] = "* { display: none; }";
const char kMinCss[] = "*{display:none}";
SetResponseWithDefaultHeaders("a.css", kContentTypeCss, kCss, 100);
GoogleString css_minified_url =
Encode(kTestDomain, RewriteOptions::kCssFilterId,
hasher()->Hash(kMinCss), "a.css", "css");
// Cold load.
EXPECT_TRUE(TryFetchResource(css_minified_url));
// We should have 3 things inserted:
// 1) the source data
// 2) the result
// 3) the rname entry for the result.
int cold_num_inserts = lru_cache()->num_inserts();
EXPECT_EQ(3, cold_num_inserts);
// Warm load. This one should not change the number of inserts at all
EXPECT_TRUE(TryFetchResource(css_minified_url));
EXPECT_EQ(cold_num_inserts, lru_cache()->num_inserts());
EXPECT_EQ(0, lru_cache()->num_identical_reinserts());
ClearStats();
int64 now_ms = timer()->NowMs();
options()->ClearSignatureForTesting();
// Set cache invalidation (to now) for all URLs with "a.css". Does not
// invalidate any metadata (the last 'true' argument below).
options()->AddUrlCacheInvalidationEntry("*a.css*", now_ms, true);
options()->ComputeSignature();
EXPECT_TRUE(TryFetchResource(css_minified_url));
// The output rewritten URL is invalidated, the input is also invalidated, and
// fetched again. The rewrite entry does not change, and gets reinserted.
// Thus, we have identical input, rname entry, and the output.
EXPECT_EQ(0, lru_cache()->num_inserts());
EXPECT_EQ(3, lru_cache()->num_identical_reinserts());
}
TEST_F(RewriteDriverTest, TestCacheUseWithRewrittenUrlAllInvalidation) {
AddFilter(RewriteOptions::kRewriteCss);
const char kCss[] = "* { display: none; }";
const char kMinCss[] = "*{display:none}";
SetResponseWithDefaultHeaders("a.css", kContentTypeCss, kCss, 100);
GoogleString css_minified_url =
Encode(kTestDomain, RewriteOptions::kCssFilterId,
hasher()->Hash(kMinCss), "a.css", "css");
// Cold load.
EXPECT_TRUE(TryFetchResource(css_minified_url));
// We should have 3 things inserted:
// 1) the source data
// 2) the result
// 3) the rname entry for the result.
int cold_num_inserts = lru_cache()->num_inserts();
EXPECT_EQ(3, cold_num_inserts);
// Warm load. This one should not change the number of inserts at all
EXPECT_TRUE(TryFetchResource(css_minified_url));
EXPECT_EQ(cold_num_inserts, lru_cache()->num_inserts());
EXPECT_EQ(0, lru_cache()->num_identical_reinserts());
ClearStats();
int64 now_ms = timer()->NowMs();
options()->ClearSignatureForTesting();
// Set a URL cache invalidation entry for output URL. Original input URL is
// not affected. Also invalidate all metadata (the
// ignores_metadata_and_pcache argument being false below).
options()->AddUrlCacheInvalidationEntry(
css_minified_url, now_ms, false /* ignores_metadata_and_pcache */);
options()->ComputeSignature();
EXPECT_TRUE(TryFetchResource(css_minified_url));
// We expect: a new rewrite entry (its version # changed), and identical
// output.
EXPECT_EQ(1, lru_cache()->num_inserts());
EXPECT_EQ(1, lru_cache()->num_identical_reinserts());
}
TEST_F(RewriteDriverTest, TestCacheUseWithRewrittenUrlOnlyInvalidation) {
AddFilter(RewriteOptions::kRewriteCss);
const char kCss[] = "* { display: none; }";
const char kMinCss[] = "*{display:none}";
SetResponseWithDefaultHeaders("a.css", kContentTypeCss, kCss, 100);
GoogleString css_minified_url =
Encode(kTestDomain, RewriteOptions::kCssFilterId,
hasher()->Hash(kMinCss), "a.css", "css");
// Cold load.
EXPECT_TRUE(TryFetchResource(css_minified_url));
// We should have 3 things inserted:
// 1) the source data
// 2) the result
// 3) the rname entry for the result.
int cold_num_inserts = lru_cache()->num_inserts();
EXPECT_EQ(3, cold_num_inserts);
// Warm load. This one should not change the number of inserts at all
EXPECT_TRUE(TryFetchResource(css_minified_url));
EXPECT_EQ(cold_num_inserts, lru_cache()->num_inserts());
EXPECT_EQ(0, lru_cache()->num_identical_reinserts());
ClearStats();
int64 now_ms = timer()->NowMs();
options()->ClearSignatureForTesting();
// Set cache invalidation (to now) for output URL. Original input URL is not
// affected. Does not invalidate any metadata (the last 'true' argument
// below).
options()->AddUrlCacheInvalidationEntry(css_minified_url, now_ms, true);
options()->ComputeSignature();
EXPECT_TRUE(TryFetchResource(css_minified_url));
// We expect: identical rewrite entry and output.
EXPECT_EQ(0, lru_cache()->num_inserts());
EXPECT_EQ(2, lru_cache()->num_identical_reinserts());
}
TEST_F(RewriteDriverTest, TestCacheUseWithOriginalUrlInvalidation) {
AddFilter(RewriteOptions::kRewriteCss);
const char kCss[] = "* { display: none; }";
const char kMinCss[] = "*{display:none}";
SetResponseWithDefaultHeaders("a.css", kContentTypeCss, kCss, 100);
GoogleString css_minified_url =
Encode(kTestDomain, RewriteOptions::kCssFilterId,
hasher()->Hash(kMinCss), "a.css", "css");
// Cold load.
EXPECT_TRUE(TryFetchResource(css_minified_url));
// We should have 3 things inserted:
// 1) the source data
// 2) the result
// 3) the rname entry for the result.
int cold_num_inserts = lru_cache()->num_inserts();
EXPECT_EQ(3, cold_num_inserts);
// Warm load. This one should not change the number of inserts at all
EXPECT_TRUE(TryFetchResource(css_minified_url));
EXPECT_EQ(cold_num_inserts, lru_cache()->num_inserts());
EXPECT_EQ(0, lru_cache()->num_identical_reinserts());
ClearStats();
int64 now_ms = timer()->NowMs();
options()->ClearSignatureForTesting();
// Set cache invalidation (to now) for input URL. Rewritten output URL is not
// affected. So there will be no cache inserts or reinserts.
// Note: Whether we invalidate all metadata (the last argument below) is
// immaterial in this test.
options()->AddUrlCacheInvalidationEntry("http://test.com/a.css", now_ms,
false);
options()->ComputeSignature();
EXPECT_TRUE(TryFetchResource(css_minified_url));
EXPECT_EQ(0, lru_cache()->num_inserts());
EXPECT_EQ(0, lru_cache()->num_identical_reinserts());
}
// Similar to TestCacheUse, but with cache-extender which reconstructs on the
// fly.
TEST_F(RewriteDriverTest, TestCacheUseOnTheFly) {
AddFilter(RewriteOptions::kExtendCacheCss);
const char kCss[] = "* { display: none; }";
SetResponseWithDefaultHeaders("a.css", kContentTypeCss, kCss, 100);
GoogleString cache_extended_url =
Encode(kTestDomain, RewriteOptions::kCacheExtenderId,
hasher()->Hash(kCss), "a.css", "css");
// Cold load.
EXPECT_TRUE(TryFetchResource(cache_extended_url));
// We should have 2 things inserted:
// 1) the source data
// 2) the rname entry for the result (only in sync)
int cold_num_inserts = lru_cache()->num_inserts();
EXPECT_EQ(2, cold_num_inserts);
// Warm load. This one does a read-check to avoid a re-inserts in the
// rname entry.
EXPECT_TRUE(TryFetchResource(cache_extended_url));
EXPECT_EQ(cold_num_inserts, lru_cache()->num_inserts());
EXPECT_EQ(2, lru_cache()->num_hits());
EXPECT_EQ(0, lru_cache()->num_identical_reinserts());
}
// Verifies that the computed rewrite delay agrees with expectations
// depending on the configuration of constituent delay variables.
TEST_F(RewriteDriverTest, TestComputeCurrentFlushWindowRewriteDelayMs) {
options()->set_rewrite_deadline_ms(1000);
// "Start" a parse to configure the start time in the driver.
ASSERT_TRUE(rewrite_driver()->StartParseId("http://site.com/",
"compute_flush_window_test",
kContentTypeHtml));
// The per-page deadline is initially unconfigured.
EXPECT_EQ(1000, GetFlushTimeout());
// If the per-page deadline is less than the per-flush window timeout,
// the per-page deadline is returned.
rewrite_driver()->set_max_page_processing_delay_ms(500);
EXPECT_EQ(500, GetFlushTimeout());
// If the per-page deadline exceeds the per-flush window timeout, the flush
// timeout is returned.
rewrite_driver()->set_max_page_processing_delay_ms(1750);
EXPECT_EQ(1000, GetFlushTimeout());
// If we advance mock time to leave less than a flush window timeout remaining
// against the page deadline, the appropriate page deadline difference is
// returned.
SetTimeMs(start_time_ms() + 1000);
EXPECT_EQ(750, GetFlushTimeout()); // 1750 - 1000
// If we advance mock time beyond the per-page limit, a value of 1 is
// returned. (This is required since values <= 0 are interpreted by internal
// timeout functions as unlimited.)
SetTimeMs(start_time_ms() + 2000);
EXPECT_EQ(1, GetFlushTimeout());
rewrite_driver()->FinishParse();
}
// Extension of above with cache invalidation.
TEST_F(RewriteDriverTest, TestCacheUseOnTheFlyWithInvalidation) {
AddFilter(RewriteOptions::kExtendCacheCss);
const char kCss[] = "* { display: none; }";
SetResponseWithDefaultHeaders("a.css", kContentTypeCss, kCss, 100);
GoogleString cache_extended_url =
Encode(kTestDomain, RewriteOptions::kCacheExtenderId,
hasher()->Hash(kCss), "a.css", "css");
// Cold load.
EXPECT_TRUE(TryFetchResource(cache_extended_url));
// We should have 2 things inserted:
// 1) the source data
// 2) the rname entry for the result
int cold_num_inserts = lru_cache()->num_inserts();
EXPECT_EQ(2, cold_num_inserts);
// Warm load. This one does a read-check to avoid a re-inserts in the
// rname entry.
EXPECT_TRUE(TryFetchResource(cache_extended_url));
EXPECT_EQ(cold_num_inserts, lru_cache()->num_inserts());
EXPECT_EQ(0, lru_cache()->num_identical_reinserts());
EXPECT_EQ(2, lru_cache()->num_hits());
// Set cache invalidation timestamp (to now, so that response date header is
// in the "past") and load.
ClearStats();
int64 now_ms = timer()->NowMs();
options()->ClearSignatureForTesting();
options()->UpdateCacheInvalidationTimestampMs(now_ms);
options()->ComputeSignature();
EXPECT_TRUE(TryFetchResource(cache_extended_url));
// We expect: input re-insert, new metadata key
EXPECT_EQ(1, lru_cache()->num_inserts());
EXPECT_EQ(1, lru_cache()->num_identical_reinserts());
}
TEST_F(RewriteDriverTest, BaseTags) {
// Starting the parse, the base-tag will be derived from the html url.
ASSERT_TRUE(rewrite_driver()->StartParse("http://example.com/index.html"));
rewrite_driver()->Flush();
EXPECT_EQ("http://example.com/index.html", BaseUrlSpec());
// If we then encounter a base tag, that will become the new base.
rewrite_driver()->ParseText("<base href='http://new.example.com/subdir/'>");
rewrite_driver()->Flush();
EXPECT_EQ(0, message_handler()->TotalMessages());
EXPECT_EQ("http://new.example.com/subdir/", BaseUrlSpec());
// A second base tag will be ignored, and an info message will be printed.
rewrite_driver()->ParseText("<base href=http://second.example.com/subdir2>");
rewrite_driver()->Flush();
EXPECT_EQ(1, message_handler()->TotalMessages());
EXPECT_EQ("http://new.example.com/subdir/", BaseUrlSpec());
// Restart the parse with a new URL and we start fresh.
rewrite_driver()->FinishParse();
ASSERT_TRUE(rewrite_driver()->StartParse(
"http://restart.example.com/index.html"));
rewrite_driver()->Flush();
EXPECT_EQ("http://restart.example.com/index.html", BaseUrlSpec());
// We should be able to reset again.
rewrite_driver()->ParseText("<base href='http://new.example.com/subdir/'>");
rewrite_driver()->Flush();
EXPECT_EQ(1, message_handler()->TotalMessages());
EXPECT_EQ("http://new.example.com/subdir/", BaseUrlSpec());
}
TEST_F(RewriteDriverTest, RelativeBaseTag) {
// Starting the parse, the base-tag will be derived from the html url.
ASSERT_TRUE(rewrite_driver()->StartParse("http://example.com/index.html"));
rewrite_driver()->ParseText("<base href='subdir/'>");
rewrite_driver()->Flush();
EXPECT_EQ(0, message_handler()->TotalMessages());
EXPECT_EQ("http://example.com/subdir/", BaseUrlSpec());
}
TEST_F(RewriteDriverTest, InvalidBaseTag) {
// Encountering an invalid base tag should be ignored (except info message).
ASSERT_TRUE(rewrite_driver()->StartParse("http://example.com/index.html"));
// Note: Even nonsensical protocols must be accepted as base URLs.
rewrite_driver()->ParseText("<base href='slwly:example.com/subdir'>");
rewrite_driver()->Flush();
EXPECT_EQ(0, message_handler()->TotalMessages());
EXPECT_EQ("slwly:example.com/subdir", BaseUrlSpec());
// Reasonable base URLs following that do not change it.
rewrite_driver()->ParseText("<base href='http://example.com/absolute/'>");
rewrite_driver()->Flush();
EXPECT_EQ("slwly:example.com/subdir", BaseUrlSpec());
}
// The TestUrlNamer produces a url like below which is too long.
// http://cdn.com/http/base.example.com/http/unmapped.example.com/dir/test.jpg.pagespeed.xy.#. NOLINT
TEST_F(RewriteDriverTest, CreateOutputResourceTooLongSeparateBase) {
SetUseTestUrlNamer(true);
OutputResourcePtr resource;
GoogleString failure_reason;
options()->set_max_url_size(94);
resource.reset(rewrite_driver()->CreateOutputResourceWithPath(
"http://mapped.example.com/dir/",
"http://unmapped.example.com/dir/",
"http://base.example.com/dir/",
"xy",
"test.jpg",
kRewrittenResource,
&failure_reason));
EXPECT_TRUE(NULL == resource.get());
EXPECT_EQ("Rewritten URL too long: http://cdn.com/http/base.example.com/"
"http/unmapped.example.com/dir/test.jpg.pagespeed.xy.#.",
failure_reason);
failure_reason = "";
options()->set_max_url_size(95);
resource.reset(rewrite_driver()->CreateOutputResourceWithPath(
"http://mapped.example.com/dir/",
"http://unmapped.example.com/dir/",
"http://base.example.com/dir/",
"xy",
"test.jpg",
kRewrittenResource,
&failure_reason));
EXPECT_TRUE(NULL != resource.get());
EXPECT_EQ("", failure_reason);
}
TEST_F(RewriteDriverTest, CreateOutputResourceTooLong) {
const OutputResourceKind resource_kinds[] = {
kRewrittenResource,
kOnTheFlyResource,
kOutlinedResource,
};
// short_path.size() < options()->max_url_size() < long_path.size()
GoogleString short_path = "http://www.example.com/dir/";
GoogleString long_path = short_path;
for (int i = 0; 2 * i < options()->max_url_size(); ++i) {
long_path += "z/";
}
// short_name.size() < options()->max_url_segment_size() < long_name.size()
GoogleString short_name = "foo.css";
GoogleString long_name =
StrCat("foo.css?",
GoogleString(options()->max_url_segment_size() + 1, 'z'));
GoogleString dummy_filter_id = "xy";
OutputResourcePtr resource;
GoogleString failure_reason;
for (int k = 0; k < arraysize(resource_kinds); ++k) {
failure_reason = "";
// Short name should always succeed at creating new resource.
resource.reset(rewrite_driver()->CreateOutputResourceWithPath(
short_path, dummy_filter_id, short_name, resource_kinds[k],
&failure_reason));
EXPECT_TRUE(NULL != resource.get());
EXPECT_EQ("", failure_reason);
failure_reason = "";
// Long leaf-name should always fail at creating new resource.
resource.reset(rewrite_driver()->CreateOutputResourceWithPath(
short_path, dummy_filter_id, long_name, resource_kinds[k],
&failure_reason));
EXPECT_TRUE(NULL == resource.get());
EXPECT_EQ("Rewritten URL segment too long.", failure_reason);
failure_reason = "";
// Long total URL length should always fail at creating new resource.
resource.reset(rewrite_driver()->CreateOutputResourceWithPath(
long_path, dummy_filter_id, short_name, resource_kinds[k],
&failure_reason));
EXPECT_TRUE(NULL == resource.get());
EXPECT_EQ(StrCat("Rewritten URL too long: ", long_path, short_name,
".pagespeed.xy.#."),
failure_reason);
}
}
TEST_F(RewriteDriverTest, MultipleDomains) {
rewrite_driver()->AddFilters();
// Make sure we authorize domains for resources properly. This is a regression
// test for where loading things from a domain would prevent loads from an
// another domain from the same RewriteDriver.
const char kCss[] = "* { display: none; }";
const char kAltDomain[] = "http://www.example.co.uk/";
SetResponseWithDefaultHeaders(StrCat(kTestDomain, "a.css"), kContentTypeCss,
kCss, 100);
SetResponseWithDefaultHeaders(StrCat(kAltDomain, "b.css"), kContentTypeCss,
kCss, 100);
GoogleString rewritten1 = Encode(kTestDomain,
RewriteOptions::kCacheExtenderId,
hasher()->Hash(kCss), "a.css", "css");
GoogleString rewritten2 = Encode(kAltDomain, RewriteOptions::kCacheExtenderId,
hasher()->Hash(kCss), "b.css", "css");
EXPECT_TRUE(TryFetchResource(rewritten1));
ClearRewriteDriver();
EXPECT_TRUE(TryFetchResource(rewritten2));
}
TEST_F(RewriteDriverTest, ResourceCharset) {
// Make sure we properly pick up the charset into a resource on read.
const char kUrl[] = "http://www.example.com/foo.css";
ResponseHeaders resource_headers;
SetDefaultLongCacheHeaders(&kContentTypeCss, &resource_headers);
resource_headers.Replace(HttpAttributes::kContentType,
"text/css; charset=koi8-r");
const char kContents[] = "\xF5\xD2\xC1!"; // Ура!
SetFetchResponse(kUrl, resource_headers, kContents);
// We do this twice to make sure the cached version is OK, too.
for (int round = 0; round < 2; ++round) {
ResourcePtr resource(
rewrite_driver()->CreateInputResourceAbsoluteUncheckedForTestsOnly(
kUrl));
MockResourceCallback mock_callback(resource, factory()->thread_system());
ASSERT_TRUE(resource.get() != NULL);
resource->LoadAsync(Resource::kReportFailureIfNotCacheable,
rewrite_driver()->request_context(),
&mock_callback);
EXPECT_TRUE(mock_callback.done());
EXPECT_TRUE(mock_callback.success());
EXPECT_EQ(kContents, resource->ExtractUncompressedContents());
ASSERT_TRUE(resource->type() != NULL);
EXPECT_EQ(ContentType::kCss, resource->type()->type());
EXPECT_STREQ("koi8-r", resource->charset());
}
}
// Test caching behavior for normal UrlInputResources.
// This is the base case that LoadResourcesFromFiles below contrasts with.
TEST_F(RewriteDriverTest, LoadResourcesFromTheWeb) {
rewrite_driver()->AddFilters();
const char kStaticUrlPrefix[] = "http://www.example.com/";
const char kResourceName[ ]= "foo.css";
GoogleString resource_url = StrCat(kStaticUrlPrefix, kResourceName);
const char kResourceContents1[] = "body { background: red; }";
const char kResourceContents2[] = "body { background: blue; }";
ResponseHeaders resource_headers;
// This sets 1 year cache lifetime :/ TODO(sligocki): Shorten this.
SetDefaultLongCacheHeaders(&kContentTypeCss, &resource_headers);
// Clear the Etag and Last-Modified headers since this
// SetDefaultLongCacheHeaders sets their value to constants which don't change
// when their value is updated.
resource_headers.RemoveAll(HttpAttributes::kEtag);
resource_headers.RemoveAll(HttpAttributes::kLastModified);
// Set the fetch value.
SetFetchResponse(resource_url, resource_headers, kResourceContents1);
// Make sure file can be loaded. Note this cannot be loaded through the
// mock_url_fetcher, because it has not been set in that fetcher.
ResourcePtr resource(
rewrite_driver()->CreateInputResourceAbsoluteUncheckedForTestsOnly(
resource_url));
MockResourceCallback mock_callback(resource, factory()->thread_system());
ASSERT_TRUE(resource.get() != NULL);
resource->LoadAsync(Resource::kReportFailureIfNotCacheable,
rewrite_driver()->request_context(),
&mock_callback);
EXPECT_TRUE(mock_callback.done());
EXPECT_TRUE(mock_callback.success());
EXPECT_EQ(kResourceContents1, resource->ExtractUncompressedContents());
// TODO(sligocki): Check it was cached.
// Change the fetch value.
SetFetchResponse(resource_url, resource_headers, kResourceContents2);
// Check that the resource loads cached.
ResourcePtr resource2(
rewrite_driver()->CreateInputResourceAbsoluteUncheckedForTestsOnly(
resource_url));
MockResourceCallback mock_callback2(resource2, factory()->thread_system());
ASSERT_TRUE(resource2.get() != NULL);
resource2->LoadAsync(Resource::kReportFailureIfNotCacheable,
rewrite_driver()->request_context(),
&mock_callback2);
EXPECT_TRUE(mock_callback2.done());
EXPECT_TRUE(mock_callback2.success());
EXPECT_EQ(kResourceContents1, resource2->ExtractUncompressedContents());
// Advance timer and check that the resource loads updated.
AdvanceTimeMs(10 * Timer::kYearMs);
// Check that the resource loads updated.
ResourcePtr resource3(
rewrite_driver()->CreateInputResourceAbsoluteUncheckedForTestsOnly(
resource_url));
MockResourceCallback mock_callback3(resource3, factory()->thread_system());
ASSERT_TRUE(resource3.get() != NULL);
resource3->LoadAsync(Resource::kReportFailureIfNotCacheable,
rewrite_driver()->request_context(),
&mock_callback3);
EXPECT_TRUE(mock_callback3.done());
EXPECT_EQ(kResourceContents2, resource3->ExtractUncompressedContents());
}
// Test that we successfully load specified resources from files and that
// file resources have the appropriate properties, such as being loaded from
// file every time they are fetched (not being cached).
TEST_F(RewriteDriverTest, LoadResourcesFromFiles) {
rewrite_driver()->AddFilters();
const char kStaticUrlPrefix[] = "http://www.example.com/static/";
const char kStaticFilenamePrefix[] = "/htmlcontent/static/";
const char kResourceName[ ]= "foo.css";
GoogleString resource_filename = StrCat(kStaticFilenamePrefix, kResourceName);
GoogleString resource_url = StrCat(kStaticUrlPrefix, kResourceName);
const char kResourceContents1[] = "body { background: red; }";
const char kResourceContents2[] = "body { background: blue; }";
// Tell RewriteDriver to associate static URLs with filenames.
options()->file_load_policy()->Associate(kStaticUrlPrefix,
kStaticFilenamePrefix);
// Write a file.
WriteFile(resource_filename.c_str(), kResourceContents1);
// Make sure file can be loaded. Note this cannot be loaded through the
// mock_url_fetcher, because it has not been set in that fetcher.
ResourcePtr resource(
rewrite_driver()->CreateInputResourceAbsoluteUncheckedForTestsOnly(
resource_url));
ASSERT_TRUE(resource.get() != NULL);
EXPECT_EQ(&kContentTypeCss, resource->type());
MockResourceCallback mock_callback(resource, factory()->thread_system());
resource->LoadAsync(Resource::kReportFailureIfNotCacheable,
rewrite_driver()->request_context(),
&mock_callback);
EXPECT_TRUE(mock_callback.done());
EXPECT_TRUE(mock_callback.success());
EXPECT_EQ(kResourceContents1, resource->ExtractUncompressedContents());
// TODO(sligocki): Check it wasn't cached.
// Change the file.
WriteFile(resource_filename.c_str(), kResourceContents2);
// Make sure the resource loads updated.
ResourcePtr resource2(
rewrite_driver()->CreateInputResourceAbsoluteUncheckedForTestsOnly(
resource_url));
ASSERT_TRUE(resource2.get() != NULL);
EXPECT_EQ(&kContentTypeCss, resource2->type());
MockResourceCallback mock_callback2(resource2, factory()->thread_system());
resource2->LoadAsync(Resource::kReportFailureIfNotCacheable,
rewrite_driver()->request_context(),
&mock_callback2);
EXPECT_TRUE(mock_callback2.done());
EXPECT_TRUE(mock_callback2.success());
EXPECT_EQ(kResourceContents2, resource2->ExtractUncompressedContents());
}
// Make sure the content-type is set correctly, even for URLs with queries.
// http://code.google.com/p/modpagespeed/issues/detail?id=405
TEST_F(RewriteDriverTest, LoadResourcesContentType) {
rewrite_driver()->AddFilters();
// Tell RewriteDriver to associate static URLs with filenames.
options()->file_load_policy()->Associate("http://www.example.com/static/",
"/htmlcontent/static/");
// Write file with readable extension.
WriteFile("/htmlcontent/foo.js", "");
// Load the file with a query param (add .css at the end of the param just
// for optimal trickyness).
ResourcePtr resource(
rewrite_driver()->CreateInputResourceAbsoluteUncheckedForTestsOnly(
"http://www.example.com/static/foo.js?version=2.css"));
EXPECT_TRUE(resource.get() != NULL);
EXPECT_EQ(&kContentTypeJavascript, resource->type());
// Write file with bogus extension.
WriteFile("/htmlcontent/bar.bogus", "");
// Load it normally.
ResourcePtr resource2(
rewrite_driver()->CreateInputResourceAbsoluteUncheckedForTestsOnly(
"http://www.example.com/static/bar.bogus"));
EXPECT_TRUE(resource2.get() != NULL);
EXPECT_TRUE(NULL == resource2->type());
}
TEST_F(RewriteDriverTest, ResolveAnchorUrl) {
rewrite_driver()->AddFilters();
ASSERT_TRUE(rewrite_driver()->StartParse("http://example.com/index.html"));
GoogleUrl resolved(rewrite_driver()->base_url(), "#anchor");
EXPECT_EQ("http://example.com/index.html#anchor", resolved.Spec());
rewrite_driver()->FinishParse();
}
// A rewrite context that's not actually capable of rewriting -- we just need
// one to pass in to InfoAt in test below.
class MockRewriteContext : public SingleRewriteContext {
public:
explicit MockRewriteContext(RewriteDriver* driver) :
SingleRewriteContext(driver, NULL, NULL) {}
virtual void RewriteSingle(const ResourcePtr& input,
const OutputResourcePtr& output) {}
virtual const char* id() const { return "mock"; }
virtual OutputResourceKind kind() const { return kOnTheFlyResource; }
};
TEST_F(RewriteDriverTest, DiagnosticsWithPercent) {
// Regression test for crash in InfoAt where location has %stuff in it.
// (make sure it actually shows up first, though).
int prev_log_level = logging::GetMinLogLevel();
logging::SetMinLogLevel(logging::LOG_INFO);
rewrite_driver()->AddFilters();
MockRewriteContext context(rewrite_driver());
ResourcePtr resource(
rewrite_driver()->CreateInputResourceAbsoluteUncheckedForTestsOnly(
"http://www.example.com/%s%s%s%d%f"));
ResourceSlotPtr slot(new FetchResourceSlot(resource));
context.AddSlot(slot);
rewrite_driver()->InfoAt(&context, "Just a test");
logging::SetMinLogLevel(prev_log_level);
}
// Tests that we reject https URLs quickly.
TEST_F(RewriteDriverTest, RejectHttpsQuickly) {
// Need to expressly authorize https even though we don't support it.
options()->WriteableDomainLawyer()->AddDomain("https://*/",
message_handler());
AddFilter(RewriteOptions::kRewriteJavascriptExternal);
// When we don't support https then we fail quickly and cleanly.
factory()->mock_url_async_fetcher()->set_fetcher_supports_https(false);
ValidateNoChanges("reject_https_quickly",
"<script src='https://example.com/a.js'></script>");
EXPECT_EQ(0, counting_url_async_fetcher()->fetch_count());
// When we do support https the fetcher fails to find the resource.
factory()->mock_url_async_fetcher()->set_fetcher_supports_https(true);
SetFetchResponse404("https://example.com/a.js");
ValidateNoChanges("reject_https_quickly",
"<script src='https://example.com/a.js'></script>");
EXPECT_EQ(1, counting_url_async_fetcher()->fetch_count());
EXPECT_EQ(0, counting_url_async_fetcher()->failure_count());
}
// Test that CreateInputResource doesn't crash when handed a data url.
// This was causing a query of death in some circumstances.
TEST_F(RewriteDriverTest, RejectDataResourceGracefully) {
MockRewriteContext context(rewrite_driver());
GoogleUrl dataUrl("data:");
bool is_authorized;
ResourcePtr resource(rewrite_driver()->CreateInputResource(dataUrl,
&is_authorized));
EXPECT_TRUE(resource.get() == NULL);
EXPECT_TRUE(is_authorized);
}
// Test that when inline_unauthorized_resources is set to false (the default
// case), no resources are created for unauthorized resources, but authorized
// ones are created with the right cache-key.
TEST_F(RewriteDriverTest, NoCreateInputResourceUnauthorized) {
MockRewriteContext context(rewrite_driver());
// Call StartParseUrl so that the base_url gets set to a non-empty string.
ASSERT_TRUE(rewrite_driver()->StartParse("http://example.com/index.html"));
// Test that an unauthorized resource is not allowed to be created.
GoogleUrl unauthorized_url("http://unauthorized.domain.com/a.js");
bool is_authorized;
ResourcePtr resource(rewrite_driver()->CreateInputResource(unauthorized_url,
&is_authorized));
EXPECT_TRUE(resource.get() == NULL);
EXPECT_FALSE(is_authorized);
// Test that an authorized resource is created with the right cache key even
// if the filter allows unauthorized domains.
GoogleUrl authorized_url("http://example.com/a.js");
ResourcePtr resource2(rewrite_driver()->CreateInputResource(
authorized_url,
RewriteDriver::kInlineUnauthorizedResources,
RewriteDriver::kIntendedForGeneral,
&is_authorized));
EXPECT_TRUE(resource2.get() != NULL);
EXPECT_TRUE(is_authorized);
EXPECT_STREQ(authorized_url.spec_c_str(), resource2->url());
EXPECT_STREQ(authorized_url.spec_c_str(), resource2->cache_key());
}
// Test that when inline_unauthorized_resources is set to true, resources
// are created for unauthorized resources with the correctly prefixed keys, and
// the authorized resources continue to get created with the right cache-keys.
TEST_F(RewriteDriverTest, CreateInputResourceUnauthorized) {
options()->AddInlineUnauthorizedResourceType(semantic_type::kScript);
MockRewriteContext context(rewrite_driver());
// Call StartParseUrl so that the base_url gets set to a non-empty string.
ASSERT_TRUE(rewrite_driver()->StartParse("http://example.com/index.html"));
// Test that an unauthorized resource is created with the right cache key.
GoogleUrl unauthorized_url("http://unauthorized.domain.com/a.js");
bool is_authorized;
ResourcePtr resource(rewrite_driver()->CreateInputResource(
unauthorized_url,
RewriteDriver::kInlineUnauthorizedResources,
RewriteDriver::kIntendedForGeneral,
&is_authorized));
EXPECT_TRUE(resource.get() != NULL);
EXPECT_FALSE(is_authorized);
EXPECT_STREQ(unauthorized_url.spec_c_str(), resource->url());
EXPECT_STREQ("unauth://unauthorized.domain.com/a.js", resource->cache_key());
// Test that an authorized resource continues to be created with the right
// cache key.
GoogleUrl authorized_url("http://example.com/a.js");
ResourcePtr resource2(rewrite_driver()->CreateInputResource(
authorized_url,
RewriteDriver::kInlineUnauthorizedResources,
RewriteDriver::kIntendedForGeneral,
&is_authorized));
EXPECT_TRUE(resource2.get() != NULL);
EXPECT_TRUE(is_authorized);
EXPECT_STREQ(authorized_url.spec_c_str(), resource2->url());
EXPECT_STREQ(authorized_url.spec_c_str(), resource2->cache_key());
// Test that an unauthorized resource is not created if
// allow_unauthorized_domain is false.
ResourcePtr resource3(rewrite_driver()->CreateInputResource(
unauthorized_url,
RewriteDriver::kInlineOnlyAuthorizedResources,
RewriteDriver::kIntendedForGeneral,
&is_authorized));
EXPECT_TRUE(resource3.get() == NULL);
EXPECT_FALSE(is_authorized);
// Test that an unauthorized resource is not created with the default
// CreateInputResource call.
ResourcePtr resource4(
rewrite_driver()->CreateInputResource(unauthorized_url, &is_authorized));
EXPECT_TRUE(resource4.get() == NULL);
EXPECT_FALSE(is_authorized);
}
// Test that when inline_unauthorized_resources is set to true, unauthorized
// resources continue to be not created when they match a disallowed pattern.
TEST_F(RewriteDriverTest, CreateInputResourceUnauthorizedWithDisallow) {
options()->AddInlineUnauthorizedResourceType(semantic_type::kScript);
options()->Disallow("http://unauthorized.domain.com/*");
MockRewriteContext context(rewrite_driver());
// Call StartParseUrl so that the base_url gets set to a non-empty string.
ASSERT_TRUE(rewrite_driver()->StartParse("http://example.com/index.html"));
// Test that an unauthorized resource is not created when it is disallowed.
GoogleUrl unauthorized_url("http://unauthorized.domain.com/a.js");
bool is_authorized;
ResourcePtr resource(rewrite_driver()->CreateInputResource(
unauthorized_url,
RewriteDriver::kInlineUnauthorizedResources,
RewriteDriver::kIntendedForGeneral,
&is_authorized));
EXPECT_TRUE(resource.get() == NULL);
EXPECT_FALSE(is_authorized);
}
// Test AllowWhenInlining overrides Disallow when inlining.
TEST_F(RewriteDriverTest, AllowWhenInliningOverridesDisallow) {
options()->AllowOnlyWhenInlining("*a.js*");
MockRewriteContext context(rewrite_driver());
// Call StartParseUrl so that the base_url gets set to a non-empty string.
ASSERT_TRUE(rewrite_driver()->StartParse("http://example.com/index.html"));
// This resource would normally not be created because it is disallowed,
// except that we explicitly allowed it with AllowWhenInlining.
GoogleUrl js_url("http://example.com/a.js");
bool is_authorized;
ResourcePtr resource(rewrite_driver()->CreateInputResource(
js_url,
RewriteDriver::kInlineUnauthorizedResources,
RewriteDriver::kIntendedForInlining,
&is_authorized));
EXPECT_FALSE(resource.get() == NULL);
EXPECT_TRUE(is_authorized);
}
// Test AllowWhenInlining fails to overrides Disallow when not inlining.
TEST_F(RewriteDriverTest, AllowWhenInliningDoesntOverrideDisallow) {
options()->AllowOnlyWhenInlining("*a.js*");
MockRewriteContext context(rewrite_driver());
// Call StartParseUrl so that the base_url gets set to a non-empty string.
ASSERT_TRUE(rewrite_driver()->StartParse("http://example.com/index.html"));
// This resource would normally not be created because it is disallowed, and
// AllowWhenInlining doesn't apply because we're not inlining.
GoogleUrl js_url("http://example.com/a.js");
bool is_authorized;
ResourcePtr resource(rewrite_driver()->CreateInputResource(
js_url,
RewriteDriver::kInlineUnauthorizedResources,
RewriteDriver::kIntendedForGeneral,
&is_authorized));
EXPECT_TRUE(resource.get() == NULL);
EXPECT_FALSE(is_authorized);
}
class ResponseHeadersCheckingFilter : public EmptyHtmlFilter {
public:
explicit ResponseHeadersCheckingFilter(RewriteDriver* driver)
: driver_(driver),
flush_occurred_(false) {
}
void CheckAccess() {
EXPECT_TRUE(driver_->response_headers() != NULL);
if (flush_occurred_) {
EXPECT_TRUE(driver_->mutable_response_headers() == NULL);
} else {
EXPECT_EQ(driver_->mutable_response_headers(),
driver_->response_headers());
}
}
virtual void StartDocument() {
flush_occurred_ = false;
CheckAccess();
}
virtual void Flush() {
CheckAccess(); // We still can access the mutable headers during Flush.
flush_occurred_ = true;
}
virtual void StartElement(HtmlElement* element) { CheckAccess(); }
virtual void EndElement(HtmlElement* element) { CheckAccess(); }
virtual void EndDocument() { CheckAccess(); }
virtual const char* Name() const { return "ResponseHeadersCheckingFilter"; }
private:
RewriteDriver* driver_;
bool flush_occurred_;
};
class DetermineEnabledCheckingFilter : public EmptyHtmlFilter {
public:
DetermineEnabledCheckingFilter() :
start_document_called_(false),
enabled_value_(false) {}
virtual void StartDocument() {
start_document_called_ = true;
}
virtual void DetermineEnabled(GoogleString* disabled_reason) {
set_is_enabled(enabled_value_);
}
void SetEnabled(bool enabled_value) {
enabled_value_ = enabled_value;
}
bool start_document_called() {
return start_document_called_;
}
virtual const char* Name() const { return "DetermineEnabledCheckingFilter"; }
private:
bool start_document_called_;
bool enabled_value_;
};
TEST_F(RewriteDriverTest, DetermineEnabledTest) {
RewriteDriver* driver = rewrite_driver();
DetermineEnabledCheckingFilter* filter =
new DetermineEnabledCheckingFilter();
driver->AddOwnedEarlyPreRenderFilter(filter);
driver->StartParse("http://example.com/index.html");
rewrite_driver()->ParseText("<div>");
driver->Flush();
EXPECT_FALSE(filter->start_document_called());
rewrite_driver()->ParseText("</div>");
driver->FinishParse();
filter = new DetermineEnabledCheckingFilter();
filter->SetEnabled(true);
driver->AddOwnedEarlyPreRenderFilter(filter);
driver->StartParse("http://example.com/index.html");
rewrite_driver()->ParseText("<div>");
driver->Flush();
EXPECT_TRUE(filter->start_document_called());
rewrite_driver()->ParseText("</div>");
driver->FinishParse();
}
// Tests that we access driver->response_headers() before/after Flush(),
// and driver->mutable_response_headers() at only before Flush().
TEST_F(RewriteDriverTest, ResponseHeadersAccess) {
RewriteDriver* driver = rewrite_driver();
ResponseHeaders headers;
driver->set_response_headers_ptr(&headers);
driver->AddOwnedEarlyPreRenderFilter(new ResponseHeadersCheckingFilter(
driver));
driver->AddOwnedPostRenderFilter(new ResponseHeadersCheckingFilter(driver));
// Starting the parse, the base-tag will be derived from the html url.
ASSERT_TRUE(driver->StartParse("http://example.com/index.html"));
rewrite_driver()->ParseText("<div>");
driver->Flush();
rewrite_driver()->ParseText("</div>");
driver->FinishParse();
}
TEST_F(RewriteDriverTest, SetSessionFetcherTest) {
AddFilter(RewriteOptions::kExtendCacheCss);
const char kFetcher1Css[] = "Fetcher #1";
const char kFetcher2Css[] = "Fetcher #2";
SetResponseWithDefaultHeaders("a.css", kContentTypeCss, kFetcher1Css, 100);
GoogleString url = Encode(kTestDomain, RewriteOptions::kCacheExtenderId,
hasher()->Hash(kFetcher1Css), "a.css", "css");
// Fetch from default.
GoogleString output;
ResponseHeaders response_headers;
EXPECT_TRUE(FetchResourceUrl(url, &output, &response_headers));
EXPECT_STREQ(kFetcher1Css, output);
EXPECT_EQ(1, counting_url_async_fetcher()->fetch_count());
// Load up a different file into a second fetcher.
// We misappropriate the response_headers from previous fetch for simplicity.
scoped_ptr<MockUrlFetcher> mock2(new MockUrlFetcher);
mock2->SetResponse(AbsolutifyUrl("a.css"), response_headers, kFetcher2Css);
// Switch over to new fetcher, making sure to set two of them to exercise
// memory management. Note the synchronous mock fetcher we still have to
// manage ourselves (as the RewriteDriver API is for async ones only).
RewriteDriver* driver = rewrite_driver();
driver->SetSessionFetcher(mock2.release());
CountingUrlAsyncFetcher* counter =
new CountingUrlAsyncFetcher(driver->async_fetcher());
driver->SetSessionFetcher(counter);
EXPECT_EQ(counter, driver->async_fetcher());
// Note that FetchResourceUrl will call driver->Clear() so we cannot
// access 'counter' past this point.
lru_cache()->Clear(); // get rid of cached version of input
EXPECT_TRUE(FetchResourceUrl(url, &output, &response_headers));
EXPECT_STREQ(kFetcher2Css, output);
EXPECT_EQ(1, counting_url_async_fetcher()->fetch_count());
// As FetchResourceUrl has cleared the driver, further fetcher should
// grab fetcher 1 version.
lru_cache()->Clear(); // get rid of cached version of input
EXPECT_TRUE(FetchResourceUrl(url, &output, &response_headers));
EXPECT_STREQ(kFetcher1Css, output);
EXPECT_EQ(2, counting_url_async_fetcher()->fetch_count());
}
class WaitAsyncFetch : public StringAsyncFetch {
public:
WaitAsyncFetch(const RequestContextPtr& req, GoogleString* content,
ThreadSystem* thread_system)
: StringAsyncFetch(req, content),
sync_(thread_system) {
}
virtual ~WaitAsyncFetch() {}
virtual void HandleDone(bool status) {
StringAsyncFetch::HandleDone(status);
sync_.Notify();
}
void Wait() { sync_.Wait(); }
private:
WorkerTestBase::SyncPoint sync_;
};
class InPlaceTest : public RewriteTestBase {
protected:
InPlaceTest() {}
virtual ~InPlaceTest() {}
bool FetchInPlaceResource(const StringPiece& url,
bool proxy_mode,
GoogleString* content,
ResponseHeaders* response) {
GoogleUrl gurl(url);
content->clear();
WaitAsyncFetch async_fetch(CreateRequestContext(), content,
server_context()->thread_system());
async_fetch.set_response_headers(response);
rewrite_driver_->SetRequestHeaders(*async_fetch.request_headers());
rewrite_driver_->FetchInPlaceResource(gurl, proxy_mode,
&async_fetch);
async_fetch.Wait();
// Make sure we let the rewrite complete, and also wait for the driver to be
// idle so we can reuse it safely.
rewrite_driver_->WaitForShutDown();
ClearRewriteDriver(); // makes sure to re-create the request context.
EXPECT_TRUE(async_fetch.done());
return async_fetch.done() && async_fetch.success();
}
bool TryFetchInPlaceResource(const StringPiece& url,
bool proxy_mode) {
GoogleString contents;
ResponseHeaders response;
return FetchInPlaceResource(url, proxy_mode, &contents, &response);
}
private:
DISALLOW_COPY_AND_ASSIGN(InPlaceTest);
};
TEST_F(InPlaceTest, FetchInPlaceResource) {
AddFilter(RewriteOptions::kRewriteCss);
GoogleString url = "http://example.com/foo.css";
SetResponseWithDefaultHeaders(url, kContentTypeCss,
".a { color: red; }", 100);
GoogleString html_url = "http://example.com/foo.html";
SetResponseWithDefaultHeaders(html_url, kContentTypeHtml,
"<b>Bold!</b>", 100);
// This will fail because cache is empty and we are not allowing HTTP fetch.
EXPECT_FALSE(TryFetchInPlaceResource(url, false /* proxy_mode */));
EXPECT_EQ(0, http_cache()->cache_hits()->Get());
EXPECT_EQ(1, http_cache()->cache_misses()->Get());
EXPECT_EQ(0, http_cache()->cache_inserts()->Get());
EXPECT_EQ(0, counting_url_async_fetcher()->fetch_count());
ClearStats();
// Now we allow HTTP fetches and we expect success.
EXPECT_TRUE(TryFetchInPlaceResource(url, true /* proxy_mode */));
EXPECT_EQ(0, http_cache()->cache_hits()->Get());
EXPECT_EQ(1, http_cache()->cache_misses()->Get());
// We insert both original and rewritten resources.
EXPECT_EQ(2, http_cache()->cache_inserts()->Get());
EXPECT_EQ(1, counting_url_async_fetcher()->fetch_count());
ClearStats();
// Now that we've loaded the resource into cache, we expect success.
EXPECT_TRUE(TryFetchInPlaceResource(url, false /* proxy_mode */));
EXPECT_EQ(1, http_cache()->cache_hits()->Get());
EXPECT_EQ(0, http_cache()->cache_misses()->Get());
EXPECT_EQ(0, http_cache()->cache_inserts()->Get());
EXPECT_EQ(0, counting_url_async_fetcher()->fetch_count());
ClearStats();
// In proxy mode, we should successfully pass through HTML.
EXPECT_TRUE(TryFetchInPlaceResource(html_url, true /* proxy_mode */));
// In non-proxy mode producing HTML should fail; it's expected the origin
// server would produce things we aren't optimizing through its usual
// code paths. Note that this needs to happen after the previous call so that
// we get the resource into cache.
EXPECT_FALSE(TryFetchInPlaceResource(html_url, false /* proxy_mode */));
}
TEST_F(InPlaceTest, InPlaceCssDebug) {
// Regression test: ipro + debug would crash when a debug message was
// produced.
options()->EnableFilter(RewriteOptions::kDebug);
options()->EnableFilter(RewriteOptions::kFlattenCssImports);
AddFilter(RewriteOptions::kRewriteCss);
GoogleString url = "http://example.com/foo.css";
SetResponseWithDefaultHeaders(url, kContentTypeCss,
"@import \"weird://foo\"; .a { color: red; }",
100);
EXPECT_TRUE(TryFetchInPlaceResource(url, true /* proxy_mode */));
}
TEST_F(RewriteDriverTest, DebugModeTest) {
// Verify that DebugMode() corresponds to RewriteOptions::kDebug as expected
EXPECT_FALSE(rewrite_driver()->DebugMode());
options()->EnableFilter(RewriteOptions::kDebug);
EXPECT_TRUE(rewrite_driver()->DebugMode());
options()->DisableFilter(RewriteOptions::kDebug);
EXPECT_FALSE(rewrite_driver()->DebugMode());
}
TEST_F(RewriteDriverTest, CachePollutionWithWrongEncodingCharacter) {
AddFilter(RewriteOptions::kRewriteCss);
const char kCss[] = "* { display: none; }";
SetResponseWithDefaultHeaders("dir/a.css", kContentTypeCss, kCss, 100);
GoogleString css_wrong_url =
"http://test.com/dir/B.a.css.pagespeed.cf.0.css";
GoogleString correct_url = Encode(
"dir/", RewriteOptions::kCssFilterId, hasher()->Hash(kCss),
"a.css", "css");
// Cold load.
EXPECT_TRUE(TryFetchResource(css_wrong_url));
// We should have 3 things inserted:
// 1) the source data
// 2) the result
// 3) the rname entry for the result
int cold_num_inserts = lru_cache()->num_inserts();
EXPECT_EQ(3, cold_num_inserts);
EXPECT_EQ(kFoundResult,
HttpBlockingFindStatus(StrCat(kTestDomain, correct_url),
http_cache()));
GoogleString input_html(CssLinkHref("dir/a.css"));
GoogleString output_html(CssLinkHref(correct_url));
ValidateExpected("wrong_encoding", input_html, output_html);
}
TEST_F(RewriteDriverTest, CachePollutionWithLowerCasedncodingCharacter) {
AddFilter(RewriteOptions::kRewriteCss);
const char kCss[] = "* { display: none; }";
SetResponseWithDefaultHeaders("dir/a.css", kContentTypeCss, kCss, 100);
GoogleString css_wrong_url =
"http://test.com/dir/a.a.css.pagespeed.cf.0.css";
GoogleString correct_url = Encode(
"dir/", RewriteOptions::kCssFilterId, hasher()->Hash(kCss),
"a.css", "css");
// Cold load.
EXPECT_TRUE(TryFetchResource(css_wrong_url));
// We should have 3 things inserted:
// 1) the source data
// 2) the result
// 3) the rname entry for the result
int cold_num_inserts = lru_cache()->num_inserts();
EXPECT_EQ(3, cold_num_inserts);
EXPECT_EQ(kFoundResult,
HttpBlockingFindStatus(StrCat(kTestDomain, correct_url),
http_cache()));
GoogleString input_html(CssLinkHref("dir/a.css"));
GoogleString output_html(CssLinkHref(correct_url));
ValidateExpected("wrong_encoding", input_html, output_html);
}
TEST_F(RewriteDriverTest, CachePollutionWithExperimentId) {
AddFilter(RewriteOptions::kRewriteCss);
const char kCss[] = "* { display: none; }";
SetResponseWithDefaultHeaders("dir/a.css", kContentTypeCss, kCss, 100);
GoogleString css_wrong_url =
"http://test.com/dir/A.a.css.pagespeed.b.cf.0.css";
GoogleString correct_url = Encode(
"dir/", RewriteOptions::kCssFilterId, hasher()->Hash(kCss),
"a.css", "css");
// Cold load.
EXPECT_TRUE(TryFetchResource(css_wrong_url));
// We should have 3 things inserted:
// 1) the source data
// 2) the result
// 3) the rname entry for the result
int cold_num_inserts = lru_cache()->num_inserts();
EXPECT_EQ(3, cold_num_inserts);
EXPECT_EQ(kFoundResult,
HttpBlockingFindStatus(StrCat(kTestDomain, correct_url),
http_cache()));
GoogleString input_html(CssLinkHref("dir/a.css"));
GoogleString output_html(CssLinkHref(correct_url));
ValidateExpected("wrong_encoding", input_html, output_html);
}
TEST_F(RewriteDriverTest, CachePollutionWithQueryParams) {
AddFilter(RewriteOptions::kRewriteCss);
const char kCss[] = "* { display: none; }";
SetResponseWithDefaultHeaders("dir/a.css?ver=3", kContentTypeCss, kCss, 100);
GoogleString css_wrong_url =
"http://test.com/dir/A.a.css,qver%3D3.pagespeed.cf.0.css";
GoogleString correct_url = Encode(
"dir/", RewriteOptions::kCssFilterId, hasher()->Hash(kCss),
"a.css?ver=3", "css");
// Cold load.
EXPECT_TRUE(TryFetchResource(css_wrong_url));
// We should have 3 things inserted:
// 1) the source data
// 2) the result
// 3) the rname entry for the result
int cold_num_inserts = lru_cache()->num_inserts();
EXPECT_EQ(3, cold_num_inserts);
EXPECT_EQ(kFoundResult,
HttpBlockingFindStatus(StrCat(kTestDomain, correct_url),
http_cache()));
GoogleString input_html(CssLinkHref("dir/a.css?ver=3"));
GoogleString output_html(CssLinkHref(correct_url));
ValidateExpected("wrong_encoding", input_html, output_html);
}
TEST_F(RewriteDriverTest, NoLoggingForImagesRewrittenInsideCss) {
options()->set_image_inline_max_bytes(100000);
options()->EnableFilter(RewriteOptions::kExtendCacheCss);
options()->EnableFilter(RewriteOptions::kRewriteCss);
options()->EnableFilter(RewriteOptions::kExtendCacheImages);
options()->EnableFilter(RewriteOptions::kRecompressPng);
options()->set_always_rewrite_css(true);
rewrite_driver_->AddFilters();
GoogleString contents = "#a {background:url(1.png) ;}";
SetResponseWithDefaultHeaders("a.css", kContentTypeCss, contents, 100);
AddFileToMockFetcher(StrCat(kTestDomain, "1.png"), kBikePngFile,
kContentTypePng, 100);
GoogleString correct_url = Encode(
"", RewriteOptions::kCssFilterId, hasher()->Hash(contents),
"a.css", "css");
GoogleString input_html(CssLinkHref("a.css"));
GoogleString output_html(CssLinkHref(correct_url));
ValidateExpected("no_logging_images_inside_css", input_html, output_html);
LoggingInfo* logging_info = rewrite_driver_->log_record()->logging_info();
ASSERT_EQ(1, logging_info->rewriter_info_size());
EXPECT_EQ("cf", logging_info->rewriter_info(0).id());
}
TEST_F(RewriteDriverTest, DecodeMultiUrlsEncodesCorrectly) {
options()->EnableFilter(RewriteOptions::kRewriteCss);
options()->EnableFilter(RewriteOptions::kCombineCss);
rewrite_driver()->AddFilters();
const char kCss[] = "* { display: none; }";
SetResponseWithDefaultHeaders("a.css", kContentTypeCss, kCss, 100);
SetResponseWithDefaultHeaders("test/b.css", kContentTypeCss, kCss, 100);
// Combine filters
GoogleString multi_url = Encode(
"", RewriteOptions::kCssFilterId, hasher()->Hash(kCss),
"a.css+test,_b.css.pagespeed.cc.0.css", "css");
EXPECT_TRUE(TryFetchResource(StrCat(kTestDomain, multi_url)));
GoogleString input_html(
StrCat(CssLinkHref("a.css"), CssLinkHref("test/b.css")));
ParseUrl(kTestDomain, input_html);
StringVector css_urls;
CollectCssLinks("multi", output_buffer_, &css_urls);
EXPECT_EQ(1UL, css_urls.size());
EXPECT_EQ(multi_url, css_urls[0]);
}
// Records URL of the last img element it sees at point of RenderDone().
class RenderDoneCheckingFilter : public EmptyHtmlFilter {
public:
RenderDoneCheckingFilter() : element_(NULL) {}
virtual ~RenderDoneCheckingFilter() {}
const GoogleString& src() const { return src_; }
protected:
virtual void StartElement(HtmlElement* element) {
if (element->keyword() == HtmlName::kImg) {
element_ = element;
}
}
virtual void RenderDone() {
if (element_ != NULL) {
const char* val = element_->AttributeValue(HtmlName::kSrc);
src_ = (val != NULL ? val : "");
}
}
virtual const char* Name() const { return "RenderDoneCheckingFilter"; }
private:
HtmlElement* element_;
GoogleString src_;
DISALLOW_COPY_AND_ASSIGN(RenderDoneCheckingFilter);
};
TEST_F(RewriteDriverTest, RenderDoneTest) {
// Test to make sure RenderDone sees output of a pre-render filter.
RewriteDriver* driver = rewrite_driver();
RenderDoneCheckingFilter* filter =
new RenderDoneCheckingFilter();
driver->AddOwnedEarlyPreRenderFilter(filter);
SetResponseWithDefaultHeaders("a.png", kContentTypePng, "PNGkinda", 100);
AddFilter(RewriteOptions::kExtendCacheImages);
driver->StartParse(kTestDomain);
rewrite_driver()->ParseText("<img src=\"a.png\">");
driver->FinishParse();
EXPECT_EQ(Encode("", RewriteOptions::kCacheExtenderId, "0", "a.png", "png"),
filter->src());
}
TEST_F(RewriteDriverTest, BlockingRewriteFlagTest) {
RequestHeaders request_headers;
RewriteDriver* driver = rewrite_driver();
options()->ClearSignatureForTesting();
options()->set_blocking_rewrite_key("blocking");
options()->ComputeSignature();
// case 1.
TestBlockingRewrite(&request_headers, false, true);
// case 2.
request_headers.Add(HttpAttributes::kXPsaBlockingRewrite, "not-blocking");
TestBlockingRewrite(&request_headers, false, true);
// case 3.
request_headers.Add(HttpAttributes::kXPsaBlockingRewrite, "blocking");
TestBlockingRewrite(&request_headers, true, true);
// case 4.
request_headers.Add(HttpAttributes::kXPsaBlockingRewrite, "blocking");
request_headers.Add(HttpAttributes::kXPsaBlockingRewriteMode, "junk");
TestBlockingRewrite(&request_headers, true, true);
// case 5.
request_headers.Add(HttpAttributes::kXPsaBlockingRewrite, "blocking");
request_headers.Add(HttpAttributes::kXPsaBlockingRewriteMode, "slow");
TestBlockingRewrite(&request_headers, true, false);
options()->ClearSignatureForTesting();
options()->EnableBlockingRewriteForRefererUrlPattern("http://example.com");
options()->ComputeSignature();
// case 6.
request_headers.Add(HttpAttributes::kReferer, "http://junk.com/");
driver->EnableBlockingRewrite(&request_headers);
TestBlockingRewrite(&request_headers, false, true);
// case 7.
request_headers.RemoveAll(HttpAttributes::kReferer);
request_headers.Add(HttpAttributes::kReferer, "http://example.com");
request_headers.Add(HttpAttributes::kXPsaBlockingRewriteMode, "junk");
TestBlockingRewrite(&request_headers, true, true);
// case 8.
request_headers.RemoveAll(HttpAttributes::kReferer);
request_headers.Add(HttpAttributes::kReferer, "http://example.com");
request_headers.Add(HttpAttributes::kXPsaBlockingRewriteMode, "slow");
TestBlockingRewrite(&request_headers, true, false);
}
TEST_F(RewriteDriverTest, PendingAsyncEventsTest) {
RewriteDriver* driver = rewrite_driver();
driver->set_fully_rewrite_on_flush(true);
driver->set_fast_blocking_rewrite(true);
TestPendingEventsIsDone(true);
// Only when we are doing a slow blocking rewrite (waiting for async events),
// IsDone() returns false for kWaitForCompletion.
driver->set_fully_rewrite_on_flush(true);
driver->set_fast_blocking_rewrite(false);
TestPendingEventsIsDone(false);
driver->set_fully_rewrite_on_flush(false);
driver->set_fast_blocking_rewrite(true);
TestPendingEventsIsDone(true);
driver->set_fully_rewrite_on_flush(false);
driver->set_fast_blocking_rewrite(false);
TestPendingEventsIsDone(true);
// Make sure we properly cleanup as well.
TestPendingEventsDriverCleanup(false, false);
TestPendingEventsDriverCleanup(false, true);
TestPendingEventsDriverCleanup(true, false);
TestPendingEventsDriverCleanup(true, true);
}
TEST_F(RewriteDriverTest, PendingRenderBlockingAsyncEventsTest) {
RewriteDriver* driver = rewrite_driver();
driver->set_fully_rewrite_on_flush(false);
// Plain async event doesn't prevent completion.
driver->IncrementAsyncEventsCount();
EXPECT_TRUE(IsDone(RewriteDriver::kWaitForCompletion, false));
EXPECT_TRUE(IsDone(RewriteDriver::kWaitForCompletion, true));
// Render blocking one does, however.
driver->IncrementRenderBlockingAsyncEventsCount();
EXPECT_FALSE(IsDone(RewriteDriver::kWaitForCompletion, false));
EXPECT_FALSE(IsDone(RewriteDriver::kWaitForCompletion, true));
driver->DecrementAsyncEventsCount();
// Still does when regular async removed.
EXPECT_FALSE(IsDone(RewriteDriver::kWaitForCompletion, false));
EXPECT_FALSE(IsDone(RewriteDriver::kWaitForCompletion, true));
// Once all counts are gone it's now Done, though.
driver->DecrementRenderBlockingAsyncEventsCount();
EXPECT_TRUE(IsDone(RewriteDriver::kWaitForCompletion, false));
EXPECT_TRUE(IsDone(RewriteDriver::kWaitForCompletion, true));
}
TEST_F(RewriteDriverTest, ValidateCacheResponseRewrittenWebp) {
const StringPiece kWebpMimeType = kContentTypeWebp.mime_type();
RequestContextPtr request_context(new RequestContext(
kDefaultHttpOptionsForTests, new NullMutex, timer()));
options()->ClearSignatureForTesting();
ResponseHeaders response_headers;
response_headers.Add(HttpAttributes::kContentType, kWebpMimeType);
response_headers.SetDateAndCaching(MockTimer::kApr_5_2010_ms,
300 * Timer::kSecondMs, "");
response_headers.ComputeCaching();
const char kOriginUrl[] = "foo.webp";
// No vary:accept, accepts_webp false. Note that we ignore the lack of
// browser capability to display webp and send it anyway.
request_context->SetAcceptsWebp(false);
options()->set_serve_rewritten_webp_urls_to_any_agent(true);
EXPECT_TRUE(OptionsAwareHTTPCacheCallback::IsCacheValid(
kOriginUrl, *options(), request_context, response_headers));
options()->set_serve_rewritten_webp_urls_to_any_agent(false);
EXPECT_TRUE(OptionsAwareHTTPCacheCallback::IsCacheValid(
kOriginUrl, *options(), request_context, response_headers));
// no vary:accept, accepts_webp true.
request_context->SetAcceptsWebp(true);
options()->set_serve_rewritten_webp_urls_to_any_agent(true);
EXPECT_TRUE(OptionsAwareHTTPCacheCallback::IsCacheValid(
kOriginUrl, *options(), request_context, response_headers));
options()->set_serve_rewritten_webp_urls_to_any_agent(false);
EXPECT_TRUE(OptionsAwareHTTPCacheCallback::IsCacheValid(
kOriginUrl, *options(), request_context, response_headers));
// Now add a Vary: Accept and we'll start paying attention to the
// browser capabilities.
response_headers.Add(HttpAttributes::kVary, HttpAttributes::kAccept);
response_headers.ComputeCaching();
request_context->SetAcceptsWebp(false);
// vary:accept, accepts_webp false.
options()->set_serve_rewritten_webp_urls_to_any_agent(true);
EXPECT_FALSE(OptionsAwareHTTPCacheCallback::IsCacheValid(
kOriginUrl, *options(), request_context, response_headers));
options()->set_serve_rewritten_webp_urls_to_any_agent(false);
EXPECT_FALSE(OptionsAwareHTTPCacheCallback::IsCacheValid(
kOriginUrl, *options(), request_context, response_headers));
// vary:accept, accepts_webp true.
request_context->SetAcceptsWebp(true);
options()->set_serve_rewritten_webp_urls_to_any_agent(true);
EXPECT_TRUE(OptionsAwareHTTPCacheCallback::IsCacheValid(
kOriginUrl, *options(), request_context, response_headers));
options()->set_serve_rewritten_webp_urls_to_any_agent(false);
EXPECT_TRUE(OptionsAwareHTTPCacheCallback::IsCacheValid(
kOriginUrl, *options(), request_context, response_headers));
}
TEST_F(RewriteDriverTest, SetRequestHeadersPopulatesWebpAccept) {
RequestHeaders headers;
headers.Add(HttpAttributes::kAccept, "image/webp");
headers.Add(HttpAttributes::kUserAgent,
UserAgentMatcherTestBase::kChrome42UserAgent);
rewrite_driver()->SetRequestHeaders(headers);
const RequestProperties* request_properties =
rewrite_driver()->request_properties();
EXPECT_TRUE(request_properties->SupportsWebpInPlace());
EXPECT_TRUE(request_properties->SupportsWebpRewrittenUrls());
EXPECT_TRUE(request_properties->SupportsWebpLosslessAlpha());
}
TEST_F(RewriteDriverTest, SetRequestHeadersPopulatesWebpNoAccept) {
RequestHeaders headers;
headers.Add(HttpAttributes::kUserAgent,
UserAgentMatcherTestBase::kAndroidICSUserAgent);
rewrite_driver()->SetRequestHeaders(headers);
const RequestProperties* request_properties =
rewrite_driver()->request_properties();
EXPECT_FALSE(request_properties->SupportsWebpInPlace());
EXPECT_TRUE(request_properties->SupportsWebpRewrittenUrls());
EXPECT_FALSE(request_properties->SupportsWebpLosslessAlpha());
}
// Test classes created for using a managed rewrite driver, so that downstream
// caching behavior (especially cache purging) can be tested. Since managed
// rewrite drivers need their filters to be setup before the custom rewrite
// driver is constructed, we need these classes with specific SetUp methods
// for configuring the options.
// This class has ExtendCacheCss filter enabled and has possibility of
// purge requests for the html because of the resources not being
// rewritten in the very first go.
class DownstreamCacheWithPossiblePurgeTest : public RewriteDriverTest {
protected:
void SetUp() {
options()->EnableFilter(RewriteOptions::kExtendCacheCss);
SetUseManagedRewriteDrivers(true);
RewriteDriverTest::SetUp();
}
};
// This class has CollapseWhitespace filter enabled and has no possibility of
// purge requests for the html because the html will always get fully rewritten
// in the very first go.
class DownstreamCacheWithNoPossiblePurgeTest : public RewriteDriverTest {
protected:
void SetUp() {
options()->EnableFilter(RewriteOptions::kCollapseWhitespace);
SetUseManagedRewriteDrivers(true);
RewriteDriverTest::SetUp();
}
};
TEST_F(DownstreamCacheWithPossiblePurgeTest, DownstreamCacheEnabled) {
SetDownstreamCacheDirectives("GET", "http://localhost:1234/purge", "");
// Use a wait fetcher so that the response does not get a chance to get
// rewritten.
SetupWaitFetcher();
// Since we want to call both FinishParse() and WaitForCompletion() (it's
// inside CallFetcherCallbacksForDriver) on a managed rewrite driver,
// we have to pin it, since otherwise FinishParse will drop our last
// reference.
rewrite_driver()->AddUserReference();
SetupResponsesForDownstreamCacheTesting();
// Setup request headers since the subsequent purge request needs this.
RequestHeaders request_headers;
rewrite_driver()->SetRequestHeaders(request_headers);
ProcessHtmlForDownstreamCacheTesting();
EXPECT_STREQ(kNonRewrittenCachableHtml, output_buffer_);
// Since the response would now have been generated (without any rewriting,
// because neither of the 2 resource fetches for a.css and b.css
// would have completed), we allow the fetches to complete now.
factory()->CallFetcherCallbacksForDriver(rewrite_driver());
EXPECT_EQ(2, counting_url_async_fetcher()->fetch_count());
// Now we want to permit fetches to go ahead once we let purge happen
factory()->wait_url_async_fetcher()->SetPassThroughMode(true);
rewrite_driver()->Cleanup(); // Drop our ref, to let purge go ahead.
// We can actually check the result of flush already because
// our fetcher is immediate.
EXPECT_EQ(3, counting_url_async_fetcher()->fetch_count());
EXPECT_STREQ("http://localhost:1234/purge/",
counting_url_async_fetcher()->most_recent_fetched_url());
EXPECT_EQ(1, factory()->rewrite_stats()->
downstream_cache_purge_attempts()->Get());
}
TEST_F(DownstreamCacheWithPossiblePurgeTest, DownstreamCacheDisabled) {
SetDownstreamCacheDirectives("GET", "", "");
// Use a wait fetcher so that the response does not get a chance to get
// rewritten.
SetupWaitFetcher();
// Since we want to call both FinishParse() and WaitForCompletion() (it's
// inside CallFetcherCallbacksForDriver) on a managed rewrite driver,
// we have to pin it, since otherwise FinishParse will drop our last
// reference.
rewrite_driver()->AddUserReference();
SetupResponsesForDownstreamCacheTesting();
// Setup request headers since the subsequent purge request needs this.
RequestHeaders request_headers;
rewrite_driver()->SetRequestHeaders(request_headers);
ProcessHtmlForDownstreamCacheTesting();
EXPECT_STREQ(kNonRewrittenCachableHtml, output_buffer_);
// Since the response would now have been generated (without any rewriting,
// because neither of the 2 resource fetches for a.css and b.css
// would have completed), we allow the fetches to complete now.
factory()->CallFetcherCallbacksForDriver(rewrite_driver());
EXPECT_EQ(2, counting_url_async_fetcher()->fetch_count());
// The purge-request-fetch can be allowed to complete without any waiting.
// Hence, we set the pass-through-mode to true.
factory()->wait_url_async_fetcher()->SetPassThroughMode(true);
rewrite_driver()->Cleanup(); // Drop our ref, to let any purge go ahead.
// We expect no purges in this flow.
EXPECT_EQ(2, counting_url_async_fetcher()->fetch_count());
EXPECT_STREQ("http://test.com/test/b.css",
counting_url_async_fetcher()->most_recent_fetched_url());
EXPECT_EQ(0, factory()->rewrite_stats()->
downstream_cache_purge_attempts()->Get());
}
TEST_F(DownstreamCacheWithPossiblePurgeTest,
DownstreamCache100PercentRewritten) {
SetDownstreamCacheDirectives("GET", "http://localhost:1234/purge", "");
// Do not use a wait fetcher here because we want both the fetches (for a.css
// and b.css) and their rewrites to finish before the response is served out.
SetupResponsesForDownstreamCacheTesting();
// Setup request headers since the subsequent purge request needs this.
RequestHeaders request_headers;
rewrite_driver()->SetRequestHeaders(request_headers);
ProcessHtmlForDownstreamCacheTesting();
EXPECT_STREQ(kRewrittenCachableHtmlWithCacheExtension, output_buffer_);
EXPECT_EQ(2, counting_url_async_fetcher()->fetch_count());
// We expect no purges in this flow.
EXPECT_EQ(2, counting_url_async_fetcher()->fetch_count());
EXPECT_STREQ("http://test.com/test/b.css",
counting_url_async_fetcher()->most_recent_fetched_url());
EXPECT_EQ(0, factory()->rewrite_stats()->
downstream_cache_purge_attempts()->Get());
}
TEST_F(DownstreamCacheWithNoPossiblePurgeTest, DownstreamCacheNoInitRewrites) {
SetDownstreamCacheDirectives("GET", "http://localhost:1234/purge", "");
// Use a wait fetcher so that the response does not get a chance to get
// rewritten.
SetupWaitFetcher();
rewrite_driver()->AddUserReference();
SetupResponsesForDownstreamCacheTesting();
// Setup request headers since the subsequent purge request needs this.
RequestHeaders request_headers;
rewrite_driver()->SetRequestHeaders(request_headers);
ProcessHtmlForDownstreamCacheTesting();
EXPECT_STREQ(kRewrittenCachableHtmlWithCollapseWhitespace, output_buffer_);
// Since only collapse-whitespace is enabled in this test, we do not expect
// any fetches (or fetch callbacks for the wait fetcher) here.
EXPECT_EQ(0, counting_url_async_fetcher()->fetch_count());
// Release RewriteDriver and trigger any purge.
rewrite_driver()->Cleanup();
EXPECT_EQ(0, counting_url_async_fetcher()->fetch_count());
EXPECT_EQ(0, factory()->rewrite_stats()->
downstream_cache_purge_attempts()->Get());
}
} // namespace
} // namespace net_instaweb