// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "content/browser/webui/web_ui_main_frame_observer.h"

#include <memory>
#include <string>

#include "base/gtest_prod_util.h"
#include "base/memory/raw_ptr.h"
#include "base/metrics/field_trial_params.h"
#include "base/task/single_thread_task_runner.h"
#include "build/build_config.h"
#include "components/crash/content/browser/error_reporting/javascript_error_report.h"  // nogncheck
#include "components/crash/content/browser/error_reporting/js_error_report_processor.h"  // nogncheck
#include "content/browser/renderer_host/render_frame_host_impl.h"
#include "content/public/browser/site_instance.h"
#include "content/public/browser/web_ui_controller.h"
#include "content/public/test/browser_task_environment.h"
#include "content/public/test/navigation_simulator.h"
#include "content/public/test/test_browser_context.h"
#include "content/public/test/test_renderer_host.h"
#include "content/test/test_web_contents.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"

#if BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_CHROMEOS)

namespace content {

namespace {
using testing::Eq;
using testing::Optional;

class FakeJsErrorReportProcessor : public JsErrorReportProcessor {
 public:
  explicit FakeJsErrorReportProcessor(
      scoped_refptr<base::SingleThreadTaskRunner> task_runner)
      : task_runner_(std::move(task_runner)) {}

  // After calling this, expect all SendErrorReport's to use this
  // BrowserContext.
  void SetExpectedBrowserContext(BrowserContext* browser_context) {
    browser_context_ = browser_context;
  }

  void SendErrorReport(JavaScriptErrorReport error_report,
                       base::OnceClosure completion_callback,
                       BrowserContext* browser_context) override {
    CHECK_EQ(browser_context, browser_context_);
    last_error_report_ = std::move(error_report);
    ++error_report_count_;
    task_runner_->PostTask(FROM_HERE, std::move(completion_callback));
  }

  const JavaScriptErrorReport& last_error_report() const {
    return last_error_report_;
  }

  int error_report_count() const { return error_report_count_; }

  // Make public for testing.
  using JsErrorReportProcessor::SetDefault;

 protected:
  ~FakeJsErrorReportProcessor() override = default;

 private:
  scoped_refptr<base::SingleThreadTaskRunner> task_runner_;
  JavaScriptErrorReport last_error_report_;
  int error_report_count_ = 0;
  raw_ptr<BrowserContext> browser_context_ = nullptr;
};

class MockWebUIController : public WebUIController {
 public:
  explicit MockWebUIController(WebUI* web_ui) : WebUIController(web_ui) {}

  bool IsJavascriptErrorReportingEnabled() override {
    return enable_javascript_error_reporting_;
  }
  void enable_javascript_error_reporting(
      bool enable_javascript_error_reporting) {
    enable_javascript_error_reporting_ = enable_javascript_error_reporting;
  }

 private:
  bool enable_javascript_error_reporting_ = true;

  WEB_UI_CONTROLLER_TYPE_DECL();
};

WEB_UI_CONTROLLER_TYPE_IMPL(MockWebUIController)

}  // namespace

class WebUIMainFrameObserverTest : public RenderViewHostTestHarness {
 public:
  void SetUp() override {
    RenderViewHostTestHarness::SetUp();
    site_instance_ = SiteInstance::Create(browser_context());
    SetContents(TestWebContents::Create(browser_context(), site_instance_));
    // Since we just created the web_contents() pointer with
    // TestWebContents::Create, the static_casts are safe.
    web_ui_ = std::make_unique<WebUIImpl>(web_contents());
    web_ui_->SetRenderFrameHost(
        static_cast<TestWebContents*>(web_contents())->GetPrimaryMainFrame());
    web_ui_->SetController(
        std::make_unique<MockWebUIController>(web_ui_.get()));
    process()->Init();

    previous_processor_ = JsErrorReportProcessor::Get();
    processor_ = base::MakeRefCounted<FakeJsErrorReportProcessor>(
        task_environment()->GetMainThreadTaskRunner());
    FakeJsErrorReportProcessor::SetDefault(processor_);
    processor_->SetExpectedBrowserContext(browser_context());
  }

