blob: d434af76f35fe60b67833c3d3d691d54b407c985 [file] [log] [blame]
/*
* Copyright 2015 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// Author: jmarantz@google.com (Joshua Marantz)
#include "net/instaweb/rewriter/public/iframe_fetcher.h"
#include "net/instaweb/http/public/async_fetch.h"
#include "net/instaweb/http/public/mock_url_fetcher.h"
#include "net/instaweb/http/public/request_context.h"
#include "net/instaweb/rewriter/public/domain_lawyer.h"
#include "net/instaweb/rewriter/public/rewrite_options.h"
#include "net/instaweb/rewriter/public/rewrite_options_test_base.h"
#include "pagespeed/kernel/base/gmock.h"
#include "pagespeed/kernel/base/gtest.h"
#include "pagespeed/kernel/base/mock_message_handler.h"
#include "pagespeed/kernel/base/null_mutex.h"
#include "pagespeed/kernel/base/scoped_ptr.h"
#include "pagespeed/kernel/base/string_util.h"
#include "pagespeed/kernel/http/content_type.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/response_headers.h"
#include "pagespeed/kernel/http/user_agent_matcher.h"
#include "pagespeed/kernel/http/user_agent_matcher_test_base.h"
namespace net_instaweb {
namespace {
const char kExpectedUrl[] = "http://example.com/foo?bar";
const char kRobotsContent[] = "robots.txt content";
const char kFaviconContent[] = "favicon content";
GoogleString ExpectedBody(StringPiece url, bool add_scrolling_attribute) {
GoogleString scrolling_attribute =
add_scrolling_attribute ? " scrolling=\"no\"" : "";
return StrCat(
"<body class=\"mob-iframe\">"
"<div id=\"psmob-iframe-container\">"
"<div id=\"psmob-spacer\"></div>"
"<iframe id=\"psmob-iframe\" src=\"",
url, "\"", scrolling_attribute, "></iframe></div></body>");
}
GoogleString ExpectedCanonical(StringPiece url) {
return StrCat("<link rel=\"canonical\" href=\"", url, "\">");
}
// Tests the AsyncFetch class and some of its derivations.
class IframeFetcherTest : public RewriteOptionsTestBase<RewriteOptions> {
protected:
enum SuffixMode {kProxySuffix, kMapOrigin};
enum AlwaysMobilize {kOnAllDevices, kOnlyOnMobile};
IframeFetcherTest()
: request_context_(new RequestContext(
kDefaultHttpOptionsForTests, new NullMutex, NULL)),
fetch_(request_context_),
handler_(new NullMutex),
options_(thread_system_.get()) {
}
void InitTest(StringPiece user_agent, SuffixMode suffix_mode,
AlwaysMobilize always_mobilize) {
fetch_.request_headers()->Add(HttpAttributes::kUserAgent, user_agent);
options_.set_mob_always(always_mobilize == kOnAllDevices);
options_.EnableFilter(RewriteOptions::kMobilize);
DomainLawyer* lawyer = options_.WriteableDomainLawyer();
if (suffix_mode == kProxySuffix) {
lawyer->set_proxy_suffix(".suffix");
} else {
lawyer->AddOriginDomainMapping("example.com", "example.us",
"example.com", &handler_);
}
fetcher_.reset(new IframeFetcher(&options_, &matcher_, &mock_url_fetcher_));
}
void InitResources() {
InitResource("http://example.us/robots.txt", kContentTypeText,
kRobotsContent);
InitResource("http://example.us/robots.txt?foo", kContentTypeText,
kRobotsContent);
InitResource("http://example.us/favicon.ico", kContentTypeIco,
kFaviconContent);
}
ResponseHeaders* FetchPage(StringPiece domain) {
return FetchUrl(domain, "/foo?bar");
}
ResponseHeaders* FetchUrl(StringPiece domain, StringPiece url) {
fetched_url_ = StrCat("http://", domain, url);
fetcher_->Fetch(fetched_url_, &handler_, &fetch_);
return fetch_.response_headers();
}
void InitResource(
StringPiece resource_name,
const ContentType& content_type,
StringPiece content) {
ResponseHeaders response_headers;
response_headers.set_major_version(1);
response_headers.set_minor_version(1);
response_headers.SetStatusAndReason(HttpStatus::kOK);
response_headers.Add(HttpAttributes::kContentType,
content_type.mime_type());
response_headers.ComputeCaching();
mock_url_fetcher_.SetResponse(resource_name, response_headers, content);
}
protected:
UserAgentMatcher matcher_;
RequestContextPtr request_context_;
StringAsyncFetch fetch_;
MockMessageHandler handler_;
RewriteOptions options_;
MockUrlFetcher mock_url_fetcher_;
scoped_ptr<IframeFetcher> fetcher_;
GoogleString fetched_url_;
};
TEST_F(IframeFetcherTest, IframeOnMobileProxySuffix) {
InitTest(UserAgentMatcherTestBase::kAndroidChrome21UserAgent,
kProxySuffix, kOnlyOnMobile);
InitResources();
ResponseHeaders* response = FetchPage("example.com.suffix");
EXPECT_EQ(HttpStatus::kOK, response->status_code());
// If not explictly configured, there is a default width=device-width
// viewport.
EXPECT_THAT(fetch_.buffer(),
::testing::HasSubstr(ExpectedBody(
kExpectedUrl, false /* add_scrolling_attribute */)));
EXPECT_THAT(
fetch_.buffer(),
::testing::HasSubstr(ExpectedCanonical(kExpectedUrl)));
}
TEST_F(IframeFetcherTest, RedirectOnOperaMiniProxySuffix) {
InitTest(UserAgentMatcherTestBase::kOperaMiniMobileUserAgent,
kProxySuffix, kOnlyOnMobile);
ResponseHeaders* response = FetchPage("example.com.suffix");
EXPECT_EQ(HttpStatus::kTemporaryRedirect, response->status_code());
EXPECT_STREQ(kExpectedUrl,
response->Lookup1(HttpAttributes::kLocation));
}
TEST_F(IframeFetcherTest, RedirectOnDesktopProxySuffix) {
InitTest(UserAgentMatcherTestBase::kChrome42UserAgent,
kProxySuffix, kOnlyOnMobile);
InitResources();
ResponseHeaders* response = FetchPage("example.com.suffix");
EXPECT_EQ(HttpStatus::kTemporaryRedirect, response->status_code());
EXPECT_STREQ(kExpectedUrl,
response->Lookup1(HttpAttributes::kLocation));
}
TEST_F(IframeFetcherTest, IframeOnDesktopProxySuffixWithAlwaysMobilize) {
InitTest(UserAgentMatcherTestBase::kChrome42UserAgent,
kProxySuffix, kOnAllDevices);
InitResources();
ResponseHeaders* response = FetchPage("example.com.suffix");
EXPECT_EQ(HttpStatus::kOK, response->status_code());
EXPECT_THAT(fetch_.buffer(),
::testing::HasSubstr(ExpectedBody(
kExpectedUrl, false /* add_scrolling_attribute */)));
EXPECT_THAT(
fetch_.buffer(),
::testing::HasSubstr(ExpectedCanonical(kExpectedUrl)));
}
TEST_F(IframeFetcherTest, ErrorProxySuffix) {
// Report an error for a configuration problem that results in the
// domain being unmapped.
InitTest(UserAgentMatcherTestBase::kOperaMiniMobileUserAgent,
kProxySuffix, kOnlyOnMobile);
InitResources();
ResponseHeaders* response = FetchPage("example.com");
EXPECT_EQ(HttpStatus::kNotImplemented, response->status_code());
}
TEST_F(IframeFetcherTest, RedirectWhenDisabledProxySuffix) {
options_.set_mob_iframe_disable(true);
InitTest(UserAgentMatcherTestBase::kAndroidChrome21UserAgent,
kProxySuffix, kOnlyOnMobile);
InitResources();
ResponseHeaders* response = FetchPage("example.com.suffix");
EXPECT_EQ(HttpStatus::kTemporaryRedirect, response->status_code());
EXPECT_STREQ(kExpectedUrl,
response->Lookup1(HttpAttributes::kLocation));
}
TEST_F(IframeFetcherTest, IframeOnMobileMapOrigin) {
InitTest(UserAgentMatcherTestBase::kAndroidChrome21UserAgent,
kMapOrigin, kOnlyOnMobile);
InitResources();
ResponseHeaders* response = FetchPage("example.us");
EXPECT_EQ(HttpStatus::kOK, response->status_code());
EXPECT_THAT(fetch_.buffer(),
::testing::HasSubstr(ExpectedBody(
kExpectedUrl, false /* add_scrolling_attribute */)));
EXPECT_THAT(
fetch_.buffer(),
::testing::HasSubstr(ExpectedCanonical(kExpectedUrl)));
}
TEST_F(IframeFetcherTest, RedirectOnOperaMiniMapOrigin) {
InitTest(UserAgentMatcherTestBase::kOperaMiniMobileUserAgent,
kMapOrigin, kOnlyOnMobile);
InitResources();
ResponseHeaders* response = FetchPage("example.us");
EXPECT_EQ(HttpStatus::kTemporaryRedirect, response->status_code());
EXPECT_STREQ(kExpectedUrl,
response->Lookup1(HttpAttributes::kLocation));
}
TEST_F(IframeFetcherTest, RedirectOnDesktopMapOrigin) {
InitTest(UserAgentMatcherTestBase::kChrome42UserAgent,
kMapOrigin, kOnlyOnMobile);
InitResources();
ResponseHeaders* response = FetchPage("example.us");
EXPECT_EQ(HttpStatus::kTemporaryRedirect, response->status_code());
EXPECT_STREQ(kExpectedUrl,
response->Lookup1(HttpAttributes::kLocation));
}
TEST_F(IframeFetcherTest, IframeOnDesktopMapOriginWithAlwaysMobilize) {
InitTest(UserAgentMatcherTestBase::kChrome42UserAgent,
kMapOrigin, kOnAllDevices);
InitResources();
ResponseHeaders* response = FetchPage("example.us");
EXPECT_EQ(HttpStatus::kOK, response->status_code());
EXPECT_THAT(fetch_.buffer(),
::testing::HasSubstr(ExpectedBody(
kExpectedUrl, false /* add_scrolling_attribute */)));
EXPECT_THAT(
fetch_.buffer(),
::testing::HasSubstr(ExpectedCanonical(kExpectedUrl)));
}
TEST_F(IframeFetcherTest, RedirectOnNoScript) {
InitTest(UserAgentMatcherTestBase::kAndroidChrome21UserAgent,
kMapOrigin, kOnlyOnMobile);
InitResources();
options_.DisableFiltersRequiringScriptExecution();
ResponseHeaders* response = FetchPage("example.us");
EXPECT_EQ(HttpStatus::kTemporaryRedirect, response->status_code());
EXPECT_STREQ(kExpectedUrl,
response->Lookup1(HttpAttributes::kLocation));
}
TEST_F(IframeFetcherTest, ErrorMapOrigin) {
// Report an error for a configuration problem that results in the
// domain being unmapped.
InitTest(UserAgentMatcherTestBase::kOperaMiniMobileUserAgent,
kMapOrigin, kOnlyOnMobile);
InitResources();
ResponseHeaders* response = FetchPage("example.com");
EXPECT_EQ(HttpStatus::kNotImplemented, response->status_code());
}
TEST_F(IframeFetcherTest, RedirectWhenDisabledMapOrigin) {
options_.set_mob_iframe_disable(true);
InitTest(UserAgentMatcherTestBase::kAndroidChrome21UserAgent,
kMapOrigin, kOnlyOnMobile);
InitResources();
ResponseHeaders* response = FetchPage("example.us");
EXPECT_EQ(HttpStatus::kTemporaryRedirect, response->status_code());
EXPECT_STREQ(kExpectedUrl,
response->Lookup1(HttpAttributes::kLocation));
}
TEST_F(IframeFetcherTest, Viewport) {
// Verify default viewport
InitTest(UserAgentMatcherTestBase::kAndroidChrome21UserAgent,
kMapOrigin, kOnlyOnMobile);
InitResources();
ResponseHeaders* response = FetchPage("example.us");
EXPECT_EQ(HttpStatus::kOK, response->status_code());
EXPECT_THAT(
fetch_.buffer(),
::testing::HasSubstr("<meta name=\"viewport\" "
"content=\"width=device-width,initial-scale=1\">"));
// Verify that the scrolling attribute is not set on the iframe, since this is
// an android user agent.
EXPECT_THAT(fetch_.buffer(),
::testing::HasSubstr(ExpectedBody(
kExpectedUrl, false /* add_scrolling_attribute */)));
}
TEST_F(IframeFetcherTest, ViewportNone) {
options_.set_mob_iframe_viewport("none");
InitTest(UserAgentMatcherTestBase::kAndroidChrome21UserAgent,
kMapOrigin, kOnlyOnMobile);
InitResources();
ResponseHeaders* response = FetchPage("example.us");
EXPECT_EQ(HttpStatus::kOK, response->status_code());
EXPECT_THAT(fetch_.buffer(),
::testing::Not(::testing::HasSubstr("<meta name=\"viewport\"")));
// Verify that the scrolling attribute is not set on the iframe.
EXPECT_THAT(fetch_.buffer(),
::testing::Not(::testing::HasSubstr("scrolling=\"no\"")));
}
TEST_F(IframeFetcherTest, ViewportEscaped) {
options_.set_mob_iframe_viewport("\">");
InitTest(UserAgentMatcherTestBase::kAndroidChrome21UserAgent,
kMapOrigin, kOnlyOnMobile);
InitResources();
ResponseHeaders* response = FetchPage("example.us");
EXPECT_EQ(HttpStatus::kOK, response->status_code());
EXPECT_THAT(
fetch_.buffer(),
::testing::HasSubstr("<meta name=\"viewport\" content=\"&quot;&gt;\">"));
EXPECT_THAT(fetch_.buffer(),
::testing::Not(::testing::HasSubstr("scrolling=\"no\"")));
}
TEST_F(IframeFetcherTest, ViewportIOsSafari) {
InitTest(UserAgentMatcherTestBase::kIPhone4Safari, kMapOrigin, kOnlyOnMobile);
InitResources();
ResponseHeaders* response = FetchPage("example.us");
EXPECT_EQ(HttpStatus::kOK, response->status_code());
EXPECT_THAT(
fetch_.buffer(),
::testing::HasSubstr("<meta name=\"viewport\" "
"content=\"width=device-width,initial-scale=1\">"));
// Verify that the scrolling attribute is set on the iframe.
EXPECT_THAT(fetch_.buffer(),
::testing::HasSubstr(ExpectedBody(
kExpectedUrl, true /* add_scrolling_attribute */)));
}
TEST_F(IframeFetcherTest, ViewportNoneIOsSafari) {
options_.set_mob_iframe_viewport("none");
InitTest(UserAgentMatcherTestBase::kIPhone4Safari, kMapOrigin, kOnlyOnMobile);
InitResources();
ResponseHeaders* response = FetchPage("example.us");
EXPECT_EQ(HttpStatus::kOK, response->status_code());
EXPECT_THAT(fetch_.buffer(),
::testing::Not(::testing::HasSubstr("<meta name=\"viewport\"")));
// Verify that the scrolling attribute is set on the iframe.
EXPECT_THAT(fetch_.buffer(),
::testing::HasSubstr(ExpectedBody(
kExpectedUrl, false /* add_scrolling_attribute */)));
}
TEST_F(IframeFetcherTest, Robots) {
options_.set_mob_iframe_viewport("\">");
InitTest(UserAgentMatcherTestBase::kAndroidChrome21UserAgent,
kMapOrigin, kOnlyOnMobile);
InitResources();
ResponseHeaders* response = FetchUrl("example.us", "/robots.txt");
EXPECT_EQ(HttpStatus::kOK, response->status_code());
EXPECT_STREQ(kRobotsContent, fetch_.buffer());
EXPECT_STREQ("text/plain", response->Lookup1(HttpAttributes::kContentType));
}
TEST_F(IframeFetcherTest, Robots404) {
options_.set_mob_iframe_viewport("\">");
InitTest(UserAgentMatcherTestBase::kAndroidChrome21UserAgent,
kMapOrigin, kOnlyOnMobile);
// Do not call InitResources()
mock_url_fetcher_.set_fail_on_unexpected(false);
ResponseHeaders* response = FetchUrl("example.us", "/robots.txt");
EXPECT_EQ(HttpStatus::kNotFound, response->status_code());
}
TEST_F(IframeFetcherTest, RobotsWithQuery) {
options_.set_mob_iframe_viewport("\">");
InitTest(UserAgentMatcherTestBase::kAndroidChrome21UserAgent,
kMapOrigin, kOnlyOnMobile);
InitResources();
ResponseHeaders* response = FetchUrl("example.us", "/robots.txt?foo");
EXPECT_EQ(HttpStatus::kOK, response->status_code());
EXPECT_STREQ(kRobotsContent, fetch_.buffer());
EXPECT_STREQ("text/plain", response->Lookup1(HttpAttributes::kContentType));
}
// Robots.txt must be at top level. Requests for robots.txt in a
// subdirectory will be iframed.
TEST_F(IframeFetcherTest, RobotsInSubdir) {
options_.set_mob_iframe_viewport("\">");
InitTest(UserAgentMatcherTestBase::kAndroidChrome21UserAgent,
kMapOrigin, kOnlyOnMobile);
InitResources();
ResponseHeaders* response = FetchUrl("example.us", "/foo/robots.txt");
EXPECT_EQ(HttpStatus::kOK, response->status_code());
EXPECT_THAT(fetch_.buffer(), ::testing::HasSubstr(ExpectedBody(
"http://example.com/foo/robots.txt",
false /* add_scrolling_attribute */)));
EXPECT_STREQ("text/html", response->Lookup1(HttpAttributes::kContentType));
}
TEST_F(IframeFetcherTest, Favicon) {
options_.set_mob_iframe_viewport("\">");
InitTest(UserAgentMatcherTestBase::kAndroidChrome21UserAgent,
kMapOrigin, kOnlyOnMobile);
InitResources();
ResponseHeaders* response = FetchUrl("example.us", "/favicon.ico");
EXPECT_EQ(HttpStatus::kOK, response->status_code());
EXPECT_STREQ(kFaviconContent, fetch_.buffer());
EXPECT_STREQ("image/x-icon", response->Lookup1(HttpAttributes::kContentType));
}
} // namespace
} // namespace net_instaweb