| // Copyright 2014 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "base/files/file_path.h" |
| #include "base/scoped_observer.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/strings/stringprintf.h" |
| #include "chrome/browser/extensions/activity_log/activity_actions.h" |
| #include "chrome/browser/extensions/activity_log/activity_log.h" |
| #include "chrome/browser/extensions/activity_log/ad_network_database.h" |
| #include "chrome/browser/extensions/extension_browsertest.h" |
| #include "chrome/browser/extensions/extension_test_message_listener.h" |
| #include "chrome/test/base/ui_test_utils.h" |
| #include "extensions/common/extension.h" |
| #include "net/test/embedded_test_server/embedded_test_server.h" |
| #include "net/test/embedded_test_server/http_response.h" |
| #include "url/gurl.h" |
| |
| namespace net { |
| namespace test_server { |
| struct HttpRequest; |
| } |
| } |
| |
| namespace extensions { |
| |
| namespace { |
| |
| // The "ad network" that we are using. Any src or href equal to this should be |
| // considered an ad network. |
| const char kAdNetwork[] = "http://www.known-ads.adnetwork"; |
| |
| // The current stage of the test. |
| enum Stage { |
| BEFORE_RESET, // We are about to reset the page. |
| RESETTING, // We are resetting the page. |
| TESTING // The reset is complete, and we are testing. |
| }; |
| |
| // The string sent by the test to indicate that the page reset will begin. |
| const char kResetBeginString[] = "Page Reset Begin"; |
| // The string sent by the test to indicate that page reset is complete. |
| const char kResetEndString[] = "Page Reset End"; |
| // The string sent by the test to indicate a JS error was caught in the test. |
| const char kJavascriptErrorString[] = "Testing Error"; |
| // The string sent by the test to indicate that we have concluded the full test. |
| const char kTestCompleteString[] = "Test Complete"; |
| |
| // An implementation of ActivityLog::Observer that, for every action, sends it |
| // through Action::DidInjectAd(). This will keep track of the observed |
| // injections, and can be enabled or disabled as needed (for instance, this |
| // should be disabled while we are resetting the page). |
| class ActivityLogObserver : public ActivityLog::Observer { |
| public: |
| explicit ActivityLogObserver(content::BrowserContext* context); |
| virtual ~ActivityLogObserver(); |
| |
| void set_enabled(bool enabled) { enabled_ = enabled; } |
| size_t injection_count() const { return injection_count_; } |
| |
| private: |
| virtual void OnExtensionActivity(scoped_refptr<Action> action) OVERRIDE; |
| |
| ScopedObserver<ActivityLog, ActivityLog::Observer> scoped_observer_; |
| content::BrowserContext* context_; |
| size_t injection_count_; |
| bool enabled_; |
| }; |
| |
| ActivityLogObserver::ActivityLogObserver(content::BrowserContext* context) |
| : scoped_observer_(this), |
| context_(context), |
| injection_count_(0u), |
| enabled_(false) { |
| ActivityLog::GetInstance(context_)->AddObserver(this); |
| } |
| |
| ActivityLogObserver::~ActivityLogObserver() {} |
| |
| void ActivityLogObserver::OnExtensionActivity(scoped_refptr<Action> action) { |
| if (enabled_ && action->DidInjectAd(NULL /* no rappor service */) != |
| Action::NO_AD_INJECTION) { |
| ++injection_count_; |
| } |
| } |
| |
| // A mock for the AdNetworkDatabase. This simply says that the URL |
| // http://www.known-ads.adnetwork is an ad network, and nothing else is. |
| class TestAdNetworkDatabase : public AdNetworkDatabase { |
| public: |
| TestAdNetworkDatabase(); |
| virtual ~TestAdNetworkDatabase(); |
| |
| private: |
| virtual bool IsAdNetwork(const GURL& url) const OVERRIDE; |
| |
| GURL ad_network_url_; |
| }; |
| |
| TestAdNetworkDatabase::TestAdNetworkDatabase() : ad_network_url_(kAdNetwork) {} |
| TestAdNetworkDatabase::~TestAdNetworkDatabase() {} |
| |
| bool TestAdNetworkDatabase::IsAdNetwork(const GURL& url) const { |
| return url == ad_network_url_; |
| } |
| |
| scoped_ptr<net::test_server::HttpResponse> HandleRequest( |
| const net::test_server::HttpRequest& request) { |
| scoped_ptr<net::test_server::BasicHttpResponse> response( |
| new net::test_server::BasicHttpResponse()); |
| response->set_code(net::HTTP_OK); |
| return response.PassAs<net::test_server::HttpResponse>(); |
| } |
| |
| } // namespace |
| |
| class AdInjectionBrowserTest : public ExtensionBrowserTest { |
| protected: |
| AdInjectionBrowserTest(); |
| virtual ~AdInjectionBrowserTest(); |
| |
| virtual void SetUpOnMainThread() OVERRIDE; |
| virtual void TearDownOnMainThread() OVERRIDE; |
| |
| // Handle the "Reset Begin" stage of the test. |
| testing::AssertionResult HandleResetBeginStage(); |
| |
| // Handle the "Reset End" stage of the test. |
| testing::AssertionResult HandleResetEndStage(); |
| |
| // Handle the "Testing" stage of the test. |
| testing::AssertionResult HandleTestingStage(const std::string& message); |
| |
| // Handle a JS error encountered in a test. |
| testing::AssertionResult HandleJSError(const std::string& message); |
| |
| const base::FilePath& test_data_dir() { return test_data_dir_; } |
| |
| ExtensionTestMessageListener* listener() { return listener_.get(); } |
| |
| ActivityLogObserver* observer() { return observer_.get(); } |
| |
| void set_expected_injections(size_t expected_injections) { |
| expected_injections_ = expected_injections; |
| } |
| |
| private: |
| // The name of the last completed test; used in case of unexpected failure for |
| // debugging. |
| std::string last_test_; |
| |
| // The number of expected injections. |
| size_t expected_injections_; |
| |
| // A listener for any messages from our ad-injecting extension. |
| scoped_ptr<ExtensionTestMessageListener> listener_; |
| |
| // An observer to be alerted when we detect ad injection. |
| scoped_ptr<ActivityLogObserver> observer_; |
| |
| // The current stage of the test. |
| Stage stage_; |
| }; |
| |
| AdInjectionBrowserTest::AdInjectionBrowserTest() |
| : expected_injections_(0u), stage_(BEFORE_RESET) {} |
| |
| AdInjectionBrowserTest::~AdInjectionBrowserTest() {} |
| |
| void AdInjectionBrowserTest::SetUpOnMainThread() { |
| ExtensionBrowserTest::SetUpOnMainThread(); |
| |
| ASSERT_TRUE(embedded_test_server()->InitializeAndWaitUntilReady()); |
| embedded_test_server()->RegisterRequestHandler(base::Bind(&HandleRequest)); |
| |
| test_data_dir_ = |
| test_data_dir_.AppendASCII("activity_log").AppendASCII("ad_injection"); |
| observer_.reset(new ActivityLogObserver(profile())); |
| |
| // We use a listener in order to keep the actions in the Javascript test |
| // synchronous. At the end of each stage, the test will send us a message |
| // with the stage and status, and will not advance until we reply with |
| // a message. |
| listener_.reset(new ExtensionTestMessageListener(true /* will reply */)); |
| |
| // Enable the activity log for this test. |
| ActivityLog::GetInstance(profile())->SetWatchdogAppActiveForTesting(true); |
| |
| // Set the ad network database. |
| AdNetworkDatabase::SetForTesting( |
| scoped_ptr<AdNetworkDatabase>(new TestAdNetworkDatabase)); |
| } |
| |
| void AdInjectionBrowserTest::TearDownOnMainThread() { |
| observer_.reset(NULL); |
| listener_.reset(NULL); |
| ActivityLog::GetInstance(profile())->SetWatchdogAppActiveForTesting(false); |
| |
| ExtensionBrowserTest::TearDownOnMainThread(); |
| } |
| |
| testing::AssertionResult AdInjectionBrowserTest::HandleResetBeginStage() { |
| if (stage_ != BEFORE_RESET) { |
| return testing::AssertionFailure() |
| << "In incorrect stage. Last Test: " << last_test_; |
| } |
| |
| // Stop looking for ad injection, since some of the reset could be considered |
| // ad injection. |
| observer()->set_enabled(false); |
| stage_ = RESETTING; |
| return testing::AssertionSuccess(); |
| } |
| |
| testing::AssertionResult AdInjectionBrowserTest::HandleResetEndStage() { |
| if (stage_ != RESETTING) { |
| return testing::AssertionFailure() |
| << "In incorrect stage. Last test: " << last_test_; |
| } |
| |
| // Look for ad injection again, now that the reset is over. |
| observer()->set_enabled(true); |
| stage_ = TESTING; |
| return testing::AssertionSuccess(); |
| } |
| |
| testing::AssertionResult AdInjectionBrowserTest::HandleTestingStage( |
| const std::string& message) { |
| if (stage_ != TESTING) { |
| return testing::AssertionFailure() |
| << "In incorrect stage. Last test: " << last_test_; |
| } |
| |
| // The format for a testing message is: |
| // "<test_name>:<expected_change>" |
| // where <test_name> is the name of the test and <expected_change> is |
| // either -1 for no ad injection (to test against false positives) or the |
| // number corresponding to ad_detection::InjectionType. |
| size_t sep = message.find(':'); |
| int expected_change = -1; |
| if (sep == std::string::npos || |
| !base::StringToInt(message.substr(sep + 1), &expected_change) || |
| (expected_change < Action::NO_AD_INJECTION || |
| expected_change >= Action::NUM_INJECTION_TYPES)) { |
| return testing::AssertionFailure() |
| << "Invalid message received for testing stage: " << message; |
| } |
| |
| last_test_ = message.substr(0, sep); |
| |
| // TODO(rdevlin.cronin): Currently, we lump all kinds of ad injection into |
| // one counter, because we can't differentiate (or catch all of them). Change |
| // this when we can. |
| // Increment the expected change, and compare. |
| if (expected_change != Action::NO_AD_INJECTION) |
| ++expected_injections_; |
| std::string error; |
| if (expected_injections_ != observer()->injection_count()) { |
| // We need these static casts, because size_t is different on different |
| // architectures, and printf becomes unhappy. |
| error = |
| base::StringPrintf("Injection Count Mismatch: Expected %u, Actual %u", |
| static_cast<unsigned int>(expected_injections_), |
| static_cast<unsigned int>( |
| observer()->injection_count())); |
| } |
| |
| stage_ = BEFORE_RESET; |
| |
| if (!error.empty()) |
| return testing::AssertionFailure() << error; |
| |
| return testing::AssertionSuccess(); |
| } |
| |
| testing::AssertionResult AdInjectionBrowserTest::HandleJSError( |
| const std::string& message) { |
| // The format for a testing message is: |
| // "Testing Error:<test_name>:<error>" |
| // where <test_name> is the name of the test and <error> is the error which |
| // was encountered. |
| size_t first_sep = message.find(':'); |
| size_t second_sep = message.find(':', first_sep + 1); |
| if (first_sep == std::string::npos || second_sep == std::string::npos) { |
| return testing::AssertionFailure() |
| << "Invalid message received: " << message; |
| } |
| |
| std::string test_name = |
| message.substr(first_sep + 1, second_sep - first_sep - 1); |
| std::string test_err = message.substr(second_sep + 1); |
| |
| // We set the stage here, so that subsequent tests don't fail. |
| stage_ = BEFORE_RESET; |
| |
| return testing::AssertionFailure() << "Javascript Error in test '" |
| << test_name << "': " << test_err; |
| } |
| |
| // This is the primary Ad-Injection browser test. It loads an extension that |
| // has a content script that, in turn, injects ads left, right, and center. |
| // The content script waits after each injection for a response from this |
| // browsertest, in order to ensure synchronicity. After each injection, the |
| // content script cleans up after itself. For significantly more detailed |
| // comments, see |
| // chrome/test/data/extensions/activity_log/ad_injection/content_script.js. |
| IN_PROC_BROWSER_TEST_F(AdInjectionBrowserTest, DetectAdInjections) { |
| const Extension* extension = LoadExtension(test_data_dir_); |
| ASSERT_TRUE(extension); |
| |
| ui_test_utils::NavigateToURL(browser(), embedded_test_server()->GetURL("/")); |
| |
| std::string message; |
| while (message != "TestComplete") { |
| listener()->WaitUntilSatisfied(); |
| message = listener()->message(); |
| if (message == kResetBeginString) { |
| ASSERT_TRUE(HandleResetBeginStage()); |
| } else if (message == kResetEndString) { |
| ASSERT_TRUE(HandleResetEndStage()); |
| } else if (!message.compare( |
| 0, strlen(kJavascriptErrorString), kJavascriptErrorString)) { |
| EXPECT_TRUE(HandleJSError(message)); |
| } else if (message == kTestCompleteString) { |
| break; // We're done! |
| } else { // We're in some kind of test. |
| EXPECT_TRUE(HandleTestingStage(message)); |
| } |
| |
| // We set the expected injections to be whatever they actually are so that |
| // we only fail one test, instead of all subsequent tests. |
| set_expected_injections(observer()->injection_count()); |
| |
| // In all cases (except for "Test Complete", in which case we already |
| // break'ed), we reply with a continue message. |
| listener()->Reply("Continue"); |
| listener()->Reset(); |
| } |
| } |
| |
| // TODO(rdevlin.cronin): We test a good amount of ways of injecting ads with |
| // the above test, but more is better in testing. |
| // See crbug.com/357204. |
| |
| } // namespace extensions |