blob: e4d2f5ea9573bf1016b58b8a203a9e7359fe8f61 [file] [log] [blame]
/*
* Copyright 2012 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: mmohabey@google.com (Megha Mohabey)
#include "net/instaweb/rewriter/public/suppress_prehead_filter.h"
#include "net/instaweb/http/public/log_record.h"
#include "net/instaweb/http/public/logging_proto_impl.h"
#include "net/instaweb/http/public/request_timing_info.h"
#include "net/instaweb/rewriter/flush_early.pb.h"
#include "net/instaweb/rewriter/public/flush_early_info_finder_test_base.h"
#include "net/instaweb/rewriter/public/rewrite_driver.h"
#include "net/instaweb/rewriter/public/rewrite_options.h"
#include "net/instaweb/rewriter/public/rewrite_test_base.h"
#include "net/instaweb/rewriter/public/server_context.h"
#include "pagespeed/kernel/base/gtest.h"
#include "pagespeed/kernel/base/string.h"
#include "pagespeed/kernel/base/string_util.h"
#include "pagespeed/kernel/base/string_writer.h"
#include "pagespeed/kernel/http/content_type.h"
#include "pagespeed/kernel/http/http.pb.h"
#include "pagespeed/kernel/http/http_names.h" // for HttpAttributes, etc
#include "pagespeed/kernel/http/response_headers.h"
namespace net_instaweb {
namespace {
const int64 kOriginTtlSec = 12000;
const char kJsData[] =
"alert ( 'hello, world!' ) "
" /* removed */ <!-- removed --> "
" // single-line-comment";
} // namespace
class SuppressPreheadFilterTest : public RewriteTestBase {
public:
SuppressPreheadFilterTest() : writer_(&output_) {}
virtual bool AddHtmlTags() const { return false; }
protected:
void InitResources() {
SetResponseWithDefaultHeaders("http://test.com/a.css", kContentTypeCss,
" a ", kOriginTtlSec);
SetResponseWithDefaultHeaders("http://test.com/b.js",
kContentTypeJavascript, kJsData,
kOriginTtlSec);
}
virtual void SetUp() {
options()->ClearSignatureForTesting();
options()->EnableFilter(RewriteOptions::kFlushSubresources);
// Disable support no script, so that we don't insert the noscript node and
// the output is simple.
options()->set_support_noscript_enabled(false);
options()->ComputeSignature();
RewriteTestBase::SetUp();
rewrite_driver()->AddFilters();
rewrite_driver()->SetWriter(&writer_);
headers_.Clear();
rewrite_driver()->set_response_headers_ptr(&headers_);
SetCurrentUserAgent("prefetch_link_script_tag");
}
GoogleString output_;
ResponseHeaders* headers() {
return &headers_;
}
void VerifyCharset(GoogleString content_type) {
VerifyHeader(HttpAttributes::kContentType, content_type);
}
void VerifyHeader(const StringPiece& name, const StringPiece& value) {
FlushEarlyInfo* flush_early_info = rewrite_driver()->flush_early_info();
HttpResponseHeaders headers = flush_early_info->response_headers();
GoogleString val;
for (int i = 0; i < headers.header_size(); ++i) {
if (name.compare(headers.header(i).name()) == 0) {
val = headers.header(i).value();
break;
}
}
EXPECT_STREQ(val, value);
}
void CallUpdateFetchLatencyInFlushEarlyProto(double latency) {
SuppressPreheadFilter::UpdateFetchLatencyInFlushEarlyProto(
latency, rewrite_driver_);
}
private:
StringWriter writer_;
ResponseHeaders headers_;
DISALLOW_COPY_AND_ASSIGN(SuppressPreheadFilterTest);
};
TEST_F(SuppressPreheadFilterTest, UpdateFetchLatencyInFlushEarlyProto) {
EXPECT_FALSE(
rewrite_driver_->flush_early_info()->has_last_n_fetch_latencies());
EXPECT_FALSE(
rewrite_driver_->flush_early_info()->has_average_fetch_latency_ms());
// When there is no entry.
CallUpdateFetchLatencyInFlushEarlyProto(100);
EXPECT_EQ("100",
rewrite_driver_->flush_early_info()->last_n_fetch_latencies());
EXPECT_EQ(100,
rewrite_driver_->flush_early_info()->average_fetch_latency_ms());
// When less than 10 entries exists.
CallUpdateFetchLatencyInFlushEarlyProto(150);
EXPECT_EQ("150,100",
rewrite_driver_->flush_early_info()->last_n_fetch_latencies());
EXPECT_EQ(125,
rewrite_driver_->flush_early_info()->average_fetch_latency_ms());
// When there are 10 entries.
rewrite_driver_->flush_early_info()->set_last_n_fetch_latencies(
"95,96,97,98,99,101,102,103,104,105");
rewrite_driver_->flush_early_info()->set_average_fetch_latency_ms(100);
CallUpdateFetchLatencyInFlushEarlyProto(205);
EXPECT_EQ("205,95,96,97,98,99,101,102,103,104",
rewrite_driver_->flush_early_info()->last_n_fetch_latencies());
EXPECT_EQ(110,
rewrite_driver_->flush_early_info()->average_fetch_latency_ms());
}
TEST_F(SuppressPreheadFilterTest, FlushEarlyHeadSuppress) {
InitResources();
const char pre_head_input[] = "<!DOCTYPE html><html><head>";
const char post_head_input[] =
"<link type=\"text/css\" rel=\"stylesheet\""
" href=\"http://test.com/a.css\"/>"
"<script src=\"http://test.com/b.js\"></script>"
"</head>"
"<body></body></html>";
GoogleString html_input = StrCat(pre_head_input, post_head_input);
RequestTimingInfo* timing_info = mutable_timing_info();
timing_info->FetchStarted();
AdvanceTimeMs(100);
timing_info->FetchHeaderReceived();
rewrite_driver_->log_record()->logging_info()->
set_is_original_resource_cacheable(false);
rewrite_driver_->flush_early_info()->set_last_n_fetch_latencies("96,98");
rewrite_driver_->flush_early_info()->set_average_fetch_latency_ms(97);
Parse("not_flushed_early", html_input);
EXPECT_EQ(html_input, output_);
// SuppressPreheadFilter should have populated the flush_early_proto with the
// appropriate pre head information.
EXPECT_EQ(pre_head_input,
rewrite_driver()->flush_early_info()->pre_head());
EXPECT_EQ("100,96,98",
rewrite_driver_->flush_early_info()->last_n_fetch_latencies());
EXPECT_EQ(98,
rewrite_driver_->flush_early_info()->average_fetch_latency_ms());
// pre head is suppressed if the dummy head was flushed early.
output_.clear();
rewrite_driver()->set_flushed_early(true);
Parse("flushed_early", html_input);
EXPECT_EQ(post_head_input, output_);
}
TEST_F(SuppressPreheadFilterTest, FlushEarlyHeadSuppressWithCacheableHtml) {
InitResources();
const char pre_head_input[] = "<!DOCTYPE html><html><head>";
const char post_head_input[] =
"<link type=\"text/css\" rel=\"stylesheet\""
" href=\"http://test.com/a.css\"/>"
"<script src=\"http://test.com/b.js\"></script>"
"</head>"
"<body></body></html>";
GoogleString html_input = StrCat(pre_head_input, post_head_input);
RequestTimingInfo* timing_info = mutable_timing_info();
timing_info->FetchStarted();
AdvanceTimeMs(100);
timing_info->FetchHeaderReceived();
rewrite_driver_->log_record()->logging_info()->
set_is_original_resource_cacheable(true);
rewrite_driver_->flush_early_info()->set_last_n_fetch_latencies("96,98");
rewrite_driver_->flush_early_info()->set_average_fetch_latency_ms(97);
Parse("not_flushed_early", html_input);
EXPECT_EQ(html_input, output_);
// SuppressPreheadFilter should have populated the flush_early_proto with the
// appropriate pre head information and ensure that last_n_fetch_latencies
// and average_fetch_latency do not get populated as we don't want to flush
// more resources for cacheable html.
EXPECT_EQ(pre_head_input,
rewrite_driver()->flush_early_info()->pre_head());
EXPECT_FALSE(
rewrite_driver_->flush_early_info()->has_last_n_fetch_latencies());
EXPECT_FALSE(
rewrite_driver_->flush_early_info()->has_average_fetch_latency_ms());
// pre head is suppressed if the dummy head was flushed early.
output_.clear();
rewrite_driver()->set_flushed_early(true);
Parse("flushed_early", html_input);
EXPECT_EQ(post_head_input, output_);
}
TEST_F(SuppressPreheadFilterTest, FlushEarlyMetaTags) {
InitResources();
const char html_input[] =
"<!DOCTYPE html>"
"<html>"
"<head>"
"<meta http-equiv=\"X-UA-Compatible\" content=\"IE=EmulateIE7\"/>"
"<meta http-equiv=\"X-UA-Compatible\" content=\"junk\"/>"
"<meta http-equiv=\"content-type\" content=\"text/html;charset=utf-8\"/>"
"<meta http-equiv=\"last-modified\" content=\"2012-08-09T11:03:27Z\"/>"
"<meta charset=\"UTF-8\">"
"</head>"
"<body></body></html>";
const char html_without_prehead[] =
"<meta http-equiv=\"X-UA-Compatible\" content=\"IE=EmulateIE7\"/>"
"<meta http-equiv=\"X-UA-Compatible\" content=\"junk\"/>"
"<meta http-equiv=\"content-type\" content=\"text/html;charset=utf-8\"/>"
"<meta http-equiv=\"last-modified\" content=\"2012-08-09T11:03:27Z\"/>"
"<meta charset=\"UTF-8\">"
"</head>"
"<body></body></html>";
Parse("not_flushed_early", html_input);
EXPECT_EQ(html_input, output_);
VerifyCharset("text/html;charset=utf-8");
VerifyHeader(HttpAttributes::kXUACompatible, "IE=EmulateIE7");
// pre head is suppressed if the dummy head was flushed early.
output_.clear();
rewrite_driver()->set_flushed_early(true);
Parse("flushed_early", html_input);
EXPECT_EQ(html_without_prehead, output_);
}
TEST_F(SuppressPreheadFilterTest, MetaTagsOutsideHead) {
InitResources();
const char html_input[] =
"<!DOCTYPE html>"
"<html><head></head>"
"<meta http-equiv=\"content-type\" content=\"text/html;charset=utf-8\"/>"
"<body></body></html>";
const char html_without_prehead[] =
"</head>"
"<meta http-equiv=\"content-type\" content=\"text/html;charset=utf-8\"/>"
"<body></body></html>";
Parse("not_flushed_early", html_input);
EXPECT_EQ(html_input, output_);
VerifyCharset("text/html;charset=utf-8");
// pre head is suppressed if the dummy head was flushed early.
output_.clear();
rewrite_driver()->set_flushed_early(true);
Parse("flushed_early", html_input);
EXPECT_EQ(html_without_prehead, output_);
}
TEST_F(SuppressPreheadFilterTest, XmlTagsBeforeDocType) {
InitResources();
const char html_input[] =
"<?xml version=\"1.0\" encoding=\"UTF-8\" ?>"
"<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\""
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">"
"<html xmlns=\"http://www.w3.org/1999/xhtml\">"
"<head profile=\"blah\">"
"</head>"
"<body></body></html>";
// pre head is suppressed if the dummy head was flushed early.
output_.clear();
rewrite_driver()->set_flushed_early(true);
Parse("flushed_early", html_input);
EXPECT_EQ("</head><body></body></html>", output_);
EXPECT_EQ("<?xml version=\"1.0\" encoding=\"UTF-8\" ?>"
"<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\""
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">"
"<html xmlns=\"http://www.w3.org/1999/xhtml\">"
"<head profile=\"blah\">",
rewrite_driver()->flush_early_info()->pre_head());
}
TEST_F(SuppressPreheadFilterTest, NoStartHtml) {
InitResources();
const char html_input[] =
"<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\""
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">"
"<head profile=\"blah\">"
"</head>"
"<body></body></html>";
// pre head is suppressed if the dummy head was flushed early.
output_.clear();
rewrite_driver()->set_flushed_early(true);
Parse("flushed_early", html_input);
EXPECT_EQ("</head><body></body></html>", output_);
EXPECT_EQ("<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\""
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">"
"<head profile=\"blah\">",
rewrite_driver()->flush_early_info()->pre_head());
}
TEST_F(SuppressPreheadFilterTest, NoHead) {
InitResources();
const char html_input[] =
"<!DOCTYPE html>"
"<html>"
"<meta http-equiv=\"content-type\" content=\"text/html;charset=utf-8\"/>"
"<body></body></html>";
const char html_output_with_head_tag[] =
"<!DOCTYPE html>"
"<html><head></head>"
"<meta http-equiv=\"content-type\" content=\"text/html;charset=utf-8\"/>"
"<body></body></html>";
const char html_output_without_prehead[] =
"</head>"
"<meta http-equiv=\"content-type\" content=\"text/html;charset=utf-8\"/>"
"<body></body></html>";
Parse("not_flushed_early", html_input);
EXPECT_EQ(html_output_with_head_tag, output_);
VerifyCharset("text/html;charset=utf-8");
// pre head is suppressed if the dummy head was flushed early.
output_.clear();
rewrite_driver()->set_flushed_early(true);
Parse("flushed_early", html_input);
EXPECT_EQ(html_output_without_prehead, output_);
}
TEST_F(SuppressPreheadFilterTest, FlushEarlyCharset) {
InitResources();
FlushEarlyRenderInfo* info = new FlushEarlyRenderInfo;
info->set_charset("utf-8");
rewrite_driver()->set_flush_early_render_info(info);
server_context()->set_flush_early_info_finder(
new MeaningfulFlushEarlyInfoFinder);
headers()->Add(HttpAttributes::kContentType, "text/html");
const char html_input[] =
"<!DOCTYPE html>"
"<html>"
"<head>"
"</head>"
"<body></body></html>";
const char html_without_prehead[] =
"</head>"
"<body></body></html>";
Parse("not_flushed_early", html_input);
EXPECT_EQ(html_input, output_);
VerifyCharset("text/html; charset=utf-8");
// pre head is suppressed if the dummy head was flushed early.
output_.clear();
rewrite_driver()->set_flushed_early(true);
Parse("flushed_early", html_input);
EXPECT_EQ(html_without_prehead, output_);
}
TEST_F(SuppressPreheadFilterTest, FlushEarlyPreExistingCharset) {
InitResources();
FlushEarlyRenderInfo* info = new FlushEarlyRenderInfo;
info->set_charset("utf-8");
rewrite_driver()->set_flush_early_render_info(info);
server_context()->set_flush_early_info_finder(
new MeaningfulFlushEarlyInfoFinder);
// The charset returned by FlushEarlyRenderInfo will never be different from
// what is already set on page. However, for the purpose of testing we have a
// different charset in response headers to ensure that we do not change the
// charset if the response headers already have them.
headers()->Add(HttpAttributes::kContentType, "text/html; charset=ISO-8859-1");
const char html_input[] =
"<!DOCTYPE html>"
"<html>"
"<head>"
"</head>"
"<body></body></html>";
const char html_without_prehead[] =
"</head>"
"<body></body></html>";
Parse("not_flushed_early", html_input);
EXPECT_EQ(html_input, output_);
VerifyCharset("text/html; charset=ISO-8859-1");
// pre head is suppressed if the dummy head was flushed early.
output_.clear();
rewrite_driver()->set_flushed_early(true);
Parse("flushed_early", html_input);
EXPECT_EQ(html_without_prehead, output_);
}
TEST_F(SuppressPreheadFilterTest, FlushEarlyCookies) {
InitResources();
headers()->Add(HttpAttributes::kSetCookie, "CG=US:CA:Mountain+View");
headers()->Add(HttpAttributes::kSetCookie, "UA=chrome");
headers()->Add(HttpAttributes::kSetCookie, "path=/");
const char html_input[] =
"<!DOCTYPE html>"
"<html>"
"<head>"
"</head>"
"<body></body></html>";
const char html_with_cookie[] =
"<script type=\"text/javascript\" data-pagespeed-no-defer>"
"(function(){"
"var data = [\"CG=US:CA:Mountain+View\",\"UA=chrome\",\"path=/\"];"
"for (var i = 0; i < data.length; i++) {"
"document.cookie = data[i];"
"}})()"
"</script>"
"</head>"
"<body></body></html>";
Parse("not_flushed_early", html_input);
EXPECT_EQ(html_input, output_);
// Javascript for setting the cookie is also flushed.
output_.clear();
rewrite_driver()->set_flushed_early(true);
Parse("flushed_early_with_cookie", html_input);
EXPECT_EQ(html_with_cookie, output_);
}
TEST_F(SuppressPreheadFilterTest, FlushEarlyCookies2) {
InitResources();
headers()->Add(HttpAttributes::kSetCookie, "RMID=266b56483f6e50519316c48a; "
"expires=Friday, 13-Sep-2013 08:02:30 GMT; path=/; "
"domain=.example.com");
const char html_input[] =
"<!DOCTYPE html>"
"<html>"
"<head>"
"</head>"
"<body></body></html>";
const char html_with_cookie[] =
"<script type=\"text/javascript\" data-pagespeed-no-defer>"
"(function(){"
"var data = [\"RMID=266b56483f6e50519316c48a; expires=Friday, "
"13-Sep-2013 08:02:30 GMT; path=/; domain=.example.com\"];"
"for (var i = 0; i < data.length; i++) {"
"document.cookie = data[i];"
"}})()"
"</script>"
"</head>"
"<body></body></html>";
Parse("not_flushed_early", html_input);
EXPECT_EQ(html_input, output_);
// Javascript for setting the cookie is also flushed.
output_.clear();
rewrite_driver()->set_flushed_early(true);
Parse("flushed_early_with_cookie", html_input);
EXPECT_EQ(html_with_cookie, output_);
}
TEST_F(SuppressPreheadFilterTest, HttpOnlyCookies) {
InitResources();
headers()->Add(HttpAttributes::kSetCookie, "CG=US:CA:Mountain+View");
headers()->Add(HttpAttributes::kSetCookie, "UA=chrome");
headers()->Add(HttpAttributes::kSetCookie, "path=/");
const char html_input[] =
"<!DOCTYPE html>"
"<html>"
"<head>"
"</head>"
"<body></body></html>";
Parse("optimized", html_input);
EXPECT_FALSE(
rewrite_driver()->flush_early_info()->http_only_cookie_present());
output_.clear();
headers()->Add(HttpAttributes::kSetCookie, "RMID=266b56483f6; HttpOnly");
Parse("optimized", html_input);
EXPECT_TRUE(rewrite_driver()->flush_early_info()->http_only_cookie_present());
}
} // namespace net_instaweb