| /* |
| * 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: guptaa@google.com (Ashish Gupta) |
| |
| #include "net/instaweb/rewriter/public/static_asset_manager.h" |
| |
| #include "net/instaweb/rewriter/public/common_filter.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/scoped_ptr.h" |
| #include "pagespeed/kernel/base/string_util.h" |
| #include "pagespeed/kernel/html/html_element.h" |
| #include "pagespeed/kernel/html/html_name.h" |
| #include "pagespeed/kernel/html/html_parse_test_base.h" |
| #include "pagespeed/kernel/http/content_type.h" |
| |
| namespace net_instaweb { |
| |
| namespace { |
| |
| const char kHtml[] = "<body><br></body>"; |
| const char kScript[] = "alert('foo');"; |
| |
| class StaticAssetManagerTest : public RewriteTestBase { |
| protected: |
| StaticAssetManagerTest() { |
| manager_.reset(new StaticAssetManager("http://proxy-domain", |
| server_context()->thread_system(), |
| server_context()->hasher(), |
| server_context()->message_handler())); |
| } |
| |
| virtual void SetUp() { |
| RewriteTestBase::SetUp(); |
| } |
| |
| // Helper filters to help test inserting of static JS. |
| class AddStaticJsBeforeBr: public CommonFilter { |
| public: |
| explicit AddStaticJsBeforeBr(RewriteDriver* driver) |
| : CommonFilter(driver) { } |
| virtual void StartDocumentImpl() { } |
| virtual void StartElementImpl(HtmlElement* element) { } |
| virtual void EndElementImpl(HtmlElement* element) { |
| if (element->keyword() == HtmlName::kBr) { |
| HtmlElement* script = driver()->NewElement(element->parent(), |
| HtmlName::kScript); |
| driver()->InsertNodeBeforeNode(element, script); |
| AddJsToElement(kScript, script); |
| } |
| } |
| virtual const char* Name() const { return "AddStaticJsBeforeBr"; } |
| private: |
| DISALLOW_COPY_AND_ASSIGN(AddStaticJsBeforeBr); |
| }; |
| |
| // Extracts the first comment sequences in a script that doesn't |
| // contain any whitelisted substrings. |
| // |
| // TODO(jmarantz): Note that this is not a proper lexer and will be |
| // fooled by comment sequences in strings, thus this might require |
| // refactoring to use our js tokenizer in the future, should this |
| // yield false positives. |
| StringPiece ExtractCommentSkippingWhitelist(StringPiece script) { |
| StringPieceVector v; |
| stringpiece_ssize_type pos = 0; |
| while (true) { |
| pos = script.find("/*", pos); |
| if (pos == StringPiece::npos) { |
| return ""; |
| } |
| stringpiece_ssize_type endpos = 0; |
| endpos = script.find("*/", pos + 2); |
| if (endpos == StringPiece::npos) { |
| return ""; |
| } |
| endpos += 2; |
| StringPiece comment = script.substr(pos, endpos - pos); |
| if (comment.find("MochiKit") == StringPiece::npos) { |
| return comment; |
| } |
| pos = endpos; |
| } |
| } |
| |
| scoped_ptr<StaticAssetManager> manager_; |
| }; |
| |
| TEST_F(StaticAssetManagerTest, TestBlinkHandler) { |
| const char blink_url[] = "http://proxy-domain/psajs/blink.0.js"; |
| EXPECT_STREQ(blink_url, manager_->GetAssetUrl(StaticAssetEnum::BLINK_JS, |
| options_)); |
| } |
| |
| TEST_F(StaticAssetManagerTest, TestBlinkGstatic) { |
| manager_->set_static_asset_base("http://proxy-domain"); |
| manager_->ServeAssetsFromGStatic(StaticAssetManager::kGStaticBase); |
| manager_->SetGStaticHashForTest( |
| StaticAssetEnum::BLINK_JS, "1"); |
| const char blink_url[] = |
| "//www.gstatic.com/psa/static/1-blink.js"; |
| EXPECT_STREQ(blink_url, manager_->GetAssetUrl(StaticAssetEnum::BLINK_JS, |
| options_)); |
| } |
| |
| TEST_F(StaticAssetManagerTest, TestBlinkDebug) { |
| manager_->ServeAssetsFromGStatic(StaticAssetManager::kGStaticBase); |
| manager_->SetGStaticHashForTest(StaticAssetEnum::BLINK_JS, "1"); |
| options_->EnableFilter(RewriteOptions::kDebug); |
| const char blink_url[] = "//www.gstatic.com/psa/static/1-blink.js"; |
| EXPECT_STREQ(blink_url, manager_->GetAssetUrl(StaticAssetEnum::BLINK_JS, |
| options_)); |
| } |
| |
| TEST_F(StaticAssetManagerTest, TestDeferJsGstatic) { |
| manager_->ServeAssetsFromGStatic(StaticAssetManager::kGStaticBase); |
| manager_->SetGStaticHashForTest(StaticAssetEnum::DEFER_JS, "1"); |
| const char defer_js_url[] = |
| "//www.gstatic.com/psa/static/1-js_defer.js"; |
| EXPECT_STREQ(defer_js_url, manager_->GetAssetUrl( |
| StaticAssetEnum::DEFER_JS, options_)); |
| } |
| |
| TEST_F(StaticAssetManagerTest, TestDeferJsDebug) { |
| manager_->ServeAssetsFromGStatic(StaticAssetManager::kGStaticBase); |
| manager_->SetGStaticHashForTest(StaticAssetEnum::DEFER_JS, "1"); |
| options_->EnableFilter(RewriteOptions::kDebug); |
| const char defer_js_debug_url[] = |
| "//www.gstatic.com/psa/static/1-js_defer.js"; |
| EXPECT_STREQ(defer_js_debug_url, manager_->GetAssetUrl( |
| StaticAssetEnum::DEFER_JS, options_)); |
| } |
| |
| TEST_F(StaticAssetManagerTest, TestDeferJsNonGStatic) { |
| const char defer_js_url[] = |
| "http://proxy-domain/psajs/js_defer.0.js"; |
| EXPECT_STREQ(defer_js_url, manager_->GetAssetUrl( |
| StaticAssetEnum::DEFER_JS, options_)); |
| } |
| |
| TEST_F(StaticAssetManagerTest, TestDeferJsNonGStaticDebug) { |
| const char defer_js_url[] = |
| "http://proxy-domain/psajs/js_defer_debug.0.js"; |
| options_->EnableFilter(RewriteOptions::kDebug); |
| EXPECT_STREQ(defer_js_url, manager_->GetAssetUrl( |
| StaticAssetEnum::DEFER_JS, options_)); |
| } |
| |
| TEST_F(StaticAssetManagerTest, TestJsDebug) { |
| options_->EnableFilter(RewriteOptions::kDebug); |
| for (int i = 0; i < StaticAssetEnum::StaticAsset_ARRAYSIZE; ++i) { |
| StaticAssetEnum::StaticAsset module = |
| static_cast<StaticAssetEnum::StaticAsset>(i); |
| // TODO(sligocki): This should generalize to all resources which don't have |
| // kContentTypeJs. But no interface provides content types currently :/ |
| if (module != StaticAssetEnum::BLANK_GIF && |
| module != StaticAssetEnum::MOBILIZE_CSS && |
| module != StaticAssetEnum::MOBILIZE_LAYOUT_CSS) { |
| GoogleString script(manager_->GetAsset(module, options_)); |
| // Debug code is also put through the closure compiler to resolve any uses |
| // of goog.require. As part of this, comments also get stripped out. |
| EXPECT_STREQ("", ExtractCommentSkippingWhitelist(script)) |
| << "Comment found in debug version of asset " << module; |
| } |
| } |
| } |
| |
| TEST_F(StaticAssetManagerTest, TestJsOpt) { |
| for (int i = 0; i < StaticAssetEnum::StaticAsset_ARRAYSIZE; ++i) { |
| StaticAssetEnum::StaticAsset module = |
| static_cast<StaticAssetEnum::StaticAsset>(i); |
| // TODO(sligocki): This should generalize to all resources which don't have |
| // kContentTypeJs. But no interface provides content types currently :/ |
| if (module != StaticAssetEnum::BLANK_GIF && |
| module != StaticAssetEnum::MOBILIZE_CSS && |
| module != StaticAssetEnum::MOBILIZE_LAYOUT_CSS) { |
| GoogleString script(manager_->GetAsset(module, options_)); |
| EXPECT_STREQ("", ExtractCommentSkippingWhitelist(script)) |
| << "Comment found in debug version of asset " << module; |
| } |
| } |
| } |
| |
| TEST_F(StaticAssetManagerTest, TestHtmlInsertInlineJs) { |
| SetHtmlMimetype(); |
| AddStaticJsBeforeBr filter(rewrite_driver()); |
| rewrite_driver()->AddFilter(&filter); |
| ParseUrl(kTestDomain, kHtml); |
| EXPECT_EQ("<html>\n<body><script type=\"text/javascript\">alert('foo');" |
| "</script><br></body></html>", output_buffer_); |
| } |
| |
| TEST_F(StaticAssetManagerTest, TestXhtmlInsertInlineJs) { |
| SetXhtmlMimetype(); |
| AddStaticJsBeforeBr filter(rewrite_driver()); |
| rewrite_driver()->AddFilter(&filter); |
| ParseUrl(kTestDomain, kHtml); |
| EXPECT_EQ("<html>\n<body><script type=\"text/javascript\">//<![CDATA[\n" |
| "alert('foo');\n//]]></script><br></body></html>", |
| output_buffer_); |
| } |
| |
| TEST_F(StaticAssetManagerTest, TestHtml5InsertInlineJs) { |
| SetHtmlMimetype(); |
| AddStaticJsBeforeBr filter(rewrite_driver()); |
| rewrite_driver()->AddFilter(&filter); |
| GoogleString html = StrCat("<!DOCTYPE html>", kHtml); |
| ParseUrl(kTestDomain, html); |
| EXPECT_EQ("<html>\n<!DOCTYPE html><body><script>alert('foo');" |
| "</script><br></body></html>", output_buffer_); |
| } |
| |
| TEST_F(StaticAssetManagerTest, TestEncodedUrls) { |
| for (int i = 0; i < StaticAssetEnum::StaticAsset_ARRAYSIZE; ++i) { |
| StaticAssetEnum::StaticAsset module = |
| static_cast<StaticAssetEnum::StaticAsset>(i); |
| |
| GoogleString url = manager_->GetAssetUrl(module, options_); |
| StringPiece file_name = url; |
| const StringPiece kDomainAndPath = "http://proxy-domain/psajs/"; |
| ASSERT_TRUE(file_name.starts_with(kDomainAndPath)); |
| file_name.remove_prefix(kDomainAndPath.size()); |
| |
| StringPiece content, cache_header; |
| ContentType content_type; |
| EXPECT_TRUE(manager_->GetAsset(file_name, &content, &content_type, |
| &cache_header)); |
| EXPECT_EQ("max-age=31536000", cache_header); |
| } |
| } |
| |
| TEST_F(StaticAssetManagerTest, FullGStaticConf) { |
| scoped_ptr<RewriteOptions> debug_options(options_->Clone()); |
| debug_options->EnableFilter(RewriteOptions::kDebug); |
| |
| manager_->ServeAssetsFromGStatic("http://actually_any_cdn.com/"); |
| |
| // Setup initial batch configuration. |
| StaticAssetConfig config; |
| config.set_release_label("100"); |
| StaticAssetConfig::Asset* asset_conf = config.add_asset(); |
| asset_conf->set_role(StaticAssetEnum::ADD_INSTRUMENTATION_JS); |
| asset_conf->set_name("add_instr.js"); |
| asset_conf->set_debug_hash("dbg1"); |
| asset_conf->set_opt_hash("opt1"); |
| |
| asset_conf = config.add_asset(); |
| asset_conf->set_role(StaticAssetEnum::LAZYLOAD_IMAGES_JS); |
| asset_conf->set_name("lazy.js"); |
| asset_conf->set_debug_hash("dbg2"); |
| asset_conf->set_opt_hash("opt2"); |
| |
| manager_->ApplyGStaticConfiguration( |
| config, StaticAssetManager::kInitialConfiguration); |
| |
| // The configuration is sparse, so things still retain defaults. |
| EXPECT_EQ("http://proxy-domain/psajs/js_defer.0.js", |
| manager_->GetAssetUrl(StaticAssetEnum::DEFER_JS, options_)); |
| EXPECT_EQ("http://proxy-domain/psajs/js_defer_debug.0.js", |
| manager_->GetAssetUrl(StaticAssetEnum::DEFER_JS, |
| debug_options.get())); |
| |
| // The configured things do work, however. |
| EXPECT_EQ("http://actually_any_cdn.com/opt1-add_instr.js", |
| manager_->GetAssetUrl(StaticAssetEnum::ADD_INSTRUMENTATION_JS, |
| options_)); |
| EXPECT_EQ("http://actually_any_cdn.com/dbg1-add_instr.js", |
| manager_->GetAssetUrl(StaticAssetEnum::ADD_INSTRUMENTATION_JS, |
| debug_options.get())); |
| |
| EXPECT_EQ("http://actually_any_cdn.com/opt2-lazy.js", |
| manager_->GetAssetUrl(StaticAssetEnum::LAZYLOAD_IMAGES_JS, |
| options_)); |
| EXPECT_EQ("http://actually_any_cdn.com/dbg2-lazy.js", |
| manager_->GetAssetUrl(StaticAssetEnum::LAZYLOAD_IMAGES_JS, |
| debug_options.get())); |
| |
| // Now try updating with a config with different version. |
| StaticAssetConfig config2; |
| config2.set_release_label("99"); |
| asset_conf = config2.add_asset(); |
| asset_conf->set_role(StaticAssetEnum::ADD_INSTRUMENTATION_JS); |
| asset_conf->set_name("add_instr.js"); |
| asset_conf->set_debug_hash("dbg0"); |
| asset_conf->set_opt_hash("opt0"); |
| |
| asset_conf = config2.add_asset(); |
| asset_conf->set_role(StaticAssetEnum::LAZYLOAD_IMAGES_JS); |
| asset_conf->set_name("lazy.js"); |
| asset_conf->set_debug_hash("dbg0"); |
| asset_conf->set_opt_hash("opt0"); |
| |
| asset_conf = config2.add_asset(); |
| asset_conf->set_role(StaticAssetEnum::DEFER_JS); |
| asset_conf->set_name("defer.js"); |
| asset_conf->set_debug_hash("dbg0"); |
| asset_conf->set_opt_hash("opt0"); |
| |
| manager_->ApplyGStaticConfiguration( |
| config2, |
| StaticAssetManager::kUpdateConfiguration); |
| |
| // Nothing is actually changed. |
| EXPECT_EQ("http://proxy-domain/psajs/js_defer.0.js", |
| manager_->GetAssetUrl(StaticAssetEnum::DEFER_JS, options_)); |
| EXPECT_EQ("http://proxy-domain/psajs/js_defer_debug.0.js", |
| manager_->GetAssetUrl(StaticAssetEnum::DEFER_JS, |
| debug_options.get())); |
| |
| EXPECT_EQ("http://actually_any_cdn.com/opt1-add_instr.js", |
| manager_->GetAssetUrl(StaticAssetEnum::ADD_INSTRUMENTATION_JS, |
| options_)); |
| EXPECT_EQ("http://actually_any_cdn.com/dbg1-add_instr.js", |
| manager_->GetAssetUrl(StaticAssetEnum::ADD_INSTRUMENTATION_JS, |
| debug_options.get())); |
| |
| EXPECT_EQ("http://actually_any_cdn.com/opt2-lazy.js", |
| manager_->GetAssetUrl(StaticAssetEnum::LAZYLOAD_IMAGES_JS, |
| options_)); |
| EXPECT_EQ("http://actually_any_cdn.com/dbg2-lazy.js", |
| manager_->GetAssetUrl(StaticAssetEnum::LAZYLOAD_IMAGES_JS, |
| debug_options.get())); |
| |
| // Now with matching version, things can change. Notice that we still didn't |
| // update things that were not specified in initial config. This is a design |
| // tradeoff: it makes it harder to hotfix things, but means we won't get stuck |
| // with a stale override in CDD. |
| StaticAssetConfig config3; |
| config3.set_release_label("100"); |
| asset_conf = config3.add_asset(); |
| asset_conf->set_role(StaticAssetEnum::ADD_INSTRUMENTATION_JS); |
| asset_conf->set_name("add_instr.js"); |
| asset_conf->set_debug_hash("dbg3"); |
| asset_conf->set_opt_hash("opt3"); |
| |
| asset_conf = config3.add_asset(); |
| asset_conf->set_role(StaticAssetEnum::LAZYLOAD_IMAGES_JS); |
| asset_conf->set_name("lazy.js"); |
| asset_conf->set_debug_hash("dbg4"); |
| asset_conf->set_opt_hash("opt4"); |
| |
| asset_conf = config3.add_asset(); |
| asset_conf->set_role(StaticAssetEnum::DEFER_JS); |
| asset_conf->set_name("defer.js"); |
| asset_conf->set_debug_hash("dbg5"); |
| asset_conf->set_opt_hash("opt5"); |
| |
| manager_->ApplyGStaticConfiguration( |
| config3, |
| StaticAssetManager::kUpdateConfiguration); |
| |
| // Everything that was initially configured via this is changed. |
| EXPECT_EQ("http://proxy-domain/psajs/js_defer.0.js", |
| manager_->GetAssetUrl(StaticAssetEnum::DEFER_JS, options_)); |
| EXPECT_EQ("http://proxy-domain/psajs/js_defer_debug.0.js", |
| manager_->GetAssetUrl(StaticAssetEnum::DEFER_JS, |
| debug_options.get())); |
| |
| EXPECT_EQ("http://actually_any_cdn.com/opt3-add_instr.js", |
| manager_->GetAssetUrl(StaticAssetEnum::ADD_INSTRUMENTATION_JS, |
| options_)); |
| EXPECT_EQ("http://actually_any_cdn.com/dbg3-add_instr.js", |
| manager_->GetAssetUrl(StaticAssetEnum::ADD_INSTRUMENTATION_JS, |
| debug_options.get())); |
| |
| EXPECT_EQ("http://actually_any_cdn.com/opt4-lazy.js", |
| manager_->GetAssetUrl(StaticAssetEnum::LAZYLOAD_IMAGES_JS, |
| options_)); |
| EXPECT_EQ("http://actually_any_cdn.com/dbg4-lazy.js", |
| manager_->GetAssetUrl(StaticAssetEnum::LAZYLOAD_IMAGES_JS, |
| debug_options.get())); |
| |
| // Now test that resetting config works. |
| manager_->ResetGStaticConfiguration(); |
| EXPECT_EQ("http://proxy-domain/psajs/js_defer.0.js", |
| manager_->GetAssetUrl(StaticAssetEnum::DEFER_JS, options_)); |
| EXPECT_EQ("http://proxy-domain/psajs/js_defer_debug.0.js", |
| manager_->GetAssetUrl(StaticAssetEnum::DEFER_JS, |
| debug_options.get())); |
| |
| // The configured things do work, however. |
| EXPECT_EQ("http://actually_any_cdn.com/opt1-add_instr.js", |
| manager_->GetAssetUrl(StaticAssetEnum::ADD_INSTRUMENTATION_JS, |
| options_)); |
| EXPECT_EQ("http://actually_any_cdn.com/dbg1-add_instr.js", |
| manager_->GetAssetUrl(StaticAssetEnum::ADD_INSTRUMENTATION_JS, |
| debug_options.get())); |
| |
| EXPECT_EQ("http://actually_any_cdn.com/opt2-lazy.js", |
| manager_->GetAssetUrl(StaticAssetEnum::LAZYLOAD_IMAGES_JS, |
| options_)); |
| EXPECT_EQ("http://actually_any_cdn.com/dbg2-lazy.js", |
| manager_->GetAssetUrl(StaticAssetEnum::LAZYLOAD_IMAGES_JS, |
| debug_options.get())); |
| } |
| |
| } // namespace |
| |
| } // namespace net_instaweb |