/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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.
 */


// Unit-test the javascript filter

#include "net/instaweb/rewriter/public/javascript_filter.h"

#include "net/instaweb/http/public/http_cache.h"
#include "net/instaweb/http/public/logging_proto.h"
#include "net/instaweb/http/public/logging_proto_impl.h"
#include "net/instaweb/http/public/mock_url_fetcher.h"
#include "net/instaweb/http/public/request_context.h"
#include "net/instaweb/rewriter/public/debug_filter.h"
#include "net/instaweb/rewriter/public/javascript_code_block.h"
#include "net/instaweb/rewriter/public/javascript_library_identification.h"
#include "net/instaweb/rewriter/public/js_outline_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 "net/instaweb/rewriter/public/support_noscript_filter.h"
#include "pagespeed/kernel/base/basictypes.h"
#include "pagespeed/kernel/base/gmock.h"
#include "pagespeed/kernel/base/gtest.h"
#include "pagespeed/kernel/base/hasher.h"
#include "pagespeed/kernel/base/md5_hasher.h"
#include "pagespeed/kernel/base/statistics.h"
#include "pagespeed/kernel/base/string.h"
#include "pagespeed/kernel/base/string_util.h"
#include "pagespeed/kernel/cache/lru_cache.h"
#include "pagespeed/kernel/html/html_parse_test_base.h"
#include "pagespeed/kernel/http/content_type.h"
#include "pagespeed/kernel/http/google_url.h"
#include "pagespeed/kernel/http/http_names.h"
#include "pagespeed/kernel/http/response_headers.h"
#include "pagespeed/kernel/http/semantic_type.h"
#include "pagespeed/opt/logging/log_record.h"

namespace {

const char kHtmlFormat[] =
    "<script type='text/javascript' src='%s'></script>\n";
const char kInlineScriptFormat[] =
    "<script type='text/javascript'>"
    "%s"
    "</script>";
const char kEndInlineScript[] = "<script type='text/javascript'>";

const char kCdataWrapper[] = "//<![CDATA[\n%s\n//]]>";
const char kCdataAltWrapper[] = "//<![CDATA[\r%s\r//]]>";

const char kInlineJs[] =
    "<script type='text/javascript'>%s</script>\n";

const char kJsData[] =
    "alert     (    'hello, world!'    ) "
    " /* removed */ <!-- removed --> "
    " // single-line-comment";
const char kJsMinData[] = "alert('hello, world!')";
const char kFilterId[] = "jm";
const char kOrigJsName[] = "hello.js";
const char kOrigJsNameRegexp[] = "*hello.js*";
const char kUnauthorizedJs[] = "http://other.domain.com/hello.js";
const char kRewrittenJsName[] = "hello.js";
const char kLibraryUrl[] = "https://www.example.com/hello/1.0/hello.js";
const char kIntrospectiveJS[] =
    "<script type='text/javascript' src='introspective.js'></script>";

const char kJsonData[] = "  {  'foo' :  [ 'bar' , 'baz' ]  }  ";
const char kJsonMinData[] = "{'foo':['bar','baz']}";
const char kOrigJsonName[] = "hello.json";
const char kRewrittenJsonName[] = "hello.json";

GoogleString ScriptSrc(const StringPiece& url) {
  return net_instaweb::StrCat("<script src=\"", url, "\"></script>");
}

}  // namespace

