1 // Copyright 2014 The Chromium Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 #include "base/files/file_path.h" 6 #include "base/scoped_observer.h" 7 #include "base/strings/string_number_conversions.h" 8 #include "base/strings/string_util.h" 9 #include "base/strings/stringprintf.h" 10 #include "chrome/browser/extensions/activity_log/activity_actions.h" 11 #include "chrome/browser/extensions/activity_log/activity_log.h" 12 #include "chrome/browser/extensions/activity_log/ad_network_database.h" 13 #include "chrome/browser/extensions/extension_browsertest.h" 14 #include "chrome/browser/extensions/extension_test_message_listener.h" 15 #include "chrome/test/base/ui_test_utils.h" 16 #include "extensions/common/extension.h" 17 #include "net/test/embedded_test_server/embedded_test_server.h" 18 #include "net/test/embedded_test_server/http_response.h" 19 #include "url/gurl.h" 20 21 namespace net { 22 namespace test_server { 23 struct HttpRequest; 24 } 25 } 26 27 namespace extensions { 28 29 namespace { 30 31 // The "ad network" that we are using. Any src or href equal to this should be 32 // considered an ad network. 33 const char kAdNetwork1[] = "http://www.known-ads.adnetwork"; 34 const char kAdNetwork2[] = "http://www.also-known-ads.adnetwork"; 35 36 // The current stage of the test. 37 enum Stage { 38 BEFORE_RESET, // We are about to reset the page. 39 RESETTING, // We are resetting the page. 40 TESTING // The reset is complete, and we are testing. 41 }; 42 43 // The string sent by the test to indicate that the page reset will begin. 44 const char kResetBeginString[] = "Page Reset Begin"; 45 // The string sent by the test to indicate that page reset is complete. 46 const char kResetEndString[] = "Page Reset End"; 47 // The string sent by the test to indicate a JS error was caught in the test. 48 const char kJavascriptErrorString[] = "Testing Error"; 49 // The string sent by the test to indicate that we have concluded the full test. 50 const char kTestCompleteString[] = "Test Complete"; 51 52 std::string InjectionTypeToString(Action::InjectionType type) { 53 switch (type) { 54 case Action::NO_AD_INJECTION: 55 return "No Ad Injection"; 56 case Action::INJECTION_NEW_AD: 57 return "Injection New Ad"; 58 case Action::INJECTION_REMOVED_AD: 59 return "Injection Removed Ad"; 60 case Action::INJECTION_REPLACED_AD: 61 return "Injection Replaced Ad"; 62 case Action::INJECTION_LIKELY_NEW_AD: 63 return "Injection Likely New Ad"; 64 case Action::INJECTION_LIKELY_REPLACED_AD: 65 return "Injection Likely Replaced Ad"; 66 case Action::NUM_INJECTION_TYPES: 67 return "Num Injection Types"; 68 } 69 return std::string(); 70 } 71 72 // An implementation of ActivityLog::Observer that, for every action, sends it 73 // through Action::DidInjectAd(). This will keep track of the observed 74 // injections, and can be enabled or disabled as needed (for instance, this 75 // should be disabled while we are resetting the page). 76 class ActivityLogObserver : public ActivityLog::Observer { 77 public: 78 explicit ActivityLogObserver(content::BrowserContext* context); 79 virtual ~ActivityLogObserver(); 80 81 // Disable the observer (e.g., to reset the page). 82 void disable() { enabled_ = false; } 83 84 // Enable the observer, resetting the state. 85 void enable() { 86 injection_type_ = Action::NO_AD_INJECTION; 87 found_multiple_injections_ = false; 88 enabled_ = true; 89 } 90 91 Action::InjectionType injection_type() const { return injection_type_; } 92 93 bool found_multiple_injections() const { return found_multiple_injections_; } 94 95 private: 96 virtual void OnExtensionActivity(scoped_refptr<Action> action) OVERRIDE; 97 98 ScopedObserver<ActivityLog, ActivityLog::Observer> scoped_observer_; 99 100 // The associated BrowserContext. 101 content::BrowserContext* context_; 102 103 // The type of the last injection. 104 Action::InjectionType injection_type_; 105 106 // Whether or not we found multiple injection types (which shouldn't happen). 107 bool found_multiple_injections_; 108 109 // Whether or not the observer is enabled. 110 bool enabled_; 111 }; 112 113 ActivityLogObserver::ActivityLogObserver(content::BrowserContext* context) 114 : scoped_observer_(this), 115 context_(context), 116 injection_type_(Action::NO_AD_INJECTION), 117 found_multiple_injections_(false), 118 enabled_(false) { 119 ActivityLog::GetInstance(context_)->AddObserver(this); 120 } 121 122 ActivityLogObserver::~ActivityLogObserver() {} 123 124 void ActivityLogObserver::OnExtensionActivity(scoped_refptr<Action> action) { 125 if (!enabled_) 126 return; 127 128 Action::InjectionType type = 129 action->DidInjectAd(NULL /* no rappor service */); 130 if (type != Action::NO_AD_INJECTION) { 131 if (injection_type_ != Action::NO_AD_INJECTION) 132 found_multiple_injections_ = true; 133 injection_type_ = type; 134 } 135 } 136 137 // A mock for the AdNetworkDatabase. This simply says that the URL 138 // http://www.known-ads.adnetwork is an ad network, and nothing else is. 139 class TestAdNetworkDatabase : public AdNetworkDatabase { 140 public: 141 TestAdNetworkDatabase(); 142 virtual ~TestAdNetworkDatabase(); 143 144 private: 145 virtual bool IsAdNetwork(const GURL& url) const OVERRIDE; 146 147 GURL ad_network_url1_; 148 GURL ad_network_url2_; 149 }; 150 151 TestAdNetworkDatabase::TestAdNetworkDatabase() : ad_network_url1_(kAdNetwork1), 152 ad_network_url2_(kAdNetwork2) { 153 } 154 155 TestAdNetworkDatabase::~TestAdNetworkDatabase() {} 156 157 bool TestAdNetworkDatabase::IsAdNetwork(const GURL& url) const { 158 return url == ad_network_url1_ || url == ad_network_url2_; 159 } 160 161 scoped_ptr<net::test_server::HttpResponse> HandleRequest( 162 const net::test_server::HttpRequest& request) { 163 scoped_ptr<net::test_server::BasicHttpResponse> response( 164 new net::test_server::BasicHttpResponse()); 165 response->set_code(net::HTTP_OK); 166 return response.PassAs<net::test_server::HttpResponse>(); 167 } 168 169 } // namespace 170 171 class AdInjectionBrowserTest : public ExtensionBrowserTest { 172 protected: 173 AdInjectionBrowserTest(); 174 virtual ~AdInjectionBrowserTest(); 175 176 virtual void SetUpOnMainThread() OVERRIDE; 177 virtual void TearDownOnMainThread() OVERRIDE; 178 179 // Handle the "Reset Begin" stage of the test. 180 testing::AssertionResult HandleResetBeginStage(); 181 182 // Handle the "Reset End" stage of the test. 183 testing::AssertionResult HandleResetEndStage(); 184 185 // Handle the "Testing" stage of the test. 186 testing::AssertionResult HandleTestingStage(const std::string& message); 187 188 // Handle a JS error encountered in a test. 189 testing::AssertionResult HandleJSError(const std::string& message); 190 191 const base::FilePath& test_data_dir() { return test_data_dir_; } 192 193 ExtensionTestMessageListener* listener() { return listener_.get(); } 194 195 ActivityLogObserver* observer() { return observer_.get(); } 196 197 private: 198 // The name of the last completed test; used in case of unexpected failure for 199 // debugging. 200 std::string last_test_; 201 202 // A listener for any messages from our ad-injecting extension. 203 scoped_ptr<ExtensionTestMessageListener> listener_; 204 205 // An observer to be alerted when we detect ad injection. 206 scoped_ptr<ActivityLogObserver> observer_; 207 208 // The current stage of the test. 209 Stage stage_; 210 }; 211 212 AdInjectionBrowserTest::AdInjectionBrowserTest() : stage_(BEFORE_RESET) { 213 } 214 215 AdInjectionBrowserTest::~AdInjectionBrowserTest() { 216 } 217 218 void AdInjectionBrowserTest::SetUpOnMainThread() { 219 ExtensionBrowserTest::SetUpOnMainThread(); 220 221 ASSERT_TRUE(embedded_test_server()->InitializeAndWaitUntilReady()); 222 embedded_test_server()->RegisterRequestHandler(base::Bind(&HandleRequest)); 223 224 test_data_dir_ = 225 test_data_dir_.AppendASCII("activity_log").AppendASCII("ad_injection"); 226 observer_.reset(new ActivityLogObserver(profile())); 227 228 // We use a listener in order to keep the actions in the Javascript test 229 // synchronous. At the end of each stage, the test will send us a message 230 // with the stage and status, and will not advance until we reply with 231 // a message. 232 listener_.reset(new ExtensionTestMessageListener(true /* will reply */)); 233 234 // Enable the activity log for this test. 235 ActivityLog::GetInstance(profile())->SetWatchdogAppActiveForTesting(true); 236 237 // Set the ad network database. 238 AdNetworkDatabase::SetForTesting( 239 scoped_ptr<AdNetworkDatabase>(new TestAdNetworkDatabase)); 240 } 241 242 void AdInjectionBrowserTest::TearDownOnMainThread() { 243 observer_.reset(NULL); 244 listener_.reset(NULL); 245 ActivityLog::GetInstance(profile())->SetWatchdogAppActiveForTesting(false); 246 247 ExtensionBrowserTest::TearDownOnMainThread(); 248 } 249 250 testing::AssertionResult AdInjectionBrowserTest::HandleResetBeginStage() { 251 if (stage_ != BEFORE_RESET) { 252 return testing::AssertionFailure() 253 << "In incorrect stage. Last Test: " << last_test_; 254 } 255 256 // Stop looking for ad injection, since some of the reset could be considered 257 // ad injection. 258 observer()->disable(); 259 stage_ = RESETTING; 260 return testing::AssertionSuccess(); 261 } 262 263 testing::AssertionResult AdInjectionBrowserTest::HandleResetEndStage() { 264 if (stage_ != RESETTING) { 265 return testing::AssertionFailure() 266 << "In incorrect stage. Last test: " << last_test_; 267 } 268 269 // Look for ad injection again, now that the reset is over. 270 observer()->enable(); 271 stage_ = TESTING; 272 return testing::AssertionSuccess(); 273 } 274 275 testing::AssertionResult AdInjectionBrowserTest::HandleTestingStage( 276 const std::string& message) { 277 if (stage_ != TESTING) { 278 return testing::AssertionFailure() 279 << "In incorrect stage. Last test: " << last_test_; 280 } 281 282 // The format for a testing message is: 283 // "<test_name>:<expected_change>" 284 // where <test_name> is the name of the test and <expected_change> is 285 // either -1 for no ad injection (to test against false positives) or the 286 // number corresponding to ad_detection::InjectionType. 287 size_t sep = message.find(':'); 288 int expected_change = -1; 289 if (sep == std::string::npos || 290 !base::StringToInt(message.substr(sep + 1), &expected_change) || 291 (expected_change < Action::NO_AD_INJECTION || 292 expected_change >= Action::NUM_INJECTION_TYPES)) { 293 return testing::AssertionFailure() 294 << "Invalid message received for testing stage: " << message; 295 } 296 297 last_test_ = message.substr(0, sep); 298 299 Action::InjectionType expected_injection = 300 static_cast<Action::InjectionType>(expected_change); 301 std::string error; 302 if (observer()->found_multiple_injections()) { 303 error = "Found multiple injection types. " 304 "Only one injection is expected per test."; 305 } else if (expected_injection != observer()->injection_type()) { 306 // We need these static casts, because size_t is different on different 307 // architectures, and printf becomes unhappy. 308 error = base::StringPrintf( 309 "Incorrect Injection Found: Expected: %s, Actual: %s", 310 InjectionTypeToString(expected_injection).c_str(), 311 InjectionTypeToString(observer()->injection_type()).c_str()); 312 } 313 314 stage_ = BEFORE_RESET; 315 316 if (!error.empty()) { 317 return testing::AssertionFailure() 318 << "Error in Test '" << last_test_ << "': " << error; 319 } 320 321 return testing::AssertionSuccess(); 322 } 323 324 testing::AssertionResult AdInjectionBrowserTest::HandleJSError( 325 const std::string& message) { 326 // The format for a testing message is: 327 // "Testing Error:<test_name>:<error>" 328 // where <test_name> is the name of the test and <error> is the error which 329 // was encountered. 330 size_t first_sep = message.find(':'); 331 size_t second_sep = message.find(':', first_sep + 1); 332 if (first_sep == std::string::npos || second_sep == std::string::npos) { 333 return testing::AssertionFailure() 334 << "Invalid message received: " << message; 335 } 336 337 std::string test_name = 338 message.substr(first_sep + 1, second_sep - first_sep - 1); 339 std::string test_err = message.substr(second_sep + 1); 340 341 // We set the stage here, so that subsequent tests don't fail. 342 stage_ = BEFORE_RESET; 343 344 return testing::AssertionFailure() << "Javascript Error in test '" 345 << test_name << "': " << test_err; 346 } 347 348 // This is the primary Ad-Injection browser test. It loads an extension that 349 // has a content script that, in turn, injects ads left, right, and center. 350 // The content script waits after each injection for a response from this 351 // browsertest, in order to ensure synchronicity. After each injection, the 352 // content script cleans up after itself. For significantly more detailed 353 // comments, see 354 // chrome/test/data/extensions/activity_log/ad_injection/content_script.js. 355 IN_PROC_BROWSER_TEST_F(AdInjectionBrowserTest, DetectAdInjections) { 356 const Extension* extension = LoadExtension(test_data_dir_); 357 ASSERT_TRUE(extension); 358 359 ui_test_utils::NavigateToURL(browser(), embedded_test_server()->GetURL("/")); 360 361 std::string message; 362 while (message != "TestComplete") { 363 listener()->WaitUntilSatisfied(); 364 message = listener()->message(); 365 if (message == kResetBeginString) { 366 ASSERT_TRUE(HandleResetBeginStage()); 367 } else if (message == kResetEndString) { 368 ASSERT_TRUE(HandleResetEndStage()); 369 } else if (!message.compare( 370 0, strlen(kJavascriptErrorString), kJavascriptErrorString)) { 371 EXPECT_TRUE(HandleJSError(message)); 372 } else if (message == kTestCompleteString) { 373 break; // We're done! 374 } else { // We're in some kind of test. 375 EXPECT_TRUE(HandleTestingStage(message)); 376 } 377 378 // In all cases (except for "Test Complete", in which case we already 379 // break'ed), we reply with a continue message. 380 listener()->Reply("Continue"); 381 listener()->Reset(); 382 } 383 } 384 385 // TODO(rdevlin.cronin): We test a good amount of ways of injecting ads with 386 // the above test, but more is better in testing. 387 // See crbug.com/357204. 388 389 } // namespace extensions 390