  void TearDown() override {
    FakeJsErrorReportProcessor::SetDefault(previous_processor_);

    // We have to destroy the site_instance_ before
    // RenderViewHostTestHarness::TearDown() destroys the
    // BrowserTaskEnvironment. We've gotten a lot of things that depend directly
    // or indirectly on BrowserTaskEnvironment, so just destroy everything we
    // can.
    previous_processor_.reset();
    processor_.reset();
    web_ui_.reset();
    site_instance_.reset();

    RenderViewHostTestHarness::TearDown();
  }

  // Simulate navigating to the WebUI page. Basically so that
  // WebUIMainFrameObserver::ReadyToCommitNavigation() gets called, since that
  // initializes the error handling.
  void NavigateToPage() {
    NavigationSimulator::NavigateAndCommitFromBrowser(web_contents(),
                                                      GURL(kPageURL8));
  }

  // Calls the observer's OnDidAddMessageToConsole with the given arguments.
  // This is just here so that we don't need to FRIEND_TEST_ALL_PREFIXES for
  // each and every test.
  void CallOnDidAddMessageToConsole(
      RenderFrameHost* source_frame,
      blink::mojom::ConsoleMessageLevel log_level,
      const std::u16string& message,
      int32_t line_no,
      const std::u16string& source_id,
      const std::optional<std::u16string>& stack_trace) {
    web_ui_->GetWebUIMainFrameObserverForTest()->OnDidAddMessageToConsole(
        source_frame, log_level, message, line_no, source_id, stack_trace);
  }

 protected:
  scoped_refptr<SiteInstance> site_instance_;
  std::unique_ptr<WebUIImpl> web_ui_;
  scoped_refptr<FakeJsErrorReportProcessor> processor_;
  scoped_refptr<JsErrorReportProcessor> previous_processor_;

