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