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/test/base/ui_test_utils.h"
15#include "extensions/common/extension.h"
16#include "extensions/test/extension_test_message_listener.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
21namespace net {
22namespace test_server {
23struct HttpRequest;
24}
25}
26
27namespace extensions {
28
29namespace {
30
31// The "ad network" that we are using. Any src or href equal to this should be
32// considered an ad network.
33const char kAdNetwork1[] = "http://www.known-ads.adnetwork";
34const char kAdNetwork2[] = "http://www.also-known-ads.adnetwork";
35
36// The current stage of the test.
37enum 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.
44const char kResetBeginString[] = "Page Reset Begin";
45// The string sent by the test to indicate that page reset is complete.
46const char kResetEndString[] = "Page Reset End";
47// The string sent by the test to indicate a JS error was caught in the test.
48const char kJavascriptErrorString[] = "Testing Error";
49// The string sent by the test to indicate that we have concluded the full test.
50const char kTestCompleteString[] = "Test Complete";
51
52std::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).
76class 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
113ActivityLogObserver::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
122ActivityLogObserver::~ActivityLogObserver() {}
123
124void 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.
139class 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
151TestAdNetworkDatabase::TestAdNetworkDatabase() : ad_network_url1_(kAdNetwork1),
152                                                 ad_network_url2_(kAdNetwork2) {
153}
154
155TestAdNetworkDatabase::~TestAdNetworkDatabase() {}
156
157bool TestAdNetworkDatabase::IsAdNetwork(const GURL& url) const {
158  return url == ad_network_url1_ || url == ad_network_url2_;
159}
160
161scoped_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
171class 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
212AdInjectionBrowserTest::AdInjectionBrowserTest() : stage_(BEFORE_RESET) {
213}
214
215AdInjectionBrowserTest::~AdInjectionBrowserTest() {
216}
217
218void 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
242void AdInjectionBrowserTest::TearDownOnMainThread() {
243  observer_.reset(NULL);
244  listener_.reset(NULL);
245  ActivityLog::GetInstance(profile())->SetWatchdogAppActiveForTesting(false);
246
247  ExtensionBrowserTest::TearDownOnMainThread();
248}
249
250testing::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
263testing::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
275testing::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
324testing::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.
355IN_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