1 // Copyright 2020 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 "content/browser/webui/web_ui_main_frame_observer.h"
6 
7 #include <memory>
8 #include <string>
9 
10 #include "base/metrics/field_trial_params.h"
11 #include "base/strings/string16.h"
12 #include "base/test/scoped_feature_list.h"
13 #include "build/build_config.h"
14 #include "components/crash/content/browser/error_reporting/javascript_error_report.h"
15 #include "components/crash/content/browser/error_reporting/js_error_report_processor.h"
16 #include "content/public/browser/site_instance.h"
17 #include "content/public/common/content_features.h"
18 #include "content/public/test/browser_task_environment.h"
19 #include "content/public/test/test_browser_context.h"
20 #include "content/public/test/test_renderer_host.h"
21 #include "content/test/test_web_contents.h"
22 #include "testing/gmock/include/gmock/gmock.h"
23 #include "testing/gtest/include/gtest/gtest.h"
24 
25 #if defined(OS_LINUX) || defined(OS_CHROMEOS)
26 
27 namespace content {
28 
29 namespace {
30 
31 class FakeJsErrorReportProcessor : public JsErrorReportProcessor {
32  public:
FakeJsErrorReportProcessor(scoped_refptr<base::SingleThreadTaskRunner> task_runner)33   explicit FakeJsErrorReportProcessor(
34       scoped_refptr<base::SingleThreadTaskRunner> task_runner)
35       : task_runner_(std::move(task_runner)) {}
36 
37   // After calling this, expect all SendErrorReport's to use this
38   // BrowserContext.
SetExpectedBrowserContext(BrowserContext * browser_context)39   void SetExpectedBrowserContext(BrowserContext* browser_context) {
40     browser_context_ = browser_context;
41   }
42 
SendErrorReport(JavaScriptErrorReport error_report,base::OnceClosure completion_callback,BrowserContext * browser_context)43   void SendErrorReport(JavaScriptErrorReport error_report,
44                        base::OnceClosure completion_callback,
45                        BrowserContext* browser_context) override {
46     CHECK_EQ(browser_context, browser_context_);
47     last_error_report_ = std::move(error_report);
48     ++error_report_count_;
49     task_runner_->PostTask(FROM_HERE, std::move(completion_callback));
50   }
51 
last_error_report() const52   const JavaScriptErrorReport& last_error_report() const {
53     return last_error_report_;
54   }
55 
error_report_count() const56   int error_report_count() const { return error_report_count_; }
57 
58   // Make public for testing.
59   using JsErrorReportProcessor::SetDefault;
60 
61  protected:
62   ~FakeJsErrorReportProcessor() override = default;
63 
64  private:
65   scoped_refptr<base::SingleThreadTaskRunner> task_runner_;
66   JavaScriptErrorReport last_error_report_;
67   int error_report_count_ = 0;
68   BrowserContext* browser_context_ = nullptr;
69 };
70 }  // namespace
71 
72 class WebUIMainFrameObserverTest : public RenderViewHostTestHarness {
73  public:
SetUp()74   void SetUp() override {
75     RenderViewHostTestHarness::SetUp();
76     scoped_feature_list_.InitAndEnableFeatureWithParameters(
77         features::kSendWebUIJavaScriptErrorReports,
78         {{features::kSendWebUIJavaScriptErrorReportsSendToProductionVariation,
79           "true"}});
80     site_instance_ = SiteInstance::Create(browser_context());
81     SetContents(TestWebContents::Create(browser_context(), site_instance_));
82     // Since we just created the web_contents() pointer with
83     // TestWebContents::Create, the static_cast is safe.
84     web_ui_ = std::make_unique<WebUIImpl>(
85         static_cast<TestWebContents*>(web_contents()), main_rfh());
86     observer_ =
87         std::make_unique<WebUIMainFrameObserver>(web_ui_.get(), web_contents());
88 
89     previous_processor_ = JsErrorReportProcessor::Get();
90     processor_ = base::MakeRefCounted<FakeJsErrorReportProcessor>(
91         task_environment()->GetMainThreadTaskRunner());
92     FakeJsErrorReportProcessor::SetDefault(processor_);
93     processor_->SetExpectedBrowserContext(browser_context());
94   }
95 
TearDown()96   void TearDown() override {
97     FakeJsErrorReportProcessor::SetDefault(previous_processor_);
98 
99     // We have to destroy the site_instance_ before
100     // RenderViewHostTestHarness::TearDown() destroys the
101     // BrowserTaskEnvironment. We gotten a lot of things that depend directly
102     // or indirectly on BrowserTaskEnvironment, so just destroy everything we
103     // can.
104     previous_processor_.reset();
105     processor_.reset();
106     observer_.reset();
107     web_ui_.reset();
108     site_instance_.reset();
109 
110     RenderViewHostTestHarness::TearDown();
111   }
112 
113   // Calls observer_->OnDidAddMessageToConsole with the given arguments. This
114   // is just here so that we don't need to FRIEND_TEST_ALL_PREFIXES for each
115   // and every test.
CallOnDidAddMessageToConsole(RenderFrameHost * source_frame,blink::mojom::ConsoleMessageLevel log_level,const base::string16 & message,int32_t line_no,const base::string16 & source_id)116   void CallOnDidAddMessageToConsole(RenderFrameHost* source_frame,
117                                     blink::mojom::ConsoleMessageLevel log_level,
118                                     const base::string16& message,
119                                     int32_t line_no,
120                                     const base::string16& source_id) {
121     observer_->OnDidAddMessageToConsole(source_frame, log_level, message,
122                                         line_no, source_id);
123   }
124 
125  protected:
126   base::test::ScopedFeatureList scoped_feature_list_;
127   scoped_refptr<SiteInstance> site_instance_;
128   std::unique_ptr<WebUIImpl> web_ui_;
129   std::unique_ptr<WebUIMainFrameObserver> observer_;
130   scoped_refptr<FakeJsErrorReportProcessor> processor_;
131   scoped_refptr<JsErrorReportProcessor> previous_processor_;
132 
133   static constexpr char kMessage8[] = "An Error Is Me";
134   const base::string16 kMessage16 = base::UTF8ToUTF16(kMessage8);
135   static constexpr char kSourceURL8[] = "chrome://here.is.error/";
136   const base::string16 kSourceId16 = base::UTF8ToUTF16(kSourceURL8);
137 };
138 
139 constexpr char WebUIMainFrameObserverTest::kMessage8[];
140 constexpr char WebUIMainFrameObserverTest::kSourceURL8[];
141 
TEST_F(WebUIMainFrameObserverTest,ErrorReported)142 TEST_F(WebUIMainFrameObserverTest, ErrorReported) {
143   CallOnDidAddMessageToConsole(web_ui_->frame_host_for_test(),
144                                blink::mojom::ConsoleMessageLevel::kError,
145                                kMessage16, 5, kSourceId16);
146   task_environment()->RunUntilIdle();
147   EXPECT_EQ(processor_->error_report_count(), 1);
148   EXPECT_EQ(processor_->last_error_report().message, kMessage8);
149   EXPECT_EQ(processor_->last_error_report().url, kSourceURL8);
150   // WebUI should use default product & version.
151   EXPECT_EQ(processor_->last_error_report().product, "");
152   EXPECT_EQ(processor_->last_error_report().version, "");
153   EXPECT_EQ(*processor_->last_error_report().line_number, 5);
154   EXPECT_FALSE(processor_->last_error_report().column_number);
155   EXPECT_FALSE(processor_->last_error_report().stack_trace);
156   EXPECT_FALSE(processor_->last_error_report().app_locale);
157   EXPECT_TRUE(processor_->last_error_report().send_to_production_servers);
158 }
159 
TEST_F(WebUIMainFrameObserverTest,NonErrorsIgnored)160 TEST_F(WebUIMainFrameObserverTest, NonErrorsIgnored) {
161   CallOnDidAddMessageToConsole(web_ui_->frame_host_for_test(),
162                                blink::mojom::ConsoleMessageLevel::kWarning,
163                                kMessage16, 5, kSourceId16);
164   CallOnDidAddMessageToConsole(web_ui_->frame_host_for_test(),
165                                blink::mojom::ConsoleMessageLevel::kInfo,
166                                kMessage16, 5, kSourceId16);
167   CallOnDidAddMessageToConsole(web_ui_->frame_host_for_test(),
168                                blink::mojom::ConsoleMessageLevel::kVerbose,
169                                kMessage16, 5, kSourceId16);
170   task_environment()->RunUntilIdle();
171   EXPECT_EQ(processor_->error_report_count(), 0);
172 }
173 
TEST_F(WebUIMainFrameObserverTest,NoProcessorDoesntCrash)174 TEST_F(WebUIMainFrameObserverTest, NoProcessorDoesntCrash) {
175   FakeJsErrorReportProcessor::SetDefault(nullptr);
176   CallOnDidAddMessageToConsole(web_ui_->frame_host_for_test(),
177                                blink::mojom::ConsoleMessageLevel::kError,
178                                kMessage16, 5, kSourceId16);
179   task_environment()->RunUntilIdle();
180 }
181 
TEST_F(WebUIMainFrameObserverTest,NotSentIfFlagDisabled)182 TEST_F(WebUIMainFrameObserverTest, NotSentIfFlagDisabled) {
183   scoped_feature_list_.Reset();
184   scoped_feature_list_.InitAndDisableFeature(
185       features::kSendWebUIJavaScriptErrorReports);
186   CallOnDidAddMessageToConsole(web_ui_->frame_host_for_test(),
187                                blink::mojom::ConsoleMessageLevel::kError,
188                                kMessage16, 5, kSourceId16);
189   task_environment()->RunUntilIdle();
190   EXPECT_EQ(processor_->error_report_count(), 0);
191 }
192 
TEST_F(WebUIMainFrameObserverTest,NotSentIfInvalidURL)193 TEST_F(WebUIMainFrameObserverTest, NotSentIfInvalidURL) {
194   CallOnDidAddMessageToConsole(web_ui_->frame_host_for_test(),
195                                blink::mojom::ConsoleMessageLevel::kError,
196                                kMessage16, 5, base::UTF8ToUTF16("invalid URL"));
197   task_environment()->RunUntilIdle();
198   EXPECT_EQ(processor_->error_report_count(), 0);
199 }
200 
TEST_F(WebUIMainFrameObserverTest,URLPathIsPreservedOtherPartsRemoved)201 TEST_F(WebUIMainFrameObserverTest, URLPathIsPreservedOtherPartsRemoved) {
202   struct URLTest {
203     const char* const input;
204     const char* const expected;
205   };
206   const URLTest kTests[] = {
207       // No path still has no path.
208       {"chrome://version", "chrome://version/"},
209       {"chrome://version/", "chrome://version/"},
210       // Path is kept.
211       {"chrome://discards/graph", "chrome://discards/graph"},
212       {"chrome://discards/graph/", "chrome://discards/graph/"},
213       // Longer paths are kept.
214       {"chrome://discards/graph/a/b/c/d", "chrome://discards/graph/a/b/c/d"},
215       // Queries are removed, with or without a path.
216       {"chrome://bookmarks/?q=chromium", "chrome://bookmarks/"},
217       {"chrome://bookmarks/add?q=chromium", "chrome://bookmarks/add"},
218       {"chrome://bookmarks/add/?q=chromium", "chrome://bookmarks/add/"},
219       // Fragments are removed, with or without a path.
220       {"chrome://flags/#tab-groups", "chrome://flags/"},
221       {"chrome://flags/available/#tab-groups", "chrome://flags/available/"},
222       // Queries & fragments are removed.
223       {"chrome://bookmarks/add?q=chromium#code", "chrome://bookmarks/add"},
224       // User name and password are removed.
225       {"https://chronos:test0000@www.chromium.org/Home",
226        "https://www.chromium.org/Home"},
227       // Port is not removed.
228       {"http://127.0.0.1:8088/static/legend_help.html",
229        "http://127.0.0.1:8088/static/legend_help.html"},
230       {"http://127.0.0.1:8088/#active", "http://127.0.0.1:8088/"},
231       // about: pages also have their queries & fragments removed. (They really
232       // shouldn't have their "about:" removed either, but that's hard to
233       // prevent.)
234       {"about:blank/?q=foo", "blank/"},
235       {"about:blank/#foo", "blank/"},
236   };
237 
238   for (const URLTest& test : kTests) {
239     int previous_count = processor_->error_report_count();
240     CallOnDidAddMessageToConsole(web_ui_->frame_host_for_test(),
241                                  blink::mojom::ConsoleMessageLevel::kError,
242                                  kMessage16, 5, base::UTF8ToUTF16(test.input));
243     task_environment()->RunUntilIdle();
244     EXPECT_EQ(processor_->error_report_count(), previous_count + 1)
245         << "for " << test.input;
246     EXPECT_EQ(processor_->last_error_report().url, test.expected)
247         << "for " << test.input;
248   }
249 }
250 
251 }  // namespace content
252 
253 #endif  // defined(OS_LINUX) || defined(OS_CHROMEOS)
254