namespace net_instaweb {

class JavascriptFilterTest : public RewriteTestBase,
                             public ::testing::WithParamInterface<bool> {
 protected:
  virtual void SetUp() {
    RewriteTestBase::SetUp();
    options()->set_use_experimental_js_minifier(GetParam());
    expected_rewritten_path_ = Encode("", kFilterId, "0",
                                      kRewrittenJsName, "js");

    blocks_minified_ = statistics()->GetVariable(
        JavascriptRewriteConfig::kBlocksMinified);
    libraries_identified_ = statistics()->GetVariable(
        JavascriptRewriteConfig::kLibrariesIdentified);
    minification_failures_ = statistics()->GetVariable(
        JavascriptRewriteConfig::kMinificationFailures);
    total_bytes_saved_ = statistics()->GetVariable(
        JavascriptRewriteConfig::kTotalBytesSaved);
    total_original_bytes_ = statistics()->GetVariable(
        JavascriptRewriteConfig::kTotalOriginalBytes);
    num_uses_ = statistics()->GetVariable(
        JavascriptRewriteConfig::kMinifyUses);
    did_not_shrink_ = statistics()->GetVariable(
        JavascriptRewriteConfig::kJSDidNotShrink);
  }

  void InitFilters() {
    options()->EnableFilter(RewriteOptions::kRewriteJavascriptExternal);
    options()->EnableFilter(RewriteOptions::kRewriteJavascriptInline);
    options()->EnableFilter(RewriteOptions::kCanonicalizeJavascriptLibraries);
    rewrite_driver_->AddFilters();
  }

  void InitTest(int64 ttl) {
    SetResponseWithDefaultHeaders(
        kOrigJsName, kContentTypeJavascript, kJsData, ttl);
    SetResponseWithDefaultHeaders(
        kUnauthorizedJs, kContentTypeJavascript, kJsData, ttl);
  }

  void InitFiltersAndTest(int64 ttl) {
    InitFilters();
    InitTest(ttl);
  }

  void RegisterLibrary() {
    MD5Hasher hasher(JavascriptLibraryIdentification::kNumHashChars);
    GoogleString hash = hasher.Hash(kJsMinData);
    EXPECT_TRUE(
        options()->RegisterLibrary(
            STATIC_STRLEN(kJsMinData), hash, kLibraryUrl));
    EXPECT_EQ(JavascriptLibraryIdentification::kNumHashChars, hash.size());
  }

  // Generate HTML loading a single script with the specified URL.
  GoogleString GenerateHtml(const char* a) {
    return StringPrintf(kHtmlFormat, a);
  }

  // Generate HTML loading a single script twice from the specified URL.
  GoogleString GenerateTwoHtml(const char* a) {
    GoogleString once = GenerateHtml(a);
    return StrCat(once, once);
  }

  void TestCorruptUrl(const char* new_suffix) {
    // Do a normal rewrite test
    InitFiltersAndTest(100);
    ValidateExpected("no_ext_corruption",
                    GenerateHtml(kOrigJsName),
                    GenerateHtml(expected_rewritten_path_.c_str()));

    // Fetch messed up URL.
    ASSERT_TRUE(StringCaseEndsWith(expected_rewritten_path_, ".js"));
    GoogleString munged_url =
        ChangeSuffix(expected_rewritten_path_, false /* replace */,
                     ".js", new_suffix);

    GoogleString out;
    EXPECT_TRUE(FetchResourceUrl(StrCat(kTestDomain, munged_url), &out));

    // Rewrite again; should still get normal URL
    ValidateExpected("no_ext_corruption",
                    GenerateHtml(kOrigJsName),
                    GenerateHtml(expected_rewritten_path_.c_str()));
  }

  void SourceMapTest(StringPiece input_js, StringPiece expected_output_js,
                     StringPiece expected_mapping_vlq);

  GoogleString expected_rewritten_path_;

  // Stats
  Variable* blocks_minified_;
  Variable* libraries_identified_;
  Variable* minification_failures_;
  Variable* did_not_shrink_;
  Variable* total_bytes_saved_;
  Variable* total_original_bytes_;
  Variable* num_uses_;
};

TEST_P(JavascriptFilterTest, DoRewrite) {
  InitFiltersAndTest(100);
  AbstractLogRecord* log_record =
      rewrite_driver_->request_context()->log_record();
  log_record->SetAllowLoggingUrls(true);
  ValidateExpected("do_rewrite",
                   GenerateHtml(kOrigJsName),
                   GenerateHtml(expected_rewritten_path_.c_str()));

  EXPECT_EQ(1, blocks_minified_->Get());
  EXPECT_EQ(0, minification_failures_->Get());
  EXPECT_EQ(STATIC_STRLEN(kJsData) - STATIC_STRLEN(kJsMinData),
            total_bytes_saved_->Get());
  EXPECT_EQ(STATIC_STRLEN(kJsData), total_original_bytes_->Get());
  EXPECT_EQ(1, num_uses_->Get());
  EXPECT_STREQ(kFilterId, AppliedRewriterStringFromLog());
  VerifyRewriterInfoEntry(log_record, kFilterId, 0, 0, 1, 1,
                        "http://test.com/hello.js");
}

TEST_P(JavascriptFilterTest, DontRewriteUnauthorizedDomain) {
  InitFiltersAndTest(100);
  ValidateNoChanges("dont_rewrite", GenerateHtml(kUnauthorizedJs));
}

TEST_P(JavascriptFilterTest, DebugForUnauthorizedDomain) {
  StringVector expected_disabled_filters;
  SupportNoscriptFilter tmp(rewrite_driver());
  expected_disabled_filters.push_back(tmp.Name());
  const char kCaseId[] = "debug_unauthorized_domain";
  const GoogleString html_input = GenerateHtml(kUnauthorizedJs);
  // Remove the trailing newline as it's in the way :-(
  GoogleString html_output = html_input.substr(0, html_input.length() - 1);
  GoogleUrl gurl(kUnauthorizedJs);
  StrAppend(&html_output,
            "<!--",
            rewrite_driver()->GenerateUnauthorizedDomainDebugComment(
                gurl, RewriteDriver::InputRole::kScript),
            "-->"
            "\n");
  html_output = AddHtmlBody(html_output);
  GoogleString end_document_message = DebugFilter::FormatEndDocumentMessage(
      0, 0, 0, 0, 0, false, StringSet(), expected_disabled_filters);
  options()->EnableFilter(RewriteOptions::kDebug);
  InitFiltersAndTest(100);
  Parse(kCaseId, html_input);
  EXPECT_HAS_SUBSTR(html_output, output_buffer_) << "Test id:" << kCaseId;
  EXPECT_HAS_SUBSTR(end_document_message, output_buffer_)
      << "Test id:" << kCaseId;
}

TEST_P(JavascriptFilterTest, DontRewriteUnauthorizedDomainWithUnauthOptionSet) {
  InitFiltersAndTest(100);
  options()->ClearSignatureForTesting();
  options()->AddInlineUnauthorizedResourceType(semantic_type::kScript);
  server_context()->ComputeSignature(options());
  ValidateNoChanges("dont_rewrite", GenerateHtml(kUnauthorizedJs));
}

TEST_P(JavascriptFilterTest, DontRewriteDisallowedScripts) {
  SetResponseWithDefaultHeaders(
      "a.js", kContentTypeJavascript, "document.write('a');", 100);
  options()->Disallow("*a.js*");
  options()->EnableFilter(RewriteOptions::kInlineJavascript);
  SetHtmlMimetype();
  InitFiltersAndTest(100);
  ValidateExpected("inline javascript disallowed",
                   StringPrintf(kHtmlFormat, "a.js"),
                   StringPrintf(kHtmlFormat, "a.js"));
}

TEST_P(JavascriptFilterTest, DoInlineAllowedForInliningScripts) {
  SetResponseWithDefaultHeaders(
      "a.js", kContentTypeJavascript, "document.write('a');", 100);
  options()->AllowOnlyWhenInlining("*a.js*");
  options()->EnableFilter(RewriteOptions::kInlineJavascript);
  SetHtmlMimetype();
  InitFiltersAndTest(100);
  ValidateExpected("inline javascript allowed for inlining",
                   StringPrintf(kHtmlFormat, "a.js"),
                   StringPrintf(kInlineJs, "document.write('a');"));
}

TEST_P(JavascriptFilterTest, RewriteButExceedLogThreshold) {
  InitFiltersAndTest(100);
  rewrite_driver_->log_record()->SetRewriterInfoMaxSize(0);
  ValidateExpected("do_rewrite",
                   GenerateHtml(kOrigJsName),
                   GenerateHtml(expected_rewritten_path_.c_str()));
  EXPECT_STREQ("", AppliedRewriterStringFromLog());
}

TEST_P(JavascriptFilterTest, DoRewriteUnhealthy) {
  lru_cache()->set_is_healthy(false);

  InitFiltersAndTest(100);
  ValidateNoChanges("do_rewrite", GenerateHtml(kOrigJsName));
  EXPECT_STREQ("", AppliedRewriterStringFromLog());
}

TEST_P(JavascriptFilterTest, RewriteAlreadyCachedProperly) {
  InitFiltersAndTest(100000000);  // cached for a long time to begin with
  // But we will rewrite because we can make the data smaller.
  ValidateExpected("rewrite_despite_being_cached_properly",
                   GenerateHtml(kOrigJsName),
                   GenerateHtml(expected_rewritten_path_.c_str()));
}

TEST_P(JavascriptFilterTest, NoRewriteOriginUncacheable) {
  InitFiltersAndTest(0);  // origin not cacheable
  ValidateExpected("no_extend_origin_not_cacheable",
                   GenerateHtml(kOrigJsName),
                   GenerateHtml(kOrigJsName));

  EXPECT_EQ(0, blocks_minified_->Get());
  EXPECT_EQ(0, minification_failures_->Get());
  EXPECT_EQ(0, total_bytes_saved_->Get());
  EXPECT_EQ(0, total_original_bytes_->Get());
  EXPECT_EQ(0, num_uses_->Get());
}

TEST_P(JavascriptFilterTest, IdentifyLibrary) {
  RegisterLibrary();
  InitFiltersAndTest(100);
  ValidateExpected("identify_library",
                   GenerateHtml(kOrigJsName),
                   GenerateHtml(kLibraryUrl));

  EXPECT_EQ(1, libraries_identified_->Get());
  EXPECT_EQ(1, blocks_minified_->Get());
  EXPECT_EQ(0, minification_failures_->Get());
}

TEST_P(JavascriptFilterTest, IdentifyLibraryTwice) {
  // Make sure cached recognition is handled properly.
  RegisterLibrary();
  InitFiltersAndTest(100);
  ValidateExpected("identify_library_twice",
                   GenerateTwoHtml(kOrigJsName),
                   GenerateTwoHtml(kLibraryUrl));
  // The second rewrite uses cached data from the first rewrite.
  EXPECT_EQ(1, libraries_identified_->Get());
  EXPECT_EQ(1, blocks_minified_->Get());
  EXPECT_EQ(0, minification_failures_->Get());
}

TEST_P(JavascriptFilterTest, JsPreserveURLsOnTest) {
  // Make sure that when in conservative mode the URL stays the same.
  RegisterLibrary();
  options()->SoftEnableFilterForTesting(
      RewriteOptions::kRewriteJavascriptExternal);
  options()->SoftEnableFilterForTesting(
      RewriteOptions::kCanonicalizeJavascriptLibraries);
  options()->set_js_preserve_urls(true);
  rewrite_driver()->AddFilters();
  EXPECT_TRUE(options()->Enabled(RewriteOptions::kRewriteJavascriptExternal));
  // Verify that preserve had a chance to forbid some filters.
  EXPECT_FALSE(options()->Enabled(
      RewriteOptions::kCanonicalizeJavascriptLibraries));
  InitTest(100);
  // Make sure the URL doesn't change.
  ValidateExpected("js_urls_preserved",
                   GenerateHtml(kOrigJsName),
                   GenerateHtml(kOrigJsName));

  // We should have optimized the JS even though we didn't render the URL.
  ClearStats();
  GoogleString out_js_url = Encode(
      kTestDomain, kFilterId, "0", kRewrittenJsName, "js");
  GoogleString out_js;
  EXPECT_TRUE(FetchResourceUrl(out_js_url, &out_js));
  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(1, static_cast<int>(lru_cache()->num_hits()));
  EXPECT_EQ(0, static_cast<int>(lru_cache()->num_misses()));
  EXPECT_EQ(0, static_cast<int>(lru_cache()->num_inserts()));

  // Was the JS minified?
  EXPECT_EQ(kJsMinData, out_js);
}

TEST_P(JavascriptFilterTest, JsPreserveOverridingExtend) {
  // Make sure that when in conservative mode the URL stays the same.
  RegisterLibrary();

  scoped_ptr<RewriteOptions> global_options(options()->NewOptions());
  global_options->EnableFilter(RewriteOptions::kExtendCacheCss);

  scoped_ptr<RewriteOptions> vhost_options(options()->NewOptions());
  vhost_options->SoftEnableFilterForTesting(
      RewriteOptions::kRewriteJavascriptExternal);
  vhost_options->SoftEnableFilterForTesting(
      RewriteOptions::kCanonicalizeJavascriptLibraries);
  vhost_options->set_js_preserve_urls(true);
  options()->Merge(*global_options);
  options()->Merge(*vhost_options);

  rewrite_driver()->AddFilters();
  EXPECT_TRUE(options()->Enabled(RewriteOptions::kRewriteJavascriptExternal));
  InitTest(100);

  // Make sure the URL doesn't change.
  ValidateExpected("js_urls_preserved",
                   GenerateHtml(kOrigJsName),
                   GenerateHtml(kOrigJsName));

  // We should have preemptively optimized the JS even though we didn't render
  // the URL.
  ClearStats();
  GoogleString out_js_url = Encode(
      kTestDomain, kFilterId, "0", kOrigJsName, "js");
  GoogleString out_js;
  EXPECT_TRUE(FetchResourceUrl(out_js_url, &out_js));
  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(1, static_cast<int>(lru_cache()->num_hits()));
  EXPECT_EQ(0, static_cast<int>(lru_cache()->num_misses()));
  EXPECT_EQ(0, static_cast<int>(lru_cache()->num_inserts()));

  // Was the JS minified?
  EXPECT_EQ(kJsMinData, out_js);
}

TEST_P(JavascriptFilterTest, JsExtendOverridingPreserve) {
  // Make sure that when in conservative mode the URL stays the same.
  RegisterLibrary();

  scoped_ptr<RewriteOptions> global_options(options()->NewOptions());
  global_options->set_js_preserve_urls(true);
  global_options->EnableFilter(RewriteOptions::kRewriteJavascriptExternal);

  scoped_ptr<RewriteOptions> vhost_options(options()->NewOptions());
  vhost_options->EnableFilter(RewriteOptions::kExtendCacheScripts);
  options()->Merge(*global_options);
  options()->Merge(*vhost_options);
  rewrite_driver()->AddFilters();
  EXPECT_TRUE(options()->Enabled(RewriteOptions::kRewriteJavascriptExternal));
  InitTest(100);

  // Make sure the URL is updated.
  ValidateExpected("js_extend_overrides_preserve",
                   GenerateHtml(kOrigJsName),
                   GenerateHtml(Encode(
                       "", kFilterId, "0", kRewrittenJsName, "js").c_str()));

  ClearStats();
  GoogleString out_js;
  GoogleString out_js_url = Encode(
      kTestDomain, kFilterId, "0", kRewrittenJsName, "js");
  EXPECT_TRUE(FetchResourceUrl(out_js_url, &out_js));
  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(1, static_cast<int>(lru_cache()->num_hits()));
  EXPECT_EQ(0, static_cast<int>(lru_cache()->num_misses()));
  EXPECT_EQ(0, static_cast<int>(lru_cache()->num_inserts()));

  // Was the JS minified?
  EXPECT_EQ(kJsMinData, out_js);
}

TEST_P(JavascriptFilterTest, JsPreserveURLsNoPreemptiveRewriteTest) {
  // Make sure that when in conservative mode the URL stays the same.
  RegisterLibrary();
  options()->SoftEnableFilterForTesting(
      RewriteOptions::kRewriteJavascriptExternal);
  options()->set_js_preserve_urls(true);
  options()->set_in_place_preemptive_rewrite_javascript(false);
  rewrite_driver()->AddFilters();
  EXPECT_TRUE(options()->Enabled(
      RewriteOptions::kRewriteJavascriptExternal));
  InitTest(100);
  // Make sure the URL doesn't change.
  ValidateExpected("js_urls_preserved_no_preemptive",
                   GenerateHtml(kOrigJsName),
                   GenerateHtml(kOrigJsName));

  // We should not have attempted any rewriting.
  EXPECT_EQ(0, http_cache()->cache_hits()->Get());
  EXPECT_EQ(0, http_cache()->cache_misses()->Get());
  EXPECT_EQ(0, http_cache()->cache_inserts()->Get());
  EXPECT_EQ(0, static_cast<int>(lru_cache()->num_hits()));
  EXPECT_EQ(0, static_cast<int>(lru_cache()->num_misses()));
  EXPECT_EQ(0, static_cast<int>(lru_cache()->num_inserts()));

  // But, if we fetch the JS directly, we should receive the optimized version.
  ClearStats();
  GoogleString out_js_url = Encode(
      kTestDomain, kFilterId, "0", kRewrittenJsName, "js");
  GoogleString out_js;
  EXPECT_TRUE(FetchResourceUrl(out_js_url, &out_js));
  EXPECT_EQ(kJsMinData, out_js);
}

TEST_P(JavascriptFilterTest, IdentifyLibraryNoMinification) {
  // Don't enable kRewriteJavascript.  This should still identify the library.
  RegisterLibrary();
  options()->EnableFilter(RewriteOptions::kCanonicalizeJavascriptLibraries);
  rewrite_driver_->AddFilters();
  InitTest(100);
  ValidateExpected("identify_library_no_minification",
                   GenerateHtml(kOrigJsName),
                   GenerateHtml(kLibraryUrl));

  EXPECT_EQ(1, libraries_identified_->Get());
  EXPECT_EQ(1, blocks_minified_->Get());
  EXPECT_EQ(0, minification_failures_->Get());
  EXPECT_EQ(0, total_bytes_saved_->Get());
}

TEST_P(JavascriptFilterTest, DisallowedUrlsNotCheckedForCanonicalization) {
  RegisterLibrary();
  options()->EnableFilter(RewriteOptions::kCanonicalizeJavascriptLibraries);
  options()->Disallow(kOrigJsNameRegexp);
  rewrite_driver_->AddFilters();
  InitTest(100);
  ValidateExpected("no_library_identification",
                   GenerateHtml(kOrigJsName),
                   GenerateHtml(kOrigJsName));

  EXPECT_EQ(0, libraries_identified_->Get());
  EXPECT_EQ(0, blocks_minified_->Get());
  EXPECT_EQ(0, minification_failures_->Get());
  EXPECT_EQ(0, total_bytes_saved_->Get());
}

TEST_P(JavascriptFilterTest, AllowWhenInliningUrlsStillNotChecked) {
  RegisterLibrary();
  options()->EnableFilter(RewriteOptions::kCanonicalizeJavascriptLibraries);
  options()->AllowOnlyWhenInlining(kOrigJsNameRegexp);
  rewrite_driver_->AddFilters();
  InitTest(100);
  ValidateExpected("no_library_identification",
                   GenerateHtml(kOrigJsName),
                   GenerateHtml(kOrigJsName));

  EXPECT_EQ(0, libraries_identified_->Get());
  EXPECT_EQ(0, blocks_minified_->Get());
  EXPECT_EQ(0, minification_failures_->Get());
  EXPECT_EQ(0, total_bytes_saved_->Get());
}

TEST_P(JavascriptFilterTest, IdentifyFailureNoMinification) {
  // Don't enable kRewriteJavascript.  We should attempt library identification,
  // fail, and not modify the code even though it can be minified.
  options()->EnableFilter(RewriteOptions::kCanonicalizeJavascriptLibraries);
  rewrite_driver_->AddFilters();
  InitTest(100);
  // We didn't register any libraries, so we should see that minification
  // happened but that nothing changed on the page.
  ValidateExpected("identify_failure_no_minification",
                   GenerateHtml(kOrigJsName),
                   GenerateHtml(kOrigJsName));

  EXPECT_EQ(0, libraries_identified_->Get());
  EXPECT_EQ(1, blocks_minified_->Get());
  EXPECT_EQ(0, minification_failures_->Get());
}

TEST_P(JavascriptFilterTest, IgnoreLibraryNoIdentification) {
  RegisterLibrary();
  // We register the library but don't enable library redirection.
  options()->EnableFilter(RewriteOptions::kRewriteJavascriptExternal);
  rewrite_driver_->AddFilters();
  InitTest(100);
  ValidateExpected("ignore_library",
                   GenerateHtml(kOrigJsName),
                   GenerateHtml(expected_rewritten_path_.c_str()));

  EXPECT_EQ(0, libraries_identified_->Get());
  EXPECT_EQ(1, blocks_minified_->Get());
  EXPECT_EQ(0, minification_failures_->Get());
}

TEST_P(JavascriptFilterTest, DontCombineIdentified) {
  // Don't combine a 3rd-party library with other scripts if we'd otherwise
  // redirect that library to its canonical url.  Doing so will cause us to
  // download content that we think has a fair probability of being cached in
  // the browser already.  If we're better off combining, we shouldn't be
  // considering the library as a candidate for library identification in the
  // first place.
  RegisterLibrary();
  options()->EnableFilter(RewriteOptions::kCombineJavascript);
  InitFiltersAndTest(100);
  ValidateExpected("DontCombineIdentified",
                   GenerateTwoHtml(kOrigJsName),
                   GenerateTwoHtml(kLibraryUrl));
}

TEST_P(JavascriptFilterTest, DontInlineIdentified) {
  // Don't inline a one-line library that was rewritten to a canonical url.
  RegisterLibrary();
  options()->EnableFilter(RewriteOptions::kInlineJavascript);
  InitFiltersAndTest(100);
  ValidateExpected("DontInlineIdentified",
                   GenerateHtml(kOrigJsName),
                   GenerateHtml(kLibraryUrl));
}

TEST_P(JavascriptFilterTest, ServeFiles) {
  InitFilters();
  TestServeFiles(&kContentTypeJavascript, kFilterId, "js",
                 kOrigJsName, kJsData,
                 kRewrittenJsName, kJsMinData);

  EXPECT_EQ(1, blocks_minified_->Get());
  EXPECT_EQ(0, minification_failures_->Get());
  EXPECT_EQ(STATIC_STRLEN(kJsData) - STATIC_STRLEN(kJsMinData),
            total_bytes_saved_->Get());
  EXPECT_EQ(STATIC_STRLEN(kJsData), total_original_bytes_->Get());
  // Note: We do not count any uses, because we did not write the URL into
  // an HTML file, just served it on request.
  EXPECT_EQ(0, num_uses_->Get());

  // Finally, serve from a completely separate server.
  ServeResourceFromManyContexts(StrCat(kTestDomain, expected_rewritten_path_),
                                kJsMinData);
}

TEST_P(JavascriptFilterTest, ServeFilesUnhealthy) {
  lru_cache()->set_is_healthy(false);

  InitFilters();
  InitTest(100);
  TestServeFiles(&kContentTypeJavascript, kFilterId, "js",
                 kOrigJsName, kJsData,
                 kRewrittenJsName, kJsMinData);
}

TEST_P(JavascriptFilterTest, ServeRewrittenLibrary) {
  // If a request comes in for the rewritten version of a JS library
  // that we have identified as matching a canonical library, we should
  // still serve some useful content.  It won't be minified because we
  // don't want to update metadata cache entries on the fly.
  RegisterLibrary();
  InitFiltersAndTest(100);
  GoogleString content;
  EXPECT_TRUE(
      FetchResource(kTestDomain, kFilterId, kRewrittenJsName, "js", &content));
  EXPECT_EQ(kJsData, content);

  // And having done so, we should still identify the library in subsequent html
  // (ie the cache should not be corrupted to prevent library identification).
  ValidateExpected("identify_library",
                   GenerateHtml(kOrigJsName),
                   GenerateHtml(kLibraryUrl));
}

TEST_P(JavascriptFilterTest, ServeJsonFile) {
  InitFilters();
  // Set content type extension to "js" because filter is caching it with js
  // extension, but the in-place lookup will still cache and serve with original
  // content type. Tests for this in in_place_rewrite_context_test.
  TestServeFiles(&kContentTypeJson, kFilterId, "js",
                 kOrigJsonName, kJsonData,
                 kRewrittenJsonName, kJsonMinData);

  EXPECT_EQ(1, blocks_minified_->Get());
  EXPECT_EQ(0, minification_failures_->Get());
  EXPECT_EQ(STATIC_STRLEN(kJsonData) - STATIC_STRLEN(kJsonMinData),
            total_bytes_saved_->Get());
  EXPECT_EQ(STATIC_STRLEN(kJsonData), total_original_bytes_->Get());
  // Note: We do not count any uses, because we did not write the URL into
  // an HTML file, just served it on request.
  EXPECT_EQ(0, num_uses_->Get());
}

TEST_P(JavascriptFilterTest, IdentifyAjaxLibrary) {
  // If ajax rewriting is enabled, we won't minify a library when it is fetched,
  // but it will still be replaced on the containing page.
  RegisterLibrary();
  options()->set_in_place_rewriting_enabled(true);
  options()->EnableFilter(RewriteOptions::kCanonicalizeJavascriptLibraries);
  rewrite_driver_->AddFilters();
  InitTest(100);
  GoogleString url = StrCat(kTestDomain, kOrigJsName);
  // Do resource fetch for js; this will cause it to be filtered in the
  // background.
  GoogleString content;
  EXPECT_TRUE(FetchResourceUrl(url, &content));
  EXPECT_EQ(kJsData, content);
  // A second resource fetch for the js will still obtain the unminified
  // content, since we don't save the minified version for identified libraries.
  content.clear();
  EXPECT_TRUE(FetchResourceUrl(url, &content));
  EXPECT_EQ(kJsData, content);
  // But rewriting the page will see the js url.
  ValidateExpected("IdentifyAjaxLibrary",
                   GenerateHtml(kOrigJsName),
                   GenerateHtml(kLibraryUrl));
}

TEST_P(JavascriptFilterTest, InvalidInputMimetype) {
  InitFilters();
  // Make sure we can rewrite properly even when input has corrupt mimetype.
  ContentType not_java_script = kContentTypeJavascript;
  not_java_script.mime_type_ = "text/semicolon-inserted";
  const char* kNotJsFile = "script.notjs";

  SetResponseWithDefaultHeaders(kNotJsFile, not_java_script, kJsData, 100);
  ValidateExpected("wrong_mime",
                   GenerateHtml(kNotJsFile),
                   GenerateHtml(Encode("", kFilterId, "0",
                                       kNotJsFile, "js").c_str()));
}

TEST_P(JavascriptFilterTest, RewriteJs404) {
  InitFilters();
  // Test to make sure that a missing input is handled well.
  SetFetchResponse404("404.js");
  ValidateNoChanges("404", "<script src='404.js'></script>");
  EXPECT_EQ(0, blocks_minified_->Get());
  EXPECT_EQ(0, minification_failures_->Get());
  EXPECT_EQ(0, num_uses_->Get());

  // Second time, to make sure caching doesn't break it.
  ValidateNoChanges("404", "<script src='404.js'></script>");
  EXPECT_EQ(0, blocks_minified_->Get());
  EXPECT_EQ(0, minification_failures_->Get());
  EXPECT_EQ(0, num_uses_->Get());
}

// Make sure bad requests do not corrupt our extension.
TEST_P(JavascriptFilterTest, NoExtensionCorruption) {
  TestCorruptUrl(".js%22");
}

TEST_P(JavascriptFilterTest, NoQueryCorruption) {
  TestCorruptUrl(".js?query");
}

TEST_P(JavascriptFilterTest, NoWrongExtCorruption) {
  TestCorruptUrl(".html");
}

TEST_P(JavascriptFilterTest, InlineJavascript) {
  // Test minification of a simple inline script
  InitFiltersAndTest(100);
  ValidateExpected("inline javascript",
                   StringPrintf(kInlineJs, kJsData),
                   StringPrintf(kInlineJs, kJsMinData));

  EXPECT_EQ(1, blocks_minified_->Get());
  EXPECT_EQ(0, minification_failures_->Get());
  EXPECT_EQ(STATIC_STRLEN(kJsData) - STATIC_STRLEN(kJsMinData),
            total_bytes_saved_->Get());
  EXPECT_EQ(STATIC_STRLEN(kJsData), total_original_bytes_->Get());
  EXPECT_EQ(1, num_uses_->Get());
}

TEST_P(JavascriptFilterTest, NoMinificationInlineJS) {
  // Test no minification of a simple inline script.
  InitFiltersAndTest(100);
  const char kSmallJS[] = "alert('hello');";
  ValidateExpected("inline javascript",
                   StringPrintf(kInlineJs, kSmallJS),
                   StringPrintf(kInlineJs, kSmallJS));

  // There was no minification error, so 1 here.
  EXPECT_EQ(1, blocks_minified_->Get());
  EXPECT_EQ(1, did_not_shrink_->Get());
  // No failures though.
  EXPECT_EQ(0, minification_failures_->Get());
}

TEST_P(JavascriptFilterTest, StripInlineWhitespace) {
  // Make sure we strip inline whitespace when minifying external scripts.
  InitFiltersAndTest(100);
  ValidateExpected(
      "StripInlineWhitespace",
      StrCat("<script src='", kOrigJsName, "'>   \t\n   </script>"),
      StrCat("<script src='",
             Encode("", kFilterId, "0", kOrigJsName, "js"),
             "'></script>"));
}

TEST_P(JavascriptFilterTest, RetainInlineData) {
  // Test to make sure we keep inline data when minifying external scripts.
  InitFiltersAndTest(100);
  ValidateExpected("StripInlineWhitespace",
                   StrCat("<script src='", kOrigJsName, "'> data </script>"),
                   StrCat("<script src='",
                          Encode("", kFilterId, "0", kOrigJsName, "js"),
                          "'> data </script>"));
}

// Test minification of a simple inline script in markup with no
// mimetype, where the script is wrapped in a commented-out CDATA.
//
// Note that javascript_filter never adds CDATA.  It only removes it
// if it's sure the mimetype is HTML.
TEST_P(JavascriptFilterTest, CdataJavascriptNoMimetype) {
  InitFiltersAndTest(100);
  ValidateExpected(
      "cdata javascript no mimetype",
      StringPrintf(kInlineJs, StringPrintf(kCdataWrapper, kJsData).c_str()),
      StringPrintf(kInlineJs, StringPrintf(kCdataWrapper, kJsMinData).c_str()));
  ValidateExpected(
      "cdata javascript no mimetype with \\r",
      StringPrintf(kInlineJs, StringPrintf(kCdataAltWrapper, kJsData).c_str()),
      StringPrintf(kInlineJs, StringPrintf(kCdataWrapper, kJsMinData).c_str()));
}

// Same as CdataJavascriptNoMimetype, but with explicit HTML mimetype.
TEST_P(JavascriptFilterTest, CdataJavascriptHtmlMimetype) {
  SetHtmlMimetype();
  InitFiltersAndTest(100);
  ValidateExpected(
      "cdata javascript with explicit HTML mimetype",
      StringPrintf(kInlineJs, StringPrintf(kCdataWrapper, kJsData).c_str()),
      StringPrintf(kInlineJs, kJsMinData));
  ValidateExpected(
      "cdata javascript with explicit HTML mimetype and \\r",
      StringPrintf(kInlineJs, StringPrintf(kCdataAltWrapper, kJsData).c_str()),
      StringPrintf(kInlineJs, kJsMinData));
}

// Same as CdataJavascriptNoMimetype, but with explicit XHTML mimetype.
TEST_P(JavascriptFilterTest, CdataJavascriptXhtmlMimetype) {
  SetXhtmlMimetype();
  InitFiltersAndTest(100);
  ValidateExpected(
      "cdata javascript with explicit XHTML mimetype",
      StringPrintf(kInlineJs, StringPrintf(kCdataWrapper, kJsData).c_str()),
      StringPrintf(kInlineJs, StringPrintf(kCdataWrapper, kJsMinData).c_str()));
  ValidateExpected(
      "cdata javascript with explicit XHTML mimetype and \\r",
      StringPrintf(kInlineJs, StringPrintf(kCdataAltWrapper, kJsData).c_str()),
      StringPrintf(kInlineJs, StringPrintf(kCdataWrapper, kJsMinData).c_str()));
}

TEST_P(JavascriptFilterTest, XHtmlInlineJavascript) {
  // Test minification of a simple inline script in xhtml
  // where it must be wrapped in CDATA.
  InitFiltersAndTest(100);
  const GoogleString xhtml_script_format =
      StrCat(kXhtmlDtd, StringPrintf(kInlineJs, kCdataWrapper));
  ValidateExpected("xhtml inline javascript",
                   StringPrintf(xhtml_script_format.c_str(), kJsData),
                   StringPrintf(xhtml_script_format.c_str(), kJsMinData));
  const GoogleString xhtml_script_alt_format =
      StrCat(kXhtmlDtd, StringPrintf(kInlineJs, kCdataAltWrapper));
  ValidateExpected("xhtml inline javascript",
                   StringPrintf(xhtml_script_alt_format.c_str(), kJsData),
                   StringPrintf(xhtml_script_format.c_str(), kJsMinData));
}

// http://github.com/apache/incubator-pagespeed-mod/issues/324
TEST_P(JavascriptFilterTest, RetainExtraHeaders) {
  InitFilters();
  GoogleString url = StrCat(kTestDomain, kOrigJsName);
  SetResponseWithDefaultHeaders(url, kContentTypeJavascript, kJsData, 300);
  TestRetainExtraHeaders(kOrigJsName, kFilterId, "js");
}

// http://github.com/apache/incubator-pagespeed-mod/issues/327 -- we were
// previously busting regexps with backslashes in them.
TEST_P(JavascriptFilterTest, BackslashInRegexp) {
  InitFilters();
  GoogleString input = StringPrintf(kInlineJs, "/http:\\/\\/[^/]+\\//");
  ValidateNoChanges("backslash_in_regexp", input);
}

TEST_P(JavascriptFilterTest, WeirdSrcCrash) {
  InitFilters();
  // These used to crash due to bugs in the lexer breaking invariants some
  // filters relied on.
  //
  // Note that the attribute-value "foo<bar" gets converted into "foo%3Cbar"
  // by this line:
  //   const GoogleUrl resource_url(base_url(), input_url);
  // in CommonFilter::CreateInputResource.  Following that, resource_url.Spec()
  // has the %3C in it.  I guess that's probably the right thing to do, but
  // I was a little surprised.
  static const char kUrl[] = "foo%3Cbar";
  SetResponseWithDefaultHeaders(kUrl, kContentTypeJavascript, kJsData, 300);
  ValidateExpected("weird_attr", "<script src=foo<bar>Content",
                   StrCat("<script src=",
                          Encode("", kFilterId, "0", kUrl, "js"),
                          ">Content"));
  ValidateNoChanges("weird_tag", "<script<foo>");
}

TEST_P(JavascriptFilterTest, MinificationFailure) {
  InitFilters();
  SetResponseWithDefaultHeaders("foo.js", kContentTypeJavascript,
                                "/* truncated comment", 100);
  ValidateNoChanges("fail", "<script src=foo.js></script>");

  EXPECT_EQ(0, blocks_minified_->Get());
  EXPECT_EQ(1, minification_failures_->Get());
  EXPECT_EQ(0, num_uses_->Get());
  EXPECT_EQ(1, did_not_shrink_->Get());
}

TEST_P(JavascriptFilterTest, ReuseRewrite) {
  InitFiltersAndTest(100);

  ValidateExpected("reuse_rewrite1",
                   GenerateHtml(kOrigJsName),
                   GenerateHtml(expected_rewritten_path_.c_str()));
  // First time: We minify JS and use the minified version.
  EXPECT_EQ(1, blocks_minified_->Get());
  EXPECT_EQ(1, num_uses_->Get());

  ClearStats();
  ValidateExpected("reuse_rewrite2",
                   GenerateHtml(kOrigJsName),
                   GenerateHtml(expected_rewritten_path_.c_str()));
  // Second time: We reuse the original rewrite.
  EXPECT_EQ(0, blocks_minified_->Get());
  EXPECT_EQ(1, num_uses_->Get());
}

TEST_P(JavascriptFilterTest, NoReuseInline) {
  InitFiltersAndTest(100);

  ValidateExpected("reuse_inline1",
                   StringPrintf(kInlineJs, kJsData),
                   StringPrintf(kInlineJs, kJsMinData));
  // First time: We minify JS and use the minified version.
  EXPECT_EQ(1, blocks_minified_->Get());
  EXPECT_EQ(1, num_uses_->Get());

  ClearStats();
  ValidateExpected("reuse_inline2",
                   StringPrintf(kInlineJs, kJsData),
                   StringPrintf(kInlineJs, kJsMinData));
  // Second time: Apparently we minify it again.
  // NOTE: This test is here to document current behavior. It should be fine
  // to change this behavior so that the rewrite is cached (although it may
  // not be worth it).
  EXPECT_EQ(1, blocks_minified_->Get());
  EXPECT_EQ(1, num_uses_->Get());
}

// See http://github.com/apache/incubator-pagespeed-mod/issues/542
TEST_P(JavascriptFilterTest, ExtraCdataOnMalformedInput) {
  InitFiltersAndTest(100);

  // This is an entirely bogus thing to have in a script tag, but that was
  // what was reported  by a user.  We were wrapping this an an extra CDATA
  // tag, so this test proves we are no longer doing that.
  static const char kIssue542LinkInScript[] =
      "<![CDATA[<link href='http://fonts.googleapis.com/css'>]]>";

  const GoogleString kHtmlInput = StringPrintf(
      kInlineScriptFormat,
      StrCat("\n", kIssue542LinkInScript, "\n").c_str());
  const GoogleString kHtmlOutput = StringPrintf(
      kInlineScriptFormat,
      kIssue542LinkInScript);
  ValidateExpected("broken_cdata", kHtmlInput, kHtmlOutput);
}

TEST_P(JavascriptFilterTest, ValidCdata) {
  InitFiltersAndTest(100);

  const GoogleString kHtmlInput = StringPrintf(
      kInlineScriptFormat,
      StringPrintf(kCdataWrapper, "alert ( 'foo' ) ; \n").c_str());
  const GoogleString kHtmlOutput = StringPrintf(
      kInlineScriptFormat,
      StringPrintf(kCdataWrapper, "alert('foo');").c_str());
  ValidateExpected("valid_cdata", kHtmlInput, kHtmlOutput);
}

TEST_P(JavascriptFilterTest, FlushInInlineJS) {
  InitFilters();
  SetupWriter();
  rewrite_driver()->StartParse(kTestDomain);
  rewrite_driver()->ParseText("<html><body><script>  alert  (  'Hel");
  // Flush in middle of inline JS.
  rewrite_driver()->Flush();
  rewrite_driver()->ParseText("lo, World!'  )  </script></body></html>");
  rewrite_driver()->FinishParse();

  // Expect text to be rewritten because it is coalesced.
  // HtmlParse will send events like this to filter:
  //   StartElement script
  //   Flush
  //   Characters ...
  //   EndElement script
  EXPECT_EQ("<html><body><script>alert('Hello, World!')</script></body></html>",
            output_buffer_);
}

TEST_P(JavascriptFilterTest, FlushInEndTag) {
  InitFilters();
  SetupWriter();
  rewrite_driver()->StartParse(kTestDomain);
  rewrite_driver()->ParseText(
      "<html><body><script>  alert  (  'Hello, World!'  )  </scr");
  // Flush in middle of closing </script> tag.
  rewrite_driver()->Flush();
  rewrite_driver()->ParseText("ipt></body></html>");
  rewrite_driver()->FinishParse();

  // Expect text to be rewritten because it is coalesced.
  // HtmlParse will send events like this to filter:
  //   StartElement script
  //   Characters ...
  //   Flush
  //   EndElement script
  EXPECT_EQ("<html><body><script>alert('Hello, World!')</script></body></html>",
            output_buffer_);
}

TEST_P(JavascriptFilterTest, FlushAfterBeginScript) {
  InitFilters();
  SetupWriter();
  rewrite_driver()->StartParse(kTestDomain);
  rewrite_driver()->ParseText(
      "<html><body><script>");
  rewrite_driver()->Flush();
  rewrite_driver()->ParseText("alert  (  'Hello, World!'  )  </script>"
                              "<script></script></body></html>");
  rewrite_driver()->FinishParse();

  // Expect text to be rewritten because it is coalesced.
  // HtmlParse will send events like this to filter:
  //   StartElement script
  //   Flush
  //   Characters ...
  //   EndElement script
  //   StartElement script
  //   EndElement script
  EXPECT_EQ("<html><body><script>alert('Hello, World!')</script>"
            "<script></script></body></html>",
            output_buffer_);
}

TEST_P(JavascriptFilterTest, StripInlineWhitespaceFlush) {
  // Make sure we strip inline whitespace when minifying external scripts even
  // if there's a flush between open and close.
  InitFiltersAndTest(100);
  SetupWriter();
  const GoogleString kScriptTag =
      StrCat("<script type='text/javascript' src='", kOrigJsName, "'>");
  rewrite_driver()->StartParse(kTestDomain);
  rewrite_driver()->ParseText(kScriptTag);
  rewrite_driver()->ParseText("   \t\n");
  rewrite_driver()->Flush();
  rewrite_driver()->ParseText("   </script>\n");
  rewrite_driver()->FinishParse();
  const GoogleString expected =
      StringPrintf(kHtmlFormat, expected_rewritten_path_.c_str());
  EXPECT_EQ(expected, output_buffer_);
}

TEST_P(JavascriptFilterTest, Aris) {
  options()->EnableFilter(RewriteOptions::kDebug);
  InitFilters();

  const char introspective_js[] =
      "var script_tags = document.getElementsByTagName('script');";
  SetResponseWithDefaultHeaders("introspective.js", kContentTypeJavascript,
                                introspective_js, 100);

  Parse("introspective", GenerateHtml("introspective.js"));
  const GoogleString kInsertComment =
      StrCat(kIntrospectiveJS, "<!--",
             JavascriptCodeBlock::kIntrospectionComment, "-->");
  EXPECT_THAT(output_buffer_, ::testing::HasSubstr(kInsertComment));
}

TEST_P(JavascriptFilterTest, ArisSourceMaps) {
  options()->EnableFilter(RewriteOptions::kIncludeJsSourceMaps);
  options()->EnableFilter(RewriteOptions::kDebug);
  InitFilters();

  const char introspective_js[] =
      "var script_tags = document.getElementsByTagName('script');";
  SetResponseWithDefaultHeaders("introspective.js", kContentTypeJavascript,
                                introspective_js, 100);

  Parse("introspective", GenerateHtml("introspective.js"));
  const GoogleString kInsertComment =
      StrCat(kIntrospectiveJS, "<!--",
             JavascriptCodeBlock::kIntrospectionComment, "-->");
  EXPECT_THAT(output_buffer_, ::testing::HasSubstr(kInsertComment));
}

TEST_P(JavascriptFilterTest, ArisCombineJs) {
  options()->EnableFilter(RewriteOptions::kCombineJavascript);
  options()->EnableFilter(RewriteOptions::kDebug);
  InitFilters();

  const char introspective_js[] =
      "var script_tags = document.getElementsByTagName('script');";
  SetResponseWithDefaultHeaders("introspective.js", kContentTypeJavascript,
                                introspective_js, 100);
  SetResponseWithDefaultHeaders("a.js", kContentTypeJavascript,
                                kJsData, 100);
  SetResponseWithDefaultHeaders("b.js", kContentTypeJavascript,
                                kJsData, 100);

  static const char kHtmlBefore[] =
      "<script type='text/javascript' src='introspective.js'></script>\n"
      "<script type='text/javascript' src='a.js'></script>\n"
      "<script type='text/javascript' src='b.js'></script>\n";
  const GoogleString kHtmlAfter = StrCat(
      kIntrospectiveJS, "<!--", JavascriptCodeBlock::kIntrospectionComment,
      "-->\n", "<script src=\"a.js+b.js.pagespeed.jc.0.js\"></script>",
      "<script>eval(mod_pagespeed_0);</script>\n",
      "<script>eval(mod_pagespeed_0);</script>\n");
  Parse("introspective", kHtmlBefore);
  EXPECT_THAT(output_buffer_, ::testing::HasSubstr(kHtmlAfter));
}

void JavascriptFilterTest::SourceMapTest(StringPiece input_js,
                                         StringPiece expected_output_js,
                                         StringPiece expected_mapping_vlq) {
  UseMd5Hasher();
  options()->EnableFilter(RewriteOptions::kIncludeJsSourceMaps);
  InitFilters();

  SetResponseWithDefaultHeaders("input.js", kContentTypeJavascript,
                                input_js, 100);

  GoogleString expected_map = StrCat(
      ")]}'\n{\"mappings\":\"", expected_mapping_vlq, "\",\"names\":[],"
      "\"sources\":[\"http://test.com/input.js?PageSpeed=off\"],"
      "\"version\":3}\n");

  GoogleString source_map_url =
      Encode(kTestDomain, RewriteOptions::kJavascriptMinSourceMapId,
             hasher()->Hash(expected_map), "input.js", "map");

  GoogleString expected_output = expected_output_js.as_string();
  if (options()->use_experimental_js_minifier()) {
    StrAppend(&expected_output, "\n"
              "//# sourceMappingURL=", source_map_url, "\n");
  }

  const GoogleString rewritten_js_name =
      Encode("", RewriteOptions::kJavascriptMinId,
             hasher()->Hash(expected_output), "input.js", "js");
  ValidateExpected("source_maps",
                   GenerateHtml("input.js"),
                   GenerateHtml(rewritten_js_name.c_str()));


  GoogleString output_js;
  EXPECT_TRUE(FetchResourceUrl(StrCat(kTestDomain, rewritten_js_name),
                               &output_js));
  EXPECT_STREQ(expected_output, output_js);

  if (options()->use_experimental_js_minifier()) {
    GoogleString map;
    EXPECT_TRUE(FetchResourceUrl(source_map_url, &map));
    EXPECT_STREQ(expected_map, map);

    // Test Resource flow without HTML flow.
    ServeResourceFromManyContexts(source_map_url, expected_map);

    // Test fetching Source Map with wrong/out-of-date hash.
    GoogleString different_hash_url =
        Encode(kTestDomain, RewriteOptions::kJavascriptMinSourceMapId,
               "Different", "input.js", "map");
    ResponseHeaders map_headers;
    // Test both the uncached and cached case.
    for (int i = 0; i < 2; ++i) {
      EXPECT_TRUE(FetchResourceUrl(different_hash_url, &map, &map_headers));
      EXPECT_EQ(HttpStatus::kNotFound, map_headers.status_code());
      EXPECT_STREQ(RewriteContext::kHashMismatchMessage, map);
    }
  }
}

TEST_P(JavascriptFilterTest, SourceMapsSimple) {
  const char input_js[] = "  foo  bar  ";
  const char expected_output_js[] = "foo bar";
  const char vlq[] =
      // Comment format: (gen_line, gen_col, src_file, src_line, src_col) token
      "AAAE,"  // (0,  0,  0,  0,  2)  foo [space]
      "IAAK";  // (0, +4, +0, +0, +5)  bar
  SourceMapTest(input_js, expected_output_js, vlq);
}

TEST_P(JavascriptFilterTest, SourceMapsMedium) {
  const char input_js[] =
      "alert     (    'hello, world!'    ) \n"
      " /* removed */ <!-- removed --> \n"
      " // single-line-comment\n"
      "document.write( \"<!-- comment -->\" );";
  const char expected_output_js[] =
      "alert('hello, world!')\n"
      "document.write(\"<!-- comment -->\");";
  const char vlq[] =
      // Comment format: (gen_line, gen_col, src_file, src_line, src_col) token
      "AAAA,"    // (0,   0,  0,  0,   0)  alert
      "KAAU,"    // (0,  +5, +0, +0, +10)  (
      "CAAK,"    // (0,  +1, +0, +0,  +5)  'hello, world!'
      "eAAmB;"   // (0, +15, +0, +0, +19)  )
      "AAGlC,"   // (1,   0, +0, +3, -34)  document.write(
      "eAAgB,"   // (1, +15, +0, +0, +16)  "<!-- comment -->"
      "kBAAmB";  // (1, +18, +0, +0, +19)  );
  SourceMapTest(input_js, expected_output_js, vlq);
}

TEST_P(JavascriptFilterTest, NoSourceMapJsCombine) {
  options()->EnableFilter(RewriteOptions::kCombineJavascript);
  options()->EnableFilter(RewriteOptions::kIncludeJsSourceMaps);
  InitFilters();

  SetResponseWithDefaultHeaders("a.js", kContentTypeJavascript,
                                kJsData, 100);
  SetResponseWithDefaultHeaders("b.js", kContentTypeJavascript,
                                kJsData, 100);

  const char combined_name[] = "a.js+b.js.pagespeed.jc.0.js";

  static const char kHtmlBefore[] =
      "<script type='text/javascript' src='a.js'></script>\n"
      "<script type='text/javascript' src='b.js'></script>\n";
  GoogleString kHtmlAfter = StrCat(
      "<script src=\"", combined_name, "\"></script>"
      "<script>eval(mod_pagespeed_0);</script>\n"
      "<script>eval(mod_pagespeed_0);</script>\n");
  ValidateExpected("introspective", kHtmlBefore, kHtmlAfter);

  // Note: There is no //# ScriptSourceMap in combine output.
  GoogleString expected_output = StrCat(
      "var mod_pagespeed_0 = \"", kJsMinData, "\";\n"
      "var mod_pagespeed_0 = \"", kJsMinData, "\";\n");
  GoogleString output_js;
  EXPECT_TRUE(FetchResourceUrl(StrCat(kTestDomain, combined_name), &output_js));
  EXPECT_EQ(expected_output, output_js);
}

TEST_P(JavascriptFilterTest, SourceMapUnsanitaryUrl) {
  if (!options()->use_experimental_js_minifier()) return;

  options()->EnableFilter(RewriteOptions::kIncludeJsSourceMaps);
  InitFilters();
  // Most servers will ignore unknown query params.
  mock_url_fetcher()->set_strip_query_params(true);

  SetResponseWithDefaultHeaders("input.js", kContentTypeJavascript,
                                kJsData, 100);

  GoogleString unsanitary_url =
      Encode(kTestDomain, RewriteOptions::kJavascriptMinId,
             "0", "input.js?evil=\n", "js");

  GoogleString output_js;
  EXPECT_TRUE(FetchResourceUrl(unsanitary_url, &output_js));
  // Note: The important thing is that there's no newline in the mapping URL.
  GoogleString expected_output_js = StrCat(
      kJsMinData, "\n//# sourceMappingURL="
      "http://test.com/input.js,qevil=.pagespeed.sm.0.map\n");
  EXPECT_EQ(expected_output_js, output_js);
}

// For non-pagespeed input JS, we must add ?PageSpeed=off to the source URL
// to avoid IPRO rewriting the source. However, we should not add ?PageSpeed=off
// for .pagespeed. input files, because that doesn't make any sense.
// https://github.com/apache/incubator-pagespeed-mod/issues/1043
TEST_P(JavascriptFilterTest, ProperSourceMapForPagespeedInput) {
  if (!options()->use_experimental_js_minifier()) return;

  options()->EnableFilter(RewriteOptions::kIncludeJsSourceMaps);
  options()->EnableFilter(RewriteOptions::kRewriteJavascriptExternal);
  options()->EnableFilter(RewriteOptions::kOutlineJavascript);
  options()->set_js_outline_min_bytes(0);  // Make sure everything is outlined.
  UseMd5Hasher();  // We should use a real hasher for outlining.
  rewrite_driver_->AddFilters();

  const char input_js[] = "foo = 1;";
  GoogleString outlined_js_tail =
      Encode("", JsOutlineFilter::kFilterId,
             hasher()->Hash(input_js), "_", "js");

  const char expected_mapping_vlq[] = "AAAA,GAAI,CAAE";
  // Note: No ?PageSpeed=off
  GoogleString expected_source_map = StrCat(
      ")]}'\n{\"mappings\":\"", expected_mapping_vlq, "\",\"names\":[],"
      "\"sources\":[\"", kTestDomain, outlined_js_tail, "\"],"
      "\"version\":3}\n");
  GoogleString source_map_url =
      Encode(kTestDomain, RewriteOptions::kJavascriptMinSourceMapId,
             hasher()->Hash(expected_source_map), outlined_js_tail, "map");

  const char rewritten_js[] = "foo=1;";
  GoogleString expected_output_js =
      StrCat(rewritten_js, "\n"
             "//# sourceMappingURL=", source_map_url, "\n");
  GoogleString rewritten_js_url =
      Encode(kTestDomain, RewriteOptions::kJavascriptMinId,
             hasher()->Hash(expected_output_js), outlined_js_tail, "js");

  GoogleString input_html = StrCat("<script>", input_js, "</script>");
  GoogleString expected_output_html =
      StrCat("<script src=\"", rewritten_js_url, "\"></script>");
  ValidateExpected("source_map_pagespeed", input_html, expected_output_html);

  GoogleString output_js;
  EXPECT_TRUE(FetchResourceUrl(rewritten_js_url, &output_js));
  EXPECT_EQ(expected_output_js, output_js);

  GoogleString source_map;
  EXPECT_TRUE(FetchResourceUrl(source_map_url, &source_map));
  EXPECT_EQ(expected_source_map, source_map);
}

TEST_P(JavascriptFilterTest, SourceMapOnDemandNotEnabled) {
  options()->DisableFilter(RewriteOptions::kIncludeJsSourceMaps);
  options()->EnableFilter(RewriteOptions::kRewriteJavascriptExternal);
  rewrite_driver_->AddFilters();

  // From SourceMapsSimple
  const char input_js[] = "  foo  bar  ";
  const char expected_vlq[] = "AAAE,IAAK";
  GoogleString expected_map = StrCat(
      ")]}'\n{\"mappings\":\"", expected_vlq, "\",\"names\":[],"
      "\"sources\":[\"http://test.com/input.js?PageSpeed=off\"],"
      "\"version\":3}\n");

  SetResponseWithDefaultHeaders("input.js", kContentTypeJavascript,
                                input_js, 100);

  GoogleString source_map_url =
      Encode(kTestDomain, RewriteOptions::kJavascriptMinSourceMapId,
             "0", "input.js", "map");

  GoogleString source_map;
  bool result = FetchResourceUrl(source_map_url, &source_map);
  if (options()->use_experimental_js_minifier()) {
    EXPECT_TRUE(result);
    EXPECT_STREQ(expected_map, source_map);
  }

  // Note: !options()->use_experimental_js_minifier() also checks the
  // code_block.SourceMappings().empty() case.
}

// If JS isn't optimizable, do not fallback to serving js for source_map!
TEST_P(JavascriptFilterTest, SourceMapNoOpt) {
  if (!options()->use_experimental_js_minifier()) return;

  options()->EnableFilter(RewriteOptions::kIncludeJsSourceMaps);
  InitFilters();

  // Empty contents (not optimizable).
  SetResponseWithDefaultHeaders("input.js", kContentTypeJavascript, "", 100);

  GoogleString source_map_url =
      Encode(kTestDomain, RewriteOptions::kJavascriptMinSourceMapId,
             "0", "input.js", "map");

  // Test both the uncached and cached case.
  for (int i = 0; i < 2; ++i) {
    GoogleString map;
    // Note: Right now we are failing the fetch in this case, but 404ing would
    // also be reasonable. If we change that, update this test.
    EXPECT_FALSE(FetchResourceUrl(source_map_url, &map));
  }
}

TEST_P(JavascriptFilterTest, InlineAndNotExternal) {
  options()->EnableFilter(RewriteOptions::kRewriteJavascriptInline);
  options()->DisableFilter(RewriteOptions::kRewriteJavascriptExternal);
  rewrite_driver_->AddFilters();
  InitTest(100);
  ValidateExpected("inline_not_external",
                   StrCat(StringPrintf(kInlineJs, kJsData),
                          StringPrintf(kHtmlFormat, kOrigJsName)),
                   StrCat(StringPrintf(kInlineJs, kJsMinData),
                          StringPrintf(kHtmlFormat, kOrigJsName)));
}

TEST_P(JavascriptFilterTest, InlineAndNotExternalPreserve) {
  // js_preserve_urls should not affect minification of inline JS.
  options()->set_js_preserve_urls(true);
  options()->set_in_place_preemptive_rewrite_javascript(false);
  options()->EnableFilter(RewriteOptions::kRewriteJavascriptInline);
  options()->DisableFilter(RewriteOptions::kRewriteJavascriptExternal);
  rewrite_driver_->AddFilters();
  InitTest(100);
  ValidateExpected("inline_not_external",
                   StrCat(StringPrintf(kInlineJs, kJsData),
                          StringPrintf(kHtmlFormat, kOrigJsName)),
                   StrCat(StringPrintf(kInlineJs, kJsMinData),
                          StringPrintf(kHtmlFormat, kOrigJsName)));
}

TEST_P(JavascriptFilterTest, InlineAndCanonicalNotExternal) {
  options()->EnableFilter(RewriteOptions::kRewriteJavascriptInline);
  options()->EnableFilter(RewriteOptions::kCanonicalizeJavascriptLibraries);
  options()->DisableFilter(RewriteOptions::kRewriteJavascriptExternal);
  rewrite_driver_->AddFilters();
  InitTest(100);
  ValidateExpected("inline_and_canonical_and_not_external",
                   StrCat(StringPrintf(kInlineJs, kJsData),
                          StringPrintf(kHtmlFormat, kOrigJsName)),
                   StrCat(StringPrintf(kInlineJs, kJsMinData),
                          StringPrintf(kHtmlFormat, kOrigJsName)));
}

TEST_P(JavascriptFilterTest, ExternalAndNotInline) {
  options()->EnableFilter(RewriteOptions::kRewriteJavascriptExternal);
  options()->DisableFilter(RewriteOptions::kRewriteJavascriptInline);
  rewrite_driver_->AddFilters();
  InitTest(100);
  ValidateExpected("external_not_inline",
                   StrCat(StringPrintf(kInlineJs, kJsData),
                          StringPrintf(kHtmlFormat, kOrigJsName)),
                   StrCat(StringPrintf(kInlineJs, kJsData),
                          StringPrintf(kHtmlFormat,
                                       expected_rewritten_path_.c_str())));
}

TEST_P(JavascriptFilterTest, ContentTypeValidation) {
  ValidateFallbackHeaderSanitization(kFilterId);
}

TEST_P(JavascriptFilterTest, BasicCsp) {
  InitFilters();
  EnableDebug();

  SetResponseWithDefaultHeaders(
      "scripts/a.js", kContentTypeJavascript, kJsData, 100);
  SetResponseWithDefaultHeaders(
      "uploads/sneaky.png", kContentTypeJavascript, kJsData, 100);

  static const char kCsp[] =
      "<meta http-equiv=\"Content-Security-Policy\" "
      "content=\"script-src */scripts/;  default-src */uploads/\">";

  ValidateExpected(
      "basic_csp",
      StrCat(kCsp,
             ScriptSrc("scripts/a.js"),
             ScriptSrc("uploads/sneaky.png")),
      StrCat(kCsp,
             ScriptSrc(Encode("scripts/", "jm", "0", "a.js", "js")),
             ScriptSrc("uploads/sneaky.png"),
              "<!--The preceding resource was not rewritten "
             "because CSP disallows its fetch-->"));
}

TEST_P(JavascriptFilterTest, RenderCsp) {
  InitFilters();
  EnableDebug();

  SetResponseWithDefaultHeaders(
      "scripts/a.js", kContentTypeJavascript, kJsData, 100);
  SetResponseWithDefaultHeaders(
      "uploads/sneaky.png", kContentTypeJavascript, kJsData, 100);

  static const char kCsp[] =
      "<meta http-equiv=\"Content-Security-Policy\" "
      "content=\"script-src */scripts/a.js;\">";

  // First try w/o CSP, should rewrite.
  ValidateExpected(
      "no_csp",
      ScriptSrc("scripts/a.js"),
      ScriptSrc(Encode("scripts/", "jm", "0", "a.js", "js")));

  // Now render again w/CSP -- blocked since .pagespeed. resource isn't
  // permitted.
  ValidateExpected(
      "render_csp",
      StrCat(kCsp, ScriptSrc("scripts/a.js")),
      StrCat(kCsp, ScriptSrc("scripts/a.js"),
             "<!--PageSpeed output (by JavascriptFilter) not permitted by "
             "Content Security Policy-->"));
}

TEST_P(JavascriptFilterTest, CspIrrelevant) {
  InitFilters();
  EnableDebug();

  SetResponseWithDefaultHeaders(
      "scripts/a.js", kContentTypeJavascript, kJsData, 100);

  static const char kCsp[] =
      "<meta http-equiv=\"Content-Security-Policy\" "
      "content=\"img-src https:\">";

  ValidateExpected(
      "basic_csp",
      StrCat(kCsp,
             ScriptSrc("scripts/a.js")),
      StrCat(kCsp,
             ScriptSrc(Encode("scripts/", "jm", "0", "a.js", "js"))));
}

TEST_P(JavascriptFilterTest, InlineCsp) {
  InitFilters();
  EnableDebug();

  static const char kCsp[] =
      "<meta http-equiv=\"Content-Security-Policy\" "
      "content=\"script-src */scripts/;  default-src */uploads/\">";
  static const char kScript[] =
      "<script> var a  = 42;</script>";

  ValidateExpected(
      "inline_csp",
      StrCat(kCsp, kScript),
      StrCat(kCsp, kScript,
             "<!--Avoiding modifying inline script with CSP present-->"));
}

TEST_P(JavascriptFilterTest, InlineCsp2) {
  InitFilters();
  EnableDebug();

  static const char kCsp[] =
      "<meta http-equiv=\"Content-Security-Policy\" "
      "content=\"default-src */uploads/\">";
  static const char kScript[] =
      "<script> var a  = 42;</script>";

  ValidateExpected(
      "inline_csp2",
      StrCat(kCsp, kScript),
      StrCat(kCsp, kScript,
             "<!--Avoiding modifying inline script with CSP present-->"));
}

TEST_P(JavascriptFilterTest, InlineCsp3) {
  InitFilters();
  EnableDebug();

  // This one doesn't restrict scripts.
  static const char kCsp[] =
      "<meta http-equiv=\"Content-Security-Policy\" "
      "content=\"img-src */uploads/\">";
  static const char kScript[] =
      "<script> var a  = 42;</script>";
  static const char kScriptMin[] =
      "<script>var a=42;</script>";

  ValidateExpected(
      "inline_csp3",
      StrCat(kCsp, kScript),
      StrCat(kCsp, kScriptMin));
}

TEST_P(JavascriptFilterTest, CspBaseUri) {
  InitFilters();
  EnableDebug();
  SetResponseWithDefaultHeaders(
      "scripts/a.js", kContentTypeJavascript, kJsData, 100);

  static const char kCspAndBase[] =
      "<meta http-equiv=\"Content-Security-Policy\" "
      "content=\"base-uri whatever; script-src *\">"
      "<base href=\"http://test.com/\">";

  ValidateExpected(
      "base_uri_csp",
      StrCat(kCspAndBase,
             ScriptSrc("scripts/a.js"),
             ScriptSrc("http://test.com/scripts/a.js")),
      StrCat(kCspAndBase,
             "<!--Unable to check safety of a base with CSP base-uri, "
             "proceeding conservatively.-->",
             ScriptSrc("scripts/a.js"),
             ScriptSrc("http://test.com/scripts/a.js.pagespeed.jm.0.js")));
}

// We test with use_experimental_minifier == GetParam() as both true and false.
INSTANTIATE_TEST_CASE_P(JavascriptFilterTestInstance, JavascriptFilterTest,
                        ::testing::Bool());

}  // namespace net_instaweb