  static constexpr char kMessage8[] = "An Error Is Me";
  static constexpr char16_t kMessage16[] = u"An Error Is Me";
  static constexpr char kSourceURL8[] = "chrome://here.is.error/bad.js";
  static constexpr char16_t kSourceURL16[] = u"chrome://here.is.error/bad.js";
  static constexpr char kPageURL8[] = "chrome://here.is.error/index.html";
  static constexpr char kStackTrace8[] =
      "at badFunction (chrome://page/my.js:20:30)\n"
      "at poorCaller (chrome://page/my.js:50:10)\n";
  static constexpr char16_t kStackTrace16[] =
      u"at badFunction (chrome://page/my.js:20:30)\n"
      u"at poorCaller (chrome://page/my.js:50:10)\n";
};

constexpr char WebUIMainFrameObserverTest::kMessage8[];
constexpr char16_t WebUIMainFrameObserverTest::kMessage16[];
constexpr char WebUIMainFrameObserverTest::kSourceURL8[];
constexpr char16_t WebUIMainFrameObserverTest::kSourceURL16[];
constexpr char WebUIMainFrameObserverTest::kPageURL8[];
constexpr char WebUIMainFrameObserverTest::kStackTrace8[];
constexpr char16_t WebUIMainFrameObserverTest::kStackTrace16[];

TEST_F(WebUIMainFrameObserverTest, ErrorReported) {
  NavigateToPage();
  CallOnDidAddMessageToConsole(web_ui_->GetRenderFrameHost(),
                               blink::mojom::ConsoleMessageLevel::kError,
                               kMessage16, 5, kSourceURL16, kStackTrace16);
  task_environment()->RunUntilIdle();
  EXPECT_EQ(processor_->error_report_count(), 1);
  EXPECT_EQ(processor_->last_error_report().message, kMessage8);
  EXPECT_EQ(processor_->last_error_report().url, kSourceURL8);
  EXPECT_EQ(processor_->last_error_report().page_url, kPageURL8);
  EXPECT_EQ(processor_->last_error_report().source_system,
            JavaScriptErrorReport::SourceSystem::kWebUIObserver);
  EXPECT_THAT(processor_->last_error_report().stack_trace,
              Optional(Eq(kStackTrace8)));
  // WebUI should use default product & version.
  EXPECT_EQ(processor_->last_error_report().product, "");
  EXPECT_EQ(processor_->last_error_report().version, "");
  EXPECT_EQ(*processor_->last_error_report().line_number, 5);
  EXPECT_FALSE(processor_->last_error_report().column_number);
  EXPECT_FALSE(processor_->last_error_report().app_locale);
  EXPECT_TRUE(processor_->last_error_report().send_to_production_servers);
}

TEST_F(WebUIMainFrameObserverTest, NoStackTrace) {
  NavigateToPage();
  CallOnDidAddMessageToConsole(web_ui_->GetRenderFrameHost(),
                               blink::mojom::ConsoleMessageLevel::kError,
                               kMessage16, 5, kSourceURL16, std::nullopt);
  task_environment()->RunUntilIdle();
  EXPECT_EQ(processor_->error_report_count(), 1);
  EXPECT_EQ(processor_->last_error_report().stack_trace, std::nullopt);
}

TEST_F(WebUIMainFrameObserverTest, NonErrorsIgnored) {
  NavigateToPage();
  CallOnDidAddMessageToConsole(web_ui_->GetRenderFrameHost(),
                               blink::mojom::ConsoleMessageLevel::kWarning,
                               kMessage16, 5, kSourceURL16, kStackTrace16);
  CallOnDidAddMessageToConsole(web_ui_->GetRenderFrameHost(),
                               blink::mojom::ConsoleMessageLevel::kInfo,
                               kMessage16, 5, kSourceURL16, kStackTrace16);
  CallOnDidAddMessageToConsole(web_ui_->GetRenderFrameHost(),
                               blink::mojom::ConsoleMessageLevel::kVerbose,
                               kMessage16, 5, kSourceURL16, kStackTrace16);
  task_environment()->RunUntilIdle();
  EXPECT_EQ(processor_->error_report_count(), 0);
}

TEST_F(WebUIMainFrameObserverTest, NoProcessorDoesntCrash) {
  NavigateToPage();
  FakeJsErrorReportProcessor::SetDefault(nullptr);
  CallOnDidAddMessageToConsole(web_ui_->GetRenderFrameHost(),
                               blink::mojom::ConsoleMessageLevel::kError,
                               kMessage16, 5, kSourceURL16, kStackTrace16);
  task_environment()->RunUntilIdle();
}

TEST_F(WebUIMainFrameObserverTest, NotSentIfInvalidURL) {
  NavigateToPage();
  CallOnDidAddMessageToConsole(web_ui_->GetRenderFrameHost(),
                               blink::mojom::ConsoleMessageLevel::kError,
                               kMessage16, 5, u"invalid URL", kStackTrace16);
  task_environment()->RunUntilIdle();
  EXPECT_EQ(processor_->error_report_count(), 0);
}

TEST_F(WebUIMainFrameObserverTest, NotSentIfDisabledForPage) {
  static_cast<MockWebUIController*>(web_ui_->GetController())
      ->enable_javascript_error_reporting(false);
  NavigateToPage();
  CallOnDidAddMessageToConsole(web_ui_->GetRenderFrameHost(),
                               blink::mojom::ConsoleMessageLevel::kError,
                               kMessage16, 5, kSourceURL16, kStackTrace16);
  task_environment()->RunUntilIdle();
  EXPECT_EQ(processor_->error_report_count(), 0);
}

TEST_F(WebUIMainFrameObserverTest, URLPathIsPreservedOtherPartsRemoved) {
  NavigateToPage();
  struct URLTest {
    const char16_t* const input;
    const char* const expected;
  };
  const URLTest kTests[] = {
      // No path still has no path.
      {u"chrome://version", "chrome://version/"},
      {u"chrome://version/", "chrome://version/"},
      // Path is kept.
      {u"chrome://discards/graph", "chrome://discards/graph"},
      {u"chrome://discards/graph/", "chrome://discards/graph/"},
      // Longer paths are kept.
      {u"chrome://discards/graph/a/b/c/d", "chrome://discards/graph/a/b/c/d"},
      // Queries are removed, with or without a path.
      {u"chrome://bookmarks/?q=chromium", "chrome://bookmarks/"},
      {u"chrome://bookmarks/add?q=chromium", "chrome://bookmarks/add"},
      {u"chrome://bookmarks/add/?q=chromium", "chrome://bookmarks/add/"},
      // Fragments are removed, with or without a path.
      {u"chrome://flags/#tab-groups", "chrome://flags/"},
      {u"chrome://flags/available/#tab-groups", "chrome://flags/available/"},
      // Queries & fragments are removed.
      {u"chrome://bookmarks/add?q=chromium#code", "chrome://bookmarks/add"},
      // User name and password are removed. (It's weird to have a user name or
      // password on a chrome URL, but otherwise we get blocked by the
      // no-non-chrome-URLs check)
      {u"chrome://chronos:test0000@version/Home", "chrome://version/Home"},
  };

  for (const URLTest& test : kTests) {
    int previous_count = processor_->error_report_count();
    CallOnDidAddMessageToConsole(web_ui_->GetRenderFrameHost(),
                                 blink::mojom::ConsoleMessageLevel::kError,
                                 kMessage16, 5, test.input, kStackTrace16);
    task_environment()->RunUntilIdle();
    EXPECT_EQ(processor_->error_report_count(), previous_count + 1)
        << "for " << test.input;
    EXPECT_EQ(processor_->last_error_report().url, test.expected)
        << "for " << test.input;
  }
}

TEST_F(WebUIMainFrameObserverTest, PageURLAlsoRedacted) {
  constexpr char kPageWithQueryAndFragment[] =
      "chrome://bookmarks/add?q=chromium#code";
  NavigationSimulator::NavigateAndCommitFromBrowser(
      web_contents(), GURL(kPageWithQueryAndFragment));
  CallOnDidAddMessageToConsole(web_ui_->GetRenderFrameHost(),
                               blink::mojom::ConsoleMessageLevel::kError,
                               kMessage16, 5, kSourceURL16, kStackTrace16);
  task_environment()->RunUntilIdle();
  EXPECT_EQ(processor_->error_report_count(), 1);
  EXPECT_EQ(processor_->last_error_report().page_url, "chrome://bookmarks/add");
}

TEST_F(WebUIMainFrameObserverTest, ErrorsNotReportedInOtherFrames) {
  NavigateToPage();
  auto another_contents =
      TestWebContents::Create(browser_context(), site_instance_);
  CHECK(another_contents->GetPrimaryMainFrame());
  CallOnDidAddMessageToConsole(another_contents->GetPrimaryMainFrame(),
                               blink::mojom::ConsoleMessageLevel::kError,
                               kMessage16, 5, kSourceURL16, kStackTrace16);
  task_environment()->RunUntilIdle();
  EXPECT_EQ(processor_->error_report_count(), 0);
}

TEST_F(WebUIMainFrameObserverTest, ErrorsNotReportedForNonChromeURLs) {
  NavigateToPage();
  const char16_t* const kNonChromeSourceURLs[] = {
      u"chrome-untrusted://media-app",
      u"chrome-error://chromewebdata/",
      u"chrome-extension://abc123/",
      u"about:blank",
  };

  for (const auto* url : kNonChromeSourceURLs) {
    CallOnDidAddMessageToConsole(web_ui_->GetRenderFrameHost(),
                                 blink::mojom::ConsoleMessageLevel::kError,
                                 kMessage16, 5, url, kStackTrace16);
    task_environment()->RunUntilIdle();
    EXPECT_EQ(processor_->error_report_count(), 0) << url;
  }
}

TEST_F(WebUIMainFrameObserverTest, SuppressedDuringTeardown) {
  NavigateToPage();

  // Set the lifecycle state to kRunningUnloadHandlers, which maps to
  // LifecycleState::kPendingDeletion.
  static_cast<RenderFrameHostImpl*>(web_ui_->GetRenderFrameHost())
      ->SetLifecycleState(
          RenderFrameHostImpl::LifecycleStateImpl::kRunningUnloadHandlers);

  CallOnDidAddMessageToConsole(web_ui_->GetRenderFrameHost(),
                               blink::mojom::ConsoleMessageLevel::kError,
                               kMessage16, 5, kSourceURL16, kStackTrace16);

  // The error should be suppressed because the frame is in kPendingDeletion.
  EXPECT_EQ(processor_->error_report_count(), 0);
}

}  // namespace content

#endif  // BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_CHROMEOS)
